你有没有过这种体验:教程里的模型都能跑通,一到自己手里的数据就全部失效;训练时精度不错,上线后一天不如一天;调参全靠猜,因为根本不知道哪一步出了问题。如果有,说明你缺的不是算法知识,而是完整的AI工程视角。ai-engineering-from-scratch这条路,正好是用来补上这一课的。
我先把话说明白:这里的“从零”不是让你从数学公式推导开始,也不是让你从零重写一遍深度学习框架。它的核心是让你亲手把一个AI项目从原始数据一路推到可上线、可维护、可复现的状态。这是一个全链路的训练过程,涉及数据、建模、评估、部署、监控,每一层都要自己走一遍,并在过程中建立真正的工程判断力。
我实践下来最适合起步的是文本情感分类项目。文本数据不需要特殊硬件,普通笔记本就能跑,最容易把注意力放在工程环节而不是模型炫技上。这篇文章我就用自己做过的一个IMDB电影评论情感分类的小项目当例子,把从零到上线的完整过程、关键决策、以及踩过的坑全部捋一遍。你照着做一遍,后面再接触其他AI项目,心里会很有底。
1. 从零构建AI工程,到底在构建什么
1.1 为什么“从零”是绕不开的路
先接受一个现实:市面上的课程和书,绝大多数教的是“模型”。它们告诉你什么是Transformer、什么是梯度下降、为什么过拟合,但很少告诉你数据从哪里来、标注质量怎么保证、模型上线后如何知道它退化、服务挂了谁来发现。这就导致很多人在真正干活时崩溃。
我带过不少新人,算法题刷得飞起,但给一份没清洗过的真实数据,他们往往先花三天调模型,而不是花两小时看数据。训练集上分数漂亮,测试集上崩掉,还以为是模型不行。这种局面不是能力问题,是训练方式的问题——你从来没完整地做过一个AI工程,自然不知道问题出在哪一层。
“从零”这个路线就是为了治这个病。你不是在复现别人的实验,而是要从一个裸的数据集出发,自己决定清洗规则、特征方案、模型选择、验证方法、部署方式和监控指标。这个过程走完,你对“精度”这个概念的理解会发生根本变化:精度不是模型属性,而是整个链路的总和。
后面你会发现,很多问题根本不是模型能解决的,而是数据、评估、部署层面先出了问题。把这几层亲手过一遍,你就有了定位问题的直觉。
1.2 一条完整的能力地图
如果把ai-engineering压缩成一张地图,我会画成五段:数据获取与清洗、特征与标签设计、建模与实验、评估与可解释、部署与监控。
这五段是层层依赖的。数据没搞对,后面所有模型实验都是在自我欺骗;评估方式不严谨,上线就翻车;部署不稳定,模型再准用户也感受不到。任何一段缺掉,整体就会很拧巴。
我把每一段的核心任务和验证方式列一下:
| 阶段 | 核心任务 | 验证方法 |
|---|---|---|
| 数据获取与清洗 | 拿到原始数据,去除噪声、补全缺失、统一格式 | 数据质量报告,抽样人工检查 |
| 特征与标签设计 | 把原始信息变成模型能理解的输入,定义清楚预测目标 | 特征覆盖率、标签一致性人工抽检 |
| 建模与实验 | 跑通基线,逐步迭代模型结构和参数 | 独立测试集指标,多次实验方差 |
| 评估与可解释 | 多指标看结果,理解模型为什么这么判 | 混淆矩阵、错误样本分析、置信度分布 |
| 部署与监控 | 把模型变成服务,持续观察线上指标 | 压测报告、接口验收、上线后监控看板 |
你不需要每一段都上很重的工具,但每一段都得有可验证的产出。我后面的例子,就是沿着这张地图来的。
1.3 什么时候别从零
任何经验都有适用边界,我不想把“从零”讲成信仰。有些时候你真的不该从零。
如果目标任务属于通用能力,比如OCR、语音识别、通用物体检测,而且你的数据和人家预训练数据高度重合,那你应该先去调用现成开源模型或接口,跑通业务再说造轮子的事。用成熟能力做POC,两天就能验证想法,而不是花两个月死在细节里。
如果团队已经有统一的ML平台、特征平台、部署流水线,你也不需要从零搭基础设施。这时候“从零”主要指算法实验部分的从零,不要把已经存在的东西再重复造一遍。
那什么时候值得从零?我给你一个判断标准:当你的数据有自己的分布、业务有自己的定义、评估有自己的标准,而这些和公开教程、现成API都不一样,并且你还需要长期迭代的时候,从零走一遍就非常值。因为只有自己掌控每一层,你才知道后续该改哪里。
我的建议是:哪怕目标是用现成工具,也要花时间完整走一个从零的最小项目。它治的不是技术深度,而是工程直觉。
2. 核心思路拆解:工程分层与设计逻辑
2.1 把项目拆成四层,每一层只关心一件事
做AI工程最忌讳的是把所有事情混成一锅粥。我的习惯是拆成四层:数据层、实验层、交付层、监控层。
数据层的职责是“把世界变成样本”。原始文本、日志、图片、数据库记录,在这个层都要变成结构化、可建模的样本集。这层最容易被看轻,但它决定了模型能力上限。一句话,垃圾进垃圾出。
实验层的职责是“在样本上找规律”。包括划分训练集、验证集、测试集,跑不同的模型,比较指标,迭代改进。这里最需要注意的是验证方法要能和上线后的真实场景对齐。
交付层的职责是“让模型可被调用”。可能是REST API,可能是批处理脚本,也可能是嵌入到App里的SDK。关键是性能、稳定性、可回滚。
监控层的职责是“让模型活着”。上线不是终点,数据会变、业务会变、用户行为会变。没有监控,模型死掉你都不知道。
每一层之间要有明确的接口。数据层输出样本集和字段说明;实验层输出模型文件和评估报告;交付层输出服务地址和运维手册;监控层输出告警规则和看板。
这种分层的价值在排障时特别明显。服务变慢了,你不会去怀疑模型结构,而是先查交付层;指标掉得厉害,你不会去改服务器配置,而是先看数据漂移。问题定位快,通常就是因为边界清晰。
2.2 设计数据方案:模型是数据的影子
这句话我反复讲:模型是数据的影子。你在模型迭代上耗费的时间,往往与你在数据上偷懒的时间成正比。
做数据方案,有几个关键检查项。
第一,标签定义必须一致。文本情感分类里,一条“电影拍得不错,但剪辑太乱”到底是正面还是负面?如果没有明确的规则,标注员会凭感觉标,模型就会学到互相矛盾的信号。所以动手前必须把标签规则写成文档,最好带上正例和反例。
第二,样本覆盖要尽量贴近真实上线场景。训练数据如果全是标准书面语,上线遇到口语缩写、emoji、错别字,模型就懵了。宁可引入一些“脏”的真实样本,让模型见过,也不能只在干净数据里养尊处优。
第三,划分数据一定要警惕数据泄漏。时序数据不能随机切分,否则你用未来信息预测过去,评估结果虚高得离谱。别人跑出来F1等于0.95,你线上只有0.6,大概率就是泄漏问题。
在动手训练之前,我会强制自己做三件事:写一份字段说明文档,包含每个字段的含义、缺失率、示例;做一次标签抽检,至少人工看200条随机样本的一致性;记录数据版本和切分种子。这些听起来麻烦,但后面你会感谢自己。
2.3 实验设计:先定评估方法,再谈模型
很多人拿到数据就急着训练,这其实把顺序搞反了。第一步应该是确定“什么叫做好”。
拿情感分类来说,如果正样本占90%,你无脑预测正样本就有90%的准确率,但业务上完全没用。这时候要看F1或者AUC,更准确的做法是看业务最关心的那部分样本的召回率。
我通常会建立一个评估基线:随机预测或频繁类别预测能拿到多少分。这个分数是所有模型的天花板起点,如果模型连基线都跑不过,就别浪费时间调参了。
实验阶段还要想清楚超参数的搜索方式。别一上来就搞贝叶斯搜索,先把一两个最关键的超参数网格跑一遍,比如学习率、batch size,记录全表,再决定下一步。我看到的更多情况是:超参没搜几个,反而在数据增强和模型结构上反复横跳,最后连哪个改动有效都不知道。
比较模型时,最好固定一个实验管理习惯:每次实验记录config、代码版本、数据版本、指标、随机种子。没有这些记录,过两周你再回头看实验结果,只会剩下“当时感觉还行”这种模糊记忆,毫无价值。
2.4 何时停止:成本与收益的边界
工程里最难的其实是“知道该停”。模型精度从0.88提到0.89可能花一周,但业务根本感知不到;部署稳定性从99%提到99.9%,用户体感却很明显。
所以我会在项目一开始就写下一个“上线最低标准”:比如F1达到0.85、线上单次预测P95小于300毫秒、回滚流程验证通过。达到标准后,剩下的时间应该投到数据监控、容灾、文档交接这些工程环节上,而不是继续卷模型指标。
这个思路需要顶住诱惑。调参太容易上瘾,每跑一次都有新反馈,而写监控、写文档没有即时奖励,很多人就会拖延。但真正的工程判断力,恰恰体现在你主动选择在什么地方投入时间。
3. 手把手走一遍:文本情感分类的从零工程
这一章我以IMDB电影评论情感分类为例,把你从环境到部署的完整过程走一遍。选这个数据是因为它公开、干净、且不涉及复杂业务,最适合把工程环节看清楚。
3.1 准备环境与项目骨架
我建议用独立的Python环境,省得依赖冲突把自己逼疯。我用conda,你也可以用venv,但从零项目我推荐conda,因为深度学习相关库的依赖管理更方便,不同平台行为也一致。
conda create -n ai-eng python=3.10 -y conda activate ai-eng pip install scikit-learn pandas numpy pip install fastapi uvicorn项目目录我会这样组织:
project/ ├── data/ # 原始数据与中间数据 ├── notebooks/ # 探索性分析 ├── src/ # 训练与处理代码 ├── models/ # 产出的模型文件 ├── tests/ # 关键逻辑测试 └── deploy/ # 部署配置与服务代码这个结构的价值是长期可维护。你把数据、代码、模型、部署分开,做版本回溯时就清晰。不要把所有脚本堆在一个文件夹里,那种项目三个月后连作者自己都看不懂。
3.2 数据清洗与标签设计
IMDB原始数据包含评论文本和情感标签。第一步不是建模,而是做探索性分析。我会用pandas先看缺失值、文本长度分布、标签分布。
import pandas as pd df = pd.read_csv("data/imdb_reviews.csv") print(df.isnull().sum()) # 检查缺失 print(df["sentiment"].value_counts()) # 标签分布 df["review_len"] = df["review"].str.len() print(df["review_len"].describe())常见脏数据包括:HTML标签、多余空白、大小写不一致。这些不一定都要处理得干干净净,关键是你得知道它们存在,并且训练和预测时用同一套处理逻辑。我后面会在代码里把清洗函数单独提出来,保证训练和推理共用同一个函数,而不是各写一份。这是最容易被忽略却最容易出事故的点。
标签设计方面,IMDB本身就有pos、neg标签。但假设是你自己的数据集,一定要先抽检200条,确认标签是否可理解、是否有歧义。一个标签只要存在10%的不确定性,模型效果就会大打折扣。
3.3 模型实验:从基线到增强
第一步永远先做简单模型。我用TF-IDF加上LogisticRegression,这个组合训练快、可解释、也能给出一个不错的起点。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline train_texts, test_texts, train_labels, test_labels = train_test_split( df["review"], df["sentiment"], test_size=0.2, random_state=42 ) pipe = Pipeline([ ("tfidf", TfidfVectorizer(max_features=5000, ngram_range=(1, 2))), ("clf", LogisticRegression(max_iter=1000)), ]) pipe.fit(train_texts, train_labels) print(pipe.score(test_texts, test_labels))这个基线跑出来,通常准确率在85%上下。这时候你已经有资格去谈下一步——是否需要用更强的模型。如果在业务评测标准下87%已经够用,那就先推进部署;如果不够,再上深度学习。
如果你要上深度学习模型,我的建议是先别贪大。从一个embedding层加一个双向LSTM开始,参数量小,训练快,能让你快速体验深度模型和传统模型的差距在哪里。一上来就上大规模预训练模型,调度复杂度、显存压力、复现难度都会指数级上升,那不是现阶段最重要的。
训练神经网络的代码我用PyTorch写。要点有几个:固定随机种子、给数据做批处理、保存最佳模型。
import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset seed = 42 torch.manual_seed(seed) class TextDataset(Dataset): def __init__(self, texts, labels, word2idx, max_len=128): self.texts = texts self.labels = labels self.word2idx = word2idx self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): tokens = [self.word2idx.get(w, 1) for w in self.texts[idx].split()] tokens = tokens[:self.max_len] + [0] * max(0, self.max_len - len(tokens)) return torch.tensor(tokens, dtype=torch.long), torch.tensor(self.labels[idx], dtype=torch.float)这里有个容易踩的坑:padding长度和词表大小会影响最终效果,max_len建议根据训练集长度分布来定,而不是拍脑袋。我一般取P90的句子长度作为max_len,这样信息损失最小又不会太浪费显存。
训练循环的检查点保存我习惯这样写:
best_f1 = 0 for epoch in range(epochs): model.train() for batch in train_loader: ... val_f1 = evaluate(model, val_loader) if val_f1 > best_f1: torch.save(model.state_dict(), "models/best_model.pt") best_f1 = val_f1只按验证集最优保存,别在最后一个epoch保存。不这么干,后期过拟合时你会存下一个垃圾模型。
3.4 封装与部署:让模型变成一个服务
模型训练完,离交付还有一段路。我用FastAPI写一个简单的预测接口,而不是用Flask。原因很直接:FastAPI自带请求校验、异步支持和OpenAPI文档,工程体验好很多,几乎不需要额外配置。
from fastapi import FastAPI from pydantic import BaseModel import joblib app = FastAPI() model = joblib.load("models/logistic_pipeline.joblib") class Review(BaseModel): text: str def clean_text(text: str) -> str: # 与训练阶段完全一致的清洗逻辑 return " ".join(text.lower().split()) @app.post("/predict") async def predict(review: Review): cleaned = clean_text(review.text) prob = model.predict_proba([cleaned])[0][1] return {"score": float(prob), "is_positive": bool(prob > 0.5)}这一步的关键不是代码本身,而是“清洗逻辑一致性”。很多线上效果崩掉的项目,原因就是训练用的清洗函数和部署用的清洗函数写了两遍,后来改了一个忘了另一个。所以我在项目里会把清洗逻辑放到一个共享模块中,训练导入一次,服务导入一次,绝不再复制粘贴。
部署时我推荐用Docker打包,这样环境差异、依赖版本、启动命令都固化下来,换机器不会有惊喜。
FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ src/ COPY models/ models/ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]3.5 效果验收与回归检查
上线前的验收,不是看一眼测试集分数就完事。我会做三件事。
第一,跑一遍完整测试集,输出分类报告和混淆矩阵。只看准确率掩盖的问题,混淆矩阵会告诉你:模型是不是把某类样本全判成了另一类。
第二,抽样看错误预测。翻50条预测错误的样本,理解它们为什么错。大概率你会发现规则性问题,比如某些短语反复触发误判,这时候与其改模型,不如检查特征或数据。
第三,对接口并发做一个简单的压测,确认在预期并发下P95延迟满足要求。别等到上线后用户反馈“转圈”,那会非常被动。
只有这三项都通过了,我才会把模型交付给下游。不要指望“先上线再看看”,在AI工程里,上线前少做的每一分钟检查,都会在上线后花一个小时来还。
4. 常见问题与排查技巧实录
4.1 训练集指标很好,测试集一塌糊涂
这是个老生常谈,但每次都会被不同的原因触发。除了最常见的过拟合,还要查两个地方:数据预处理是否泄漏,以及切分是否合理。
泄漏的典型例子是:全量数据上做了归一化或fit了词表,再切分训练测试集。这样测试集信息已经偷渡到模型里,指标虚高。判断泄漏有个土办法:把训练流程完全重放在一小块“最后被使用”的保留数据上,如果指标明显下降,多半有泄漏。
另一个常见原因是类别分布和文本风格在时间段上变化。比如评论里的流行用语半年一换,你用半年前的数据训练,测试集里全是最近三个月的新表达,分数自然难看。这种问题要做时间切分,而不是随机切分。
4.2 数据泄漏与分布偏移,怎么提前发现
我见过太多人把分布偏移当成模型调参问题,方向从一开始就错了。判断方法其实不复杂:对比训练集和最近线上数据的特征分布。
文本类项目里,最简单的是看词频、句长、缺失率。比如线上评论突然出现大量英文夹杂,而训练集几乎没有,模型当然不会用。图表类项目可以看像素分布、颜色直方图。有异常再去查数据源,通常很快就能定位。
我现在的习惯是:项目一上线就建立特征分布快照,每周自动对比一次,偏差超过阈值就发告警。这套东西早期手动做就很有效,后面再自动化。
4.3 依赖版本变动导致模型复现失败
理论上模型训练完就定住了,但实操中经常踩这类坑:换个环境加载模型报错、依赖库版本升级后推理结果不一致。
第一个建议:训练完马上把关键库版本写入requirements,包括Python版本、pandas、numpy、sklearn、torch这些。你甚至可以单独跑一个“冻结环境”的验证脚本,在干净环境里加载模型,确保能复现。
第二个建议:对于跨环境部署,模型格式要用兼容性好的。sklearn模型用joblib没问题,但如果宿主机环境差异大,我建议用ONNX导出,把模型结构、权重都固化。PyTorch模型则优先考虑TorchScript导出,比裸state_dict更省心。
4.4 显存不足与训练不稳定怎么办
小项目也会遇到显存不足,常见原因是batch size太大、句子长度padding到固定长度浪费太多。先把batch size砍半,通常能解决。如果还要更省,就把max_len往下调,或者用动态padding,一个batch内按这个batch的最大长度来padding。
训练不稳定则要先看学习率。我习惯用warmup加线性衰减,而不是从头到尾固定一个学习率。别小看这一步,很多模型loss震荡,就是因为开头学习率太大,模型在最优点附近反复横跳。
如果真的遇到loss变成NaN,先查输入数据有没有NaN或Inf,再查学习率是不是太高。80%的情况逃不过这两个原因。
4.5 问题速查表
我把以上问题和几个老生常谈但永远会再犯的问题整理成一张速查表,方便你贴在手边:
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| 测试集分数虚高 | 预处理泄漏 | 检查fit是否在split之前发生 |
| 线上与训练差异大 | 数据分布偏移 | 对比特征分布快照 |
| 换环境推理失败 | 版本不一致 | 固定依赖版本,用ONNX/TorchScript |
| 显存溢出 | batch大或padding冗长 | 减小batch,动态padding |
| Loss为NaN | 数据含NaN或学习率过高 | 检查输入,降低学习率 |
| 训练震荡不收敛 | 学习率策略不合适 | 加warmup和衰减 |
这张表不是理论,每一行都是我和同事真实遇到过的场景。你能提前知道这些坑,比多学十个模型结构有用得多。
5. 我的工具链选型与参数经验
5.1 环境管理:conda 还是 venv
在从零项目里我推荐conda。不是因为它万能,而是因为它对Python版本和底层库的管理相对省心。深度学习生态里经常有“这个库只支持Python 3.8”或者“那个库在3.10下行为不同”的情况,conda可以快速切换隔离环境,不干扰系统Python。
不过你要注意,conda环境一旦创建好,就把环境导出结果写入项目文档,保证别人能复现。我一般固定Python 3.10,目前主流AI库的兼容性都覆盖到了,旧一点的项目也不会有太大摩擦。
5.2 实验跟踪:用最简单的方案坚持下来
实验跟踪工具很多,MLflow、Weights & Biases都很好。但我发现大多数人最后不用这些工具,不是工具不好,而是维护成本超过耐心。所以我建议从小处入手:每次实验写一个config.json,记录参数、数据版本、代码commit号、结果指标,全部丢进experiments目录。
{ "data_version": "v20240701", "model": "logistic_tfidf", "params": {"max_features": 5000, "ngram_range": [1, 2]}, "metrics": {"test_accuracy": 0.8721, "test_f1": 0.8703}, "git_commit": "a1b2c3d", "seed": 42 }这个习惯坚持下来,你会建立对实验节奏的掌控感。中途想放弃复杂工具,说明还不是时候,自己先把最朴素的方案用熟。
5.3 模型格式与部署方案
部署这块,我推荐以下组合:常规机器学习模型用joblib保存;深度模型用TorchScript或ONNX导出;服务框架优先FastAPI;容器化用Docker。
单一模型用REST API就好。如果以后并发量上来,再考虑网关、多副本、以及专门的推理服务框架,但那是后面的故事。
一个小技巧:服务里模型加载可以放在模块加载时,而不是每个请求里;推理前再做一次输入类型校验。用FastAPI的lifespan事件来加载模型,确保启动时只加载一次,这个习惯能省不少不必要的IO开销。
5.4 一套可复用的配置清单
我在不同项目里反复用到同一套基础配置,分享给你:
| 项目 | 推荐配置 | 原因 |
|---|---|---|
| Python版本 | 3.10 | 兼容性好,深度学习生态支持全 |
| 依赖管理 | conda + requirements.txt | 兼顾快速环境创建和版本固化 |
| 文本特征 | TF-IDF 5000维 + 1-2 gram | 基线稳定,训练快 |
| 深度学习基础 | Embedding 128维 + BiLSTM | 参数量小,容易出基线 |
| 服务框架 | FastAPI + Uvicorn | 自带校验与OpenAPI文档 |
| 容器 | python:3.10-slim | 镜像小,安全 |
| 实验记录 | config.json + metrics.json | 零依赖,可持续 |
这套配置不是唯一答案,但它能保证你在一个没有历史包袱的从零项目里快速跑通并保持干净。
6. 再往前走半步:上线之后的工程化打磨
做完第一个从零项目,我的建议是不要急着开下一个新技术,先回头补补工程化的几块短板。就我的经验来说,这几块短板的回报率比再学一个模型结构高得多。
第一块是数据快照与漂移监测。上线第一天就把训练数据的特征分布存下来。之后每天做一次线上数据特征对比,差得多了就发告警。这个动作可以手动做,也可以后面自动化。我见过太多模型上线三个月后才被发现已经失效,原因就是没有这个对照。
第二块是模型版本与回滚。线上模型保存版本号,接口返回中最好带上模型版本。这样用户反馈不好时,你可以快速知道是哪个版本的模型在服务,回滚也有据可查。
第三块是文档与合作节奏。AI项目不是一个人的事,要和标注团队、后端、产品对齐口径。我习惯把数据说明、标签规则、模型限制都写进一份简短的README,项目交接时这份文档比所有代码都值钱。
我个人的体会是:ai-engineering-from-scratch最珍贵的产出不是那个模型文件,而是你建立起来的“全链路敏感度”。遇到问题你会本能地先分层定位,而不是一把梭调参。有了这种感觉,后面再接触任何新工具、新框架,你都能快速找到它在你链路里的位置。
最后再分享一个小技巧:每次项目结束后,花半天时间把踩过的坑整理成一篇简短的复盘,记录触发条件、排查过程、最终方案。这些复盘才是你自己最可靠的专家库,比任何现成教程都贴近你的真实场景。