15. Normalization Techniques | 归一化技术
难度: Medium | 环境: CPU-first | 标签: PyTorch, 归一化, 稳定性 | 目标人群: Part 2-4 前置补课者
🚀 云端运行环境
本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:
本页聚焦:理解 BatchNorm 和 LayerNorm 的最小区别;知道训练态和推理态统计量为什么不同;会看最小归一化实现和输出分布。激活函数看完之后,下一步就是看数值为什么能稳住。对还不熟大模型结构的人来说,可以先把归一化理解成“帮模型把数值范围稳住”的工具:它会在深层网络、残差块和 Transformer block 里反复出现。
如果你把 block 先想成“Embedding 之后反复堆叠的稳定单元”,那么 norm 的作用就是稳住每一层的输入分布,再让 Attention 和 MLP 更容易学到变化。
关键词: BatchNorm, LayerNorm, running stats
前置阅读
导语: 先看 0D 组页,把激活函数和训练稳定性的边界对齐,再进入这一页会更顺。
相关阅读
导语: 本页先把 BatchNorm、LayerNorm 和训练态 / 推理态的最小判断讲清楚;如果想继续看 Attention 如何接上这些稳定性直觉,再顺着看下面这一页。
- P1: 12. TensorCore and Mixed Precision | TensorCore 与混合精度
- 16. Attention Mechanism Intro | Attention 机制导论
Q1:BatchNorm 和 LayerNorm 分别解决什么问题?
归一化的核心作用,是让训练中的数值分布更稳定。BatchNorm 更依赖 batch 统计,LayerNorm 更偏向样本内部统计。对于没有大模型基础的人,可以先把它们理解成两种“稳住数值范围”的方式,只是统计维度不同。
import torch
import torch.nn.functional as F
def batch_norm_train(x, gamma, beta, eps=1e-5):
batch_mean = x.mean(dim=0)
batch_var = x.var(dim=0, unbiased=False)
normalized = (x - batch_mean) / torch.sqrt(batch_var + eps)
return normalized * gamma + beta, batch_mean, batch_var
def batch_norm_eval(x, gamma, beta, running_mean, running_var, eps=1e-5):
normalized = (x - running_mean) / torch.sqrt(running_var + eps)
return normalized * gamma + beta
def layer_norm_last_dim(x, gamma, beta, eps=1e-5):
mean = x.mean(dim=-1, keepdim=True)
var = x.var(dim=-1, unbiased=False, keepdim=True)
normalized = (x - mean) / torch.sqrt(var + eps)
return normalized * gamma + beta
x = torch.tensor([[1.0, 2.0, 3.0], [3.0, 4.0, 5.0]])
gamma = torch.ones(3)
beta = torch.zeros(3)
print('BatchNorm 输出:')
y, mean, var = batch_norm_train(x, gamma, beta)
print(y)
print('LayerNorm 输出:')
print(layer_norm_last_dim(x, gamma, beta))Q1验证:两种归一化的输出是否可见?
这里直接打印 BatchNorm 和 LayerNorm 的输出,先看它们对同一组输入会做什么。
x = torch.tensor([[1.0, 2.0, 3.0], [2.0, 4.0, 6.0]])
gamma = torch.tensor([1.0, 1.5, 2.0])
beta = torch.tensor([0.0, 1.0, -1.0])
y, mean, var = batch_norm_train(x, gamma, beta)
expected_mean = torch.tensor([1.5, 3.0, 4.5])
expected_var = torch.tensor([0.25, 1.0, 2.25])
expected = (x - expected_mean) / torch.sqrt(expected_var + 1e-5) * gamma + beta
assert torch.allclose(mean, expected_mean)
assert torch.allclose(var, expected_var)
assert torch.allclose(y, expected, atol=1e-6, rtol=1e-6)
print('✅ BatchNorm 基本验证通过')Q2:什么时候必须区分训练态和推理态?
训练态和推理态可能用不同统计量。BatchNorm 在训练时看 batch,在推理时看 running stats;LayerNorm 通常不依赖 batch 统计。这里先把它当成“统计来源切换”来看,而不只是一个模式开关。
def update_running_stats(running_mean, running_var, batch_mean, batch_var, momentum=0.1):
new_mean = (1.0 - momentum) * running_mean + momentum * batch_mean
new_var = (1.0 - momentum) * running_var + momentum * batch_var
return new_mean, new_var
running_mean = torch.tensor([0.0, 0.0, 0.0])
running_var = torch.tensor([1.0, 1.0, 1.0])
batch_mean = torch.tensor([2.0, 4.0, 6.0])
batch_var = torch.tensor([3.0, 5.0, 7.0])
new_mean, new_var = update_running_stats(running_mean, running_var, batch_mean, batch_var, momentum=0.5)
print('new_mean:', new_mean.tolist())
print('new_var:', new_var.tolist())Q2验证:running stats 是否按预期更新?
这里直接检查 running_mean 和 running_var 是否按 momentum 更新。
running_mean = torch.tensor([0.0, 0.0, 0.0])
running_var = torch.tensor([1.0, 1.0, 1.0])
batch_mean = torch.tensor([2.0, 4.0, 6.0])
batch_var = torch.tensor([3.0, 5.0, 7.0])
new_mean, new_var = update_running_stats(running_mean, running_var, batch_mean, batch_var, momentum=0.5)
assert torch.allclose(new_mean, torch.tensor([1.0, 2.0, 3.0]))
assert torch.allclose(new_var, torch.tensor([2.0, 3.0, 4.0]))
print('✅ running stats 通过')Q3:什么时候必须把归一化和数值分布一起看?
如果训练震荡严重,先别急着换架构,先看归一化和学习率是否合理。归一化本质上是在控制每层特征的统计范围。对没有大模型基础的人,可以先记住:norm 不是为了让数字“好看”,而是为了让后面的层更容易学。
x = torch.tensor([[1.0, 2.0, 3.0], [2.0, 2.0, 2.0]])
gamma = torch.tensor([1.0, 2.0, 3.0])
beta = torch.tensor([0.5, 0.0, -0.5])
y = layer_norm_last_dim(x, gamma, beta)
print('LayerNorm 输出:')
print(y)
print('输入均值:', x.mean(dim=-1).tolist())
print('输出均值:', y.mean(dim=-1).tolist())Q3验证:LayerNorm 是否按最后一维归一化?
这里直接看每一行的输出均值是否被拉回到稳定范围。
x = torch.tensor([[1.0, 2.0, 3.0], [2.0, 2.0, 2.0]])
gamma = torch.tensor([1.0, 2.0, 3.0])
beta = torch.tensor([0.5, 0.0, -0.5])
y = layer_norm_last_dim(x, gamma, beta)
expected = F.layer_norm(x, x.shape[-1:], gamma, beta, eps=1e-5)
assert torch.allclose(y, expected, atol=1e-6, rtol=1e-6)
print('✅ LayerNorm 通过')Q4:什么时候必须同时看归一化、激活和残差?
后面你看到 Transformer block、Pre-Norm、Post-Norm 时,归一化通常不是孤立出现的,而是和激活、残差一起控制稳定性。这里要把它看成一个小配件组,而不是单独的层。
x = torch.tensor([[1.0, 2.0, 3.0], [3.0, 4.0, 5.0]])
gamma = torch.ones(3)
beta = torch.zeros(3)
bn_y, bn_mean, bn_var = batch_norm_train(x, gamma, beta)
ln_y = layer_norm_last_dim(x, gamma, beta)
print('BatchNorm mean:', bn_mean.tolist())
print('BatchNorm var:', bn_var.tolist())
print('BatchNorm 输出:')
print(bn_y)
print('LayerNorm 输出:')
print(ln_y)Q4验证:不同归一化的输出是否可以直接对照?
这里直接把 BatchNorm 和 LayerNorm 的输出放在一起,确认它们解决的问题确实不同。
x = torch.tensor([[1.0, 2.0, 3.0], [3.0, 4.0, 5.0]])
gamma = torch.ones(3)
beta = torch.zeros(3)
bn_y, _, _ = batch_norm_train(x, gamma, beta)
ln_y = layer_norm_last_dim(x, gamma, beta)
assert bn_y.shape == x.shape
assert ln_y.shape == x.shape
print('✅ 归一化对照通过')Q5:什么时候先调归一化,再调激活或残差?
如果问题主要来自层间统计漂移、训练震荡或不同层数值范围差异,先调归一化;如果统计本身已经稳了,再去看激活和残差。对于新手,可以简单记成:先稳数值,再调非线性,最后再看信息通路。
def fix_norm_first(variance_shift, layer_drift, activation_saturation):
if variance_shift or layer_drift:
return {'first_fix': 'normalization', 'next': 'inspect_running_stats'}
if activation_saturation:
return {'first_fix': 'activation', 'next': 'inspect_non_linearity'}
return {'first_fix': 'residual_or_lr', 'next': 'profile_more'}
print(fix_norm_first(True, False, False))
print(fix_norm_first(False, False, True))
# 输出示例: normalization / activation / residual_or_lrQ6:什么时候归一化会改变训练动态而不是只改输出值?
只要归一化影响了梯度尺度、running stats 或不同 token / feature 的相对关系,它改变的就不只是输出值,而是训练动态本身。也就是说,norm 是训练行为的一部分,不只是前向里的一个公式。
def norm_changes_dynamics(affects_gradient, updates_running_stats, changes_relative_scale):
score = sum([affects_gradient, updates_running_stats, changes_relative_scale])
if score >= 2:
return {'dynamic_change': True, 'next': 're-tune_lr_or_warmup'}
return {'dynamic_change': False, 'next': 'continue'}
print(norm_changes_dynamics(True, True, False))
print(norm_changes_dynamics(False, False, True))
# 输出示例: dynamic_change=True 时通常要联动学习率或 warmup