深度学习归一化 fully 指南:BN/LN/IN/GN/RMSNorm 公式推导 + PyTorch 实验全验证(附完整可运行代码)

深度学习归一化 fully 指南:BN/LN/IN/GN/RMSNorm 公式推导 + PyTorch 实验全验证(附完整可运行代码)

导读 :本文系统整理深度学习五大归一化方法------BatchNorm、LayerNorm、InstanceNorm、GroupNorm、RMSNorm ,给出每种方法的完整公式、符号说明、应用场景与 PyTorch 实现;并用 6 组真实实验(RTX 5070 Ti 实测)验证每一条公式和结论:手写实现与官方模块误差 ~1e-7、BN 让收敛快 9 倍、batch=2 时 BN 精度暴跌 28 个百分点而 LN 纹丝不动、RMSNorm 比 LayerNorm 快 8%~14%......全部代码可直接复制运行。

阅读收获:一个统一视角记住五种归一化(唯一区别是统计维度)+ 一棵选型决策树 + 7 条实战避坑清单。

目录

  1. 为什么需要归一化
  2. 五大归一化公式详解
    • 2.1 BatchNorm(批归一化)
    • 2.2 LayerNorm(层归一化)
    • 2.3 InstanceNorm(实例归一化)
    • 2.4 GroupNorm(组归一化)
    • 2.5 RMSNorm(均方根归一化)
  3. 统一视角与对比总表
  4. PyTorch 实验设计与代码解读(6 组)
  5. 实验运行结果(RTX 5070 Ti 实测)
  6. 选型指南与实战避坑
  7. 完整实验脚本(可直接运行)
  8. 参考文献

1. 为什么需要归一化

深层网络中,每一层的输入分布会随前面层参数的更新而不断变化(内部协变量偏移,Internal Covariate Shift),导致:

  • 后层需要不停适应前层的分布漂移 → 收敛慢;
  • 激活值落入 sigmoid/tanh 的饱和区 → 梯度消失;
  • 不同特征量纲差异大 → 损失面呈狭长山谷状 → 只能用很小的学习率。

归一化的核心思想 :把每层的激活重新拉回均值 0、方差 1 的标准分布,再用可学习参数 γ , β \gamma, \beta γ,β 恢复表达能力。统一模板:

y = γ ⋅ x − μ S σ S 2 + ϵ + β y = \gamma \cdot \frac{x - \mu_{\mathcal{S}}}{\sqrt{\sigma_{\mathcal{S}}^{2} + \epsilon}} + \beta y=γ⋅σS2+ϵ x−μS+β

其中 S \mathcal{S} S 是统计集合 (沿哪些维度算 μ , σ \mu, \sigma μ,σ)。记住这句话:五大归一化的唯一本质区别就是 S \mathcal{S} S 的选取不同------下面所有公式都只是这一句话的展开。


2. 五大归一化公式详解

对卷积特征张量 x ∈ R B × C × H × W x \in \mathbb{R}^{B \times C \times H \times W} x∈RB×C×H×W(批大小 × 通道 × 高 × 宽):

复制代码
统计方向示意(谁和谁放在一起算 μ/σ):

           C 通道 →                    BatchNorm : 竖穿整摞(跨 B,H,W,每通道一组)
   B₁ ┌─────────────┐                  LayerNorm : 单张全图(跨 C,H,W,每样本一组)
   B₂ │▓▓▓▓▓▓▓▓▓▓▓▓▓│  ← 一张特征图   InstanceNorm: 单张单通道(跨 H,W,每样本每通道一组)
   B₃ │▓▓▓▓▓▓▓▓▓▓▓▓▓│                  GroupNorm : 单张的一组通道(跨 C/G,H,W)
   B₄ └─────────────┘                  RMSNorm   : 同 LayerNorm 但不减均值

2.1 BatchNorm(批归一化,Ioffe & Szegedy, 2015)

公式

μ B = 1 m ∑ i = 1 m x i , σ B 2 = 1 m ∑ i = 1 m ( x i − μ B ) 2 \mu_{\mathcal{B}} = \frac{1}{m}\sum_{i=1}^{m} x_i, \qquad \sigma_{\mathcal{B}}^{2} = \frac{1}{m}\sum_{i=1}^{m} (x_i - \mu_{\mathcal{B}})^{2} μB=m1i=1∑mxi,σB2=m1i=1∑m(xi−μB)2

x ^ i = x i − μ B σ B 2 + ϵ , y i = γ   x ^ i + β \hat{x}i = \frac{x_i - \mu{\mathcal{B}}}{\sqrt{\sigma_{\mathcal{B}}^{2} + \epsilon}}, \qquad y_i = \gamma\, \hat{x}_i + \beta x^i=σB2+ϵ xi−μB,yi=γx^i+β

推理时 改用训练期累积的滑动平均统计量(动量 p p p,PyTorch 默认 p = 0.1 p=0.1 p=0.1):

μ run ← ( 1 − p )   μ run + p   μ B , σ run 2 ← ( 1 − p )   σ run 2 + p   σ B 2 \mu_{\text{run}} \leftarrow (1-p)\,\mu_{\text{run}} + p\,\mu_{\mathcal{B}}, \qquad \sigma^{2}{\text{run}} \leftarrow (1-p)\,\sigma^{2}{\text{run}} + p\,\sigma^{2}_{\mathcal{B}} μrun←(1−p)μrun+pμB,σrun2←(1−p)σrun2+pσB2

符号说明 :统计在 mini-batch B \mathcal{B} B(大小 m m m)内进行------对 BatchNorm2d,同一通道 c c c 把所有样本 × 所有空间位置 ( B × H × W B \times H \times W B×H×W 个值)放在一起算一组 μ c , σ c 2 \mu_c, \sigma_c^2 μc,σc2; ϵ ≈ 10 − 5 \epsilon \approx 10^{-5} ϵ≈10−5 防除零; γ , β \gamma, \beta γ,β 逐通道可学习;方差用有偏估计 (除以 m m m 而非 m − 1 m-1 m−1)。

应用场景 :CNN/MLP 训练加速(本文实验 4 实测:达到 90% 准确率所需 epoch 从 9 → 1)、允许更大学习率、轻微正则化效果。对 batch 大小敏感 : m m m 很小时统计量噪声大,性能显著退化(实验 5 实测:batch=2 时准确率从 100% 跌到 71.7%)。

PyTorch

python 复制代码
bn = torch.nn.BatchNorm1d(256)      # MLP 版: 输入 (B,256),沿 B 维按列统计
bn2d = torch.nn.BatchNorm2d(64)     # CNN 版: 输入 (B,64,H,W),沿 (B,H,W) 统计、每通道一组参数
z = torch.randn(32, 64, 8, 8)
h = bn2d(z)                         # 训练态: 用当前 batch 统计
print(bn2d.running_mean.shape)      # (64,) 训练期累积的全局统计,推理时用它
model.eval()                        # ⚠ 推理前必须切换: 改用 running 统计且不再更新

代码解读

  • 构造参数 = 通道/特征数 ;每个通道独享一对 ( γ , β ) (\gamma,\beta) (γ,β) + 两个 running 统计 buffer;
  • train/eval 双模式是 BN 最大的坑 :train 用当前 batch 统计(同一样本换不同"邻居"输出会变,见实验 3),eval 用滑动平均(输出确定);忘记 model.eval() = 推理结果随 batch 组成漂移;
  • batch=1 时训练态方差无意义 → BN 失效;小 batch 场景请换 LayerNorm / GroupNorm。

2.2 LayerNorm(层归一化,Ba et al., 2016)

公式

μ = 1 d ∑ j = 1 d x j , σ 2 = 1 d ∑ j = 1 d ( x j − μ ) 2 , y j = γ j   x j − μ σ 2 + ϵ + β j \mu = \frac{1}{d}\sum_{j=1}^{d} x_j, \qquad \sigma^{2} = \frac{1}{d}\sum_{j=1}^{d} (x_j - \mu)^{2}, \qquad y_j = \gamma_j\, \frac{x_j - \mu}{\sqrt{\sigma^{2} + \epsilon}} + \beta_j μ=d1j=1∑dxj,σ2=d1j=1∑d(xj−μ)2,yj=γjσ2+ϵ xj−μ+βj

符号说明 : d d d 为单个样本的特征数;统计只在一个样本内部沿特征维进行------与 batch 完全无关,也没有 running 统计。

应用场景 :Transformer 标配(每个 token 的 d d d 维向量独立归一化)、RNN、batch 很小或序列变长的场景;推理部署无 train/eval 之分。

PyTorch

python 复制代码
ln = torch.nn.LayerNorm(512)        # 参数 = 归一化形状: 对最后一维(512 特征)算 μ,σ
x = torch.randn(32, 10, 512)        # (B, T, d): 32 条序列 × 10 个 token × 512 维
h = ln(x)                           # 每个 (b,t) 位置独立归一化 → 320 组统计量互不干扰
ln3 = torch.nn.LayerNorm([64, 8, 8])  # 也可传形状元组: 对 (C,H,W) 三维一起统计(每样本一组)

代码解读

  • 构造参数可以是 int(最后一维)或元组(最后几维一起);
  • 统计只依赖单样本 → batch=1、变长序列、train/eval 行为完全一致(实验 5 实测 batch=2 仍 100% 准确率),这是 Transformer 选它的根本原因;
  • 参数量仅 2 d 2d 2d 个( γ , β \gamma,\beta γ,β 各 d d d)。

2.3 InstanceNorm(实例归一化,Ulyanov et al., 2016)

公式

μ c = 1 H W ∑ h , w x c , h , w , σ c 2 = 1 H W ∑ h , w ( x c , h , w − μ c ) 2 \mu_{c} = \frac{1}{HW}\sum_{h,w} x_{c,h,w}, \qquad \sigma_{c}^{2} = \frac{1}{HW}\sum_{h,w} (x_{c,h,w} - \mu_{c})^{2} μc=HW1h,w∑xc,h,w,σc2=HW1h,w∑(xc,h,w−μc)2

(对每个样本、每个通道 独立计算,即统计集合 S \mathcal{S} S = 单张特征图的 H × W H \times W H×W)

符号说明 :可视为 LayerNorm 在图像上的特例------LN 对单样本所有通道一起统计,IN 对单样本每个通道分别统计;等价于 GroupNorm 取 G = C G=C G=C。

应用场景 :风格迁移/图像生成的主力------归一化掉每个通道(每种滤镜响应)的对比度统计量 = 抹掉原图的"风格"信息,只保留内容结构。

PyTorch

python 复制代码
inn = torch.nn.InstanceNorm2d(64, affine=True)  # 风格迁移常用 affine=True 让网络学习风格统计
x = torch.randn(4, 64, 32, 32)                  # (B,C,H,W)
h = inn(x)                                      # 每个 (样本,通道) 对 32×32 空间单独归一化

代码解读

  • 默认 affine=False(与 BN/LN 默认相反!)------因为风格迁移中常只需要"去风格"不需要恢复;
  • 每个样本独立 → 天然支持 batch=1,推理无模式切换问题。

2.4 GroupNorm(组归一化,Wu & He, 2018)

公式

将 C C C 个通道均分为 G G G 组,每组 C / G C/G C/G 个通道;对每个样本的每组:

μ g = 1 ( C / G ) H W ∑ c ∈ g ∑ h , w x c , h , w , y = γ   x − μ g σ g 2 + ϵ + β \mu_{g} = \frac{1}{(C/G)HW}\sum_{c \in g}\sum_{h,w} x_{c,h,w}, \qquad y = \gamma\, \frac{x - \mu_{g}}{\sqrt{\sigma_{g}^{2} + \epsilon}} + \beta μg=(C/G)HW1c∈g∑h,w∑xc,h,w,y=γσg2+ϵ x−μg+β

符号说明 : G G G 为组数(常用 32)。统一特例 : G = 1 G=1 G=1 退化为 LayerNorm(对图像), G = C G=C G=C 退化为 InstanceNorm(实验 1e 已数值验证)------GN 是 LN 与 IN 之间的连续插值。

应用场景 :小 batch 的检测/分割任务(显存限制下 batch 常为 1~2,BN 失效,GN 精度稳定);扩散模型(Diffusion/U-Net)主流选择。

PyTorch

python 复制代码
gn = torch.nn.GroupNorm(32, 64)     # 64 通道分 32 组(每组 2 通道),对 (C/G,H,W) 统计
x = torch.randn(2, 64, 16, 16)      # batch=2 也完全没问题 ------ 统计与 batch 无关
h = gn(x)

代码解读

  • 实现上等价于 reshape 成 ( B , G , C / G , H , W ) (B, G, C/G, H, W) (B,G,C/G,H,W) 后对后三维统计(见实验 1e 手写代码);
  • batch=1/2 时精度几乎不掉 → 显存受限任务首选;
  • G G G 的选择:通道少时减小 G G G(保证每组有足够元素算统计量)。

2.5 RMSNorm(均方根归一化,Zhang & Sennrich, 2019)

公式

R M S ( x ) = 1 d ∑ j = 1 d x j 2 + ϵ , y j = γ j   x j R M S ( x ) \mathrm{RMS}(x) = \sqrt{\frac{1}{d}\sum_{j=1}^{d} x_j^{2} + \epsilon}, \qquad y_j = \gamma_j\, \frac{x_j}{\mathrm{RMS}(x)} RMS(x)=d1j=1∑dxj2+ϵ ,yj=γjRMS(x)xj

符号说明 :与 LayerNorm 相比去掉了减均值(re-centering)和偏置 β \beta β,只做 re-scaling。核心假设:归一化的收益主要来自"尺度不变性"而非"中心化"。

应用场景 :大语言模型标配------LLaMA、Qwen、Gemma、T5 等全部使用;省一次归约运算 → 更快(实验 6 实测快 8%~14%),大模型上数值表现与 LN 相当。

PyTorch

python 复制代码
rms = torch.nn.RMSNorm(4096)        # torch ≥ 2.4 内置;对最后一维算均方根
x = torch.randn(8, 2048, 4096)      # (B, T, d) LLM 典型形状
h = rms(x)                          # y = x / RMS(x) * γ,无 β、无中心化

代码解读

  • elementwise_affine=True 时只有 γ \gamma γ( d d d 个参数),没有 β \beta β;
  • 不减均值 → 少一次 mean 归约和一次减法 → kernel 更省带宽,序列越长收益越明显;
  • 若任务强依赖特征中心化(如某些 CNN),LN 仍可能更稳;LLM 场景无脑选 RMSNorm。

3. 统一视角与对比总表

五者都是 y = γ x − μ S σ S 2 + ϵ + β y = \gamma \frac{x-\mu_{\mathcal{S}}}{\sqrt{\sigma^2_{\mathcal{S}}+\epsilon}} + \beta y=γσS2+ϵ x−μS+β,区别仅在统计集合 S \mathcal{S} S:

方法 统计维度 S \mathcal{S} S(对 B , C , H , W B,C,H,W B,C,H,W) 依赖 batch? train/eval 不同? 可学习参数 主战场
BatchNorm ( B , H , W ) (B, H, W) (B,H,W),每通道一组 ✅ 强依赖 ✅(running 统计) 2 C 2C 2C CNN/MLP,大 batch
LayerNorm ( C , H , W ) (C, H, W) (C,H,W),每样本一组 ❌ ❌ 2 d 2d 2d Transformer / RNN
InstanceNorm ( H , W ) (H, W) (H,W),每样本每通道 ❌ ❌ 2 C 2C 2C(默认无) 风格迁移 / 生成
GroupNorm ( C / G , H , W ) (C/G, H, W) (C/G,H,W),每样本每组 ❌ ❌ 2 C 2C 2C 小 batch 检测/分割、扩散模型
RMSNorm 同 LN 但不减均值 ❌ ❌ d d d(仅 γ \gamma γ) LLM(LLaMA/Qwen)

特殊关系(实验 1e 已数值验证):

GroupNorm ∣ G = 1 = LayerNorm , GroupNorm ∣ G = C = InstanceNorm \text{GroupNorm}\big|{G=1} = \text{LayerNorm}, \qquad \text{GroupNorm}\big|{G=C} = \text{InstanceNorm} GroupNorm G=1=LayerNorm,GroupNorm G=C=InstanceNorm


4. PyTorch 实验设计与代码解读(6 组)

环境:Python 3.12 + PyTorch 2.14 + CUDA 13.0(RTX 5070 Ti Laptop 12GB)。完整代码见第 7 章,可整体复制运行。

实验 1:公式验证 ------ 手写实现 vs 官方模块

思路 :按第 2 节公式手写每种归一化,与 nn.* 官方模块输出逐元素对比(最大误差应 < 1e-5),证明"公式 = 实现"。统一设 affine=False( γ = 1 , β = 0 \gamma=1,\beta=0 γ=1,β=0)只验证标准化部分,方差用有偏估计 unbiased=False。

python 复制代码
# ---------- BatchNorm2d:沿 (B, H, W) 逐通道统计 ----------
x4d = torch.randn(8, 6, 5, 7)                              # (B,C,H,W)
mu  = x4d.mean(dim=(0, 2, 3), keepdim=True)                # (1,C,1,1) ← 关键: dim 跳过 C
var = x4d.var(dim=(0, 2, 3), keepdim=True, unbiased=False) # 有偏方差(除以 N)
x_hat = (x4d - mu) / torch.sqrt(var + 1e-5)                # 标准化
assert torch.allclose(x_hat, nn.BatchNorm2d(6, affine=False)(x4d))

# ---------- GroupNorm:reshape 出"组"维后对 (C/G, H, W) 统计 ----------
G = 3
x_g   = x4d.reshape(8, G, 6 // G, 5, 7)                    # (B, G, C/G, H, W)
mu    = x_g.mean(dim=(2, 3, 4), keepdim=True)              # 每(样本,组)一组统计
var   = x_g.var(dim=(2, 3, 4), keepdim=True, unbiased=False)
x_gn  = ((x_g - mu) / torch.sqrt(var + 1e-5)).reshape(8, 6, 5, 7)  # 还原形状

# ---------- RMSNorm:不减均值,只除以均方根 ----------
x_seq = torch.randn(4, 10, 512)                            # (B,T,d)
rms   = torch.sqrt(x_seq.pow(2).mean(dim=-1, keepdim=True) + 1e-5)
y     = x_seq / rms                                        # 对比 nn.RMSNorm(512)

解读:

  • dim 的选择就是"统计方向":BN 跳过通道维 (0,2,3),LN 跳过 batch 维 (1,2,3),IN 只留空间维 (2,3),GN 通过 reshape 把"组"变成独立维度再统计------"跳过哪个维度,哪个维度就保留独立的统计量";
  • unbiased=False 很重要:归一化层的标准定义是除以 N N N(有偏),而 torch.var 默认除以 N − 1 N-1 N−1;
  • GN 的 reshape 技巧是理解"分组"的最直观方式:分组 = 在 C 维上切一刀变成 ( G , C / G ) (G, C/G) (G,C/G) 两维。

实验 2:统计方向探针

思路:归一化后沿其统计维度求输出的 mean/std,应精确等于 0/1。用这个"探针"反向验证每种方法到底沿哪些维度统计。

python 复制代码
def probe(name, out, dims, dim_names):
    m = out.mean(dim=dims).abs().max().item()       # 沿统计维求均值 → 应≈0
    s = out.std(dim=dims, unbiased=False)           # 沿统计维求标准差 → 应≈1
    print(f"{name} 沿{dim_names}: |mean|max={m:.1e}, std∈[{s.min():.3f},{s.max():.3f}]")

实验 3:BN 双模式与滑动平均

思路 :三个验证点------① 喂 200 个 batch 的 N ( 5 , 2 2 ) N(5, 2^2) N(5,22) 数据,看 running_mean/var 是否收敛到真值(EMA 系数 0.1 → 初始值权重衰减 0.9 200 ≈ 7 × 10 − 10 0.9^{200} \approx 7 \times 10^{-10} 0.9200≈7×10−10,可忽略);② 同一样本放进两个不同"邻居"的 batch,train 模式下输出应不同(batch 依赖);③ eval() 后同一样本输出应相同(用 running 统计,与 batch 无关)。

python 复制代码
bn3 = nn.BatchNorm1d(4, momentum=0.1)
bn3.train()
for _ in range(200):                                  # 喂 200 个 batch
    bn3(torch.randn(32, 4) * 2.0 + 5.0)               # 真实分布 N(μ=5, σ=2)
print(bn3.running_mean[0], bn3.running_var[0])        # 应 ≈ 5.0, 4.0

same = torch.tensor([[1.0, 2.0, 3.0, 4.0]])           # 同一个样本
batch_a = torch.cat([same, torch.randn(31, 4) * 0.1])       # 邻居分布 A: N(0,0.1)
batch_b = torch.cat([same, torch.randn(31, 4) * 10 + 100])  # 邻居分布 B: N(100,10)
out_a, out_b = bn3(batch_a)[0], bn3(batch_b)[0]       # train: 两次输出不同!
bn3.eval()
out_a, out_b = bn3(batch_a)[0], bn3(batch_b)[0]       # eval: 两次输出相同 ✓

解读 :train 模式下 μ , σ \mu,\sigma μ,σ 来自当前 batch,样本的归一化结果被"邻居"影响------这既是 BN 的隐性正则化来源,也是它不能用于小 batch / 在线推理的原因。eval 模式换用确定性的 running 统计,这就是 model.eval() 不可省略的实证。

实验 4:训练对比 ------ 深层 MLP ± BatchNorm

思路 :控制变量法。10 类高斯团分类任务(20 维输入,5120 训练样本),6 隐藏层 × 256 的 MLP(故意做深:层数越多分布漂移越明显),两模型同种子初始化、同数据、同超参 (朴素 SGD lr=0.01,batch=64,20 epochs,不加动量以免"帮忙"),唯一区别是有无 BN。重点指标:达到 90% 测试准确率所需 epoch 数。

python 复制代码
def build_mlp(use_bn, dim=20, hidden=256, depth=6, n_classes=10):
    layers, d_in = [], dim
    for _ in range(depth):
        layers.append(nn.Linear(d_in, hidden))
        if use_bn:
            layers.append(nn.BatchNorm1d(hidden))     # 经典位置: Linear 之后、ReLU 之前
        layers.append(nn.ReLU())
        d_in = hidden
    layers.append(nn.Linear(d_in, n_classes))
    return nn.Sequential(*layers)

torch.manual_seed(42); model_nobn = build_mlp(use_bn=False)  # 无 BN 版
torch.manual_seed(42); model_bn   = build_mlp(use_bn=True)   # 同种子 → 同初始化,公平对比

解读:

  • BN 放 Linear 后、ReLU 前是 ResNet 原文的经典位置------Linear 输出接近高斯分布,归一化最有效;ReLU 后一半值为 0,分布偏斜;
  • 两次 manual_seed(42) 保证 Linear 权重初始化逐位相同(BN 的 γ=1,β=0 初始化不消耗随机数),把差异唯一归因于 BN;
  • 训练循环中 model.train()/model.eval() 的切换正是实验 3 结论的实际应用。

实验 5:小 batch 敏感性

思路 :3 隐藏层 MLP 分别用 BN / LN,各以 batch=2 和 batch=64 训练 8 epochs,对比最终测试准确率。理论预期:BN 的 μ , σ \mu,\sigma μ,σ 是对 m m m 个样本的估计 , m = 2 m=2 m=2 时噪声极大( m = 1 m=1 m=1 时方差恒为 0,完全失效);LN 统计只来自单样本内部 256 个特征值,与 batch 彻底无关。

实验 6:RMSNorm vs LayerNorm GPU 速度

思路 :Transformer 典型形状 ( 8 , 2048 , 4096 ) (8, 2048, 4096) (8,2048,4096) fp32(≈256MB,远超 L2 缓存 → 访存受限特征明显),各跑 50 次前向取平均。GPU 计时两大陷阱:预热 (首次调用含 kernel 编译)与 torch.cuda.synchronize()(CUDA 异步执行,不同步测到的是"提交任务的时间")。

python 复制代码
def bench(fn, iters=50):
    for _ in range(5): fn(x_big)          # 预热
    torch.cuda.synchronize()              # GPU 异步 → 同步后再计时
    t0 = time.perf_counter()
    for _ in range(iters): fn(x_big)
    torch.cuda.synchronize()
    return (time.perf_counter() - t0) / iters * 1000

5. 实验运行结果(RTX 5070 Ti 实测)

text 复制代码
运行设备: cuda
GPU 型号 : NVIDIA GeForce RTX 5070 Ti Laptop GPU
======================================================================

【实验 1】公式验证:手写实现 vs PyTorch 官方模块
----------------------------------------------------------------------
1a. BatchNorm1d  手写 vs nn.BatchNorm1d : 最大误差 = 2.38e-07  → 一致 ✓
1b. BatchNorm2d  手写 vs nn.BatchNorm2d : 最大误差 = 2.38e-07  → 一致 ✓
1c. LayerNorm    手写 vs nn.LayerNorm   : 最大误差 = 4.77e-07  → 一致 ✓
1d. InstanceNorm 手写 vs nn.InstanceNorm2d: 最大误差 = 4.77e-07  → 一致 ✓
1e. GroupNorm    手写 vs nn.GroupNorm   : 最大误差 = 4.77e-07  → 一致 ✓
    特例验证: G=1 时应等于 LayerNorm, G=C 时应等于 InstanceNorm
    G=1 vs LayerNorm  误差 = 4.77e-07 ✓   G=C vs InstanceNorm 误差 = 4.77e-07 ✓
1f. RMSNorm      手写 vs nn.RMSNorm     : 最大误差 = 2.38e-07  → 一致 ✓
1g. γ/β 仿射变换 手写 vs affine=True    : 最大误差 = 7.15e-07  → 一致 ✓

【实验 2】统计方向演示(对同一 (B=8, C=6, H=5, W=7) 张量)
----------------------------------------------------------------------
BatchNorm2d    沿 (B,H,W)    → |mean|max=1.2e-08, std∈[1.000,1.000]
LayerNorm      沿 (C,H,W)    → |mean|max=1.8e-08, std∈[1.000,1.000]
InstanceNorm   沿 (H,W)      → |mean|max=3.9e-08, std∈[1.000,1.000]
GroupNorm      沿 (H,W)*     → |mean|max=3.7e-01, std∈[0.755,1.179]
  * GroupNorm 的 std 沿 (H,W) 看每组内各通道共享同一统计量,故组内通道 std 相同
  → 结论: 五种归一化本质相同,唯一区别是'在哪些维度上算 μ 和 σ'

【实验 3】BatchNorm 双模式与滑动平均
----------------------------------------------------------------------
数据真实分布: mean=5.0, std=2.0 (var=4.0)
200 个 batch 后 running_mean = 5.085 (→ 收敛到 5.0)
200 个 batch 后 running_var  = 4.153 (→ 收敛到 4.0)

train 模式: 同一样本在不同 batch 中的输出
  batch_a 中输出: [4.741, 5.322, 5.487, 5.499]
  batch_b 中输出: [-4.734, -4.892, -4.671, -4.725]
  两次输出相同吗? False → BN 训练态有 batch 依赖
eval  模式: 同一样本输出是否一致? True → 与 batch 无关 ✓
  ⚠ 这就是为什么推理前必须 model.eval(),否则结果随 batch 组成漂移

【实验 4】训练对比:深层 MLP ± BatchNorm
----------------------------------------------------------------------
  MLP 无归一化           最终 train_loss=0.0051, test_acc=100.0%
  MLP + BatchNorm    最终 train_loss=0.0045, test_acc=100.0%
  达到 90% 准确率所需 epoch: 无BN = 9, 有BN = 1
  对比图已保存: figures/norm_exp_training.png

【实验 5】小 batch 敏感性(batch=2 vs batch=64)
----------------------------------------------------------------------
  BN batch=2         最终 train_loss=1.7209, test_acc=71.7%
  BN batch=64        最终 train_loss=0.0153, test_acc=100.0%
  LN batch=2         最终 train_loss=0.0003, test_acc=100.0%
  LN batch=64        最终 train_loss=0.0096, test_acc=100.0%

【实验 6】GPU 速度对比:RMSNorm vs LayerNorm
----------------------------------------------------------------------
输入形状: (8, 2048, 4096) fp32, 各 50 次前向平均
LayerNorm : 1.299 ms/次
RMSNorm   : 1.142 ms/次  (快 14%)

(注:GPU 计时受显卡频率/温度影响,多次运行 RMSNorm 领先幅度在 8%~14% 间波动,方向恒定)

训练对比图 :左图为训练损失曲线、右图为测试准确率曲线,蓝 = +BN、红 = 无归一化。

结果解读

实验 关键数字 印证了哪条公式/结论
1 全部误差 ~1e-7 第 2 节公式与官方实现严格一致;GN 的 G = 1 / G = C G=1/G=C G=1/G=C 特例关系成立
2 各归一化沿其统计维 std 精确 = 1.000 "统计集合 S \mathcal{S} S 不同"是五者唯一区别
3 running_mean 5.085→5.0;train 态同一样本两种输出 滑动平均公式收敛;BN 的 batch 依赖与 eval 必要性
4 90% 准确率所需 epoch:9 → 1(快 9 倍) BN 加速收敛的核心价值
5 BN batch=2 时 100% → 71.7%;LN 两种 batch 均 100% BN 依赖 m m m 个样本估计 μ , σ \mu,\sigma μ,σ, m m m 太小估计噪声大;LN 统计与 batch 无关
6 RMSNorm 快 8%~14% 少一次"减均值"归约 → 更省显存带宽

6. 选型指南与实战避坑

选型决策树

复制代码
你的模型是什么?
├─ Transformer / LLM ............... RMSNorm(现代首选)或 LayerNorm
├─ RNN / 序列模型 .................. LayerNorm
├─ CNN,batch ≥ 16 ................. BatchNorm2d(收敛最快)
├─ CNN,batch ≤ 8(检测/分割)...... GroupNorm(G=32 起步)
├─ 扩散模型 U-Net .................. GroupNorm
└─ 风格迁移 / 图像生成 ............. InstanceNorm

避坑清单

  1. BN 忘切 model.eval():推理结果随 batch 组成漂移,且 running 统计被污染------实验 3 实证;
  2. torch.var 默认无偏 :手写归一化必须 unbiased=False,否则和官方模块对不上;
  3. 小 batch 硬用 BN:batch=2 时准确率暴跌 28 个百分点(实验 5),换 GN/LN;
  4. BN 的 batch 内样本泄漏:BN 让同 batch 样本互相影响 → 在线学习、联邦学习、隐私敏感场景慎用;
  5. InstanceNorm 默认 affine=False :与 BN/LN 默认相反,需要 γ , β \gamma,\beta γ,β 时记得显式打开;
  6. Transformer 里别用 BN:变长序列 padding 会污染统计量,且推理 batch=1 时 BN 失效;
  7. 归一化位置 :CNN 经典为 Conv → BN → ReLU;Transformer 现代实践多为 Pre-Norm (x + Attn(Norm(x))),训练更稳。

7. 完整实验脚本(可直接运行)

保存为 norm_experiments.py,环境要求 PyTorch ≥ 2.4(nn.RMSNorm),执行 python norm_experiments.py。GPU 上约 1~2 分钟跑完;无 GPU 自动回退 CPU(实验 6 计时跳过)。

python 复制代码
# -*- coding: utf-8 -*-
"""
深度学习归一化(Normalization)系列实验
=====================================

--------------------------------------------------------------------------
背景知识(读懂本脚本需要的全部数学)
--------------------------------------------------------------------------
所有归一化层共享同一个模板公式:

        y = γ · (x - μ_S) / √(σ²_S + ε) + β

其中 S 是"统计集合"------即沿张量的哪些维度计算均值 μ 和方差 σ²。
五大归一化的唯一本质区别就是 S 的选取不同:

  对卷积特征 x ∈ R^(B×C×H×W)(批大小×通道×高×宽):
    BatchNorm    : S = (B, H, W)   → 每通道一组统计(跨样本+跨空间)
    LayerNorm    : S = (C, H, W)   → 每样本一组统计(跨全部特征)
    InstanceNorm : S = (H, W)      → 每(样本,通道)一组统计(只跨空间)
    GroupNorm    : S = (C/G, H, W) → 每(样本,通道组)一组统计
    RMSNorm      : 同 LayerNorm 的 S,但不减均值:y = γ·x/√(mean(x²)+ε)

三个贯穿全脚本的实现细节(也是最常见的手写错误来源):
  1) 方差是"有偏估计"------除以 N 而不是 N-1。torch.var 默认 unbiased=True
     (除以 N-1),手写对比时必须显式传 unbiased=False,否则对不上。
  2) ε (eps) 加在方差上再开方:√(σ²+ε),防止 σ≈0 时除零爆炸。
     PyTorch 各归一化层默认 eps=1e-5,本脚本统一取 1e-5。
  3) γ (weight) / β (bias) 是逐特征可学习参数。设 affine=False 时
     γ≡1, β≡0,只剩"标准化"部分------便于和手写公式逐元素对比。

--------------------------------------------------------------------------
实验清单
--------------------------------------------------------------------------
  实验1  公式验证:手写 5 种归一化 vs PyTorch 官方模块(数值逐元素对比)
         覆盖 BatchNorm1d/2d、LayerNorm、InstanceNorm、GroupNorm、RMSNorm
         以及 γ/β 仿射变换、GroupNorm 的两个特例(G=1→LN, G=C→IN)
  实验2  统计方向演示:归一化后沿统计维 mean≈0、std≈1 的"探针"验证
  实验3  BatchNorm 的 train/eval 双模式、滑动平均收敛、batch 依赖性
  实验4  训练对比:深层 MLP 加/不加 BatchNorm 的收敛速度与精度(生成对比图)
  实验5  小 batch 敏感性:batch=2 时 BN 退化 vs LN 不受影响
  实验6  GPU 速度对比:RMSNorm vs LayerNorm(Transformer 典型形状)
"""

import time
import torch
import torch.nn as nn

# 固定随机种子 → 所有 randn/初始化/数据打乱都可复现。
# 注意:这只固定了 CPU 端的默认生成器;实验 4 的数据生成另用了独立
# Generator(seed=0),两者互不干扰(这样数据与模型初始化可以分别复现)。
torch.manual_seed(42)

# 优先用 GPU;没有 CUDA 时自动回退 CPU(实验 6 的计时部分会跳过)。
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"运行设备: {DEVICE}")
if DEVICE.type == "cuda":
    print(f"GPU 型号 : {torch.cuda.get_device_name(0)}")
print("=" * 70)

# ======================================================================
# 实验 1:公式验证 ------ 手写实现 vs PyTorch 官方模块
# ----------------------------------------------------------------------
# 目的:证明官方模块的行为与数学公式完全一致。
# 验证方法:手写公式算一遍 → 官方模块算一遍 → 逐元素求最大绝对误差,
#           误差 < 1e-5 即认为一致(float32 机器精度约 1.2e-7,
#           归一化涉及除法/开方,误差放大到 1e-7 量级属正常浮点舍入)。
# 统一约定(三个"对齐",缺一个就对不上):
#   * 官方模块设 affine=False(即 γ=1, β=0),只验证"标准化"部分;
#   * eps 统一取 1e-5,与 PyTorch 默认值一致;
#   * 方差用有偏估计 unbiased=False(除以 N),这是归一化层的标准定义。
# ======================================================================
print("\n【实验 1】公式验证:手写实现 vs PyTorch 官方模块")
print("-" * 70)

# ---------- 1a. BatchNorm1d:输入 (B, F),沿 batch 维 B 逐特征统计 ----------
# 对每个特征 j,把 batch 里所有样本的该特征取值放在一起算 μ_j, σ_j。
# 直觉:一"列"数据(同一特征跨样本)共享一组统计量。
B, F = 32, 16                                  # batch=32 个样本,每个 16 维特征
x1d = torch.randn(B, F)                        # 形状 (32, 16)

# dim=0 就是"沿 batch 维归约";keepdim=True 保留该维为 1,
# 这样 (1,F) 的 μ 能和 (B,F) 的 x 直接广播相减,无需手动扩维。
mu = x1d.mean(dim=0, keepdim=True)             # (1, F)  每个特征一个均值
# ⚠ unbiased=False 是关键:torch.var 默认除以 B-1(无偏估计,统计学惯例),
# 但归一化层的定义是除以 B(有偏/总体方差)。不设这个参数误差会很大。
var = x1d.var(dim=0, keepdim=True, unbiased=False)  # (1, F) 有偏方差(除以B)
# 标准化:x̂ = (x - μ)/√(σ²+ε)。eps 加在方差上(不是标准差上),与官方一致。
x1d_hat = (x1d - mu) / torch.sqrt(var + 1e-5)

# 官方模块:BatchNorm1d(num_features) ------ 构造参数是特征数 F,不是 batch!
bn1d = nn.BatchNorm1d(F, affine=False, eps=1e-5)
out_ref = bn1d(x1d)
err = (x1d_hat - out_ref).abs().max().item()   # 逐元素最大绝对误差
print(f"1a. BatchNorm1d  手写 vs nn.BatchNorm1d : 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# ---------- 1b. BatchNorm2d:输入 (B, C, H, W),沿 (B, H, W) 逐通道统计 ----------
# 对每个通道 c,把【整个 batch × 所有空间位置】的像素(共 B×H×W 个值)
# 放在一起算一组 μ_c, σ_c²。
# 直觉:同一通道 = 同一种滤波器响应,无论出现在哪张图哪个位置,
#       都视为同一分布的样本 → 统计量最"充足",BN 训练最稳的原因。
B, C, H, W = 8, 6, 5, 7                        # batch=8, 6通道, 5×7 特征图
x4d = torch.randn(B, C, H, W)                  # 形状 (8, 6, 5, 7)

# 关键:dim=(0,2,3) ------ 跳过通道维 C(dim=1),对其余三个维度一起归约。
# "跳过哪个维度,哪个维度就保留独立的统计量",这是记统计方向的诀窍。
mu = x4d.mean(dim=(0, 2, 3), keepdim=True)     # (1, C, 1, 1) 每通道一个均值
var = x4d.var(dim=(0, 2, 3), keepdim=True, unbiased=False)
x4d_hat = (x4d - mu) / torch.sqrt(var + 1e-5)  # (1,C,1,1) 与 (B,C,H,W) 广播

bn2d = nn.BatchNorm2d(C, affine=False, eps=1e-5)  # 参数仍是通道数 C
err = (x4d_hat - bn2d(x4d)).abs().max().item()
print(f"1b. BatchNorm2d  手写 vs nn.BatchNorm2d : 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# ---------- 1c. LayerNorm:对单个样本的全部特征统计(与 batch 无关) ----------
# 统计方向从"跨样本"翻转为"样本内部":对每个样本 b,把它自己的
# C×H×W 个值放在一起算一组 μ_b, σ_b²。样本之间互不干扰。
# 直觉:把 batch 维当作"不相干的外界",每个样本自给自足 →
#       batch=1、变长序列、在线推理全都行为一致(Transformer 选它的原因)。
mu = x4d.mean(dim=(1, 2, 3), keepdim=True)     # (B, 1, 1, 1) 每样本一个均值
var = x4d.var(dim=(1, 2, 3), keepdim=True, unbiased=False)
x_ln_hat = (x4d - mu) / torch.sqrt(var + 1e-5)

# elementwise_affine=False 等价于 affine=False(LayerNorm 的参数名不同)
ln = nn.LayerNorm([C, H, W], eps=1e-5, elementwise_affine=False)
err = (x_ln_hat - ln(x4d)).abs().max().item()
print(f"1c. LayerNorm    手写 vs nn.LayerNorm   : 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# ---------- 1d. InstanceNorm:每个样本、每个通道单独统计(只看 H×W) ----------
# 比 LayerNorm 更"碎":LN 是每样本一组统计,IN 是每(样本,通道)一组。
# 直觉:每个通道的响应强度(对比度/亮度统计)被独立抹平 →
#       正好抹掉图像的"风格"信息(风格 ≈ 各通道的统计量),保留内容结构,
#       所以 IN 是风格迁移的主力。
mu = x4d.mean(dim=(2, 3), keepdim=True)        # (B, C, 1, 1) B×C 组统计量
var = x4d.var(dim=(2, 3), keepdim=True, unbiased=False)
x_in_hat = (x4d - mu) / torch.sqrt(var + 1e-5)

# ⚠ InstanceNorm2d 默认 affine=False(与 BN/LN 默认 affine=True 相反)!
inn = nn.InstanceNorm2d(C, affine=False, eps=1e-5)
err = (x_in_hat - inn(x4d)).abs().max().item()
print(f"1d. InstanceNorm 手写 vs nn.InstanceNorm2d: 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# ---------- 1e. GroupNorm:把 C 个通道分成 G 组,每(样本,组)统计 (C/G, H, W) ----------
# LN 与 IN 的折中:LN 把 C 个通道全放一组(粒度太粗,通道语义混淆),
# IN 把每个通道单独一组(粒度太细,每组只有 H×W 个值,统计噪声大)。
# GN 取中间:C 个通道均分 G 组(常用 G=32),每组 C/G 个通道 ×H×W 一起统计。
# 实现技巧:reshape 把 C 维"劈"成 (G, C/G) 两维,组就变成独立维度,
#           然后对 (C/G, H, W) 三维归约即可 ------ 分组归一化 = 变形 + LN。
G = 3                                          # 6 个通道分 3 组,每组 2 个通道
B_, C_, H_, W_ = x4d.shape                     # 解包形状,避免与全局变量混淆
x_g = x4d.reshape(B_, G, C_ // G, H_, W_)      # (8,6,5,7) → (8,3,2,5,7)
mu = x_g.mean(dim=(2, 3, 4), keepdim=True)     # (B, G, 1, 1, 1)
var = x_g.var(dim=(2, 3, 4), keepdim=True, unbiased=False)
x_gn_hat = ((x_g - mu) / torch.sqrt(var + 1e-5)).reshape(B_, C_, H_, W_)

gn = nn.GroupNorm(G, C, eps=1e-5, affine=False)  # 参数: (组数G, 通道数C)
err = (x_gn_hat - gn(x4d)).abs().max().item()
print(f"1e. GroupNorm    手写 vs nn.GroupNorm   : 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# 特例验证:GN 是 LN 和 IN 的连续插值 ------
#   G=1 : 所有通道一组,每样本统计 (C,H,W) → 恰好等于 LayerNorm
#   G=C : 每通道一组,每(样本,通道)统计 (H,W) → 恰好等于 InstanceNorm
print(f"    特例验证: G=1 时应等于 LayerNorm, G=C 时应等于 InstanceNorm")
gn1 = nn.GroupNorm(1, C, eps=1e-5, affine=False)   # G=1 → LayerNorm(每样本)
gnC = nn.GroupNorm(C, C, eps=1e-5, affine=False)   # G=C → InstanceNorm
err_ln = (gn1(x4d) - ln(x4d)).abs().max().item()
err_in = (gnC(x4d) - inn(x4d)).abs().max().item()
print(f"    G=1 vs LayerNorm  误差 = {err_ln:.2e} ✓   G=C vs InstanceNorm 误差 = {err_in:.2e} ✓")

# ---------- 1f. RMSNorm:不减均值,只除以均方根(LLaMA/Qwen 等 LLM 标配) ----------
# 公式: y = x / √(mean(x²) + ε) · γ    (没有 β,没有 x-μ 的中心化步骤)
# 动机(Zhang & Sennrich, 2019):归一化的收益主要来自"尺度不变性"
# (re-scaling),而"中心化"(re-centering) 贡献很小 → 干脆省掉:
#   * 少一次 mean 归约 + 少一次减法 → kernel 更省显存带宽,速度更快;
#   * 参数从 2d 个减到 d 个(只有 γ)。
D = 512                                        # 特征维 d(LLM 中的 d_model)
x_seq = torch.randn(4, 10, D)                  # (B, T, d): 4条序列×10个token×512维

# dim=-1 表示只对最后一维(特征维 d)统计 ------ 每个 token 独立归一化。
rms = torch.sqrt(x_seq.pow(2).mean(dim=-1, keepdim=True) + 1e-5)
x_rms_hat = x_seq / rms                        # 注意:没有减均值这一步!

# nn.RMSNorm 需要 torch ≥ 2.4。为对齐手写版显式传 eps=1e-5。
rmsn = nn.RMSNorm(D, eps=1e-5, elementwise_affine=False)
err = (x_rms_hat - rmsn(x_seq)).abs().max().item()
print(f"1f. RMSNorm      手写 vs nn.RMSNorm     : 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# ---------- 1g. 可学习参数 γ/β 的作用验证 ----------
# 为什么标准化后还要乘 γ 加 β?------ 恢复表达能力:
# 强制每层输出都是 0 均值 1 方差,等于限制了层的表示能力。
# γ,β 可学习意味着网络可以自己决定"归一化多少":极端情况学出
# γ=σ, β=μ 就能完全"撤销"归一化 → 归一化只做了一次可逆的重参数化。
bn_ab = nn.BatchNorm1d(F, affine=True)         # affine=True: γ,β 可学习
with torch.no_grad():                          # 直接改参数值,不需要记录梯度
    bn_ab.weight.copy_(torch.rand(F) * 2 + 0.5)  # 随机 γ ∈ [0.5, 2.5](weight=γ)
    bn_ab.bias.copy_(torch.randn(F))             # 随机 β ∈ N(0,1)   (bias=β)
manual_ab = bn_ab.weight * x1d_hat + bn_ab.bias  # γ,β 形状 (F,) 与 (B,F) 广播
err = (manual_ab - bn_ab(x1d)).abs().max().item()
print(f"1g. γ/β 仿射变换 手写 vs affine=True    : 最大误差 = {err:.2e}  → {'一致 ✓' if err < 1e-5 else '不一致 ✗'}")

# ======================================================================
# 实验 2:统计方向演示 ------ 归一化后,哪些维度的均值/方差变成了 0/1?
# ----------------------------------------------------------------------
# "探针"思想:归一化的定义就是让输出沿统计维 S 满足 mean=0, std=1。
# 反过来:对输出沿某组维度求 mean/std,若精确 ≈0/≈1,
# 就证明归一化正是沿这组维度做的。
# GroupNorm 是个有趣的"不命中":它的统计维是五维张量上的 (C/G,H,W),
# 而探针只沿 (H,W) → 组内跨通道的均值不会被消掉 → 恰好证明 GN 的
# 统计粒度是"组"而不是"通道"(组内多个通道共享同一组 μ/σ)。
# ======================================================================
print("\n【实验 2】统计方向演示(对同一 (B=8, C=6, H=5, W=7) 张量)")
print("-" * 70)

def probe(name, out, dims, dim_names):
    """沿 dims 求输出的均值/标准差,验证是否被标准化到 0/1"""
    m = out.mean(dim=dims).abs().max().item()      # 该方向均值的最大绝对值(应≈0)
    s = out.std(dim=dims, unbiased=False)          # 该方向标准差(应≈1)
    s_lo, s_hi = s.min().item(), s.max().item()
    print(f"{name:<14s} 沿 {dim_names:<10s} → |mean|max={m:.1e}, std∈[{s_lo:.3f},{s_hi:.3f}]")

probe("BatchNorm2d", bn2d(x4d), (0, 2, 3), "(B,H,W)")   # 每通道: 跨batch和空间
probe("LayerNorm",   ln(x4d),   (1, 2, 3), "(C,H,W)")   # 每样本: 跨全部特征
probe("InstanceNorm",inn(x4d),  (2, 3),    "(H,W) ")    # 每(样本,通道): 只跨空间
probe("GroupNorm",   gn(x4d),   (2, 3),    "(H,W)*")    # 每(样本,组): 跨组内通道+空间
print("  * GroupNorm 的 std 沿 (H,W) 看每组内各通道共享同一统计量,故组内通道 std 相同")
print("  → 结论: 五种归一化本质相同,唯一区别是'在哪些维度上算 μ 和 σ'")

# ======================================================================
# 实验 3:BatchNorm 的 train/eval 双模式与滑动平均
# ----------------------------------------------------------------------
# 验证三件事:
#   ① 滑动平均(EMA)收敛性: running_mean ← (1-m)·running_mean + m·μ_batch
#   ② train 模式的 batch 依赖: 同一样本换"邻居"输出完全不同
#   ③ eval 模式的确定性: 改用 running 统计,输出与 batch 无关
# ======================================================================
print("\n【实验 3】BatchNorm 双模式与滑动平均")
print("-" * 70)

bn3 = nn.BatchNorm1d(4, momentum=0.1)          # 4 个特征;momentum=EMA 更新步长
bn3.train()
# 构造真实分布 N(μ=5, σ=2)(方差=4)的数据流。
# 喂 200 个 batch,EMA 系数 0.1 → 旧值权重衰减 0.9^200 ≈ 7e-10,
# 初始值(0/1)的影响可忽略,running 统计应非常接近真值。
for _ in range(200):
    batch = torch.randn(32, 4) * 2.0 + 5.0     # ~ N(5, 2²),每 batch 32 个样本
    bn3(batch)                                 # 前向的同时自动更新 running 统计
print(f"数据真实分布: mean=5.0, std=2.0 (var=4.0)")
print(f"200 个 batch 后 running_mean = {bn3.running_mean[0]:.3f} (→ 收敛到 5.0)")
print(f"200 个 batch 后 running_var  = {bn3.running_var[0]:.3f} (→ 收敛到 4.0)")

# batch 依赖演示:同一样本 [1,2,3,4] 放进两个"邻居分布"截然不同的 batch
same_sample = torch.tensor([[1.0, 2.0, 3.0, 4.0]])
batch_a = torch.cat([same_sample, torch.randn(31, 4) * 0.1])        # 邻居≈N(0,0.1)
batch_b = torch.cat([same_sample, torch.randn(31, 4) * 10 + 100])   # 邻居≈N(100,10)
bn3.train()
out_a = bn3(batch_a)[0]
out_b = bn3(batch_b)[0]
print(f"\ntrain 模式: 同一样本在不同 batch 中的输出")
print(f"  batch_a 中输出: {out_a.tolist()}")
print(f"  batch_b 中输出: {out_b.tolist()}")
print(f"  两次输出相同吗? {torch.allclose(out_a, out_b)} → BN 训练态有 batch 依赖")

bn3.eval()   # 推理模式: 改用 running 统计,且不再更新它们
out_a = bn3(batch_a)[0]
out_b = bn3(batch_b)[0]
print(f"eval  模式: 同一样本输出是否一致? {torch.allclose(out_a, out_b)} → 与 batch 无关 ✓")
print("  ⚠ 这就是为什么推理前必须 model.eval(),否则结果随 batch 组成漂移")

# ======================================================================
# 实验 4:训练对比 ------ 深层 MLP 加/不加 BatchNorm
# ----------------------------------------------------------------------
# 严格控制变量:同种子初始化(权重逐位相同)、同数据、同超参
# (朴素 SGD lr=0.01 不加动量,batch=64, 20 epochs),唯一区别 = 有无 BN。
# 6 隐藏层故意做深:层数越多分布漂移越严重,BN 的加速效果越明显。
# 核心指标:"达到 90% 测试准确率所需的 epoch 数"。
# ======================================================================
print("\n【实验 4】训练对比:深层 MLP ± BatchNorm")
print("-" * 70)

def make_data(n_per_class=512, dim=20, n_classes=10):
    """生成多类高斯团分类数据。
    每类中心 ~N(0,3)(中心间距足够大 → 任务可学);
    类内样本 ~N(中心,1)(单位噪声 → 类间少量重叠,非平凡任务)。
    独立 Generator(seed=0):数据与模型初始化解耦,两模型数据完全一致。"""
    g = torch.Generator().manual_seed(0)
    centers = torch.randn(n_classes, dim, generator=g) * 3       # (10, 20) 类中心
    X = centers.repeat_interleave(n_per_class, dim=0) + torch.randn(n_classes * n_per_class, dim, generator=g)
    y = torch.arange(n_classes).repeat_interleave(n_per_class)
    perm = torch.randperm(len(y), generator=g)                   # 打乱,避免按类聚集
    return X[perm], y[perm]

X_train, y_train = make_data(n_per_class=512)          # 5120 训练样本
X_test, y_test = make_data(n_per_class=200)            # 2000 测试样本(独立生成)
X_train, y_train = X_train.to(DEVICE), y_train.to(DEVICE)
X_test, y_test = X_test.to(DEVICE), y_test.to(DEVICE)

def build_mlp(use_bn, dim=20, hidden=256, depth=6, n_classes=10):
    """构建 MLP;use_bn=True 时每个 Linear 后插入 BatchNorm1d。
    BN 经典位置:Linear 后、ReLU 前(ResNet 排法)------Linear 输出接近
    高斯分布归一化最有效;ReLU 后一半值为 0,分布偏斜收益打折。"""
    layers, d_in = [], dim
    for _ in range(depth):
        layers.append(nn.Linear(d_in, hidden))
        if use_bn:
            layers.append(nn.BatchNorm1d(hidden))        # Linear → BN → ReLU
        layers.append(nn.ReLU())
        d_in = hidden
    layers.append(nn.Linear(d_in, n_classes))            # 输出层不加 BN
    return nn.Sequential(*layers).to(DEVICE)

def train_model(model, epochs=20, lr=0.01, batch_size=64, tag=""):
    """标准训练循环:train 态过 mini-batch(SGD) → eval 态测准确率。
    train()/eval() 切换正是实验 3 结论的实际应用。"""
    opt = torch.optim.SGD(model.parameters(), lr=lr)   # 朴素 SGD 不用动量:
    lossf = nn.CrossEntropyLoss()                      # 差异全归因于 BN
    losses, accs = [], []
    for ep in range(epochs):
        model.train()                                  # BN 用当前 batch 统计
        perm = torch.randperm(len(y_train), device=DEVICE)
        total = 0.0
        for i in range(0, len(perm), batch_size):      # 手动切 mini-batch
            idx = perm[i:i + batch_size]
            opt.zero_grad()
            loss = lossf(model(X_train[idx]), y_train[idx])
            loss.backward()                            # BN 的 γ,β 也有梯度
            opt.step()
            total += loss.item()                       # .item() 取标量脱离计算图
        model.eval()                                   # BN 切换为 running 统计
        with torch.no_grad():
            acc = (model(X_test).argmax(1) == y_test).float().mean().item()
        losses.append(total / (len(perm) // batch_size))
        accs.append(acc)
    print(f"  {tag:<18s} 最终 train_loss={losses[-1]:.4f}, test_acc={accs[-1]*100:.1f}%")
    return losses, accs

# 两次 manual_seed(42):BN 版的 Linear 初始化与无 BN 版逐位相同
# (BN 层 γ=1,β=0 初始化不消耗随机数),实现严格"同起点"对照。
torch.manual_seed(42)
model_nobn = build_mlp(use_bn=False)
losses_a, accs_a = train_model(model_nobn, tag="MLP 无归一化")

torch.manual_seed(42)
model_bn = build_mlp(use_bn=True)
losses_b, accs_b = train_model(model_bn, tag="MLP + BatchNorm")

def epochs_to(accs, target=0.90):
    """达到目标准确率所需 epoch 数(越小收敛越快)"""
    for i, a in enumerate(accs):
        if a >= target:
            return i + 1
    return None
e_a, e_b = epochs_to(accs_a), epochs_to(accs_b)
print(f"  达到 90% 准确率所需 epoch: 无BN = {e_a or '>20'}, 有BN = {e_b or '>20'}")

# 绘制对比图(try/except 包裹:绘图失败不影响实验主体)
try:
    import matplotlib
    matplotlib.use("Agg")                              # 无界面后端,直接存文件
    import matplotlib.pyplot as plt
    plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]  # 中文字体(Win 自带)
    plt.rcParams["axes.unicode_minus"] = False             # 修复负号显示为方块
    fig, axes = plt.subplots(1, 2, figsize=(11, 4))
    axes[0].plot(losses_a, "o-", label="无归一化", color="#d62728")
    axes[0].plot(losses_b, "s-", label="+ BatchNorm", color="#1f77b4")
    axes[0].set_xlabel("Epoch"); axes[0].set_ylabel("训练损失")
    axes[0].set_title("训练损失对比(6 层 MLP, lr=0.01)"); axes[0].legend(); axes[0].grid(alpha=0.3)
    axes[1].plot([a * 100 for a in accs_a], "o-", label="无归一化", color="#d62728")
    axes[1].plot([a * 100 for a in accs_b], "s-", label="+ BatchNorm", color="#1f77b4")
    axes[1].axhline(90, ls="--", c="gray", lw=0.8)     # 90% 参考线
    axes[1].set_xlabel("Epoch"); axes[1].set_ylabel("测试准确率 (%)")
    axes[1].set_title("测试准确率对比"); axes[1].legend(); axes[1].grid(alpha=0.3)
    fig.tight_layout()
    fig.savefig("figures/norm_exp_training.png", dpi=150)
    print("  对比图已保存: figures/norm_exp_training.png")
except Exception as e:
    print(f"  [跳过绘图: {e}]")

# ======================================================================
# 实验 5:小 batch 敏感性 ------ BN 依赖 batch 统计,LN 不依赖
# ----------------------------------------------------------------------
# BN 的 μ,σ 是从 m 个样本"估计"的:m=2 时噪声极大,每步归一化用的
# 统计量本身就在剧烈抖动(m=1 时方差恒为 0,BN 完全失效)。
# LN 的统计只来自单样本内部 d=256 个特征 → 足够稳,与 batch 彻底无关。
# 这就是"小 batch 检测/分割用 GN、Transformer 用 LN"的实验依据。
# 用 3 隐藏层浅网络 + 8 epochs 控制总耗时(batch=2 时每 epoch 2560 步)。
# ======================================================================
print("\n【实验 5】小 batch 敏感性(batch=2 vs batch=64)")
print("-" * 70)

def build_mlp_norm(norm_type, dim=20, hidden=256, depth=3, n_classes=10):
    """norm_type ∈ {'bn','ln','none'}:BN/LN 放在完全相同的位置,
    唯一区别是归一化类型 → 差异只能来自"统计是否依赖 batch"。"""
    layers, d_in = [], dim
    for _ in range(depth):
        layers.append(nn.Linear(d_in, hidden))
        if norm_type == "bn":
            layers.append(nn.BatchNorm1d(hidden))
        elif norm_type == "ln":
            layers.append(nn.LayerNorm(hidden))        # 沿特征维统计,与 batch 无关
        layers.append(nn.ReLU())
        d_in = hidden
    layers.append(nn.Linear(d_in, n_classes))
    return nn.Sequential(*layers).to(DEVICE)

# 2(归一化类型) × 2(batch大小) = 4 组对照,每组都同种子初始化
for norm_type in ["bn", "ln"]:
    for bs in [2, 64]:
        torch.manual_seed(42)
        m = build_mlp_norm(norm_type)
        l, a = train_model(m, epochs=8, lr=0.01, batch_size=bs,
                           tag=f"{norm_type.upper()} batch={bs:<3d}")

# ======================================================================
# 实验 6:GPU 速度对比 ------ RMSNorm vs LayerNorm
# ----------------------------------------------------------------------
# 理论:RMSNorm 少两步(求均值的归约 + 减均值的逐元素运算)。
# 归一化 kernel 是访存受限(memory-bound)的:少读写一遍数据 ≈ 直接省时间。
# GPU 计时两大陷阱:① 预热(首次调用含 kernel 编译);
# ② torch.cuda.synchronize()(CUDA 异步执行,不同步测的是提交时间)。
# ======================================================================
print("\n【实验 6】GPU 速度对比:RMSNorm vs LayerNorm")
print("-" * 70)

d_model = 4096
x_big = torch.randn(8, 2048, d_model, device=DEVICE)   # ≈256MB fp32,远超 L2 缓存
ln_big = nn.LayerNorm(d_model).to(DEVICE)              # 默认 affine=True,两边一致
rms_big = nn.RMSNorm(d_model).to(DEVICE)               # γ 参数量相同,对比公平

def bench(fn, iters=50):
    """GPU 前向耗时基准:预热 → 同步 → 计时 iters 次 → 同步 → 取平均(ms)"""
    for _ in range(5):                                 # 预热,排除 kernel 编译影响
        fn(x_big)
    torch.cuda.synchronize()                           # 等预热 kernel 执行完
    t0 = time.perf_counter()                           # 高精度单调时钟
    for _ in range(iters):
        fn(x_big)
    torch.cuda.synchronize()                           # 等全部执行完再停表
    return (time.perf_counter() - t0) / iters * 1000

if DEVICE.type == "cuda":
    t_ln, t_rms = bench(ln_big), bench(rms_big)
    print(f"输入形状: (8, 2048, 4096) fp32, 各 50 次前向平均")
    print(f"LayerNorm : {t_ln:.3f} ms/次")
    print(f"RMSNorm   : {t_rms:.3f} ms/次  ({'快' if t_rms < t_ln else '慢'} {abs(t_ln/t_rms - 1)*100:.0f}%)")
else:
    print("[CPU 环境,跳过 GPU 计时]")

print("\n" + "=" * 70)
print("全部实验完成。")

8. 参考文献

  1. Ioffe S, Szegedy C. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift . ICML 2015. arXiv:1502.03167
  2. Ba J, Kiros J R, Hinton G E. Layer Normalization . 2016. arXiv:1607.06450
  3. Ulyanov D, Vedaldi A, Lempitsky V. Instance Normalization: The Missing Ingredient for Fast Stylization . 2016. arXiv:1607.08022
  4. Wu Y, He K. Group Normalization . ECCV 2018. arXiv:1803.08494
  5. Zhang B, Sennrich R. Root Mean Square Layer Normalization . NeurIPS 2019. arXiv:1910.07467

本文所有实验数字均为 RTX 5070 Ti Laptop / PyTorch 2.14+cu130 实际运行输出,随机种子已固定(torch.manual_seed(42) + 数据 Generator(seed=0)),可直接复现。

如果本文对你有帮助,欢迎 点赞 👍 收藏 ⭐ 关注 三连支持!有问题评论区交流。

标签 :深度学习 PyTorch BatchNormalization LayerNorm Transformer 大语言模型 计算机视觉

相关推荐
uncle_ll2 小时前
分类任务解决样本数据不均衡的落地实战指南
人工智能·深度学习·机器学习·分类·数据处理
一切皆是因缘际会2 小时前
掌控信息论:同源星际通信零延迟架构
人工智能·深度学习·ai·系统架构·信息与通信·星际通信·同源通信
Ivanqhz3 小时前
线性模型计算量、MFU、反向传播 FLOPs
人工智能·深度学习·机器学习
FelixZhang0283 小时前
量化求真10|回测通过以后,策略就能上场吗?
人工智能·python·深度学习·学习·机器学习·金融·lstm
程序员的账号4 小时前
《Python工匠》资源分享
人工智能·python·深度学习·机器学习
程序员的账号4 小时前
《深度学习入门2自制框架》中文PDF+源代码+斋藤康毅
人工智能·深度学习·pdf
空奈qwq5 小时前
深度学习入门指南:从核心概念到 PyTorch 实战
人工智能·pytorch·深度学习
兴通物联科技5 小时前
对俄出口诚实标识 2026 时间节点与产线赋码采集技术方案
大数据·服务器·单片机·嵌入式硬件·深度学习·计算机视觉
Rocky Ding*5 小时前
一文读懂Qwen-Audio-3.1核心基础知识:从语音识别到可控声景与实时Agent
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·qwen-audio