矩阵、概率与反向传播
从张量形状、链式法则和数值稳定性出发,亲手验证梯度如何从 Loss 回到模型参数。
- 建议时长
- 7–9 小时
- 难度
- 基础强化
- 课程位置
- 1 / 12
学完这一课,你应该能:
- 能在纸上推导两层网络的前向和反向传播
- 能解释框架为什么计算的是向量–Jacobian 乘积
- 能用有限差分检查自定义算子的梯度
- 能定位 softmax、交叉熵和混合精度中的数值问题
1. 先把神经网络看成一串带形状的函数
设批次为 (B),输入维度为 (D),隐藏层宽度为 (H),类别数为 (C):
X [B, D]
W1 [D, H] b1 [H]
W2 [H, C] b2 [C]
Z1 = X @ W1 + b1
H1 = ReLU(Z1)
Z2 = H1 @ W2 + b2
P = softmax(Z2)
L = cross_entropy(P, y)
先写形状,再写公式。X @ W1 的内部维度 (D) 相等,输出是 [B,H];b1 [H] 沿批次维广播。很多训练 Bug 不是算法错误,而是某个广播“恰好能运行”,语义却错了。
形状检查原则:每次矩阵乘法都写出输入、收缩维和输出;每次广播都说明复制到了哪些轴。
2. 反向传播就是链式法则的工程化
若标量损失 (L) 依赖向量 (y),而 (y) 依赖向量 (x):
∂L/∂x = (∂y/∂x)ᵀ · ∂L/∂y
(\partial y/\partial x) 是 Jacobian。深度网络的 Jacobian 可能巨大,框架不会完整构造它,而是从上游梯度出发逐节点计算 vector–Jacobian product(VJP)。
对线性层 (Z=XW),设 (G=\partial L/\partial Z):
∂L/∂X = G @ Wᵀ [B,H] @ [H,D] -> [B,D]
∂L/∂W = Xᵀ @ G [D,B] @ [B,H] -> [D,H]
这两行是以后理解 Attention、LoRA 与 MoE 反向传播的基础。若 y=f(x)+g(x),梯度通过两条路径返回并相加。残差连接 y=x+block(x) 因而保留一条恒等梯度路径。
3. Softmax 与交叉熵要一起推导
pᵢ = exp(zᵢ) / Σⱼ exp(zⱼ)
直接计算 exp(z) 可能溢出。softmax 对所有 logits 同减一个常数不敏感:
import numpy as np
def stable_softmax(x):
shifted = x - x.max(axis=-1, keepdims=True)
exp_x = np.exp(shifted)
return exp_x / exp_x.sum(axis=-1, keepdims=True)
单样本交叉熵 (L=-\log p_y)。把 softmax 与交叉熵合并求导:
∂L/∂z = p - one_hot(y)
所以分类模型 logits 的梯度是“预测概率减真实标签”。框架把 log-softmax 与 NLL 合并,不只是 API 方便,也是为避免概率接近 0 时再取对数的数值问题。
4. 用有限差分验证梯度
中心差分:
df/dx ≈ [f(x+ε) - f(x-ε)] / (2ε)
import torch
torch.manual_seed(17)
x = torch.randn(4, dtype=torch.float64, requires_grad=True)
w = torch.randn(4, dtype=torch.float64)
def loss_fn(value):
return torch.tanh(value * w).sum()
loss_fn(x).backward()
autograd_grad = x.grad.detach().clone()
epsilon = 1e-6
numeric_grad = torch.zeros_like(x)
with torch.no_grad():
for i in range(x.numel()):
plus, minus = x.detach().clone(), x.detach().clone()
plus[i] += epsilon
minus[i] -= epsilon
numeric_grad[i] = (loss_fn(plus) - loss_fn(minus)) / (2 * epsilon)
print("max diff:", (autograd_grad - numeric_grad).abs().max().item())
梯度检查优先用 float64。也不要认为 (\epsilon) 越小越好:太大时差分近似误差大,太小时两个函数值在浮点表示中过于接近,消减误差增大。扫描 1e-2 到 1e-10,误差通常呈 U 型。
5. Autograd 中最容易误解的四件事
梯度默认累积
.backward() 把梯度加到 .grad,不是覆盖:
optimizer.zero_grad(set_to_none=True)
loss.backward()
optimizer.step()
梯度累积会故意多次 backward 再 step;通常要把每个 micro-batch 的 loss 除以累积步数。
detach() 切断计算图
tensor.detach() 与原张量共享存储,但不再追踪梯度。它适合记录指标、固定 target 或截断循环状态;误用会让某段网络永远收不到梯度。
In-place 操作会改写中间量
若 backward 需要前向旧值,而你执行 x += ...,框架可能报版本错误。自定义模块先使用非 in-place 版本,再优化性能。
model.eval() 不等于关闭梯度
eval() 控制 Dropout、BatchNorm;no_grad() 或 inference_mode() 控制 Autograd。评测通常两者都需要。
6. 梯度爆炸、消失与混合精度
局部 Jacobian 的谱范数长期大于 1,梯度可能爆炸;长期小于 1,则可能消失。不要只看 Loss:
total_norm = torch.nn.utils.clip_grad_norm_(
model.parameters(), max_norm=float("inf")
)
tracker.log({
"loss": loss.item(),
"grad_norm": total_norm.item(),
"lr": optimizer.param_groups[0]["lr"],
})
FP16 中,小梯度可能下溢为 0。Loss scaling 先放大 loss 和梯度,更新前再反缩放。出现 NaN 时:
- 找到第一个非有限的 activation 或 gradient。
- 检查学习率、输入范围、除零、
log(0)与 mask。 - 确认在反缩放后做梯度裁剪。
- 比较 BF16 与 FP16 的指数范围和精度。
7. 本课实验:两层网络梯度审计
实现 [8,4] -> [8,6] -> [8,3] 分类器:
| 证据 | 验收条件 |
|---|---|
| 形状表 | 列出每个前向张量与梯度张量 |
| 手推梯度 | 至少推导 W2 与 W1 |
| 数值检查 | float64 最大绝对误差小于 1e-5 |
| 故障实验 | 去掉稳定 softmax,构造溢出 |
| 训练曲线 | 同时记录 loss、grad norm、parameter norm |
第一版不要调用 nn.Linear,先用裸参数与矩阵乘法完成,再与 nn.Linear 对齐。
8. 课后练习与答案提示
y=x*x+x的梯度?**提示:**两条路径相加,得到2x+1。- 为什么减去
max(logits)不改变 softmax?**提示:**分子分母乘了相同常数。 - 为什么交叉熵接收 logits?**提示:**可合并 log-softmax 与 NLL,数值更稳定。
- 累积 4 步不除 loss 会怎样?**提示:**梯度约放大 4 倍,等效学习率改变。
- 为什么 (\epsilon) 不是越小越好?**提示:**截断误差与浮点消减误差同时存在。