6.2 Normalization
这篇文章主要介绍了 Batch Normalization 的概念,以及 PyTorch 中的 1d/2d/3d Batch Normalization 实现。

Batch Normalization

称为批标准化。批是指一批数据,通常为 mini-batch;标准化是处理后的数据服从$N(0,1)$的正态分布。
批标准化的优点有如下:
  • 可以使用更大的学习率,加速模型收敛
  • 可以不用精心设计权值初始化
  • 可以不用 dropout 或者较小的 dropout
  • 可以不用 L2 或者较小的 weight decay
  • 可以不用 LRN (local response normalization)
假设输入的 mini-batch 数据是$\mathcal{B}=\left{x_{1 \dots m}\right}$,Batch Normalization 的可学习参数是$\gamma, \beta$,步骤如下:
  • 求 mini-batch 的均值:$\mu{\mathcal{B}} \leftarrow \frac{1}{m} \sum{i=1}^{m} x_{i}$
  • 求 mini-batch 的方差:$\sigma{\mathcal{B}}^{2} \leftarrow \frac{1}{m} \sum{i=1}\left(x{i}-\mu{\mathcal{B}}\right)^{2}$
  • 标准化:$\widehat{x}{i} \leftarrow \frac{x{i}-\mu{\mathcal{B}}}{\sqrt{\sigma{B}^{2}+\epsilon}}$,其中$\epsilon$ 是放置分母为 0 的一个数
  • affine transform(缩放和平移):$y{i} \leftarrow \gamma \widehat{x}{i}+\beta \equiv \mathrm{B} \mathrm{N}{\gamma, \beta}\left(x{i}\right)$,这个操作可以增强模型的 capacity,也就是让模型自己判断是否要对数据进行标准化,进行多大程度的标准化。如果$\gamma= \sqrt{\sigma{B}^{2}}$,$\beta=\mu{\mathcal{B}}$,那么就实现了恒等映射。
Batch Normalization 的提出主要是为了解决 Internal Covariate Shift (ICS)。在训练过程中,数据需要经过多层的网络,如果数据在前向传播的过程中,尺度发生了变化,可能会导致梯度爆炸或者梯度消失,从而导致模型难以收敛。
Batch Normalization 层一般在激活函数前一层。
下面的代码打印一个网络的每个网络层的输出,在没有进行初始化时,数据尺度越来越小。
1
import torch
2
import numpy as np
3
import torch.nn as nn
4
from common_tools import set_seed
5
6
set_seed(1) # 设置随机种子
7
8
9
class MLP(nn.Module):
10
def __init__(self, neural_num, layers=100):
11
super(MLP, self).__init__()
12
self.linears = nn.ModuleList([nn.Linear(neural_num, neural_num, bias=False) for i in range(layers)])
13
self.bns = nn.ModuleList([nn.BatchNorm1d(neural_num) for i in range(layers)])
14
self.neural_num = neural_num
15
16
def forward(self, x):
17
18
for (i, linear), bn in zip(enumerate(self.linears), self.bns):
19
x = linear(x)
20
# x = bn(x)
21
x = torch.relu(x)
22
23
if torch.isnan(x.std()):
24
print("output is nan in {} layers".format(i))
25
break
26
27
print("layers:{}, std:{}".format(i, x.std().item()))
28
29
return x
30
31
def initialize(self):
32
for m in self.modules():
33
if isinstance(m, nn.Linear):
34
35
# method 1
36
# nn.init.normal_(m.weight.data, std=1) # normal: mean=0, std=1
37
38
# method 2 kaiming
39
nn.init.kaiming_normal_(m.weight.data)
40
41
42
neural_nums = 256
43
layer_nums = 100
44
batch_size = 16
45
46
net = MLP(neural_nums, layer_nums)
47
# net.initialize()
48
49
inputs = torch.randn((batch_size, neural_nums)) # normal: mean=0, std=1
50
51
output = net(inputs)
52
print(output)
Copied!
当使用nn.init.kaiming_normal_()初始化后,数据的标准差尺度稳定在 [0.6, 0.9]。
当我们不对网络层进行权值初始化,而是在每个激活函数层之前使用 bn 层,查看数据的标准差尺度稳定在 [0.58, 0.59]。因此 Batch Normalization 可以不用精心设计权值初始化。
下面以人民币二分类实验中的 LeNet 为例,添加 bn 层,对比不带 bn 层的网络和带 bn 层的网络的训练过程。
不带 bn 层的网络,并且使用 kaiming 初始化权值,训练过程如下:
可以看到训练过程中,训练集的 loss 在中间激增到 1.4,不够稳定。
带有 bn 层的 LeNet 定义如下:
1
class LeNet_bn(nn.Module):
2
def __init__(self, classes):
3
super(LeNet_bn, self).__init__()
4
self.conv1 = nn.Conv2d(3, 6, 5)
5
self.bn1 = nn.BatchNorm2d(num_features=6)
6
7
self.conv2 = nn.Conv2d(6, 16, 5)
8
self.bn2 = nn.BatchNorm2d(num_features=16)
9
10
self.fc1 = nn.Linear(16 * 5 * 5, 120)
11
self.bn3 = nn.BatchNorm1d(num_features=120)
12
13
self.fc2 = nn.Linear(120, 84)
14
self.fc3 = nn.Linear(84, classes)
15
16
def forward(self, x):
17
out = self.conv1(x)
18
out = self.bn1(out)
19
out = F.relu(out)
20
21
out = F.max_pool2d(out, 2)
22
23
out = self.conv2(out)
24
out = self.bn2(out)
25
out = F.relu(out)
26
27
out = F.max_pool2d(out, 2)
28
29
out = out.view(out.size(0), -1)
30
31
out = self.fc1(out)
32
out = self.bn3(out)
33
out = F.relu(out)
34
35
out = F.relu(self.fc2(out))
36
out = self.fc3(out)
37
return out
Copied!
带 bn 层的网络,并且不使用 kaiming 初始化权值,训练过程如下:
虽然训练过程中,训练集的 loss 也有激增,但只是增加到 0.4,非常稳定。

Batch Normalization in PyTorch

在 PyTorch 中,有 3 个 Batch Normalization 类
  • nn.BatchNorm1d(),输入数据的形状是 $B \times C \times 1D_feature$
  • nn.BatchNorm2d(),输入数据的形状是 $B \times C \times 2D_feature$
  • nn.BatchNorm3d(),输入数据的形状是 $B \times C \times 3D_feature$
nn.BatchNorm1d()为例,如下:
1
torch.nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
Copied!
参数:
  • num_features:一个样本的特征数量,这个参数最重要
  • eps:在进行标准化操作时的分布修正项
  • momentum:指数加权平均估计当前的均值和方差
  • affine:是否需要 affine transform,默认为 True
  • track_running_stats:True 为训练状态,此时均值和方差会根据每个 mini-batch 改变。False 为测试状态,此时均值和方差会固定
主要属性:
  • runninng_mean:均值
  • running_var:方差
  • weight:affine transform 中的 $\gamma$
  • bias:affine transform 中的 $\beta$
在训练时,均值和方差采用指数加权平均计算,也就是不仅考虑当前 mini-batch 的值均值和方差还考虑前面的 mini-batch 的均值和方差。
在训练时,均值方差固定为当前统计值。
所有的 bn 层都是根据特征维度计算上面 4 个属性,详情看下面例子。

nn.BatchNorm1d()

输入数据的形状是 $B \times C \times 1D_feature$。在下面的例子中,数据的维度是:(3, 5, 1),表示一个 mini-batch 有 3 个样本,每个样本有 5 个特征,每个特征的维度是 1。那么就会计算 5 个均值和方差,分别对应每个特征维度。momentum 设置为 0.3,第一次的均值和方差默认为 0 和 1。输入两次 mini-batch 的数据。
数据如下图:
代码如下所示:
1
batch_size = 3
2
num_features = 5
3
momentum = 0.3
4
5
features_shape = (1)
6
7
feature_map = torch.ones(features_shape) # 1D
8
feature_maps = torch.stack([feature_map*(i+1) for i in range(num_features)], dim=0) # 2D
9
feature_maps_bs = torch.stack([feature_maps for i in range(batch_size)], dim=0) # 3D
10
print("input data:\n{} shape is {}".format(feature_maps_bs, feature_maps_bs.shape))
11
12
bn = nn.BatchNorm1d(num_features=num_features, momentum=momentum)
13
14
running_mean, running_var = 0, 1
15
mean_t, var_t = 2, 0
16
for i in range(2):
17
outputs = bn(feature_maps_bs)
18
19
print("\niteration:{}, running mean: {} ".format(i, bn.running_mean))
20
print("iteration:{}, running var:{} ".format(i, bn.running_var))
21
22
23
24
running_mean = (1 - momentum) * running_mean + momentum * mean_t
25
running_var = (1 - momentum) * running_var + momentum * var_t
26
27
print("iteration:{}, 第二个特征的running mean: {} ".format(i, running_mean))
28
print("iteration:{}, 第二个特征的running var:{}".format(i, running_var))
Copied!
输出为:
1
input data:
2
tensor([[[1.],
3
[2.],
4
[3.],
5
[4.],
6
[5.]],
7
[[1.],
8
[2.],
9
[3.],
10
[4.],
11
[5.]],
12
[[1.],
13
[2.],
14
[3.],
15
[4.],
16
[5.]]]) shape is torch.Size([3, 5, 1])
17
iteration:0, running mean: tensor([0.3000, 0.6000, 0.9000, 1.2000, 1.5000])
18
iteration:0, running var:tensor([0.7000, 0.7000, 0.7000, 0.7000, 0.7000])
19
iteration:0, 第二个特征的running mean: 0.6
20
iteration:0, 第二个特征的running var:0.7
21
iteration:1, running mean: tensor([0.5100, 1.0200, 1.5300, 2.0400, 2.5500])
22
iteration:1, running var:tensor([0.4900, 0.4900, 0.4900, 0.4900, 0.4900])
23
iteration:1, 第二个特征的running mean: 1.02
24
iteration:1, 第二个特征的running var:0.48999999999999994
Copied!
虽然两个 mini-batch 的数据是一样的,但是 bn 层的均值和方差却不一样。以第二个特征的均值计算为例,值都是 2。
  • 第一次 bn 层的均值计算:$running_mean=(1-momentum) \times pre_running_mean + momentum \times mean_t =(1-0.3) \times 0 + 0.3 \times 2 =0.6$
  • 第二次 bn 层的均值计算:$running_mean=(1-momentum) \times pre_running_mean + momentum \times mean_t =(1-0.3) \times 0.6 + 0.3 \times 2 =1.02$
网络还没进行前向传播之前,断点查看 bn 层的属性如下:

nn.BatchNorm2d()

输入数据的形状是 $B \times C \times 2D_feature$。在下面的例子中,数据的维度是:(3, 3, 2, 2),表示一个 mini-batch 有 3 个样本,每个样本有 3 个特征,每个特征的维度是 $1 \times 2$。那么就会计算 3 个均值和方差,分别对应每个特征维度。momentum 设置为 0.3,第一次的均值和方差默认为 0 和 1。输入两次 mini-batch 的数据。
数据如下图:
代码如下:
1
batch_size = 3
2
num_features = 3
3
momentum = 0.3
4
5
features_shape = (2, 2)
6
7
feature_map = torch.ones(features_shape) # 2D
8
feature_maps = torch.stack([feature_map*(i+1) for i in range(num_features)], dim=0) # 3D
9
feature_maps_bs = torch.stack([feature_maps for i in range(batch_size)], dim=0) # 4D
10
11
# print("input data:\n{} shape is {}".format(feature_maps_bs, feature_maps_bs.shape))
12
13
bn = nn.BatchNorm2d(num_features=num_features, momentum=momentum)
14
15
running_mean, running_var = 0, 1
16
17
for i in range(2):
18
outputs = bn(feature_maps_bs)
19
20
print("\niter:{}, running_mean: {}".format(i, bn.running_mean))
21
print("iter:{}, running_var: {}".format(i, bn.running_var))
22
23
print("iter:{}, weight: {}".format(i, bn.weight.data.numpy()))
24
print("iter:{}, bias: {}".format(i, bn.bias.data.numpy()))
Copied!
输出如下:
1
iter:0, running_mean: tensor([0.3000, 0.6000, 0.9000])
2
iter:0, running_var: tensor([0.7000, 0.7000, 0.7000])
3
iter:0, weight: [1. 1. 1.]
4
iter:0, bias: [0. 0. 0.]
5
iter:1, running_mean: tensor([0.5100, 1.0200, 1.5300])
6
iter:1, running_var: tensor([0.4900, 0.4900, 0.4900])
7
iter:1, weight: [1. 1. 1.]
8
iter:1, bias: [0. 0. 0.]
Copied!

nn.BatchNorm3d()

输入数据的形状是 $B \times C \times 3D_feature$。在下面的例子中,数据的维度是:(3, 2, 2, 2, 3),表示一个 mini-batch 有 3 个样本,每个样本有 2 个特征,每个特征的维度是 $2 \times 2 \times 3$。那么就会计算 2 个均值和方差,分别对应每个特征维度。momentum 设置为 0.3,第一次的均值和方差默认为 0 和 1。输入两次 mini-batch 的数据。
数据如下图:
代码如下:
1
batch_size = 3
2
num_features = 3
3
momentum = 0.3
4
5
features_shape = (2, 2, 3)
6
7
feature = torch.ones(features_shape) # 3D
8
feature_map = torch.stack([feature * (i + 1) for i in range(num_features)], dim=0) # 4D
9
feature_maps = torch.stack([feature_map for i in range(batch_size)], dim=0) # 5D
10
11
# print("input data:\n{} shape is {}".format(feature_maps, feature_maps.shape))
12
13
bn = nn.BatchNorm3d(num_features=num_features, momentum=momentum)
14
15
running_mean, running_var = 0, 1
16
17
for i in range(2):
18
outputs = bn(feature_maps)
19
20
print("\niter:{}, running_mean.shape: {}".format(i, bn.running_mean.shape))
21
print("iter:{}, running_var.shape: {}".format(i, bn.running_var.shape))
22
23
print("iter:{}, weight.shape: {}".format(i, bn.weight.shape))
24
print("iter:{}, bias.shape: {}".format(i, bn.bias.shape))
Copied!
输出如下:
1
iter:0, running_mean.shape: torch.Size([3])
2
iter:0, running_var.shape: torch.Size([3])
3
iter:0, weight.shape: torch.Size([3])
4
iter:0, bias.shape: torch.Size([3])
5
iter:1, running_mean.shape: torch.Size([3])
6
iter:1, running_var.shape: torch.Size([3])
7
iter:1, weight.shape: torch.Size([3])
8
iter:1, bias.shape: torch.Size([3])
Copied!

Layer Normalization

提出的原因:Batch Normalization 不适用于变长的网络,如 RNN
思路:每个网络层计算均值和方差
注意事项:
  • 不再有 running_mean 和 running_var
  • $\gamma$ 和 $\beta$ 为逐样本的
1
torch.nn.LayerNorm(normalized_shape, eps=1e-05, elementwise_affine=True)
Copied!
参数:
  • normalized_shape:该层特征的形状,可以取$C \times H \times W$、$H \times W$、$W$
  • eps:标准化时的分母修正项
  • elementwise_affine:是否需要逐个样本 affine transform
下面代码中,输入数据的形状是 $B \times C \times feature$,(8, 2, 3, 4),表示一个 mini-batch 有 8 个样本,每个样本有 2 个特征,每个特征的维度是 $3 \times 4$。那么就会计算 8 个均值和方差,分别对应每个样本。
1
batch_size = 8
2
num_features = 2
3
4
features_shape = (3, 4)
5
6
feature_map = torch.ones(features_shape) # 2D
7
feature_maps = torch.stack([feature_map * (i + 1) for i in range(num_features)], dim=0) # 3D
8
feature_maps_bs = torch.stack([feature_maps for i in range(batch_size)], dim=0) # 4D
9
10
# feature_maps_bs shape is [8, 6, 3, 4], B * C * H * W
11
# ln = nn.LayerNorm(feature_maps_bs.size()[1:], elementwise_affine=True)
12
# ln = nn.LayerNorm(feature_maps_bs.size()[1:], elementwise_affine=False)
13
# ln = nn.LayerNorm([6, 3, 4])
14
ln = nn.LayerNorm([2, 3, 4])
15
16
output = ln(feature_maps_bs)
17
18
print("Layer Normalization")
19
print(ln.weight.shape)
20
print(feature_maps_bs[0, ...])
21
print(output[0, ...])
Copied!
1
Layer Normalization
2
torch.Size([2, 3, 4])
3
tensor([[[1., 1., 1., 1.],
4
[1., 1., 1., 1.],
5
[1., 1., 1., 1.]],
6
[[2., 2., 2., 2.],
7
[2., 2., 2., 2.],
8
[2., 2., 2., 2.]]])
9
tensor([[[-1.0000, -1.0000, -1.0000, -1.0000],
10
[-1.0000, -1.0000, -1.0000, -1.0000],
11
[-1.0000, -1.0000, -1.0000, -1.0000]],
12
[[ 1.0000, 1.0000, 1.0000, 1.0000],
13
[ 1.0000, 1.0000, 1.0000, 1.0000],
14
[ 1.0000, 1.0000, 1.0000, 1.0000]]], grad_fn=<SelectBackward>)
Copied!
Layer Normalization 可以设置 normalized_shape 为 (3, 4) 或者 (4)。

Instance Normalization

提出的原因:Batch Normalization 不适用于图像生成。因为在一个 mini-batch 中的图像有不同的风格,不能把这个 batch 里的数据都看作是同一类取标准化。
思路:逐个 instance 的 channel 计算均值和方差。也就是每个 feature map 计算一个均值和方差。
包括 InstanceNorm1d、InstanceNorm2d、InstanceNorm3d。
InstanceNorm1d为例,定义如下:
1
torch.nn.InstanceNorm1d(num_features, eps=1e-05, momentum=0.1, affine=False, track_running_stats=False)
Copied!
参数:
  • num_features:一个样本的特征数,这个参数最重要
  • eps:分母修正项
  • momentum:指数加权平均估计当前的的均值和方差
  • affine:是否需要 affine transform
  • track_running_stats:True 为训练状态,此时均值和方差会根据每个 mini-batch 改变。False 为测试状态,此时均值和方差会固定
下面代码中,输入数据的形状是 $B \times C \times 2D_feature$,(3, 3, 2, 2),表示一个 mini-batch 有 3 个样本,每个样本有 3 个特征,每个特征的维度是 $2 \times 2 $。那么就会计算 $3 \times 3 $ 个均值和方差,分别对应每个样本的每个特征。如下图所示:
下面是代码:
1
batch_size = 3
2
num_features = 3
3
momentum = 0.3
4
5
features_shape = (2, 2)
6
7
feature_map = torch.ones(features_shape) # 2D
8
feature_maps = torch.stack([feature_map * (i + 1) for i in range(num_features)], dim=0) # 3D
9
feature_maps_bs = torch.stack([feature_maps for i in range(batch_size)], dim=0) # 4D
10
11
print("Instance Normalization")
12
print("input data:\n{} shape is {}".format(feature_maps_bs, feature_maps_bs.shape))
13
14
instance_n = nn.InstanceNorm2d(num_features=num_features, momentum=momentum)
15
16
for i in range(1):
17
outputs = instance_n(feature_maps_bs)
18
19
print(outputs)
Copied!
输出如下:
1
Instance Normalization
2
input data:
3
tensor([[[[1., 1.],
4
[1., 1.]],
5
[[2., 2.],
6
[2., 2.]],
7
[[3., 3.],
8
[3., 3.]]],
9
[[[1., 1.],
10
[1., 1.]],
11
[[2., 2.],
12
[2., 2.]],
13
[[3., 3.],
14
[3., 3.]]],
15
[[[1., 1.],
16
[1., 1.]],
17
[[2., 2.],
18
[2., 2.]],
19
[[3., 3.],
20
[3., 3.]]]]) shape is torch.Size([3, 3, 2, 2])
21
tensor([[[[0., 0.],
22
[0., 0.]],
23
[[0., 0.],
24
[0., 0.]],
25
[[0., 0.],
26
[0., 0.]]],
27
[[[0., 0.],
28
[0., 0.]],
29
[[0., 0.],
30
[0., 0.]],
31
[[0., 0.],
32
[0., 0.]]],
33
[[[0., 0.],
34
[0., 0.]],
35
[[0., 0.],
36
[0., 0.]],
37
[[0., 0.],
38
[0., 0.]]]])
Copied!

Group Normalization

提出的原因:在小 batch 的样本中,Batch Normalization 估计的值不准。一般用在很大的模型中,这时 batch size 就很小。
思路:数据不够,通道来凑。 每个样本的特征分为几组,每组特征分别计算均值和方差。可以看作是 Layer Normalization 的基础上添加了特征分组。
注意事项:
  • 不再有 running_mean 和 running_var
  • $\gamma$ 和 $\beta$ 为逐通道的
定义如下:
1
torch.nn.GroupNorm(num_groups, num_channels, eps=1e-05, affine=True)
Copied!
参数:
  • num_groups:特征的分组数量
  • num_channels:特征数,通道数。注意 num_channels 要可以整除 num_groups
  • eps:分母修正项
  • affine:是否需要 affine transform
下面代码中,输入数据的形状是 $B \times C \times 2D_feature$,(2, 4, 3, 3),表示一个 mini-batch 有 2 个样本,每个样本有 4 个特征,每个特征的维度是 $3 \times 3 $。num_groups 设置为 2,那么就会计算 $2 \times (4 \div 2) $ 个均值和方差,分别对应每个样本的每个特征。
1
batch_size = 2
2
num_features = 4
3
num_groups = 2
4
features_shape = (2, 2)
5
6
feature_map = torch.ones(features_shape) # 2D
7
feature_maps = torch.stack([feature_map * (i + 1) for i in range(num_features)], dim=0) # 3D
8
feature_maps_bs = torch.stack([feature_maps * (i + 1) for i in range(batch_size)], dim=0) # 4D
9
10
gn = nn.GroupNorm(num_groups, num_features)
11
outputs = gn(feature_maps_bs)
12
13
print("Group Normalization")
14
print(gn.weight.shape)
15
print(outputs[0])
Copied!
输出如下:
1
Group Normalization
2
torch.Size([4])
3
tensor([[[-1.0000, -1.0000],
4
[-1.0000, -1.0000]],
5
[[ 1.0000, 1.0000],
6
[ 1.0000, 1.0000]],
7
[[-1.0000, -1.0000],
8
[-1.0000, -1.0000]],
9
[[ 1.0000, 1.0000],
10
[ 1.0000, 1.0000]]], grad_fn=<SelectBackward>)
Copied!
参考资料
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。
我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学
最近更新 1yr ago