第 6 章:反向传播
💡 导读: 这是整本小册子最核心的章节。反向传播是神经网络训练的引擎,我们将从最末端的损失函数出发,一步步反向推导,带你领略数学公式中奇妙的“抵消与坍缩”之美。
6.1 问题:参数该如何更新?(量化推导)
训练的终极目标是让损失函数
这里我们需要用微积分中的一阶泰勒展开式来进行量化。假设我们给参数
我们的目标是让新的损失变小,即
这就得出了参数更新的铁律:变化量
- 如果梯度 > 0:为了让乘积小于 0,
必须为负(即减小参数)。 - 如果梯度 < 0:为了让乘积小于 0,
必须为正(即增大参数)。
反向传播算法,就是用来极其高效地计算出所有参数对应的梯度
6.2 计算图与链式法则
我们的神经网络前向传播是一条严密的流水线(从左到右):
输入
[线性输出 ] [激活输出 ] [线性输出 ] [激活输出 ] [计算损失 ]
反向传播则是时光倒流,利用链式法则,从最右边的
6.3 符号约定
为了让接下来的推导清晰无负担,我们约定以下符号。黄金规律:任何一层变量的梯度矩阵,其形状(Shape)必定与原变量完全一致。
| 变量含义 | 梯度简写 | 数学表达 | 矩阵形状 (Shape) |
|---|---|---|---|
| 第二层激活输出 (预测概率) | (n, 10) | ||
| 第二层线性输出 (Logits) | (n, 10) | ||
| 第一层激活输出 | (n, 50) | ||
| 第一层线性输出 | (n, 50) | ||
| 第二层权重参数 | (50, 10) | ||
| 第二层偏置参数 | (10,) | ||
| 第一层权重参数 | (784, 50) | ||
| 第一层偏置参数 | (50,) |
(注:
6.4 第一步:损失对激活输出求导 ( )
我们要弄清楚:交叉熵的导数
首先,我们给出多分类交叉熵损失函数(Cross-Entropy Loss)的通用标准公式。 对于单个样本,假设网络总共有
化简过程(One-Hot 编码的魔法): 这里的真实标签
所以,标准公式直接坍缩成了我们在书里常用的极简形式:
求导过程: 现在,我们要看这个损失
当求导目标是正确类别的概率
时: 公式 里刚好有 ,直接套用对数求导公式,保留外面的负号: 当求导目标是其他错误类别的概率
时 ( ): 公式 里根本没有 这个变量!对于 来说,整项 就像一个常数。常数求导等于 :
这就是第一步梯度的完整由来,没有任何跳跃。这构成了我们向后传递的第一级梯度向量
6.5 第二步:激活输出对线性输出求导 ( )
接下来,梯度要穿过 Softmax 函数,追溯到第二层的线性输出
在这里,输入是一个长度为 10 的向量
为了求这个导数,我们使用商的求导法则:
情况 A:当
将其拆分并化简:
情况 B:当
将其拆分并化简:
至此,我们得到了完整的雅可比矩阵
6.6 见证奇迹:损失对线性输出的联合求导 ( )
现在,我们要将第一步和第二步结合。根据多元微积分的链式法则,损失
展开为代数求和的形式,我们要计算
魔法时刻 1:雅可比矩阵的坍缩 回想 6.4 节,除了
这意味着,长达 10 项的求和公式直接坍缩,只剩下唯一的一项:
魔法时刻 2:复杂分式的完美抵消 现在,把 6.5 节求得的雅可比矩阵元素代入进来:
- 如果
(即求导位置恰好是正确类别的打分): 代入情况 A 的结果: - 如果
(即求导位置是错误类别的打分): 代入情况 B 的结果:
极简结论: 无论是哪种情况,最终结果都可以统一为一句极度优美的话:损失对该层线性输出的梯度,就等于网络的预测概率 减去 该类别的真实标签(1 或 0)。
若推广到
(其中
6.7 第三步:线性输出对参数及前一层求导 ( )
源头梯度
- 对权重
求导: 根据矩阵微积分法则,梯度等于输入端 的转置 乘以 输出端梯度 : - 对偏置
求导: 因为偏置是对所有 个样本进行“广播”相加的,反向传播时需要把这 个样本的梯度按列累加: - 向上一层回传梯度: 为了让网络继续反向传播,需要求出对隐藏层输出
的梯度:
6.8 第四步:梯度穿透隐藏层激活函数 ( )
梯度来到了第一层的 Sigmoid 激活函数:
Sigmoid 的导数公式为:
6.9 第五步:第一层参数的梯度 ( )
这就完全是 6.7 节的重演了。 第一层前向公式:
权重梯度:
偏置梯度(对
6.10 反向传播:极其优雅的代码实现
将上述纯数学公式翻译成代码,利用 NumPy 的高级索引,我们连构造 One-Hot 矩阵的内存都省了:
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
z1 = sigmoid(a1)
a2 = z1 @ W2 + b2
y = softmax(a2)
# ================= 反向传播 =================
# 6.4 - 6.6: Softmax 与交叉熵联合梯度 (抵消坍缩后的极简实现)
da2 = y.copy()
da2[np.arange(n), t] -= 1 # 精准在正确类别的位置减 1
da2 /= n
# 6.7: 第二层参数与回传梯度
dW2 = z1.T @ da2
db2 = np.sum(da2, axis=0)
dz1 = da2 @ W2.T
# 6.8: 穿透 Sigmoid 激活函数
da1 = dz1 * (z1 * (1 - z1)) # '*' 代表逐元素相乘
# 6.9: 第一层参数梯度
dW1 = X.T @ da1
db1 = np.sum(da1, axis=0)
return {'W1': dW1, 'b1': db1, 'W2': dW2, 'b2': db2}6.11 为什么不用数值梯度?
你可能会问:既然可以通过微小的变化
- 数值梯度: 要更新 39,760 个参数,你需要做近 80,000 次前向传播。慢如蜗牛,但代码好写,通常只用来做测试,验证反向传播代码是否写错了。
- 反向传播: 只需要 1 次前向传播 + 1 次反向计算。速度快约 1000 倍。这是工业界实际训练的唯一方案。
下一章预告:现在我们拿到了所有参数的“指导意见”(梯度),接下来该怎么走?SGD、Momentum、Adam 等各种高级“走法”即将登场。
← 第 5 章 | 返回目录 | 第 7 章:优化器 →