🏠 总目录📚 本教程 12 · 反向传播完整推导
📑 本页目录(点开跳转)

12 · 反向传播的完整推导

36 分钟 | 🎁 基础教程第 8 章的数学补完


🎯 一句话

基础教程给了你能跑的代码,这一章给你为什么那几行是对的—— 一次严格的链式法则推导,做完之后任何网络结构你都能自己推。

输入 x隐层 a¹隐层 a²输出 ŷ前向:算出预测反向:把「责任」δ 一层层传回去δ = ∂J/∂z这一层要担多少责任
δ = ∂J/∂z 就是图里那条橙色回路上传的东西。⭐ 把它展开到底,会得到一串 W·f′ 的连乘 —— 那就是梯度消失和爆炸的精确来源。

🏗️ 一、设定与记号

   第 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 损失函数从哪来

✅ 检查点

  1. δˡ 的定义是什么?为什么定义在 z 上而不是 a 上?
  2. 手算那个例子:δ¹ 是多少?它比 δ² 小说明了什么?
  3. 方程 2 里为什么有个求和(矩阵转置)?
  4. 从方程 2 怎么看出梯度消失的原因?sigmoid 10 层后剩多少?
  5. 残差连接在公式上多了什么?展开后为什么能救梯度?
  6. 为什么 sigmoid+MSE 在"错得最离谱"时反而学不动?交叉熵怎么解决的?
  7. 为什么训练比推理吃显存多得多?梯度检查点是拿什么换什么?
  8. 梯度检验为什么必须用中心差分?做之前必须关掉什么?
  9. 为什么深度学习用反向模式而不是前向模式自动微分?
👀 答案
  1. δˡ = ∂J/∂zˡ,即"第 l 层线性输出变一点,损失变多少"。定义在 z 上是因为 W 和 b 直接产生 z,有了 δ 一步就能得到 W、b 的梯度,推导最简洁。
  2. δ¹ = (2×0.245)×0.235 = 0.1152,约为 δ² 的一半。说明梯度在往回传的过程中已经在衰减——这就是梯度消失的种子,层数一多就是指数级衰减。
  3. 因为第 l 层的一个神经元会影响下一层的所有神经元,它的责任要把下一层各条路径的责任加起来。矩阵形式就是 (W^{l+1})ᵀδ^{l+1}。
  4. 展开后梯度是一串 W·f'(z) 的连乘。若每项 <1 则指数衰减(消失),>1 则指数增长(爆炸)。sigmoid 的 f'≤0.25,10 层后 0.25¹⁰ ≈ 10⁻⁶
  5. 多了一个单位矩阵 I:∂aˡ/∂aˡ⁻¹ = f'·W + I。展开后有一条 I·I···I 的恒等路径永远是 1,即使 f'·W 全是小数,梯度也能原样传回。
  6. sigmoid+MSE 时 δ = (a−y)·σ'(z),预测严重错误时 σ 饱和 → σ'≈0 → 梯度几乎为零。交叉熵把 σ' 约掉了,δ = a−y,错得越多梯度越大。
  7. 因为方程 3 需要前向时的激活值 aˡ⁻¹,所有中间激活都必须缓存到反向用完;推理用完就丢。梯度检查点只存部分层,反向时重算中间的——用时间换空间
  8. 单边差分误差 O(ε),中心差分 O(ε²),差两个数量级。做之前必须关掉 Dropout 和 BN 的随机性,否则两次前向的网络不同,比较无意义。
  9. 反向模式一次反向求出一个输出对所有输入的导数,适合"输入多、输出少"。深度学习正是参数上百万、损失是一个标量——完美匹配。

🛑 可以停在这里

走神救援

核心量 δˡ = ∂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

打卡记录保存在你的浏览器里,首页能看到总进度