主题
第 11 章 BatchNorm:训练时看一批,推理时用固定统计量
初始化只安排了出发时的尺度。训练更新参数以后,中间信号仍会改变。Batch Normalization,简称 BatchNorm 或 BN,在网络中增加一项操作:按特征收集一组观测,减去它们的均值,再除以尺度,最后允许网络学习一次缩放和平移。
它不是把整个矩阵统一压成一个标准正态分布,也不会让数据自动变成正态分布。本章要弄清三件事:统计哪些数,谁通过梯度学习,以及一个样本单独推理时该用谁的统计量。原论文提供训练与推理算法;课程则用手写层把它们拆开。BatchNorm 原论文
11.1 一列一列地算,而不是一行一行地算
设激活前矩阵
这里 correction=0 与分母为 correction=1。
拿三行两列来算:
均值是
A.mean(0, keepdim=True) 的结果形状是 keepdim=True 把被消去的维度留为 1,使这层意图直接写在形状里。若误写 mean(1),你是在每个样本内部混合不同特征,算法已经变了。
11.2 Bessel 校正与原生 PyTorch 的区别
均值是用同一批数据估计的,中心化残差有一个约束:它们之和为零。估计总体方差时,除以
课程手写实现与 torch.nn.BatchNorm1d 不完全一致。 第 4、5、6 讲 notebook 使用 std() 或 var() 的默认样本校正;第 5 讲更明确写出 BatchNorm(correction=1) 保留这个教学约定,所以后面手推梯度会出现 correction=0 的批方差,写入 running_var 的批方差估计则使用 correction=1。
配套实验直接用上面的三行矩阵检查已安装的原生层:设 momentum=1, affine=False,前向输出与除以 running_var 却等于
11.3 为什么还要 gamma 和 beta
如果 BN 永远强制每列均值为零、尺度固定,网络就失去了一部分表达选择。后接 tanh 时,缩放决定多少输入进入饱和区,平移决定工作区间。可训练的
BN 前面的线性偏置通常可以删掉。假设
同一个偏置被减掉了。BN 自己已有 beta 可负责输出平移。这个消去关系也解释了第 14 章里 BN 前偏置的梯度为何理论上为零;浮点计算可能留下很小的残差。它不表示任意位置的偏置都可以删除:最后一个没有后接 BN 的分类层偏置仍然有作用。
11.4 训练会让同批样本彼此影响
把批次中的一个数从 1 改成 100,会同时改变列均值与列方差,因此同列其他样本的归一化结果也会变化。BN 不是逐样本独立的函数。这既带来一定的随机扰动,也制造了使用限制:小批次、彼此高度相关的样本、训练与部署数据分布差异,都可能使统计量不可靠。
课程 notebook 的附加 widget 演示了这个现象:只拖动五个输入中的一个,五个输出都会重新移动。这部分在 notebook 中标为视频之外的 bonus。本书用前面的三点例子即可检查:把第一列的 1 改成 9,新的列为
另一个直接后果是:用校正数 1 训练时,每个通道必须至少有两个观测。只有一个观测,分母为零。即便使用校正数 0,单观测的中心化结果仍为零,网络也无法靠它恢复有用的相对差异。配套层默认使用 correction=1,会明确拒绝每通道仅一个观测的训练输入。
11.5 推理不该随着同行请求改变
假设线上一次请求只生成一个名字。若还使用训练分支,它既缺少可靠批统计量,也会让预测受一起入批的其他请求影响。推理时应冻结统计量,使用训练期间积累的均值与方差:
常见积累方式是指数移动平均。设本批均值为
方差同理。若旧均值为 10、新批均值为 20、momentum 指新统计量占多少,与 SGD 优化器中动量的参数含义不同。
running_mean 和 running_var 是缓冲状态,不放进 parameters(),不由损失梯度更新。更新时放进 torch.no_grad(),避免为移动平均不断连接历史计算图。eval() 切换 BN 使用的分支;no_grad() 关闭自动求导记录。两者做的事不同,推理一般需要同时使用。
第 4 讲早期代码对标准差直接做移动平均;后来的手写层对方差做移动平均并在使用时开方。两种量不能互换:
另一种教学做法是在训练结束后,固定权重,用训练数据重新估计统计量。第 5 讲对单隐藏层这样校准。不能用验证集或测试集来“帮忙校准”,否则评估已经使用了待评估数据。多层网络的校准还涉及前层状态如何影响后层分布,不能简单把单层做法不加说明地复制到所有层。
11.6 官方 E02:把 BN 折进线性层
推理统计量固定后,BN 是逐通道仿射变换。设上一层
展开后得到
其中
例如单输出层
配套 fold() 实现完整公式,包括 eps 和“前层无偏置”的情况。batchnorm_tests() 先训练一个包含三层 Linear、两层 BN 的小网络,再删除两个 BN、把参数折进相邻 Linear。在九个新输入上的最大输出误差为
11.7 练习、提示与答案
- 官方 E02。 先不看
fold(),实现三层 MLP 的推理折叠。验证随机输入、单样本输入、批次输入,并检查删除 BN 前后参数数目如何变化。 - 补充。 为什么
with torch.no_grad(): model(x)仍可能修改running_mean? - 补充。 一列观测数为 4,用
标准化且忽略 eps 后,这列以分母 计算的方差是多少?
第一题答案就是上一节的列缩放公式;必须先 eval(),测试时比较 logits 而非只比较 argmax,因为很大的数值误差也可能保留同一个最高类别。BN 的 gamma、beta 被吸收到新权重与偏置,不再单列为独立参数。第二题中 no_grad() 不会改变 training=True,训练分支仍会执行缓冲更新。第三题是
完整实现见 examples/part2/layers.py,验证见 examples/part2/experiments.py 的 batchnorm_tests(),实测记录见 qa/part2/experiments.json。
来源与范围
- 课程第 4 讲与固定 notebook:手写归一化、运行统计量、校准、推理、bonus widget 与 E02。
- BatchNorm 论文:原始算法与推理变换;本章公式逐项展开及数值例子为原创教学。
- PyTorch v2.6.0 源码:训练/推理状态与方差约定。课程对 ResNet-50 的代码浏览由第 12 章补足,不在这里冒充复现整网训练。