前置讲 P3 · 神经网络与训练速成 · A Crash Course on Neural Networks and Training

Large Language Model Systems · 预备知识 3。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

这是一篇前置讲,不对应原课的某一次课。原课的先修写着”最好学过机器学习,但不强制”,第 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

  1. 说出训练循环的四步(前向、损失、反向、更新),理解梯度下降为什么能找到好的参数。
  2. 写出线性层的前向和反向公式;用实验说明为什么网络必须有非线性;比较 ReLU、sigmoid、tanh、GELU。
  3. 理解深层网络中激活值的爆炸与消失,以及初始化和 LayerNorm 怎样让它稳定。
  4. 写出数值稳定的 softmax 和交叉熵,推出交叉熵对 logits 的梯度 p − onehot(y),并解释为什么分类不用均方误差。
  5. 理解学习率、动量、Adam、小批量各自解决什么问题,知道优化器要为每个参数多存几个数。
  6. 从零实现原课第 2 讲的文本分类网络(含嵌入层的 scatter-add 反向),用梯度检查验证;能解释它在哪些句子上必然失败、为什么。
  7. 理解训练集、验证集、测试集,过拟合以及 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 线性回归:最简单的例子

数据 (xi,yi)(x_i, y_i),模型 y^=x⊤w+b\hat{y} = x^\top w + b,损失是均方误差 L=1n∑i(y^i−yi)2L = \frac{1}{n}\sum_i (\hat{y}_i - y_i)^2,梯度:

∂L∂w=2nX⊤(y^−y),∂L∂b=2n∑i(y^i−yi)\frac{\partial L}{\partial w} = \frac{2}{n} X^\top (\hat{y} - y), \qquad \frac{\partial L}{\partial b} = \frac{2}{n}\sum_i (\hat{y}_i - y_i)

01 号脚本第 1 部分在 256 个三维样本上,用学习率 0.1 做梯度下降:

步数均方误差wb
01.7827[0, 0, 0]0
50.1582[−0.548, 0.363, 0.501]0.364
200.00783[−0.752, 0.517, 0.728]0.485
2000.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 线性层

神经网络的基本积木是线性层(也叫全连接层)。对一批 BB 个样本,每个是 dind_{\text{in}} 维向量,排成矩阵 X∈RB×dinX \in \mathbb{R}^{B \times d_{\text{in}}}:

Y=XW+b,W∈Rdin×dout, b∈RdoutY = X W + b, \qquad W \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}},\ b \in \mathbb{R}^{d_{\text{out}}}

bb 被广播加到每一行上(P1 §5.4:步长为 0 的视图)。参数量 dindout+doutd_{\text{in}} d_{\text{out}} + d_{\text{out}};每个样本的计算量是 2dindout2 d_{\text{in}} d_{\text{out}} 次浮点运算(每个权重一次乘、一次加)——这就是第 1 讲”前向每个参数每个 token 约 2 FLOPs”的来历。

1.4 为什么要有非线性

把两个线性层叠起来:(XW1)W2=X(W1W2)(X W_1) W_2 = X (W_1 W_2)——还是一个线性层。01 号脚本第 2 部分验证了两者只差 1.8×10−151.8 \times 10^{-15}(浮点舍入)。没有非线性,叠多少层都等于一层,只能画出直线(超平面)形状的决策边界。

所以每个线性层之后要接一个逐元素的非线性激活函数:H=σ(XW1+b1)H = \sigma(X W_1 + b_1),Y=HW2+b2Y = H W_2 + b_2。这叫多层感知机(MLP)。在三条相互缠绕的螺旋臂上(600 个点、3 类):

模型训练集准确率
线性 softmax 分类器0.525
一个 ReLU 隐藏层,宽度 81.000
一个 ReLU 隐藏层,宽度 641.000

直觉:每个 ReLU 神经元 max⁡(0,w⊤x+b)\max(0, w^\top x + b) 在输入空间里沿一条直线”折一下”;几条折痕拼起来,就能围出弯曲的边界。

1.5 激活函数

01 号脚本第 3 部分:

函数公式x = −3 的导数x = 0.1 的导数x = 3 的导数特点
ReLUmax⁡(0,x)\max(0, x)011简单、不饱和;负半轴梯度为 0(“死亡 ReLU”)
sigmoid1/(1+e−x)1/(1+e^{-x})0.0450.2490.045两端饱和,导数最大只有 0.25
tanhtanh⁡x\tanh x0.0100.9900.010两端饱和
GELUx⋅Φ(x)x \cdot \Phi(x)(常用 tanh 近似)−0.0120.5801.012平滑版的 ReLU,GPT / BERT 的 MLP 用它

反向传播时梯度要一层层乘上这些导数。sigmoid 和 tanh 在 ∣x∣|x| 大时导数接近 0(饱和),乘几层就没了——梯度消失,这是早期深层网络难训练的原因之一。ReLU 在正半轴导数恒为 1,不会饱和。GELU 比 ReLU 多几次运算,但逐元素运算是访存受限的(第 1 讲 §4.4),多算几下几乎不花时间。

1.6 深度:初始化与 LayerNorm

直觉:信号每经过一层,大约被乘上一个系数。如果这个系数是 0.9,50 层后只剩 0.950≈0.0050.9^{50} \approx 0.005;如果是 1.1,就变成 117 倍。01 号脚本第 4 部分让一个标准正态分布的输入穿过 50 层宽度 256 的 ReLU 网络:

配置第 1 层第 10 层第 25 层第 50 层
权重标准差 0.01(太小)0.0922.7e-101.9e-243.3e-48
He 初始化 2/256=0.088\sqrt{2/256} = 0.0880.8160.7730.8740.685
权重标准差 0.12(只大了约 1.36 倍)1.1116.418203.0e6
0.12 + 每层 LayerNorm1111
  • He 初始化把权重方差设为 2/din2 / d_{\text{in}}:ReLU 把一半的输入置 0,方差减半,乘 2 正好补回来,信号的尺度一层层保持不变。
  • LayerNorm 对每个样本(每个 token)的向量减均值、除标准差,再乘一个可学习的缩放 γ\gamma、加一个偏移 β\beta:
LN(x)=γ⊙x−μσ2+ϵ+β,μ=1d∑ixi, σ2=1d∑i(xi−μ)2\text{LN}(x) = \gamma \odot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta, \quad \mu = \frac{1}{d}\sum_i x_i,\ \sigma^2 = \frac{1}{d}\sum_i (x_i - \mu)^2

不管权重的尺度如何,每层的输出都被拉回均值 0、方差 1。Transformer 的每一层都有 LayerNorm(或它的简化版 RMSNorm,第 7 讲)。从系统的角度,它是一个归约算子(要先求均值和方差),原课 HW4 要你写它的前向和反向 CUDA 内核;P2 §3.5 讲过,算方差时要先减均值,否则会灾难性抵消。

1.7 反向传播:链式法则的系统化

损失 LL 是参数的复合函数:L=ℓ(f3(f2(f1(x))))L = \ell(f_3(f_2(f_1(x))))。链式法则说,LL 对某一层输入的梯度 = 对它输出的梯度 × 这一层的局部导数。反向传播就是从损失出发,按前向的逆序,每一层做一件事:拿到”损失对我的输出的梯度”,算出”损失对我的输入的梯度”(传给前一层)和”损失对我的参数的梯度”(交给优化器)。

线性层 Y=XW+bY = XW + b 的反向(记 dY=∂L/∂YdY = \partial L / \partial Y):

dX=dY⋅W⊤,dW=X⊤⋅dY,db=∑行dYdX = dY \cdot W^\top, \qquad dW = X^\top \cdot dY, \qquad db = \sum_{\text{行}} dY

(形状检查:dXdX 是 B×dinB \times d_{\text{in}},dWdW 是 din×doutd_{\text{in}} \times d_{\text{out}},和各自的变量一样。)一次线性层的反向是两次矩阵乘法,每次都和前向一样大——所以训练的计算量约是前向的 3 倍(第 1 讲 §5.3 的 6ND = 2ND 前向 + 4ND 反向)。

ReLU 的反向是逐元素的:dX=dY⊙[X>0]dX = dY \odot [X > 0]。注意它需要前向时的 XX——这就是为什么训练时要保存激活值,而激活值的显存常常比参数还大(第 1 讲 §5.5、第 18 讲的重计算)。


2. softmax 与交叉熵 · Softmax and cross-entropy

2.1 softmax:分数变概率

分类网络的最后一层输出 CC 个任意实数(logits)z1,…,zCz_1, \dots, z_C。softmax 把它们变成概率分布:

softmax(z)i=ezi∑jezj\mathrm{softmax}(z)_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

先取指数(变成正数、放大差距),再归一化(和为 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] 得到同样的概率)。
  • 温度:除以 TT 再做 softmax。T=0.5T = 0.5 → [0.864, 0.117, 0.019](更尖),T=2T = 2 → [0.502, 0.304, 0.194](更平)。语言模型采样时的”温度”就是它(第 1 讲 03 号脚本、第 9 讲)。

2.2 溢出:先减最大值

直接按公式算会溢出(02 号脚本第 2 部分):

精度logitsexp(最大的那个)直接按公式先减最大值
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 里连 e12≈162755e^{12} \approx 162755 都超过了它的最大值 65504(P2 §2.5)。利用平移不变性,先减去最大值:最大的那项变成 e0=1e^0 = 1,其余都在 (0,1](0, 1] 之间,永远不会溢出。所有框架、所有 softmax 内核都这样做。

2.3 交叉熵

分类的损失是交叉熵:模型给正确类别 yy 的概率取负对数,

L=−log⁡softmax(z)y=−zy+log⁡∑jezjL = -\log \mathrm{softmax}(z)_y = -z_y + \log\sum_j e^{z_j}

它只关心”正确答案得到了多少概率”:给 1 时损失 0,给 0 时损失无穷大。均匀猜测 CC 个类别时损失是 log⁡C\log C(3 类是 1.0986)。一个有用的检查:一个刚初始化的语言模型,预测 50257 个 token 时损失应该在 ln⁡50257≈10.8\ln 50257 \approx 10.8 附近——如果你看到初始损失远大于它,多半是哪里写错了。

计算时用 log⁡softmax(z)=z−logsumexp(z)\log\mathrm{softmax}(z) = z - \mathrm{logsumexp}(z)(同样先减最大值),不要先算 softmax 再取 log:概率很小时 softmax 会下溢成 0,log⁡0=−∞\log 0 = -\infty。

2.4 梯度:p − y

对 L=−zy+log⁡∑jezjL = -z_y + \log\sum_j e^{z_j} 求导:

∂L∂zi=−[i=y]+ezi∑jezj=pi−yi\frac{\partial L}{\partial z_i} = -[i = y] + \frac{e^{z_i}}{\sum_j e^{z_j}} = p_i - y_i

其中 yiy_i 是 one-hot 标签(正确类别为 1,其余为 0)。梯度 = 预测概率 − 真实标签:正确类别的 logit 被往上推,其余被往下压,推的力度正比于”错了多少”。02 号脚本第 3 部分:logits [2, 1, 0.1]、正确类别 2 时梯度是 [0.659, 0.242, −0.901];在随机的 4×5 logits 上,解析梯度和数值梯度的相对误差 5.2×10−115.2 \times 10^{-11}。

这个公式的系统意义:反向时只需要 softmax 的输出和标签,不需要保存 log、exp 的中间结果。第 10 讲的 LightSeq 把”softmax + 交叉熵 + 求梯度”融合成一个内核,靠的就是它。对词表 12.8 万、每批几百万 token 的 LLM,logits 张量本身就有几十 GB(P2 §1.4),少读写一遍都是大事。

2.5 为什么不用均方误差

把 softmax 的输出和 one-hot 标签做均方误差 ∑i(pi−yi)2\sum_i (p_i - y_i)^2 不行吗?02 号脚本第 4 部分让模型越来越自信地选错(正确类别 0,logits = [−a, a, 0]):

ap(正确类别)交叉熵交叉熵对 z0z_0 的梯度均方误差对 z0z_0 的梯度
00.3331.10−0.667−0.444
20.0164.14−0.984−0.055
54.5e-510.01−1.000−1.8e-4
102.1e-920.00−1.000−8.3e-9

均方误差的梯度要乘上 softmax 的导数(大约是 p(1−p)p(1-p)),pp 接近 0 时它也接近 0:模型错得最离谱的时候,几乎学不到东西。交叉熵的 log⁡\log 恰好抵消了 softmax 的指数,梯度稳定在 −1。所以分类任务(包括语言模型的”预测下一个 token”)都用交叉熵。

2.6 一个 softmax 要读写几遍数据

按定义,softmax 是 5 个独立的算子:求最大值(归约)、减最大值(逐元素)、取指数(逐元素)、求和(归约)、除以和(逐元素)——同一行数据被读 5 遍、写 3 遍。一个融合的 softmax 内核只读一遍、写一遍,中间结果留在寄存器 / 共享内存里,对访存受限的 softmax 是好几倍的加速(第 10 讲、HW4)。更进一步的”在线 softmax”边读边更新最大值和求和,是 FlashAttention 的核心技巧(第 21 讲)。


3. 优化 · Optimization

3.1 学习率与”病态”的山谷

直觉:损失函数像一片山地,梯度下降是一个蒙着眼睛、每次只能感受脚下坡度的人。学习率是他每步迈多大。

03 号脚本第 1 部分用最简单的”狭长山谷” f(x)=12(x12+κx22)f(x) = \frac{1}{2}(x_1^2 + \kappa x_2^2),κ=25\kappa = 25(x2x_2 方向比 x1x_1 陡 25 倍):

学习率到 f < 1e-6 的步数
0.001> 2000
0.01653
0.04161
0.079323
0.081发散

每一步,x2x_2 被乘以 1−ηκ1 - \eta\kappa,x1x_1 被乘以 1−η1 - \eta。所以:

  • η>2/κ=0.08\eta > 2/\kappa = 0.08 时 ∣1−ηκ∣>1|1 - \eta\kappa| > 1,陡的方向每步都冲过头、越冲越远——发散。学习率的上限由最陡的方向决定。训练时 loss 突然变成 NaN,最常见的原因就是学习率太大。
  • 但也不是越接近上限越好:0.079 时陡的方向每步乘以 −0.975,来回震荡、衰减很慢,反而比 0.04 慢一倍。最快的学习率让两个方向衰减得一样快,对二次函数是 2/(1+κ)2/(1+\kappa)。
  • 收敛速度却被最平缓的方向拖慢:即使用最优学习率,κ\kappa 从 25 变成 1000,步数从 103 涨到 5008(大约正比于 κ\kappa)。κ\kappa 叫条件数,越大问题越”病态”。深度网络的损失面在不同参数方向上的曲率差别极大,这是训练困难的核心原因之一。

3.2 动量

动量(momentum)让参数带着”惯性”移动:

v←βv+g,θ←θ−ηvv \leftarrow \beta v + g, \qquad \theta \leftarrow \theta - \eta v

在一致的方向上,历史梯度不断累加,越走越快;在来回震荡的方向上,正负梯度相互抵消。03 号脚本第 2 部分用二次函数上理论最优的超参数(我一开始随手选了 β = 0.9,结果动量反而比梯度下降慢——超参数选不好,就没有收益):

山谷梯度下降(最优学习率)动量(最优 β 和学习率)Adam(学习率 0.05)
κ = 25103 步30 步82 步
κ = 10005008 步257 步151 步

选得好的动量需要的步数大约正比于 κ\sqrt{\kappa} 而不是 κ\kappa。

3.3 Adam 与 AdamW

Adam 为每个参数维护两个”滑动平均”:梯度的一阶矩 mm(方向)和二阶矩 vv(尺度),步长约为 η⋅m/v\eta \cdot m / \sqrt{v}:

m←β1m+(1−β1)g,v←β2v+(1−β2)g2m^=m/(1−β1t),v^=v/(1−β2t)(偏差修正:开始时 m,v 从 0 起步)θ←θ−η⋅m^/(v^+ϵ)\begin{aligned} m &\leftarrow \beta_1 m + (1-\beta_1) g, \qquad v \leftarrow \beta_2 v + (1-\beta_2) g^2 \\ \hat{m} &= m / (1-\beta_1^t), \quad \hat{v} = v / (1-\beta_2^t) \quad\text{(偏差修正:开始时 } m, v \text{ 从 0 起步)} \\ \theta &\leftarrow \theta - \eta \cdot \hat{m} / (\sqrt{\hat{v}} + \epsilon) \end{aligned}

除以 v\sqrt{v} 相当于给每个参数一个自己的步长:梯度一直很大的方向(陡)自动走小步,梯度一直很小的方向(缓)自动走大步。上表里 Adam 的学习率完全没调,κ\kappa 涨 40 倍,步数只是大约翻倍。这种”按参数自适应”是 Adam 成为训练 Transformer 的默认优化器的主要原因。(固定学习率时 Adam 会在最优点附近小幅震荡,真实训练会配合学习率衰减。)

AdamW 把权重衰减(每步把参数往 0 拉一点:θ←θ−ηλθ\theta \leftarrow \theta - \eta \lambda \theta)从梯度里拿出来单独做(“解耦”),而不是把 λθ\lambda\theta 加进梯度、再被 v\sqrt{v} 除掉。今天训练 LLM 用的几乎都是 AdamW。

3.4 小批量

用全部数据算一次梯度太贵;用一小批样本估计梯度,叫小批量随机梯度下降。03 号脚本第 3 部分:线性回归(4096 个样本、20 维),每种批大小都处理 8192 个样本(2 遍数据):

批大小更新步数学习率处理完后的均方误差
181920.010.0129
165120.050.0107
256320.050.0345
4096(全批量)20.0512.78
  • 从”每个样本的进展”看,小批量更划算:同样的数据,能走更多步。全批量只走了 2 步,离最优还很远。
  • 批太小(1)时梯度噪声太大,只能用更小的学习率,结果反而不如 16。
  • 从”硬件”看正好相反:每一步都是矩阵乘法,批越小矩阵越”瘦”,GPU 越用不满(第 1 讲 §4.7 的批处理实验);多卡训练时每一步还要同步一次梯度(第 14 讲)。LLM 训练用每步几百万个 token 的大批量,是在两者之间找平衡。

3.5 学习率调度

实际训练很少用固定的学习率。最常见的是预热 + 衰减:前几百到几千步从 0 线性升到最大学习率(开始时参数是随机的,梯度方向很乱,Adam 的 vv 也还不准,步子太大容易发散),之后按余弦曲线慢慢降低(后期步子小,便于收敛到更低的损失)。第 1 讲的 tiny_gpt.py 用的就是这种调度。

3.6 优化器要为每个参数多存几个数

优化器额外状态FP32 下”梯度 + 状态”的字节
SGD无4
动量速度 vv4 + 4
Adam / AdamW一阶矩 mm、二阶矩 vv4 + 4 + 4

一个 7B 模型用 Adam:FP32 的 mm 和 vv 就要 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 poorbut 之后的形容词的极性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 × 16592按下标读取(gather)
平均池化—0逐元素乘掩码 + 归约(求和、除以长度)
线性层 1W1:16 × 32,b1544矩阵乘法 + 逐元素加偏置
ReLU—0逐元素映射
线性层 2W2:32 × 2,b266矩阵乘法 + 逐元素加偏置
softmax + 交叉熵—0归约 + 逐元素映射

合计 1,202 个参数。这正是原课第 2 讲的那张表:一个神经网络最终只用到矩阵乘法、逐元素运算、归约、按下标读写四类运算——HW1 就是为这四类运算写 CUDA 内核。

嵌入是什么:每个词对应一个可学习的 dd 维向量,查表就是把 id 换成向量。它等价于”one-hot 向量乘以嵌入矩阵”,但查表不需要做任何乘法,只是按下标读一行。

平均池化与掩码:一批句子长短不一,要补齐(P1)到同样长度,再用掩码(真实 token 为 1、填充为 0)把填充位置排除在平均之外。

4.3 逐层的反向

TextClassifier.backward 按前向的逆序调用每一层的反向:

  1. softmax + 交叉熵:dz=(p−onehot(y))/Bdz = (p - \text{onehot}(y)) / B(§2.4)。
  2. 线性层 2:dA=dz W2⊤dA = dz \, W_2^\top,dW2=A⊤dzdW_2 = A^\top dz,db2=∑dzdb_2 = \sum dz(§1.7)。
  3. ReLU:dH=dA⊙[H>0]dH = dA \odot [H > 0]。
  4. 线性层 1:同 2。
  5. 平均池化:每个真实位置分到 dpooled/长度d\text{pooled} / \text{长度},填充位置为 0。
  6. 嵌入查表:把每个位置的梯度按下标累加回嵌入矩阵的对应行(scatter-add)。同一个词在一批里出现几次,它那一行的梯度就要加几次。NumPy 里要用 np.add.at(普通的 E[ids] += g 遇到重复下标只加一次);GPU 上要用原子加法,所以结果的加法顺序不固定(P2 §3.3)。

4.4 梯度检查

手推的梯度对不对?用数值梯度检查:对每个参数加减一个小量 hh,用 [L(θ+h)−L(θ−h)]/2h[L(\theta + h) - L(\theta - h)] / 2h 估计导数,和反向传播的结果比较。04 号脚本第 3 部分:有无隐藏层的两个小模型,每个参数的相对误差都在 10−1010^{-10} 量级(数值差分本身的误差)——反向传播写对了。数值梯度要对每个参数做两次前向,对百万参数的模型完全不可行,只用来检查。原课 HW2 也是先实现反向传播、再这样检查。

4.5 训练结果:它在哪里失败

04 号脚本第 4 部分:4000 条训练、2000 条独立生成的测试,Adam(学习率 0.01),15 个 epoch:

模型测试准确率单形容词否定转折并列
没有隐藏层(平均嵌入 + 线性分类器)0.6861.0000.0000.4781.000
一个 ReLU 隐藏层(h = 32)0.9011.0001.0000.5191.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.9900.651
100dropout 0.5 + 权重衰减 0.050.9900.615
300不加正则0.9870.767
300dropout 0.5 + 权重衰减 0.050.9770.781
3000不加正则0.8310.840
3000dropout 0.5 + 权重衰减 0.050.7850.896

100 条数据时,模型在训练集上几乎全对——连那 15% 故意标错的标签也背了下来——测试集却只有 0.651。这就是过拟合:模型记住了训练数据,而不是学会了规律。

5.3 正则化

  • dropout:训练时随机把一部分神经元的输出置 0(其余放大 1/(1−p)1/(1-p) 保持期望),推理时不做。模型不能依赖任何单个神经元,被迫学到更稳健的特征。
  • 权重衰减:每步把参数往 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

  • 训练 = 前向、损失、反向、更新四步的循环。神经网络没有解析解,只能靠梯度下降。
  • 线性层 Y=XW+bY = XW + b:前向 2 FLOPs/参数/样本,反向是两次同样大小的矩阵乘法(所以训练 ≈ 3 倍前向)。没有非线性,叠多少层都等于一层;ReLU 不饱和,sigmoid / tanh 两端饱和,GELU 是平滑的 ReLU。
  • 深层网络的信号会指数级爆炸或消失;He 初始化和 LayerNorm 让它保持稳定。
  • softmax 要先减最大值;交叉熵对 logits 的梯度是 p − y;分类不用均方误差,因为模型错得越自信、它的梯度越小。
  • 学习率的上限由最陡的方向决定,收敛速度被最缓的方向拖慢(条件数);动量把依赖从 κ 降到 √κ(要选好超参数);Adam 给每个参数自己的步长,代价是每个参数多存两个数;小批量在”每个样本的进展”和”硬件利用率”之间取舍。
  • 原课第 2 讲的文本分类网络只用到矩阵乘法、逐元素、归约、按下标读写四类运算;嵌入的反向是 scatter-add。它在否定句上需要非线性才能做对,在转折句上无论如何只能猜一半——词袋看不到顺序。
  • 过拟合:数据少时模型会背下噪声;dropout、权重衰减有时有帮助,更多的数据最可靠。

8. 常见误区 · Common pitfalls

  1. “层数越多越强,线性层叠起来也行。” 没有非线性,叠多少层都等于一层。
  2. “学习率越大收敛越快。” 超过 2/最大曲率就发散;接近上限时也会因为震荡而变慢。
  3. “softmax 直接按公式写就行。” 必须先减最大值,否则 FP16 里 logits 超过 11 左右(e^11.09 ≈ 65504)就会溢出。
  4. “先算 softmax 再取 log 得到 log 概率。” 小概率会下溢成 0,log 0 = −∞;要用 log_softmax(logsumexp)。
  5. “训练准确率高就说明模型好。” 可能只是背下了训练数据(过拟合);要看验证集 / 测试集。
  6. “Adam 不用调学习率。” Adam 对学习率不那么敏感,但仍然要调,而且通常要配合预热和衰减。
  7. “正则化总能提高测试准确率。” 数据太少时它可能帮不上、甚至变差(04 号脚本:100 条数据时 0.651 → 0.615)。
  8. “嵌入层的反向就是普通的赋值。” 同一个词出现多次时梯度要累加(scatter-add),用 E[ids] += g 会漏加。
  9. “数值梯度可以代替反向传播。” 它要对每个参数做两次前向,只适合在小模型上检查。

9. 练习 · Exercises

练习 1(线性层)。 一个线性层把 512 维映射到 2048 维。参数有多少个?对一批 64 个样本,前向要多少 FLOPs?反向呢?

参考答案:参数 512 × 2048 + 2048 = 1,050,624。前向 2×64×512×2048≈1.34×1082 \times 64 \times 512 \times 2048 \approx 1.34 \times 10^8 FLOPs(不计偏置的加法)。反向两次同样大小的矩阵乘法(dXdX 和 dWdW),约 2.68×1082.68 \times 10^8——是前向的 2 倍。

练习 2(非线性)。 证明带偏置的两个线性层 (XW1+b1)W2+b2(XW_1 + b_1)W_2 + b_2 也等于一个线性层,并写出等价的 WW 和 bb。

参考答案:展开得 XW1W2+(b1W2+b2)X W_1 W_2 + (b_1 W_2 + b_2),所以 W=W1W2W = W_1 W_2,b=b1W2+b2b = b_1 W_2 + b_2。

练习 3(激活函数的导数)。 写出 sigmoid 的导数 σ′(x)=σ(x)(1−σ(x))\sigma'(x) = \sigma(x)(1-\sigma(x)) 在 x=0x = 0 的值。为什么说 sigmoid 网络很深时梯度一定会衰减?

参考答案:σ(0)=0.5\sigma(0) = 0.5,导数 0.25,这是 sigmoid 导数的最大值。反向传播每经过一层都要乘上一个不超过 0.25 的导数(再乘权重),10 层后就是 0.2510≈10−60.25^{10} \approx 10^{-6} 量级——除非权重很大来补偿,梯度必然衰减。

练习 4(softmax)。 计算 softmax([1, 2, 3]),以及温度 0.5 时的结果。

参考答案:e1,e2,e3e^{1}, e^{2}, e^{3} 归一化:[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,可能出了什么问题?

参考答案:ln⁡128256≈11.76\ln 128256 \approx 11.76。初始损失远大于它,说明初始化的 logits 尺度太大(输出层权重太大,分布很尖却猜错了),或者损失的计算有 bug(例如没有减最大值、标签和输入错位)。

练习 6(交叉熵的梯度)。 logits [0, 0, 0],正确类别 1。写出损失和梯度。走一步学习率 1 的梯度下降后,正确类别的概率变成多少?

参考答案:p=[1/3,1/3,1/3]p = [1/3, 1/3, 1/3],损失 ln⁡3=1.0986\ln 3 = 1.0986,梯度 p−y=[1/3,−2/3,1/3]p - y = [1/3, -2/3, 1/3]。更新后 logits = [−1/3, 2/3, −1/3],正确类别概率 e2/3/(e2/3+2e−1/3)=1/(1+2e−1)≈0.576e^{2/3} / (e^{2/3} + 2e^{-1/3}) = 1/(1 + 2e^{-1}) \approx 0.576。

练习 7(学习率上限)。 对 f(x)=12(x12+100x22)f(x) = \frac{1}{2}(x_1^2 + 100 x_2^2) 做梯度下降,学习率最大能取多少?最快的学习率是多少?此时每步误差缩小到原来的多少?

参考答案:上限 2/100=0.022/100 = 0.02。最快 2/(1+100)≈0.01982/(1+100) \approx 0.0198,此时两个方向都乘以 1−0.0198=0.9801 - 0.0198 = 0.980(陡的方向是 1−1.98=−0.981 - 1.98 = -0.98),每步只缩小 2%——条件数 100 的问题,梯度下降要几百步才能把误差降一个数量级。

练习 8(Adam 的显存)。 用 AdamW 训练一个 70B 模型,FP32 的 mm 和 vv 一共多少 GB?至少要几块 80 GB 的 GPU 才放得下(只算这一项)?

参考答案:70×109×870 \times 10^9 \times 8 字节 = 560 GB,至少 7 块。这还没算权重、梯度、主权重和激活值——所以必须把优化器状态切分到多张卡上(第 18 讲 ZeRO)。

练习 9(小批量)。 训练集 100 万条,批大小 256,一个 epoch 要多少步?如果每步的时间几乎不随批大小变化(GPU 没用满),把批大小翻倍会怎样?

参考答案:106/256≈390710^6 / 256 \approx 3907 步。批大小翻倍后每个 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),能做到吗?加一个隐藏层后怎样做到?

参考答案:设分数 sg,sb,sns_g, s_b, s_n。要求 sg>0s_g > 0,sn+sb>0s_n + s_b > 0,sb<0s_b < 0,sn+sg<0s_n + s_g < 0。前两式相加:sg+sn+sb>0s_g + s_n + s_b > 0;后两式相加:sg+sn+sb<0s_g + s_n + s_b < 0,矛盾,做不到。隐藏层可以构造四个 ReLU 特征(设输入里 good、bad、not 是否出现分别记为 g、b、n ∈ {0, 1}):h1=ReLU(g−n)h_1 = \mathrm{ReLU}(g - n)(有 good、没有 not)、h2=ReLU(b+n−1)h_2 = \mathrm{ReLU}(b + n - 1)(not bad)、h3=ReLU(b−n)h_3 = \mathrm{ReLU}(b - n)(有 bad、没有 not)、h4=ReLU(g+n−1)h_4 = \mathrm{ReLU}(g + n - 1)(not good),输出分数 h1+h2−h3−h4h_1 + h_2 - h_3 - h_4。逐一代入: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 正是借鉴了它);以及网络的带宽、延迟与拓扑。

← 返回课程目录