ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI工程从零开始:从数据清洗到部署监控的完整实践指南

AI工程从零开始:从数据清洗到部署监控的完整实践指南

ai-engineering-from-scratch,看到这个标题,可能很多人第一反应是“从零手写神经网络”。但我更愿意把它当成另一回事来读:做AI工程,别靠黑盒。我见过不少同事照着教程把模型调出来了,可真到了数据喂不进来、loss蹦出NaN、线上效果和离线差十万八千里的时候,才发现自己根本不了解链路里发生了什么。与其说我们缺一门“AI工程从零开始”的课程,不如说我们缺一套完整的工程心智地图——从数据到模型,从训练到部署,从评估到监控,每一步都亲手走过一遍,心里才有底。

这篇文章适合两类人。一类是有Python基础、但还没完整跑过AI项目的同学;另一类是已经在做模型训练、却总觉得自己在“调参”而不是“做工程”的人。我会把AI工程从零开始需要重点掌握的四个板块——底层能力、训练评估、部署监控、工程习惯——拆开来讲,每一段都带上项目里的真实经验。看完之后,你应该能拼出一张属于自己的行动清单。

1. AI工程到底是什么:先搞清楚边界

1.1 一条AI系统的完整生命周期长什么样

假设要做一个商品评论态度分析,需求听上去很简单:把正面、负面评论区分开。但如果真按AI工程的思路来做,你就会发现,至少要跨过这几道关卡:数据从哪来、字段是否完整、标签谁打、质量怎么抽查;文本要不要清洗,emoji、缩写、错别字如何处理;用什么方式向量化、序列长度怎么截断;训练方案怎么定,哪些指标算“达标”;模型怎么部署,用户请求进来之后多少毫秒能返回;上线之后准确率降了,怎么及时发现、怎么迭代。

这些环节串在一起才是真正的AI工程,任何一环掉链子,最终产品都会翻车。刚入门的人最容易犯的错误,是把目光全部放在“训练模型”上,好像只要模型选得好,一切问题就自动解决了。实际上训练只是整条链路里的一个环节,在很多项目里,数据问题和部署问题花掉的时间远超建模本身。我做过的一个报表自动解析项目,光是把各种格式的Excel统一成结构化数据,就占了整个项目周期的四成时间,模型部分反而快。

1.2 from scratch不是重复造轮子,是亲手打通链路

我强调AI工程要from scratch,并非让你丢掉PyTorch去手写反向传播,那是“从零写框架”,是另一件事。工程意义上的from scratch,是指不依赖一键式平台、不满足于黑盒调用,而是让自己置身于系统制造现场。举个很直接的例子:同样是用于文本分类的模型,只调用平台API的人,可能连模型输入如何组织、输出如何解析都搞不清楚;而亲手写过训练脚本的人,至少明白模型拿到了什么数据、loss在优化什么、预测结果由什么决定。遇到线上问题,这两种人的排查速度差异巨大——前者只能等平台方回复,后者能直接翻日志、复现样例、定位环节。

我自己的建议是:上手阶段可以用sklearn和PyTorch作为工具,但每个关键环节都往深走一层。损失函数到底在计算什么、梯度更新做了什么、预测时为什么要做softmax、为什么必须加model.eval(),这些原理不弄明白,后面做任何调试都是盲人摸象。等你完整走过一遍,再看那些高级框架和自动化平台,就不会再发怵了,因为它们只是把你知道的流程包装得更平滑而已。

1.3 AI工程和机器学习研究的本质区别

还有一个容易混淆的点,我想专门拿出来讲。算法研究的目标是让模型效果“更好”,比拼的是谁刷分刷得高;AI工程的目标是让系统“更稳、更省、更可控”,核心关注的是交付质量、成本和可维护性。两者有重叠,但工作重心完全不同。

举几个直观对比:研究里可以为了提升0.5个点把模型参数翻倍,工程上要同时考虑响应时间、显存占用和推理成本;研究里跑实验可以依赖随机种子跑很多次再取平均,工程上要求实验可复现、结果可对比;研究里效果不好可以换数据集,工程上数据是业务的,不能随便换。理解了这层差异,再去看各种AI工程讨论,就不会觉得“怎么都在纠结些琐碎的事情”。琐碎恰恰是工程的主体,细节决定的是线上稳定性和成本账。

2. 搭地基:从零开始需要掌握的底层能力

2.1 Python与NumPy:建立向量化思维

AI工程的学习,建议从编程能力切入。Python是主流语言,但很多人只熟悉基础语法,对数据操作缺乏直觉。这里最需要扭转的一个思维,是从“写循环”转换到“写向量化代码”。

一个最简单的例子,给一组数做标准化,减均值再除以标准差。新手写法通常是用for循环逐个处理,数据量在几百条时感觉还行,一旦到千万级,速度会慢得让人崩溃。用NumPy写就两行:

import numpy as np arr = np.random.randn(10_000_000) arr_norm = (arr - arr.mean()) / arr.std()

这段代码背后是高度优化的数值计算库,跟逐元素循环完全不是一个量级。别觉得这只是效率问题,大模型训练、多模态推理时,几乎每一步都是这种张量运算。向量化思维直接决定了你能不能看懂模型中间层的形状变化,也决定了当矩阵维度报错时你能不能一眼定位到是哪一步拼接出了问题。很多新手做文本分类卡住,就卡在“序列长度”“batch大小”“词向量维度”三者的匹配关系上,这本质上是张量维度感没建立起来。

2.2 机器学习的地基:从线性回归理解优化

学AI工程,我不建议一上来就追大模型。先把最基础的线性回归吃透,因为它几乎涵盖了机器学习的所有核心概念:特征、标签、模型、损失、梯度、优化。你可以这样理解:线性回归是机器学习里的“最小可运行系统”,把它的每个细节弄懂,后面的神经网络只是在这个基础上加了多层变换和非线性激活。

这里给一段用梯度下降手写线性回归的最小实现:

import numpy as np X = np.random.randn(1000, 4) true_w = np.array([1.5, -2.0, 0.8, 3.2]) y = X @ true_w + 0.1 * np.random.randn(1000) w = np.zeros(4) lr = 0.05 for epoch in range(200): grad = -2 * X.T @ (y - X @ w) / len(y) w -= lr * grad loss = ((y - X @ w) ** 2).mean() if epoch % 20 == 0: print(f"epoch {epoch}, loss {loss:.4f}")

这段代码虽短,但每一步都值得反复琢磨:为什么梯度是X.T @ (y - X @ w)?为什么最后要除以样本数?学习率从0.05换成0.5会发生什么?如果你能独立回答这些问题,就说明你对训练机制有了真实理解,不再是照着抄。还有一点容易被忽视:这里是在全部样本上计算梯度,属于批量梯度下降,真正的训练代码在大规模场景下会改成mini-batch形式,因为显存有限,而且小批量更新本身也带有一定的正则化效果,对收敛有帮助。

2.3 数据清洗与特征工程:脏数据决定模型上限

在AI工程这个行当里流传一句话:模型决定了下限,数据决定了上限。我自己的项目经历也反复验证了这一点。数据清洗和预处理包括缺失值处理、重复样本清理、异常值识别、字段类型修正,很多人容易轻视这步,觉得模型自己会“学习”,可现实是,模型对脏数据几乎没有容错能力,垃圾进垃圾出。

处理缺失值时通常有几种思路:数值型字段用中位数或均值填充,类别型字段用众数填充,或者把“缺失”本身标记成一个独立类别。关键是你填充策略要和模型匹配,树模型对缺失值有天然容忍度,线性模型和神经网络则不能接受NaN。所以在动手之前,第一步要看的不是代码而是数据体检报告:每个字段缺失率多高、分布长什么样、有没有明显离群值,想清楚之后再做决定,而不是按模板机械执行。

特征工程方面,归一化也是一个经典话题。距离敏感模型比如KNN、SVM、神经网络,对数值尺度很敏感;树模型则无所谓。在大模型时代,处理文本、图像这类非结构化数据时,手工特征的价值确实下降了不少,因为预训练模型会自动提取特征。但在结构化表格数据场景里,特征工程仍然是决定效果的关键环节。我的建议是分类看待:不要盲目迷信全自动特征提取,也不要抱着旧方法不撒手,根据数据类型选择合适的方法。

3. 第一次构建AI系统:从训练到评估

3.1 最小可运行的实验流程

建议第一次完整做AI项目时,不要选太复杂的场景。文本情感分类是比较合适的选择:数据量大、标签明确、模型选择多,从传统机器学习到预训练模型都能试。先把最小流程跑通,再横向扩展难度。

实验流程按这个顺序走:读数据、划分训练集/验证集/测试集、定义模型、训练、验证、预测、评估。划分数据集时有一个容易被忽视的原则:验证集和测试集的分布必须跟真实场景一致。什么意思?如果你处理的文本来自线上用户评论,那划分时就要保证训练集、验证集、测试集都覆盖到各种来源类型,而不是简单随机切完就完事。对时序数据更是如此,绝对不能乱shuffle,否则会模拟出“用未来预测过去”的虚假效果。

3.2 训练循环里的关键参数

第一次写训练循环,要关注的参数别贪多:学习率、batch size、epoch数、优化器,这四样就够用了。其他参数等有经验了再慢慢加。

学习率是训练中最敏感的参数,没有之一。学习率太大会导致loss震荡甚至发散,太小则收敛极慢。常用的经验做法是先用默认值跑短几个epoch观察曲线,再根据曲线坡度调整。batch size影响的是梯度的稳定性:批次越大,梯度越稳,但显存占用越高;批次太小,训练噪声大,收敛不稳定。epoch数要结合验证集表现来定,核心是看验证指标是否还在提升,而不是看训练loss低到多少。

优化器选择上,现代神经网络训练几乎无脑用Adam或AdamW。Adam对学习率的适应性强,收敛速度快,对新手尤其友好。但在预训练模型微调后期,有人会切成带动量的SGD做更精细的收敛,让模型在最优解附近走得更稳。这个属于进阶操作,第一次训练不建议过度调参,直接跑通再建立基线。基线模型的含义很重要:先有一个能work的版本,后续所有改动才有对比参照。

3.3 评估指标别选错

这步是新手最容易掉坑的地方。很多人习惯用准确率(accuracy)衡量一切模型,但现实场景里准确率经常会骗人。假设你的任务是识别10000条评论里的100条恶意评论,一个什么都不干的模型,只要把所有样本都判为正常,准确率就已经是99%。这时候如果只看准确率,你会以为模型很棒,实际上它连一条恶意评论都没抓住。

所以分类任务至少要打开混淆矩阵,看看真正例、假正例、真负例、假负例的分布,再根据业务场景决定看哪个指标。以垃圾短信拦截为例:如果漏掉真正的垃圾短信代价高,就要重点看召回率;如果拦截太狠导致大量正常短信被误伤,就要优先保证精确率。现实业务里很少有完美的平衡点,通常都是根据成本权衡。AI工程里没有“最好的指标”,只有“匹配业务目标的指标”。做评估时还要把预测错误的样本捞出来逐个看,分析错误模式,这些人工分析往往是下一轮迭代的重要素材。

3.4 过拟合:从曲线判断模型状态

训练过程中一定要同时盯两条曲线:训练集上的loss和验证集上的loss。理想情况是两者同步下降并趋向平稳;如果训练loss还在降、验证loss已经开始回头,那就是典型的过拟合信号,模型开始背诵训练数据了。

解决过拟合有一个优先级:先想办法增加数据或做数据增强,再考虑正则化,最后才考虑模型简化。为什么这个顺序很重要?因为增加数据是在扩大模型见过的真实世界,最直接有效;正则化比如Dropout、权重衰减,本质是限制模型复杂度,多多少少会牺牲训练集表现;模型简化更得慎重,你本来就是因为业务复杂才选了强模型,贸然简化很容易变成欠拟合。早期训练时可以用early stopping,当验证指标连续多个epoch没有提升就停止训练,这招能省大量时间,也比较稳妥。

4. 工程化落地:模型上线只是起点

4.1 在线推理与批处理怎么选

模型训练完成,真正的工程工作才进行到一半。部署形态要结合业务场景、请求量、延迟要求来定。在线推理适合需要实时响应的场景,比如搜索排序、在线风控;批处理适合离线计算场景,比如每天凌晨跑一遍全量数据更新标签库。

两者的技术路径差别很大。在线推理对模型体积、计算速度要求高,通常要做模型量化、部署成API服务,并对下游容错有明确设计;批处理则更关注吞吐量和稳定性,可以分片跑、错峰跑,甚至可以用多机并行。不少项目第一步只做批处理,每天生成结果表就已经满足业务需求,等量大了再升级到在线服务。所以我的建议很实际:不用一上来就上K8s、GPU集群,先想清楚业务到底需要哪种形态,用最简方案先跑起来。

4.2 从模型到API:用FastAPI做一个最小服务

如果确实需要在线推理,我会推荐从FastAPI入手。它轻量、写法直观、性能足够,一个最简的预测服务可以写成这样:

from fastapi import FastAPI from pydantic import BaseModel import torch app = FastAPI() model = torch.load("model.pt", map_location="cpu") model.eval() class Item(BaseModel): text: str @app.post("/predict") def predict(item: Item): inputs = tokenizer(item.text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): logits = model(**inputs).logits prob = torch.softmax(logits, dim=-1) return {"probabilities": prob.tolist()}

这段代码看起来简单,但有几个工程细节我特别想提醒。第一,模型加载后一定要加model.eval(),否则训练模式下的Dropout依然生效,预测结果会不稳定,同一个输入可能每次输出不一样。第二,推理过程包上torch.no_grad(),不追踪梯度,内存占用和耗时都会降低。第三,输入长度要提前定好截断上限,max_length=128这个值是根据业务文本长度统计出来的,不是随便拍的。网上大量demo代码把这些细节全隐掉了,本地跑着没事,一上真实流量就露馅。

4.3 性能与成本:推理优化的几个方向

模型部署完之后,用户抱怨“慢”,你要先搞清楚慢在哪。是模型计算慢,还是网络传输慢,还是数据预处理慢?拿profiler看每个环节的耗时分布,再针对性优化,不要一上来就换模型架构。模型推理本身的优化,常用手段有量化、减少batch、请求聚合、模型蒸馏。

每种优化方案都有代价。比如把模型从FP32压缩到FP16或INT8,推理速度会变快、显存占用明显下降,但精度可能损失一点。具体损失多少,要用验证集实测,不能拍脑袋。我习惯用对比表来记录不同方案的效果,像下面这样:

方案显存占用单次推理耗时精度对比
FP32原版基准基准基准
FP16半精度约为原版一半明显提升通常无损或轻微损失
INT8量化进一步降低大幅提升需要实测确认

做量化时还有个小坑:不是所有模型都适合直接量化,特别是训练时用了某些特殊算子的模型,量化后可能掉点严重。所以每次量化前后都要跑一遍评估集,把关键指标记录下来。如果INT8掉点太多,可以试试FP16加batch聚合的组合,往往能在精度和吞吐之间找到更合适的平衡点。

4.4 评估监控与反馈闭环

模型上线后,评估还没有结束。离线评估表现再优秀,线上数据分布一旦漂移,效果会很快退化。所以监控体系至少要覆盖四个方面:请求量、平均响应时间、模型预测结果分布、核心效果指标的抽样评估。数据侧要监控特征缺失率、取值分布变化、有没有出现新类别。这些异常往往是模型性能下降之前的先兆。

另一个容易被忽视的环节是反馈闭环。模型预测错误的case有没有渠道收集回来?用户行为能不能间接反映效果?做推荐系统时,用户点击率就是天然的反馈信号;做内容审核时,人工复审的标注结果就是最重要的迭代数据。没有反馈闭环的系统,相当于模型上线后就开始自然腐坏。必须设计好定期更新和重新训练的节奏,让数据和模型都保持活跃。

5. 从个人项目到团队协作:工程习惯比模型更重要

5.1 实验追踪:没有记录的调参等于白干

做AI工程时间长了,最痛恨的事就是这个效果我以前跑出来过,但记不得当时用什么参数了。实验记录一旦缺失,整个项目就像没存过档的游戏,随时可能从头再来。所以从第一次训练开始,就要养成记录习惯。

不需要一开始就上多复杂的实验管理系统。最简单的方式是维护一张实验记录表,至少包括:实验时间、模型结构、训练数据版本、超参数、验证指标、备注。项目复杂以后,再迁到MLflow或Weights & Biases这类平台上。核心目标是每一个实验都能被追溯回完整的参数和历史环境。有了这个参照系,调参才不是瞎试,而是在对比里找规律。

5.2 模型和数据的版本管理

代码用git管理,模型和数据同样需要版本管理。这是很多团队从个人项目迈向协作时最不适应的点。模型权重文件动辄几百MB甚至几GB,不适合放git,更好的做法是放到对象存储或专业模型仓库,同时在git里记录模型文件的版本ID,保证代码和模型能一一对应。

数据版本的管理思路类似。训练集和测试集每次调整都可能影响指标,要在实验记录里写明数据来源。我曾经就吃过这个亏:调整了一个清洗逻辑,旧数据被覆盖,结果指标下降,排查了半天以为是模型问题,最后发现是对比错了数据版本。数据版本管理可以用DVC这类工具来追踪,它的底层实现其实就是管理元数据和文件引用,但能让团队避免“我用的数据和你用的数据根本不一样”这类混乱。

5.3 可复现性:固定随机种子和依赖版本

可复现性是AI工程的隐形地基。深度学习训练充满了随机性,参数初始化、数据打乱顺序、Dropout、GPU并行时的通信顺序都可能引入不确定性。要让实验基本可复现,首先要固定随机种子:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)

除了随机种子,依赖库版本也要锁住。今天装的新版torch和上个月的旧版torch,即便只是小版本差异,也可能让结果浮动。所以项目里必须维护requirements.txt或pyproject.toml,把环境锁定。真正严谨的工程还会记录容器镜像ID或操作系统信息,确保换台机器结果依然一致。别小看这些琐事,它们能让一个团队节省大量“对不上结果”的争吵时间。

6. 常见问题与排查技巧实录

6.1 训练时loss不降或直接NaN

先说loss不降。常见原因有三个:数据和标签没有正确对齐、特征尺度差异悬殊、优化器或学习率设置不合理。我的排查顺序是:先检查数据加载代码有没有把样本和标签对错位,再画特征分布看有没有异常离群点,最后把学习率调小一个数量级试一下。大多数情况下,loss完全不降是数据链路出了问题,模型本身反而挺清白。

而loss变成NaN,基本是数值计算已经失控。最常见的原因是学习率太大导致梯度爆炸,或者输入数据里带着NaN/Inf,又或者损失函数里出现除零。排查也按这个顺序来:先检查输入数据是否干净,把学习率调小一个数量级,再检查损失函数实现有没有边界漏洞。我踩过最蠢的一个坑是手写多分类损失时忘了在log真数前加一个很小的epsilon,遇到某个类概率恰好为0的样本,loss当场变成NaN,程序直接崩。这类问题真得自己踩一次,印象才深刻。

6.2 数据泄漏:完美的评估结果是假象

数据泄漏是AI工程里最隐蔽也最致命的问题之一。表面上模型在测试集上效果拔群,上线后立刻打回原形,这时十有八九是评估过程泄漏了未来信息。最简单的例子:按时间序列发生的数据,直接随机划分训练集和测试集,没有按时间边界切分,测试集里就会出现早于训练集发生的数据,相当于模型已经提前看过了未来。

文本场景里也容易踩这个坑。比如预测用户是否投诉,如果特征里包含了“客服是否已经介入处理”这种事后变量,模型在离线评估时表现会出奇地好,但真实场景里根本没这个信息。所以做特征时要对每个变量问一句:这个值在预测时刻能拿到吗?如果答案是拿不到,就不能进特征。很多时间序列项目做不好,根子就在这里。

6.3 线上效果和离线评估差距很大

这类问题我遇到很多次,原因通常集中在几个方面:训练数据和线上数据的特征加工方式不一致、线上数据分布发生了变化、请求延迟要求导致输入被截断、部署的模型版本不对。排查时先做数据一致性检查,把线上真实请求复现成离线样本,再跑一遍模型,比较输出差异。我见过不少团队靠“线上日志复现”这个动作,直接定位到特征字段错位或缺失的问题,省掉了大把猜谜时间。

这里有个很实用的建议:在服务端把模型预测时的输入特征哈希值和模型版本号一起记录下来,存进日志。一旦线上出问题,可以随时用真实请求回放排查,不用靠猜。这个习惯越早养成越好,后面能省出巨大时间成本,尤其是当你需要判断新训练模型能不能上线时,这套日志就是最佳准绳。

6.4 预算有限怎么做AI工程

最后聊一聊资源受限的环境。不是每个团队都有充裕的GPU预算,但这不代表做不了AI工程。几个切实可行的思路:尽量使用开源预训练模型,只在少量业务数据上做微调,不要什么事都从头训练;能用量化提升吞吐就尽量量化;能走批处理就不要硬扛实时请求。如果业务场景复杂,还可以用模型级联,先让小模型跑第一轮筛选,把简单样本直接处理掉,只把最难判定的少数样本交给大模型。这样平均推理成本能大幅下降,效果损失也不大。

按这条链路走下来,AI工程从零开始不再是一个含糊的口号,而是一张清晰的行动地图。我个人最大的体会是:当你完整地亲手把一个小型AI系统从数据清洗一路做到部署监控之后,再回头学大模型微调、Agent编排,甚至去读最新论文,会发现不过是这张地图上某个环节的升级。地基稳了,学什么都快。

最后再分享一个小技巧:每个项目启动时,先花半天时间把“数据怎么流动、模型怎么更新、结果怎么反馈”画成一张架构草图,手绘在纸上都行。别小看这一步,它能在设计阶段就帮你暴露很多环节之间的断点,省掉后续大量返工。AI工程这条路,从来不是比谁会调参,而是比谁少踩坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进