bobofamilyNIU CHUN
16/36

输入差一千倍,输出都是 1.000:BatchNorm 拿什么换的

2026-09-0812 分钟—阅读

第十四篇的导读和正文末尾都写着「BatchNorm 是下一段」,第十五篇结尾把它的做法都剧透完了 —— 「每层输出直接减均值除标准差,不管前面发生了什么」。这一篇兑现。

一句话:初始化只管第 0 步,BatchNorm 管每一步。

而它是整个网络里唯一让样本互相影响的操作 —— 这既是它的威力,也是它全部麻烦的根源。 下面两件事各占一半篇幅。


Kaiming 是预测,BatchNorm 是测量

第十四篇最后那节已经说清了 Kaiming 会失效:它假设输入的统计性质已知且稳定, 而第 2 层的输入就是第 1 层的输出,上游一动,下游算好的一切就作废。 第十五篇把这件事追到了根上 —— 公式靠「W 与数据独立」和「均值为 0」两个假设, 训练一开始两个都在退化。

这一篇不重复那两节。只补一个提法上的对比:

做法会不会失效
Kaiming预测 —— 开训前算好 σ_W,指望它一直成立会
BatchNorm测量 —— 每一步现场量一次,直接拉到标准位置不会

它怎么同时满足两个打架的目标

第十四篇留了一句没兑现的话:「不饱和希望 z 小,不衰减希望 z 大。 Kaiming 只能折中 —— 真正同时解决两者的是 BatchNorm。」

Kaiming 只有 σ_W 一个旋钮,而这个旋钮同时控制着两件事,所以只能折中。 BatchNorm 把它拆成了两个独立的旋钮:

  1. 每一步现场把 z 拉到 σ = 1 —— 不饱和这件事被无条件保证了,跟 σ_W 是多少无关
  2. 再让 γ 把确实需要更宽的那些神经元放回去 —— 需要多大方差,网络自己学

折中之所以能取消,是因为「保证尺度」和「决定尺度」不再是同一个参数在管。


两步,各管一件事

先划清一个词。本系列里「归一化」一直指 softmax 那个除以总和(让一组数加起来等于 1)。 BatchNorm 做的是另一件事 —— 减均值、除标准差,让一组数的平均值变 0、标准差变 1。 下面统一叫标准化。BatchNorm 后面简称 BN。

ẑ = (z − μ_batch) / σ_batch
符号形状是什么
z(32, 200)标准化之前的加权和
μ(1, 200)每个神经元在这 32 个样本上的均值
σ(1, 200)同上,标准差

μ 和 σ 是 200 个数,不是一个数。 这一点下一节还要展开。

减均值是平移 —— 每个点挪的距离一样,宽度一点没动。除标准差是收缩 —— 越远的点挪得越多。只有后者能改变 σ,而 σ 才是决定饱和的那个量。

很多人以为「标准化」是一步,其实两步各管一件事,而且只有第二步在解决问题。

⚠️ 笔记里写的是「减均值之后饱和还是 47%」,这条不成立。平移会让点穿过饱和线, 而居中恰好是固定 σ 下饱和比例的最小值(μ=1.8 时 48.2%,居中后 41.4%)。

但落点是对的,而且更值钱:减均值改不了 σ,而 σ 才是决定饱和比例的量 —— 真正把 41.4% 压到 0.8% 的是除以标准差那一步。

减均值另有它自己的理由:tanh 关于原点对称,偏在一侧等于只用了它的一半。

两步都是仿射变换,不改变分布的形状。 双峰的进去还是双峰出来, 只是中心移到 0、宽度变成 1。(z 本来就近似正态,那是第十五篇讲的中心极限定理,跟 BN 无关。)

为什么目标是 μ=0、σ=1

理由说明
匹配激活函数σ=1 时 99.7% 落在 ±3 内,而 tanh 恰好在 ±3 才饱和
层间可传递σ=1 是唯一的不动点 —— 其余任何值多层之后都指数发散
数值安全float32 在 1 附近精度最好

第二条是最要紧的:第十四篇那张「每层比值 1.5 → 十层后 57 倍」的表说的就是它。


沿哪个维度

z.mean(0) # 沿样本方向 → (200,) ✅
z.mean(1) # 沿神经元方向 → (32,) ❌
z.mean() # 全部当一堆 → 标量 ❌
两个维度的含义根本不同:竖着是「同一个神经元的 32 个取值」,横着是「同一个样本在 200 个神经元上的取值」。后者混在一起求平均,等于把身高和体重平均一下。

要对每个神经元、跨 batch 内所有样本求统计量。 200 个神经元学的是完全不同的特征, 把它们平均起来没有任何意义。

这也是 W 和 z 的区别

两个维度的含义能忽略形状吗
W都是「权重」,来自同一个分布✅ 展平算 W.std()
z样本 × 神经元❌ 必须 std(0)

第十五篇最后那条「矩阵的标准差是个偷懒的说法」只对 W 成立 —— 因为 randn 是逐元素独立生成的, 怎么切都一样。z 不是,它两个维度是两回事。

实测:

BN 前 每列 std: [3.712, 6.196, 3.600, 6.854, 5.480] ← 各不相同
BN 后 每列 std: [1.0, 1.0, 1.0, 1.0, 1.0 ] ← 全部拉齐

γ 和 β:可学习的撤销开关

只做标准化会剥夺网络自己决定输出分布的权利,所以后面接一个可学习的还原:

y = γ · ẑ + β
参数形状初始值作用
bngain (γ)(1, 200)全 1缩放
bnbias (β)(1, 200)全 0平移

初始时 y = 1·ẑ + 0 = ẑ,等于什么都没做。 但它们是参数,训练会推动它们。

它们是普通参数,没有魔法

步骤W1bngain
初始化randn * 0.3ones
在 parameters 里✅✅
requires_gradTrueTrue
求导规则xᵀ · dzΣ(dy · ẑ),与权重同构
更新p.data += -lr * p.grad同一行代码

求导公式(沿 32 个样本求和,因为一整批共用同一组 γβ):

∂L/∂γⱼ = Σᵢ (∂L/∂yᵢⱼ) · ẑᵢⱼ ∂L/∂βⱼ = Σᵢ (∂L/∂yᵢⱼ)

γ 对应权重、β 对应偏置,规则完全照搬线性层 —— 就是第十三篇那两条式子。

实测:它们真的被训走了

bngain: 均值 1.216 范围 [0.02, 3.08]
bnbias: 均值 −0.016 范围 [−0.87, +1.59]

初始全是 1 和 0,训完之后 γ 跨了 154 倍。这直接回答了「强行标准化会不会破坏训练」:

神经元γ含义
某个0.02网络主动把它关掉了(输出几乎是常数)
某个3.08它需要比标准形态宽三倍的方差

γ 接近 0 相当于自动剪枝 —— 有些模型压缩方法就是靠找出 γ ≈ 0 的通道来删神经元的。

为什么不算白做

标准化不是「锁死」,是换了个参数化方式:

分布由谁决定
没有 BNW 间接决定 —— W 一动,均值、方差、形状全变
有 BNγβ 直接控制尺度和位置,W 只管形状

下游的层看到的输入分布永远是标准的,不会因为上游微调就被迫重学。

⚠️ 机制至今有争议。 第十四篇点出过 internal covariate shift 这个名字, 那是 2015 年原论文给的解释。2018 年 MIT 一篇论文反驳了它, 认为真正起作用的是 BN 让损失曲面更平滑,跟分布漂移关系不大。

有效性公认,机制没有定论。 这一节讲的是它做了什么,不是它为什么有用 —— 后者还没有答案。


b1 消失了

((z + b) − mean(z + b)) / std(z + b) = (z − mean(z)) / std(z)

b1 加的常数在减均值时被原样减掉,对结果毫无影响,梯度也是 0。 平移的职责已经由 bnbias 接管了。

有 BN 的层不需要 bias。 ResNet 里 Conv → BN → ReLU 的搭配中, 卷积层都写 bias=False —— 就是这个原因,不是什么优化技巧。


样本耦合

参数一个没动、输入一个字没改,变的只有同一批里的另外 31 个样本。这是整条链路上唯一会这样的一步 —— 后面「推理为什么要另存统计量」全部由它引出。

整个网络里只有 BN 打破了「样本独立」:

操作样本之间
x @ W1、tanh、softmax独立 —— 第 0 行不影响第 1 行
每个样本的 loss独立,.mean() 只在最后一步合并
BatchNorm❌ 耦合 —— 中途就把样本混在一起

实测:

# 推理态:同一样本两次
a = forward(Xtr[:1], training=False)
b = forward(Xtr[:1], training=False)
torch.allclose(a, b) # True ✅
# 训练态:同一样本放不同 batch
r1 = forward(cat([Xtr[:1], Xtr[100:131]]), training=True)[0,0] # −3.8143
r2 = forward(cat([Xtr[:1], Xtr[500:531]]), training=True)[0,0] # −4.3548

同一个样本、同一套参数,换一批陪跑样本,输出差 0.54。

⚠️ 医疗场景里这不可接受:同一张 X 光片,跟不同的片子一起送进模型,诊断结果不一样。

但这个 bug 反而成了 feature

耦合引入的随机噪声起到了正则化作用 —— 这就是为什么用了 BN 之后往往可以少用甚至不用 dropout。 一个为了稳定训练而引入的副作用,顺手解决了另一个问题。


推理怎么办

第十一篇给「评估」下过一个定义:评估 = 只做前向,算一遍 loss,不更新参数, 和训练循环差三处:不抽样、不反向、不更新。 那条定义到这里要补一条 —— BatchNorm 是全系列第一个让「前向」本身在训练和推理时不一样的东西。

1 个样本时 BN 直接崩

推理时一次往往只有 1 个样本(生成一个名字、用户传一张片子):

z1 = [−1.406, −2.973, 0.255, 2.261]
z.mean(0) = [−1.406, −2.973, 0.255, 2.261] ← 均值就是它自己
z.std(0) = [nan, nan, nan, nan] ← 一个数没有标准差
情况结果
ddof=1(PyTorch 默认)分母 n−1 = 0 → 全是 nan
ddof=0(z−z)/1e-5 → 全是 0,不报错但模型废了

整条链路只有 BN 这一处卡住。 x @ W1、tanh、softmax 对 1 个样本都完全正常 —— 因为它们逐样本独立。这是上一节那件事的直接后果。

解法:训练时顺手记一份 running 统计量

bnmean_running = torch.zeros((1, n_hidden)) # 不是参数,是统计记录
bnstd_running = torch.ones((1, n_hidden))
with torch.no_grad(): # ❗ 不进计算图
bnmean_running = 0.999 * bnmean_running + 0.001 * zmean
bnstd_running = 0.999 * bnstd_running + 0.001 * zstd

每步保留 99.9% 的历史、混入 0.1% 的当前值。展开看就是一个加权平均:

多少步之前权重
当前1.000
100 步前0.905
1000 步前0.368(衰减到 1/e)
3000 步前0.050

有效窗口 ≈ 1/(1−0.999) = 1000 步 = 3.2 万个样本,比单个 batch 的 32 个稳定得多。

不用简单平均,是因为简单平均会记住训练早期的值 —— 第 1 步模型刚初始化, 那时的分布早就过时了。EMA 自动遗忘旧数据,跟着训练慢慢移动。

和第十一篇平滑 loss 曲线是同一套路:单次观测噪声大,用移动平均得到稳定估计。

推理态用固定值

def forward(X, training=True):
global bnmean_running, bnstd_running
z = C[X].view(X.shape[0], -1) @ W1
if training:
zmean = z.mean(0, keepdim=True)
zstd = z.std(0, keepdim=True)
with torch.no_grad():
bnmean_running = 0.999 * bnmean_running + 0.001 * zmean
bnstd_running = 0.999 * bnstd_running + 0.001 * zstd
else:
zmean, zstd = bnmean_running, bnstd_running # ← 固定值
z = bngain * (z - zmean) / (zstd + 1e-5) + bnbias
return torch.tanh(z) @ W2 + b2

推理时 BN 退化成一个固定的仿射变换,样本之间重新独立 —— 上一节那个 0.54 的差就消失了。

这就是 model.train() / model.eval()

模式BN 的行为
model.train()用当前 batch 的统计量,同时更新 running
model.eval()用 running 值,不更新

❗ 忘记调 model.eval() 是深度学习最常见的 bug 之一。 症状:验证 loss 异常地低、同一样本两次预测不同、改 batch size 结果就变。 Dropout 也受这两个开关影响。

训练结束后,每个 BN 层留下四组数

怎么得到在 parameters 里吗PyTorch 叫什么
γ、β反向传播学的✅parameter
running mean/var前向时顺手统计的❌buffer

但保存模型时两者都要存 —— state_dict() 同时包含这两类。 只存 γβ 不存 running,加载之后推理会彻底失效。


BN 在这个网络上没赢

版本val参数量
手调 0.22.099411897
Kaiming2.108711897
Kaiming + BN2.114012097
BN + running2.110612097

标准化本身是生效的:z.std 从 1.597 降到 0.984,饱和降到 0.8%。 但四个版本的 val 最大只差 0.0146,全在噪声范围内。

参数账要算清楚。 这里有两个看着打架的数字:

γ 和 β 各 200 个 → 加了 400
b1 在这一层失效了 → 去掉 200
11897 − 200 + 400 = 12097 → 净增 200

「代价是 400 个参数」和「表里只多了 200」都对,说的是两件事 —— 上一节那个「b1 消失了」 正好在这里结账。

为什么没赢

它要解决的问题 —— 深网络的信号衰减、初始化难调、层间分布漂移 —— 在 2 层网络里一个都不存在, 而代价(每步多算一次统计、batch 噪声、推理要另存一套数)是当场付的。

这是这一集第三次遇到「为深度设计的技术在浅网络上没收益」了。 前两次是 Kaiming 输给手调的 0.2,以及把隐藏层加宽反而变差。

但它的真正价值在别处

三种差一千倍的初始化,实测:

σ_WBN 前 z.stdBN 后 z.std
0.010.0541.000
1.05.2961.000
10.053.0841.000

输入差一千倍,输出完全一样。 这就是「与其算准,不如强制」那句话的全部含义 —— 第十四篇花了整整一篇在调 σ_W,BN 把这件事变成了不用调。

推论:BN 之后 W1 的长度没有意义,只有方向有意义 —— 尺度会被标准化吃掉。 这也是 weight decay 作用在有 BN 的层上时机制不同的原因。


两个副作用

① 它是一种正则化 —— 样本耦合引入的噪声类似 dropout,前面说过了。

② batch 太小时失效 —— batch = 2 时统计量的噪声极大,BN 反而有害。

第二条是 LayerNorm 出现的直接原因:它沿特征方向标准化(也就是上面那张图里 被判了 ❌ 的横向),跟 batch 大小无关,也没有样本耦合。

代价是它放弃了「跨样本统计」这个信息来源 —— 但对序列模型来说, 每个位置的特征本来就该独立处理,这个代价不存在。Transformer 全部用 LayerNorm,不用 BatchNorm。


这一篇的账很清楚:BN 拿走的是样本独立,换回来的是初始化不再重要。 在一个 2 层网络上这笔交易不划算 —— 它要解决的问题还没出现,代价倒是全付了。 但代价里的每一项(running 统计量、model.eval()、batch 不能太小)都不是临时补丁, 而是「让样本互相影响」这一个决定的必然后果。

第十四篇的诊断清单里,第 ② 步是查初始 loss、第 ③ 步是查逐层 σ 比值。 BN 之后这两步的答案被无条件保证了 —— 清单没有失效,是它的前两级被架空了。