📑 本页目录(点开跳转)
12 · 反向传播的完整推导
⏱ 36 分钟 | 🎁 基础教程第 8 章的数学补完
🎯 一句话
基础教程给了你能跑的代码,这一章给你为什么那几行是对的—— 一次严格的链式法则推导,做完之后任何网络结构你都能自己推。
🏗️ 一、设定与记号
第 l 层:
zˡ = Wˡ aˡ⁻¹ + bˡ (线性部分,pre-activation)
aˡ = f(zˡ) (非线性激活)
最后一层 L 的输出 a^L 进损失函数 J
核心中间量(整个推导的关键):
$$\delta^l \equiv \frac{\partial J}{\partial z^l}$$
💡 人话:δˡ 是「第 l 层的线性输出变一点点,最终损失变多少」——也就是这一层要承担的责任。
🔑 为什么定义在 z 上而不是 a 上:因为 W 和 b 直接产生 z, 有了 δ 就能一步得到 W 和 b 的梯度。这个选择是整个推导简洁的原因。
💡 一个通用技巧:推导任何复杂求导时,先找那个"定义在它上面最省事"的中间量。 这个技巧在 Transformer、CNN、RNN 的推导里同样管用。
📐 二、四个方程(反向传播的全部)
📐 完整推导(想看再点)
方程 1:输出层的 δ
$$\delta^L = \frac{\partial J}{\partial a^L}\odot f'(z^L)$$
由链式法则:$\frac{\partial J}{\partial z^L_j} = \frac{\partial J}{\partial a^L_j}\cdot\frac{\partial a^L_j}{\partial z^L_j}$,而 $a^L_j = f(z^L_j)$。
方程 2:δ 的反向递推(最关键的一步)
$$\delta^l = \left((W^{l+1})^\top \delta^{l+1}\right)\odot f'(z^l)$$
推导:$z^{l+1} = W^{l+1}f(z^l) + b^{l+1}$,所以
$$\frac{\partial J}{\partial z^l_j} = \sum_k \frac{\partial J}{\partial z^{l+1}_k}\cdot\frac{\partial z^{l+1}_k}{\partial z^l_j} = \sum_k \delta^{l+1}_k W^{l+1}_{kj} f'(z^l_j)$$
写成矩阵形式即得。注意求和号——第 l 层的一个神经元影响了下一层的所有神经元,责任要全加起来。
方程 3、4:参数的梯度
$$\frac{\partial J}{\partial W^l} = \delta^l (a^{l-1})^\top, \qquad \frac{\partial J}{\partial b^l} = \delta^l$$
因为 $z^l = W^l a^{l-1}+b^l$,对 $W^l_{jk}$ 求导得 $a^{l-1}_k$。
💡 四个方程的人话版
| 方程 | 人话 |
|---|---|
| $\delta^L = \nabla_a J \odot f'(z^L)$ | 输出层的责任 = 损失对输出的敏感度 × 激活函数的斜率 |
| $\delta^l = (W^{l+1})^\top\delta^{l+1}\odot f'(z^l)$ | 责任往回传:下一层的责任按权重分摊回来,再乘本层激活的斜率 ⭐ |
| $\partial J/\partial W^l = \delta^l(a^{l-1})^\top$ | 权重的梯度 = 本层责任 × 上一层的输出 |
| $\partial J/\partial b^l = \delta^l$ | 偏置的梯度 = 本层责任 |
🔑 整个反向传播就是方程 2 在反复执行:从最后一层出发,一层层把"责任"传回去。
🧮 一个能手算的极小例子
网络:1 输入 → 1 隐层(sigmoid) → 1 输出,MSE 损失
x = 1, w¹ = 0.5, b¹ = 0, w² = 2, b² = 0, y = 1
【前向】
z¹ = 0.5·1 + 0 = 0.5
a¹ = σ(0.5) = 0.6225
z² = 2·0.6225 = 1.2450
a² = z² = 1.2450 (输出层用恒等激活)
J = ½(a²−y)² = ½(0.245)² = 0.0300
【反向】
δ² = (a²−y)·1 = 0.2450 ← 方程1
δ¹ = (w²·δ²)·σ'(z¹)
= (2·0.245)·(0.6225·(1−0.6225))
= 0.490 · 0.2350 = 0.1152 ← 方程2 ⭐
【梯度】
∂J/∂w² = δ²·a¹ = 0.245·0.6225 = 0.1525 ← 方程3
∂J/∂b² = δ² = 0.2450
∂J/∂w¹ = δ¹·x = 0.1152·1 = 0.1152
∂J/∂b¹ = δ¹ = 0.1152
✅ 建议真的手算一遍(或写 5 行代码验证)。 手算过一次之后,再复杂的网络你都只是在重复这四步。 注意 δ¹ 已经比 δ² 小了一半多(0.115 vs 0.245)——这就是梯度消失的种子。
⭐ 三、四个立刻能用的洞察
① 梯度消失/爆炸的精确来源
把方程 2 展开到底:
$$\delta^1 = \left[\prod_{l=2}^{L} (W^l)^\top \,\text{diag}(f'(z^{l-1}))\right]\delta^L$$
💡 看那个连乘号:
每穿过一层,梯度就乘一次 W 和一次 f'(z)
若 |W·f'| < 1 → L 层后指数衰减 → 【梯度消失】
若 |W·f'| > 1 → L 层后指数增长 → 【梯度爆炸】
具体数字(假设 W 的尺度是 1):
| 激活函数 | f' 的最大值 | 10 层后 | 30 层后 |
|---|---|---|---|
| sigmoid | 0.25 | 0.25¹⁰ ≈ 10⁻⁶ 💀 | 10⁻¹⁸ 💀💀 |
| tanh | 1.0(但只在 0 附近) | 实际约 10⁻² | 10⁻⁶ |
| ReLU | 1.0(正区间恒为 1) | 1.0 ✅ | 1.0 ✅ |
🔗 这就是基础教程第 7 章说"ReLU 是深度学习转折点"的数学根据。 不是实验发现,是可以算出来的。
② 残差连接为什么有效
有残差时:a^l = f(z^l) + a^{l-1}
→ ∂a^l/∂a^{l-1} = f'(z^l)·W^l + I
↑ 多了个单位矩阵!
→ 即使 f'·W 很小,梯度也能通过 I 这条路【原样】传回去
→ 连乘变成了"连乘 + 直通"
💡 展开来看更清楚:
无残差:∂J/∂a¹ = (f'W)_L · (f'W)_{L-1} ··· (f'W)_2
→ 一条路,全乘小数 → 死
有残差:∂J/∂a¹ = (f'W + I)_L ··· (f'W + I)_2
= I·I···I + (交叉项) + (f'W)···(f'W)
↑ 恒等路径,永远是 1 ⭐
🔗 基础教程第 12 章说的"梯度高速公路",公式上就是那个
+I。 这也是为什么 Transformer 每个子层都套残差——没有它,几十层根本训不动。
③ 为什么 softmax + 交叉熵那么干净
单独看:softmax 的导数是个矩阵,交叉熵的导数有 1/p
合起来:所有复杂项【互相约掉】
δ^L = a^L − y (预测 − 真实)⭐
这不只是"好看",有三个实际后果:
| 后果 | 说明 |
|---|---|
| 计算快 | 不用真的算 softmax 的雅可比矩阵(那是 K×K 的) |
| 数值稳 | 避免了 1/p 在 p→0 时爆炸 ⭐ |
| 不饱和 | 梯度直接正比于误差,预测越离谱梯度越大——不会像 sigmoid+MSE 那样在错得最离谱时梯度反而最小 |
⚠️ 第三点是个真实的坑:sigmoid + MSE 时, δ = (a−y)·σ'(z),如果预测严重错误(σ 饱和),σ' ≈ 0 → 梯度几乎为零,学不动。 换成交叉熵,σ' 被约掉了,梯度 = (a−y),错得越多学得越快 ✅
🔗 第 1 章告诉你为什么用交叉熵(伯努利/类别分布的 MLE), 这里告诉你为什么这个选择在计算上也是最优的——它和 softmax 是配对设计的。
④ 为什么必须缓存前向的中间值
看方程 3:∂J/∂Wˡ = δˡ (aˡ⁻¹)ᵀ
↑ 需要【前向时】的激活值
→ 所有中间激活都必须存下来,直到反向传播用完
→ 显存占用 ∝ 层数 × batch × 每层激活大小
这解释了三件实践中的事:
| 现象 | 原因 |
|---|---|
| 训练比推理吃显存多得多 ⭐ | 推理不用存中间激活,用完就丢 |
| batch size 一大就 OOM | 激活显存和 batch 成正比 |
| 梯度检查点(gradient checkpointing)能省显存 | 只存部分层的激活,反向时重算中间的——用时间换空间 |
💡
torch.no_grad()为什么能省显存——它告诉 autograd 不用建计算图、不用存激活。 推理时永远记得加。
🧮 四、复杂度:为什么反向传播是个突破
朴素做法(数值梯度):
对每个参数 ±ε 各跑一次前向 → 2P 次前向传播
P = 100 万参数 → 200 万次前向 💀
反向传播:
1 次前向 + 1 次反向 ≈ 2 次前向的代价
一次拿到【全部】参数的梯度 ⭐
→ 从 O(P) 次前向降到 O(1) 次
💡 这就是为什么 1986 年反向传播的普及是个里程碑——不是想法新(链式法则几百年了), 而是它让训练大网络在计算上变得可行。
🔍 一个更深的视角:反向模式自动微分
链式法则可以【从前往后】乘,也可以【从后往前】乘
前向模式:一次前向求出【一个输入】对所有输出的导数
→ 适合 输入少、输出多
反向模式:一次反向求出【一个输出】对所有输入的导数 ⭐
→ 适合 输入多、输出少
深度学习:参数上百万(输入多),损失是一个标量(输出 1 个)
→ 反向模式完胜 ⭐
🔑 所以"反向传播"其实是"反向模式自动微分"在神经网络上的应用。 PyTorch 的
loss.backward()就是通用的反向模式 AD—— 它不知道什么叫"神经网络",它只知道计算图。🔗 基础教程第 18 章挑战 A 让你亲手实现这个通用机制。
✅ 五、梯度检验(自己实现时必做)
$$\frac{\partial J}{\partial \theta_i} \approx \frac{J(\theta + \varepsilon e_i) - J(\theta-\varepsilon e_i)}{2\varepsilon}$$
import numpy as np
def grad_check(f, theta, analytic_grad, eps=1e-5):
"""f: 参数 → 标量损失;analytic_grad: 你的反向传播算出的梯度"""
num_grad = np.zeros_like(theta)
it = np.nditer(theta, flags=["multi_index"])
while not it.finished:
i = it.multi_index
old = theta[i]
theta[i] = old + eps; plus = f(theta)
theta[i] = old - eps; minus = f(theta)
theta[i] = old
num_grad[i] = (plus - minus) / (2 * eps) # ⭐ 中心差分,误差 O(ε²)
it.iternext()
rel = np.abs(num_grad - analytic_grad) / (
np.abs(num_grad) + np.abs(analytic_grad) + 1e-12)
return rel.max()
相对误差判定:
< 1e-7 ✅ 正确
1e-7~1e-4 ⚠️ 可疑(可能是 ReLU 折点,可接受)
> 1e-4 ❌ 有 bug
四个必知的坑 ⚠️:
| 坑 | 说明 |
|---|---|
| 必须用中心差分 | 单边差分误差是 O(ε),中心差分是 O(ε²)——差两个数量级 ⭐ |
| ε 不能太小 | 1e-5 左右最好。太小会被浮点舍入误差淹没 |
| ReLU 的折点 | 在 z≈0 处数值梯度不准,这是函数本身不可导,不是 bug |
| 关掉 Dropout 和 BN 的随机性 | 否则两次前向的网络不一样,比较毫无意义 ⭐ |
🔗 基础教程第 18 章挑战 A会大量用到这个。 实测参考值:基础教程第 8 章那个手写两层网络的反传,梯度检验相对误差是 1.49e-08 ✅ —— 那是作者跑出来的数字,可以拿来对标。 ⚠️ 第 18 章本身不给参考实现(它是让你自己造 mini-torch 的项目章),它给的是两条验收线: 逐算子梯度检验 < 1e-6、和 PyTorch 端到端对拍 < 1e-5。
🔗 六、和站内其他章的关系
| 相关的地方 | 这里的位置 |
|---|---|
| 基础教程第 8 章的代码 | 本章是它的推导 |
| ReLU 是深度学习转折点 | 洞察 ① 的连乘公式 |
| 残差连接 / 梯度高速公路 | 洞察 ② 的 +I |
| 分类要用交叉熵不用 MSE | 洞察 ③ 的不饱和性质 |
torch.no_grad() 省显存 |
洞察 ④ 不建计算图 |
| PyTorch 的 autograd | 反向模式自动微分 |
| 第 1 章 MLE | 损失函数从哪来 |
✅ 检查点
- δˡ 的定义是什么?为什么定义在 z 上而不是 a 上?
- 手算那个例子:δ¹ 是多少?它比 δ² 小说明了什么?
- 方程 2 里为什么有个求和(矩阵转置)?
- 从方程 2 怎么看出梯度消失的原因?sigmoid 10 层后剩多少?
- 残差连接在公式上多了什么?展开后为什么能救梯度?
- 为什么 sigmoid+MSE 在"错得最离谱"时反而学不动?交叉熵怎么解决的?
- 为什么训练比推理吃显存多得多?梯度检查点是拿什么换什么?
- 梯度检验为什么必须用中心差分?做之前必须关掉什么?
- 为什么深度学习用反向模式而不是前向模式自动微分?
👀 答案
- δˡ = ∂J/∂zˡ,即"第 l 层线性输出变一点,损失变多少"。定义在 z 上是因为 W 和 b 直接产生 z,有了 δ 一步就能得到 W、b 的梯度,推导最简洁。
- δ¹ = (2×0.245)×0.235 = 0.1152,约为 δ² 的一半。说明梯度在往回传的过程中已经在衰减——这就是梯度消失的种子,层数一多就是指数级衰减。
- 因为第 l 层的一个神经元会影响下一层的所有神经元,它的责任要把下一层各条路径的责任加起来。矩阵形式就是 (W^{l+1})ᵀδ^{l+1}。
- 展开后梯度是一串 W·f'(z) 的连乘。若每项 <1 则指数衰减(消失),>1 则指数增长(爆炸)。sigmoid 的 f'≤0.25,10 层后 0.25¹⁰ ≈ 10⁻⁶。
- 多了一个单位矩阵 I:∂aˡ/∂aˡ⁻¹ = f'·W + I。展开后有一条 I·I···I 的恒等路径永远是 1,即使 f'·W 全是小数,梯度也能原样传回。
- sigmoid+MSE 时 δ = (a−y)·σ'(z),预测严重错误时 σ 饱和 → σ'≈0 → 梯度几乎为零。交叉熵把 σ' 约掉了,δ = a−y,错得越多梯度越大。
- 因为方程 3 需要前向时的激活值 aˡ⁻¹,所有中间激活都必须缓存到反向用完;推理用完就丢。梯度检查点只存部分层,反向时重算中间的——用时间换空间。
- 单边差分误差 O(ε),中心差分 O(ε²),差两个数量级。做之前必须关掉 Dropout 和 BN 的随机性,否则两次前向的网络不同,比较无意义。
- 反向模式一次反向求出一个输出对所有输入的导数,适合"输入多、输出少"。深度学习正是参数上百万、损失是一个标量——完美匹配。
🛑 可以停在这里
⚡ 走神救援
核心量 δˡ = ∂J/∂zˡ = 第 l 层要担的责任(定义在 z 上是因为 W、b 直接产生 z——"找那个定义在它上面最省事的中间量"是通用推导技巧)。四方程:输出层 δ = 损失敏感度×激活斜率;递推 δˡ = (Wˡ⁺¹)ᵀδˡ⁺¹ ⊙ f'(zˡ)(求和是因为一个神经元影响下层全部);权重梯度 = δ×上层输出;偏置梯度 = δ。建议手算一遍那个 1-1-1 的例子。四洞察:①展开是 W·f' 的连乘 → 梯度消失的精确来源(sigmoid 10层后 10⁻⁶,ReLU 正区间恒为 1)②残差 +I,展开后有条恒等路径永远是 1 ③softmax+交叉熵约掉后 δ = 预测−真实,不饱和——sigmoid+MSE 错得最离谱时反而学不动 ④方程3需要前向激活 → 必须缓存 → 训练比推理吃显存多,梯度检查点用时间换空间。复杂度 O(P)→O(1) 次前向,本质是反向模式自动微分(输入多输出少时完胜)。梯度检验:中心差分,ε≈1e-5,先关 Dropout/BN。
下一节 👉 13-EM与高斯混合.md