共计 2594 个字符,预计需要花费 7 分钟才能阅读完成。
为什么我们需要批量归一化?
刚开始接触 CNN 训练时,最让我头疼的就是模型收敛不稳定。有时候学习率调小就训练不动,调大又容易梯度爆炸。后来才发现,这其实是典型的 ” 内部协变量偏移 ” 问题——神经网络每一层的输入分布会随着前层参数更新不断变化,导致后续层要不停适应这种漂移。

批量归一化(Batch Normalization)的提出就是为了解决这个问题。它通过在每个 mini-batch 内对特征进行标准化,使得每层的输入稳定在零均值、单位方差的分布附近。实验表明,这样做可以:
- 允许使用更大的学习率加速训练
- 减少对参数初始化的依赖
- 在一定程度上起到正则化的效果
数学原理拆解
批量归一化的计算过程可以分为三个核心步骤:
-
计算当前 batch 的均值与方差:
$$\mu_B = \frac{1}{m}\sum_{i=1}^m x_i$$
$$\sigma_B^2 = \frac{1}{m}\sum_{i=1}^m (x_i – \mu_B)^2$$ -
标准化处理:
$$\hat{x}_i = \frac{x_i – \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}$$ -
缩放和平移(引入可学习参数 γ 和 β):
$$y_i = \gamma \hat{x}_i + \beta$$
这里的 γ 和 β 参数非常关键。如果只有前两步,网络每层输出的分布会被强制限制在固定范围,可能损失表达能力。加入这两个参数后,网络可以自行决定是否要保留归一化效果(当 γ =σ, β= μ 时完全还原原始分布)。
框架 API 对比
PyTorch 和 TensorFlow 的实现略有差异,下面是典型用法对比:
PyTorch (1.9.0+)
import torch.nn as nn
# 对 4D 输入(batch_size×channels×height×width)进行 BN
self.bn1 = nn.BatchNorm2d(num_features=64, momentum=0.1)
TensorFlow (2.6.0+)
from tensorflow.keras.layers import BatchNormalization
# 默认对最后一个轴归一化
x = BatchNormalization(momentum=0.99, epsilon=0.001)(x)
注意点:
– momentum参数控制 running_mean/running_var 的更新速度(PyTorch 中较小值意味着更快更新)
– 卷积层后的 BN 一般放在 ReLU 激活前(学术界有争议,但实践多如此)
实战代码示例
下面是一个完整的 CIFAR-10 分类示例(PyTorch 版):
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
# 数据准备
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_set = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=128, shuffle=True)
# 带 BN 的 CNN 模型
class CNNWithBN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
self.bn1 = nn.BatchNorm2d(64) # ← BN 层插入位置
self.conv2 = nn.Conv2d(64, 128, 3, padding=1)
self.bn2 = nn.BatchNorm2d(128)
self.fc = nn.Linear(128*8*8, 10)
def forward(self, x):
x = torch.relu(self.bn1(self.conv1(x)))
x = torch.relu(self.bn2(self.conv2(x)))
x = x.view(-1, 128*8*8)
return self.fc(x)
# 训练对比
model = CNNWithBN().cuda()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)
for epoch in range(20):
for inputs, labels in train_loader:
inputs, labels = inputs.cuda(), labels.cuda()
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
避坑指南
- Batch Size 陷阱
- BN 的效果高度依赖 batch 统计量,当 batch_size<16 时效果可能变差
-
小 batch 下可尝试 Group Normalization 作为替代
-
验证 / 测试模式
- 训练时:使用当前 batch 的统计量
-
推理时:使用 running_mean 和 running_var(需调用
model.eval()切换) -
与 Dropout 的配合
- BN 本身有轻微正则效果,可以适当降低 Dropout 比率
- 建议顺序:Conv → BN → ReLU → Dropout
延伸思考
- Transformer 中的应用
- Vision Transformer 中更多使用 LayerNorm
-
因为 Transformer 的序列长度可变,BN 难以处理
-
LayerNorm vs BatchNorm
- LayerNorm 对每个样本单独归一化,适合 RNN 和变长数据
- BatchNorm 需要足够大的 batch,但对 CNN 效果通常更好
完整代码已上传 GitHub 仓库:[示例代码链接](包含有无 BN 的训练曲线对比)
最后提醒新手朋友:批量归一化虽然强大,但不要无脑堆砌。在浅层网络或小数据集上,有时简单模型 + 适当初始化就能取得不错效果,过度依赖 BN 反而可能掩盖模型结构本身的问题。
