有个读者问了我一个特别实在的问题:想做AI工程,是不是先把主流大模型的API都接一遍、把提示词模板背熟就够了。我说,你要是只想在朋友圈发个Demo,这么说没毛病;但要是想把这行当饭吃,API只是最外面一层壳,壳底下的引擎才是安身立命的根本。这也是我一直坚持用from-scratch方式带人入门的原因——不是让大家回到石器时代不用框架,而是先亲手造一台最简单的机器,弄清楚它的每一个零件,之后再拿起现成的框架,你才知道哪里容易坏、坏了怎么修。
这篇内容围绕ai-engineering-from-scratch这条路径展开:先讲清楚AI工程到底在解决什么问题,再给出从零起步需要补的三块地基;然后我会手写线性回归,带你完整走一遍梯度下降的推导与实现;接着聊聊从玩具模型到生产系统之间那些绕不开的工程环节;最后分享我在真实项目里踩过的五个坑。无论你是刚转行的新手,还是有两三年经验想系统补底子的工程师,这条路线都适用。
1. AI工程和调包、调参到底差在哪
1.1 三个相似但完全不同的角色
入行之前,我花了很长时间才分清楚算法研究、数据科学和AI工程这三个岗位的区别。简单来说:
| 维度 | 算法研究 | 数据科学 | AI工程 |
|---|---|---|---|
| 核心目标 | 提出新方法、发论文 | 分析数据、验证假设、产出洞察 | 把模型变成稳定运行的产品能力 |
| 交付物 | 新模型、新理论 | 实验报告、AB实验结论 | 线上服务、监控体系、可维护的管线 |
| 关心的问题 | 能不能收敛、精度有没有提升 | 业务有什么规律、指标为什么波动 | 延迟高不高、漂移有没有、挂了能不能快速恢复 |
AI工程这个角色,恰好坐在研究者和数据科学家的中间。研究者给你一个在干净数据集上跑出SOTA的模型,数据科学家告诉你有价值的业务场景和特征方向,而你负责的是让这个东西在真实流量里活下来。它既不像算法岗那样可以无视工程细节,也不像纯后端开发那样可以完全不懂模型机理。
很多半路入行的朋友习惯把自己定义成"调参侠",模型效果不好就换超参数,换完不行就换个模型。这种打法在教程项目里能糊弄过去,因为教程数据是干净且确定的。真实世界里,模型表现诡异往往不是超参数的问题,而是数据分布变了、特征口径错了、训练和线上不一致,这些问题的定位都需要你对模型机制有足够深的理解。
1.2 为什么在AutoML和基础模型时代,还是要"手搓"
现在确实有大量现成工具:AutoML帮你搜网络结构,大模型API帮你做通用任务,微调工具链也越来越完善。那为什么还要坚持from-scratch式的练习?
第一个理由是调试能力。框架把你的梯度藏起来了,当loss出现NaN、收敛到错误位置、或者训练和测试指标严重背离时,你只能靠猜。而如果你自己实现过一个完整的梯度下降,你会知道NaN大概率来自数值溢出或除零、梯度消失和激活函数有关、过拟合在损失曲线上有什么形态。这些直觉不是看文档能看出来的,必须亲手做一遍。
第二个理由是表达能力。工作中总有框架覆盖不到的场景,比如要改一个损失函数、要加一个自定义采样逻辑、要把模型部署到内存受限的嵌入式设备上。不懂底层机制,你连"能不能改"都判断不了,更不用说改了之后要怎么验证。亲手写过一遍,你才知道哪些部分是框架给你的福利,哪些部分只是薄薄的一层封装。
第三个理由比较现实:面试和晋升。算法工程师的面试基本逃不开手推反向传播、手写注意力机制这类题目。这不是故意刁难,而是考察你遇到新结构时有没有能力把它吃透。有过从零实现的经历,你面对陌生论文时会习惯性地把它拆成"输入→运算图→损失→梯度"几个环节,而不是一头雾水。
1.3 这条路走通之后,你会获得什么
按我的经验,真正走完一遍ai-engineering-from-scratch的人,通常会有三个明显变化:
- 拿到新模型论文,第一反应是画计算图推导梯度,而不是等别人封装好库。
- 遇到线上效果变差,知道从哪里开始排查:先看数据分布,再看特征口径,最后才怀疑模型本身。
- 跟算法研究员、产品经理对接需求时,能准确判断"这个需求是数据问题还是模型问题",避免两头白忙活。
这三个能力,说实话比会调任何热门框架都值钱。框架会过时,但理解问题、拆解问题、定位问题的能力不会。
2. 从零起步的三块地基:数学直觉、代码习惯、任务拆解
2.1 数学:别被劝退,补到"能看懂公式在说什么"就行
一提到AI就先补数学,很多人被劝退在高数第三章。我的观点是:你不需要成为数学家,但需要建立"看得懂公式在说什么"的能力。说白了,数学是地图,框架是汽车,看得懂地图的人开导航才不会迷路。
需要重点补的三块,按优先级排:
- 线性代数:重点理解矩阵乘法是"批量计算"的缩写,内积表达相关性,向量和矩阵的形状对应数据的不同维度。特征值、特征向量这些可以先放一放,用到再看。
- 微积分:核心是导数就是"某个参数变化一点点,损失会跟着变多少";偏导数是"在多个参数里只看其中一个的影响"。链式法则必须滚瓜烂熟,因为反向传播就是把链式法则套在计算图上。
- 概率统计:均值、方差、正态分布、条件概率、极大似然估计是主力。你要能理解过拟合的统计本质:在有限样本上估计参数,估计得越精细,越容易把噪声也学进去。
我自己有一个很实用的自检标准:能独立从零推导出 softmax 交叉熵对 logits 的梯度;能解释为什么 L2 正则化等价于给参数一个零均值的高斯先验。这两条做到,基础就够用了。
学数学最忌干啃教材。我的方法是以代码为导向——每学一个概念,就想办法用NumPy实现一遍。实现矩阵乘法时理解了广播机制,实现softmax时理解了数值稳定性,亲手踩过坑之后,公式就不再是纸上的符号了。
2.2 代码:先练NumPy,再碰框架
代码地基这块,我的建议可能跟主流教程相反:别一上来就学PyTorch,先用NumPy裸写算法。
原因很简单:PyTorch把你的梯度全自动算了,你写出来的代码只是前向传播,反向传播的细节全被框架藏起来了。而NumPy逼你显式写出每一步运算、每一个梯度更新。这个过程虽然笨,但恰好把AI最核心的机制刻进你的脑子里。
我建议的练习阶梯:
- 用NumPy实现线性回归(后面第3节我会完整走一遍)。
- 用NumPy实现k-means聚类,体会迭代优化的感觉。
- 实现一个两层的MLP,亲手写前向传播和反向传播,然后用
gradient checking(数值梯度对比解析梯度)验证自己的推导对不对。 - 再用PyTorch复现同样的模型,对比自己和框架算出的梯度是否一致。
第三条特别重要。很多人以为理解了反向传播,一上手才发现求导时符号错了、维度没对齐。把梯度检查当成单元测试跑一跑,错误立刻现形。
除了算法本身,工程习惯也要从第一天开始养成:把代码写成函数而不是Notebook里一大坨;用git管理每次改动;给训练函数写一个简单的单元测试断言loss在下降;用虚拟环境锁好依赖版本。这些习惯在个人项目里锦上添花,在团队项目里就是救命稻草。
2.3 任务拆解:把模糊业务翻译成可学习问题
如果说数学和代码是"怎么做",任务拆解就是"做什么"。这一步定错,后面所有工作都是白干,偏偏新手最容易忽略。
拆解业务问题我常用一个四步模板:
- 问题定义:业务方到底想要什么结果?是预测、分类、排序还是生成?
- 样本单位:什么算一个样本?一个用户?一次交易?一条消息?
- 标签定义:目标值是什么?如何在没有人工标注的情况下构造?
- 评估协议:用什么指标判断好坏?怎么切训练验证集才不泄漏?
举个例子,用户流失预测:
| 拆解项 | 定义 |
|---|---|
| 样本单位 | 某个用户在过去30天的行为序列,切成按月的窗口 |
| 标签 | 未来30天内该用户是否取消订阅 |
| 特征 | 登录频率、活跃时长、付费金额的变化趋势 |
| 评估协议 | 按时间顺序划分训练/验证集,不能用未来的数据预测过去 |
你可能觉得这有什么难的。但实际上,我见过太多项目栽在样本单位和标签定义上:用用户做单位却把同一个人重复采样;标签里混进了预测时刻之后才发生的信息(数据泄漏);时序问题上随机切分导致验证集"偷看"未来。这些坑在后文第4节还会细聊。
3. 手撕线性回归:不靠框架走一遍梯度下降全流程
3.1 为什么选线性回归当作"最小完备系统"
线性回归是监督学习里最朴素、最"没技术含量"的模型,但它是唯一一个你能把全流程看透的系统:参数、损失函数、梯度、优化、评估,一个都不少。它就像学车时的教练车:速度慢、视野好、所有操作都在明面上。
更重要的是,线性回归和神经网络之间没有鸿沟。给线性模型加一个非线性激活函数,再堆几层,就变成了MLP。梯度下降更新参数的逻辑一模一样,只是梯度从"手推"变成了"反向传播自动算"。把线性回归吃透,深度学习的入门门槛就跨过去了。
3.2 从MSE出发,把梯度亲手推导一遍
先建立模型和数据记号。假设我们有 n 个样本,每个样本只有一个特征 x,标签是 y。模型预测值:
y_hat = w * x + bw是权重,b是偏置。损失函数选择均方误差(MSE):
L(w, b) = (1 / n) * Σ(y_hat - y)^2 = (1 / n) * Σ(w*x + b - y)^2为什么选MSE而不是绝对误差?因为平方函数处处可导,梯度光滑,优化起来方便;而且它天然惩罚大误差——差1个单位代价是1,差2个单位代价是4,这符合很多业务里"离谱预测比小偏差更致命"的直觉。
接下来算梯度。我们要回答的问题是:如果w动一点点,L会怎么变?
对w求偏导:
∂L/∂w = (1 / n) * Σ 2 * (w*x + b - y) * x = (2 / n) * Σ x * (y_hat - y)对b求偏导:
∂L/∂b = (2 / n) * Σ (y_hat - y)这两个式子的直觉很直接:某个样本的预测值比真实值高(y_hat - y > 0),为了减小损失,权重应该往哪个方向调?答案是往反方向调——这正是梯度下降做的事情。
更新公式:
w = w - lr * (∂L/∂w) b = b - lr * (∂L/∂b)学习率lr决定了每次迈的步子有多大。这里有一个朴素但准确的比喻:你站在山上要找谷底,梯度告诉你是往左还是往右,学习率告诉你这一步得迈多大。迈太大直接冲过谷底甚至跨到对面山上,迈太小天黑都走不到。
3.3 用NumPy实现,并观察三个关键现象
下面这段代码我建议你亲手敲一遍,不要复制粘贴。代码故意不用任何框架,只有NumPy。
import numpy as np # 生成带噪声的线性数据,设置固定随机种子便于复现 np.random.seed(42) x = np.random.randn(200, 1) true_w, true_b = 2.5, -1.0 y = true_w * x + true_b + 0.3 * np.random.randn(200, 1) def mse(y_true, y_pred): return np.mean((y_true - y_pred) ** 2) # 初始化参数 w = np.random.randn(1, 1) b = np.zeros(1) lr = 0.05 epochs = 300 for epoch in range(epochs): y_pred = x @ w + b # 手动计算梯度 grad_w = (2 / len(x)) * (x.T @ (y_pred - y)) grad_b = (2 / len(x)) * np.sum(y_pred - y) # 参数更新 w -= lr * grad_w b -= lr * grad_b if epoch % 50 == 0: print(f"epoch {epoch:3d}, loss {mse(y, y_pred):.4f}, " f"w {w.item():.3f}, b {b.item():.3f}")跑完这段代码,你会看到损失在下降,w逐渐接近2.5,b逐渐接近-1.0。但我建议你做三个小实验,分别观察三个关键现象:
第一个实验:把学习率从0.05改成0.5。你会发现loss先降后飙,甚至变成NaN。这就是"步子迈太大冲过谷底"的直观演示。实际项目中,我习惯先用很小的学习率(比如1e-3)确认loss能稳定下降,再慢慢加大并观察曲线,这个方法比盲调高效得多。
第二个实验:把x换成不经过标准化的量级(比如乘以100)。你会发现收敛变慢,loss曲线变得很抖。原因是x量级大了,梯度也随之变大,优化过程不稳定。这就是为什么工程上几乎要对特征做标准化——它直接关系到优化效率。
第三个实验:把w初始值换成一堆不同的随机数。你会发现最终都收敛到差不多的结果,因为MSE是凸函数,只有一个谷底。这个现象在神经网络里不成立——非凸的损失面有很多局部极小,所以深度学习里初始化策略才那么讲究。
顺手把梯度检查做了:在某个w附近算一个小的扰动,用数值差分估计梯度,再跟解析梯度对比。两者应该非常接近。这一步能验证你没有推错公式,也是我后来所有模型实验的标配动作。
3.4 从线性回归到神经网络的延伸
线性回归的更新公式和神经网络的反向传播,本质是同一件事:链式法则。区别只在于计算图深了,梯度传递的链条长了。
线性模型的梯度是∂L/∂w = (∂L/∂y_hat) * (∂y_hat/∂w),两层网络里,假设中间有激活函数h:
z1 = W1 @ x + b1 a1 = relu(z1) z2 = W2 @ a1 + b2 y_hat = z2要对W1求偏导,就要把误差从L传到z2、传到a1、传到z1、最后传到W1,这就是"反向"传播。每一步都只是线性回归里那个链式法则的不断套用。
所以我给想从零进阶的朋友一条明确路线:线性回归 → 逻辑回归(引入sigmoid和交叉熵)→ 两层MLP(完整手写反向传播)→ 再用PyTorch复现并和手写梯度对拍。走到第四步,你对深度学习"流水线"的理解会比90%只会调框架的人更扎实。
4. 越过模型之后才是主战场:数据、评估、部署与监控
很多人以为模型训练好就大功告成。以我的经验,模型代码只占一个AI项目工作量的两到三成,剩下七八成全部消耗在数据、评估、部署和监控这些"不性感"的环节上。可恰恰是这些环节决定了一个项目能不能真正上线。
4.1 数据环节:验证集是底线,泄漏是红灯
数据环节第一条铁律是:训练集、验证集、测试集必须严格分离,任何可能借用验证集信息的选择(选超参数、选模型、选阈值)都不该碰测试集。新手最常见的错误是把测试集当验证集反复用,最后测试集泄漏到超参数里,测试分数虚高得离谱。
第二条铁律是:警惕数据泄漏。最常见的是标准化时机错误——我犯过一次特别典型的错误:先用全量数据计算均值和方差做标准化,再切训练验证集。结果每个验证样本的"标准值"里都混进了训练集的统计信息,模型离线AUC高达0.98,上线后直接崩到0.6。正确做法是先切分,再只在训练集上拟合标准化器,然后用同一组参数去转换验证集和测试集。
第三条铁律:时序数据必须按时间切分。用未来预测过去是不可原谅的泄漏。预测用户明天是否流失,特征只能用到今天为止,验证集也只能是"明天之后"的数据。这个看起来像常识,实际操作中因为特征表生成时间的bug而出错的项目我见过不止一个。
至于类别不平衡,我建议先别忙着上过采样、SMOTE这些花活。先检查你的评估指标:如果用AUC,它对类别不平衡其实不敏感;如果用F1、精确率、召回率,那优先调决策阈值比换采样方法更直接。只有在确认阈值无法解决问题时,再考虑代价敏感学习或数据增强。
4.2 评估环节:指标要跟业务成本挂钩
模型评估最核心的认知是:不是所有模型都一样好,比如把一个垃圾邮件分错的代价和把一个重要业务邮件分错的代价就完全不同。一个准的模型可能不是最好的模型,关键是它在你所在业务场景里值不值。
打个比方,风控场景里,把一笔欺诈交易放过(假阴性)的代价可能是几千上万,而误杀一个正常交易(假阳性)的代价可能只是用户投诉。两张错误类型的代价不同,最优阈值就应该从"精确率=召回率"的位置往"降低假阴性"方向移动。实操上,我会先算出每个阈值对应的混淆矩阵,再套一个业务代价矩阵:
| 实际\预测 | 预测为欺诈 | 预测为正常 |
|---|---|---|
| 实际欺诈 | 0(正确) | 5000元(漏过) |
| 实际正常 | 50元(误杀安抚成本) | 0(正确) |
把每个阈值下的总代价算出来,选最小的那个,而不是看F1分数,这样选出的模型才是真正对业务有价值的模型。
同时我强烈建议建立基线。所谓基线,可以是一个简单的规则(比如"历史违规超过3次的用户直接拦截"),也可以是一个线性模型。先跑通基线,再上复杂模型,比较两者的增量收益。如果没有基线,你根本不知道复杂模型的提升是真实效果还是过拟合的幻象。
4.3 部署与监控:模型上线只是开始
部署模式选择先想清楚,别一上来就上微服务。我按场景区分:
- 实时API:延迟敏感,比如在线推荐、支付风控。需要关注响应时间、并发量,模型通常要精简,输入特征要能实时取到。
- 批量计算:T+1离线跑,比如次日用户分群、流失预警。实现简单,用调度系统定时跑即可。
- 边缘端部署:手机、IOT设备。模型要量化压缩,关注体积和推理速度。
部署之外,模型版本管理是我最想强调的事。模型本身要进模型注册表,记录它对应的训练代码版本、数据版本、特征版本、超参数,这样出现线上问题时可以快速回滚。没有版本管理的模型线上出问题,定位就像大海捞针。
监控是"模型上线之后"的第一优先级。至少要盯四类信号:预测分布漂移(模型给出的分数分布有没有突变)、特征缺失率和取值范围(有没有字段突然全空)、服务本身的延迟和错误率、业务指标联动(比如点击率、转化率的实际变化)。当预测分布漂移明显时,触发重训或者人工介入,而不是等业务方投诉之后再跑去查。
说到重训触发,定期重训(比如每月一次)和按漂移报警触发各有适用场景。我的习惯是两者结合:低频的业务用定期,高频的业务用漂移指标做二次触发。关键在于,你定义"漂移"的时候要想清楚:是特征分布变了,还是真实标签分布变了,还是模型打分变了,三者对应的处置方式完全不同。
5. 从零到能上线的路上,我踩过的五个坑
5.1 坑一:拿清洗好的数据练手,真实项目第一天就崩
我早期练手全用Kaggle的干净数据,字段完整、缺失已处理、目标已定义,跑模型跑得顺风顺水。直到接手第一个真实项目,才发现现实里根本没有"干净数据"这回事:埋点日志有重复、时间戳有时区错乱、同一个用户在不同表里的ID对不上、缺失值的含义分"没有"和"本来就是0"两种。
后来学乖了,做任何数据项目的第一步永远是"数据体检":统计每个字段的缺失率、唯一值数量、分布形状、时间跨度,把口径彻底搞清楚再动手建模。项目能不能成功,往往在建模之前就已经决定了。
5.2 坑二:不做基线,直接上XGBoost和深度学习
刚入门时我对复杂模型有迷之崇拜,什么问题都先上一顿XGBoost,再叠加神经网络。结果模型训练慢、调参复杂、排查困难,最后效果还不如一个简单的规则脚本。
现在我每个项目都强制先做基线:一条简单的规则,或者一个线性模型。基线能帮你校准预期、提供对照、还能在后续排查时当"下界参照"。打个不夸张的比方,基线就是体检时的基础血压值,没有它,后面测得再精细你也判断不了自己正常不正常。
5.3 坑三:训练和上线环境不一致
有次模型离线指标很好,上线后却完全不是一回事。排查半天发现问题出在特征处理:训练代码里我对缺失值填充的是"中位数",上线服务里因为依赖库版本不同,填充的成了"0"。特征处理逻辑不一致,离线跑的模型和线上跑的模型根本不是一个东西。
解决方式很笨但很有效:把特征处理写成一个独立模块,训练和预测共用同一个函数,并且把这个模块做成版本化。再配合线上回放历史数据对比特征输出,基本能杜绝这类问题。现在很多工具链已经把"训练/服务一致性"当成基本要求,但你自己动手实现过一遍,才能理解这个设计为什么这么重要。
5.4 坑四:数据和模型版本对不上
模型是用"上周五的数据"训练出来的,这周一上线的时候,线上用的特征表已经更新到"昨天"。特征口径变了,模型表现自然漂移。这个坑特别隐蔽,因为不是报错,只是效果悄悄变差。
我的做法是给每次训练记录三样东西:数据快照的时间戳、特征代码的commit号、模型产物的版本号。这三者绑定在一起,任何一个不一致都能立刻看出来。听起来很基础,但能坚持做下来的团队真的不多。
5.5 坑五:过度工程化,一上来就是高可用集群
听到"生产环境"就紧张,一上来就规划Kubernetes集群、分布式训练、实时流处理。结果项目还没跑通,光搭基础设施就耗了一个月。这是典型的用大炮打蚊子——模型都没验证过值得投入,就先背上了基础设施的包袱。
正确的姿势是:能用定时脚本解决的,不要上调度系统;能用单体服务解决的,不要拆微服务;能在一台机器上训练的,不要上分布式。先把端到端的最小链路跑通,确认模型有业务价值,再按需加固。工程化的程度永远要匹配业务的复杂度,过度的工程化同样是一种浪费。
最后再分享一个我坚持了很多年的习惯:到现在,我拿到一个不熟悉的模型结构,还是会先在纸上把计算图简化、把梯度推导一遍,哪怕只是最粗糙的版本。这个习惯帮我避开了太多"看起来能跑但一碰就碎"的代码。如果你也在ai-engineering-from-scratch这条路上,别急着追新框架,先把手底下的每一层纸都捅破。捅破的每一张纸,最后都会变成你排查线上问题时最值钱的经验。