我们知道网络开始训练之后,每一层网络的参数都会发生变化。又由于每一层网络的输入是上一层网络的输出,上一层网络参数的变化,就导致当前层的输入分布发生变化,这个问题我们称之为 Internal Convariate Shift。
BN 的出现就是试图去解决该问题。我们在每一层的网络输出之后,将输出数据经过 BN 层,来实现控制数据的分布。BN 中的 Batch 指的是以 Batch 为单位,对输入的批次样本的同一特征进行 Normalization。很显然,BN 的 batch size 不同,也会影响到网络的收敛。一般而言,我们希望 batch 的样本计算得到均值和方差能够更加接近真实分布的均值和方差,所以在设置 batch size 时会选择较大。
1. 批量归一化公式
- λ 和 β 是可学习的参数,它相当于对标准化后的值做了一个线性变换,λ 为系数,β 为偏置;
- eps 通常指为 1e-5,避免分母为 0;
- E(x) 表示变量的均值;
- Var(x) 表示变量的方差;
数据在经过 BN 层之后,无论数据以前的分布是什么,都会被归一化成均值为 β,标准差为 γ 的分布。
注意:BN 层不会改变输入数据的维度,只改变输入数据的的分布. 在实际使用过程中,BN 常常和卷积神经网络结合使用,卷积层的输出结果后接 BN 层,即:BN 层在激活函数之前。
关于 BN 的一些有趣探讨:https://stackoverflow.com/questions/45493384/is-it-normal-to-use-batch-normalization-in-rnn-lstm
2. BN 层的接口
torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.9, affine=True)
- 由于每次使用的 mini batch 的数据集,所以 BN 使用移动加权平均来近似计算均值和方差,而 momentum 参数则调节移动加权平均值的计算;
- affine = False 表示 γ=1,β=0,反之,则表示 γ 和 β 要进行学习;
- BatchNorm2d 适用于输入的数据为 4D,输入数据的形状 [N,C,H,W]
均值使用的是移动加权平均:
running_mean = (1 - momentum) * running_mean + momentum * batch_mean running_var = (1 - momentum) * running_var + momentum * batch_var
示例代码:
import torch import torch.nn as nn def test01(): torch.manual_seed(0) batch_input = torch.randint(0, 10, [2, 2, 2, 2]).float() print(batch_input) # num_features 表示每个样本特征图的数量,通道数量 # affine 为 False 表示不带 gama 和 beta 两个学习参数 # eps 小常数,避免分母为 0 # momentum 移动加权平均系数: # running_mean = (1 - momentum) * running_mean + momentum * batch_mean # running_var = (1 - momentum) * running_var + momentum * batch_var batch_norm = nn.BatchNorm2d(num_features=2, affine=False, eps=1e-5, momentum=0.9) output = batch_norm(batch_input) # 均值是批次相同通道的均值 # 方差是批次相同通道的均值 # tensor([3.7125, 5.7375]) # 3.7125 表示第一个通道的均值 # 5.7375 表示第二个通道的均值 print('批次均值:', batch_norm.running_mean) # tensor([8.4411, 5.2268]) # 8.4411 表示第一个通道的方差 # 5.2268 表示第二个通道的方差 # print(batch_norm.running_var) def test02(): torch.manual_seed(0) batch_input = torch.randint(0, 10, [2, 2, 2, 2]).float() # 取出第一个通道所有的值 channel = batch_input[:, 0, :, :] # 计算第一个通道元素均值 batch_mean = torch.sum(channel) / channel.numel() # 计算第一个通道移动加权均值 momentum = 0.9 print('动作分解:', (1 - momentum) * 0 + momentum * batch_mean) if __name__ == '__main__': test01() test02()
程序执行结果:
tensor([[[[4., 9.], [3., 0.]], [[3., 9.], [7., 3.]]], [[[7., 3.], [1., 6.]], [[6., 9.], [8., 6.]]]]) 批次均值: tensor([3.7125, 5.7375]) 动作分解: tensor(3.7125)
当我们进行预测时,可以使用该移动均值和方差对输入数据进行批次归一化。
3. 贝塞尔校正
贝塞尔校正是一个与统计学的方法和标准差相关的修正方法,在计算样本的方差和标准差时,将分母中的 N 替换成 N-1,这种修正方法得到的方差和标准差,更近似总体集合分布中的方差和标准差。