LeetGPU习题06:Norm系列详解
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-64conv3-128 → conv3-128conv3-256 → conv3-256 → conv3-256 → conv3-256conv3-512 → conv3-512 → conv3-512 → conv3-512conv3-512 → conv3-512 → conv3-512 → conv3-512每个 conv3-64 的权重是一个 的张量。假设用 初始化(当时的常见做法,后来何恺明才发现这不够好),经过 5-6 层卷积后,激活值的分布会怎样?
不妨做一个简单的数值模拟。取一张 ImageNet 图片,像素值归一化到 :
- 第 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 的饱和区在 左右就开始梯度接近 0。如果某层的输入掉进了 ,这层的梯度就几乎为零,前面的层收不到任何有效的学习信号。
这个问题在当时是普遍存在的——不止 VGG,几乎所有深层网络都在遭受同样的折磨,但还没有人系统地解释这个现象。
1.2. Internal Covariate Shift:给问题起个名字
Ioffe 和 Szegedy 在 2015 年的论文 Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift 中,给这个问题起了名字。
Consider a network computing where and are arbitrary transformations, and the parameters , are to be learned so as to minimize the loss . Learning can be viewed as if the inputs are fed into the sub-network .
从 的视角看,它的输入是 。问题是: 每次更新后, 的输出分布都会变。对于 来说,这等于训练数据和测试数据的分布不一样了,输入分布一直在漂移,而它必须不断重新适应。
For example, a gradient descent step is exactly equivalent to that for a stand-alone network with input . 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 to remain fixed over time. Then, does not have to readjust to compensate for the change in the distribution of .
这段话的核心洞察是:每个子网络在训练时都会面临输入分布的不断变化,就像一个人在学习射箭,但靶子每次都在随机移动。论文将这一现象正式定义为:
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 调参。
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 的论文里就写了。
两个致命问题:
-
白化和梯度下降会打架。考虑一层加了 bias:,然后做减均值归一化 。当你对 做梯度下降 后,紧接着的减均值操作又把 的贡献给抹掉了。 会无限增长,但 loss 纹丝不动,网络不收敛。
-
算不动。完整的白化要算协方差矩阵的逆平方根,还要算它的梯度用于反向传播。对一个 维的隐藏层,这是 的计算量。一个典型的全连接层 ,每步 SGD 都算一次矩阵逆平方根,对当时的 GPU 来说不现实。
所以白化这条路走不通。需要的是一个计算便宜、可导、不跟梯度下降打架的替代方案。
简化方案:Batch Normalization
Ioffe 和 Szegedy 做了两个关键的简化,直接推导出了 BN:
简化一:不做 joint whitening,对每个维度独立归一化。
每个标量维度独立算自己的 和 。这就把 的矩阵运算变成了 的逐元素操作。代价是不能消除维度间的相关性,但实验证明这不是必需的——只需要把每维的均值和方差固定下来,就已经大幅改善了训练。
简化二:不用全训练集算统计量,用当前 mini-batch。
全训练集统计量需要在每个 SGD 步骤后重新遍历整个数据集,不现实。但 mini-batch 就在手上——直接用当前 batch 内的 个样本来估计均值和方差。这样归一化操作天然嵌入 SGD 的每一步,梯度可以顺畅地流过 和 回传到参数。
这两个简化合在一起,就是 BN 的数学形式。设一个 mini-batch :
先求均值和方差:
再加入一个可学习的线性变换:
这里 和 的设计非常精妙。把激活值强行拉到 有一个副作用:如果每一层的输出都被锁死在标准正态分布,网络的表达能力就废了。举个具体的例子——sigmoid 在 区间几乎是线性的。一个全是线性变换的网络,再深也只是一层:。
和 就是给网络的”反悔权”。归一化之后,网络可以通过学到的 把方差拉大(让 sigmoid 重新进入饱和区),通过 把均值挪走。极端情况下,设 ,BN 层就退化成恒等映射,什么都没做。
所以 BN 不是一个单纯的归一化操作。它默认行为是归一化,但网络有自由去偏离这个默认值——既稳住了分布,又保留了表达能力。Ioffe 和 Szegedy 搞清楚了为什么”每层都做白化”这条路走不通,然后找到了正好能绕过那两个坑的简洁方案。
BN 在 CNN 中归一化的是什么维度?
这是理解 BN 具体行为的关键。对于 CNN 的特征图,shape 为 [N, C, H, W]:
- :batch size
- :通道数
- :空间维度
BN 对每个 channel 独立归一化,统计量来自 [N, H, W] 三个维度。也就是说,对于第 个通道,它的均值是该 batch 内所有样本、所有空间位置的该通道值的平均。
为什么是 per-channel?在 CNN 中,一个卷积核在整张图的所有空间位置上滑动,共享同一组参数。因此一个 channel 的所有位置在统计意义上服从相似的分布——它们是被同一个检测器扫描出来的。把它们放在一起统计是合理的。
| 维度 | 是否参与统计 | 原因 |
|---|---|---|
| Channel | 否(独立统计) | 每个 channel 是不同的特征检测器,分布不同 |
| Batch | 是 | 跨样本统计 |
| H, W | 是 | 跨空间位置统计 |
训练与推理的不同行为
BN 在训练和推理时的行为不同,这是一个容易踩的坑。
训练时:
- 用当前 mini-batch 的 和 做归一化
- 同时维护一组全局的 running mean 和 running variance,通过指数滑动平均更新:
推理时:
- 不再计算 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 torchimport torch.nn as nnimport torch.optim as optimfrom torch.utils.data import DataLoaderimport torchvisionimport torchvision.transforms as transforms
torch.manual_seed(42)
# CIFAR-10: 3x32x32 -> 3072transform = 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) 用实验做了有力反驳。
他们的关键实验如下:
- 正常训练一个带 BN 的网络
- 训练中途,人为向 BN 输出注入随机噪声(破坏归一化后的分布,主动制造 ICS)
- 继续训练
结果:网络照样收敛,和对照组差别不大。
如果 BN 的核心价值真的是”减少 ICS”,那人为恢复 ICS 应该导致训练崩溃。但实际没有。这说明 ICS 不是故事的全部。
BN 真正做了什么?
Santurkar 等人的核心发现是:BN 让 loss landscape 变得显著更平滑。
他们分析了 loss 函数对参数梯度的 Lipschitz 常数。对于一个函数 ,-smoothness 定义为:
越小,函数越平滑,梯度下降越稳定。实验测量发现:
- 不加 BN:梯度变化剧烈,loss 曲面上有大量尖锐的局部极小值和鞍点,优化器容易被卡住或震荡。
- 加 BN:梯度的 Lipschitz 常数大幅减小,loss 曲面变得圆滑,SGD 能稳定使用更大的学习率。


把优化想象成在一片山地中找最低点:
- 没有 BN:地表布满尖锐的沟壑和陡坡。每一步只能迈很小(小 lr),否则就摔下去了。方向稍有偏差就可能滑向错误的山谷。
- 有了 BN:地表变成了起伏的缓坡。可以用大学习率,方向大致对就能快速下降。
辅助机制
除了平滑 loss landscape,BN 还有几个附带效果:
(1)解耦 weight 的 scale 和 direction
当权重缩放 时,BN 层的输出不变(归一化抹掉了 scale),所以 loss 对 的梯度不受 的影响。不管 的范数多大,梯度的尺度始终合理。
(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 的依赖。
BN 是跨 batch 归一化 → 那就换个方向,跨 feature 归一化,完全抛弃 batch 维度。每个样本独立计算自己的均值和方差。
2.1. 数学形式与归一化维度
对于一层输出 ( 为 batch 大小, 为 feature 数量),LN 对每个样本的所有 feature 做归一化:
和 的作用和 BN 中完全一样。
在 Transformer 中,每个 token 的 hidden state 是一个 维向量(通常 512~4096)。LN 就对这个向量内的所有元素做归一化。 每个 token 独立算自己的 和 。
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
Weight
Weight Normalization把每个权重向量分解为方向和模长:
其中 是方向向量, 是标量模长, 是 的二范数。
把方向()和尺度()解耦后:
- 方向训练更稳定: 的范数不影响参数化神经元的输出尺度,因为模长被重新归一化为 了。
- 显式控制尺度: 直接控制该神经元的输出幅度。初始化时可以根据激活函数设置 ,让输出落在激活函数的有效区间。
因为 WN 不依赖任何 batch 或样本统计量,它在 batch=1 和序列模型中都能工作。WN 在强化学习领域一度很流行,因为 RL 的 batch 通常很小且样本不独立。
但是WN 的收敛速度通常比 BN 慢,且稳定性稍差。
后来 BN 和 LN 的普及让 WN 逐渐边缘化,但它的思想参数解耦启发了后续很多 normalization-free 的工作。
4. Instance Normalization
2016 年,Ulyanov 等人在做图像风格迁移时发现了一个现象: 给定一张内容图 和一张风格图 ,目标是生成一张新图 ,内容来自 ,风格来自 。
关键洞察是:每张图片的全局对比度和亮度信息在风格迁移中应该被去除,因为它们属于”风格域”而非”内容域”。
但是,BN却不能做这个,为什么?
归一化!
归一化意味着把不同的风格混合在一起。因此IN尝试对每个 channel、每个样本独立做归一化:
归一化后,每张图在该通道的均值被强制移动到 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 内部独立做归一化。
GN 是一个统一的归一化框架:
| 参数 | 对应的 normalization 方法 |
|---|---|
| (全 channel 在同一组) | LayerNorm |
| (每 channel 一组) | InstanceNorm |
所以 GN 的本质是在 LN 和 IN 之间寻找一个最优粒度。
实验表明, 或 在很多任务上都效果很好,而且与 batch size 完全无关。
Mask R-CNN、Detectron2 等下流行的检测框架大量使用了 GN。
在大 batch 的 ImageNet 分类上,GN 略逊于 BN。但在小 batch 场景下,GN 远超 BN。
6. Filter Response Normalization
2019 年,Singh 和 Krishnan 在一篇论文中提出了一个问题:
FRN 只做方差归一化,省略了 mean-centering 步骤:
后续跟一个可学习的 thresholding 激活(TLU):
作者指出:
- 在 ReLU 网络中,activation 已经非负,均值的意义本来就不大
- 减均值需要一次 reduction,省略后计算更快
- 实验中小 batch 下的表现与 BN/ GN 相当甚至更好
后来的 RMSNorm 几乎就是 FRN 思想在 Transformer 领域的一次”重新发现”。
7. RMSNorm
2019 年,Biao Zhang 和 Rico Sennrich 发表了 RMSNorm 论文。核心修改极简单:
LayerNorm:
RMSNorm:
去掉了 mean-centering 和 bias 项,只保留 scaling。
作者的核心论点是:
真正重要的是 scale invariance,而不是 zero-mean。
LN 的成功主要是因为重新缩放(除以 )稳定了梯度,减均值这个操作是次要的。实验也证实:RMSNorm 在各任务上的表现与 LN 持平甚至略好,但计算量更少。
LLM(如 LLaMA、Mistral、Qwen 等)基本都选择了 RMSNorm。原因非常实际:
-
更少的计算: RMSNorm 比 LN 少一次 reduction。
-
更容易 kernel fusion: 在 GPU 上,element-wise 操作可以与前面的矩阵乘法或注意力计算合并成一个 kernel,减少 HBM 访问次数。
-
大模型的最大瓶颈不是算力,是内存带宽: 在 LLM 推理中,内存带宽通常才是瓶颈。少一次 reduction 就是少一次 HBM 访问,对吞吐量有直观的提升。
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 的发展史本质上是一个”逐步简化”和”逐步解耦”的过程:
-
BN (2015):开创性地把归一化引入网络内部,核心贡献不是数学,而是让归一化成为一个可导的网络层 + 发现它能极大改善优化 landscape。
-
LN (2016):解耦了归一化和 batch 维度,为 Transformer 的崛起铺平了道路。可以说,没有 LN 就没有 BERT/GPT。
-
IN (2016):解耦到单样本,为图像生成任务提供了专属的归一化方案。
-
GN (2018):在 LN 和 IN 之间寻找最优粒度,统一了两者。小 batch CNN 的救星。
-
FRN / RMSNorm (2019):发现关键的是 scale invariance 而不是 zero-mean,进一步简化归一化操作。LLM 时代的事实标准。
到了 2024-2025 年,出现了 normalization-free 的探索方向:通过特殊的初始化、权重约束和残差连接设计)。
看山是山,看山不是山,看山还是山。
9. Batch Norm 的三种版本实现
10. Layer Norm 的三种版本实现
11. RMSNorm 的三种版本实现
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!