动手学深度学习-28

敖炜 Lv5

批量归一化

训练深层神经网络是十分困难的,特别是在较短的时间内使他们收敛更加棘手。批量规范化(batch normalization)是一种流行且有效的技术,可持续加速深层网络的收敛速度。再结合在之后将介绍的残差块,批量规范化使得研究人员能够训练100层以上的网络。

训练深层网络

为什么需要批量规范化层呢?让我们来回顾一下训练神经网络时出现的一些实际挑战。

首先,数据预处理的方式通常会对最终结果产生巨大影响。
回想一下我们应用多层感知机来预测房价的例子
使用真实数据时,我们的第一步是标准化输入特征,使其平均值为0,方差为1。
直观地说,这种标准化可以很好地与我们的优化器配合使用,因为它可以将参数的量级进行统一。

第二,对于典型的多层感知机或卷积神经网络。当我们训练时,中间层中的变量(例如,多层感知机中的仿射变换输出)可能具有更广的变化范围:不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数的随着训练更新变幻莫测。
批量规范化的发明者非正式地假设,这些变量分布中的这种偏移可能会阻碍网络的收敛。
直观地说,我们可能会猜想,如果一个层的可变值是另一层的100倍,这可能需要对学习率进行补偿调整。

第三,更深层的网络很复杂,容易过拟合。
这意味着正则化变得更加重要。

另一个角度看训练深层网络时的问题

问题

- 收敛速度慢:
  - 由于训练时先正向传播后反向传播,且每层的梯度一般较小,若网络较深,则反向传播时会出现类似于梯度消失的现象,导致距离数据更近的层梯度较小,收敛慢,而距离输出更近的层梯度较大,收敛快。然而底部的层一般都用于提取较基础的特征信息,上方的层收敛后,由于底部提取基础特征的层仍在变化,上方的层一直在不停的重新训练,导致整个网络难以收敛,训练较慢。

- 内部协变量偏移

批量规范化应用于单个可选层(也可以应用到所有层),其原理如下:在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。接下来,我们应用比例系数和比例偏移。正是由于这个基于批量统计的标准化,才有了批量规范化的名称。

请注意,如果我们尝试使用大小为1的小批量应用批量规范化,我们将无法学到任何东西。这是因为在减去均值之后,每个隐藏单元将为0。所以,只有使用足够大的小批量,批量规范化这种方法才是有效且稳定的。请注意,在应用批量规范化时,批量大小的选择可能比没有批量规范化时更重要。

从形式上来说,用表示一个来自小批量的输入,批量规范化根据以下表达式转换

在以上表达式中,是小批量的样本均值,是小批量的样本标准差。
应用标准化后,生成的小批量的平均值为0和单位方差为1。
由于单位方差(与其他一些魔法数)是一个主观的选择,因此我们通常包含
拉伸参数(scale)偏移参数(shift),它们的形状与相同。
请注意,是需要与其他模型参数一起学习的参数。

由于在训练过程中,中间层的变化幅度不能过于剧烈,而批量规范化将每一层主动居中,并将它们重新调整为给定的平均值和大小(通过)。

从形式上来看,我们计算出上面表达式中的,如下所示:

$$\begin{aligned} \hat{\boldsymbol{\mu}}\mathcal{B} &= \frac{1}{|\mathcal{B}|} \sum{\mathbf{x} \in \mathcal{B}} \mathbf{x},\
\hat{\boldsymbol{\sigma}}\mathcal{B}^2 &= \frac{1}{|\mathcal{B}|} \sum{\mathbf{x} \in \mathcal{B}} (\mathbf{x} - \hat{\boldsymbol{\mu}}_{\mathcal{B}})^2 + \epsilon.\end{aligned}$$

请注意,我们在方差估计值中添加一个小的常量,以确保我们永远不会尝试除以零,即使在经验方差估计值可能消失的情况下也是如此。估计值通过使用平均值和方差的噪声(noise)估计来抵消缩放问题。
乍看起来,这种噪声是一个问题,而事实上它是有益的。

事实证明,这是深度学习中一个反复出现的主题。
由于尚未在理论上明确的原因,优化中的各种噪声源通常会导致更快的训练和较少的过拟合:这种变化似乎是正则化的一种形式。
在一些初步研究中,分别将批量规范化的性质与贝叶斯先验相关联。这些理论揭示了为什么批量规范化最适应范围中的中等批量大小的难题。

另外,批量规范化层在”训练模式“(通过小批量统计数据规范化)和“预测模式”(通过数据集统计规范化)中的功能不同。
在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型。
而在预测模式下,可以根据整个数据集精确计算批量规范化所需的平均值和方差。

现在,我们了解一下批量规范化在实践中是如何工作的。

批量规范化层

批量规范化和其他层之间的一个关键区别是,由于批量规范化在完整的小批量上运行,因此我们不能像以前在引入其他层时那样忽略批量大小。

全连接层和卷积层在批量规范化的实现上略有不同

全连接层

通常将批量规范化层置于全连接层中的仿射变换和激活函数之间。
设全连接层的输入为x,权重参数和偏置参数分别为,激活函数为,批量规范化的运算符为
那么,使用批量规范化的全连接层的输出的计算详情如下:

均值和方差是在应用变换的”相同”小批量上计算的。

卷积层

在卷积层中,我们将通道视作每个位置的特征,将每个样本中的每个位置视作一个样本进行计算。每个通道都有着自己的拉伸参数和偏移参数,所有通道加在一起组成了拉伸参数向量和偏移参数向量,若样本数为m,卷积输出为p*q,计算时对m*p*q个向量进行批量归一化运算(即视作有m*p*q个样本)

对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量规范化。对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量规范化。对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量规范化。假设我们的小批量包含个样本,并且对于每个通道,卷积的输出具有高度和宽度
那么对于卷积层,我们在每个输出通道的个元素上同时执行每个批量规范化。
因此,在计算平均值和方差时,我们会收集所有空间位置的值,然后在给定通道内应用相同的均值和方差,以便在每个空间位置对值进行规范化。

预测过程中的批量规范化

在训练过程中,我们需要不断地更新模型,方差和均值也就在不断地变化,就必须计算当前小批量数据对应的方差和均值,然而预测时我们的模型已经确定下来,可以用在整个训练数据集上得到的均值和方差来对预测时的结果进行归一化。

批量规范化在训练模式和预测模式下的行为通常不同。
首先,将训练好的模型用于预测时,我们不再需要样本均值中的噪声以及在微批次上估计每个小批次产生的样本方差了。
其次,例如,我们可能需要使用我们的模型对逐个样本进行预测。
一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。
可见,和暂退法一样,批量规范化层在训练模式和预测模式下的计算结果也是不一样的。

实现细节

在实际实现时,一般使用指数加权平均来更新小批量的均值和方差,指数加权平均将旧值和当前计算结果不断进行加权平均,最终做到平滑的向更新值靠拢,公式如下:

批量归一化的参数可以通过动量梯度下降,RMSProp,Adam等多种优化方法进行训练。

从零实现

从头开始实现一个具有张量的批量规范化层

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
26
import torch 
from torch import nn
from d2l import torch as d2l

def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0)
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
# 训练模式下,用当前的均值和方差做标准化
X_hat = (X-mean) / torch.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 缩放和移位
return Y, moving_mean.data, moving_var.data

创建一个正确的BatchNorm

撇开算法细节,注意实现层的基础设计模式。 通常情况下,我们用一个单独的函数定义其数学原理,比如说batch_norm。 然后,我们将此功能集成到一个自定义层中,其代码主要处理数据移动到训练设备(如GPU)、分配和初始化任何必需的变量、跟踪移动平均线(此处为均值和方差)等问题。 为了方便起见,我们并不担心在这里自动推断输入形状,因此我们需要指定整个特征的数量。 不用担心,深度学习框架中的批量规范化API将解决上述问题,稍后将展示这一点。

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
26
27
class BatchNorm(nn.Module):
# num_features: 完全连接层的输出数量或卷积层的输出通道数
# num_dims: 2表示完全连接层,4表示卷积层
def __init__(self, num_features, num_dims):
super().__init__()
if num_dims == 2:
shape = (1, num_features)
else:
shape = (1, num_features, 1, 1)
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化为1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.bate = nn.Parameter(torch.zeros(shape))
# 非模型参数的变量初始化为0和1
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.ones(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
Y, self.moving_mean, self.moving_var = batch_norm(
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y

使用批量规范化层的LeNet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
nn.Linear(84, 10)
)

# 注意这里的学习率比之前大的多
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

简洁实现

使用深度学习框架中定义的BatchNorm,通常高级API变体运行速度快得多,因为它的代码已编译为C++或CUDA,而我们的自定义代码由Python实现。

1
2
3
4
5
6
7
8
9
10
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16), nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
nn.Linear(256, 120), nn.BatchNorm1d(120), nn.Sigmoid(),
nn.Linear(120, 84), nn.BatchNorm1d(84), nn.Sigmoid(),
nn.Linear(84, 10))

d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

小结

  • 在模型训练过程中,批量规范化利用小批量的均值和标准差,不断调整神经网络的中间输出,使整个神经网络各层的中间输出值更加稳定。
  • 批量规范化在全连接层和卷积层的使用略有不同。
  • 批量规范化层和暂退层一样,在训练模式和预测模式下计算不同。
  • 批量规范化有许多有益的副作用,主要是正则化。另一方面,”减少内部协变量偏移“的原始动机似乎不是一个有效的解释。
  • 标题: 动手学深度学习-28
  • 作者: 敖炜
  • 创建于 : 2023-09-03 17:07:38
  • 更新于 : 2024-04-19 09:29:04
  • 链接: https://ao-wei.github.io/2023/09/03/动手学深度学习-28/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论