如是我闻: 在深度学习模型中,Batch Normalization(简称 BN)是一种常用的技术,能有效加速训练并提高模型的稳定性。BN 通过对 mini-batch 数据进行归一化,使每层的输入数据分布保持稳定,从而缓解梯度消失/爆炸问题,同时减少对权重初始化的敏感性。

在本篇文章中,我们将从 数学推导、代码实现经验总结 三个方面,详细探讨 Batch Normalization 的工作原理,并分析为什么 BN 应该放在线性层(Linear)和激活函数(ReLU)之间,而不是之后

在这里插入图片描述


1. 数学推导:BN 的计算过程

假设某层的输入数据为 XXX,它的形状是 (batch_size, num_features),其中每一列代表某个特征在 mini-batch 内的分布,BN 的计算过程如下:

Step 1: 计算 mini-batch 内的均值

对 batch 内所有样本的某个特征维度 jjj 计算均值:
μj=1m∑i=1mxi,j\mu_j = \frac{1}{m} \sum_{i=1}^{m} x_{i,j}μj=m1i=1mxi,j
其中:

  • mmm 是 mini-batch 内的样本数
  • xi,jx_{i,j}xi,j 是第 iii 个样本在特征维度 jjj 的值

Step 2: 计算 mini-batch 内的方差

σj2=1m∑i=1m(xi,j−μj)2\sigma_j^2 = \frac{1}{m} \sum_{i=1}^{m} (x_{i,j} - \mu_j)^2σj2=m1i=1m(xi,jμj)2
为了防止除零,加上一个很小的数 ϵ\epsilonϵ

σj2^=1m∑i=1m(xi,j−μj)2+ϵ\hat{\sigma_j^2} = \frac{1}{m} \sum_{i=1}^{m} (x_{i,j} - \mu_j)^2 + \epsilonσj2^=m1i=1m(xi,jμj)2+ϵ

Step 3: 归一化

x^i,j=xi,j−μjσj2+ϵ\hat{x}_{i,j} = \frac{x_{i,j} - \mu_j}{\sqrt{\sigma_j^2 + \epsilon}}x^i,j=σj2+ϵxi,jμj
这样,x^\hat{x}x^ 具有均值 0、方差 1 的特性。

Step 4: 重新缩放和平移(学习参数)

BN 不是单纯地归一化,而是 允许网络学习合适的缩放和平移参数,以恢复模型的表达能力:
yi,j=γjx^i,j+ηjy_{i,j} = \gamma_j \hat{x}_{i,j} +\eta_jyi,j=γjx^i,j+ηj
其中:

  • γj\gamma_jγj 是缩放参数
  • ηj\eta_jηj 是平移参数
  • 这两个参数都是可学习的

总结:BN 公式

yi,j=γjxi,j−μjσj2+ϵ+ηjy_{i,j} = \gamma_j \frac{x_{i,j} - \mu_j}{\sqrt{\sigma_j^2 + \epsilon}} +\eta_jyi,j=γjσj2+ϵxi,jμj+ηj

这些步骤的作用是:

  • 让输入保持稳定的均值和方差,避免梯度消失/爆炸问题。
  • 允许 BN 学习合适的缩放和平移,以保持模型的表达能力。

2. 代码实现:BN 在 MLP 里的使用

我们用 PyTorch 搭建一个简单的 MLP(多层感知机),并在隐藏层之间加入 Batch Normalization:

import torch
import torch.nn as nn

class SimpleMLP(nn.Module):
    def __init__(self):
        super(SimpleMLP, self).__init__()
        self.fc1 = nn.Linear(2, 4)  # 输入层 -> 隐藏层 1
        self.bn1 = nn.BatchNorm1d(4)  # BN 归一化
        self.fc2 = nn.Linear(4, 3)  # 隐藏层 1 -> 隐藏层 2
        self.bn2 = nn.BatchNorm1d(3)  # BN 归一化
        self.fc3 = nn.Linear(3, 2)  # 隐藏层 2 -> 输出层

    def forward(self, x):
        x = self.fc1(x)
        x = self.bn1(x)  # 先归一化
        x = torch.relu(x)  # 再激活

        x = self.fc2(x)
        x = self.bn2(x)  # 先归一化
        x = torch.relu(x)  # 再激活

        x = self.fc3(x)  # 最后输出层不使用 BN
        return x

# 创建模型
model = SimpleMLP()

# 生成一个 batch 的数据,假设 batch_size=5,每个样本有 2 个特征
x = torch.randn(5, 2)

# 运行前向传播
output = model(x)

print(output)

关键点

  1. BN 层 nn.BatchNorm1d(隐藏层大小) 直接加在 Linear 层之后。
  2. BatchNorm1d 作用于 mini-batch 的每个特征维度,而不是单个样本。
  3. BN 放在 ReLU 之前,而不是之后,这一点非常重要。

3. 经验分析:为什么 BN 应该放在 ReLU 之前?

很多人会问:BN 是否应该放在激活函数之后?
答案是:不应该,BN 应该放在 Linear 层之后,ReLU 之前,主要有以下几个原因:

(1) BN 后再 ReLU,可以稳定数据分布

  • Linear -> BN 之后,数据已经被标准化成 均值 0,方差 1
  • 如果 BN 放在 ReLU 之后,ReLU 会把负数变成 0,这样会改变均值和方差,使 BN 失效。

(2) 让梯度更稳定

  • BN 使输入数据分布平稳,梯度不会太大也不会太小,从而加快收敛速度。
  • 如果 BN 在 ReLU 之后,负值部分会变成 0,这部分的梯度直接消失,影响训练效果。

(3) 从大数定律来看

  • BN 计算的是 mini-batch 内的均值和方差。
  • 由于大数定律,BN 能够在 batch 里提供稳定的估计,使得梯度更加平稳。
  • 但如果 BN 在 ReLU 之后,负值部分全变成 0,均值和方差的估计会偏向正数部分,导致训练不稳定。
放在 ReLU 之前(正确)放在 ReLU 之后(错误)
数据分布均值 0,方差 1均值会偏移,方差变小
梯度流让梯度更稳定负值部分梯度消失
收敛速度收敛更快训练可能不稳定

因此,在 MLP 或 CNN 里,Batch Normalization 一般放在线性层(Linear/Conv)之后,ReLU 之前


4. 总的来说

  • Batch Normalization 通过对 mini-batch 归一化,使数据分布更稳定,提高训练效率。
  • 数学上,BN 计算均值和方差,然后用缩放因子 γ\gammaγ 和偏移量 η\etaη 进行调整。
  • 代码上,BN 层应该加在 Linear 层后,ReLU 之前。
  • 经验上,BN 使梯度更稳定,并减少梯度消失问题,加快收敛速度。
  • 大数定律解释了 BN 在 batch 内的统计计算是合理的,但如果 BN 放在 ReLU 之后,会破坏这个平稳性。

以上

更多推荐