
1. 从“调包”到“造轮子”ai-engineering-from-scratch 到底在学什么我见过太多这样的同行了用 PyTorch 写个 ResNet 训练脚本溜溜的精度不够就换模型、调学习率看起来像个熟手。可一旦模型上线出问题——推理延迟高、显存溢出、数据分布漂移——就抓瞎了。为什么因为大部分人的学习路径是从框架入手的框架帮你封装了反向传播、数据加载、GPU 调度你只管填参数。这本身没错但框架屏蔽掉的恰恰是 AI 工程里最核心的部分系统理解模型从数据到部署的全链路并在任何一环出问题时能自己动手拆解、修复。ai-engineering-from-scratch 这个思路本质就是把你扔回“没有框架”的原始环境逼着你从张量计算、梯度推导、数据管道、模型部署一点点自己搭起来。它不是让你重复造轮子而是让你把轮子的结构看穿之后再用框架时你才知道它替你省了什么、漏了什么、哪里可能有坑。这个方向最适合三类人一是刚入门但不想只停留在“调包侠”阶段的算法工程师二是做业务系统、需要把模型真正落地成服务的后端开发三是在校学生趁有时间把底层基础砸实比追着新模型跑有价值得多。如果你已经能熟练训练模型但一遇到工程化问题就头大这篇文章也会给你一条查漏补缺的路线。我把整个“from scratch”拆成五个核心模块来说认知地图、数学地基、手工实现、工程管线、问题排查。这五块走完你再看任何 AI 项目视角都会完全不同。2. 先把认知地图铺开AI 工程不是“训练模型”四个字很多人的误区是把 AI 工程等同于“写模型训练代码”。实际上一个能稳定运行的 AI 系统训练代码只是冰山一角。我按自己的经验把完整链路画成下面这张认知地图用文字描述你脑子里过一遍就行。数据层采集、清洗、标注、版本管理、特征工程。这层决定模型的上限。一个常见的数据集清洗规则和版本回溯比你用什么模型重要十倍。from scratch 的第一步是亲手写数据加载器搞清楚 batch、shuffle、num_workers 这些参数到底在干什么而不是直接调 DataLoader。模型层网络结构设计、损失函数选择、优化器配置、初始化策略。这里最大的坑是“拿来主义”——不知道网络为什么这样设计改起结构就无从下手。from scratch 要求你从感知机推到多层感知机再推到 CNN每一步都知道参数的形状怎么流动。训练层前向传播、反向传播、梯度更新、学习率调度、正则化、早停。这层是数学和工程的交汇处。我见过太多人训练不收敛第一反应是换模型却不知道先看梯度范数和 loss 曲线。from scratch 让你亲手实现一次反向传播之后所有训练问题都有了排查的抓手。评估与验证层指标设计、交叉验证、数据泄露检测、置信度校准。这层最容易被忽略。很多人模型精度刷得高一上线就崩多半是评估方式和真实场景脱节。from scratch 阶段要亲手写评估脚本理解 train/val/test 分割的真正意义。部署与运维层模型导出、推理优化、容器化、监控告警、版本回滚、A/B 测试。这是“AI 工程”中最“工程”的部分。很多算法岗的同学到这里就断档了——没写过 Dockerfile不知道 ONNX 是什么更没处理过线上推理延迟抖动。这条链路里每一层都值得单独立项去“from scratch”一遍。别想着一次全学完我的建议是一层一层打通每一步都做出一个能跑的东西再进下一层。3. 数学地基不用怕但关键概念必须亲手推一遍聊到 from scratch很多人第一反应是数学会不会劝退。我的看法是你不需要成为数学家但几个核心概念必须亲手推一遍否则后面寸步难行。3.1 线性代数矩阵乘法就是数据的“批处理”线性代数在 AI 里的核心就是矩阵乘法。一张 32x32 的灰度图展平是 1024 维向量一个 batch 的 64 张图就是一个 64x1024 的矩阵。权重矩阵 W 的形状是 [输出维度, 输入维度]二者相乘形状规则是 [64, 1024] x [1024, 10] [64, 10]。我建议你 from scratch 第一步用纯 Python 的 list 嵌套写一个矩阵乘法不调 numpy。写完之后你会对 shape 的匹配有肌肉记忆之后看任何框架报的 shape mismatch 错误一眼就定位。3.2 微积分链式法则就是反向传播的全部秘密反向传播的本质就是把 loss 对每个参数的梯度用链式法则一层层传回去。你不需要会求复杂积分但必须能手推一个三层网络的梯度。我拿最简单的例子说y W2 * ReLU(W1 * x b1) b2loss (y - target)^2。先算 dloss/dy 2(y - target)再算 dy/dW2 ReLU 的输出然后往 W1 传的时候ReLU 的导数是分段函数——输入大于 0 导数为 1否则为 0。这个过程亲手推三次以上你再看 PyTorch 的 backward()就是透明人了。3.3 概率统计理解不确定性和分布偏移AI 里概率统计的核心不是背公式而是理解“模型是对数据分布的拟合”这句话。训练集和测试集分布不一致就是常说的 distribution shift。一个特别直观的例子你拿网上爬的猫狗图片训练分类器部署到用户的手机上用户拍的照片光照、角度、画质完全不同精度断崖式下跌——这就是分布偏移。from scratch 阶段建议你亲手做一件事把一个数据集的顺序打乱对比打乱前后训练曲线的差异然后刻意构造一个“训练分布和测试分布不一致”的实验亲眼看看精度怎么崩的。4. 核心技能手搓实录从零实现一个线性回归和一个小型神经网络这个章节是全文的重头戏。我会带你从最原始的 Python 开始一步步实现线性回归和两层神经网络不依赖任何深度学习框架只允许用 numpy 做矩阵运算。这个过程你会踩到真正的工程坑也是 from scratch 最出价值的地方。4.1 手写线性回归梯度下降的最小闭环先准备一份最简单的数据y 3x 2加一点噪声。我们要做的是从随机初始化的 w 和 b 出发通过梯度下降学出接近 3 和 2 的参数。核心代码如下import numpy as np # 生成数据 np.random.seed(42) x np.random.randn(1000, 1) true_w, true_b 3.0, 2.0 y x * true_w true_b 0.05 * np.random.randn(1000, 1) # 初始化参数 w np.random.randn(1, 1) * 0.1 b np.zeros(1) lr 0.01 epochs 500 # 训练 for epoch in range(epochs): # 前向传播 y_pred x w b loss np.mean((y_pred - y) ** 2) # 反向传播手动求导 grad_y_pred 2 * (y_pred - y) / len(x) grad_w x.T grad_y_pred grad_b np.sum(grad_y_pred) # 梯度更新 w - lr * grad_w b - lr * grad_b if epoch % 100 0: print(fEpoch {epoch}, loss: {loss:.6f}, w: {w.item():.4f}, b: {b.item():.4f})跑完w 会收敛到 3.0 附近b 收敛到 2.0 附近。这个最小闭环包含了一个深度学习训练的全部要素数据、参数、前向、loss、反向、更新。你把它吃透后面任何框架都是水到渠成。我强烈建议你在这里做几个小实验把学习率调到 1.0 看怎么发散把初始化标准差调大到 10 看梯度爆炸把数据不归一化直接喂进去看 loss 震荡。这三个坑在后续真实项目里全都会遇到。4.2 手写两层神经网络反向传播的完整落地线性回归只是热身真正的 from scratch 是写一个两层神经网络去跑 MNIST 手写数字识别。这一步你会感受到维度灾难、数值稳定性这些“真问题”。核心结构输入层 784 维28x28 图片展平隐藏层 128 维ReLU 激活输出层 10 维Softmax 交叉熵损失。关键代码import numpy as np def relu(x): return np.maximum(x, 0) def relu_grad(x): return (x 0).astype(float) def softmax(z): # 减去最大值防止数值溢出 e np.exp(z - np.max(z, axis1, keepdimsTrue)) return e / np.sum(e, axis1, keepdimsTrue) # 网络参数 input_dim, hidden_dim, output_dim 784, 128, 10 W1 np.random.randn(input_dim, hidden_dim) * np.sqrt(2.0 / input_dim) b1 np.zeros((1, hidden_dim)) W2 np.random.randn(hidden_dim, output_dim) * np.sqrt(2.0 / hidden_dim) b2 np.zeros((1, output_dim)) lr 0.1 num_epochs 20 batch_size 128 # 训练循环 for epoch in range(num_epochs): # 这里的 X, y 需要提前加载好并做 one-hot for i in range(0, X.shape[0], batch_size): x_batch X[i:ibatch_size] y_batch Y[i:ibatch_size] # 前向传播 z1 x_batch W1 b1 a1 relu(z1) z2 a1 W2 b2 probs softmax(z2) # 反向传播 dz2 probs - y_batch # softmax cross-entropy 的联合导数 dW2 a1.T dz2 / batch_size db2 np.sum(dz2, axis0, keepdimsTrue) / batch_size dz1 dz2 W2.T * relu_grad(z1) dW1 x_batch.T dz1 / batch_size db1 np.sum(dz1, axis0, keepdimsTrue) / batch_size # 更新 W2 - lr * dW2 b2 - lr * db2 W1 - lr * dW1 b1 - lr * db1 # 每个 epoch 结束算一下训练精度 train_acc np.mean(np.argmax(probs, axis1) np.argmax(y_batch, axis1)) print(fEpoch {epoch}, accuracy: {train_acc:.4f})这里有几个很关键的点都是在 from scratch 阶段必须踩过的初始化用 sqrt(2/n) 而不是直接 randn。ReLU 网络的传说级初始化方法来自 Kaiming He直接 randn 会让方差逐层放大深层网络梯度爆炸。softmax 里先减最大值。如果不减exp(100) 直接变成 infloss 直接 NaN。这个细节在 PyTorch 里被封装了你感觉不到但自己实现立刻炸给你看。softmax cross-entropy 的联合导数就是 probs - y。这个式子短得惊人看起来像魔术实际上是把 log_softmax 的梯度一路化简化出来的。你亲手推一遍这个化简会对反向传播有质变的理解。我当时跑这个实验第一次跑完精度大概 92% 左右。虽然 PyTorch 里随便一行就能到 99%但这个 92% 是自己“长”出来的那种掌控感完全不一样。4.3 从手写切回框架让 PyTorch 做你的“翻译官”手写一遍之后就要回到 PyTorch 里把同一个网络用框架实现一遍。这时候你会明显感觉到框架帮你把反向传播、GPU 调度、梯度裁剪、自动混合精度全包了但你已经知道它在底层干了什么。用 PyTorch 重写的网络核心只有这几行import torch import torch.nn as nn model nn.Sequential( nn.Linear(784, 128), nn.ReLU(), nn.Linear(128, 10) ) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.1)但请注意切回框架不代表你可以不看细节。你仍然要手写训练循环显式管理 model.train()/model.eval()自己写学习率调度、早停、模型保存。我见过很多人用框架半年了model.eval() 是什么都不知道——推理时 BN 层还在用 batch 统计量线上效果和离线评估完全对不上。5. 工程管线从零搭建数据、版本、训练、部署一条龙模型能跑通之后真正的“ai-engineering”才刚开始。工程化的核心不再是模型结构而是整个系统的可靠性、可复现性和可维护性。5.1 数据层从零写一个干净的数据加载管道数据管道我建议用最简单的类来封装不急着上复杂框架。核心要满足流式读取不爆内存、可 shuffle、可重复、带有版本标识。一个可用的最小实现长这样import os import random from PIL import Image import numpy as np class SimpleDataset: def __init__(self, image_paths, labels, transformNone, shuffleTrue): self.image_paths image_paths self.labels labels self.transform transform if shuffle: combined list(zip(image_paths, labels)) random.shuffle(combined) self.image_paths, self.labels zip(*combined) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img Image.open(self.image_paths[idx]).convert(RGB) if self.transform: img self.transform(img) return np.array(img), self.labels[idx]这个类看起来简单但有一个容易被忽略的工程细节shuffleTrue必须在构造时完成而不是在 epoch 内部否则每个 epoch 数据顺序一样模型会“背住”顺序。另一个细节是图片的读取尽量用 PIL 而不是 cv2因为 PIL 在 Docker 里依赖更少。5.2 训练层把训练脚本变成可复现的“实验系统”很多人写训练脚本是一个文件从头写到尾超参数写死在代码里。from scratch 阶段我强烈建议你从第一天就养成“配置与代码分离”的习惯。最朴素的做法是用 yaml 或 json 写配置# config.yaml data: train_path: ./data/train val_path: ./data/val batch_size: 64 num_workers: 4 model: hidden_dim: 128 dropout: 0.2 train: lr: 0.01 epochs: 50 weight_decay: 0.0001 scheduler: cosine训练脚本里用 argparse 读进配置文件然后所有超参数都从 config 对象取。这样你后续跑实验、调参、复现结果只需要对比 yaml 文件差异即可而不是翻聊天记录找“上次那个 lr 是多少”。同时建议给每个实验打一个哈希标签比如run_20250115_1430_lr0.01_hd128所有日志、模型权重、tensorboard 曲线都归到这个标签目录下。这个习惯能救你无数次——尤其是你同时跑五个实验一周后回来复盘的时候。5.3 部署层用 ONNX 导出和 Docker 容器把模型交出去模型训练完工程化的临门一脚是部署。我建议的路径是PyTorch 模型导出为 ONNX然后再用 ONNX Runtime 做推理最后打 Docker 镜像。导出 ONNX 的核心代码很短import torch model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )这里最容易被忽略的是dynamic_axes。如果不设置导出的模型固定 batch 只能为 1线上服务一旦并发请求就报错。另一个坑是模型必须处于 eval 模式再导出否则 BN 层和 Dropout 会把训练行为带进推理线上结果错得离谱。Docker 部署我给的方案尽量精简FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, serve.py]serve.py里用 FastAPI 包一层接口import onnxruntime as ort import numpy as np from fastapi import FastAPI from pydantic import BaseModel app FastAPI() sess ort.InferenceSession(model.onnx) class Item(BaseModel): image: list app.post(/predict) def predict(item: Item): data np.array(item.image, dtypenp.float32).reshape(1, 3, 224, 224) result sess.run(None, {input: data}) return {prediction: result[0].tolist()}跑起来之后用 curl 测一下curl -X POST http://localhost:8000/predict -H Content-Type: application/json -d {image: [1.0, 2.0, ...]}到这一步你已经亲手完成了一个“从零开始训练 - 导出 - 容器部署”的完整 AI 工程闭环。这个闭环的价值比单纯把精度刷高几个点重要得多。6. 常见问题排查我在实操中踩过的坑整理成速查表from scratch 的路线走完你一定会在某个环节卡住。我把这些年实操中反复出现的坑按类别整理成速查表每一条都是真实发生过的问题。问题现象可能原因排查步骤解决方案loss 不降反升最终 NaN学习率过大 / 数值不稳定打印每 10 步的梯度范数看是否爆炸降低 lr 到 1e-3 以下检查输入是否归一化训练精度高验证精度低过拟合 / 数据泄露检查训练集和验证集是否重叠增加 dropout增加数据增强加正则化早停显存 OOMbatch_size 过大 / 模型太大用 nvidia-smi 看显存消耗曲线减小 batch_size用梯度累积混合精度训练推理结果与训练不一致模型未设 eval 模式 / 数据预处理不一致对比推理和训练时的预处理代码导出前强制 model.eval()统一图像的归一化参数和通道顺序部署后首次推理很慢未用 ONNX / CPU 推理测一下单次推理时延导出 ONNX 并开启 CPU 优化改用 batch 推理考虑量化重启服务后结果变了模型权重未固定 / 随机种子未设检查 checkpoint 是否稳定加载训练时固定所有随机种子保存完整模型参数和配置这里面有几个值得专门展开说说。梯度范数排查是最实用的技能之一。我训练不收敛的时候首先不是看 loss而是在每个 step 后计算torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)里的总范数。如果这个数值开始指数级增长说明梯度爆炸此时要么减学习率要么加梯度裁剪。我见过新手把 lr 调到 0.1 训练 Transformer梯度范数在 30 步内从 1 涨到 1e10loss 瞬间 NaN换成 1e-4 一下就稳了。数据泄露是个隐性大坑。有次我用一个图像分类数据集自己写了简单的数据切分函数结果因为文件路径排序和 shuffle 逻辑写反了导致同一张图的多个增强版本同时出现在训练集和验证集。模型训练精度 99%验证精度 99%一上线真实数据精度掉到 60%。这个教训让我从此以后每次切分完都会做一个“重复样本检查”跑一跑np.intersect1d看 train 和 val 的样本 index 有没有重叠。eval 模式的问题是教科书不会写的。你用 PyTorch 训练完直接拿 model 做推理BN 层会默认使用当前 batch 的均值和方差——这跟训练时行为一致。但模型在部署时尤其 Docker 容器里batch 通常只有 1BN 统计量噪声极大输出乱跳。解决办法是推理前一定要model.eval()导出 ONNX 前务必 eval。如果你用的是别人封装好的推理代码一定要点进去看有没有这一行。还有两个环境层面的建议。一是固定依赖版本用pip freeze requirements.txt或 Conda 导出 lock 文件不要写模糊版本号如1.9。我有一次升级了 numpy 从 1.x 到 2.x结果一个旧模型的推理结果轻微漂移排查了大半天。二是训练代码里固定所有随机种子包括random.seed、np.random.seed、torch.manual_seed和torch.cuda.manual_seed_all否则你的实验永远不可复现后续调优无从谈起。7. 一些真心话这条路怎么走才不白费如果你看到这里说明已经准备好走这条从零开始的路线了。我的核心建议是别追求把所有理论都弄懂才动手而是先跑通最小闭环再回头补理论。先照着上一章代码把线性回归跑出来再去想“梯度为什么是这样”。先踩一脚 loss NaN 的坑再去看数学推导那种理解深度完全不一样。每次踩完坑建议你顺手记录在一个“实验日志”里现象、原因、排查过程、最终解决。我自己早期踩坑的记录后来基本成了一本私人排查字典工作中同事遇到一样的问题直接翻旧日志就能定位比搜索引擎快得多。ai-engineering-from-scratch 这条路不轻松至少比跟着教程调超参数累。但走完之后你再看那些光鲜的新模型、新框架心里就不再发虚了——你知道底下是怎么长出来的也知道一旦出了问题该往哪儿看。这是我从一个“调包侠”成长为能独立扛起模型生命周期的人走完的最大感受。