这是一篇前置讲,不对应原课的某一次课。原课的先修写着”最好学过机器学习,但不强制”,第 2 讲开头就用一个文本分类网络(嵌入查表 → 平均池化 → 线性层 → softmax)说明神经网络会被拆成哪些底层运算,第 5 讲讲”神经网络的通用迭代学习算法”和自动微分。这篇为没有学过机器学习的读者把这些概念补齐,并用 NumPy 从零实现那个文本分类网络、手写它的反向传播。 依据说明:本讲讲的是标准的机器学习 / 深度学习基础,参考 Goodfellow, Bengio & Courville《深度学习》第 6–8 章,以及几篇原始论文:He et al. (2015)(ReLU 网络的初始化)、Ba et al. (2016) LayerNorm、Srivastava et al. (2014) dropout、Kingma & Ba (2015) Adam、Loshchilov & Hutter (2019) AdamW。文本分类网络的结构取自原课第 2 讲的 slides;影评小语料和全部实验都是本讲义自己设计的。 配套代码 Code:
code/prereq3/(4 个脚本 + 1 个模块;只需要 numpy;每个脚本 1–4 秒) 预计阅读时间:3–4 小时(含运行代码) 建议阅读时机:第 5 讲之前;如果完全没接触过神经网络,第 1 讲之前读一遍 §1–§2 会更顺
0. 本讲导航 · How to read this lecture
0.1 为什么要懂这些
这门课是讲”系统”的,但系统要服务的对象是神经网络的训练和推理。不懂网络本身,很多系统设计就无从理解:
- 第 1 讲说”训练的计算量约是前向的 3 倍”——为什么是 3?
- 第 1 讲说”每个参数要 16 字节”——其中 8 字节是 Adam 的”一阶矩和二阶矩”,那是什么?
- 第 10 讲的 LightSeq 把 softmax 和交叉熵”融合”成一个内核,依据是”交叉熵对 logits 的梯度 = p − y”——这个公式怎么来的?
- 原课 HW2 要你实现 miniTorch 的反向传播,HW4 要你写 LayerNorm 的反向内核——反向传播到底在算什么?
0.2 学习目标 Learning goals
- 说出训练循环的四步(前向、损失、反向、更新),理解梯度下降为什么能找到好的参数。
- 写出线性层的前向和反向公式;用实验说明为什么网络必须有非线性;比较 ReLU、sigmoid、tanh、GELU。
- 理解深层网络中激活值的爆炸与消失,以及初始化和 LayerNorm 怎样让它稳定。
- 写出数值稳定的 softmax 和交叉熵,推出交叉熵对 logits 的梯度 p − onehot(y),并解释为什么分类不用均方误差。
- 理解学习率、动量、Adam、小批量各自解决什么问题,知道优化器要为每个参数多存几个数。
- 从零实现原课第 2 讲的文本分类网络(含嵌入层的 scatter-add 反向),用梯度检查验证;能解释它在哪些句子上必然失败、为什么。
- 理解训练集、验证集、测试集,过拟合以及 dropout、权重衰减的作用。
0.3 本讲地图 Map
§1 从线性模型到神经网络:训练循环;线性回归;线性层;非线性;激活函数;深度与归一化;反向传播 ← 代码 01
§2 softmax 与交叉熵:温度;溢出与减最大值;梯度 p − y;为什么不用均方误差;读写几遍 ← 代码 02
§3 优化:学习率与条件数;动量;Adam / AdamW;小批量;学习率调度;优化器状态的显存 ← 代码 03
§4 一个完整的文本分类网络:数据;结构与底层运算;逐层的前向与反向;梯度检查;它学不会什么 ← 代码 04 第 1–4、6 部分
§5 泛化:训练 / 验证 / 测试;过拟合;dropout 与权重衰减 ← 代码 04 第 5 部分
§6 和后面讲次的联系 §7 小结 §8 常见误区 §9 练习 §10 延伸阅读 §11 下一篇预告
0.4 先修
导数和链式法则;矩阵乘法;会用 NumPy。不需要任何机器学习经验。
1. 从线性模型到神经网络 · From linear models to neural networks
1.1 训练循环:四步
原课第 5 讲把神经网络的学习算法写成一个通用的迭代过程。几乎所有模型的训练(包括千亿参数的 LLM)都是这四步的循环:
重复很多次:
1. 前向(forward):用当前参数 θ,根据输入 x 算出预测 ŷ = f(x; θ)
2. 损失(loss): 算出预测和真实答案 y 差多少:L = loss(ŷ, y)
3. 反向(backward):算出损失对每个参数的梯度 ∂L/∂θ
4. 更新(update): 沿梯度的反方向走一小步:θ ← θ − η · ∂L/∂θ(η 是学习率)
梯度指向”损失增长最快的方向”,往反方向走,损失就会下降。整门课里,第 2–4、10–13、21 讲在加速第 1、3 步,第 5 讲让第 3 步自动完成,第 14–18 讲让这四步能在上千块 GPU 上一起做。
1.2 线性回归:最简单的例子
数据 ,模型 ,损失是均方误差 ,梯度:
01 号脚本第 1 部分在 256 个三维样本上,用学习率 0.1 做梯度下降:
| 步数 | 均方误差 | w | b |
|---|---|---|---|
| 0 | 1.7827 | [0, 0, 0] | 0 |
| 5 | 0.1582 | [−0.548, 0.363, 0.501] | 0.364 |
| 20 | 0.00783 | [−0.752, 0.517, 0.728] | 0.485 |
| 200 | 0.00772 | [−0.756, 0.5218, 0.736] | 0.4866 |
200 步后和最小二乘的解析解 w = [−0.756, 0.5218, 0.736]、b = 0.4866 一致到小数点后 4 位。(生成数据用的真值是 [−0.7557, 0.5313, 0.7384] 和 0.5——有噪声、样本有限,“对这批数据最好”的参数和真值不完全一样。)
线性回归有解析解,神经网络没有,只能靠梯度下降。所以”怎样算梯度”(§1.7、第 5 讲)和”怎样算得快”(整门课)这么重要。
1.3 线性层
神经网络的基本积木是线性层(也叫全连接层)。对一批 个样本,每个是 维向量,排成矩阵 :
被广播加到每一行上(P1 §5.4:步长为 0 的视图)。参数量 ;每个样本的计算量是 次浮点运算(每个权重一次乘、一次加)——这就是第 1 讲”前向每个参数每个 token 约 2 FLOPs”的来历。
1.4 为什么要有非线性
把两个线性层叠起来:——还是一个线性层。01 号脚本第 2 部分验证了两者只差 (浮点舍入)。没有非线性,叠多少层都等于一层,只能画出直线(超平面)形状的决策边界。
所以每个线性层之后要接一个逐元素的非线性激活函数:,。这叫多层感知机(MLP)。在三条相互缠绕的螺旋臂上(600 个点、3 类):
| 模型 | 训练集准确率 |
|---|---|
| 线性 softmax 分类器 | 0.525 |
| 一个 ReLU 隐藏层,宽度 8 | 1.000 |
| 一个 ReLU 隐藏层,宽度 64 | 1.000 |
直觉:每个 ReLU 神经元 在输入空间里沿一条直线”折一下”;几条折痕拼起来,就能围出弯曲的边界。
1.5 激活函数
01 号脚本第 3 部分:
| 函数 | 公式 | x = −3 的导数 | x = 0.1 的导数 | x = 3 的导数 | 特点 |
|---|---|---|---|---|---|
| ReLU | 0 | 1 | 1 | 简单、不饱和;负半轴梯度为 0(“死亡 ReLU”) | |
| sigmoid | 0.045 | 0.249 | 0.045 | 两端饱和,导数最大只有 0.25 | |
| tanh | 0.010 | 0.990 | 0.010 | 两端饱和 | |
| GELU | (常用 tanh 近似) | −0.012 | 0.580 | 1.012 | 平滑版的 ReLU,GPT / BERT 的 MLP 用它 |
反向传播时梯度要一层层乘上这些导数。sigmoid 和 tanh 在 大时导数接近 0(饱和),乘几层就没了——梯度消失,这是早期深层网络难训练的原因之一。ReLU 在正半轴导数恒为 1,不会饱和。GELU 比 ReLU 多几次运算,但逐元素运算是访存受限的(第 1 讲 §4.4),多算几下几乎不花时间。
1.6 深度:初始化与 LayerNorm
直觉:信号每经过一层,大约被乘上一个系数。如果这个系数是 0.9,50 层后只剩 ;如果是 1.1,就变成 117 倍。01 号脚本第 4 部分让一个标准正态分布的输入穿过 50 层宽度 256 的 ReLU 网络:
| 配置 | 第 1 层 | 第 10 层 | 第 25 层 | 第 50 层 |
|---|---|---|---|---|
| 权重标准差 0.01(太小) | 0.092 | 2.7e-10 | 1.9e-24 | 3.3e-48 |
| He 初始化 | 0.816 | 0.773 | 0.874 | 0.685 |
| 权重标准差 0.12(只大了约 1.36 倍) | 1.11 | 16.4 | 1820 | 3.0e6 |
| 0.12 + 每层 LayerNorm | 1 | 1 | 1 | 1 |
- He 初始化把权重方差设为 :ReLU 把一半的输入置 0,方差减半,乘 2 正好补回来,信号的尺度一层层保持不变。
- LayerNorm 对每个样本(每个 token)的向量减均值、除标准差,再乘一个可学习的缩放 、加一个偏移 :
不管权重的尺度如何,每层的输出都被拉回均值 0、方差 1。Transformer 的每一层都有 LayerNorm(或它的简化版 RMSNorm,第 7 讲)。从系统的角度,它是一个归约算子(要先求均值和方差),原课 HW4 要你写它的前向和反向 CUDA 内核;P2 §3.5 讲过,算方差时要先减均值,否则会灾难性抵消。
1.7 反向传播:链式法则的系统化
损失 是参数的复合函数:。链式法则说, 对某一层输入的梯度 = 对它输出的梯度 × 这一层的局部导数。反向传播就是从损失出发,按前向的逆序,每一层做一件事:拿到”损失对我的输出的梯度”,算出”损失对我的输入的梯度”(传给前一层)和”损失对我的参数的梯度”(交给优化器)。
线性层 的反向(记 ):
(形状检查: 是 , 是 ,和各自的变量一样。)一次线性层的反向是两次矩阵乘法,每次都和前向一样大——所以训练的计算量约是前向的 3 倍(第 1 讲 §5.3 的 6ND = 2ND 前向 + 4ND 反向)。
ReLU 的反向是逐元素的:。注意它需要前向时的 ——这就是为什么训练时要保存激活值,而激活值的显存常常比参数还大(第 1 讲 §5.5、第 18 讲的重计算)。
2. softmax 与交叉熵 · Softmax and cross-entropy
2.1 softmax:分数变概率
分类网络的最后一层输出 个任意实数(logits)。softmax 把它们变成概率分布:
先取指数(变成正数、放大差距),再归一化(和为 1)。02 号脚本第 1 部分:logits [2.0, 1.0, 0.1] → [0.659, 0.242, 0.099]。两条性质:
- 平移不变:所有 logits 加同一个常数,结果不变([2, 1, 0.1] 和 [102, 101, 100.1] 得到同样的概率)。
- 温度:除以 再做 softmax。 → [0.864, 0.117, 0.019](更尖), → [0.502, 0.304, 0.194](更平)。语言模型采样时的”温度”就是它(第 1 讲 03 号脚本、第 9 讲)。
2.2 溢出:先减最大值
直接按公式算会溢出(02 号脚本第 2 部分):
| 精度 | logits | exp(最大的那个) | 直接按公式 | 先减最大值 |
|---|---|---|---|---|
| float64 | [1000, 999, 998] | inf | [nan, nan, nan] | [0.665, 0.245, 0.090] |
| float32 | [100, 99, 98] | inf | [nan, nan, nan] | [0.665, 0.245, 0.090] |
| float16 | [12, 11, 10] | inf | [nan, 0, 0] | [0.666, 0.245, 0.090] |
FP16 里连 都超过了它的最大值 65504(P2 §2.5)。利用平移不变性,先减去最大值:最大的那项变成 ,其余都在 之间,永远不会溢出。所有框架、所有 softmax 内核都这样做。
2.3 交叉熵
分类的损失是交叉熵:模型给正确类别 的概率取负对数,
它只关心”正确答案得到了多少概率”:给 1 时损失 0,给 0 时损失无穷大。均匀猜测 个类别时损失是 (3 类是 1.0986)。一个有用的检查:一个刚初始化的语言模型,预测 50257 个 token 时损失应该在 附近——如果你看到初始损失远大于它,多半是哪里写错了。
计算时用 (同样先减最大值),不要先算 softmax 再取 log:概率很小时 softmax 会下溢成 0,。
2.4 梯度:p − y
对 求导:
其中 是 one-hot 标签(正确类别为 1,其余为 0)。梯度 = 预测概率 − 真实标签:正确类别的 logit 被往上推,其余被往下压,推的力度正比于”错了多少”。02 号脚本第 3 部分:logits [2, 1, 0.1]、正确类别 2 时梯度是 [0.659, 0.242, −0.901];在随机的 4×5 logits 上,解析梯度和数值梯度的相对误差 。
这个公式的系统意义:反向时只需要 softmax 的输出和标签,不需要保存 log、exp 的中间结果。第 10 讲的 LightSeq 把”softmax + 交叉熵 + 求梯度”融合成一个内核,靠的就是它。对词表 12.8 万、每批几百万 token 的 LLM,logits 张量本身就有几十 GB(P2 §1.4),少读写一遍都是大事。
2.5 为什么不用均方误差
把 softmax 的输出和 one-hot 标签做均方误差 不行吗?02 号脚本第 4 部分让模型越来越自信地选错(正确类别 0,logits = [−a, a, 0]):
| a | p(正确类别) | 交叉熵 | 交叉熵对 的梯度 | 均方误差对 的梯度 |
|---|---|---|---|---|
| 0 | 0.333 | 1.10 | −0.667 | −0.444 |
| 2 | 0.016 | 4.14 | −0.984 | −0.055 |
| 5 | 4.5e-5 | 10.01 | −1.000 | −1.8e-4 |
| 10 | 2.1e-9 | 20.00 | −1.000 | −8.3e-9 |
均方误差的梯度要乘上 softmax 的导数(大约是 ), 接近 0 时它也接近 0:模型错得最离谱的时候,几乎学不到东西。交叉熵的 恰好抵消了 softmax 的指数,梯度稳定在 −1。所以分类任务(包括语言模型的”预测下一个 token”)都用交叉熵。
2.6 一个 softmax 要读写几遍数据
按定义,softmax 是 5 个独立的算子:求最大值(归约)、减最大值(逐元素)、取指数(逐元素)、求和(归约)、除以和(逐元素)——同一行数据被读 5 遍、写 3 遍。一个融合的 softmax 内核只读一遍、写一遍,中间结果留在寄存器 / 共享内存里,对访存受限的 softmax 是好几倍的加速(第 10 讲、HW4)。更进一步的”在线 softmax”边读边更新最大值和求和,是 FlashAttention 的核心技巧(第 21 讲)。
3. 优化 · Optimization
3.1 学习率与”病态”的山谷
直觉:损失函数像一片山地,梯度下降是一个蒙着眼睛、每次只能感受脚下坡度的人。学习率是他每步迈多大。
03 号脚本第 1 部分用最简单的”狭长山谷” ,( 方向比 陡 25 倍):
| 学习率 | 到 f < 1e-6 的步数 |
|---|---|
| 0.001 | > 2000 |
| 0.01 | 653 |
| 0.04 | 161 |
| 0.079 | 323 |
| 0.081 | 发散 |
每一步, 被乘以 , 被乘以 。所以:
- 时 ,陡的方向每步都冲过头、越冲越远——发散。学习率的上限由最陡的方向决定。训练时 loss 突然变成 NaN,最常见的原因就是学习率太大。
- 但也不是越接近上限越好:0.079 时陡的方向每步乘以 −0.975,来回震荡、衰减很慢,反而比 0.04 慢一倍。最快的学习率让两个方向衰减得一样快,对二次函数是 。
- 收敛速度却被最平缓的方向拖慢:即使用最优学习率, 从 25 变成 1000,步数从 103 涨到 5008(大约正比于 )。 叫条件数,越大问题越”病态”。深度网络的损失面在不同参数方向上的曲率差别极大,这是训练困难的核心原因之一。
3.2 动量
动量(momentum)让参数带着”惯性”移动:
在一致的方向上,历史梯度不断累加,越走越快;在来回震荡的方向上,正负梯度相互抵消。03 号脚本第 2 部分用二次函数上理论最优的超参数(我一开始随手选了 β = 0.9,结果动量反而比梯度下降慢——超参数选不好,就没有收益):
| 山谷 | 梯度下降(最优学习率) | 动量(最优 β 和学习率) | Adam(学习率 0.05) |
|---|---|---|---|
| κ = 25 | 103 步 | 30 步 | 82 步 |
| κ = 1000 | 5008 步 | 257 步 | 151 步 |
选得好的动量需要的步数大约正比于 而不是 。
3.3 Adam 与 AdamW
Adam 为每个参数维护两个”滑动平均”:梯度的一阶矩 (方向)和二阶矩 (尺度),步长约为 :
除以 相当于给每个参数一个自己的步长:梯度一直很大的方向(陡)自动走小步,梯度一直很小的方向(缓)自动走大步。上表里 Adam 的学习率完全没调, 涨 40 倍,步数只是大约翻倍。这种”按参数自适应”是 Adam 成为训练 Transformer 的默认优化器的主要原因。(固定学习率时 Adam 会在最优点附近小幅震荡,真实训练会配合学习率衰减。)
AdamW 把权重衰减(每步把参数往 0 拉一点:)从梯度里拿出来单独做(“解耦”),而不是把 加进梯度、再被 除掉。今天训练 LLM 用的几乎都是 AdamW。
3.4 小批量
用全部数据算一次梯度太贵;用一小批样本估计梯度,叫小批量随机梯度下降。03 号脚本第 3 部分:线性回归(4096 个样本、20 维),每种批大小都处理 8192 个样本(2 遍数据):
| 批大小 | 更新步数 | 学习率 | 处理完后的均方误差 |
|---|---|---|---|
| 1 | 8192 | 0.01 | 0.0129 |
| 16 | 512 | 0.05 | 0.0107 |
| 256 | 32 | 0.05 | 0.0345 |
| 4096(全批量) | 2 | 0.05 | 12.78 |
- 从”每个样本的进展”看,小批量更划算:同样的数据,能走更多步。全批量只走了 2 步,离最优还很远。
- 批太小(1)时梯度噪声太大,只能用更小的学习率,结果反而不如 16。
- 从”硬件”看正好相反:每一步都是矩阵乘法,批越小矩阵越”瘦”,GPU 越用不满(第 1 讲 §4.7 的批处理实验);多卡训练时每一步还要同步一次梯度(第 14 讲)。LLM 训练用每步几百万个 token 的大批量,是在两者之间找平衡。
3.5 学习率调度
实际训练很少用固定的学习率。最常见的是预热 + 衰减:前几百到几千步从 0 线性升到最大学习率(开始时参数是随机的,梯度方向很乱,Adam 的 也还不准,步子太大容易发散),之后按余弦曲线慢慢降低(后期步子小,便于收敛到更低的损失)。第 1 讲的 tiny_gpt.py 用的就是这种调度。
3.6 优化器要为每个参数多存几个数
| 优化器 | 额外状态 | FP32 下”梯度 + 状态”的字节 |
|---|---|---|
| SGD | 无 | 4 |
| 动量 | 速度 | 4 + 4 |
| Adam / AdamW | 一阶矩 、二阶矩 | 4 + 4 + 4 |
一个 7B 模型用 Adam:FP32 的 和 就要 56 GB;混合精度时再加 BF16 权重、BF16 梯度、FP32 主权重,合计 16 字节/参数 = 112 GB(第 1 讲 §5.5、P2 §4.5)。Adam 的自适应不是免费的——第 18 讲的 ZeRO 第一步就是把优化器状态切分到多张卡上。
4. 一个完整的文本分类网络 · A complete text classifier
4.1 数据:一个能看清”模型学不会什么”的影评语料
原课第 2 讲用影评情感分类(正面 / 负面)做例子。nn_lab.py 的 ReviewWorld 按模板生成影评,标签规则完全确定(词表 37 个词):
| 句型 | 例子 | 标签规则 | 比例 |
|---|---|---|---|
| 单形容词 | the cast was so poor | 形容词的极性 | 0.4 |
| 否定 | the dialogue was not dull | 形容词的极性取反 | 0.2 |
| 转折 | the script was good but the cast was poor | but 之后的形容词的极性 | 0.2 |
| 并列 | a fun story and a good music | 两个形容词极性相同,取该极性 | 0.2 |
这样设计是为了让三类”难度”分开:单形容词和并列句只要知道每个词的好坏;否定句要看 not 和后面那个词的组合;转折句要看顺序——“the plot was good but the acting was bad”(负面)和”the acting was bad but the plot was good”(正面)的词袋完全一样(04 号脚本验证了这一点),标签却相反。
4.2 网络结构与底层运算
原课第 2 讲的网络:
token id ──► 嵌入查表 E[ids] ──► 平均池化(按掩码) ──► 线性层 1 ──► ReLU ──► 线性层 2 ──► softmax
[B, T] → [B, T, d] [B, T, d] → [B, d] → [B, h] → [B, h] → [B, 2]
| 层 | 参数(d = 16,h = 32) | 参数个数 | 底层运算 |
|---|---|---|---|
| 嵌入查表 | E:37 × 16 | 592 | 按下标读取(gather) |
| 平均池化 | — | 0 | 逐元素乘掩码 + 归约(求和、除以长度) |
| 线性层 1 | W1:16 × 32,b1 | 544 | 矩阵乘法 + 逐元素加偏置 |
| ReLU | — | 0 | 逐元素映射 |
| 线性层 2 | W2:32 × 2,b2 | 66 | 矩阵乘法 + 逐元素加偏置 |
| softmax + 交叉熵 | — | 0 | 归约 + 逐元素映射 |
合计 1,202 个参数。这正是原课第 2 讲的那张表:一个神经网络最终只用到矩阵乘法、逐元素运算、归约、按下标读写四类运算——HW1 就是为这四类运算写 CUDA 内核。
嵌入是什么:每个词对应一个可学习的 维向量,查表就是把 id 换成向量。它等价于”one-hot 向量乘以嵌入矩阵”,但查表不需要做任何乘法,只是按下标读一行。
平均池化与掩码:一批句子长短不一,要补齐(P1)到同样长度,再用掩码(真实 token 为 1、填充为 0)把填充位置排除在平均之外。
4.3 逐层的反向
TextClassifier.backward 按前向的逆序调用每一层的反向:
- softmax + 交叉熵:(§2.4)。
- 线性层 2:,,(§1.7)。
- ReLU:。
- 线性层 1:同 2。
- 平均池化:每个真实位置分到 ,填充位置为 0。
- 嵌入查表:把每个位置的梯度按下标累加回嵌入矩阵的对应行(scatter-add)。同一个词在一批里出现几次,它那一行的梯度就要加几次。NumPy 里要用
np.add.at(普通的E[ids] += g遇到重复下标只加一次);GPU 上要用原子加法,所以结果的加法顺序不固定(P2 §3.3)。
4.4 梯度检查
手推的梯度对不对?用数值梯度检查:对每个参数加减一个小量 ,用 估计导数,和反向传播的结果比较。04 号脚本第 3 部分:有无隐藏层的两个小模型,每个参数的相对误差都在 量级(数值差分本身的误差)——反向传播写对了。数值梯度要对每个参数做两次前向,对百万参数的模型完全不可行,只用来检查。原课 HW2 也是先实现反向传播、再这样检查。
4.5 训练结果:它在哪里失败
04 号脚本第 4 部分:4000 条训练、2000 条独立生成的测试,Adam(学习率 0.01),15 个 epoch:
| 模型 | 测试准确率 | 单形容词 | 否定 | 转折 | 并列 |
|---|---|---|---|---|---|
| 没有隐藏层(平均嵌入 + 线性分类器) | 0.686 | 1.000 | 0.000 | 0.478 | 1.000 |
| 一个 ReLU 隐藏层(h = 32) | 0.901 | 1.000 | 1.000 | 0.519 | 1.000 |
- 否定句:线性模型全错,加一层非线性就全对。 线性分类器只能给每个词一个固定的分数再相加。要做对否定句,需要 good 为正、bad 为负、“not + good” 为负、“not + bad” 为正——not 的分数既要大于 bad 的绝对值、又要小于 good 的负值,不可能同时满足(这是一个异或问题)。于是它跟着形容词走,在否定句上全错。隐藏层的 ReLU 能表达”not 和 good 同时出现”这样的组合(§1.4 的结论)。
- 转折句:两个模型都只有一半。 平均池化丢掉了词序,对调两个从句后输入完全相同,任何只看词袋的模型都只能猜。要做对它,模型必须看到顺序——这正是 RNN、注意力和 Transformer 的出发点(第 6 讲)。这个网络的测试准确率天花板约是 0.9。
4.6 嵌入学到了什么
对没有隐藏层的模型,每个词的嵌入投影到分类方向上,就是它对”正面”的贡献(04 号脚本第 6 部分):最正面的词 fun +6.99、good +6.94、enjoyable +5.85……;最负面的 boring −6.36、poor −6.08、bad −5.46……;名词和虚词接近 0(movie −0.09、the −0.22)。没有人告诉过模型哪些词是好词,它从数据里学了出来:嵌入把每个词变成一个向量,向量的方向编码了它对任务有用的信息。not 的分数只有 +1.27——线性模型无法让它”翻转”后面的词,只能学到一个无关紧要的妥协。LLM 的词嵌入(第 7–8 讲)是同一件事在海量数据上的大规模版本。
5. 泛化:过拟合与正则化 · Generalization
5.1 训练集、验证集、测试集
模型在训练数据上表现好不算数,要看它在没见过的数据上表现如何(泛化)。通常把数据分成三份:训练集(用来算梯度、更新参数)、验证集(用来调超参数、决定什么时候停)、测试集(最后只用一次,报告结果)。只在测试集上反复调参,测试集就悄悄变成了训练集的一部分。
5.2 过拟合
04 号脚本第 5 部分给训练标签加了 15% 的噪声(故意标错),用一个更大的模型(隐藏层 64、嵌入 32 维)训练 100 个 epoch,测试集标签是干净的:
| 训练条数 | 设置 | 训练准确率(对着带噪声的标签) | 测试准确率 |
|---|---|---|---|
| 100 | 不加正则 | 0.990 | 0.651 |
| 100 | dropout 0.5 + 权重衰减 0.05 | 0.990 | 0.615 |
| 300 | 不加正则 | 0.987 | 0.767 |
| 300 | dropout 0.5 + 权重衰减 0.05 | 0.977 | 0.781 |
| 3000 | 不加正则 | 0.831 | 0.840 |
| 3000 | dropout 0.5 + 权重衰减 0.05 | 0.785 | 0.896 |
100 条数据时,模型在训练集上几乎全对——连那 15% 故意标错的标签也背了下来——测试集却只有 0.651。这就是过拟合:模型记住了训练数据,而不是学会了规律。
5.3 正则化
- dropout:训练时随机把一部分神经元的输出置 0(其余放大 保持期望),推理时不做。模型不能依赖任何单个神经元,被迫学到更稳健的特征。
- 权重衰减:每步把参数往 0 拉一点,偏好”小”的参数(AdamW,§3.3)。
- 更多的数据:最可靠的正则化。
上表的结果比”正则化总有帮助”复杂:100 条数据时正则化反而让测试准确率从 0.651 降到 0.615——数据太少,正则化也变不出没见过的词和组合;300 条时只提高了 1.4 个百分点;3000 条时模型本身已经背不下噪声(训练准确率 0.831),加上正则后测试准确率到了 0.896,几乎碰到词袋模型的天花板。更多的数据 + 适度的正则化最可靠。LLM 预训练的数据量巨大,通常一个 epoch 都跑不完,过拟合不是主要问题;但在微调(数据少)时,它又回来了(第 23 讲)。
6. 和后面讲次的联系 · Where this goes next
| 本讲的概念 | 后面在哪里出现 |
|---|---|
| 训练循环四步;反向 = 两次矩阵乘法 → 3 倍 FLOPs | 第 1 讲 §5(6ND)、第 5 讲(框架)、第 14–18 讲(分布式地做这四步) |
| 线性层、激活函数 | 第 6 讲(Transformer 的 MLP 用 GELU)、第 7 讲(SwiGLU) |
| LayerNorm 的前向 / 反向 | 第 6–7 讲(Pre-LN、RMSNorm)、第 10 讲和 HW4(融合内核) |
| softmax 减最大值、p − y | 第 6 讲(注意力里的 softmax)、第 10 讲(融合的交叉熵)、第 21 讲(在线 softmax) |
| Adam 的状态 | 第 1 讲 §5.5(16 字节/参数)、第 18 讲(ZeRO 切分优化器状态) |
| 嵌入查表与 scatter-add | 第 8 讲(分词与嵌入)、第 10 讲(融合的嵌入前向 / 反向) |
| 数值梯度检查 | 第 5 讲、HW2 |
| 小批量 vs 硬件利用率 | 第 1 讲 §4.7、第 14 讲(数据并行的大批量) |
| 词袋模型做不到的”顺序” | 第 6 讲(注意力与位置编码) |
7. 小结 · Summary
- 训练 = 前向、损失、反向、更新四步的循环。神经网络没有解析解,只能靠梯度下降。
- 线性层 :前向 2 FLOPs/参数/样本,反向是两次同样大小的矩阵乘法(所以训练 ≈ 3 倍前向)。没有非线性,叠多少层都等于一层;ReLU 不饱和,sigmoid / tanh 两端饱和,GELU 是平滑的 ReLU。
- 深层网络的信号会指数级爆炸或消失;He 初始化和 LayerNorm 让它保持稳定。
- softmax 要先减最大值;交叉熵对 logits 的梯度是 p − y;分类不用均方误差,因为模型错得越自信、它的梯度越小。
- 学习率的上限由最陡的方向决定,收敛速度被最缓的方向拖慢(条件数);动量把依赖从 κ 降到 √κ(要选好超参数);Adam 给每个参数自己的步长,代价是每个参数多存两个数;小批量在”每个样本的进展”和”硬件利用率”之间取舍。
- 原课第 2 讲的文本分类网络只用到矩阵乘法、逐元素、归约、按下标读写四类运算;嵌入的反向是 scatter-add。它在否定句上需要非线性才能做对,在转折句上无论如何只能猜一半——词袋看不到顺序。
- 过拟合:数据少时模型会背下噪声;dropout、权重衰减有时有帮助,更多的数据最可靠。
8. 常见误区 · Common pitfalls
- “层数越多越强,线性层叠起来也行。” 没有非线性,叠多少层都等于一层。
- “学习率越大收敛越快。” 超过 2/最大曲率就发散;接近上限时也会因为震荡而变慢。
- “softmax 直接按公式写就行。” 必须先减最大值,否则 FP16 里 logits 超过 11 左右(e^11.09 ≈ 65504)就会溢出。
- “先算 softmax 再取 log 得到 log 概率。” 小概率会下溢成 0,log 0 = −∞;要用 log_softmax(logsumexp)。
- “训练准确率高就说明模型好。” 可能只是背下了训练数据(过拟合);要看验证集 / 测试集。
- “Adam 不用调学习率。” Adam 对学习率不那么敏感,但仍然要调,而且通常要配合预热和衰减。
- “正则化总能提高测试准确率。” 数据太少时它可能帮不上、甚至变差(04 号脚本:100 条数据时 0.651 → 0.615)。
- “嵌入层的反向就是普通的赋值。” 同一个词出现多次时梯度要累加(scatter-add),用
E[ids] += g会漏加。 - “数值梯度可以代替反向传播。” 它要对每个参数做两次前向,只适合在小模型上检查。
9. 练习 · Exercises
练习 1(线性层)。 一个线性层把 512 维映射到 2048 维。参数有多少个?对一批 64 个样本,前向要多少 FLOPs?反向呢?
参考答案:参数 512 × 2048 + 2048 = 1,050,624。前向 FLOPs(不计偏置的加法)。反向两次同样大小的矩阵乘法( 和 ),约 ——是前向的 2 倍。
练习 2(非线性)。 证明带偏置的两个线性层 也等于一个线性层,并写出等价的 和 。
参考答案:展开得 ,所以 ,。
练习 3(激活函数的导数)。 写出 sigmoid 的导数 在 的值。为什么说 sigmoid 网络很深时梯度一定会衰减?
参考答案:,导数 0.25,这是 sigmoid 导数的最大值。反向传播每经过一层都要乘上一个不超过 0.25 的导数(再乘权重),10 层后就是 量级——除非权重很大来补偿,梯度必然衰减。
练习 4(softmax)。 计算 softmax([1, 2, 3]),以及温度 0.5 时的结果。
参考答案: 归一化:[0.090, 0.245, 0.665]。温度 0.5 等于 softmax([2, 4, 6]):[0.016, 0.117, 0.867]。
练习 5(初始损失)。 一个刚初始化的语言模型,词表 128,256(Llama-3),如果它对每个 token 给出近似均匀的分布,初始交叉熵大约是多少?如果你看到初始损失是 50,可能出了什么问题?
参考答案:。初始损失远大于它,说明初始化的 logits 尺度太大(输出层权重太大,分布很尖却猜错了),或者损失的计算有 bug(例如没有减最大值、标签和输入错位)。
练习 6(交叉熵的梯度)。 logits [0, 0, 0],正确类别 1。写出损失和梯度。走一步学习率 1 的梯度下降后,正确类别的概率变成多少?
参考答案:,损失 ,梯度 。更新后 logits = [−1/3, 2/3, −1/3],正确类别概率 。
练习 7(学习率上限)。 对 做梯度下降,学习率最大能取多少?最快的学习率是多少?此时每步误差缩小到原来的多少?
参考答案:上限 。最快 ,此时两个方向都乘以 (陡的方向是 ),每步只缩小 2%——条件数 100 的问题,梯度下降要几百步才能把误差降一个数量级。
练习 8(Adam 的显存)。 用 AdamW 训练一个 70B 模型,FP32 的 和 一共多少 GB?至少要几块 80 GB 的 GPU 才放得下(只算这一项)?
参考答案: 字节 = 560 GB,至少 7 块。这还没算权重、梯度、主权重和激活值——所以必须把优化器状态切分到多张卡上(第 18 讲 ZeRO)。
练习 9(小批量)。 训练集 100 万条,批大小 256,一个 epoch 要多少步?如果每步的时间几乎不随批大小变化(GPU 没用满),把批大小翻倍会怎样?
参考答案: 步。批大小翻倍后每个 epoch 只要约 1954 步,一个 epoch 的时间几乎减半;但每个样本带来的进展可能变少(§3.4),通常要相应地调大学习率。这就是”批大小要大到用满硬件、又不能大到浪费数据”的权衡。
练习 10(scatter-add)。 一个批次的 ids = [3, 5, 3],每个位置传回来的嵌入梯度分别是 [1, 0]、[0, 2]、[4, 4]。嵌入矩阵第 3 行和第 5 行的梯度是多少?如果用 dE[ids] += g(NumPy)会得到什么?
参考答案:第 3 行 = [1, 0] + [4, 4] = [5, 4],第 5 行 = [0, 2]。
dE[ids] += g对重复的下标只生效一次(后写的覆盖先写的),第 3 行会得到 [4, 4],漏掉了 [1, 0]。要用np.add.at(dE, ids, g)。
练习 11(否定为什么是异或)。 假设线性模型给每个词一个分数,句子的分数是各词分数之和(“the dialogue was”等词的分数为 0)。要让”good”、“not bad”判为正面(分数 > 0),“bad”、“not good”判为负面(分数 < 0),能做到吗?加一个隐藏层后怎样做到?
参考答案:设分数 。要求 ,,,。前两式相加:;后两式相加:,矛盾,做不到。隐藏层可以构造四个 ReLU 特征(设输入里 good、bad、not 是否出现分别记为 g、b、n ∈ {0, 1}):(有 good、没有 not)、(not bad)、(有 bad、没有 not)、(not good),输出分数 。逐一代入:good → +1,bad → −1,not good → −1,not bad → +1,四种情况全对——04 号脚本里隐藏层模型在否定句上做到了 1.000。
练习 12(思考题)。 为什么”转折”句对任何词袋模型都只能猜一半,而加更多的隐藏层、更多的数据都没用?要做对它,模型的输入至少需要多保留什么信息?
参考答案:对调两个从句后,词袋(以及平均池化后的向量)完全相同,模型的输入一样,输出就一样,而两句的标签相反——所以对每一对这样的句子,它至多做对一句。加层、加数据都改变不了”输入相同”这个事实。输入至少要保留顺序(或者每个词相对于”but”的位置):RNN 按顺序读、Transformer 加位置编码,都是为了这个(第 6 讲)。
10. 延伸阅读 · Further reading
- Goodfellow, Bengio & Courville,《深度学习》第 6 章(前馈网络与反向传播)、第 7 章(正则化)、第 8 章(优化)。
- Michael Nielsen, Neural Networks and Deep Learning(在线书,第 2 章把反向传播讲得很直观)。
- Andrej Karpathy, micrograd 与 “The spelled-out intro to neural networks and backpropagation”(用几十行代码实现自动微分,第 5 讲的好预习)。
- He et al. (2015) Delving Deep into Rectifiers(He 初始化);Ba, Kiros & Hinton (2016) Layer Normalization;Srivastava et al. (2014) Dropout;Kingma & Ba (2015) Adam;Loshchilov & Hutter (2019) Decoupled Weight Decay Regularization(AdamW)。
- 原课第 2 讲 slides 的开头(文本分类网络与底层运算)和第 5 讲 slides(通用迭代学习算法、计算图)。
11. 下一篇预告 · Next
本讲和前两篇反复说”访存受限""按行读比按列读快""带宽差两个数量级”。前置讲 P4(计算机系统速成) 把这些说法背后的系统知识补齐:存储层次(寄存器、缓存、内存、SSD、网络)和它们的延迟与带宽、局部性;延迟与吞吐量、排队(为什么服务器快满载时延迟会爆炸)、流水线的瓶颈;进程、线程与并发,计算和通信的重叠;虚拟内存、分页与内存碎片(vLLM 的 PagedAttention 正是借鉴了它);以及网络的带宽、延迟与拓扑。