LeetGPU习题06:Norm系列详解

6804 字
34 分钟
LeetGPU习题06:Norm系列详解
2026-05-22
更新中…

1. Batch Norm从无到有#

在深度学习的历史上,Normalization 是少数几个”加进去就能训得更好”的技术之一。但它的诞生不是凭空而来的。 在 BN 出现之前,训练深层网络是一件极其痛苦的事。本章从源头讲起,一步步推导出 Normalization 为什么被发明、它解决了什么问题、以及背后的设计权衡,包括使用Triton、CUDA来写一下这一系列算子。

1.1. 溯源:训练深网为什么这么难#

2014 年,Oxford 的 Visual Geometry Group 提出了 VGG 网络。以 VGG-19 为例:19 层,144M 参数,在当年已经算”大模型”了。

但 VGG-19 的训练极其困难。Karen Simonyan 和 Andrew Zisserman 在论文中描述的训练流程是这样的:先训一个 11 层的浅版(VGG-11),收敛后把浅版权重拿出来,塞进 VGG-19 的前 11 层初始化,再继续训更深的版本。

为什么要这样?

在当时,19 层直接从头训根本收敛不了

具体来说,VGG-19 的卷积层堆叠如下(忽略 pooling 和 FC):

conv3-64 → conv3-64
conv3-128 → conv3-128
conv3-256 → conv3-256 → conv3-256 → conv3-256
conv3-512 → conv3-512 → conv3-512 → conv3-512
conv3-512 → conv3-512 → conv3-512 → conv3-512

每个 conv3-64 的权重是一个 3×3×Cin×643 \times 3 \times C_{in} \times 64 的张量。假设用 N(0,0.01)\mathcal{N}(0, 0.01) 初始化(当时的常见做法,后来何恺明才发现这不够好),经过 5-6 层卷积后,激活值的分布会怎样?

不妨做一个简单的数值模拟。取一张 ImageNet 图片,像素值归一化到 [0,1][0, 1]

  • 第 1 层 conv3-64 的输出:均值约 0,方差约 0.5
  • 第 4 层 conv3-128 的输出:方差膨胀到约 3
  • 第 8 层 conv3-256 的输出:方差约 20
  • 第 13 层 conv3-512 的输出中,某些通道的激活值已经超过 100

到第 16 层 ReLU 之后,超过 90% 的神经元输出为 0,剩下的非零值集中在 [50, 200] 区间。此时反向传播经过这层时,梯度要么是 0,要么梯度爆炸。这就是 VGG-19 需要”逐阶段预热训练”的原因。

更麻烦的是 sigmoid 和 tanh。VGG 用的是 ReLU 还好,但同期的很多网络还在用 sigmoid。sigmoid 的饱和区在 x>5|x| > 5 左右就开始梯度接近 0。如果某层的输入掉进了 [20,10][-20, -10],这层的梯度就几乎为零,前面的层收不到任何有效的学习信号。

这个问题在当时是普遍存在的——不止 VGG,几乎所有深层网络都在遭受同样的折磨,但还没有人系统地解释这个现象。

1.2. Internal Covariate Shift:给问题起个名字#

Ioffe 和 Szegedy 在 2015 年的论文 Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift 中,给这个问题起了名字。

Consider a network computing =F2(F1(u,Θ1),Θ2)\ell = F_2(F_1(u, \Theta_1), \Theta_2) where F1F_1 and F2F_2 are arbitrary transformations, and the parameters Θ1\Theta_1, Θ2\Theta_2 are to be learned so as to minimize the loss \ell. Learning Θ2\Theta_2 can be viewed as if the inputs x=F1(u,Θ1)x = F_1(u, \Theta_1) are fed into the sub-network =F2(x,Θ2)\ell = F_2(x, \Theta_2).

Θ2\Theta_2 的视角看,它的输入是 x=F1(u,Θ1)x = F_1(u, \Theta_1)。问题是:Θ1\Theta_1 每次更新后,F1F_1 的输出分布都会变。对于 Θ2\Theta_2 来说,这等于训练数据和测试数据的分布不一样了,输入分布一直在漂移,而它必须不断重新适应。

For example, a gradient descent step Θ2Θ2αmi=1mF2(xi,Θ2)Θ2\Theta_2 \leftarrow \Theta_2 - \frac{\alpha}{m} \sum_{i=1}^{m} \frac{\partial F_2(x_i, \Theta_2)}{\partial \Theta_2} is exactly equivalent to that for a stand-alone network F2F_2 with input xx. Therefore, the input distribution properties that make training more efficient – such as having the same distribution between the training and test data – apply to training the sub-network as well. As such it is advantageous for the distribution of xx to remain fixed over time. Then, Θ2\Theta_2 does not have to readjust to compensate for the change in the distribution of xx.

这段话的核心洞察是:每个子网络在训练时都会面临输入分布的不断变化,就像一个人在学习射箭,但靶子每次都在随机移动。论文将这一现象正式定义为:

Internal Covariate Shift

We refer to the change in the distributions of internal nodes of a deep network, in the course of training, as Internal Covariate Shift(ICS)。ICS 指的是网络内部激活值分布随参数更新而发生的持续变化。

ICS 带来了一系列连锁反应,让深层网络的训练举步维艰:

  • 不敢用大学习率。学习率稍大,参数更新幅度大,下一层的输入分布就剧烈变化,网络直接发散。
  • 对初始化极度敏感。VGG 的”逐阶段预训练”本质上就是用浅层训练来手动矫正初始化的分布。
  • 饱和非线性(sigmoid/tanh)极易梯度消失。分布漂到饱和区 → 梯度为零 → 浅层停止学习。
  • 训练深层网络需要一大堆技巧:gradual warmup、learning rate decay schedule、momentum 调参、gradient clipping、Dropout ratio 调参。
ICS 解释的争议

2018 年,MIT 的 Santurkar 等人在 How Does Batch Normalization Help Optimization? 中做了一个关键实验:在 BN 层之后人为注入随机噪声,主动破坏归一化后的分布(人为制造 ICS),然后继续训练。结果网络照样收敛得很好。这说明 Ioffe 对 BN 机制的原始解释(“减少 ICS”)并不准确。BN 的真正作用更可能是让 loss landscape 变得平滑。详见 1.5 节

1.3. 解法:从白化到 Batch Normalization#

既然问题出在分布不稳定,最直觉的解就是:每层之后,手动把分布拉回来。但”拉回来”这三个字说起来简单,做起来全是坑。

第一反应:白化#

白化(whitening)是机器学习里的标准操作——对数据做线性变换,使均值为 0、方差为 1、维度之间去相关。输入端做白化能加速收敛,这个在 1998 年 LeCun 的论文里就写了。

那为什么不每层都做白化?

两个致命问题:

  1. 白化和梯度下降会打架。考虑一层加了 bias:x=u+bx = u + b,然后做减均值归一化 x^=xE[x]\hat{x} = x - E[x]。当你对 bb 做梯度下降 bb+Δbb \leftarrow b + \Delta b 后,紧接着的减均值操作又把 Δb\Delta b 的贡献给抹掉了。bb 会无限增长,但 loss 纹丝不动,网络不收敛。

  2. 算不动。完整的白化要算协方差矩阵的逆平方根,还要算它的梯度用于反向传播。对一个 dd 维的隐藏层,这是 O(d3)O(d^3) 的计算量。一个典型的全连接层 d=4096d=4096,每步 SGD 都算一次矩阵逆平方根,对当时的 GPU 来说不现实。

所以白化这条路走不通。需要的是一个计算便宜、可导、不跟梯度下降打架的替代方案。

简化方案:Batch Normalization#

Ioffe 和 Szegedy 做了两个关键的简化,直接推导出了 BN:

简化一:不做 joint whitening,对每个维度独立归一化。

每个标量维度独立算自己的 μ\muσ\sigma。这就把 O(d3)O(d^3) 的矩阵运算变成了 O(d)O(d) 的逐元素操作。代价是不能消除维度间的相关性,但实验证明这不是必需的——只需要把每维的均值和方差固定下来,就已经大幅改善了训练。

简化二:不用全训练集算统计量,用当前 mini-batch。

全训练集统计量需要在每个 SGD 步骤后重新遍历整个数据集,不现实。但 mini-batch 就在手上——直接用当前 batch 内的 mm 个样本来估计均值和方差。这样归一化操作天然嵌入 SGD 的每一步,梯度可以顺畅地流过 μ\muσ\sigma 回传到参数。

这两个简化合在一起,就是 BN 的数学形式。设一个 mini-batch B={x1,,xm}\mathcal{B} = \{x_1, \dots, x_m\}

先求均值和方差:

μB=1mi=1mxi,σB2=1mi=1m(xiμ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

再加入一个可学习的线性变换:

x^i=xiμBσB2+ϵ,yi=γ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

这里 γ\gammaβ\beta 的设计非常精妙。把激活值强行拉到 N(0,1)\mathcal{N}(0,1) 有一个副作用:如果每一层的输出都被锁死在标准正态分布,网络的表达能力就废了。举个具体的例子——sigmoid 在 x[1,1]x \in [-1, 1] 区间几乎是线性的。一个全是线性变换的网络,再深也只是一层:W3(W2(W1x))=WxW_3(W_2(W_1 x)) = W' x

γ\gammaβ\beta 就是给网络的”反悔权”。归一化之后,网络可以通过学到的 γ\gamma 把方差拉大(让 sigmoid 重新进入饱和区),通过 β\beta 把均值挪走。极端情况下,设 γ=Var[x],β=E[x]\gamma = \sqrt{\text{Var}[x]}, \beta = \mathbb{E}[x],BN 层就退化成恒等映射,什么都没做。

所以 BN 不是一个单纯的归一化操作。它默认行为是归一化,但网络有自由去偏离这个默认值——既稳住了分布,又保留了表达能力。Ioffe 和 Szegedy 搞清楚了为什么”每层都做白化”这条路走不通,然后找到了正好能绕过那两个坑的简洁方案。

BN 在 CNN 中归一化的是什么维度?#

这是理解 BN 具体行为的关键。对于 CNN 的特征图,shape 为 [N, C, H, W]

  • NN:batch size
  • CC:通道数
  • H,WH, W:空间维度

BN 对每个 channel 独立归一化,统计量来自 [N, H, W] 三个维度。也就是说,对于第 cc 个通道,它的均值是该 batch 内所有样本、所有空间位置的该通道值的平均。

为什么是 per-channel?在 CNN 中,一个卷积核在整张图的所有空间位置上滑动,共享同一组参数。因此一个 channel 的所有位置在统计意义上服从相似的分布——它们是被同一个检测器扫描出来的。把它们放在一起统计是合理的。

维度是否参与统计原因
Channel否(独立统计)每个 channel 是不同的特征检测器,分布不同
Batch跨样本统计
H, W跨空间位置统计

训练与推理的不同行为#

BN 在训练和推理时的行为不同,这是一个容易踩的坑。

训练时

  • 用当前 mini-batch 的 μB\mu_{\mathcal{B}}σB2\sigma_{\mathcal{B}}^2 做归一化
  • 同时维护一组全局的 running mean 和 running variance,通过指数滑动平均更新: μrunning=αμrunning+(1α)μB\mu_{\text{running}} = \alpha \cdot \mu_{\text{running}} + (1-\alpha) \cdot \mu_{\mathcal{B}}

推理时

  • 不再计算 batch 内统计量
  • 直接使用训练期间累积的 running mean / running variance

这种行为差异意味着 BN 的训练和推理不等价。在某些场景(如 GAN、域适应)中,这个差异会造成问题。

常见问题

Fine-tune 时切换到小 batch 训练,忘记调整 momentum 参数——running statistics 更新过快(因为 mini-batch 太小不可靠),导致推理时 normalization 参数不对。典型症状是训练 loss 下降但验证 loss 爆炸。

实验验证#

论文在 ImageNet 上验证了这个方案到底管多大用。他们把 BN 塞进 Inception(当时最强的分类网络之一),然后做了一系列对比。

原来 Inception 的初始学习率是 0.0015。加了 BN 后,学习率直接拉到 0.045,训练反而更稳定。最终 BN 版用原来 1/14 的步数就达到了原版同样的精度。

他们还做了两个有意思的实验:

第一,把 Dropout 从网络里拆了。 之前大家认为 Dropout 是防过拟合的必需品,结果 BN 自带的正则化效果完全够用,拆掉后训练还更快了。

第二,把 ReLU 换回 sigmoid。 前面说过,深网 + sigmoid 在 2015 年基本等于训不了。但加上 BN 后,BN-Inception-sigmoid 在 ImageNet 上做到了 69.8% 的 top-1。Wao,Amazing!

最终,6 个 BN 版 Inception 集成后,ImageNet top-5 error 降到了 4.82%,第一次超过了人类标注者的水平。Normalization 的时代,由此拉开序幕。

1.4. 简易模型对比:有 BN vs 无 BN#

用一段简单的代码来看看效果。20 层全连接 + ReLU,MNIST 分类:

iimport torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
import torchvision
import torchvision.transforms as transforms
torch.manual_seed(42)
# CIFAR-10: 3x32x32 -> 3072
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
trainloader = DataLoader(trainset, batch_size=128, shuffle=True, num_workers=0)
testloader = DataLoader(testset, batch_size=128, shuffle=False, num_workers=0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")
class DeepMLP(nn.Module):
def __init__(self, use_bn=False):
super().__init__()
layers = [nn.Linear(3072, 256)]
if use_bn:
layers.append(nn.BatchNorm1d(256))
layers.append(nn.ReLU())
for i in range(10):
layers.append(nn.Linear(256, 256))
if use_bn:
layers.append(nn.BatchNorm1d(256))
layers.append(nn.ReLU())
layers.append(nn.Linear(256, 10))
self.net = nn.Sequential(*layers)
def forward(self, x):
return self.net(x)
def train_one(name, model, lr, epochs=10):
model.to(device)
opt = optim.SGD(model.parameters(), lr=lr, momentum=0.9)
loss_fn = nn.CrossEntropyLoss()
for ep in range(epochs):
model.train()
total_loss, correct, total = 0.0, 0, 0
for x, y in trainloader:
x = x.view(x.size(0), -1).to(device)
y = y.to(device)
opt.zero_grad()
out = model(x)
loss = loss_fn(out, y)
loss.backward()
opt.step()
total_loss += loss.item() * x.size(0)
correct += (out.argmax(1) == y).sum().item()
total += y.size(0)
train_acc = correct / total
# test
model.eval()
test_correct, test_total = 0, 0
with torch.no_grad():
for x, y in testloader:
x = x.view(x.size(0), -1).to(device)
y = y.to(device)
out = model(x)
test_correct += (out.argmax(1) == y).sum().item()
test_total += y.size(0)
test_acc = test_correct / test_total
print(f" {name} epoch {ep:2d}: train_loss={total_loss/total:.4f} train_acc={train_acc:.4f} test_acc={test_acc:.4f}")
if __name__ == '__main__':
print("=== CIFAR-10: 20-layer MLP (3072 -> 256x20 -> 10), 10 epochs each ===\n")
for lr in [0.1, 0.01, 0.001]:
print(f"--- lr={lr} ---")
print(" Without BN:")
try:
train_one("noBN", DeepMLP(use_bn=False), lr, epochs=5)
except Exception as e:
print(f" FAILED: {e}")
print(" With BN:")
try:
train_one("BN", DeepMLP(use_bn=True), lr, epochs=5)
except Exception as e:
print(f" FAILED: {e}")
print()

结果如下:

=== CIFAR-10: 20-layer MLP (3072 -> 256x20 -> 10), 10 epochs each ===
--- lr=0.1 ---
Without BN:
noBN epoch 4: train_loss=2.3043 train_acc=0.0997 test_acc=0.1000
With BN:
BN epoch 4: train_loss=1.4089 train_acc=0.4981 test_acc=0.4997
--- lr=0.01 ---
Without BN:
noBN epoch 4: train_loss=2.3030 train_acc=0.0971 test_acc=0.1000
With BN:
BN epoch 4: train_loss=1.2639 train_acc=0.5478 test_acc=0.5166
--- lr=0.001 ---
Without BN:
noBN epoch 4: train_loss=2.3026 train_acc=0.0981 test_acc=0.1000
With BN:
BN epoch 4: train_loss=1.3385 train_acc=0.5245 test_acc=0.4884
(mycuda) 014

即使最简单的MLP,在加入BN之后,只训练5次也能够实现50%的准确率,而不加BN的MLP几乎不动。

1.5. BN 为什么有效?#

前面 1.2 节提到,Ioffe & Szegedy 的原论文将 BN 的成功归因于”减少了 Internal Covariate Shift”。但 Santurkar et al. (2018) 用实验做了有力反驳。

他们的关键实验如下:

  1. 正常训练一个带 BN 的网络
  2. 训练中途,人为向 BN 输出注入随机噪声(破坏归一化后的分布,主动制造 ICS)
  3. 继续训练

结果:网络照样收敛,和对照组差别不大。

如果 BN 的核心价值真的是”减少 ICS”,那人为恢复 ICS 应该导致训练崩溃。但实际没有。这说明 ICS 不是故事的全部。

BN 真正做了什么?#

Santurkar 等人的核心发现是:BN 让 loss landscape 变得显著更平滑。

他们分析了 loss 函数对参数梯度的 Lipschitz 常数。对于一个函数 ffβ\beta-smoothness 定义为:

f(x)f(y)βxy\|\nabla f(x) - \nabla f(y)\| \leq \beta \|x - y\|

β\beta 越小,函数越平滑,梯度下降越稳定。实验测量发现:

  • 不加 BN:梯度变化剧烈,loss 曲面上有大量尖锐的局部极小值和鞍点,优化器容易被卡住或震荡。
  • 加 BN:梯度的 Lipschitz 常数大幅减小,loss 曲面变得圆滑,SGD 能稳定使用更大的学习率。

论文展示
论文展示
论文展示
论文展示

直觉理解

把优化想象成在一片山地中找最低点:

  • 没有 BN:地表布满尖锐的沟壑和陡坡。每一步只能迈很小(小 lr),否则就摔下去了。方向稍有偏差就可能滑向错误的山谷。
  • 有了 BN:地表变成了起伏的缓坡。可以用大学习率,方向大致对就能快速下降。

辅助机制#

除了平滑 loss landscape,BN 还有几个附带效果:

(1)解耦 weight 的 scale 和 direction

当权重缩放 WaWW \rightarrow aW 时,BN 层的输出不变(归一化抹掉了 scale),所以 loss 对 WW 的梯度不受 aa 的影响。不管 WW 的范数多大,梯度的尺度始终合理。

(2)隐式正则化

每个 mini-batch 的统计量都有随机波动,这种噪声充当了隐式的数据增强,有一定正则化效果。实验发现 BN 可以部分替代 Dropout。

(3)允许更大的 learning rate

这是最实际的好处。BN 出现之前,大学习率的网络极容易发散;有了 BN,学习率可以大一个数量级。更大的 lr 通常意味着更好的泛化性能。大 lr 的 SGD 天然倾向于收敛到更平坦的极小值,而平坦的极小值通常泛化更好。

1.6. BN 的局限性#

尽管 BN 在 CNN 中大获成功,它有几个结构性限制:

(1)依赖 batch size

当batch 太小时,统计量估计不准,这会导致归一化失效。 以下场景中 batch 通常很小:

  • 目标检测(高分辨率图片,显存放不下大 batch)
  • 语义分割(同理)
  • 视频理解(时序维度也占显存)
  • 3D 视觉

当 batch=1 或 2 时,BN 的表现可能比不用 normalization 还差。

(2)不适合 RNN / 序列模型

在 RNN 中,每个时间步的输入共享权重,但不同时间步的统计分布可能差异很大。此外,序列长度不固定也为 BN 的统计量带来了额外的噪声。

(3)训练 / 推理不一致

训练用 batch stats,推理用 running stats。两者的分布可能有差异(尤其微调阶段),导致推理时性能下降。

(4)分布式训练的同步开销

在多 GPU 训练中,标准的 BN 只在单个 GPU 内计算统计量。要让 BN 在分布式下等价,需要用 SyncBN,这会引入额外的通信开销。


2. Layer Normalization#

BN 在 CNN 上大获成功,但到了 RNN 这边就碰了壁。 2016 年,Hinton 组的 Ba et al. 提出了 Layer Normalization,换了一个归一化方向,彻底绕开了 BN 对 batch size 的依赖。

LN 的核心思路

BN 是跨 batch 归一化 → 那就换个方向,跨 feature 归一化,完全抛弃 batch 维度。每个样本独立计算自己的均值和方差。

2.1. 数学形式与归一化维度#

对于一层输出 xRB×Fx \in \mathbb{R}^{B \times F}BB 为 batch 大小,FF 为 feature 数量),LN 对每个样本的所有 feature 做归一化:

μ=1Fj=1Fxj,σ2=1Fj=1F(xjμ)2\mu = \frac{1}{F} \sum_{j=1}^{F} x_j, \qquad \sigma^2 = \frac{1}{F} \sum_{j=1}^{F} (x_j - \mu)^2y=γxμσ2+ϵ+βy = \gamma \cdot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta

γ\gammaβ\beta 的作用和 BN 中完全一样。

在 Transformer 中,每个 token 的 hidden state 是一个 dmodeld_{model} 维向量(通常 512~4096)。LN 就对这个向量内的所有元素做归一化。 每个 token 独立算自己的 μ\muσ\sigma

BN 和 LN 的归一化维度对比:

维度BatchNorm (CNN)LayerNorm
Batch参与统计独立
Feature / Channel独立参与统计
Spatial (H, W)参与统计N/A
关键区别

BN 的统计量依赖整个 batch,并且训练和推理行为不同。 LN 的统计量只依赖单个样本自身,训练和推理完全一致

2.2. 为什么 LN 天然适合 Transformer?#

2017 年 Transformer 提出后,LN 几乎成了标配。 我们就要想一下Transformer的特性:

变长序列。

Transformer 处理的序列长度参差不齐,padding 不可避免。BN 的跨样本统计会被 padding 值污染,而 LN 在单个 token 内部操作,完全不受序列长度和 padding 影响。

自回归解码。

GPT 这类自回归模型,推理时逐个生成 token,LN 完全不受 batch size 限制,即使 batch=1,每个 token 自身的 feature 维度足够大(512~4096),统计量依然稳定。

训推一致。

LN 训练时怎么算,推理时就怎么算。

2.3. LN 的局限性#

LN 在 CNN 上的效果通常不如 BN。原因在于两者的归一化方向不同,而 CNN 和 Transformer 的特征结构刚好适合不同的方向。

CNN 中,不同 channel 代表不同的特征检测器,边缘检测器、纹理检测器、颜色检测器等,它们的响应强度和分布天然不同。 LN 把同一层所有 channel 混在一起归一化到均值 0、方差 1,相当于强行抹掉了 channel 之间的天然统计差异

反过来,在 Transformer 中,每个 token 的 feature 维度内各元素并没有像 CNN channel 那样明确的”不同检测器”语义,它们更像是一个统一的表示空间的不同坐标轴。因此跨 feature 归一化不会破坏有意义的统计结构。#

3. Weight Normalization#

Batch Norm 和 Layer Norm实际上都是对Activation进行归一化,除了归一化 activation,还能归一化什么?

Weight

Weight Normalization把每个权重向量分解为方向和模长:

w=gvv\mathbf{w} = \frac{g}{\|\mathbf{v}\|} \mathbf{v}

其中 v\mathbf{v} 是方向向量,gg 是标量模长,v\|\mathbf{v}\|v\mathbf{v} 的二范数。

把方向(v\mathbf{v})和尺度(gg)解耦后:

  • 方向训练更稳定v\mathbf{v} 的范数不影响参数化神经元的输出尺度,因为模长被重新归一化为 gg 了。
  • 显式控制尺度gg 直接控制该神经元的输出幅度。初始化时可以根据激活函数设置 gg,让输出落在激活函数的有效区间。

因为 WN 不依赖任何 batch 或样本统计量,它在 batch=1 和序列模型中都能工作。WN 在强化学习领域一度很流行,因为 RL 的 batch 通常很小且样本不独立。

但是WN 的收敛速度通常比 BN 慢,且稳定性稍差。 后来 BN 和 LN 的普及让 WN 逐渐边缘化,但它的思想参数解耦启发了后续很多 normalization-free 的工作。


4. Instance Normalization#

2016 年,Ulyanov 等人在做图像风格迁移时发现了一个现象: 给定一张内容图 CC 和一张风格图 SS,目标是生成一张新图 GG,内容来自 CC,风格来自 SS

关键洞察是:每张图片的全局对比度和亮度信息在风格迁移中应该被去除,因为它们属于”风格域”而非”内容域”。

但是,BN却不能做这个,为什么?

归一化!

归一化意味着把不同的风格混合在一起。因此IN尝试对每个 channel、每个样本独立做归一化:

μnc=1HWh=1Hw=1Wxnchw\mu_{nc} = \frac{1}{HW} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{nchw}

归一化后,每张图在该通道的均值被强制移动到 0。这意味着全局对比度信息被彻底移除

方法独立于 batch?独立于 channel?独立于 sample?独立于 spatial?
BN
LN
IN

IN 在 style transfer、image-to-image translation中效果极好。但在分类任务中,IN 会损害性能。因为对比度信息对物体识别是有用的。


5. Group Normalization#

在检测和分割任务中,GPU 显存限制了 batch size(只能开到个位数)。 BN 在小 batch 下表现骤降。 但 LN 直接把所有 channel 混在一起归一化,又破坏了 CNN 中 channel 的独立语义。

有没有介于两者之间的方案?

Group Normalization 把 channels 分成若干 group,每个 group 内部独立做归一化。

μg=1(C/G)HWxgroupgx\mu_g = \frac{1}{(C/G) \cdot H \cdot W} \sum_{x \in \text{group}_g} x

GN 是一个统一的归一化框架

参数对应的 normalization 方法
G=1G = 1(全 channel 在同一组)LayerNorm
G=CG = C(每 channel 一组)InstanceNorm

所以 GN 的本质是在 LN 和 IN 之间寻找一个最优粒度。

实验表明,G=32G = 32G=16G = 16 在很多任务上都效果很好,而且与 batch size 完全无关。

Mask R-CNN、Detectron2 等下流行的检测框架大量使用了 GN。

在大 batch 的 ImageNet 分类上,GN 略逊于 BN。但在小 batch 场景下,GN 远超 BN。


6. Filter Response Normalization#

2019 年,Singh 和 Krishnan 在一篇论文中提出了一个问题:

归一化一定要减均值吗?

FRN 只做方差归一化,省略了 mean-centering 步骤:

x^=xVar(x)+ϵ\hat{x} = \frac{x}{\sqrt{\text{Var}(x) + \epsilon}}

后续跟一个可学习的 thresholding 激活(TLU):

y=max(x^,τ)y=max(γx^+β,τ)y = \max(\hat{x}, \tau) \quad \text{或} \quad y = \max(\gamma \hat{x} + \beta, \tau)

作者指出:

  • 在 ReLU 网络中,activation 已经非负,均值的意义本来就不大
  • 减均值需要一次 reduction,省略后计算更快
  • 实验中小 batch 下的表现与 BN/ GN 相当甚至更好

后来的 RMSNorm 几乎就是 FRN 思想在 Transformer 领域的一次”重新发现”。


7. RMSNorm#

为LLM而生

2019 年,Biao Zhang 和 Rico Sennrich 发表了 RMSNorm 论文。核心修改极简单:

LayerNorm:

y=xμσg+by = \frac{x - \mu}{\sigma} \odot g + b

RMSNorm:

y=xRMS(x)g,RMS(x)=1ni=1nxi2y = \frac{x}{\text{RMS}(x)} \odot g, \quad \text{RMS}(x) = \sqrt{\frac{1}{n} \sum_{i=1}^{n} x_i^2}

去掉了 mean-centering 和 bias 项,只保留 scaling。

作者的核心论点是:

真正重要的是 scale invariance,而不是 zero-mean。

LN 的成功主要是因为重新缩放(除以 σ\sigma)稳定了梯度,减均值这个操作是次要的。实验也证实:RMSNorm 在各任务上的表现与 LN 持平甚至略好,但计算量更少。

LLM(如 LLaMA、Mistral、Qwen 等)基本都选择了 RMSNorm。原因非常实际:

  • 更少的计算: RMSNorm 比 LN 少一次 reduction。

  • 更容易 kernel fusion: 在 GPU 上,element-wise 操作可以与前面的矩阵乘法或注意力计算合并成一个 kernel,减少 HBM 访问次数。

  • 大模型的最大瓶颈不是算力,是内存带宽: 在 LLM 推理中,内存带宽通常才是瓶颈。少一次 reduction 就是少一次 HBM 访问,对吞吐量有直观的提升。

LLaMA 中的 RMSNorm

Meta 的 LLaMA 系列模型在所有 attention 和 FFN 子层之前都使用了 RMSNorm(即 Pre-Norm 架构 + RMSNorm)。Tokenizer 之后的 embedding 也使用了 RMSNorm。可以说,LLaMA 就是从 LN 全面迁移到 RMSNorm 的代表性案例。


8. 归一化的统一视角#

回顾所有方法,可以抽象出归一化的三个要素:

要素含义例子
统计维度在哪些数据维度上计算均值和方差?BN: [N, H, W] / LN: [F] / GN: [G_F, H, W]
统计内容计算哪些统计量?均值+方差 (BN/LN)、仅方差 (FRN)、仅 RMS (RMSNorm)
后处理归一化后做什么变换?仿射变换 (BN/LN)、阈值激活 (FRN)、无后处理

不同方法之间最大的区别在于在哪些轴上计算统计量。下面的图可以直观展示:

假设输入 tensor shape = [N, C, H, W]
N (batch)
┌────────────────────────┐
│ C1 C2 C3 ... │ ← channel
│ H×W H×W H×W │ ← 每个格子是空间维度
└────────────────────────┘
BN: 对每个 C,在 [N, H, W] 三轴上统计
LN: 对每个 [N] 样本,在 [C, H, W] 三轴上统计
IN: 对每个 [N, C] 组,在 [H, W] 上统计
GN: 对每个 [N, G] 组(G 个 channel 为一组),在 [G_C, H, W] 三轴上统计

Normalization 的发展史本质上是一个”逐步简化”和”逐步解耦”的过程:

  1. BN (2015):开创性地把归一化引入网络内部,核心贡献不是数学,而是让归一化成为一个可导的网络层 + 发现它能极大改善优化 landscape

  2. LN (2016):解耦了归一化和 batch 维度,为 Transformer 的崛起铺平了道路。可以说,没有 LN 就没有 BERT/GPT。

  3. IN (2016):解耦到单样本,为图像生成任务提供了专属的归一化方案。

  4. GN (2018):在 LN 和 IN 之间寻找最优粒度,统一了两者。小 batch CNN 的救星。

  5. FRN / RMSNorm (2019):发现关键的是 scale invariance 而不是 zero-mean,进一步简化归一化操作。LLM 时代的事实标准。

到了 2024-2025 年,出现了 normalization-free 的探索方向:通过特殊的初始化、权重约束和残差连接设计)。

看山是山,看山不是山,看山还是山。

9. Batch Norm 的三种版本实现#

10. Layer Norm 的三种版本实现#

11. RMSNorm 的三种版本实现#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
LeetGPU习题06:Norm系列详解
https://dlog.com.cn/posts/leetgpu06/norm/
作者
杜子源
发布于
2026-05-22
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
杜子源
都是风景,幸会
公告
请狠狠地打赏我,打赏一次,爆更一篇!!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
26
分类
8
标签
11
总字数
64,572
运行时长
0
最后活动
0 天前

目录