共计 4090 个字符,预计需要花费 11 分钟才能阅读完成。
为什么需要批量归一化?
在训练深度神经网络时,尤其是 CNN,我们经常会遇到两个棘手的问题:
- 内部协变量偏移(Internal Covariate Shift):随着网络层数的加深,每一层的输入分布会不断变化,导致后续层需要不断适应新的数据分布,这会显著减慢训练速度。
- 梯度消失 / 爆炸:在深层网络中,梯度在反向传播时可能会变得非常小或非常大,使得网络难以训练。
批量归一化(Batch Normalization,简称 BN)就是为了解决这些问题而提出的。它通过对每一层的输入进行归一化,使得网络各层的输入分布保持稳定,从而加速训练并提高模型性能。
批量归一化的数学原理
BN 的核心思想很简单:对每个 mini-batch 的数据进行归一化,使其均值为 0,方差为 1。具体来说,对于输入特征 x,BN 的计算过程如下:
- 计算 mini-batch 的均值:
$$\mu_B = \frac{1}{m} \sum_{i=1}^m x_i$$ - 计算 mini-batch 的方差:
$$\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$$
其中,ε 是一个很小的常数(如 1e-5),用于数值稳定性。γ 和 β 是可学习的参数,允许网络在需要时恢复原始数据分布。
框架对比:PyTorch vs TensorFlow
PyTorch 中的实现
在 PyTorch 中,我们可以使用 nn.BatchNorm2d 来实现 BN。例如:
import torch.nn as nn
# 定义一个带有 BN 的 CNN 层
conv_layer = nn.Sequential(nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2)
)
TensorFlow 中的实现
在 TensorFlow 中,对应的实现是 BatchNormalization 层:
from tensorflow.keras.layers import BatchNormalization, Conv2D, ReLU, MaxPool2D
# 定义一个带有 BN 的 CNN 层
model = tf.keras.Sequential([Conv2D(64, 3, padding='same'),
BatchNormalization(),
ReLU(),
MaxPool2D()])
两者的 API 非常相似,但 PyTorch 的 BatchNorm2d 需要明确指定特征数(即通道数),而 TensorFlow 的 BatchNormalization 不需要。
代码实战:CIFAR-10 分类
下面我们用一个完整的 CNN 示例来展示 BN 的效果。我们将对比有无 BN 时的训练曲线。
数据加载
import torch
import torchvision
import torchvision.transforms as transforms
# 数据预处理
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
# 加载 CIFAR-10 数据集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True)
# 定义有 BN 和没有 BN 的两个模型
class CNNWithBN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Conv2d(3, 32, 3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64*8*8, 256),
nn.BatchNorm1d(256),
nn.ReLU(),
nn.Linear(256, 10)
)
def forward(self, x):
return self.net(x)
class CNNWithoutBN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Conv2d(3, 32, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64*8*8, 256),
nn.ReLU(),
nn.Linear(256, 10)
)
def forward(self, x):
return self.net(x)
训练过程
import torch.optim as optim
# 初始化模型
model_with_bn = CNNWithBN().cuda()
model_without_bn = CNNWithoutBN().cuda()
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer_bn = optim.Adam(model_with_bn.parameters(), lr=0.001)
optimizer_no_bn = optim.Adam(model_without_bn.parameters(), lr=0.001)
# 训练函数
def train(model, optimizer, trainloader, epochs=10):
model.train()
losses = []
for epoch in range(epochs):
running_loss = 0.0
for i, (inputs, labels) in enumerate(trainloader):
inputs, labels = inputs.cuda(), labels.cuda()
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99:
print(f'Epoch {epoch+1}, Batch {i+1}, Loss: {running_loss/100:.3f}')
losses.append(running_loss/100)
running_loss = 0.0
return losses
# 训练两个模型
losses_with_bn = train(model_with_bn, optimizer_bn, trainloader)
losses_without_bn = train(model_without_bn, optimizer_no_bn, trainloader)
结果对比

图:有 BN 和无 BN 时的训练损失对比
从图中可以明显看出,使用了 BN 的模型收敛更快,训练过程更稳定。
避坑指南
Batch Size 设置
BN 的效果对 Batch Size 非常敏感:
- Batch Size 过小(如 <16)时,统计的均值和方差可能不准确,导致 BN 效果下降。
- 建议在可能的情况下使用较大的 Batch Size(如 32-256)。
- 如果受限于显存必须使用小 Batch Size,可以考虑使用 Group Normalization 等替代方案。
验证 / 测试阶段的处理
训练时 BN 使用当前 batch 的均值和方差,但在验证和测试时,我们通常使用训练过程中计算的 running_mean 和 running_var:
- PyTorch 中,
BatchNorm2d会自动维护这两个统计量(默认 momentum=0.1)。 - 在测试时,
model.eval()会切换到使用 running_mean/running_var 的模式。
与 Dropout 共用
BN 和 Dropout 都是正则化技术,但共用时需要注意:
- BN 本身有一定的正则化效果,可能不需要很强的 Dropout。
- 建议在 BN 层之后使用较小的 Dropout 率(如 0.2-0.3)。
延伸思考
BN 在 Transformer 中的应用
Transformer 中更常用 Layer Normalization(LN)而不是 BN,原因包括:
- Transformer 处理的是序列数据,不同序列长度使得 BN 难以应用。
- LN 对每个样本单独归一化,不依赖 batch 统计量,更适合序列数据。
Layer Normalization vs Batch Normalization
- LN:对每个样本的所有特征进行归一化,常用于 RNN/Transformer。
- BN:对 batch 中所有样本的每个特征分别归一化,最适用于 CNN。
完整代码
完整的可运行代码已上传至 GitHub 仓库:CNN-BN-Example
总结
批量归一化是训练深度 CNN 的重要技术,它能显著加速收敛并提高模型性能。通过本文,你应该已经掌握了 BN 的原理、实现方式以及实际应用时的注意事项。记住:
- BN 通常插入在卷积层和激活函数之间。
- 训练和测试阶段 BN 的行为不同,需要正确切换模式。
- BN 对 Batch Size 敏感,需要根据实际情况调整。
希望这篇指南能帮助你更好地理解和应用批量归一化技术!
