| 项 | 内容 |
|---|---|
| 课程版本 | 李宏毅 2016 秋季《Machine Learning》Lecture 7:Backpropagation |
| 视频分 P | https://www.bilibili.com/video/BV1ou411N7X3?p=12 (31:27;搬运合集) |
| 官方讲义 | BP.pdf,19 页 |
| 材料说明 | 对应官方讲义整理+必要补充,非视频逐字稿 |
页码为 PDF 物理页(封面第 1 页)。这一讲只讲「怎么算梯度」,不重复梯度下降本身;第 2 页开宗明义:参数有 millions 个,为了高效算梯度才用 backpropagation。
一、本节的问题与直观理解
网络有几十万个参数,损失是这些参数的高维函数。若对每个参数单独做一次「扰动—重算损失」,一次梯度要前向传播几十万次,训练不可行。反向传播要解决的就是这个算术问题:一次前向 + 一次反向,把所有参数的偏导一起算出来,代价只相当于两三次前向。
直观理解可以拆成两个因子。对某个权重 \(w\),它先影响所在神经元的输入 \(z\),\(z\) 再往后影响损失:
第一个因子是局部的、几乎不用算:\(z=wx+\dots\),所以 \(\partial z/\partial w\) 就是连在这个权重上的输入值(第 6 页:\(\partial z/\partial w_1=x_1\),\(\partial z/\partial w_2=x_2\))。第二个因子 \(\partial C/\partial z\) 是「这个神经元的输入每变一点,损失变多少」,它要看完所有下游路径,所以从输出层往回传。前向存下输入,反向传递误差信号,这就是整个算法。
二、核心知识:两个因子与一条递推
总损失拆成单样本。 第 4 页:
所以只需推导单个样本的 \(\partial C/\partial w\),最后求和。
定义 \(\delta\)。 记第 \(l\) 层第 \(j\) 个神经元的输入为 \(z_j^{l}\),定义
第 8 页先给单神经元版本:
第 10–11 页再把 \(\partial C/\partial a\) 展开。一个神经元的输出 \(a\) 会喂给下一层多个神经元,所以要把每条路径加起来:
第 13 页处理输出层:若 \(z'\) 直接产出 \(y_1\),则 \(\partial C/\partial z'=(\partial y_1/\partial z')(\partial C/\partial y_1)\),链路到此结束;第 14–16 页处理非输出层,把同样的式子继续往回套,直到抵达输出层。第 17–18 页把这个过程画成「从输出层往输入层逐层点亮」。
矩阵形式(复习补充的整理,讲义是标量写法)。 对第 \(l\) 层:
维度必须能对上:\(\mathbf{a}^{l-1}\in\mathbb{R}^{n_{l-1}}\)、\(W^{l}\in\mathbb{R}^{n_l\times n_{l-1}}\)、\(\boldsymbol{\delta}^{l}\in\mathbb{R}^{n_l}\)、\(\partial C/\partial W^{l}\in\mathbb{R}^{n_l\times n_{l-1}}\)。\(\odot\) 是逐元素乘,\(\sigma'(\mathbf{z}^{l})\) 与 \(\boldsymbol{\delta}^{l+1}\) 过 \(W^{l+1\top}\) 之后的维度都是 \(n_l\)。
批量版:若一个 batch 的 \(\Delta^{l}\in\mathbb{R}^{B\times n_l}\)、\(A^{l-1}\in\mathbb{R}^{B\times n_{l-1}}\),则
这里默认 \(L\) 是 batch 内求和;若损失定义成平均,再除以 \(B\)。求和还是平均,会让梯度差一个 \(B\) 倍,和 P1 里 \(1/N\) 的问题是同一条。
三、关键推导:一个 2-3-2 网络的局部导数(复习补充)
讲义第 7 页给了三个具体数:\(\partial z/\partial w=-1\)、\(0.12\)、\(0.11\),用来说明「前向因子的值就是输入」。但讲义没有把整条链算完,这里补一个可以手算、可以用代码复核的小例子。
网络:输入 2 维 → 隐藏层 3 个 sigmoid → 输出 2 个 sigmoid,损失 \(C=\frac12\sum_k(y_k-t_k)^2\)。取
前向得 \(\mathbf{z}^{1}=(0.6,0.05,-0.6)\)、\(\mathbf{a}^{1}=(0.645656,0.512497,0.354344)\)、\(\mathbf{z}^{2}=(0.202014,0.787170)\)、\(\mathbf{y}=(0.550332,0.687223)\),损失 \(C=0.337238\)。
输出层用 MSE + sigmoid,局部导数恰好是「误差 × sigmoid 导数」:
往回传:
于是
用中心差分逐元素复核,四个参数块的最大绝对误差分别是 \(2.7\times10^{-11}\)、\(2.7\times10^{-11}\)、\(9.3\times10^{-11}\)、\(3.2\times10^{-11}\),纯浮点噪声。可复现的 NumPy 代码(不依赖 torch):
import numpy as np
sig = lambda z: 1 / (1 + np.exp(-z))
x = np.array([0.5, -1.0]); t = np.array([1.0, 0.0])
W1 = np.array([[0.2, -0.4], [0.7, 0.1], [-0.3, 0.5]]); b1 = np.array([0.1, -0.2, 0.05])
W2 = np.array([[0.6, -0.5, 0.2], [-0.1, 0.8, 0.4]]); b2 = np.array([0.0, 0.3])
a1 = sig(W1 @ x + b1) # (3,)
y = sig(W2 @ a1 + b2) # (2,)
d2 = (y - t) * y * (1 - y) # dC/dz2, (2,)
d1 = (W2.T @ d2) * a1 * (1 - a1) # dC/dz1, (3,)
gW1, gW2 = np.outer(d1, x), np.outer(d2, a1)
print(d2, d1, gW1.shape, gW2.shape) # (3,2) and (2,3), same as W1/W2
这个例子说明三件事:\(\delta\) 的维度与本层神经元数一致;\(\partial C/\partial W\) 的形状与 \(W\) 完全一致(否则没法更新);每一层的 \(\delta\) 都由「下一层的 \(\delta\) 过转置权重」再乘本层激活导数得到。
四、原讲义例子与结论
- 第 2 页:参数上百万,逐参数数值微分不可行,所以需要 BP。
- 第 3 页:链式法则两种情形——单链 \(z=h(y),y=g(x)\) 时 \(\frac{dz}{dx}=\frac{dz}{dy}\frac{dy}{dx}\);多路径 \(z=k(x,y)\) 时要把两条路径相加。第 10–11 页的多分支求和就是第二种情形。
- 第 7 页:前向因子的三个数 \(-1,0.12,0.11\),对应「连到该权重的输入」。
- 第 19 页总结成一句:\(\partial C/\partial w=a\cdot\partial C/\partial z\),\(a\) 是该权重连接的输入,\(\partial C/\partial z\) 由反向传播给出。这张总结页把整个算法压缩成「前向存 \(a\),反向存 \(\delta\),相乘得梯度」。
五、易错点
- BP 不是梯度下降。 BP 只负责算 \(\nabla L\);怎么用梯度(SGD、momentum、Adam)是另一层。说「用 BP 训练网络」是口语,严格说是「用 BP 算梯度、用某种优化器更新」。
- \(\delta\) 是从后往前递推的。 先算输出层 \(\delta^{L}\),再用 \(W^{l+1\top}\delta^{l+1}\) 往前推;方向搞反(用 \(W^{l}\) 往前乘)维度也许凑得上,但数学上完全错。
- \(\sigma'(z)\) 用的是前向时的 \(z\)。 所以必须在前向阶段把 \(z\) 或 \(a\) 存下来;反向时重新算一遍既浪费又容易和参数更新后的值不一致。
- 一个神经元的下游有多条路径时要相加。 第 10 页的 \(w_3\delta'+w_4\delta''\) 就是这件事;只取一条会漏梯度。
- 共享权重要把各处的梯度累加。 讲义这一讲是全连接网络,每个权重只出现一次;换成 CNN(卷积核共享)或 RNN(时间步共享)时,同一个参数在多处使用,\(\partial C/\partial w\) 必须是各处之和。
- 输出层的 \(\delta\) 依赖损失函数。 MSE + sigmoid 是 \((\mathbf{y}-\mathbf{t})\odot\mathbf{y}\odot(1-\mathbf{y})\);softmax + cross-entropy 化简后直接是 \(\mathbf{y}-\mathbf{t}\),少一个 \(\sigma'\)。换损失不换这行代码,梯度就错了。
- 批量求和与平均。 见第二节;差一个 \(B\) 倍,表现为学习率「突然不灵」。
六、自测
Q1. 为什么 \(\partial C/\partial W^{l}\) 的形状一定和 \(W^{l}\) 一样?
参考答案
因为损失对某个参数的偏导只是一个标量,把所有参数排成与 \(W^{l}\) 相同的矩阵,就得到同形状的梯度。从公式看 \(\boldsymbol{\delta}^{l}(\mathbf{a}^{l-1})^\top\) 是 \((n_l\times1)(1\times n_{l-1})\),正好是 \(n_l\times n_{l-1}\),与 \(W^{l}\) 一致;形状不一致就无法做 \(W\leftarrow W-\eta G\)。
Q2. 某神经元输出 \(a\) 同时喂给下一层 3 个神经元,\(\partial C/\partial a\) 怎么写?
参考答案
把三条路径相加:\(\partial C/\partial a=\sum_{k=1}^{3}w_k\,\partial C/\partial z_k\),\(w_k\) 是 \(a\) 到第 \(k\) 个神经元的权重。这就是多元链式法则,漏掉任何一条都会低估梯度。
Q3. 把输出层从 MSE 换成 softmax + cross-entropy,\(\delta^{L}\) 会怎么变?
参考答案
对 one-hot target,\(\delta^{L}=\mathbf{y}-\mathbf{t}\),不再乘 \(\sigma'\)。原因是 softmax 的雅可比与 cross-entropy 的导数相消;这也是分类任务几乎都用这一对的原因——梯度不会因为输出饱和而消失。
Q4. 训练时发现梯度数值和中心差分对不上,最可能检查哪三处?
参考答案
一、\(\delta\) 的递推方向与转置(\(W^{\top}\delta\) 而非 \(W\delta\));二、激活导数是否用了前向存的 \(z\)、是否漏乘;三、损失是求和还是平均、共享权重是否累加。数值校验时记得用双精度、\(\epsilon\) 取 \(10^{-6}\) 左右,太大截断误差、太小学到舍入噪声。
七、30 秒速记
\(\partial L/\partial w=\sum_n\partial C^n/\partial w\) → 每个权重拆成「前向因子 \(\partial z/\partial w=a\)」×「反向因子 \(\delta=\partial C/\partial z\)」→ \(\delta^{l}=\sigma'(z^{l})\odot(W^{l+1\top}\delta^{l+1})\) → 输出层 \(\delta=(\mathbf{y}-\mathbf{t})\odot\sigma'\)(MSE)或 \(\mathbf{y}-\mathbf{t}\)(softmax+CE)→ \(\partial C/\partial W^{l}=\delta^{l}(a^{l-1})^\top\),形状与本层权重一致 → BP 只算梯度,更新是优化器的事。
八、和其他 P 的关系
- P1 给过 sigmoid 的局部导数 \(\sigma'=a(1-a)\),本文把它放进整条链;P1 的 \(c\,a(1-a)\) 就是本文 \(\delta\) 的单神经元版本。
- P2 讲义第 42 页写下的 \(\mathbf{g}=\nabla L(\boldsymbol{\theta})\) 是「结果」,本文是「怎么算」。P2 的 batch 概念在这里变成 \(\Delta^{\top}A\) 的批量矩阵乘。
- P11 第 27 页只有一句「BP 是高效算 \(\partial L/\partial w\) 的方法」,本文把那句话展开成前向/反向两趟。
- P13 / P14 用的梯度下降都建立在本文之上:P13 的线性回归只有两个参数,可以直接写解析梯度;P14 的高斯生成模型甚至不需要 BP,用最大似然闭式解。参数一多、模型一深,就必须回到本文这套递推。