Skip to content

第 13 章 把反向传播写成张量运算 ​

前面的小型自动求导引擎以标量为单位。现在一行矩阵乘法包含成千上万次乘加,但规则没有改变:沿每条使用路径应用链式法则,再把同一个变量收到的贡献相加。难点转移到了形状、广播与归约。

课程第 5 讲让读者手写整个两层 MLP 的反传,而不是再实现一个通用 autograd 引擎。本章走完损失、输出线性层和 tanh;下一章继续 BatchNorm、输入线性层和 embedding。配套代码 manual_backprop.py 保留每一个中间张量,能逐项对照 PyTorch,而不是只看最终 loss 能否下降。官方练习 notebook

13.1 梯度的形状为什么应该跟原变量一样 ​

记标量损失为 L,张量 X 的梯度为 dX=∂L/∂X。dX 的第 (i,j) 项回答:只让 Xij 增加一点,损失会怎样变化?因此每个原元素对应一个梯度元素,形状应相同。

以 Y=X+b 为例,X∈RB×H,b∈RH。前向把同一个 bj 用在所有行,反向自然要把这些行的贡献加起来:

dXbj=dYbj,dbj=∑b=1BdYbj.

若 dY=[1234],则 db=[4,6]。这里没有除以批大小,因为前向是广播加法,没有平均。若损失之前已经求过平均,那个 1/B 会随上游梯度传下来,不应再次凭感觉除一次。

反过来,s=X.sum(0,keepdim=True) 把多行加成一行,反向就将 ds 广播给每个输入。可以记住这对对应关系:前向广播,反向求和;前向求和,反向广播。 它们是同一元素被重复使用的结果,不是库的特殊技巧。

对于一般的前向广播,应把新增加的前置轴和原来长度为 1 而后来被扩展的轴都求和,最后恢复原形状。例如 a∈R1×H 与 X∈RB×T×H 相乘,da 要对批次与时间求和,最后仍为 1×H。只检查数值 allclose 不够,还要先断言形状一致,以免比较本身又发生广播。

13.2 从平均负对数概率开始往回走 ​

设一个批次有 B 个样本,词表大小为 V,模型给出 logits Z∈RB×V,标签 y∈{0,…,V−1}B。我们先把交叉熵拆成小步:

mb=maxjZbj,Nbj=Zbj−mb,Qbj=eNbj,sb=∑jQbj,rb=sb−1,Pbj=Qbjrb,Rbj=log⁡Pbj,L=−1B∑bRb,yb.

Z,N,Q,P,R 都是 B×V;m,s,r 保留为 B×1。从每行挑一个正确标签,再对批次求平均,最终得到标量。

先把 dR 全置零,在每个目标位置写入 −1/B。例如 B=2,V=3,y=[2,0] 时,

dR=[00−1/2−1/200].

然后逐元素反传 log:dP=dR/P。从 P=Qr 往回,通向 Q 的贡献是 r⊙dP;由于每行的 rb 被这一行所有类别使用,drb=∑jQbjdPbj。继续过倒数,得到 ds=−s−2⊙dr。

此时不能直接结束 dQ。Q 既直接进入 P=Qr,又经过行和 s 进入倒数路径,因此

dQbj=rbdPbj+dsb.

第二项广播回各类别,正是“前向求和,反向广播”。少掉它,通常仍能得到非零梯度,却不再是交叉熵的正确导数。配套代码中这一步明确使用 +=。图中分叉又汇合的两条路线提醒我们为什么需要相加。

交叉熵与 BatchNorm 中的共享节点:前向箭头,反向沿所有路径累加
可切换原始尺寸,滚动查看细节

查看大图

尺寸读取中

适合窗口:完整显示图片,不放大超过原始尺寸。 按 Esc 关闭。

指数的导数等于自己,故 dN=Q⊙dQ。到这里,梯度已经回到减最大值之后的 logits。

13.3 减最大值的那条路径不能凭感觉删掉 ​

从 Nbj=Zbj−mb,直接路径贡献 dZ=dN,而 dmb=−∑jdNbj。若每行最大值唯一,max 的反传把 dmb 加到达到最大值的位置,其余位置贡献零。

python
dlogits = dnorm.clone()
dmax = -dnorm.sum(1, keepdim=True)
dlogits.scatter_add_(1, logits.argmax(1, keepdim=True), dmax)

对 softmax 交叉熵,精确算术下行梯度之和为零,所以这条最大值路径最终抵消。可是本练习要求匹配逐个中间节点的图,不应先跳过它,再把“不太一样”归咎于 PyTorch。最大值并列时局部不可微,库会按具体算子的规则选择梯度路径;我们用随机、非退化的 logits 做有限差分检查,避开这个拐点。

减去同一行的最大值不会改变 softmax,因为分子分母同时乘以 e−mb。它避免指数上溢:若 logits 为 [1000,1001,999],直接求 exp 很容易溢出,减最大值后变为 [−1,0,−2],指数最大为 1。

但把 exp、除法和 log 分开,仍可能遇到很小概率下溢成零,随后 log(0) 出现无穷。生产训练应优先使用融合的 F.cross_entropy 或稳定的 log-softmax,而不是为了模仿教学图坚持这套展开。手写展开适合学习和小范围核对;本书梯度实验刻意使用温和随机 logits。 配套 numerical_ce_tests() 还实际运行了正反例:[1000,1001,999] 直接 exp 归一化产生非有限值,稳定计算的概率为 [0.244728,0.665241,0.090031],目标为中间类别时融合损失为 0.407606;[0,-1000] 的目标若为第二类,先 softmax 后 log 得到无穷,融合交叉熵仍得到有限的 1000。

13.4 官方练习 2:把交叉熵压缩成三行 ​

对一个样本,softmax 交叉熵也可写成

ℓ=−Zy+log⁡∑keZk.

对第 j 个 logit 求导:第一项给出 −1[j=y];第二项先求 log 的导数,再乘内部指数和的导数,得到

∂ℓ∂Zj=−1[j=y]+eZj∑keZk=Pj−1[j=y].

批平均再乘 1/B:

python
dlogits = logits.softmax(1)
dlogits[torch.arange(B), targets] -= 1
dlogits /= B

举一个不需要计算指数的例子。若某行概率为 [0.2,0.3,0.5],正确类别为第 2 类(零起编号 1),批大小为 2,梯度是 [0.1,−0.35,0.25]。SGD 减去梯度后,会增加正确类 logit,降低其余 logit;这一行梯度和为零。所有 logits 同时加常数不改变概率,与这个零和性质一致。

该简式假设普通硬标签、无类别权重、无 label smoothing、无忽略标签,并且 reduction 为 mean。换成加权损失、平滑标签或 sum,系数会改变。不要把这三行不加修改地移植到所有分类损失。

13.5 矩阵乘法反传,先按一个元素推 ​

输出层为 Z=hW2+b2,其中 h∈RB×H,W2∈RH×V。单个输出为 Zbj=∑khbkW2,kj+b2,j。

固定 hbk,它影响该行所有类别,所以

dhbk=∑jdZbjW2,kj,dh=dZW2T.

固定 W2,kj,它被所有样本使用,所以

dW2,kj=∑bhbkdZbj,dW2=hTdZ.

偏置照旧求批次和,db2=∑bdZb。核对形状:(B,V)(V,H)=(B,H);(H,B)(B,V)=(H,V)。

用一个两输入、两输出的样本验证:h=[2,−1],W2=[1342],上游梯度 dZ=[0.5,−0.25]。于是 dh=[−0.25,1.5],dW2=[1−0.5−0.50.25],db2=[0.5,−0.25]。能用标量乘加算出这一行,矩阵公式就不再只是转置口诀。

再穿过 h=tanh⁡a,得到 da=(1−h2)⊙dh。这一步逐元素发生,不改变形状,不做跨批求和。若一个位置 h=0.6,dh=2,传回去的是 (1−0.36)×2=1.28。

13.6 怎样判断自己的反传真的正确 ​

官方练习 1 要求逐中间张量核对,练习 2 才允许使用简式。配套程序先运行自动求导作参照,调用 retain_grad() 保存中间梯度,再运行 backward() 这个手写函数。注意名称碰巧相同:它是本书的普通 Python 函数,不调用 loss.backward()。

核对先看形状,再看最大绝对误差、allclose。精确相等可以帮助定位计算顺序,但不能作为数学正确性的唯一条件。s**-1 与 1/s 在浮点实现中可能走不同路径,重排求和也会改变末尾位;课程正好用这个例子解释位级一致和近似一致的区别。

为了避免错误被特殊初值遮住,测试中的偏置和 BN 参数带有小随机扰动,不都设为零。批次故意重复同一字符,让下一章 embedding 的累加错误也能暴露。torch.autograd.gradcheck 用 float64 中心差分检查整个标量函数,作为另一道证据。损失能下降、生成文本看起来合理,都不足以替代这些检查。

练习与答案 ​

  1. 官方练习 1,前半。 按原子步骤实现从 logprobs 到 hpreact 的所有梯度,不能先使用 softmax 简式。提示:把前向变量画成图,标出被用两次的 counts。
  2. 官方练习 2。 不参考代码,用索引推导交叉熵简式,并解释每行梯度为什么为零和。
  3. 补充反例。 在 dcounts += ... 处删掉加法,哪一项测试最先失败?

前两题的完整推导就在本章各节,逐行答案在 manual_backprop.py:backward。第三题中 dprobs、倒数路径的梯度仍可能正确,但合并后的 dcounts 就首先不匹配,此后的 logits、线性层参数等也会受影响。正是因为从输出向输入逐项比较,才不必在整个网络中盲猜。

来源与范围 ​

  • 第 5 讲官方视频与四项练习;固定源码,单元 7–18。
  • 官方 Colab 是留空的练习版本,固定仓库 notebook 带参考答案;本章同时核对两者,没有把 Colab 的空答案当作未覆盖内容。
  • 全部推导、图与数值例子为本书原创讲解。MIT 源码改编声明见配套 NOTICE;未获得逐字字幕,不声明逐秒覆盖视频措辞。

保留各书来源与许可 · 示例运行条件以正文为准