ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零搭建AI工程:环境配置到模型部署的全链路实践

从零搭建AI工程:环境配置到模型部署的全链路实践 1. 聊聊这个项目到底在做什么如果你在技术社区待得够久会发现一个挺有意思的现象AI相关的内容两极分化特别严重。一头是铺天盖地的框架教程教你三分钟调用某个现成接口跑通一个demo另一头是顶会论文的数学推导恨不得从矩阵求导开始讲起。真正卡在中间的AI工程落地这一层反而很少有人系统讲清楚。我做的这个ai-engineering-from-scratch项目说白了就是一次自我和解不追新框架、不套现成模板、不碰拖拽式平台纯手工从零把一条完整的AI应用链路搭起来。从环境配置、数据准备、模型训练、性能调优到服务部署每一个环节都自己动手过一遍。整个项目做下来最大的感受是网上那些看似高效的一键训练、一行代码部署恰恰是最容易让你丧失工程判断力的东西。模型不收敛、显存溢出、推理延迟高得离谱出了问题你真不知道去哪一层排查。这个项目适合谁我觉得有三类人可以重点参考一是刚入行、对AI工程还没有全局概念的算法工程师你可以把它当作一条完整的地图二是被各种自动化平台惯坏了、想补一补底层功底的开发者三是准备转行做AI工程、想系统梳理知识体系的同学。后面我会完整记录我踩过的坑、做过的选型对比和最终沉淀下来的工程方案所有折腾过程都是实打实的。2. 项目整体设计思路拆解2.1 from scratch到底是从哪一步算起很多人一说从零开始搞AI第一反应是那我得先手写一个神经网络的前向反向传播。说实话这属于数学课的内容不完全是工程。工程意义上的from scratch指的是不依赖任何一站式的AI开发平台也不直接套用某个完整开源项目的全部代码而是从基础环境开始逐步搭建。包括自己管理Python环境、自己处理原始数据、自己设计训练脚本、自己写部署代码。我在项目里定的起点是一台只有GPU驱动、其他啥都没有的机器所有东西都手动装。很多刚接触AI工程的同学会忽略一个事实框架装多了一样是灾难。我见过太多人机器上cuda、cudnn、pytorch、tensorflow版本互相打架最后只能全部卸载重来。所以在项目第一步我就把环境管理这个问题认真对待了后面会细说。2.2 为什么不做拿来主义项目里所有的代码、配置、脚本都是我自己搭建的这个决定一开始就做好了。原因特别简单做项目的目的不是交付一个能跑的东西而是建立从问题到方案的映射能力。你只有亲手把数据清洗、模型训练、接口封装每个环节做过一遍下次看到别人的方案才知道好在哪里、坑在哪里、取舍在哪里。举个例子网上一堆开源的推荐系统项目你clone下来是能跑的。但如果面试官问你的embedding维度为什么设成32、负样本采样为什么用这种策略你没有从头搭建的底子大概率只能支支吾吾说都是默认参数。但我做完这个项目后这类问题基本都能拆解清楚因为每一步我都做过实验、折腾过参数、记录过对比结果。这就是从零搭建不可替代的价值。2.3 技术栈选型不追新只求稳这个项目做技术选型的时候我坚持一个原则选那些生态成熟、资料多、踩坑成本低的方案。深度学习框架选了PyTorch不选TensorFlow不是因为TensorFlow不好而是PyTorch的调试体验对工程新手更友好报错信息可读性强想着反正要与时俱进那就PyTorch的同学其实忽略了一点模型调试时能不能快速定位问题比框架本身先进不先进更重要。数据方面用的Pandas加Polars混合图像预处理用的Albumentations训练过程追踪用的WandB部署走的FastAPI加ONNX Runtime。这套组合的好处是每一层都有清晰边界出了问题能快速定位是数据层、训练层还是服务层。过一阵子我甚至打算试一下Ray来跑分布式调度但目前这套已经把复杂度控制得很均衡。3. 环境与依赖管理动手搭一个干净可复现的基础3.1 Python环境隔离的正确姿势我见过太多人装深度学习依赖的时候直接往系统环境里怼然后用了一阵子后pip list一长串、完全不知道谁依赖谁、一升级就崩。这里强烈推荐用虚拟环境加依赖清单双保险的方案。第一步是装Miniconda而不是Anaconda原因很简单Anaconda自带的包太多太杂多数你用不上还容易跟系统的Python冲突。Miniconda足够干净按需装即可。装完之后创建一个独立的conda环境conda create -n ai-engineering python3.10 -y conda activate ai-engineering之后所有依赖都装在这个环境里系统Python永远保持干净状态。这一步看似简单却是我见过无数人忽略导致环境地狱的根源。3.2 GPU相关依赖的精准安装GPU环境的配置是重灾区中的重灾区。很多人装CUDA的时候想都不想直接装最新的结果模型起不来才发现你用的PyTorch版本可能根本不支持最新的CUDA版本。我踩过的坑是装了CUDA 12.4结果PyTorch稳定版只适配到12.1折腾半天只能重装。我的建议是先确定你打算用的PyTorch版本然后反推CUDA版本。比如项目里我计划用PyTorch 2.3.0这个版本对应的稳定CUDA是11.8和12.1。考虑到我显卡是RTX 4090选12.1更合适。安装命令用PyTorch官方渠道pip install torch2.3.0 torchvision0.18.0 --index-url https://download.pytorch.org/whl/cu121装完之后千万别急着往下走先验证一下GPU是否真的可用import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))这行验证代码能救你很多次。我身边同事就遇到过明明装完显示成功但一跑就是CPU模式的情况就是没做这个验证。3.3 可复现的依赖锁全环境装好只是第一步真正让人抓狂的是明明代码一样换个机器跑结果对不上。为了彻底解决这个问题建议引入依赖锁定文件。我在项目里用pip-tools来管理pip install pip-tools pip-compile requirements.in -o requirements.txt pip-sync requirements.txtrequirements.in里只写顶层依赖例如pytorch、pandas、fastapi这些pip-compile会自动帮你解析出所有传递依赖并锁定精确版本。换新机器时执行pip-sync就能完整复现项目环境。这套流程做完我这边能跑啊这句程序员名场面基本能杜绝。4. 数据工程AI项目里最费时间、最见功力的部分4.1 数据获取与清洗的实战笔记这个项目的场景是图片分类任务数据来自公开数据集。下载下来后我没有直接灌进模型而是先做了一轮基础检查。这一步是很多教程跳过的但现实项目里数据脏的程度绝对超出预期。我遇到的第一类问题是图片损坏。有的图片下载不完整有的虽然扩展名是jpg实际是png格式编码有的甚至直接是0字节的文件。对这类问题写了一个专门的清洗脚本from PIL import Image import os invalid_files [] for root, dirs, files in os.walk(raw_data_dir): for f in files: path os.path.join(root, f) try: img Image.open(path) img.verify() except Exception: invalid_files.append(path) os.remove(path)这类脚本的价值不在于代码有多复杂而在于你能在训练前发现数据真的有问题这件事本身。很多同学直接跳过这一步训练到一半报错或者效果奇差回头查才发现是数据导致的。第二类问题是类别分布极度不均。我做的这个场景里有个类别样本量是另一类别的30倍。如果直接训练模型基本会懒惰地把所有样本预测为多数类因为这样损失最低。应对方案有两个思路一是对少数类做过采样比如图像数据随机旋转、裁剪、色彩抖动增强二是给损失函数加类别权重。我最终两个都做了效果验证在后面的实验部分会讲到。4.2 数据版本管理DVC的落地实践数据管理这块大部分入门教程完全不会提但到了协作和复现环节就会痛。这次项目从第一天开始就引入了DVC把数据集、中间产物都纳入版本管理。DVC的使用姿势很简单数据目录先通过dvc add托管然后push到远程存储我用的S3兼容存储再用dvc repro管理数据流水线的执行过程。这样别人clone仓库代码后执行dvc pull就能还原当时的数据版本。我用到的最核心命令是dvc init dvc add data/raw dvc remote add storage s3://my-bucket/dvc-store dvc push这带来的直接好处是回滚版本时数据和代码能保持同步。跟G端项目配合尤其顺团队伙伴只要拉最新的commit和对应dvc数据即可。如果你的项目还没用上DVC在做任何AI工程前真建议先装上时间会加倍还给你。4.3 数据集划分不要天真地随机切按8:2切训练集和测试集是所有教程里的标准操作但在真实做分类任务时我建议你一定要想一下需不需要分层采样。如果原始数据本身就类别不均衡随机切大概率会导致某个类别在测试集里样本过少甚至没有。我的做法是先对数据按类别做分组再每组内按比例切。直接调sklearn的StratifiedShuffleSplit一行就能完成from sklearn.model_selection import StratifiedShuffleSplit split StratifiedShuffleSplit(n_splits1, test_size0.2, random_state42) for train_idx, val_idx in split.split(data, labels): train_data data.iloc[train_idx] val_data data.iloc[val_idx]随机种子固定为42这样每次跑出来的划分都一致。这看起来都是小操作但就决定了一个模型评估结果的可靠度。测试集划分不严谨后面所有结论都可能是错的。5. 模型训练工程化从单卡调试到实验管理5.1 训练脚本的正确写法写训练脚本这件事听起来很简单但大多数人写出来的是只可意会不可言传的脚本全局变量满天飞、模型结构改一下要动五个地方、超参改完自己都忘了上次用的什么值。我这次从一开始就定了脚本结构规范核心是三个文件分工config.yaml集中保存所有超参数学习率、batch size、epoch数、模型结构参数等dataset.py独立的数据加载与预处理模块train.py主训练流程训练启动时只读config.yaml里的配置不硬编码任何参数import yaml with open(config.yaml, r) as f: cfg yaml.safe_load(f) learning_rate cfg[training][learning_rate] batch_size cfg[training][batch_size] num_epochs cfg[training][num_epochs]这样改超参只需编辑yaml文件也方便把不同实验的配置文件留存对比。我在项目里就把每次实验的config全部归档到experiments目录下后续要复现哪个结果直接找到当时的config即可。5.2 训练过程的实时监控与日志管理传统print式日志已经跟不上调试需求了。这次我在项目中用WandB做实验追踪每步训练都记录loss值每个epoch自动记录验证集准确率。关键指标通过仪表盘实时查看多组实验还可以在同一个面板上对比import wandb wandb.init(projectai-engineering-from-scratch, configcfg) ... wandb.log({ train_loss: loss, train_acc: train_acc, val_acc: val_acc, learning_rate: optimizer.param_groups[0][lr], })一个常被忽略的小技巧是除了记录loss和acc把学习率变化也记进去。你才能看出是不是学习率策略导致loss震荡。除此之外日志文件也别省我在项目里用logging模块同时输出到控制台和文件WandB挂了也不影响排查。5.3 超参数调优从手动到半自动模型不收敛第一个想到的是调学习率模型欠拟合第一个想到的是加大模型容量。但真实的超参数空间是多维的你手动一个一个试效率极其低下。这次项目里做了一部分Optuna自动搜索import optuna def objective(trial): lr trial.suggest_float(lr, 1e-5, 1e-3, logTrue) batch_size trial.suggest_categorical(batch_size, [32, 64, 128]) weight_decay trial.suggest_float(weight_decay, 1e-6, 1e-3, logTrue) config { training: { learning_rate: lr, batch_size: batch_size, weight_decay: weight_decay, } } return train_and_evaluate(config) study optuna.create_study(directionmaximize) study.optimize(objective, n_trials50)这里最大的心得是先粗搜确定量级再精搜微调。比如学习率最优值在1e-4附近直接告诉Optuna在1e-5到1e-3的log空间搜比暴力网格搜索效率高得多。此外每次跑完实验记录下来最优超参组合对应的验证集表现避免被随机误差主导做出错误判断。建议至少用三个随机种子跑取均值对比类似这个情况在真正调参时经常出现。5.4 单机多卡训练的正确打开方式项目中期我已经不满足于单卡训练了开始折腾分布式训练。PyTorch的DistributedDataParallel聊起来很复杂但其实工程实现起来没有想象中那么困难。核心步骤是先初始化进程组然后把模型包一层DDPimport torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP dist.init_process_group(backendnccl, init_methodenv://) model model.to(local_rank) model DDP(model, device_ids[local_rank])这里有一个新手最容易犯的错忘了给每个进程设置不同的种子结果所有卡加载的数据一模一样训练起来跟单卡没区别。正确做法是在每个进程里用rank偏移种子seed 42 dist.get_rank() torch.manual_seed(seed)另外需要注意的是DataLoader里必须设置sampler为DistributedSampler否则每张卡都会取到全量数据等于白做分布式。调通之后我实测训练速度提升大约是单卡的三倍多虽然不是线性四倍但省下的等待时间是完全值得投入去折腾的。6. 模型优化与性能提升实战6.1 从能跑到跑得快的量化之旅模型训练完并不算结束真正上线前还有一道坎推理性能。这个项目在部署阶段遇到了一个经典问题模型精度已经很好了但单张图片推理需要120毫秒跟不上业务的实时性要求。先分析瓶颈模型结构本身不算大主要耗时在浮点运算量上。我做的第一步优化是把PyTorch模型导出为ONNX格式然后用ONNX Runtime做推理引擎import torch.onnx dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, )这里有两点值得注意一个是export的时候把动态batch size打开否则固定成1服务端批量推理寸步难行另一个是需要拆掉embedding层直接用onnx导出完整模型结构否则会导出失败或推理结果对不上。导出之后推理速度降到大约85毫秒但离目标还是差了一段。于是继续上量化把模型从FP32降到INT8精度。ONNX Runtime提供了现成接口用一些校准数据算好量化参数from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model.onnx, model_quantized.onnx, weight_typeQuantType.QInt8, )动态量化实测推理速度又降到了35毫秒左右精度只掉了0.5个点不到。这一步的收益极其显著但要注意不是所有层都适合量化尤其是模型里如果大量存在LayerNorm这类操作量化后的数值分布会受到明显影响。如果你的模型量化后精度掉得离谱试着只量化Conv和Linear层把敏感层留给FP16。6.2 推理服务化FastAPI封装细节把模型变成能对外服务的接口我选了FastAPI。它自带OpenAPI文档、性能也不错最关键是写起来特别简洁一行一个路由。核心代码如下from fastapi import FastAPI, UploadFile, File import io import numpy as np from PIL import Image app FastAPI() model load_model(model_quantized.onnx) app.post(/predict) async def predict(file: UploadFile File(...)): image_bytes await file.read() image Image.open(io.BytesIO(image_bytes)).convert(RGB) # 预处理与模型推理 ... return {class_id: int(pred_id), confidence: float(conf)}这里有几个生产环境的细节值得提一下。第一个是线程池与GIL的问题。如果你直接在FastAPI的async函数里跑同步的ONNX推理多个请求进来时会互相阻塞。我踩了这个坑后来把推理逻辑放到线程池里执行。第二个是输入校验。真实业务传进来的图片千奇百怪有的超大、有的损坏、有的带了奇怪的通道数。上线前务必加上异常拦截不要让一张坏图片把整个worker拖崩。我在项目里加了图片大小上限、格式检查、解码异常兜底这些问题都说不上难但在线上才是真正的救命稻草。第三个是服务优雅退出。用uvicorn启动项目时我建议加上--timeout-keep-alive参数防止慢请求一直被挂起把worker拖到超时被杀。7. 常见问题与排查技巧实录7.1 显存溢出的六个排查方向显存溢出绝对是AI工程师遇到频率最高的报错没有之一。遇到了不要慌按下面的顺序排查。第一看batch size是不是设太大。最简单直接减小batch size到原来的一半试试。但有些人马上推出那我把batch size开小就行了这其实是治标不治本要算清楚你模型的实际显存需求。第二看输入尺寸。有的数据集图片尺寸巨大模型结构里全连接层参数量跟输入尺寸强相关输入越大显存占用越恐怖。如果图片是1024x1024试着先缩到512x512显存占用会大幅下降。第三看梯度是否在反向传播时集中在某层。输出中间层变量的引用会导致该部分的显存无法释放。这种需要检查代码里是不是保留了不该保留的tensor。第四看混合精度。用AMP自动混合精度Automatic Mixed Precision把部分计算从FP32降到FP16显存能直接省一半。这在RTX 30系之后的显卡上尤其好用代价几乎可以忽略。第五看PyTorch的显存缓存机制。有时候你看到占用了90%显存但其实并不是峰值PyTorch会自动缓存一些已释放的显存块以便下次复用。这时加上环境变量PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128有时会有奇效。第六看是否多个进程重复加载了模型。DDP训练时每张卡都有一个完整的模型副本这是正常的但如果你在同一张卡上启动了多个训练进程显存就是在成倍开销。7.2 训练时loss不下降的三个排查角度这个问题的可能性太多了没有万能钥匙但是有个高效排查路径。第一个角度是数据问题。label是不是错位的数据增强是不是太猛把图片搞到失真这些都能让loss卡住。我的习惯是训练前先跑一小批数据过一下模型前向看输出shape对不对再算loss反向确认全链路没有bug。第二个角度是优化器和学习率。学习率设置过高会让loss在初期不降反升设置过低则是原地踏步。实用的调试法是先用一小段数据做LR扫描找一个初始量级。我在项目里跑过一次发现用3e-4比用1e-3收敛快不少这就省去了很多盲目尝试。第三个角度是归一化层。如果你的网络里用了BatchNormbatch size太小会导致batch统计量不稳定影响收敛。解决方案是换成LayerNorm或者GroupNorm。在图像任务里如果batch size只能开到8GroupNorm往往是个稳妥选择。7.3 模型训练很快但测试精度差过拟合的天敌过拟合问题高频出现典型的症状是训练集acc接近99%验证集只有70%多。我的排查清单大概是这么五条模型容量过大。参数量太多、训练数据太少模型就把训练集的噪声一并记下来了。缺少数据增强。图片分类任务里随机裁剪、翻转、色彩抖动就是免费的午餐能大幅提升泛化能力。正则化强度不足。给权重加L2正则dropout比例调大这些手段可以有效抑制过拟合。早停策略没做。训练到一定epoch后验证集loss开始回升就应该停掉重出checkpoint选验证集最优的那个时期。样本分布不均。少数类过拟合严重用类别加权损失或专门的采样器来平衡。这些手段在项目里我基本都用过一遍最终组合是增强加早停加正则把验证集精度从72%拉到了89%效果非常明显。8. 这个项目的实际收获与扩展方向先说说我在整个过程中对AI工程这件事认知的变化。没做这个项目之前我以为AI工程的核心难点在模型结构设计、在数学推导真正做完之后发现工程落地最大的成本其实是在数据质量、环境管理、实验规范、部署细节这些看不见的地方。模型结构你可以抄数学公式可以推但这些工程素养必须自己一步一个脚印积累。从技术角度看这次项目的经验完全可以迁移到其他场景。环境管理那一套逻辑放到NLP、语音、强化学习项目都通用数据处理和版本管理的思路放到推荐系统、风控模型也成立部署优化的流程不管是对接云端还是边缘设备都是同一套方法论。这就是from scratch带来的最大红利你掌握的是底层规律而不是某个工具的熟练度。如果你也想做类似的从零项目我的建议是别贪大选一个你感兴趣的领域图像、文本、音频都行定一个足够小但完整的场景比如从零实现一个垃圾分类模型、从零搭一条文本情感分析服务然后所有环节都自己动手。跑通之后你会发现再次面对一个新的AI业务需求时心里完全不慌了因为你知道从数据到模型到服务每一步的坑和路都长什么样子。这种心里有底的状态比任何证书和课程都值钱。
返回列表