1. 这个项目到底在解决什么问题
第一次看到 "ai-engineering-from-scratch" 这个标题,我脑子里蹦出来的第一个念头是:又是一个教人调库的教程仓库?但仔细琢磨了一下 "from scratch" 这几个字,我意识到它想做的事情可能完全不一样。市面上讲 AI 工程的内容,绝大多数都是从import torch或者from transformers import ...开始的,你学到的是怎么用别人的轮子,而不是轮子本身是怎么造出来的。这个项目标题传递的信号很明确:它要带你从最底层开始,把 AI 工程中那些被封装得严严实实的核心组件,一层一层地剥开,用最原始的方式重新实现一遍。
这件事的价值在哪儿?我举个自己的例子。早些年我做推荐系统的时候,TF-IDF 和 BM25 这些概念背得滚瓜烂熟,公式也推得出来,但真到线上出了问题——比如某个 query 的召回结果突然变得莫名其妙——我发现自己根本说不清楚 sklearn 的TfidfVectorizer内部到底做了哪些平滑处理、默认的 tokenizer 是怎么切词的、sublinear_tf 参数打开后数值上发生了什么变化。这就是典型的"会用但不懂"。而 "from scratch" 的学习路径,恰恰是逼着你去面对这些细节。你自己手写一遍 TF-IDF,就会被迫去思考:词频要不要取对数?文档频率要不要加一平滑?停用词表怎么构建?这些看似琐碎的选择,在实际工程中往往就是效果差异的来源。
所以这个项目适合谁?我认为有三类人特别值得投入时间。第一类是有一定编程基础、想转行做 AI 工程但苦于只会调包的开发者,你需要一个系统性的路径把地基打牢。第二类是在工作中已经在用 AI 模型、但总觉得心里没底的工程师,你想知道那些黑盒子里到底发生了什么。第三类是在校学生或者刚入行的新人,你时间充裕,与其碎片化地刷教程,不如跟着一条主线从头到尾走一遍。当然,如果你已经能徒手推导反向传播、手写过 Transformer,那这个项目对你来说可能偏基础了,但拿来查漏补缺也未尝不可。
我打算按照一个完整的 AI 工程学习路径来拆解这个项目应该包含的内容,从最基础的数值计算和数据处理开始,一路走到模型训练、评估和部署。每一块我都会说清楚为什么要从零实现、关键的技术点在哪里、实际操作中容易踩什么坑。这不是一篇简单的项目介绍,而是我结合自己这些年的工程经验,对这个学习路径的一次完整梳理。
2. 整体学习路径的设计逻辑
2.1 为什么不能一上来就搞深度学习
我见过太多人学 AI 的第一天就去跑 MNIST 手写数字识别,用 PyTorch 十几行代码就能达到 99% 的准确率,然后觉得自己入门了。但接下来呢?换一个数据集就不会了,调参全靠试,出了问题完全不知道从哪儿排查。这种学习方式的根本问题在于跳过了太多中间层。从零构建 AI 工程能力,正确的顺序应该是先把数学工具和数据处理的基本功练扎实,再往上走。
具体来说,这条路径应该包含这么几个阶段:数值计算基础(向量、矩阵、梯度)、经典机器学习算法(线性回归、逻辑回归、决策树)、特征工程与数据处理、神经网络基础(前向传播、反向传播、优化器)、再到稍微现代一点的内容(注意力机制、Transformer 的基本组件)。每个阶段都要求你亲手实现核心算法,而不是调库。你可能会问,都什么年代了还手写线性回归?我的回答是:手写线性回归的目的不是让你以后用自己写的版本,而是让你理解梯度下降的每一步在做什么,学习率设大了会怎样,特征没归一化会怎样。这些直觉,调库是调不出来的。
2.2 从零实现和调库的边界在哪里
这里有一个很实际的问题:什么该从零写,什么可以直接用现成的?我的判断标准是这样的——凡是涉及核心算法逻辑的,从零写;凡是涉及工程效率的,用现成的。举个例子,你实现一个简单的神经网络,矩阵乘法可以用 NumPy 的np.dot,不需要自己去写三重循环,因为矩阵乘法的优化是编译器和硬件层面的事情,你手写不可能比 BLAS 库快。但反向传播的链式法则推导和代码实现,必须自己来,因为这是理解神经网络训练过程的关键。再比如,数据加载可以用 pandas 读 CSV,但特征标准化、缺失值处理、类别编码这些逻辑,最好自己实现一遍,这样你才知道StandardScaler到底做了什么。
这个边界感很重要,因为如果什么都要从零写,你会陷入无穷无尽的细节里,比如自己实现一个高效的矩阵乘法,那可能几个月就过去了,而且对理解 AI 工程的核心帮助不大。反过来,如果什么都用现成的,那 "from scratch" 就名不副实了。我的建议是:算法逻辑自己写,数值计算和数据结构用库,工程框架用成熟的。
2.3 学习路径的阶段性目标
我把这条路径分成四个阶段,每个阶段有明确的目标和验收标准。
第一阶段是基础工具期,目标是能用 NumPy 熟练地进行向量化计算,理解广播机制,能手写梯度下降。验收标准是:不查资料,用 NumPy 实现一个多元线性回归,包括损失函数、梯度计算和参数更新。
第二阶段是经典算法期,目标是理解并实现逻辑回归、KNN、决策树、朴素贝叶斯这几个经典算法。验收标准是:在一个真实数据集上,自己实现的算法效果和 sklearn 版本差距在合理范围内(比如准确率差不超过 2%)。
第三阶段是神经网络期,目标是从零实现一个两层神经网络,包括前向传播、反向传播、各种激活函数和损失函数。验收标准是:在 MNIST 或者类似的分类任务上,自己实现的网络能达到 95% 以上的准确率。
第四阶段是现代架构期,目标是理解注意力机制的原理,实现一个简化版的 Self-Attention 和 Transformer Block。验收标准是:能用自己的实现跑通一个简单的序列任务,比如字符级文本生成。
这四个阶段走下来,你对 AI 工程的理解会完全不一样。你不会再害怕看论文里的公式,因为你知道那些公式对应的代码长什么样;你也不会再盲目调参,因为你知道每个参数背后的数学含义。
3. 核心模块的从零实现细节
3.1 数值计算基础:向量化与广播
从零开始的第一步,是把 NumPy 用熟。很多人觉得自己会用 NumPy,但其实只是停留在np.array和np.mean的层面。真正重要的是理解向量化和广播这两个概念。向量化的意思是,尽量用数组级别的操作代替 Python 循环。比如计算两个向量的点积,用np.dot(a, b)比用sum(a[i] * b[i] for i in range(len(a)))快几十倍甚至上百倍。原因在于 NumPy 底层的运算是在 C 语言层面执行的,而且可以利用 SIMD 指令并行处理。
广播机制则是 NumPy 最强大也最容易让人困惑的特性。简单说,当两个形状不同的数组进行运算时,NumPy 会尝试自动扩展维度使它们兼容。比如一个形状为(3, 4)的矩阵和一个形状为(4,)的向量相加,NumPy 会把向量广播成(3, 4),每一行都加上这个向量。这个机制在实现神经网络时极其有用,比如给一个 batch 的所有样本加上偏置项,一行代码就够了。
我建议在这个阶段做几个练习:手写一个不依赖 NumPy 的矩阵乘法(用三重循环),然后和np.dot的结果对比,感受一下性能差距;实现一个 softmax 函数,注意处理数值溢出的问题(减去最大值再取指数);实现一个 sigmoid 函数,观察它在输入很大或很小时的输出特性。这些练习看起来简单,但它们是后面所有内容的基础。
注意:在实现 softmax 时,如果不做数值稳定处理,当输入值很大时
np.exp会溢出变成 inf,导致结果全是 nan。标准做法是先减去每行的最大值,这个技巧在实际工程中非常常用。
3.2 梯度下降的手写实现与调参
梯度下降是机器学习的核心优化方法,但很多人对它的理解停留在"沿着梯度反方向走"这个层面。从零实现一遍,你会遇到很多实际问题。首先是学习率的选择,太大了会震荡甚至发散,太小了收敛太慢。我一般会先用一个较大的学习率(比如 0.1)跑几步,观察损失变化,如果损失在增大就缩小 10 倍,直到损失稳定下降。这个过程叫学习率搜索,虽然原始,但在没有自适应优化器的时候非常有效。
其次是梯度计算。对于简单的线性回归,梯度可以手动推导出来:损失函数对权重的偏导等于预测值和真实值之差的均值乘以对应的特征值。但对于更复杂的模型,手动推导就不现实了,这时候就需要数值梯度来验证。数值梯度的原理是:用有限差分近似导数,即(f(x+h) - f(x-h)) / (2h)。虽然数值梯度计算慢,但它可以作为解析梯度的验证工具。我养成的习惯是,每实现一个新的损失函数和梯度,都会用数值梯度检查一遍,确保解析梯度没写错。
还有一个容易被忽略的点是批量大小的选择。全批量梯度下降每次用所有样本计算梯度,稳定但慢;随机梯度下降每次用一个样本,快但震荡严重;小批量梯度下降是折中方案,也是实际中最常用的。批量大小一般取 32 到 256 之间,具体取决于数据集大小和内存限制。我的经验是,先从 64 开始试,如果训练不稳定就增大,如果收敛太慢就减小。
3.3 从逻辑回归到神经网络的跨越
逻辑回归可以看作是最简单的神经网络——没有隐藏层,只有一个输出层,激活函数是 sigmoid。从逻辑回归过渡到神经网络,关键的变化是引入了隐藏层和非线性激活函数。如果没有非线性激活函数,再多层的神经网络本质上还是线性变换,表达能力有限。常用的激活函数有 ReLU、tanh、sigmoid,现在最常用的是 ReLU,因为它的梯度在正区间恒为 1,不容易出现梯度消失。
从零实现一个两层神经网络,需要写这几个部分:参数初始化、前向传播、损失计算、反向传播、参数更新。参数初始化很关键,不能全部初始化为 0,否则所有神经元的输出都一样,反向传播时梯度也一样,网络永远学不到东西。常用的初始化方法有 Xavier 初始化和 He 初始化,前者适合 tanh 激活函数,后者适合 ReLU。初始化的标准差一般和输入维度的平方根成反比。
反向传播是难点,本质上是链式法则的应用。我建议在实现的时候,把每一层的梯度推导写清楚,然后用数值梯度验证。具体来说,对于每一层,你需要计算三个梯度:损失对权重的梯度、损失对偏置的梯度、损失对输入的梯度(用于传给前一层)。这三个梯度的计算都依赖于损失对输出的梯度,也就是从后一层传回来的梯度。理解了这个链条,反向传播就不神秘了。
3.4 注意力机制的核心原理与实现
注意力机制是 Transformer 的核心,也是现代 AI 工程中绕不开的内容。从零实现一个 Self-Attention,需要理解 Query、Key、Value 这三个概念。简单类比:Query 是"我在找什么",Key 是"我有什么",Value 是"我实际的内容"。注意力权重的计算方式是 Query 和 Key 的点积,经过缩放和 softmax 归一化后,再对 Value 加权求和。
缩放这一步很重要,点积的结果会随着维度增大而增大,如果不缩放,softmax 后的分布会非常尖锐,梯度会变得很小。缩放因子是维度的平方根,这个设计在原始论文里有详细解释。实现的时候还要注意 mask 的处理,比如在解码器中,当前位置不能看到未来的位置,需要用 mask 把未来位置的注意力权重设为负无穷,这样 softmax 后这些位置的权重就变成 0 了。
多头注意力是在这个基础上,把 Query、Key、Value 分成多个头,每个头独立计算注意力,最后拼接起来。这样做的好处是,不同的头可以关注不同的模式,比如一个头关注语法关系,另一个头关注语义相似度。从零实现多头注意力,关键是把维度切分和拼接的逻辑写对,这里很容易出现维度不匹配的错误。
4. 实操过程中的关键环节
4.1 环境搭建与工具选择
虽然这个项目强调从零实现,但基本的开发环境还是要搭好的。我的推荐配置是:Python 3.10 以上,NumPy 用于数值计算,Matplotlib 用于可视化,Jupyter Notebook 用于实验和调试。不需要装 PyTorch 或 TensorFlow,因为整个学习过程就是手写这些框架的核心功能。编辑器用 VS Code 或者 PyCharm 都可以,关键是配好代码补全和调试功能。
虚拟环境一定要用,我见过太多人因为包版本冲突浪费大量时间。用python -m venv ai-scratch创建一个干净的环境,然后pip install numpy matplotlib jupyter就够了。如果你需要处理真实数据集,再加一个 pandas。版本方面,NumPy 建议用 1.24 以上,因为一些新的 API 在老版本里没有。
提示:在 Jupyter Notebook 里做实验时,记得经常重启内核并从头运行。因为 Notebook 的变量是全局的,很容易出现"改了代码但结果没变"的情况,其实是旧变量还在内存里。
4.2 数据预处理的手写实现
真实数据从来不是干净的,缺失值、异常值、类别不平衡这些问题都会遇到。从零实现数据预处理,你需要写这几个函数:缺失值填充(均值、中位数、众数)、特征标准化(Z-score 归一化)、类别编码(独热编码、标签编码)、训练集测试集划分。这些函数都不复杂,但自己写一遍,你会对每个步骤的细节有更深的印象。
以特征标准化为例,公式是(x - mean) / std。看起来简单,但有几个坑:计算 mean 和 std 只能用训练集,不能用整个数据集,否则会造成数据泄露;如果某个特征的 std 为 0,除法会出错,需要加一个很小的 epsilon;对于稀疏数据,标准化会破坏稀疏性,这时候可能需要用 MaxAbs 归一化代替。这些细节,调库的时候你根本不会注意到,但自己写就会被迫面对。
4.3 模型训练与评估的完整流程
一个完整的模型训练流程包括:数据加载、模型初始化、前向传播、损失计算、反向传播、参数更新、验证集评估、早停判断。从零实现这个流程,我建议写成一个类,把训练相关的逻辑封装起来。这样每换一个模型,只需要改前向传播和反向传播的部分,训练循环可以复用。
评估指标的选择也很重要。分类任务常用准确率、精确率、召回率、F1 分数,回归任务常用均方误差、平均绝对误差、R 平方。从零实现这些指标,你会更清楚它们的定义和适用场景。比如准确率在类别不平衡的时候会失真,这时候就需要看 F1 或者 AUC。我一般会在训练过程中同时记录多个指标,这样能更全面地了解模型的表现。
早停是防止过拟合的常用手段。具体做法是,在每个 epoch 结束后计算验证集上的损失,如果连续几个 epoch 验证损失都没有下降,就停止训练。这里的"几个"是一个超参数,一般取 5 到 10。早停的好处是避免模型在训练集上过度拟合,同时节省训练时间。
4.4 调试与验证的实用技巧
从零实现算法,调试是家常便饭。我总结了几条实用的调试技巧。第一,用小数据验证。在完整数据集上跑之前,先构造一个很小的数据集(比如 10 个样本),手动计算预期结果,然后对比你的实现。第二,梯度检查。前面提到的数值梯度验证,是检查反向传播是否正确的最可靠方法。第三,可视化中间结果。比如把损失曲线画出来,如果损失不下降或者震荡严重,说明学习率或者梯度有问题。第四,单元测试。给每个函数写测试用例,确保输入输出符合预期,这样修改代码时不会引入新的 bug。
还有一个很实用的技巧是过拟合一个小批量。取 10 个样本,用你的模型去训练,如果模型能把这 10 个样本的损失降到接近 0,说明模型的前向传播和反向传播基本正确。如果连这 10 个样本都拟合不了,那肯定有 bug。这个方法能快速定位问题是出在模型实现上还是数据上。
5. 常见问题与排查实录
5.1 损失不下降的排查思路
损失不下降是从零实现时最常见的问题。排查顺序一般是这样的:先检查数据,确保输入和标签的对应关系没错,特征没有全为 0 或者全为 NaN;再检查前向传播,手动算一个样本的输出,看看和预期是否一致;然后检查损失函数,确保公式没写错;最后检查梯度,用数值梯度对比解析梯度。根据我的经验,问题出在梯度上的概率最大,尤其是反向传播的链式法则写错了,或者某个地方的转置忘了。
还有一个容易被忽略的原因是学习率太小。如果学习率是 1e-8,损失几乎不会变化,看起来就像没在训练。这时候把学习率调大几个数量级试试。反过来,如果损失变成 nan,那多半是学习率太大导致发散了。
5.2 梯度消失与梯度爆炸的应对
梯度消失和梯度爆炸是深层网络的经典问题。梯度消失的表现是,靠近输入层的参数几乎不更新,损失下降非常慢;梯度爆炸的表现是,损失突然变成 nan,参数值变得极大。从零实现的时候,你可以通过打印每一层的梯度范数来观察这个问题。如果某一层的梯度范数接近 0,说明梯度消失了;如果梯度范数非常大(比如 1e10),说明梯度爆炸了。
应对梯度爆炸,最直接的方法是梯度裁剪,即如果梯度的范数超过某个阈值,就按比例缩放。这个阈值一般取 1 到 5。应对梯度消失,可以用 ReLU 代替 sigmoid,或者用残差连接。另外,合理的参数初始化也能缓解这个问题,比如 He 初始化就是专门为 ReLU 设计的。
5.3 过拟合的识别与处理
过拟合的表现是,训练集上的损失持续下降,但验证集上的损失先下降后上升。识别过拟合很简单,画一条训练损失和验证损失的对比曲线就一目了然。处理过拟合的方法有几种:增加数据量、数据增强、正则化(L1、L2)、Dropout、早停。从零实现的时候,L2 正则化最容易加,只需要在损失函数里加上权重的平方和乘以一个系数。Dropout 稍微复杂一点,需要在训练时随机将一些神经元的输出置为 0,测试时则用所有神经元但输出要乘以保留概率。
我个人的经验是,先加 L2 正则化和早停,这两个方法简单有效。如果还是过拟合,再考虑 Dropout 和数据增强。数据增强对图像任务特别有效,比如随机裁剪、翻转、旋转,但对表格数据效果有限。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 损失为 nan | 学习率过大、除零、log(0) | 打印中间值,检查是否有 inf/nan | 减小学习率,加 epsilon,用数值稳定版本 |
| 损失不下降 | 梯度错误、学习率过小、数据问题 | 数值梯度检查,检查数据分布 | 修正梯度,调整学习率,清洗数据 |
| 验证损失上升 | 过拟合 | 对比训练和验证损失曲线 | 加正则化,早停,增加数据 |
| 梯度为 0 | 激活函数饱和、初始化不当 | 打印每层梯度范数 | 换激活函数,改初始化方法 |
| 训练速度慢 | 未向量化、批量太小 | 检查是否有 Python 循环 | 向量化,增大批量 |
| 准确率不变 | 类别不平衡、模型太简单 | 检查标签分布 | 重采样,增加模型复杂度 |
6. 从学习到实践的转化建议
6.1 如何把从零实现的经验用到工作中
你可能会问,工作中都是用 PyTorch 或者 TensorFlow,手写这些有什么用?我的体会是,手写一遍之后,你用框架的方式会发生变化。以前调参是盲目的,现在你知道每个参数背后的数学含义,调起来有方向。以前遇到 bug 是懵的,现在你能根据现象快速定位是数据问题、模型问题还是优化问题。以前看论文是痛苦的,现在你能把论文里的公式和代码对应起来,理解速度快很多。
具体来说,我建议在学完每个模块后,做一个小项目来巩固。比如学完线性回归,用真实房价数据做一个预测;学完逻辑回归,做一个垃圾邮件分类;学完神经网络,做一个手写数字识别;学完注意力机制,做一个简单的文本分类。这些项目不需要多复杂,关键是把从零实现的代码跑通,然后和框架版本对比效果。
6.2 后续进阶方向
走完这条从零实现的路径后,你可以往几个方向深入。一个是性能优化,学习如何用向量化、并行计算、GPU 加速来提升训练效率。另一个是模型架构,深入研究 Transformer、BERT、GPT 这些现代架构的设计细节。还有一个是工程部署,学习如何把训练好的模型部署到生产环境,包括模型压缩、量化、服务化等内容。
我个人觉得,从零实现的价值不仅在于知识本身,更在于它培养的一种思维方式——遇到任何黑盒,你都有能力把它拆开看看里面是什么。这种能力在 AI 工程领域尤其重要,因为这个领域变化太快,今天流行的框架明天可能就被替代了,但底层的数学原理和工程思想是相对稳定的。
6.3 学习节奏与时间分配
最后说一下学习节奏。这条路径如果全职投入,大概需要两到三个月;如果业余时间学习,可能需要半年左右。我的建议是不要赶进度,每个模块都要确保真正理解了再往下走。具体的时间分配可以是:数值计算基础一周,经典机器学习算法三到四周,神经网络四到六周,注意力机制和 Transformer 三到四周。剩下的时间用来做项目和复习。
学习过程中遇到卡壳是很正常的,我的经验是,如果一个概念看了两小时还没懂,就先跳过去,继续往下学,很多时候后面的内容会帮你理解前面的。另外,找一个学习伙伴或者加入一个学习社区也很有帮助,遇到问题可以讨论,比自己死磕效率高得多。
我在实际带新人的过程中发现,那些愿意花时间从零实现的人,后面成长速度明显快于只会调库的人。因为前者建立的是完整的知识体系,后者建立的是零散的操作技能。当遇到没见过的问题时,前者能推理出解决方案,后者只能搜索有没有人遇到过同样的问题。这个差距,在职业生涯的早期可能不明显,但越往后越关键。