如果你也想走“从零开始”学AI工程这条路,先别急着收藏一堆论文。ai-engineering-from-scratch这个词我盯了很久,它看起来像一个开源仓库名,实际上更像一种学习思路:不靠调用现成服务“假装会了”,而是把数据、训练、部署、监控这条AI工程链路亲手搭一遍。很多新手陷在“看教程都会,一动手就废”的循环里,缺的不是模型知识,而是缺一个能跑通的完整骨架。这篇文章是我自己从零开始踩过来的路线总结,会把最容易卡住的环节拆开讲,适合刚入门、想转行,或者已经在业务里被AI项目坑过的朋友参考。
我见过太多人在模型训练阶段烧掉了90%的精力,最后却挂在部署和迭代上。AI工程不是单指训练一个模型,它是一条从数据到业务价值的流水线。这里没有题库,也没有标准答案,但有一些通用的架构思路和实操方法,能让你少走几个月弯路。下面我按自己的实操路径,把从环境准备到模型上线的完整闭环讲一遍。
1. 先想清楚:AI工程到底是什么
1.1 这个标题背后,其实是一条完整工程链路
很多初学者把AI工程等同于写模型代码,这是第一个误区。真实的AI工程至少包含四个部分:数据管理、模型训练、模型部署、持续迭代。举个例子,开一家饭店不只是请个厨师炒菜,你要解决食材供应链、厨房动线、出菜节奏、顾客反馈处理。AI项目也一样,模型只是“炒菜”那一步,前面还要处理数据获取、清洗、特征工程,后面还要做接口封装、性能监控、定期重训。
所以ai-engineering-from-scratch的真正含义,是把这条链路亲手搭起来,而不是只跑通一个notebook。你需要理解每一步为什么存在,以及它们之间如何衔接。我在第一次独立负责AI项目时,就是因为忽略了“数据版本”和“模型版本”的对应关系,导致模型怎么重训都无法复现之前的效果。那次经历让我明白:AI工程的地基不是算法,是流程。
1.2 从零开始不等于从数学开始,先跑通最小闭环
市面上的“从零开始学AI”教程,往往从线性代数、概率论开始,然后讲矩阵求导。这套内容对科研人员有用,但对绝大多数做工程的人来说,它直接把热情浇灭了。我的建议恰恰相反:先跑通一个最小闭环,再回头补数学。
什么叫最小闭环?给你一个数据集,你写代码完成“读数据 -> 训练模型 -> 评估效果 -> 保存模型”这四步,哪怕模型非常简单,也算闭环。等到你发现模型效果不好,再去查损失函数、梯度下降这些概念,你会带着真实问题去理解,效率完全不同。数学不是不需要,而是应该在“需要时”去学,而不是在“开始时”去啃。
1.3 什么人适合走这条路
如果你有基本的Python基础,能写函数、会用列表和字典,那就已经具备起步条件。做过数据分析、接触过pandas,或者写过简单脚本处理Excel,都有帮助。如果你是Java后端想转机器学习工程,也合适,但需要先把Python的数据处理生态熟悉起来。
这条路不适合谁?一点代码都不想写、只想拖拽式建模的人。我见过不少产品经理被鼓励“自己训练模型”,结果遇到一个数据格式问题就卡住一天。AI工程说到底是一门工程学科,动手能力绕不开。
2. 动手之前:环境、工具与第一个端到端模型
2.1 环境搭建的三个层次,别再纠结装哪个框架
这一步能劝退一半的人。我看到很多新手花一整天在安装PyTorch还是TensorFlow上反复横跳,最后还没开始就累了。其实环境搭建应该分三层看。
第一层是Python环境和基础库。建议直接用Python 3.10以上版本,创建虚拟环境,不要什么都装进全局环境里。我用venv或poetry隔离项目依赖,两个项目互不污染。最基础的依赖是numpy、pandas、scikit-learn、matplotlib、jupyterlab。
第二层是深度学习框架。如果你暂时做表格数据或者传统机器学习,scikit-learn就够用了,不需要急着上PyTorch。如果你想跑图像或文本模型,再装PyTorch。注意,显卡驱动、CUDA、cuDNN和PyTorch版本必须匹配,这一步最容易报错。如果你没有NVIDIA显卡,先别买设备,用在线Notebook环境或CPU版跑小模型完全足够。
第三层是依赖锁定。无论你用什么方式安装,最终都要有一个requirements.txt或pyproject.toml。这能保证你在三个月后还能跑起当初的项目。很多人“在自己电脑上跑得好好的,换台机器就跑不了”,95%是依赖版本不一致造成的。
2.2 第一个端到端示例:逻辑回归分类鸢尾花
我用一个入门例子说明最小闭环长什么样。鸢尾花数据集是经典的150条数据,三条类别,每条样本有四个特征。这个数据集没有缺失值,适合第一跑。
import pandas as pd from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report import joblib # 1. 读数据 iris = load_iris() X = pd.DataFrame(iris.data, columns=iris.feature_names) y = iris.target # 2. 划分训练测试集,固定随机种子 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 3. 训练模型 model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) # 4. 评估效果 y_pred = model.predict(X_test) print(classification_report(y_test, y_pred)) # 5. 保存模型 joblib.dump(model, "iris_model.pkl")这段代码看起来简单,但每一步都有讲究。stratify=y保证训练集和测试集里三个类别比例一致,避免因为样本分布不均导致测试结果失真。random_state=42固定随机过程,让每次跑出的结果一致,这对后续调参非常关键。max_iter=1000是逻辑回归默认迭代次数不够时的兜底,否则会报收敛警告。
初次跑通之后,你可以尝试加一个StandardScaler标准化特征,再用Pipeline封装起来。那一开始就不要用深度学习这类重武器,先把“数据、训练、评估、保存”这条链路记在肌肉记忆里。
2.3 从一开始就用git,不然一个月后你会崩溃
这部分属于老生常谈,但很多人就是不做。你至少要在项目目录下执行git init,每次完成一个阶段,提交一次代码。commit信息写成“训练完成基线模型”或“加入特征归一化”,保留过程痕迹。
我踩过的坑是,调参调了一整天,发现效果最好的模型是用某个临时脚本跑出来的,但那版脚本忘了保存。如果当时每改一个参数就commit一次,就不会出现“最强模型无法复现”的尴尬。记住:数据和模型文件不要直接塞进git仓库,大型文件用DVC或存到对象存储,代码库里只保留数据处理脚本和训练脚本。
3. 工程化要过的四道坎:数据、训练、调参、交付
3.1 数据管道:最常见的问题不是没数据,而是脏数据
有一句话我很认同:模型吃的是数据,但如果数据是垃圾,输出也是垃圾。从零开始做AI工程,你至少要亲手做一次数据清洗,才能理解为什么数据管道要单独设计。
数据清洗的常规步骤包括:处理缺失值、去重、处理异常值、统一字段格式、解决类别不平衡。这里有个经典误操作:看到缺失值就直接fillna(0)。我一开始也这么干,结果模型训练出来,线上预测时特征分布和训练分布完全不同,效果一塌糊涂。缺失值应该先判断它为什么缺失,再用均值、中位数、模型预测或单独一个“缺失标记”来处理,而不是一刀切填充。
另一个隐蔽的坑是数据泄漏。比如你用全部数据做了标准化,再划分训练集测试集,测试集的信息就已经通过均值、方差混进训练过程了。正确的做法是把标准化和标准化参数放进Pipeline,让GridSearchCV或train_test_split在交叉验证折叠内去拟合。这样做出的评估结果才可信。
3.2 训练脚本别写在notebook里,但可以从notebook起步
Jupyter Notebook适合做探索性分析和可视化,但它不适合当生产训练脚本。一个完整的训练脚本应该有几部分:配置、数据加载、模型定义、训练循环、评估、保存。我推荐的最低限度分开三个文件:
config.py:存放数据路径、模型参数、随机种子。train.py:完成读取数据、训练模型、保存产物。evaluate.py:加载模型,在测试集或新数据上评估效果。
这样做最大的好处是,你可以为每个配置跑一次训练,而不需要手动改动大量代码。比如你有个参数batch_size,直接在配置里改,脚本不用动。跑实验时把配置文件和结果一起记录,后面任何一次效果都能追溯到当时的设置。
用PyTorch的话,训练脚本里要显式声明torch.manual_seed(42),还要在DataLoader里设置shuffle=True时固定generator。这些细节看似不起眼,却是“实验结果能不能复现”的分水岭。
3.3 调参不是玄学,是一套可被执行的方法
我经常看到有人改一个学习率,跑一次,再改一个,再跑一次,完全靠手感。如果项目只有几十个样本,这可能还行,但一旦数据量变大,这种“玄学调参”就是浪费时间。系统化调参至少分三步。
第一步,固定一个合理的初始参数组合。我习惯先从默认参数开始,跑一遍确认代码和流程没问题。第二步,用随机搜索而不是网格搜索。参数组合多的时候,网格搜索会爆炸式增长,随机搜索在相同预算下能找到更好的组合。第三步,用交叉验证评估,不要用单一测试集分数来判断。
以学习率为例,我踩过的坑是刚一开始就设成1e-2,结果损失直接发散成NaN。后来我习惯从1e-3起步,如果loss不降,就降一个数量级再试。同时把每次训练的记录保存下来,画成曲线看趋势。TensorBoard是个好东西,但如果你不想额外装工具,直接保存loss列表画matplotlib图也够了。
3.4 交付不只是导出模型,还要把预处理打包带走
模型训练完成,你心里想着终于可以交付了。且慢。如果你在训练时单独做了标准化、缺失值填充、类别编码,那这些处理步骤必须跟着模型一起打包,否则线上接口拿到的只是一堆原始特征,模型却期望接收的是标准化后的特征。
解决办法就是把所有预处理步骤放进同一个Pipeline对象,然后直接保存整个Pipeline,而不是只保存模型本身。用上面鸢尾花的例子,改成这样:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipeline = Pipeline([ ("scaler", StandardScaler()), ("model", LogisticRegression(max_iter=1000)) ]) pipeline.fit(X_train, y_train) joblib.dump(pipeline, "iris_pipeline.pkl")这样推理时拿到原始输入,就可以直接pipeline.predict(...)。我见过太多事故,是开发把模型单独导出,另一人在接口里重新写了一遍预处理,结果有一个特征顺序对不上,线上预测全线漂移。把这些细节在交付前就设计好,能省掉后续无数排查时间。
4. 让模型真正落地:部署、监控与迭代
4.1 三种部署方式,先选最简单的那一种
模型部署没有想象中复杂,但也没那么简单。从零开始,优先选择其中一种最简单的方式:把模型用joblib或pickle保存,然后用FastAPI包一个HTTP接口。这种方式最容易理解,也能快速让业务方调用。
一个最小化的FastAPI推理服务长这样:
from fastapi import FastAPI from pydantic import BaseModel import joblib app = FastAPI() pipe = joblib.load("iris_pipeline.pkl") class InputData(BaseModel): sepal_length: float sepal_width: float petal_length: float petal_width: float @app.post("/predict") def predict(data: InputData): features = [[ data.sepal_length, data.sepal_width, data.petal_length, data.petal_width ]] pred = pipe.predict(features)[0] proba = pipe.predict_proba(features)[0].tolist() return {"prediction": int(pred), "probability": proba}这一步你可能会遇到一个坑:输入数据的字段顺序。Pipeline训练时的特征顺序是固定的,接口端必须严格按照同样顺序构造特征数组。我给接口设计输入结构时,会同时保存一份特征顺序清单,不靠开发人员“猜”。
比FastAPI更进一步的方案是使用MLflow或BentoML做模型管理,但我不建议一上来就学平台。先把一个模型部署出去,理解请求、响应、模型生命周期,再考虑平台能力也不迟。
4.2 上线后要盯的不只是准确率,还有数据漂移
模型部署上线只是开始,不是结束。真实世界里,用户行为、外部环境都在变,训练时的分布会慢慢和线上分布不一样。这就是“数据漂移”。如果你只盯着准确率,很可能在准确率已经下降两三个星期后才发现出问题。
一个简单的监控方案是记录线上请求的特征统计值。比如每天凌晨计算一次当天所有输入特征的平均值、标准差,跟训练集的特征统计值做对比。如果某个特征均值偏移超过阈值,就触发告警。用不到太复杂的平台,先用日志存请求,再用定时脚本做统计,就能覆盖大多数小项目。
还有输出监控:每天统计预测结果的类别分布和置信度均值。如果某天“召回率很高的那个类别”突然变少,说明线上数据分布很可能出了问题。先人工排查,再决定要不要重训。
4.3 从v0到v1:用最小闭环跑出迭代节奏
很多团队希望一步到位的完美模型,但正确的节奏是“先上线,再迭代”。第一个版本不需要复杂算法,线性回归或逻辑回归,甚至一个规则引擎都可以。目标是让数据管道跑通,让业务流程接受输出,让监控体系开始积累数据。
跑通v0之后,你记录下当前模型的问题,比如某个类别误判特别多、某个特征重要性异常。然后带着目标去做v1,加上新的特征、换更强的模型、重新调参。这个过程更像是滚雪球,每轮都基于真实反馈做决策。
迭代过程中,我强烈建议做一个“模型卡”表格,每个模型一行,记录数据来源、训练代码commit、参数设置、评估指标、上线时间、已知问题。不需要专业平台,用表格就行。这个习惯帮我规避了“哪个模型是最新的”这种日常混乱。
5. 常见问题与排查技巧实录
5.1 训练loss不降或变NaN,先按这张表检查
遇到loss值不降或直接变成NaN,不要急着改模型结构。按下面的顺序排查,90%的问题能定位。
| 现象 | 常见原因 | 快速检查方法 |
|---|---|---|
| loss一直很高不降 | 学习率太大或太小 | 打印前几个step的loss,从1e-3开始调 |
| loss直接为NaN | 梯度爆炸或输入含NaN | 检查数据里有无NaN,加梯度裁剪 |
| 训练集loss降,测试集loss升 | 过拟合 | 加入正则化,减小模型容量 |
| loss在某个值反复震荡 | 学习率过高或batch size太小 | 降低学习率,增大batch size |
| 训练集和测试集结果差很多 | 数据泄漏 | 检查是否在划分前做了全局标准化 |
有一次我的模型loss稳定在2.3附近怎么都降不下去,查了很久才发现是标签本身有错误:训练集里有将近5%的样本标签是反的。这类问题只有靠人工抽样检查数据才能发现,这恰恰说明数据质量在AI工程里占据的权重。
5.2 显存不足、内存爆掉,问题可能不在显存
做深度学习时,CUDA out of memory是高频报错。新手第一反应是换更大的显卡,但更可能的原因是代码结构问题。我踩过的坑包括:在循环里重复保存了Tensor的detach()副本,导致GPU显存不断堆积;或者DataLoader的num_workers开得太大,内存直接被占满。
正确的排查顺序是:先减小batch_size,再检查训练循环里有没有保存多余变量,最后考虑float16混合精度。如果显存仍然不够,就换一个参数量更小的模型。别让硬件限制影响了你的学习节奏。
5.3 训练准但线上不准,八成是预处理不一致
我在交付阶段反复强调过预处理一致性,因为这里翻车的人太多了。一个典型场景:训练时你用了StandardScaler,测试集评估效果不错;上线时新写了一段代码,把输入特征除以100再传给模型。由于标准化参数的偏差,预测结果变得乱七八糟。
如果你用了Pipeline,这种问题基本不会发生。如果因为历史原因必须单独写推理代码,就要在线上接口里加一个输入特征校验,比如判断均值、方差是否在合理范围内。如果发现某个特征的值域完全偏离训练分布,直接拒绝预测或返回告警,而不是给出一个没意义的结果。
5.4 代码和模型版本对不上,用这个命名习惯
我早期犯过的错误:训练了一个模型,文件名是model_final_v2.pkl,过了两天别人告诉我“线上用的是v1”,但两个文件都还在,根本分不清谁是谁。后来我定了一个最简单的规则:模型文件名包含日期和代码commit短哈希。
# 训练完模型后,用一条命令生成版本号 date +%Y%m%d_%H%M git rev-parse --short HEAD # 得到类似:20250412_1530_7a3f9c2 # 模型文件名:iris_model_20250412_1530_7a3f9c2.pkl同时在模型卡里记录对应的训练脚本路径和训练命令。这样任何人看到文件名,就能定位到是哪天、哪份代码、哪个commit产生的模型。这个习惯看起来简单,但对团队的协作效率提升是质的。
我个人在实际操作中的体会是,从零开始做AI工程,最难的其实不是某个算法原理,而是把数据、训练、交付、监控串成一条可持续运转的闭环。你只要亲手跑通一次完整的端到端项目,之后学任何新模型、新框架,都会有一个稳定的“架子”可以挂靠。最后再分享一个小技巧:每完成一个模型,强制自己写三行模型卡——数据来源、效果指标、已知缺陷。这个动作坚持下来,你会发现自己对AI工程的掌控感完全不一样。