深度学习基础:反向传播推导
反向传播的公式看起来复杂,但拆开只有一个东西:链式法则。这里用一个最小的三层网络,把每一步都完整算一遍。
前五节是 MSE + Sigmoid 的完整推导,第六节再把实际中最常用的四种组合(Softmax + 交叉熵、Sigmoid + BCE、ReLU + 线性输出、Batch 形式)各推一遍。
一、网络结构
- 输入维度 2,隐藏层 2 个神经元(Sigmoid),输出层 1 个神经元(Sigmoid)
- 损失函数用均方误差(MSE)
符号定义:
- 输入
- 隐藏层权重 ,偏置
- 输出层权重 ,偏置
- 激活函数 ,其导数
- 真实标签
前向传播:
损失(前面乘 是为了求导时把平方的 2 消掉):
要算的是 、、、。
二、核心中间量:误差项 δ
按定义 ,即损失对本层「加权和」的梯度。
之所以拿 而不是 当锚点,是因为参数 、 直接作用在 上:
一旦 已知,这一层的两个参数梯度都是它的直接产物,形状也自动对上。剩下的工作就只是把 一层层往前递推。
三、逐层反向推导
3.1 输出层
先对 求导:
再乘 Sigmoid 导数,得到输出层误差项:
于是输出层参数梯度:
直觉:权重梯度 = 本层误差项 × 本层输入(即上一层的激活值)。某个输入越大,对应的权重就要为这份误差负更多责任。
3.2 隐藏层
误差从输出层回传到隐藏层。因为 ,对 求导时权重矩阵要转置:
再逐元素乘上 Sigmoid 导数,得到隐藏层误差项:
其中 表示逐元素乘。由于 ,可以完全用前向的激活值表示,不必把 留着:
隐藏层参数梯度:
注意这里用的是 而不是 ——输入层没有激活函数,。
四、一般化到第 l 层
设第 层有:
反向传播就是两个公式:
误差项递推:
参数梯度:
输出层的起点(MSE + Sigmoid):
换损失函数和激活函数,改的只有两处:输出层 的起点、以及各层 的具体形式。递推结构一个字都不用动。
维度校验
手工推导最容易在转置上出错,用形状表核对一遍:
| 量 | 形状 | 说明 |
|---|---|---|
| 的形状由它决定 | ||
| 与隐藏层宽度一致 | ||
| (标量) | 单输出 | |
| ✓ | ||
| ✓ |
规则只有一条:梯度的形状必须与参数本身完全一致。写成 (而不是 )在形状上就会对不上,这一条能挡掉大部分笔误。
五、一个具体数值例子
取 ,,初始权重:
5.1 前向
5.2 反向
回传到隐藏层:
5.3 参数更新
学习率 ,按 更新:
这样一次完整的「前向—反向—更新」就手工算完了。
5.4 用 PyTorch 验算
手算结果对不对,让 autograd 说一遍最省事:
import torch
x = torch.tensor([1.0, 0.5])
y = torch.tensor(1.0)
W1 = torch.tensor([[0.1, 0.2],
[0.3, 0.4]], requires_grad=True)
b1 = torch.tensor([0.1, 0.2], requires_grad=True)
W2 = torch.tensor([[0.5, 0.6]], requires_grad=True)
b2 = torch.tensor([0.1], requires_grad=True)
a1 = torch.sigmoid(W1 @ x + b1)
y_hat = torch.sigmoid(W2 @ a1 + b2)
loss = 0.5 * (y_hat - y) ** 2
loss.backward()
print(y_hat.item()) # 0.6874(手算 0.6876,差在中间步骤的四舍五入)
print(W2.grad) # tensor([[-0.0385, -0.0448]])
print(b2.grad) # tensor([-0.0671])
print(W1.grad) # 与手算矩阵一致
两边的数字对齐,说明推导里的每个转置都放对了位置。
六、常见组合的推导
MSE + Sigmoid 只是众多搭配之一,而且在实际分类任务里几乎没人用。下面把四种最常用的组合各推一遍,重点看输出层——因为第四章已经说明,递推结构对任何组合都成立,唯一的变量是 的起点和各层导数。
6.1 案例一:Softmax + 交叉熵(多分类,最常用)
设定。 输出层有 个类别分数 ,Softmax 把它们变成概率分布,交叉熵衡量它与 one-hot 标签的差距:
要算的仍然是 。这里不能像 Sigmoid 那样直接乘导数——Softmax 的每个输出都依赖所有的 ,必须走完整的链式法则求和:
两个偏导分别算:
Softmax 对自身的偏导分两种情况( 时分子里有 , 时没有):
代回去:
最后一步用了 one-hot 标签的性质 。于是:
对比第五章 MSE + Sigmoid 的 ,多出来的 在这里被整个约掉了。这不是巧合:交叉熵的定义里带着 ,对 Softmax 求导时分母的 正好把这一项消干净。框架里把 Softmax 和交叉熵打包成一个算子(CrossEntropyLoss),就是为了走这条化简路径,同时用 LogSumExp 技巧避免 上溢和 。
数值例。 取 ,,标签 :
三个分量之和为 0——这是 Softmax 的性质决定的(概率和为 1,梯度必然正交于全 1 方向),也可以当成一个自检。
为什么不用 MSE。 若把损失换成 MSE, 里的 会保留下来。设想 而正确类别是第 2 个:预测错得极其离谱,但那两个极端分量上的 已经接近 0,最该修正的时刻梯度反而最小。交叉熵没有这个因子, 直接等于 ,误差多大推力就多大。
6.2 案例二:Sigmoid + 二分类交叉熵(BCE)
设定。 输出单个概率 ,损失:
先对 求导:
分母正好是 Sigmoid 的导数 ,两者相乘直接约掉:
和案例一完全同形。
数值对比。 仍用第五章的 、:
| 损失 | 相对大小 | |
|---|---|---|
| MSE | 1× | |
| BCE | 4.7× |
越接近错误端点,差距越夸张。设 、:
| 损失 | |
|---|---|
| MSE | |
| BCE |
差了 21 倍。MSE 在这个点上几乎推不动参数,而 BCE 的梯度只由误差本身决定,不受激活函数饱和的影响——这就是二分类默认用 BCE 的原因。
6.3 案例三:ReLU 隐藏层 + 线性输出(回归)
设定。 隐藏层换成 ReLU,输出层不加激活(回归任务不该把输出压进 ),损失用 MSE:
输出层。 没有激活函数,就少一层链式:
(若坚持用 MSE + Sigmoid,则要乘 ,回到第五章的形式。)
隐藏层。 递推公式不变,只把导数换成 0/1 门:
数值例。 权重沿用第五章,但把隐藏层偏置改成 :
反向:
更新后():
看第一行:全是 0。 这个神经元的 落在负半轴,梯度被门挡掉了,本次更新对它毫无影响。如果它长期收不到正输入,就会一直保持「学不到 → 输出恒 0 → 更学不到」的状态,这就是 dying ReLU。Leaky ReLU、PReLU 给负半轴留一个小斜率,正是为了让这种神经元还有机会爬回来。
另外注意与第五章的对比:同样的 、,只把 换掉,ReLU 版本的 量级是 ,Sigmoid 版本是 ——ReLU 的导数非 0 即 1,不会像 Sigmoid 那样每层再乘一次小于 1 的因子。
6.4 案例四:Batch 形式(一次算 B 个样本)
前面都是单样本。实际训练一次喂一个批次,把所有公式的向量换成矩阵:
这里 的形状是 ,靠广播加到每一列上。
损失取批内平均,注意那个 :
这个系数会平移到 上,之后所有梯度都带着它:
两处容易出错:
- 偏置要沿 batch 维求和,即乘以全 1 向量 。单样本时 ,这步退化成「梯度就是 」——第五章的写法在 batch 下会得到一个 的错误形状。
- 只出现一次。放在 的起点最省事,后面全部继承;如果每层都乘一遍,梯度会被缩小 倍。
为什么框架里看不到 for 循环。 因为上面每个式子都是矩阵乘: 个样本的梯度被合并成了少数几次 GEMM, 越大 GPU 越吃满。这也解释了为什么 batch size 会影响训练——它同时改变梯度噪声(平均得更平滑)和单步的计算效率。
用 PyTorch 核对一遍 batch 的形状和 的位置:
import torch
B, n_in, n_hid = 4, 2, 2
X = torch.randn(n_in, B)
Y = torch.rand(1, B)
W1 = torch.randn(n_hid, n_in, requires_grad=True)
b1 = torch.randn(n_hid, 1, requires_grad=True)
W2 = torch.randn(1, n_hid, requires_grad=True)
b2 = torch.randn(1, 1, requires_grad=True)
A1 = torch.sigmoid(W1 @ X + b1) # [2, 4]
Y_hat = torch.sigmoid(W2 @ A1 + b2) # [1, 4]
loss = (0.5 * (Y_hat - Y) ** 2).mean() # mean() 就是那个 1/B
loss.backward()
print(W2.grad.shape, W1.grad.shape) # torch.Size([1, 2]) torch.Size([2, 2])
6.5 四种组合横向对比
| 任务 | 输出层激活 | 损失 | 备注 | |
|---|---|---|---|---|
| 二分类 | Sigmoid | BCE | 默认选择 | |
| 多分类 | Softmax | 交叉熵 | (向量) | 框架打包成一个算子 |
| 回归 | 无 | MSE | 不要加 Sigmoid | |
| 二分类 | Sigmoid | MSE | 不推荐,饱和时梯度消失 |
表格里前三行的 完全一样,这不是巧合:只要损失函数取「与激活函数相配的负对数似然」(统计上叫指数族对应的典则链接),输出层的 就会把激活导数整个约掉。这条规律比单记公式有用——看到一个新组合,先看它是不是「配对」的;配对了就是 ,没配对就一定留着一个衰减因子。
七、要点总结
- 反向传播本质是链式法则的系统化应用,从损失对输出的梯度开始,逐层向前回传。没有任何「神经网络专属」的数学,只是把链式法则按层的结构组织了一遍。
- 误差项 是核心中间量:它等于「后一层误差经权重转置回传」再「逐元素乘本层激活导数」。整个算法可以压缩成一次前向存下所有 ,再一次反向递推。
- 参数梯度形式统一:权重梯度 = 本层误差项 × 本层输入;偏置梯度 = 本层误差项。所有层用同一个模板,这也是深度学习框架能写得这么短的原因。
- 换损失/激活只改两处:输出层 的起点、各层 的形式,递推结构不变。第六章的四个案例逐个验证了这句话——从 Softmax 到 Batch, 的递推公式一次都没改过。
- 梯度形状必须等于参数形状,这是手工推导时最有用的自检手段。
- 数值上要注意 Sigmoid 饱和:第五章的例子中 已经掉到 以下,只回传一层就衰减了一个数量级。层数一多,梯度乘到输入端基本归零——这就是梯度消失。实际中常用 ReLU(6.3)、交叉熵损失(6.1、6.2)、BatchNorm 或残差连接缓解;RNN 沿时间回传时这个问题被放大得最厉害,LSTM 的门控和 Transformer 的残差都是冲着它去的。
附:继续往下走
上面覆盖的都是全连接层。再往深处走,值得单独推一遍的有三处,难点各不相同:
- BPTT(沿时间反向传播): 除了经权重回传,还要沿时间步回传,同一套 的梯度是所有时刻梯度之和——「参数共享 → 梯度累加」的规律在这里第一次出现。参见
深度学习基础结构--RNN结构.md。 - 卷积层:前向是滑动窗口相乘,反向要把 用转置的卷积核(严格说是相关运算)铺回去,权重梯度则是输入与 的互相关。形状校验在这里尤其重要。
- LayerNorm / Softmax 的反向:这两处都是「一个输出依赖整条向量的所有元素」,链式法则要像 6.1 那样求和,不能像逐元素激活那样直接乘导数。Transformer 的反向传播里最容易算错的就是 LayerNorm。