
以独立开发者身份或者以一个想搞懂AI原理的工程师身份把一个叫ai-engineering-from-scratch的学习项目当作主线来写。整个博文的语言风格贴近一个写过不少代码、踩过不少坑的工程师在社区里分享心得的口吻。1. 为什么“从零开始”是AI工程能力的分水岭我最早接触AI工程时和大多数人一样把所有精力都花在了“怎么用”上装环境、拉模型、调接口、改提示词。当时觉得AI工程嘛无非是把现成的模型跑起来把数据喂进去然后等结果。直到有一次线上模型输出质量突然劣化我连问题出在哪里都无从下手——数据变了模型被重新部署了还是推理服务的内存出了异常那一刻我才意识到如果只会调用而不会拆解AI工程师的“工程”二字其实是空的。所以当我把项目命名为ai-engineering-from-scratch的时候想得很清楚不是要重新造一个ChatGPT而是要沿着一条“从零构建”的路径把AI系统的每个环节亲手搭一遍。数据怎么变成样本样本怎么变成损失损失怎么变成梯度梯度怎么更新参数参数怎么服务于推理推理结果怎么被评估和监控——这些链路只有自己写过、调过、坏过、修过才算真正内化。这也是为什么我说“从零开始”是一个分水岭。会调库的人很多能解释清楚某个模块为什么这样设计的人很少。两者之间的差距不是信息差而是判断力。信息可以检索判断力只能靠亲手做项目来积累。ai-engineering-from-scratch这个项目的价值不在最终产出的模型好坏而在走完这条路之后你再去看任何现成框架的文档看到的不是一堆配置文件而是一张张你能对号入座的图纸。这个项目适合两类人一类是刚入门不久希望摆脱“只会调参”状态的工程师另一类是已经在用现成框架做业务但遇到线上问题总感觉隔着一层雾的开发者。如果你属于其中任何一类下面这条从零路径应该能帮你在短时间内把散落的知识点串成一条完整的线。2. 开工前的知识地图把七个环节刻进脑子里在动手敲代码之前我先说一个容易犯的毛病很多人一上来就深度学习框架结果前置概念糊里糊涂后面越写越虚。from scratch的本质是“每一步都知道自己在做什么”所以知识地图必须提前铺好。2.1 AI工程不等于模型训练全链路七段图我习惯把AI工程拆成七段业务问题定义、数据采集与清洗、样本切分与编码、模型设计与实现、训练与调优、推理部署、评估与监控。很多教程只讲中间三段但真正的工程问题往往出在第一段和最后一段。业务问题没定义清楚后面全白做线上没有监控模型劣化了你都不知道。这里分享一个我用过的例子假设我们要做一个中文评论的情感极性分类器判断一条评论是正向、负向还是中性。这个任务足够小但能完整覆盖七段链路。业务问题定义就是“情感极性三分类”数据采集可能是一批公开的电商评论样本编码要决定分词方式和词表大小模型设计可以先从最简单的词袋逻辑回归开始再逐步过渡到神经网络训练环节要处理学习率、批量大小和过拟合推理部署要考虑并发和延迟评估监控要跟踪准确率、召回率以及线上分布漂移。七段走完一个小而完整的AI工程闭环就成立了。2.2 数学最低配向量、矩阵、导数与概率不要被数学吓退。from scratch阶段真正绕不开的数学只有四块向量和矩阵运算因为数据在模型里就是张量、导数与链式法则因为反向传播的本质就是链式求导、概率基础因为分类任务的损失函数来自概率视角、以及简单的统计概念均值、方差、分布。举个例子反向传播看起来高深拆开就是“复合函数的链式求导”。你不需要会手推复杂公式但至少要能看懂一个标量经过若干矩阵乘法后误差如何回传。为了练这个我建议你用NumPy裸写一个两层网络的前向和反向跑通一个二分类任务。这一步哪怕花掉两三天也极其值得。框架帮你自动求导但你亲手算过一次之后遇到梯度爆炸、梯度消失这类问题才有直觉去定位。2.3 工具栈取舍为什么先用NumPy而不是直接上PyTorch我的建议是第一个模型用NumPy写第二个模型开始再上PyTorch。原因很简单——from scratch的目的不是生产最优模型而是理解模型。NumPy让你亲手写矩阵乘法和损失函数中间没有任何隐藏的魔法。等你把梯度的流动写清楚再切到PyTorch看到loss.backward()时会有一种“原来它替我做的是这一步”的通透感。具体工具栈方面Python是必须的NumPy是第一个依赖后面可以引入scikit-learn做数据切分和评估指标再往后才轮到PyTorch。Jupyter Notebook适合探索正式的脚本建议用普通Python文件组织方便做版本管理和自动化运行。3. 数据生产线从原始文本到可训练样本的完整链路数据环节是ai-engineering-from-scratch里最枯燥却最不能跳过的一环。很多模型看起来“不行”根子往往不是模型结构不够先进而是数据管线做得粗糙。3.1 一条四步流水线采集、清洗、切分、张量化以中文评论情感分类为例我搭的数据流水线分四步。第一步是采集。公开数据集或爬虫都行但要注意数据来源的合法性和代表性。如果一个模型只在某一种风格的评论上训练换个平台就水土不服。第二步是清洗。去掉HTML标签、乱码字符、重复记录筛掉过长和过短的噪声样本。这里有一个容易被忽略的点中文文本的编码问题。早期我踩过不少坑比如CSV文件用GBK保存导致乱码或者繁体简体混在一起没有归一化。清洗环节必须显式处理这些否则后面张量化时会莫名其妙报错。第三步是切分。训练集、验证集、测试集的比例一般是8:1:1。切分时要保证类别分布大致均匀最好使用分层抽样避免某类样本全跑进测试集。这一条线做不好你后面看到的评估指标全是幻觉。第四步是张量化。把文本转成数字张量先分字或分词再映射到词表索引最后填充或截断到固定长度。中文场景下字符级还是词级各有取舍。字符级词表小、覆盖稳但语义粒度粗词级语义好但需要分词器且词表可能很大。起步阶段我推荐从字符级开始简单直接后面再按需升级。3.2 词表构建与序列填充的隐性坑词表构建有几个坑必须提前讲。第一个坑是频率截断低频词全部保留会让词表巨大计算量飙升且容易过拟合至少要设定一个最小出现次数比如出现次数小于2的词直接归入unk。第二个坑是填充符号的干扰填充到固定长度时填充位不能参与损失计算否则模型会学着关注无意义的位置。第三个坑是标签偏移分类标签建议从0开始编号不要中间跳号否则框架的损失函数会直接报错或者静默分错类。4. 模型骨架用手写梯度下降逼出对“学习”的本质理解走到这一步才算真正碰到from scratch的核心。这里我会用一小段可运行的NumPy代码展示最小神经网络然后逐行解释它背后的物理含义。4.1 最小可跑系统一个隐藏层的二分类网络假如我们要区分一条评论是正向还是负向先用词袋特征把文本变成固定长度的向量。每一条样本就是一个维度为vocab_size的向量。模型设计为输入层到隐藏层比如64维经过ReLU激活再到输出层一个标量经过Sigmoid得到概率。下面是核心代码省略了大部分初始化细节import numpy as np def sigmoid(x): return 1 / (1 np.exp(-np.clip(x, -500, 500)) def forward(X, W1, b1, W2, b2): hidden np.maximum(0, X W1 b1) # ReLU logit hidden W2 b2 prob sigmoid(logit) return hidden, prob def backward(X, y, prob, hidden, W2): m X.shape[0] d_logit (prob - y) / m # 输出层梯度 dW2 hidden.T d_logit db2 np.sum(d_logit, axis0) d_hidden d_logit W2.T d_hidden[hidden 0] 0 # ReLU反向 dW1 X.T d_hidden db1 np.sum(d_hidden, axis0) return dW1, db1, dW2, db2这段代码很朴素但它包含了神经网络学习的全部要素。前向过程是“输入通过参数映射到预测”反向过程是“误差按链式法则流回每个参数”参数更新则是朝负梯度方向迈一步。你亲手写了这几个矩阵乘法之后再去看PyTorch的自动求导内心会踏实很多。4.2 用代码看到学习发生损失曲线与梯度检查训练过程中最直观的证据是损失值逐轮下降。但如果你的损失没降不要急着改模型先做三件事一是检查梯度计算是否正确可以用数值梯度对比的方式——对某个参数加一个极小扰动计算损失变化除以扰动和反向传播算出的梯度对比误差应该在1e-5量级二是把学习率调小一点排除发散的可能三是检查输入数据是否存在NaN或极端值。还有一种情况损失在某个值附近反复震荡。这往往不是代码错误而是学习率太大参数在局部最优附近来回跳跃。把这个经验记下来它会在后面救你很多次。4.3 回归与分类同一个骨架如何适配不同任务同一套骨架换一个损失函数就能适配不同任务。二分类用二元交叉熵多分类用交叉熵加Softmax回归用均方误差。区别只在输出层和损失函数前面的隐藏层完全复用。这个理解非常关键因为后面无论做推荐、文本生成还是多模态你都会发现底层的“学习”机制是一致的定义损失、算梯度、更新参数。5. 训练工程化把“能跑”变成“可复现、可监控、可恢复”写到这里你已经有了一个能学习的模型。但现在它还很脆弱——换一个人复现结果可能完全不同训练中断一次就得从头再来。训练工程化要解决的就是这些问题。5.1 超参数应该先固定什么从轮数、批量大小到学习率新手最大的困惑是超参数太多不知道先调哪个。我的建议是先固定批量大小等于16或32然后固定训练轮数先设一个足够大的值靠早停机制来截断最后再调学习率。学习率是整个系统里最敏感的超参数建议用对数网格搜索比如从0.1、0.01、0.001到0.0001每个值跑一个小实验观察损失曲线。我个人的经验是学习率设得太大损失曲线会像锯齿一样震荡设得太小损失下降缓慢得像蜗牛。找到一个损失平滑下降且增速放缓的学习率再往下调其他参数。5.2 种子、日志与Checkpoint训练翻车的救生索训练工程化的三个基本功缺一不可。第一是随机种子。数据切分、权重初始化、批量采样都可能引入随机性。把随机种子固定下来训练结果才可复现。我在项目里会用一组固定的种子列表跑多次实验拿均值报告结果避免“单次跑运好”的假象。第二是结构化日志。每完成一个Batch、一个Epoch都应该记录时间戳、损失、准确率、当前学习率。日志别只输出到屏幕落盘成文件。这样出了问题时你能回放训练历史而不是凭着记忆猜。第三是Checkpoint。每隔N个Epoch保存一次模型权重和优化器状态。恢复训练时不仅权重要恢复优化器的动量和学习率调度状态也要恢复否则训练轨迹会断掉。这个小细节是好多人的盲区经常出现“恢复之后损失反而暴涨”的怪事原因就是优化器状态丢了。5.3 过拟合的战争早停、正则与验证集纪律模型在训练集上表现好、在验证集上崩溃这是永恒的斗争。早期模型因为容量小过拟合反而不明显一旦隐藏层变宽或Transformer结构入场过拟合会立刻成为主要矛盾。我的防线有三个严格隔离验证集验证集绝不参与任何形式的训练决策带早停的训练循环如果验证损失连续N轮不下降就终止并恢复最佳权重以及基础的L2正则或Dropout。正则强度也是用验证集调出来的不要在一个样本上反复试那样验证集也“脏”了。6. 推理优化从“算得出”到“扛得住”训练只是前半程部署才是AI工程师真正烧钱和烧脑的地方。这节只讲入门级优化不涉及太多分布式推理。6.1 预热、批处理与缓存推理慢的三个隐藏原因很多人第一次把模型部署上线后发现推理速度远低于预期。排查顺序是这样的。第一看是否做了模型预热。大部分深度学习框架有延迟初始化机制第一次推理会触发显存分配和算子编译比后续推理慢几倍甚至几十倍。所以服务启动后必须用一条真实样本跑一次推理作为预热再暴露对外端口。第二看是否利用批处理。如果你每来一条请求就做一次前向计算资源利用率会非常低。可以在服务端攒一批请求合并成一个大的张量做一次前向整体吞吐可以提升好几倍。代价是单条延迟略涨但吞吐收益通常值得。第三看是否做特征缓存。线上很多请求的输入其实高度重复比如热门商品评论。如果你在服务端多一层特征或结果缓存命中时直接返回可以显著减少计算压力。6.2 量化与裁剪入门级优化手段的安全边界模型量化是压缩模型的利器从32位浮点降到16位甚至8位推理速度加快显存占用下降。但精度多多少少会损失尤其对分类边界模糊的样本。建议先做16位半精度对大部分模型而言损失微小收益直接。8位量化要谨慎先在验证集上对比精度再做决定。另一个手段是结构裁剪删除不重要的神经元或注意力头。但这个操作对初学者来说风险较高我建议至少先跑通基线再引入结构化剪枝库而不是自己手写剪枝逻辑。6.3 线上监控推理质量劣化如何早发现监控往往被低估但它远比想象中重要。至少要监控三个指标请求延迟的百分位值例如P99而不是简单地看平均值输入特征的分布变化比如用户评论的平均长度是否突然变化以及预测置信度的分布如果模型越来越“犹豫”往往意味着分布漂移。一个典型的劣化场景是模型上线时准确率不错三个月后线上数据风格变化新词大量出现模型输出质量持续下滑。如果没有监控你只能等用户投诉有了监控你能在指标开始下滑的第一时间拉响警报并触发重新训练流程。这层意识是从写Demo到做产品的分界线。7. 一路下来踩过的坑排查链路与解决顺序做ai-engineering-from-scratch项目的过程大部分时间不是在写新代码而是在排查已经存在的bug。我整理了三个最典型的坑附上完整的排查顺序。7.1 损失不降的排查顺序从数据到模型逐层定位当你的损失曲线像心电图一样平稳到令人绝望时按这个顺序查。第一检查数据振型。打印前几个batch的输入张量和标签确认特征数值范围正常、标签没有错位。我遇到过标签文件行尾多了个看不见的符号导致整体标签偏移一位模型学了半天等于在学一个错误映射。第二检查初始化。把所有权重初始化为小随机数不要用零初始化。零初始化会让所有神经元对称更新整个网络退化成线性模型损失永远降不下去。第三增大学习率试一发。如果损失连变都不变说明梯度根本没传回来问题大概率在前向或反向的某个矩阵形状上。这时候把网络简化成单层逐步加回来定位坏层。7.2 训练集指标好但验证集崩盘对症下药而不是盲目加数据模型在训练集上表现优异但验证集一塌糊涂首选不是加数据而是先确认验证集的分布是否和训练集一致。我踩过一次爬虫策略导致验证集里某一类的文本长度整体偏长模型天然学不好。把验证集重新用相同流水线再切一次后指标立刻正常。如果确认验证集没问题那就是真过拟合。先加正则、加Dropout、缩小模型容量最后再考虑加数据。盲目加数据有时反而让分布更偏。7.3 工程护栏那些花半小时能省三天的笨功夫这个项目让我养成了一些写代码时的“笨习惯”现在回头看每一件都值回票价。数据管线每个环节都记录版本号。清洗脚本改了、词表重建了都要有明确的版本标识否则实验结果无法对应到具体数据。模型实验跑完立刻写实验报告哪怕只有三行字数据版本、超参数、结果指标。模型和数据集分开管理按日期和指标命名不要积攒一堆“final_final_v3”。这些习惯看起来和AI没什么关系但就是这些“笨功夫”让我在一次训练中断后能恢复到前一天的完整状态省下了整整三天的重跑时间。在真实的工程环境里这些护栏比一个花哨的模型结构更值钱。8. 下一站从手写模型到理解大模型的进阶路线当你把上面的项目完整走通一遍之后你会发现自己已经具备了看懂大模型技术文档的底层能力。接下来怎么走我给出一个清晰的进阶路线。8.1 先给自己定一个小里程碑不要直接挑战从零写一个大模型这既不现实也容易挫败。建议给自己定一个可验收的里程碑比如“用NumPy手写一个多层感知机并在公开数据集上达到90%准确率”或者“用纯Python实现一个简单的情感分析服务支持并发请求和缓存”。里程碑越具体越容易衡量进度。完成第一个里程碑后下一步是把模型切换到PyTorch重写一遍同一个任务。你会明显感觉到框架替你省了多少功夫但同时也更清楚“省”的背后是什么。这种对比学习是提升内功的最快路径。8.2 手写推理过程从词向量到自注意力大模型的核心亮点自注意力机制本质也是一种可微分的计算图。你不必从头实现完整的大模型但可以手写一个小型自注意力层比如单头注意力在一个玩具数据集上验证它的前向和反向。这一步走通之后再去看Transformer的论文或开源代码你会产生一种亲切感这不是黑盒而是我写过的那种东西的规模化版本。8.3 沿着从零构建的路线图持续挖深很多人问下一步该学什么。我的建议是顺着数据的上游和下游继续深挖数据工程包括分布式数据采集、数据质量治理、数据版本化训练加速包括混合精度训练、梯度累积、分布式数据并行评估与对齐包括评测集设计、错误分析、人类反馈信号的使用。如果只是想快速体验可以考虑复现一篇经典的模型构建文章例如手写一个小型语言模型并在开放语料上预训练几百步观察它在小规模上如何学会字词搭配和句法结构。别把目标定得太高跑出一点可观察的语言规律就够了。这个过程会给你的AI工程认知补上最关键的一块拼图原来那些看似智能的行为底层就是我们一路走来的前向、反向、更新三步循环。我个人在这条路上最大的体会是从零开始的项目很少会直接产出惊艳的东西但它给你留下的是一种“我能看穿黑盒”的信心。以后再遇到新模型、新框架你不会焦虑因为你已经掌握了解构它的底层语言。把这套动手能力保持住你的AI工程之路会越走越稳。