第 6 章:反向传播
这是整本小册子最核心的章节。反向传播是神经网络训练的引擎,理解它意味着你真正理解了深度学习的本质。
6.1 问题:参数如何更新?
训练的目标是让损失
每个参数应该增大还是减小,才能让 L 减小?
答案就是梯度(Gradient):
梯度告诉我们,当参数
- 梯度 > 0:L 随
增大而增大 → 应该减小 - 梯度 < 0:L 随
增大而减小 → 应该增大 - 梯度 = 0:当前参数处于极值点
反向传播就是高效计算所有参数梯度的算法。
6.2 链式法则:反向传播的数学基础
反向传播的核心数学工具是链式法则(Chain Rule)。
6.2.1 单变量链式法则
如果
6.2.2 直观理解
想象一个工厂流水线:
- 当 x 变化 1 单位时,y 变化
单位 - 当 y 变化 1 单位时,z 变化
单位 - 所以当 x 变化 1 单位时,z 变化
单位
反向传播就是把这个"链条"从右到左依次计算。
6.2.3 在神经网络中
我们的计算图(从左到右):
X → [a1 = X@W1+b1] → [z1 = σ(a1)] → [a2 = z1@W2+b2] → [y = softmax(a2)] → [L]反向传播(从右到左)计算每个参数对 L 的贡献:
L → ∂L/∂a2 → ∂L/∂z1 → ∂L/∂a1 → ∂L/∂W1, ∂L/∂b1
↓
∂L/∂W2, ∂L/∂b26.3 符号约定
为了简洁,我们用简写:
| 简写 | 完整含义 |
|---|---|
(n, 10) | |
(n, 50) | |
(n, 50) | |
(50, 10) | |
(10,) | |
(784, 50) | |
(50,) |
6.4 第一步:输出层梯度(Softmax + Cross-Entropy 联合推导)
这是最精彩的部分。我们要推导
6.4.1 设置
:第二层线性输出 ,
我们只关注单个样本(下标
单个样本的损失:
6.4.2 对 softmax 输出 y 的梯度
因为
6.4.3 softmax 对输入 a₂ 的导数
softmax 的输出
对输入
当
用商的求导法则,令
当
6.4.4 联合推导(链式法则)
把求和拆开,
代入刚才求的偏导(注意
情况1:
情况2:
6.4.5 合并成一个公式
其中
用向量形式(对单个样本):
其中
6.4.6 推广到批量(除以 n)
对
其中 (n, 10),
代码实现:
n = X.shape[0]
dy = y.copy() # shape (n, 10),先复制预测概率
dy[np.arange(n), t] -= 1 # 正确类别的位置减 1
dy /= n # 除以批量大小得平均梯度这段代码几乎是数学公式的直译!
6.5 第二步:第二层参数的梯度
已知 (n, 10)。
前向传播:
W₂ 的梯度
对于矩阵乘法
维度验证:
z1.T @ dy:
z1.T: (50, n)
dy: (n, 10)
结果: (50, 10) ← 和 W2 的 shape 相同 ✓直观理解: 如果
矩阵梯度的完整推导(选读):
考虑
对
所以:
写成矩阵形式:(50, n) @ (n, 10) = (50, 10) ✓
b₂ 的梯度
前向传播是
所以
维度验证:
np.sum(dy, axis=0):
dy: (n, 10), 对 n 行求和 → (10,) ← 和 b2 的 shape 相同 ✓传回隐藏层的梯度
前向传播:
维度验证:
dy @ W2.T:
dy: (n, 10)
W2.T: (10, 50) ← W2 的转置
结果: (n, 50) ← 和 z1 的 shape 相同 ✓直观理解: 每个隐藏节点
6.6 第三步:隐藏层的梯度(Sigmoid 反向传播)
已知 (n, 50)。
前向传播:
根据链式法则:
写成矩阵形式(逐元素乘法):
da1 = dz1 * sigmoid_grad(z1) # 逐元素乘,shape不变
# sigmoid_grad(z1) = z1 * (1 - z1)维度验证:
dz1: (n, 50)
sigmoid_grad(z1): (n, 50) ← z1 是 sigmoid 输出,shape 和 a1 一样
da1: (n, 50) ← 逐元素乘,shape不变 ✓6.7 第四步:第一层参数的梯度
已知
维度验证:
X.T @ da1:
X.T: (784, n)
da1: (n, 50)
结果: (784, 50) ← 和 W1 的 shape 相同 ✓维度验证:
np.sum(da1, axis=0):
da1: (n, 50), 对 n 行求和 → (50,) ← 和 b1 的 shape 相同 ✓6.8 完整反向传播总结
前向传播(左 → 右):
X(n,784) → a1(n,50) → z1(n,50) → a2(n,10) → y(n,10) → L(标量)反向传播(右 → 左):
① 输出层(softmax + 交叉熵联合):
dy = (y - T) / n shape: (n, 10)
② 第二层参数:
dW2 = z1.T @ dy shape: (50, 10) ← 和 W2 相同
db2 = sum(dy, axis=0) shape: (10,) ← 和 b2 相同
③ 向上传梯度:
dz1 = dy @ W2.T shape: (n, 50) ← 和 z1 相同
④ sigmoid 反向:
da1 = dz1 * z1*(1-z1) shape: (n, 50) ← 和 a1 相同
⑤ 第一层参数:
dW1 = X.T @ da1 shape: (784, 50) ← 和 W1 相同
db1 = sum(da1, axis=0) shape: (50,) ← 和 b1 相同黄金规律:每个参数的梯度形状,永远和该参数本身的形状相同。
6.9 代码实现逐行解析
def _backprop_gradient(self, X, t):
n = X.shape[0]
W1, b1 = self.params['W1'], self.params['b1']
W2, b2 = self.params['W2'], self.params['b2']
# ① 前向传播(缓存中间值,反向用)
a1 = X @ W1 + b1 # (n, 50)
z1 = sigmoid(a1) # (n, 50)
a2 = z1 @ W2 + b2 # (n, 10)
y = softmax(a2) # (n, 10)
# ② 输出层梯度(softmax + cross-entropy 联合)
dy = y.copy() # (n, 10)
dy[np.arange(n), t] -= 1 # 正确类别位置 -1(构造 y - T)
dy /= n # 除以 n 取平均
# ③ 第二层参数梯度
dW2 = z1.T @ dy # (50,n)@(n,10) = (50,10)
db2 = np.sum(dy, axis=0) # (10,)
# ④ 梯度传回隐藏层
dz1 = dy @ W2.T # (n,10)@(10,50) = (n,50)
# ⑤ sigmoid 反向传播
da1 = dz1 * sigmoid_grad(z1) # (n,50) ⊙ (n,50) = (n,50)
# ⑥ 第一层参数梯度
dW1 = X.T @ da1 # (784,n)@(n,50) = (784,50)
db1 = np.sum(da1, axis=0) # (50,)
return {'W1': dW1, 'b1': db1, 'W2': dW2, 'b2': db2}6.10 数值梯度 vs 反向传播
数值梯度(验证用)
用中心差分法近似导数:
- 对每个参数,分别做两次前向传播
- 39,760 个参数 → 约 80,000 次前向传播
- 慢,但无需手动推导,用于验证反向传播是否正确
反向传播
- 只需一次前向传播 + 一次反向传播
- 速度快约 1000 倍
- 是实际训练所用的方法
6.11 小结
| 步骤 | 公式 | Shape |
|---|---|---|
| 输出层梯度 | dy = (y - T) / n | (n, 10) |
| dW2 | z1.T @ dy | (50, 10) |
| db2 | sum(dy, axis=0) | (10,) |
| 传回梯度 | dz1 = dy @ W2.T | (n, 50) |
| sigmoid 反传 | da1 = dz1 * z1*(1-z1) | (n, 50) |
| dW1 | X.T @ da1 | (784, 50) |
| db1 | sum(da1, axis=0) | (50,) |
下一章:有了梯度,怎么用它来更新参数?SGD、Momentum、Adam 三种策略。
← 第 5 章 | 返回目录 | 第 7 章:优化器 →