动手学深度学习 5.10 批量归一化

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)

本节我们介绍批量归一化(batch normalization)层,它能让较深的神经网络的训练变得更加容易。

在3.16节(实战Kaggle比赛:预测房价)里,我们对输入数据做了标准化处理:处理后的任意一个特征在数据集中所有样本上的均值为0标准差为1。标准化处理输入数据使各个特征的分布相近:这往往更容易训练出有效的模型。

通常来说,数据标准化预处理对于浅层模型就足够有效了。随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化。但对深层神经网络来说,即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。

批量归一化的提出正是为了应对深度模型训练的挑战。在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。

1. 批量归一化层

全连接层卷积层做批量归一化的方法稍有不同。下面我们将分别介绍这两种情况下的批量归一化。

1.1 对全连接层做批量归一化

我们先考虑如何对全连接层做批量归一化。

通常,我们将批量归一化层置于全连接层中的仿射变换和激活函数之间。(人话来说就是放在激活函数层前,或者是计算网络层后(如卷积层或者全连接层后))

设全连接层的输入为u,权重参数和偏差参数分别为W和b,激活函数为ϕ。设批量归一化的运算符为BN。那么,使用批量归一化的全连接层的输出为 \[ ϕ(BN(x)) \] 其中批量归一化输入x由仿射变换 \[ x=Wu+b \] 得到。考虑一个由m个样本组成的小批量,仿射变换的输出为一个新的小批量\(B={x^{(1)},…,x^{(m)}}\)。它们正是批量归一化层的输入。对于小批量B中任意样本\(x^{(i)}\),批量归一化层的输出同样是d维向量 \[ y^{(i)}=BN(x^{(i)}) \] 以下是BN具体的计算内容。

首先,对小批量B求均值方差\[ \begin{align} μ_B = \frac{1}{m} \sum ^{m}_{i=1} {x^{(i)}}\\ σ_B^2 = \frac{1}{m} \sum ^{m}_{i=1} {(x^{(i)}-μ_B)}^2 \end{align} \] 其中的平方计算是按元素求平方。接下来,使用按元素开方和按元素除法对\(x^{(i)}\)标准化: \[ \hat{x}^{(i)} ←\frac{x^{(i)}-μ_B}{\sqrt {σ_B^2+ϵ}} \] 这里ϵ>0是一个很小的常数,保证分母大于0。

在上面标准化的基础上,批量归一化层引入了两个可以学习的模型参数,拉伸(scale)参数 γ偏移(shift)参数 β

两个参数\(x^{(i)}\)形状相同,皆为d维向量。它们与\(x^{(i)}\)分别做按元素乘法(符号⊙)和加法计算:

\[ y^{(i)} = γ⊙\hat{x}^{(i)}+β \] 至此,我们得到了\(x^{(i)}\)的批量归一化的输出\(y^{(i)}\) 。 值得注意的是,可学习的拉伸和偏移参数保留了不对\(x^{(i)}\)做批量归一化的可能。此时只需学出 $ γ = $ 和 $β = μ_B $ 。

即将该两参数带入公式 \(\hat{x}^{(i)} ←\frac{x^{(i)}-μ_B}{\sqrt {σ_B^2+ϵ}}\) 中刚好消除归一化计算添加的两个内容

1.2 对卷积层做批量归一化

对卷积层来说,批量归一化发生在卷积计算之后、应用激活函数之前。

如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量

设小批量中有m个样本。在单个通道上,假设卷积计算输出的高和宽分别为p和q。我们需要对该通道中m×p×q个元素同时做批量归一化。对这些元素做标准化计算时,我们使用相同的均值和方差,即该通道中m×p×q个元素的均值和方差

1.3 预测时的批量归一化

使用批量归一化训练时,我们可以将批量大小设得大一点,从而使批量内样本的均值和方差的计算都较为准确。

将训练好的模型用于预测时,我们希望模型对于任意输入都有确定的输出。因此,单个样本的输出不应取决于批量归一化所需要的随机小批量中的均值和方差。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。

🔧 举个例子:

假设我们训练图像分类模型时使用 BatchNorm:

训练时:

​ 输入一个 batch(比如32张图片),BatchNorm 会用这 32 张图的均值和方差做归一化。

​ 同时,它会更新一个 “移动平均” 的均值和方差(全局估计)。

预测时:

​ 输入一张图。不能用这一张图的均值和方差归一化(不准且不稳定)。

​ 要使用训练过程中积累下来的“全局均值和方差”。

回到实际中,归一化层 BatchNorm 中有两类“内容”

  1. 可学习的缩放系数 γ(gamma)和偏移量 β(beta)
  2. 非学习型的“均值和方差”

训练时: 每个 mini-batch 动态计算 μ 和 σ(所以叫 Batch Normalization)。

预测时: 没有 mini-batch(或batch太小不准),所以不能再用它们,只能用训练阶段保存下来的“全局均值和方差”。

可见,和丢弃层一样,批量归一化层在训练模式和预测模式下的计算结果也是不一样的。

2. 从零开始实现

下面我们自己实现批量归一化层。

代码整体易懂,就只说下两个框的意思。

第一个比较好懂:assert len(X.shape) in (2, 4) 指的是 判断X是否是2维或者4维。如果不是这两种情况则中断程序。

第二个比较复杂点。

首先举个例子

假设X为 (3,4)大小的二维矩阵,执行X.mean(dim=0),则意味着 X仅修改第0维的数据,即大小修改为 (1,N),这里的N指的是原本维度所在的数字的大小,在这例子中是4,则变成(1,4),由于并没有添加 keepdim=True 选项,所以除了N以外的其他维度会被消除,其结果大小变成(4),如果加了 keepdim=True,则大小会保持为(1,4)。

回到代码中 X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True) X原本大小为 (N,C,W,H),以上结果相当于 mean = X.mean(dim=(0, 2, 3), keepdim=True) X大小变成 (1,C,1,1) ,即,X中所有样本上某一通道上所有值加起来进行求平均,

接下来,我们自定义一个BatchNorm层。它保存参与求梯度和迭代的拉伸参数gamma和偏移参数beta,同时也维护移动平均得到的均值和方差,以便能够在模型预测时被使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class BatchNorm(nn.Module):
def __init__(self, num_features, num_dims):
super(BatchNorm, self).__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)

# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 不参与求梯度和迭代的变量,全在内存上初始化成0
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.zeros(shape)

def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
# 保存更新过的moving_mean和moving_var, Module实例的traning属性默认为true, 调用.eval()后设成false
Y, self.moving_mean, self.moving_var = batch_norm(self.training,
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y

2.1 使用批量归一化层的LeNet

下面我们修改5.5节(卷积神经网络(LeNet))介绍的LeNet模型,从而应用批量归一化层。我们在所有的卷积层或全连接层之后、激活层之前加入批量归一化层。

下面我们训练修改后的模型。

1
2
3
4
5
6
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)

最后我们查看第一个批量归一化层学习到的拉伸参数gamma和偏移参数beta。

我们知道卷积层的拉伸跟偏移参数数量只和通道数有关(二维就只和fetures数量有关)

1
2
print(net[1].gamma.view((-1,)))
print(net[1].beta.view((-1,)))

3. 简洁实现

与我们刚刚自己定义的BatchNorm类相比,Pytorch中nn模块定义的BatchNorm1dBatchNorm2d类使用起来更加简单,二者分别用于全连接层和卷积层,都需要指定输入的num_features参数值。下面我们用PyTorch实现使用批量归一化的LeNet。

使用同样的超参数进行训练。

1
2
3
4
5
6
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)

lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)