
从零开始搞“AI工程”这件事我踩过的坑你们就别再踩了。很多人一听到AI就想到高深的数学公式、昂贵的GPU集群、顶会论文但真正从零起步时最难的往往不是算法本身而是如何把一堆零散的概念、工具、流程串成一个能稳定产出的工程体系。这个“ai-engineering-from-scratch”的项目说白了就是一条从头搭建AI能力的完整路径——它不是某一个模型教程而是一套“从无到有”的方法论怎么选方向、怎么搭环境、怎么用数据、怎么训练、怎么部署、怎么排查问题每一步都有具体的做法和背后的道理。这篇内容特别适合两类人一类是刚入门但被各种术语绕晕的初学者另一类是从业务或软件开发转过来、想系统掌握AI落地能力的工程师。我会沿着一条真实可行的路径把工程化思维和实操细节揉在一起讲既讲清楚“怎么做”也解释“为什么这么做”。尤其是后面那些常见的坑和排查思路都是我一个个试出来的网上很难搜得这么全。1. 内容整体设计与思路拆解1.1 “AI工程”到底和“调模型”有什么区别很多人以为AI工程就是写个神经网络、训一下、看下准确率就完了。但真正的工程化是让一个AI系统能够可靠地解决实际问题并且在持续迭代中保持稳定。这里面的核心差异在于“系统性”三个字。单纯“调模型”像是做化学实验每次在笔记本里跑一下结果好就好不好就换参数。AI工程则更像盖房子要考虑地基数据、墙体模型、水电训练环境、装修部署上线以及后期的维护监控与更新。从这个角度说从零开始走AI工程第一步不是学TensorFlow而是建立“全流程视角”。拿我自己经历来说早先接手一个自动识别商品图片的项目第一周就拿着预训练模型跑了一版准确率看起来不错但到了上线前才发现数据分布和真实场景不一致、推理速度跟不上业务并发、模型更新没法自动化。那一版基本作废。后来重新把所有环节梳理成“数据-训练-评估-部署-监控”才发现哪怕模型精度稍低一点工程链路跑通了迭代速度反而更快、实际效果也更强。所以说标题里那个“from scratch”不是为了浪漫而是为了把整条链路上的每个细节都自己在手里过一遍做到心里有数。1.2 为什么从零开始反而更高效直接上手开源框架用现成的模型和预训练权重看起来最高效但在实际工程里常常会“卡壳”。比如你用了某个线上模型库改一个输入尺寸就要折腾一天换个推理框架又要处理一堆兼容问题。因为你没有理解它们底层的假设和局限出了问题都不知道去哪里找。从零开始不等于所有东西都自己手写——那也不现实特别是大模型时代。这里说的“从零”是指你亲手把一条最简易的端到端链路跑通理解每一个组件的作用再逐步替换、升级。就像学做菜你先用最基础的工具和食材做熟一道菜搞明白火候和调味的关系再去换更专业的设备和高级食材就容易驾驭得多。我建议初学者的路径是这样的先搭好Python环境、学会用Jupyter Notebook和虚拟环境掌握基础的数据处理和可视化再用一个小数据集比如MNIST、CIFAR-10完成一个从数据加载、模型定义、训练、评估到推理的完整闭环。这个过程不追求模型的SOTA效果追求的是“每个环节都不黑盒”。等你把这条链路跑顺了后面的迁移学习、分布式训练、模型压缩、部署上线都会事半功倍。1.3 核心关键技术栈开始之前先梳理一套最小可用技术栈。这不是说越新越好而是以“稳定、好排查、生态完善”为先。模块推荐工具理由编程语言Python 3.9生态最全教程最多团队协作成本低交互环境Jupyter Notebook / VS Code便于快速试错过程可视化数据处理NumPy Pandas基础数组操作和表格数据处理不可替代可视化Matplotlib Seaborn直接看到数据分布和训练曲线深度学习框架PyTorch 2.x调试方便动态图机制易于理解业界主流版本管理Git DVC数据版本管理代码和数据的可追溯性工程化基础日志与追踪MLflow / WandB记录每次实验参数和指标方便对比有人会问怎么不直接上TensorFlow现在PyTorch在研究和工业界占比都更高动态图调试对新手友好部署也有比较成熟的方案TorchServe、ONNX等。当然如果你所在团队已经深度依赖TensorFlow生态那另说。工具选型没那么多“非黑即白”关键是选一个能长期用、遇到问题找得到答案的。2. 核心细节解析与实操要点2.1 环境搭建从“装Python”到“虚拟环境隔离”很多人卡在第一步不是不会装而是不懂为什么装了Python还要再装Anaconda、还要建虚拟环境。我打个比方你电脑上同时有好几个项目有的需要Python 3.8有的需要3.10有的项目用PyTorch 1.13有的只能用2.0如果不做隔离你就是在把这些依赖全塞进同一个仓库迟早撞车——某个库升级后另一个项目直接跑不了。所以我的推荐是不管你是macOS、Windows还是Linux都先装MinicondaAnaconda的轻量版然后给每个项目单独建一个虚拟环境。这样环境出问题直接删掉重建不用动系统Python也不用担心污染其他项目。操作上的几个关键点创建环境命令conda create -n ai-project python3.9激活环境用conda activate ai-project。安装框架时先确认CUDA版本如果你有NVIDIA显卡用nvidia-smi查看驱动支持的CUDA版本再安装对应版本的PyTorch官网有安装命令生成器。如果显卡不够力先在CPU上跑小型模型等到后面再上云GPU。建议把项目依赖导出成requirements.txt或environment.yml放进仓库别人克隆下来一条命令就能复现环境。这里最容易被忽略的是“版本锁死”。requirements.txt里最好把关键包版本固定下来比如torch2.1.2不要写torch2.0。否则三个月后重新装环境你会发现所有依赖都升级了代码报错报得你怀疑人生。2.2 数据获取与清洗决定模型上限的“隐形上帝”在AI圈有一句话叫“垃圾进垃圾出”。模型再牛数据是脏的结果一定烂。很多人拿到数据集就急着训练我劝你先花至少一半时间在数据上。这里不只是清洗缺失值还要理解数据的分布、标注的质量、类别是否平衡。以图像分类为例你要做这几件事统计每个类别的样本数量看看有没有长尾问题。如果某个类别只有几十张而其他类别有上万张模型会严重偏向数量多的类。检查图片的尺寸、格式、是否损坏。一张坏图可能在训练几千轮后突然让loss变成NaN排查起来极费时间。做数据可视化随机挑几个样本看标签是否正确。比如“猫”这个类别里混了一张狗的照片这种标注错误哪怕只有1%也会让模型学出奇怪的边界。更工程化一点建议把数据划分训练集、验证集、测试集做成固定逻辑写成一个脚本每次运行都按同样的随机种子切分。这样你调模型时能确定性能变化是来自模型本身而不是因为数据划分变了。对于文本数据清洗步骤更多去掉HTML标签、处理表情符号、统一大小写、分词、去除停用词等。但在做这些之前先想清楚业务任务是什么。比如情感分析里表情符号往往是强特征你把它删了模型会损失关键信号。2.3 模型选择的“少即是多”原则模型不是越复杂越好而是“够用就好”。一个只有几百个参数的线性模型如果能把你的问题搞定就完全没必要上ResNet152。尤其从零开始做第一步是要有一个能跑的基线哪怕准确率只有70%也比一个“理论上能到95%但跑不起来”的复杂模型有价值得多。怎么选呢三步走先查问题类型图像分类用CNN比如ResNet、MobileNet序列任务文本、语音用RNN/Transformer结构化数据用XGBoost或LightGBM往往比深度学习更稳。再考虑数据量数据少的时候复杂模型容易过拟合。先选小模型或者用迁移学习加载预训练权重冻结大部分层只训练最后一层这是最快见效的方式。最后考虑推理环境和延迟如果是部署在手机端MobileNet之类轻量网络就比ResNet合适得多。我见过太多人上来就搬大模型结果发现光是下载预训练权重就下了半天训练一个epoch要几小时还没跑完就放弃了。从一个小模型开始快速验证数据管线和代码逻辑有没有问题等链路通了再逐步升级这是最务实的路线。2.4 训练过程的监控与记录训练不是一个“点一下run就去睡觉”的事至少要守住两条线损失曲线和验证指标。如果损失在下降、验证准确率在上升说明模型在学东西如果损失下降但验证准确率不动说明过拟合或者数据有问题如果loss是NaN基本就是学习率太大或者数据里有异常值。我每次训练都会开MLflow或者直接用TensorBoard把train loss、val loss、accuracy、learning rate都记下来。不要只看最终结果要看曲线形状。如果训练后期train loss还在降但val loss反弹那就该早停了可以用Early Stopping。这里有个小习惯每次实验记录一个备注比如“改了学习率0.01-0.001加了数据增强”这样回头看实验记录才能快速知道哪个改动起了作用。3. 实操过程与核心环节实现3.1 从一个小项目跑通全链路CIFAR-10图像分类我先用一个标准数据集CIFAR-10来演示“从零到评估”的完整过程。这个数据集有10个类别、6万张32x32的彩色小图很适合用来做管线验证。第一步加载数据。PyTorch内置了CIFAR-10但要注意做归一化和数据增强。归一化不是玄学而是为了把像素值缩放到均值为0、方差为1的分布这样模型训练更稳定。数据增强如随机翻转、裁剪能增加样本多样性降低过拟合。import torch import torchvision import torchvision.transforms as transforms transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) testset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform_test) trainloader torch.utils.data.DataLoader(trainset, batch_size128, shuffleTrue, num_workers2) testloader torch.utils.data.DataLoader(testset, batch_size128, shuffleFalse, num_workers2)这里num_workers按你CPU核心数调整Windows下建议设为0否则会报多进程错误。batch_size根据显存调整显存不够就调小但不要太小否则梯度更新不稳定。第二步定义模型。先不用花哨的网络用一个三层卷积加全连接的小网络就足够跑通流程。如果要从零理解CNN可以自己动手写这个结构后面再换成ResNet18。import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.MaxPool2d(2, 2) self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.fc1 nn.Linear(128 * 4 * 4, 256) self.fc2 nn.Linear(256, 10) self.relu nn.ReLU() def forward(self, x): x self.pool(self.relu(self.conv1(x))) x self.pool(self.relu(self.conv2(x))) x self.pool(self.relu(self.conv3(x))) x x.view(x.size(0), -1) x self.relu(self.fc1(x)) x self.fc2(x) return x model SimpleCNN()这里要注意全连接层的输入维度经过三次卷积和池化后32x32的图变成了4x4所以是128 * 4 * 4。这个计算不细心会踩坑。第三步定义损失函数和优化器。分类任务用交叉熵损失nn.CrossEntropyLoss()优化器用Adam或SGD。新手建议直接用Adam它自带自适应学习率省去很多调参功夫。不过要注意Adam加权重衰减weight_decay1e-4是常见的防过拟合手段别漏了。criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4)第四步训练循环。注意要optimizer.zero_grad()不然梯度会累积。每个epoch跑完后在测试集上评估一次把结果打印出来。for epoch in range(10): model.train() running_loss 0.0 for inputs, labels in trainloader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() model.eval() correct 0 total 0 with torch.no_grad(): for inputs, labels in testloader: outputs model(inputs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1}, Loss: {running_loss/len(trainloader):.4f}, Acc: {100*correct/total:.2f}%)这个代码跑10个epoch在CIFAR-10上大概能到70%左右准确率。别看数字不高关键是把整个链路跑通了。后面换一个更强的模型或者加更多trick会看到准确率涨到90%以上。3.2 从“能跑”到“好用”迁移学习实战小模型跑通之后下一步很自然是用预训练模型迁移学习。这是从零工程中性价比最高的一跃。用PyTorch加载预训练ResNet18很简单关键在替换最后的全连接层因为CIFAR-10有10类而预训练模型的输出是1000类。from torchvision import models model models.resnet18(pretrainedTrue) # 冻结所有层 for param in model.parameters(): param.requires_grad False # 替换最后一层 num_features model.fc.in_features model.fc nn.Linear(num_features, 10) # 只更新最后一层参数 optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3)冻结层的原因是预训练的卷积层已经学会了很通用的特征边缘、纹理、形状这些特征对大多数图像任务都适用不需要再花大力气训练。我们只需要把最后的分类器换成自己任务的。当然如果你的数据集和ImageNet差异很大比如医学影像、卫星图可以“解冻”最后几层一起微调效果会更好但也要注意过拟合风险。这一步做完同样的训练循环10个epoch后准确率能轻松到85%。这就是迁移学习的魔力。但要注意一个陷阱pretrainedTrue会从网上下载权重如果网络不稳定建议提前下载好权重文件放到本机缓存目录。3.3 模型保存与加载不要只在内存里有个模型训练完模型第一件事不是庆祝准确率而是把模型存下来。最简单的保存方式torch.save(model.state_dict(), model.pt)这里不建议直接保存整个模型对象因为后续加载时如果网络结构定义变了容易出兼容问题。只保存state_dict是最稳妥的加载时要先构建同样的网络结构再model.load_state_dict(torch.load(model.pt))。如果你还要部署到服务端输出成ONNX格式会更通用。ONNX像是一种“模型交换格式”不同框架都能加载。PyTorch导出ONNX的代码如下model.eval() dummy_input torch.randn(1, 3, 32, 32) torch.onnx.export(model, dummy_input, model.onnx, input_names[images], output_names[logits], dynamic_axes{images: {0: batch}, logits: {0: batch}})dynamic_axes表示batch维度可以动态变化这样线上推理时不用固定batch大小。导出时模型一定要用eval()模式否则BatchNorm和Dropout行为不对导出的模型推理结果和训练时不一致这是个很隐蔽的坑。4. 常见问题与排查技巧实录4.1 损失变成NaN怎么办这是新手遇到最多的噩梦。模型跑着跑着loss变成NaN后面全部作废。原因大概是下面几种学习率太大梯度爆炸。解法把学习率调小10倍比如从0.01调到0.001。数据里有NaN或无穷大值。在数据加载后assert torch.isnan(data).sum() 0检查文本数据要看有没有缺失值。模型里某层输出数值过大比如半精度训练时更容易溢出。解法在模型里对某些层的输出做norm或者换成float32。排查思路很简单先从学习率入手如果还不行就打印每一层的输出看是哪里爆炸。不要瞎猜定位到具体层才能解决。4.2 验证集准确率低但训练集很高过拟合过拟合的经典症状就是训练集准确率接近100%验证集却停滞在60%左右。这说明模型太“记死”训练数据了泛化能力差。解决办法按优先级排增加数据增强随机裁剪、翻转、色彩抖动等相当于免费扩充数据。加正则化weight_decayL2正则直接把权重压小让模型更平滑。Dropout在几层后随机丢弃一部分神经元防止某些路径被过度依赖。早停验证集指标连续几个epoch不上升就停止避免最后的过拟合阶段。如果数据太少用迁移学习代替从零训练或者收集更多数据。我记得有个项目验证集准确率一直卡在78%翻了数据才发现训练集里有一半图片打了错误标签。修正标签后准确率直接跳到91%。所以遇到诡异情况先回头检查数据10次里至少有3次是数据问题。4.3 GPU显存不够怎么办显存不够体现在报错CUDA out of memory。应对策略有几种调小batch_size这是最直接的。但batch太小会导致训练不稳定配合用多个batch的梯度累积gradient_accumulation来解决。使用混合精度训练。PyTorch自带的torch.cuda.amp能自动让部分计算用半精度显存占用直接减半速度还可能更快。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for images, labels in trainloader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()注意最后要scaler.update()否则学习率动态缩放不会更新。这是很多网上教程容易漏的一行。梯度累积技巧假设你想用batch_size128但显存只能塞下32那就连续算4个batch的梯度后再更新一次参数。代码上就是在前3个step不执行optimizer.step()只累积梯度。4.4 训练速度慢到怀疑人生小项目还好数据量大一点就慢得受不了。先看是不是瓶颈在数据加载num_workers太低会让GPU等着CPU喂数据。另外如果用Jupyter长时间跑训练内核崩溃也不要奇怪建议把训练脚本写成.py文件在终端运行。还有一个容易被忽视的问题你没有用GPU。如果安装了CPU版的PyTorch即使有独立显卡也没用。检查方法import torch print(torch.cuda.is_available())输出必须为True否则就重新安装对应CUDA版本的PyTorch。另外注意要用.cuda()把模型和tensor搬到GPU上光有GPU驱动不够。4.5 模型部署后效果变差训练和部署有个“环境鸿沟”。训练时数据有增强、有归一化推理时可能忘了做归一化或者部署时图片输入尺寸、通道顺序和训练时不一致。常见原因就是推理管线没有严格复现训练时的预处理。解决办法是把预处理逻辑封装成和模型同等的“函数”训练和部署共享同一份预处理代码而不是靠复制粘贴。另外如果用了ONNX导出一定要先在本机用导出前后的模型跑同一批样本对比输出是否一致。差一点点没关系差很多就说明导出配置有误。5. 从“模型”到“产品级AI系统”的工程化扩展5.1 用MLflow追踪每一次实验做AI工程你可能一天要跑几十次实验。如果不记录一周后回头连“这个模型效果最好”都说不清是哪个配置得出来的。MLflow把每次实验的参数、指标、模型产物、代码版本都关联起来。基本用法import mlflow mlflow.start_run() mlflow.log_param(lr, 0.001) mlflow.log_param(batch_size, 128) mlflow.log_metric(val_acc, 0.85) mlflow.pytorch.log_model(model, model) mlflow.end_run()这样在MLflow UI里就能看到每次实验的完整记录。配置上稍微花点时间但长期受益极大。尤其当你需要向别人复现实验时一份完整的MLflow记录胜过十页文档。5.2 自动化训练流水线从脚本到Pipeline一个可维护的AI项目需要把训练流程拆成多个独立步骤数据校验、预处理、训练、评估、注册模型。每一步单独运行产出的中间结果比如清洗后的数据保存成文件。这样哪一步出了问题只需要重跑那一步而不是整个流程。我用最简单的办法把全流程写成一个Makefile或Shell脚本。比如make preprocess负责数据清洗make train负责训练make evaluate负责评估。注意在跑训练前先跑make preprocess确保数据是最新的。再进阶一步可以用DVC给数据文件做版本管理这样“数据变了”也能在实验历史里追踪到。5.3 模型服务化用FastAPI暴露预测接口模型训练完最终要进入应用。最简单的部署方式是用FastAPI包一个HTTP接口。import io import torch from PIL import Image from fastapi import FastAPI, UploadFile from torchvision import models, transforms app FastAPI() model models.resnet18() model.fc torch.nn.Linear(512, 10) model.load_state_dict(torch.load(model.pt)) model.eval() transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)), ]) app.post(/predict) async def predict(file: UploadFile): image_data await file.read() image Image.open(io.BytesIO(image_data)).convert(RGB) tensor transform(image).unsqueeze(0) with torch.no_grad(): output model(tensor) pred output.argmax(dim1).item() return {class: pred} # 启动: uvicorn main:app --host 0.0.0.0 --port 8000这里的要点是model.eval()必须放在调用前并且推理全程在torch.no_grad()里。如果每次请求都重新加载模型那是灾难所以模型要在FastAPI应用启动时就加载到内存中。部署后要监控请求延迟、错误率、预测分布。如果一个类的预测比例突然暴涨大概率是数据漂移了——真实世界的输入分布和训练时不一致。这种情况下要及早触发“重新训练”的流程而不是等到模型烂了才反应。5.4 持续学习让模型跟着数据走AI系统上线不是终点而是起点。线上数据持续变化模型性能会慢慢衰减。一个有效的工程体系需要包含数据回流的机制把线上真实样本收集下来定期加入训练集。重新训练触发条件当监控指标如用户反馈、模型置信度低于某个阈值时自动触发训练任务。模型版本管理训练出的新模型先进行离线评估再灰度上线防止劣化版本全量替换。这套体系做起来需要一些基础设施但从第一版就可以先把最简单的写下来每天把线上推理输入输出存一份日志每周手动看一下分布有没有大变。等团队成熟了再慢慢把自动化补上。6. 几个让我少走弯路的实操心得就挑三个印象最深刻的点说说。第一不要迷信“调参”。我早先为了刷准确率光是学习率就试了十几种组合后来发现问题出在数据标注上。先把数据质量提到最高优先级你会发现模型精度会自己涨上来。数据就像一面镜子你喂给它什么它还给你什么。第二从最简单的基线开始再一步步加花。不要一上来就上最复杂的模型和高深的trick。基线模型至少能帮你验证数据管线、损失函数代码是否正确。如果最简单的模型都跑不出合理结果那问题大概率不在模型复杂度上而可能在数据或代码逻辑里。先把地基夯实再加楼层否则出了问题都不知道该拆哪一层。第三重视“可复现性”。做实验时的随机种子、数据切分版本、训练配置务必记录下来。这不仅是工程素养也是一种自我保护——当别人或者三个月后的你自己问起“这个结果怎么出来的”的时候你能给出准确答案。否则所有成功的经验都只是运气无法复制也无法迭代。从零学AI工程这件事最需要的不是背诵一堆理论而是亲手把一个最小系统从数据到部署完整跑一遍中间所有报错、异常、调优都是宝贵的学习素材。只要这条主链路搭建起来了后续无论面对模型架构、算法优化、还是规模化部署都只不过是在这条主链路上做增量而已。