ARTICLE DETAIL

资讯详情

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

AI工程从零实战:数据、训练、评估与部署全链路

AI工程从零实战:数据、训练、评估与部署全链路 很多刚接触 AI 工程的人心里都有同一个疑问“ai-engineering 到底和调模型有什么区别”我当年也以为跑通几套开源框架、能调参就算入门了直到自己动手从零搭建了一套完整的训练和上线链路才真正意识到差距在哪。这篇文章就讲一讲我在“ai-engineering-from-scratch”这个项目里的复刻过程和踩坑记录——不依赖迁移学习不调用高层封装把数据、训练、评估、上线、监控一条线自己走一遍。如果你也想把 AI 工程能力真正长在自己身上这篇的经验应该对你有用。1. 为什么非要从零过一遍 AI 工程1.1 那些年“调包调参”留下的控制力缺口先讲一个我自己的教训。曾经接手一个排序模型前任工程师用的是天底下最主流的训练框架代码写得很干净日志也齐全模型离线 AUC 也很漂亮。但上线之后业务方反馈商品排序点击率下降我第一反应是网络分发有问题查了半天最后发现是我们调整了学习率调度策略框架默认优化器参数覆盖掉了新的配置导致模型在微调阶段直接跑偏。这种问题在“照着模板跑通”的工程里非常隐蔽。你用框架的时候框架帮你做了大量决策什么时候保存 checkpoint、用什么方式聚合梯度、评估时是否打乱样本顺序。这些默认值大多数时候没问题但它会让你的注意力从“系统怎么运作”转移到“参数怎么配”一旦系统出问题你手里的排查工具很少只能一遍遍看文档试图找到一个“正确的 API 调用方式”。后来我把“从零实现训练循环”当成一个严肃的工程来做才开始理解那些默认值存在的理由也才逐渐获得一种更底层的控制力——模型不收敛时我能判断是梯度计算出了问题还是数据顺序出了问题而不是盲目改学习率。1.2 从零复刻能补回来的三种能力经过这个项目我发现从零过一遍 AI 工程至少能补回三种平时调包很难练出来的能力。第一种是推导和定位能力。手写一次反向传播之后你对“框架报错为什么会报在 backward 里”就有了直觉。很多 NaN、维度不匹配的问题你不再需要用二分法逐层打印张量而是能直接从计算图上的数据流看出问题。第二种是端到端的工程统筹能力。数据清洗、模型训练、效果评估、服务部署每段单独拆开看都不算难难的是它们之间的衔接。样本分布变了评估脚本要不要重新生成模型换了一个输入特征接口协议是否同步升级这些东西只在单一框架里学不到必须把链路完整跑一遍才会遇到。第三种是对生产环境的敬畏感。离线训练集上的指标再高也不代表线上行为正确。你自己从头搭过一遍服务端就会明白推理时的数据延迟、缺失值、超时重试这些旁枝末节和模型结构一样重要。我并不是说所有项目都要抛弃框架重新造轮子而是建议每个 AI 工程师至少要有过一次“不借助高层框架把模型从数据一路推到线上”的完整经历。这个项目就是我把这段经历固化下来的版本。2. 数据是 AI 工程的地基从原始样本到训练集2.1 采集与清洗的第一道坎从零开始做 AI 工程最容易低估的是数据管线。第一周你可能还在兴奋地设计模型结构第二周就会被脏数据折磨到怀疑人生。我做的第一个任务是一个图像分类场景训练数据是从开放渠道抓下来的图片数量大概六万张。表面上看类目齐全、分辨率也够。真正开始清洗时才发现数据集里有大量重复图、灰度图、带水印的图还有一部分图片的标签和内容是错的。更麻烦的是有些违规图片不在明面上混在相似类别里导致模型在几个容易混淆的类别之间来回抖动。清洗这件事没有捷径关键是建立一套“先看分布、再做规则、最后人工抽检”的流程。我的做法是先统计每个类别的样本数量、平均尺寸、色彩通道分布把明显异常的样本筛出来。计算图片的感知哈希按相似度聚类把近乎重复的样本只保留一张避免训练集和验证集之间出现“双胞胎样本”。对低置信度的样本做人工复核宁可删不可错。之所以强调“先看分布”是因为很多清洗规则是被数据分布逼出来的。比如某个类别图片普遍偏暗你直接用全局阈值去过滤会误杀一堆有效样本。先画直方图、看大小、看均值方差再定阈值才是工程做法。这一点放在任何 AI 项目里都一样。2.2 标签一致性、采样策略与数据版本化清洗之后第二道坎是标签一致性。同一个物体不同标注员写出来的标签可能完全不一样。我见过一个开源数据集的标签里“背景”和“环境”混着用模型训练时无法有效区分。自己收集数据时这种事更常见。我的建议是给每条样本生成唯一的样本 ID把原始文件、标签、清洗规则、版本号全部记录在一张清单表里。清单表用 JSONLines 存储每一行都包含样本 ID、文件路径、标签、清洗状态和备注。这样即使后来发现某个标签批量错了也能精确追溯到是哪一批、哪一天、改过什么。采样策略也需要认真对待。很多分类任务的正负样本比例极端如果不做处理模型学到的只是“大部分样本属于负类”的先验。最简单的做法是在构建 batch 时做类别采样让每个 batch 里各类别比例保持均衡但要注意不要过度均衡否则模型会高估小类的真实分布。这一点离线指标看起来不错线上却很吃亏因为线上真实分布里小类本来就不多。数据版本化同样重要。每次数据清洗规则一变整个数据集就应该打一个新的版本号并且把版本号和训练记录绑定起来。项目跑久了你会遇到“旧模型效果回不到当初”的问题很多时候不是模型代码变了而是训练数据在某个环节被悄悄替换了。没有版本号这种问题几乎没法查。2.3 特征处理的边界手工规则与模型的平衡用从零实现的模型做数据管线时不可避免会遇到一个问题哪些特征应该手工构造哪些特征应该交给模型自己学习我的经验是凡是规则明确、计算稳定的信息手工构造更好凡是规则复杂、不断变化的信息交给模型学习更好。比如图像场景里的边缘密度、灰度分布这些特征有明确物理含义手工构造既快又可解释。而像用户行为序列里“最近三次点击之间的时间间隔模式”规则很难概括这时候不如让模型自己提取。当时的项目里我保留了非常克制的特征工程。只做三类处理把原始数据里的缺失字段转成显式的缺失标记把枚举类文本映射成稳定的字典索引把数值特征做分位数归一化。其余深层交互全部交给模型去学。这样的好处是特征工程部分逻辑简单、不容易引入泄漏后续排查问题也轻松很多。3. 从零手写训练循环才看懂模型的“脾气”3.1 反向传播用最朴素的方式验证推导模型的“脾气”是什么说白了就是你对训练过程有没有直觉。这种直觉只能建立在手写过一次正向和反向传播的基础上。我当时用两层全连接网络做热身没有调用任何自动求导库只用 NumPy 手动实现了前向传播、反向传播和参数更新。示意代码如下import numpy as np def relu(x): return np.maximum(x, 0) def forward(x, w1, b1, w2, b2): h relu(x w1 b1) y (h w2 b2) # 未接 softmax便于展示 return h, y def backward(x, h, y_target, y_pred, w2): dloss y_pred - y_target # 简化后的平方误差梯度 grad_w2 h.T dloss grad_b2 dloss.sum(axis0) grad_h dloss w2.T grad_h[h 0] 0 grad_w1 x.T grad_h grad_b1 grad_h.sum(axis0) return grad_w1, grad_b1, grad_w2, grad_b2写这个代码不是为了证明自己能做矩阵乘法而是为了在调试时多一个“参照系”。一旦你在某个训练步骤怀疑梯度有问题就可以写一段数值梯度检验来验证。数值梯度检验的原理很简单对参数加一个极小的扰动用差分近似计算梯度再和你手推的反向传播结果对比。误差在 1e-5 量级就算正常。那段代码大概长这样def numerical_gradient(loss_func, param, eps1e-6): grad np.zeros_like(param) it np.nditer(param, flags[multi_index]) while not it.finished: idx it.multi_index orig param[idx] param[idx] orig eps loss_plus loss_func() param[idx] orig - eps loss_minus loss_func() grad[idx] (loss_plus - loss_minus) / (2 * eps) param[idx] orig it.iternext() return grad每实现一种新模块我就先跑一个迷你测试集用数值梯度验证手写的反向传播。样子笨但相当靠谱。这套习惯一直到今天都在使用只是现在框架自动帮我做了大部分求导我依然会在设计新损失函数时用同样的方法快速验证。3.2 损失函数、学习率与优化器的协同从零训练之后我对学习率、损失函数、优化器三者关系的理解也彻底变了。以前我会觉得“Adam 比 SGD 好”用起来也确实是这么回事。但当我看到自己实现的更新逻辑在同一个数据集上反复震荡时才明白问题不是“该用哪个优化器”而是优化器需要和损失函数共进退。举一个例子用平方误差做分类任务的损失函数配合 SGD收敛慢且容易困在局部最优附近。换成交叉熵之后哪怕学习率稍微大一点模型也更容易收敛到可接受的位置。原因在于梯度方向的质量不一样平方误差在分类任务上产生的梯度受输出饱和影响太大。这个道理不自己跑一遍看十遍文章也只是记住结论。实际训练时我的建议是先用一个小学习率在三五个 epoch 内观察损失曲线判断是否下降。如果损失纹丝不动先不要急着加模型复杂度可以先降低学习率如果训练振荡明显再考虑降低学习率或换用带动量的一类优化器。还有warmup 在小数据集上没有被很多人重视其实它能让初始阶段的参数更新稳定不少特别适合从头训练的模型。3.3 梯度检查、NaN 排查与数值稳定NaN 问题几乎是每个手写训练循环的人都会遇到的头号大敌。第一次训练时loss 打印到第三个 batch 直接变成 nan我以为是学习率太高一查发现是数据里有缺失值传到 log 计算里变成了 log(0)。排查 NaN 的完整链路我总结成三步先看输入数据确认没有 NaN、Inf也没有极端大值。在损失函数前后打印中间值看是前向过程失控还是反向过程溢出。在参数更新前后增加“梯度裁剪”限制梯度的最大范数。梯度裁剪这个操作比较简单但对训练稳定性帮助很大。它的实现逻辑类似下面if grad_norm max_norm: factor max_norm / (grad_norm 1e-6) for param in params: param.grad * factor在后期我用它接住了不少训练中的抖动问题。尤其是用了较长序列特征之后梯度大小会随着序列长度变化放大这个保护措施几乎是刚需。4. 离线评估的度量骗局与实验可复现性4.1 评估线上常踩的三个问题模型训练出来第一步当然是看离线指标。但我在这个项目里踩过的坑明确告诉我离线指标好看和线上效果靠谱是两件不同的事。第一个问题是数据切分的顺序。清洗和特征构造必须放在切分之前但必须在同一条数据流里做。如果先对整个数据集做标准化再划分训练集和验证集验证集就已经被“看”过一次。这个错误非常隐蔽因为验证集指标会偏高但你真的上线时反而表现不稳。第二个问题是验证集的采样方式要贴近线上真实分布。有的团队为了追求验证集指标稳定故意把难样本删掉一部分训练变得很容易验证集分数也一路走高。真到了线上垃圾流量、异常请求全涌进来模型就崩了。我后来在验证集里加入了一个固定比例的“困难样本池”专门模拟线上的最差情况。第三个问题是指标选择太单一。分类准确率Accuracy在一个正负样本不平衡的场景里几乎没有参考价值。同一个模型准确率 92%如果把置信度最低的 10% 样本筛掉准确率可能只有 70%。所以要同时关注精确率/召回率、混淆矩阵、分位数预测偏差以及最容易忽视的逐类别指标。下面是我整理的评估记录表格来源项目实际实验记录简化版指标全量样本常见类别困难类别准确率0.880.930.61精确率0.850.910.55召回率0.820.890.48困难类别的召回率低得离谱但总指标被“常见类别”掩盖了。只看一个数字的人很可能会带着满意的心情上线。4.2 实验记录与配置管理从零开始做工程最容易失控的反而是实验过程。我早期经常犯一个错误训练完后只记住最优的参数却不记得当时用了哪些数据版本和预处理选项。两周后想复现只能靠模糊记忆。后来我固定了一套流程每次实验启动前把训练代码所在仓库的 commit、数据集版本、配置文件的 JSON、随机种子、依赖库版本等全部写进实验记录。跑完后指标结果和模型文件的路径也会追加到同一份记录。做到这一步哪怕两周后回来看也能很清楚地知道“那次指标高”的原因到底出在哪个环节。随机种子管理尤其重要。从零写数据加载器时每个 epoch 的样本顺序都会变如果不固定种子即便模型架构完全一样两次训练出来的指标也可能差好几个点。这会让实验对比彻底失效。所以我在数据加载器和模型初始化两个位置都显式设置种子并在实验记录里写上种子值。4.3 模型不会告诉你的事边界案例与失败模式评估报告只能告诉你模型平均表现如何不会告诉你模型在什么情况下会犯离谱错误。一个有效的方法是做错误分析Error Analysis专门找 100 条预测错误的样本逐条看它们的共性。我当时发现模型把所有“明暗对比强烈”的图片都倾向分类为某一个大类原因很简单训练集里的大类样本普遍存在强对比度模型学到了对比度这个特征而不是真正的语义特征。这种情况下任何全局指标都看不出来只有把错误样本摆在一起才能发现问题。从这之后我在每次迭代里固定加入一个工作项挑出错误样本中最相似的十组手动分析失败模式。这一条比起任何花哨的调参策略都更有用。5. 模型上线之后AI 工程才算真正开始5.1 服务化接口设计从笔记本到生产环境训练模型只是 AI 工程的前半场。后半场是把它封装成稳定服务。我第一次上线时直接把训练用 Python 代码原样封装成了 API结果一个请求进来预处理模块会加载整套训练用的词汇表内存直接翻了三倍几十个并发进来就超时。正确的做法是把推理逻辑做成独立的部署单元确保它只依赖模型权重和必要的预处理配置不依赖训练代码。我后来把接口拆成两个函数preprocess(request)和predict(processed)中间用 JSON Schema 做输入校验非法字段一律走明确的错误码返回。模型更新时新模型对应新版本号旧版本保留一段时间方便快速回退。另外在线推理的 batch 策略要提前设计清楚。是单条预测还是攒批预测吞吐和延迟差别很大。攒批预测在高并发时收益明显但对单条请求延迟不友好。这个取舍没有标准答案取决于业务对延迟的容忍度。5.2 数据漂移检测和监控上线后的头号敌人是数据漂移。用户行为、流量结构、外部环境都在变训练时建立的分布假设很快就会失效。我最早没有监控这套东西直到某天模型效果连续三天下滑才发现某个特征字段在线上有超过 30% 的缺失率而训练时这个字段几乎是完整的。现在我会在服务里记录每一批请求的以下统计量各输入特征的均值、方差、缺失率、新类别出现次数。然后用一条简单的检测规则如果某个特征的均值偏移超过训练集均值的两个标准差或者缺失率比训练时高了五个百分点就触发告警。告警不一定要立刻切流但至少要让人知道“模型正在面对不熟悉的数据”。更进一步的方案是维护一个简单的在线样本池定期把线上请求和训练样本做对比输出特征分布差异报告。这一步不需要特别复杂的算法距离、卡方检验都行关键是把监控做在模型旁边而不是有问题之后再去翻日志。5.3 模型版本管理与回滚说到回滚很多人觉得“把上一个模型文件重新加载一下”就行。实际操作中更麻烦的是新旧版本之间的兼容问题。我在项目的早期吃过亏新版模型引入了两个新特征旧版接口没有对应的解析逻辑回滚时前端请求仍然拿着新版协议模型处理不了直接报错。所以现在每次发版时我会同时保存三个东西模型权重文件、推理代码版本、接口协议版本。回滚时三者一起切。新协议只能在服务端兼容不能强制线上流量立刻切换。用软件工程里那句老话来说就是保持“向后兼容”。接口协议版本和模型版本分开管理给每个模型打上独立标签这样切换时清晰、可控。6. “从零”的边界什么该动手造什么不该重复造6.1 三条判断原则从零复刻完一条 AI 工程链路我的收获不光是“我会写训练循环了”更重要的是明白什么事情值得自己造什么事情完全不必。判断标准有三条它是你做项目时的核心盲区吗如果是花时间从零做一遍是值得的。数据管线、训练循环、评估逻辑这些是 AI 工程师的看家本领不能只会用现成的。它在你的项目里是一次性投入还是反复依赖一次性投入的脚本不值得精雕细琢反复依赖的基础模块才值得自己掌控。社区方案是否足够稳定和成熟已经非常成熟的组件比如分布式训练协调、GPU 算子实现、容器编排自己从零做一遍学习价值有限维护成本却很高。我用这个方法给自己划分了边界自己实现数据采样器、训练循环、评估脚本、漂移检测小工具直接采用成熟组件来处理容器化部署、在线服务框架和资源调度。这样既保留了底层控制力又不至于被基础设施问题拖死。6.2 三个月周末版路线图如果你也想动手做“ai-engineering-from-scratch”我建议按三个月周末版来拆解压力完全可承受。第一个月核心是把“数据管线加训练循环”打通。选一个有公开数据来源的分类任务规模不需要大几千到几万条足够。自己实现数据清洗、均衡采样手写一个两层神经网络或小型卷积网络自己实现反向传播。别急着堆模型结构重点是让每个环节都在你自己的代码里可控。第二个月核心是评估和实验管理。在第一个月跑通的链路上加入分层验证集、混淆矩阵、逐类别指标做好实验记录和随机种子管理。这个阶段的产出是你能解释模型当前为什么是这个水平哪些类别拖了后腿。第三个月核心是部署和监控。把模型封装成独立接口加协议版本、错误处理、数据漂移告警。不一定要真的部署到很大的集群本地容器环境就能把整条链路跑完。这一步做完你对 AI 工程的完整感自然就有了。6.3 给正在复刻这条路的人一些额外提醒最后多说几句实操体会。第一不要一开始就选择太复杂的模型。我当时差点去买一块新显卡来跑大模型后来发现完全没必要用小网络把数据管线验证通了再去逐步换更宽的模型改动成本低很多。第二手写代码和成熟框架并存并不矛盾。我在项目后期训练循环已经从纯手写版本切回了成熟框架但底层的数据采样和评估脚本仍然保留自己写的那份。原因很简单那些逻辑和业务强相关用现成的反而要花大量时间做配置适配。第三请保留这个项目里最初级、最朴素的那版代码。我在实际调试生产中遇到的问题时经常会打开最早的手写版训练循环找灵感。那里面的数学虽然笨拙但它是理解一切复杂系统的最好坐标。这就是我认为“从零开始”最大的价值所在。
返回列表