Skip to content

第 14 章 亲手走完 BatchNorm 与 embedding 的反向传播 ​

交叉熵会在一行的类别之间分配梯度,BatchNorm 则在一列的样本之间建立联系。后者最容易写错的地方,是同一个输入同时参与“自己的中心化残差”和“整批的均值、方差”。只对分子求导,会漏掉另外两条路径。

本章按课程第 5 讲的四项练习收尾:先逐节点反传,再化简 BN,然后走回输入线性层和字符表,最后用手写梯度真正更新参数。我们采用课程的 B−1 方差,随后给出可兼容 B 方差的统一式。固定 notebook

14.1 先给每个中间量一个名字 ​

令 BN 输入为 A∈RB×H,均值 μ∈R1×H,校正数为 c,课程取 c=1。定义

μ=1B∑bAb,D=A−μ,S=D2,v=1B−c∑bSb,r=(v+ε)−1/2,Q=D⊙r,Y=γ⊙Q+β.

D,S,Q,Y 为 B×H;v,r,γ,β 在代码中为 1×H。这里 D 代表中心化残差,不再是前面用于 embedding 宽度的那个局部符号;本章后面谈 embedding 时会重新说明它的宽度。上游已经给出 G=dY,目标是得到 dA,dγ,dβ。

先过最后的仿射变换:

dγ=∑bQb⊙Gb,dβ=∑bGb,U=dQ=γ⊙G.

对 gamma、beta 求和,是因为它们被全批共享。到 Q=D⊙r,残差的直接梯度为 dDdirect=r⊙U,而逆标准差的梯度是 dr=∑bDb⊙Ub。

14.2 方差与均值两条路都要走回来 ​

对 r=(v+ε)−1/2,有

dv=−12(v+ε)−3/2⊙dr.

eps 是常量,不求它的梯度;但它留在导数里,不能为了把公式写漂亮就删除。接着从方差回到平方残差,dSb=dv/(B−c),再通过平方得到 dDvar=2D⊙dS。两条通往 D 的路线在这里相加:

dD=r⊙U+2DB−c⊙dv.

最后 D=A−μ:直接给 A 一份 dD,均值则收到 dμ=−∑bdDb。因为 μ=∑bAb/B,每个输入又收到 dμ/B:

dA=dD+1Bdμ=dD−1B∑bdDb.

这一步把整列梯度的均值减掉。因此每列 dA 的和理论上为零。BN 对输入整列增加同一个偏移不敏感,梯度沿这个整体平移方向也就为零。你可以用这条性质查错,却不能仅凭它证明整个导数正确:很多错误式子也可能被人为减去均值。

下面列出配套变量名,便于逐项核对:

数学量代码缓存名反传的关键操作
Ypre来自 tanh 的局部导数
Qraw乘 gamma
rinv对批次求和
vvar乘 −0.5 与负三次方根
Ssquare将方差梯度广播,除以 B−c
Ddiff直接路径加平方路径
μmu负的批次和
Aa残差直接项加均值路径

14.3 官方练习 3:把 BN 反传化简成一式 ​

先将 dv 代回 dD。由于 r3=(v+ε)−3/2,

dD=r⊙U−D⊙r3B−c⊙∑bDb⊙Ub.

又因为 Q=D⊙r,可以抽出一项 r:

dD=r⊙[U−QB−c⊙∑bQb⊙Ub].

中心化保证 ∑bDb=0,因此 ∑bQb=0。对上式按批次求和,第二项随之消失,得到 ∑bdDb=r⊙∑bUb。再代入减均值那一步:

dA=r⊙[U−meanb(U)−QB−c⊙∑bQb⊙Ub].

这是配套代码采用的统一式,eps 已经包含在 r,Q 内。课程取 c=1,再用 U=γG 展开,就得到

dA=γrB⊙[BG−∑bGb−BB−1Q⊙∑bQb⊙Gb].

如果前向改为除以 B,反向分母也要改为 B。少写 B/(B−1) 不是一个可忽略的排版差异,而是在求另一个函数的导数。配套反例故意犯这个错误,最大梯度误差约为 0.001149,远大于正确实现的舍入误差。

用一列、三个样本核对直觉。输入 A=[1,3,5]T,取 c=1,先为手算设 eps 为零、gamma 为 1,故 μ=3,v=4,r=1/2,Q=[−1,0,1]T。假设上游 G=[1,2,4]T,均值为 7/3,且 ∑QG=3。代入得到

dA=12([1,2,4]−73[1,1,1]−32[−1,0,1])=[1/12,−1/6,1/12].

其中中间写法只是把均值广播成三项;直接按元素算会更清楚:第一项 12(1−7/3+3/2)=1/12,第二项 12(2−7/3)=−1/6,第三项 12(4−7/3−3/2)=1/12。三项和为零。实际代码保留 eps,数值会有很小差别。

这里还有一个细节:eps 为零时,沿输入整体缩放方向的导数也会完全消去;eps 非零时,缩放不变性不再严格成立。因此不要给测试强加“∑dA⊙Q 必须精确为零”的额外条件。

14.4 第一层与 reshape:梯度按原来的路返回 ​

第一层为 A=UW1+b1,其中 U∈RB×TDe,T 是上下文长度,De 是 embedding 宽度,W1∈RTDe×H。沿用上一章的矩阵规则:

dU=dAW1T,dW1=UTdA,db1=∑bdAb.

这里 db1 理论上为零,与第 11 章的偏置消去一致。测试故意保留一个非零随机偏置,是为了验证这条路径,而不是建议在 BN 前保留冗余参数。

前向把查表输出 E∈RB×T×De reshape 成 U。reshape 没有把不同元素相加,也没有改变元素对应关系,因此反向只需把 dU reshape 回 E 的形状。如果前向还有转置或切片,就要还原相应的索引关系,不能把所有形状操作都混成一个随意的 view。

14.5 embedding 最容易漏掉的是重复索引 ​

embedding 表 C∈RV×De,前向为 Ebt,:=CXbt,:。某个字符可能在同一上下文出现多次,也可能在不同样本出现。它们使用的是同一行参数,所有梯度必须相加:

dCk,:=∑(b,t):Xbt=kdEbt,:.

例如一维 embedding 中,索引是 [1,1,2],三个使用位置的梯度分别为 [2,3,5],则字符 1 应收到 5,字符 2 收到 5。用赋值 dC[ix] = ... 会让后一次覆盖前一次;某些带重复索引的高级索引写法 dC[ix] += ... 也不能被想当然地当成可靠的散射累加。

最清楚的教学版本是两层循环逐项 +=。张量版本则使用明确具有累加语义的 index_add_:

python
dC = torch.zeros_like(C)
dC.index_add_(0, X.reshape(-1), dE.reshape(-1, C.shape[1]))

本书实验的输入包含多次字符 1;若故意用覆盖式赋值,最大误差约为 0.04305。整数索引 X 没有这里意义下的普通梯度,学习的是被索引的连续参数表 C。

14.6 从“看起来对”到确实经过检查 ​

在书根目录运行:

text
.venv/bin/python examples/part2/manual_backprop.py

程序固定 seed 2026、单 CPU 线程、float64,批大小为 4、词表大小为 5、上下文为 3、embedding 宽度为 2、隐藏宽度为 4。所有 7 组参数和 19 个中间张量分别检查形状与梯度,共 26 项。这个小模型足够包含全部路径,运行不依赖下载姓名数据。

检查本书实测结果能说明什么
26 项与 autograd 对照的最大绝对误差1.39×10−17本次输入下逐节点一致
整个标量 loss 的 float64 gradcheck通过中心差分也支持导数正确
重复字符坐标 C1,0 的解析梯度−0.0314336464423累积了多次使用
同坐标中心差分−0.0314336464635与解析值相差约 2.12×10−11
反例:BN 用错分母最大误差约 0.001149测试能拒绝真实错误
反例:embedding 梯度覆盖最大误差约 0.04305重复索引确实被测到

有限差分计算 [L(θ+ϵ)−L(θ−ϵ)]/(2ϵ),本次扰动量为 10−6,不要与 BN 的 eps 混淆。扰动过大带来截断误差,过小则受浮点相消影响。整个函数若有 max 并列等不可微点,数值检验也需要避开或专门讨论。

14.7 官方练习 4:不用 autograd 训练一次 ​

完成验证后,复制一份没有 requires_grad 的参数,在 torch.no_grad() 中前向、调用手写反传、执行 p -= lr * dp。配套代码用同一个四样本批次做 200 次更新,学习率为 0.1,真实检查值如下:

已完成更新数平均损失
01.657875
500.446956
1000.180666
2000.071273

这一步证明手写梯度确实能驱动参数更新,只证明四样本的拟合,不证明姓名模型的泛化。原课程对姓名数据做更长训练,并用训练集重新校准单层 BN 后评估、采样。本书不把 notebook 中记录的 train≈2.0719,val≈2.1162 说成本机复现;这些是上游的历史记录。

要把手写梯度训练扩到前一部分的数据,保持按完整单词划分 train/dev/test,训练循环抽样本批次;训练结束仅用训练数据计算固定 BN 统计,再用相同前向计算各划分损失。采样时从全零起始上下文出发,按 softmax 概率抽一个字符,左移上下文直到终止符。配套文件的 names_demo() 也完整执行这条流程:12 个内置词按 8/2/2 分组,全部参数关闭自动求导,500 次手写梯度更新后,仅用训练集校准。实测 train/dev/test 损失分别为 0.491513、2.405374、2.466960;固定 seed 2036 的五个样本为 tina.、tanim.、mtna.、tania.、mtina.。程序还断言单样本与批量推理结果一致。这是完整流程的缩小演示,数据太少且含训练未见字符,不用于评价课程模型性能。

练习、提示与答案 ​

  1. 官方练习 1,后半。 按 14.1–14.2 的原子图实现 BN、线性层和 embedding 反传,逐项输出误差。
  2. 官方练习 3。 从原子式推导紧凑式,并分别运行 correction=0 和 1。提示:化简只用到中心化后每列和为零,不需要假设输入服从正态。
  3. 官方练习 4。 删除训练循环中所有自动求导调用,用自己的梯度拟合四样本,再接回独立验证流程。
  4. 补充。 让某字符从未在批次出现,它的 embedding 梯度应是什么?

前三题完整答案在本章推导和 manual_backprop.py。第四题是全零,直接检查 dC 的相应行;“这个字符是输出标签”不意味着输入 embedding 一定使用过它,除非模型明确共享输入表与输出权重,本课没有这样做。

来源与范围 ​

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