如果你搜到ai-engineering-from-scratch这个名字,大概率不是冲着看热闹来的。直译过来就是“从零开始做 AI 工程”,但能把这条路线完整走下来的人,确实不多。市面上到处是“三天入门深度学习”“七天搞定大模型”的教程,打开全是框架安装、demo 跑通、结果炫图,关掉之后你发现自己依然什么都不会——换个数据集就崩,换个业务场景就懵,线上模型效果变差只会无脑重训。这个项目想解决的,正是“会调包但不懂底层、会跑实验但不会落地”的普遍困境。
它适合两类人:一类是已经有编程基础、想系统性转入 AI 方向的工程师,另一类是在校学生或研究员,模型训练跑过不少,但一聊到部署、监控、数据漂移就捉襟见肘。哪怕你刚接触 Python,也可以从这里起步,只是第一层需要多花些时间夯实。我按自己的实操经验把整条路径拆开揉碎,下面直接进入正题。
1. 这个项目到底在解决什么问题:核心思路与整体设计拆解
1.1 为什么 AI 工程值得“从零开始”
很多人一听“从零开始”就眉头一皱:现在框架这么成熟,我直接pip install torch然后调model.fit不就行了,何苦手写底层?这里必须分清楚:“从零开始”是学习路径,不是生产方案。就像你不会微积分时背一背求导公式也能应付考试,但遇到没见过的题就真的束手无策。
我在生产环境里见过太多次这样的场景:模型上线一段时间后效果变差,负责的工程师只会重新训练几次,然后对着日志发呆。问他为什么损失函数不再下降,答不上来;问他是过拟合还是数据漂移,也说不清楚。这背后的根本原因,是他对模型的认知停留在“调用接口”层面,不知道梯度从哪里来、目标函数在优化什么、线上和线下的数据分布差异会如何传导到最终指标上。
从零手写一遍梯度下降、反向传播,再亲手加上正则化项、调整学习率,你对这些机制的理解会完全不一样。当线上 loss 变成 NaN 的时候,你脑子里浮现的不再是“重启试试”,而是一张排查清单:学习率是不是太大、梯度是不是爆炸了、输入数据是否包含 NaN、网络结构是否出了问题。这种定位问题的能力,就是 AI 工程里最值钱的经验之一。
1.2 整条路线的编排逻辑:四层地基
这个项目的路线设计可以归纳成四层递进结构,每一层都是上一层的前提:
- 第一层:编程、数学与数据处理基本功;
- 第二层:经典机器学习算法与模型评估;
- 第三层:深度学习、反向传播与神经网络架构;
- 第四层:AI 全栈工程化——训练管理、部署、监控、迭代闭环。
为什么把工程放到最后?因为工程化解决的问题是“让模型稳定地跑在生产环境里”。如果你连模型都还没在自己的笔记本上跑明白,谈 Docker、Kubernetes、模型监控就完全是空中楼阁。
反过来,没有第四层,前三层就永远停留在 notebook 阶段。很多算法工程师出身的人,论文读了不少,模型结构信手拈来,但让他把模型封装成一个接口、处理线上特征一致性、设置监控告警,他会一脸茫然。而一个真正的 AI 工程师,应该既有对模型的深度理解,又有把模型变成产品的系统能力。这两条腿缺一不可。
1.3 与主流课程的本质区别
主流课程通常是“框架为中心”:教你 PyTorch 的 API、跑通几个视觉分类 demo、复现一篇论文。看似学习曲线平滑,但框架更新迭代极快,TensorFlow 和 PyTorch 的接口隔两年就变,你学到的东西可能迅速过时。
这个项目的思路是“原理与工程双主线”:一方面把模型背后的数学和计算过程掰开揉碎讲清楚,另一方面从头到尾走一遍真实项目,让你知道一个模型从数据到上线的每个环节长什么样。原理是大浪淘沙后依然稳定的东西,而工程能力是区分“会做实验”和“能交付产品”的分水岭。
框架当然要学,但学习框架的正确方式是在已经理解底层原理之后,再把它当作提升效率的工具,而不是把它当作唯一的救命稻草。
2. 打地基:编程、数学与数据处理的实用标准
2.1 Python 与向量化思维:决定性能的隐形分水岭
第一层不是“从安装 Python 开始学语法”。如果你基础薄弱,花一到两周过一遍语法、函数、类和文件读写就够了,重点是别恋战。紧接着就要进入 NumPy 的世界,理解数组创建、形状、切片、花式索引和广播机制。
为什么 NumPy 这么重要?因为模型计算本质上是矩阵运算,而 Python 原生循环执行相同运算会慢到让人怀疑人生。我实测过一个矩阵乘法:shape(30000, 200) @ (200, 100),用np.dot是一眨眼的事;如果写成三层 for 循环,笔记本上要跑几分钟甚至更久。这种性能差距,直接决定你后续能不能在合理时间内完成实验。
第一层最需要养成的习惯,就是把所有批量计算写成矩阵形式。你越早接受“向量化和广播是 AI 编程的第一语言”这个观念,后面写训练代码就越顺手。每当你发现自己写了一个嵌套深循环去处理批量数据,停一下,想想能不能用矩阵运算代替。
2.2 数学要学多少才够用:三个主题的“最小可行集”
很多人被 AI 里的数学吓得放弃,这里给大家一个非常务实的“够用”标准,不需要成为数学专家,但以下三个主题必须有直觉:
- 线性代数:理解矩阵乘法、转置、形状匹配、逆矩阵和伪逆。神经网络层与层之间全是矩阵运算,最大的坑就是形状不匹配。你至少要知道
A(m,n) @ B(n,p) → C(m,p)这条规则,它能解决 80% 的调试问题。 - 微积分:会求导数、偏导数和链式法则。反向传播就是链式法则在一张计算图上的应用,SGD 更新参数需要梯度。不需要会解偏微分方程,但看到公式里的每个符号都要知道含义。
- 概率统计:理解最大似然估计、期望、方差、正态分布和伯努利分布。交叉熵损失的根基就是最大似然,AUC 和置信区间则来自统计知识。
这是最实际的学习范围。你可以把这些知识类比成做饭:不需要成为一级厨师才能做熟一桌菜,但必须明白盐的作用是调味、油的作用是导热,否则全靠菜谱你永远无法随机应变。
2.3 数据处理:整个 AI 工程最容易被忽略、也最容易翻车的环节
我见过太多团队,模型结构选的没问题、训练代码也写得干净,最后效果翻车却翻在数据处理上。数据处理是 AI 工程里最不性感但最重要的环节,没有之一。
我的实际操作习惯是:拿到原始数据,先人工打开前几十行,看 schema 和数据类型;然后统计缺失值比例、唯一值数量、数值分布;再决定清洗策略。如果是时间序列数据,训练集/验证集/测试集必须严格按时间切分,绝对不能随机打乱。有一次我在做销售预测,图省事用了随机采样切分,线下验证集指标非常漂亮,结果上线后模型预测严重滞后于真实趋势。排查到最后发现,随机切分把未来的数据混进了训练集,模型相当于“偷看”了答案——这就是典型的数据泄漏。
数值特征要关注量纲,标准化或归一化能防止某个大尺度特征主导梯度;类别特征要区分低基数和高基数,分别采用 one-hot 或编码方案。最重要的是,整套预处理逻辑必须写成可复用的 pipeline 或函数,训练和线上预测共用同一套逻辑。如果训练时清洗了缺失值、线上推理时没清洗,预测结果直接废掉。这个坑,后面部署章节还会重点展开。
3. 经典机器学习:先建立完整心智模型再谈深度模型
3.1 为什么先学经典算法而不是直接上深度学习
直接上深度学习的最大问题是:当效果不好时,你根本分不清是数据问题、特征问题、网络结构问题还是优化问题。变量太多,新手根本无法定位。经典机器学习算法结构简单、可解释性强,适合先建立一套完整的方法论框架。
另一个现实原因是:生产环境中大量任务都是表格数据,LightGBM、XGBoost 这类梯度提升树模型往往比深度模型效果更好、训练更快、稳定性更强。我先学经典算法,就具备了解大多数实际问题的能力,而不是只会跑图像和文本的 demo。深度学习能力是重要加分项,但不该是唯一技能。
3.2 从零手写逻辑回归:建立 AI 工程思维的最小练习
如果只选一个练习来打基础,我会选“从零手写逻辑回归并用梯度下降训练”。这几乎是 AI 工程的最小闭环,链条如下:
- 定义模型:线性加权 ( z = Xw + b );
- 过 sigmoid 得到概率 ( p );
- 定义交叉熵损失;
- 计算梯度(最好能手推或数值验证);
- 用梯度更新权重;
- 重复迭代,直到验证集指标不再上升。
对二分类来说,sigmoid 加交叉熵的梯度形式简洁得惊人,直接就是 ( X^T(p - y) / N ),和线性回归的梯度公式几乎一模一样,只是残差方向不同。搞懂这一层,后面神经网络的梯度就只是“同样操作”的多次组合。
下面这段是我手写时的核心代码,关键细节都做了处理:
def train_logistic(X, y, lr=0.01, epochs=100): n_samples, n_features = X.shape w = np.zeros(n_features) b = 0.0 for _ in range(epochs): z = X @ w + b p = 1 / (1 + np.exp(-np.clip(z, -500, 500))) loss = -np.mean(y * np.log(p + 1e-12) + (1 - y) * np.log(1 - p + 1e-12)) grad_w = (X.T @ (p - y)) / n_samples grad_b = np.mean(p - y) w -= lr * grad_w b -= lr * grad_b return w, b这里np.clip是为了防止指数运算溢出,1e-12是为了防止取对数时出现零值。代码虽短,但这两个细节是实际训练里真的会遇到的坑,不处理就会得到 NaN。
3.3 经典算法的工程落点:调包也需要懂原理
手写一遍之后,生产环境当然可以用 sklearn 或 LightGBM 来提速,但这不意味着可以无脑调包。几个关键知识点必须刻进脑子:
- 归一化:线性模型和神经网络对特征尺度敏感,需要归一化;树模型不受影响,因为它做的是分裂而不是距离计算。
- 类别不平衡:先换评估指标,AUC、召回率、精确率比准确率靠谱得多;再考虑给少数类加权或采样的手段。
- Pipeline 一致性:把缺失值填充、类别编码、归一化全部放进同一个 pipeline,训练和预测共用。这样能避免线上请求和训练时特征处理不一致的问题。
4. 深度学习从零实现:反向传播、手写网络、理解框架
4.1 反向传播的直觉:链式法则在计算图上原路返回
反向传播是深度学习最核心的机制,但它的本质没有那么玄:就是链式法则在计算图上原路返回梯度。先举一个最简单链条:( z = Wx + b ),过激活函数得到 ( a ),再算损失 ( L )。要更新 ( W ),我们需要 ( \partial L / \partial W ),靠链式法则一步步从 ( L ) 传回 ( z )、再传回 ( W )。
整个过程可以类比成一个配送网络回溯:订单派发到最后一站,配送员要沿原路返回,在每个节点带上“这个节点产生的影响”,累加给上游。计算图里的前向缓存(( z )、( a ))就是每个节点在反向时需要的“地址信息”。没有这些缓存,反向传播根本无从谈起。
4.2 从零手写一个两层神经网络
下面是手写两层神经网络的核心结构,输入层到隐藏层用 ReLU 激活,输出层用 Softmax 做多分类。重点看反向传播里各矩阵的形状变化,这是最容易出错的地方:
class TwoLayerNet: def __init__(self, in_dim, hidden_dim, out_dim, lr=0.01): self.lr = lr # He初始化,避免深层梯度消失 self.W1 = np.random.randn(in_dim, hidden_dim) * np.sqrt(2 / in_dim) self.b1 = np.zeros(hidden_dim) self.W2 = np.random.randn(hidden_dim, out_dim) * np.sqrt(2 / hidden_dim) self.b2 = np.zeros(out_dim) def forward(self, X): self.z1 = X @ self.W1 + self.b1 self.a1 = np.maximum(0, self.z1) # ReLU激活 self.z2 = self.a1 @ self.W2 + self.b2 # Softmax,减去最大值防止指数溢出 exp_scores = np.exp(self.z2 - np.max(self.z2, axis=1, keepdims=True)) self.probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) return self.probs def backward(self, X, y): N = X.shape[0] dz2 = self.probs dz2[range(N), y] -= 1 # Softmax + 交叉熵梯度的简化形式 dz2 /= N self.grad_W2 = self.a1.T @ dz2 self.grad_b2 = np.sum(dz2, axis=0) da1 = dz2 @ self.W2.T dz1 = da1 * (self.z1 > 0) # ReLU的导数 self.grad_W1 = X.T @ dz1 self.grad_b1 = np.sum(dz1, axis=0) def step(self): self.W1 -= self.lr * self.grad_W1 self.b1 -= self.lr * self.grad_b1 self.W2 -= self.lr * self.grad_W2 self.b2 -= self.lr * self.grad_b2这个代码不算长,但覆盖了神经网络训练的几乎全部关键要素:参数初始化、前向传播、反向传播、参数更新。几个细节值得说明:
- 初始化不能全为零,否则对称性导致所有神经元学到同样特征;用 He 初始化能缓解深层网络中梯度消失和梯度爆炸的问题。
- Softmax 与交叉熵组合的梯度形式特别简洁:
probs − onehot,这是手写时一个可以背下来的结论。 - 激活函数 ReLU 的导数是二值化的:大于零为 1,否则为 0。代码里的
(self.z1 > 0)就是在计算这个。
手写完整的一次训练循环后,你对“神经网络到底在做什么”会有非常直观的感受。这种感受,只调框架的人永远体会不到。
4.3 从手写过渡到框架:PyTorch 的封装逻辑
手写完成之后,再看 PyTorch 会莫名亲切。它的封装无非是替你做了三件事:自动微分、GPU 并行、模块库。
手写版和 PyTorch 的对应关系:
# 手写版 self.W1 = np.random.randn(...) # 对应 nn.Linear(in_dim, hidden_dim) self.a1 = np.maximum(0, self.z1) # 对应 F.relu # 手写 backward 里的梯度计算 -> 对应 loss.backward() # 手写 step -> 对应 optimizer.step()对应代码如下:
import torch.nn as nn import torch.optim as optim model = nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10) ) loss_fn = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.01)PyTorch 使用的是动态计算图,每次前向都会重新构建图结构,反向传播由 autograd 自动完成。这非常灵活,调试也方便。你理解了手写版的计算过程,再看loss.backward()就知道它背后究竟帮你算了什么,出错时也不会把它当黑盒。
5. AI 全栈工程化:从模型到产品的最后一公里
5.1 训练管理:不要只靠“再跑一次”
AI 工程与算法比赛的本质区别在于可重复性和可追溯性。一个严肃的团队,每次实验都必须有闭环记录。哪怕只用一张表格,至少也要记下以下字段:
| 记录项 | 说明 |
|---|---|
| 数据集版本 | 数据是否改动、清洗规则是什么 |
| 特征列表 | 用了哪些特征,顺序是否锁定 |
| 模型结构 | 层数、隐藏维度、激活函数 |
| 超参数 | learning rate、batch size、epochs 等 |
| 训练指标 | 训练 loss、验证 loss、AUC 等 |
| 随机种子 | 保证可复现 |
| 代码版本 | git commit 号或业务线编号 |
| 备注 | 踩了什么坑、为什么换参数 |
有条件的话直接用 MLflow 或 wandb 这类实验追踪工具,会自动记录和可视化;没条件用 Excel 也行,关键是养成交代实验背景的习惯。
超参数调优方面,经验优先级如下:learning rate 是影响最大的,一般先按 1e-2、1e-3、1e-4 做对数尺度搜索;batch size 影响收敛噪声和显存占用,常见取 16 到 128;表格任务里神经网络 2 到 3 层、每层 64 到 256 维往往足够起步;正则化、早停则是克制过拟合的主要手段。实际训练里最常见的一条曲线是:训练 loss 一直下降,验证 loss 在某个 epoch 后开始回升,那就是过拟合信号。早停设为验证指标连续若干轮不提升即停止,同时配合 dropout 或降低模型容量。
5.2 模型部署链路:导出、服务化、容器化
模型训练完成只是起点,部署上线才是工程的真正开始。完整的部署链路至少包含四步:导出模型、服务化封装、容器化、压测与上线。
导出模型时,PyTorch 模型可以转成 TorchScript 或 ONNX 格式。ONNX Runtime 在 CPU 上的推理速度通常明显优于原始 PyTorch,而且支持 INT8/FP16 量化,能进一步降低延迟。量化前先做精度对比,防止业务指标滑坡。
服务化封装,我用得最多的是 FastAPI,结构简单、性能足够。最小可运行版本大概是这样的思路:
from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort import numpy as np app = FastAPI() session = ort.InferenceSession("model.onnx") class Payload(BaseModel): features: list[float] @app.post("/predict") def predict(payload: Payload): x = np.array(payload.features, dtype=np.float32).reshape(1, -1) prob = session.run(None, {"input": x})[0] return {"probability": float(prob[0][1])}真实生产环境需要注意几点:特征顺序必须是训练时的顺序;输入要做合法性校验;单条推理改批量推理能显著提升吞吐;日志要记录请求特征、预测分数和响应耗时;服务要设置超时和熔断,防止某个批次卡死整个接口。
容器化用 Docker 多段构建,可以把最终镜像体积压到很小。基础镜像选择要谨慎,CPU 推理环境不需要装完整的 CUDA 工具链。压测工具可以用 locust,关注 QPS 和 P99 延迟,不要只看平均延迟——平均值会被少数极慢请求拉高,P99 才是用户体验的真实体现。
5.3 监控与迭代:上线只是开始
模型上线后最忌讳的就是无人看管。我见过不止一个项目,模型上线后效果持续下滑,直到业务方投诉才有人去看,发现线上数据和训练数据分布早就面目全非。
监控至少要做三层:
- 特征层面:统计线上请求特征的均值、方差、缺失率,与训练集对比。偏差超过阈值就告警,比如连续 7 天均值超过训练集均值 3 个标准差。
- 输出层面:记录预测分数分布、正样本率。如果原来预测均值稳定在 0.3,突然涨到 0.5,先不要急着骂模型,大概率是输入分布变了。
- 业务反馈层面:人工标注、投诉、退订等反馈回流到训练集,定期重训。
刚开始可以先固定每两周重训一次,稳定后根据数据漂移告警触发重训。注意重训时验证集也要用最新的数据,防止模型停在旧分布上自嗨。
6. 实操纪实:一个客户流失预测项目的完整落地过程
6.1 业务背景与目标
拿一个我实际做过的客户流失预测项目举例。业务方是一家健身订阅平台,拿到的数据大概是 5 万条会员记录、30 个特征,目标是一个二分类任务:预测该会员下个月是否会取消订阅。评估指标没有只盯着准确率,而是把 AUC、召回率、精确率一起看。业务上希望尽可能召回真正要流失的会员,同时别误伤太多正常用户。
6.2 数据清洗与特征工程
原始数据第一件事是看质量。训练时长字段缺失了约 12%,其他字段缺失较少。缺失值处理选择用中位数填充,因为流失预测这种场景里,中位数比均值对异常值更鲁棒。
特征工程做了几件事:把最近活跃天数、连续不活跃天数这种原始字段保留;新增了派生特征,比如近 30 天消费次数、最近一次上课距今天数、会员时长;把城市、注册渠道这类类别特征做编码。所有数值特征在进入模型前做了标准化。
这里的重点依然是时间切分。会员流失天然带时间属性,如果用随机切分,前三个月的用户和下个月流失的标签会混在一起,模型会学到来训练集里“泄漏未来信息”。我在这里严格按月份切分:前 4 个月做训练集,第 5 个月做验证集,第 6 个月做测试集。
6.3 建模与调参:先跑一个能打的 Baseline
我先用逻辑回归做 baseline,AUC 0.72。这个数字不高,但作为起点很关键——它能告诉你用最简单的线性模型能做到什么程度,后面所有模型的效果提升都要和它对比。
然后上 LightGBM,第一次实验树设置太深,训练集 AUC 接近 0.98,验证集只有 0.80,典型的过拟合。把num_leaves从 128 降到 31、max_depth限制到 6、min_child_samples调到 50,验证集 AUC 回升到 0.83。最终参数大致是learning_rate=0.05, num_leaves=31, max_depth=6, min_child_samples=50。
我也简单试了一个两层 MLP,隐层 64/32 加 dropout,AUC 和 LightGBM 差不多,但模型可解释性差、推理依赖的依赖包更重。表格场景里树模型往往更务实,所以最终选了 LightGBM。
6.4 部署与迭代:真实踩坑与补救
模型导出成 ONNX,用 FastAPI 封装成在线接口,容器化后部署到内部环境。上线第一天就发现了问题:线上预测结果和线下根本对不上。排查半天,原因是线上请求传入的特征顺序和训练时的特征列表不一致,模型拿到的是错位的数据。
解决办法是定义了一个锁定的特征顺序清单,训练前和推理前都从这个清单读取字段顺序,谁都不许在中间手改。从此线上和线下的一致性有了保障。
上线一个月后监控日志显示,预测分数均值从 0.30 缓慢爬到了 0.40。这是典型的数据漂移信号。查了一下,发现线上新注册用户占比明显上升,这些用户的历史行为特征天然稀疏,导致模型输出分布偏移。应对方案是提升重训频率,从每月一次改成每两周一次,并且把近一个月的新数据全部纳入训练集。这次经历让我彻底明白:模型上线之后的维护和迭代,工作量一点都不比训练阶段小。
7. 常见问题与排查技巧实录
7.1 训练时 loss 变成 NaN
这是新手最容易碰到、也最让人慌张的问题。按优先级排查:
- 降低学习率,通常从 1e-2 降到 1e-3 或 1e-4 就能缓解;
- 检查输入数据是否有 NaN 或无穷大,很多时候问题出在特征工程阶段;
- 检查损失函数是否写了
log(0),需要加 epsilon 或使用框架自带稳定版本; - 检查网络是否有梯度爆炸,梯度裁剪能兜底;
- 检查是否把整数特征直接喂给了神经网络而没做归一化。
一个简单的定位方法:把 loss 的计算分解开,分别打印log(p)、log(1-p)的值,看看是哪个分支变成 NaN,方向就清楚了。
7.2 离线指标很好,线上却很烂
这个问题的典型原因有三个:数据泄漏、特征不一致、数据分布漂移。
- 数据泄漏:最常见的是随机切分数据集导致未来信息混入训练集。解决方法是按时间切分,或者按用户/实体去重。
- 特征不一致:训练时做了缺失值填充、标准化、编码,线上请求时没做或顺序不对。解决方法是封装统一 pipeline,让训练和推理共用同一段处理逻辑。
- 分布漂移:线上数据和训练数据的分布发生变化。解决方法是监控特征均值和预测分值分布,及时重训。
7.3 模型复现不出网上效果
网上开源代码跑不出来,或者跑出来的结果和原论文相差很远,太正常了。原因包括数据预处理细节不同、随机种子不同、学习率调度策略不同、训练时长不够等。
我的建议是:先固定环境版本,然后逐层对比中间结果。先对比输入数据是否一致,再对比损失值曲线,最后对比最终指标。把变量拆到最小范围,问题很快会暴露。
7.4 推理延迟过高
推理延迟是线上服务的生命线。按优先级尝试:批量推理,把多个请求合并成一个 batch 一次前向;模型量化,FP16、INT8 都可以显著提速;模型裁剪,去掉多余层或减少维度;用更合适的推理引擎,比如 ONNX Runtime 或 TensorRT。
教训是压测时不要只看平均延迟,要用 P99。平均延迟好看但 P99 爆炸,说明部分请求被卡住了,用户的真实体验并不好。
8. 最后分享几点个人体会
走完这条路,我最大的感受是:从零开始的价值不在于“重复造轮子”,而在于建立心智模型。你亲手写过一遍的东西,哪怕以后生产里全用框架封装,遇到问题时的反应速度和定位准确性也会完全不同。
真实生产环境里的 AI 工程,重点从来不是把模型分数刷到小数点后三位,而是让模型稳定运行、可维护、可迭代。一个能跑三个月不崩的服务,比一个单点指标好看但在线上撑不住一周的模型,价值高得多。
最后再分享一个小经验:做任何 AI 项目,都先问自己“这个模型上线后谁来维护、多久重训一次、线上数据变了怎么发现”。想清楚这三个问题,你就已经超过了很多只盯着训练曲线的人。后续想继续提升的话,可以往 MLOps、LLM 应用工程、多模态系统方向扩展,但无论走哪个方向,这条“从原理到工程”的路子都是最扎实的起点。