共计 2569 个字符,预计需要花费 7 分钟才能阅读完成。
为什么我们需要 BN 层?
深度神经网络训练过程中,过拟合始终是个令人头疼的问题。过拟合的诱因很多,但最核心的两个问题是 参数协变偏移(Internal Covariate Shift)和 梯度消失。随着网络层数加深,每层输入的分布会随着参数更新而不断变化,导致后续层需要不断适应新的数据分布,这种现象就是参数协变偏移。这会让训练过程变得不稳定,需要更小的学习率和更谨慎的参数初始化。

Batch Normalization(BN)的提出正是为了解决这些问题。它通过对每一层的输入进行规范化,强制使其保持稳定的分布,从而减少内部协变量偏移的影响。同时,BN 还通过引入可学习的缩放参数 γ 和平移参数 β,保留了网络的表达能力。
BN 层的数学原理
BN 层的核心操作可以用以下公式表示:
对于一个小批量数据 $\mathcal{B} = {x_1, …, x_m}$,BN 层首先计算该批量的均值和方差:
$$\mu_\mathcal{B} = \frac{1}{m}\sum_{i=1}^m x_i$$
$$\sigma_\mathcal{B}^2 = \frac{1}{m}\sum_{i=1}^m (x_i – \mu_\mathcal{B})^2$$
然后用这些统计量对输入进行标准化:
$$\hat{x}i = \frac{x_i – \mu\mathcal{B}}{\sqrt{\sigma_\mathcal{B}^2 + \epsilon}}$$
最后引入可学习的参数 γ 和 β 进行缩放和偏移:
$$y_i = \gamma \hat{x}_i + \beta$$
其中 γ 和 β 是需要学习的参数,它们让网络可以自主决定是否使用标准化后的结果。
训练与推理阶段的差异
在训练阶段,BN 层使用当前小批量的统计量进行标准化。但在推理阶段,我们通常使用整个训练集的统计量。PyTorch 中通过 running_mean 和running_var来实现这一点,它们是在训练过程中通过指数移动平均(EMA)累积得到的:
running_mean = momentum * running_mean + (1 - momentum) * batch_mean
running_var = momentum * running_var + (1 - momentum) * batch_var
PyTorch 实现
下面是一个完整的 BN 层 PyTorch 实现:
import torch
import torch.nn as nn
class CustomBatchNorm1d(nn.Module):
def __init__(self, num_features, eps=1e-5, momentum=0.1):
super().__init__()
self.num_features = num_features
self.eps = eps
self.momentum = momentum
# 可训练参数
self.gamma = nn.Parameter(torch.ones(num_features))
self.beta = nn.Parameter(torch.zeros(num_features))
# 非可训练参数
self.register_buffer('running_mean', torch.zeros(num_features))
self.register_buffer('running_var', torch.ones(num_features))
def forward(self, x):
if self.training:
# 训练模式
mean = x.mean(dim=0)
var = x.var(dim=0, unbiased=False)
# 更新 running_mean 和 running_var
with torch.no_grad():
self.running_mean = self.momentum * mean + (1 - self.momentum) * self.running_mean
self.running_var = self.momentum * var + (1 - self.momentum) * self.running_var
else:
# 推理模式
mean = self.running_mean
var = self.running_var
# 标准化
x_hat = (x - mean) / torch.sqrt(var + self.eps)
# 缩放和平移
y = self.gamma * x_hat + self.beta
return y
BN 如何减少过拟合
BN 减少过拟合主要通过两个机制:
-
内部协变量偏移的抑制:通过规范化每层的输入分布,BN 减少了网络对参数初始化和学习率的敏感性,使得训练更加稳定。
-
噪声注入效应:由于 BN 在训练时使用小批量统计量,这相当于给网络注入了噪声,起到了类似 Dropout 的正则化效果。
与 Dropout 和 L2 正则化相比,BN 的正则化效果更加 ” 智能 ”,它不会像 Dropout 那样直接丢弃部分神经元,也不会像 L2 那样对所有参数施加相同的惩罚。
生产环境中的注意事项
在小批量场景下,BN 的表现可能会变差。这时可以考虑使用 Group Normalization(GN)或 Weight Normalization(WN)作为替代:
# GroupNorm 示例
self.norm = nn.GroupNorm(num_groups=32, num_channels=128)
# WeightNorm 示例
self.conv = nn.utils.weight_norm(nn.Conv2d(3, 64, 3))
在模型导出时,为了提升推理效率,通常会将 Conv 层和 BN 层合并(称为 Conv-BN 折叠)。ONNX 格式的模型会自动完成这个优化:
torch.onnx.export(model, dummy_input, "model.onnx")
开放性问题
虽然 BN 在 CNN 中表现出色,但在 Transformer 架构中却很少使用。这主要是因为:
- Transformer 通常使用 LayerNorm,它在序列数据上表现更好
- Transformer 的自注意力机制本身已经具有一定的规范化效果
对于动态网络(如 NAS),BN 的适应性是一个挑战,因为网络结构可能会变化。解决方案包括:
- 使用可微分的 BN 参数
- 在网络结构变化时重新初始化 BN 层的统计量
BN 仍然是深度学习中的基石技术之一,理解它的原理和实现细节对于构建高效的神经网络至关重要。
