1. 批量归一化
现在的神经网络通常都特别深,在输出层向输入层传播导数的过程中,梯度很容易被激活函数或是权重以指数级的规模缩小或放大,从而产生“梯度消失”或“梯度爆炸”的现象,造成训练速度下降和效果不理想。
第二个是在深度神经网络之中损失都是再最后面,后面的层训练较快(梯度大),但是向后传播的最后的数据处其梯度很小,也就是说后面的层训较慢,而底层的变化会导致全部层的变化,也就致使靠近loss函数的顶部需要多次学习,导致收敛很慢
如何避免或者减轻这一现象的发生呢?归一化就是方法的一种。归一化将网络中层与层之间传递的数据限制在一定范围内,从而避免了梯度消失和爆炸的发生。下面介绍一种最基本的归一化:批量归一化(BN, Batch Normalization)。另外还有层归一化(LN, Layer Normalization)和权重归一化(WN, Weight Normalization),和BN大同小异。
批量归一化(batch normalization) Ioffe.Szegedy.2015
,是一种流行且有效的技术,可持续加速深层网络的收敛速度。再结合在 resnet
中将介绍的残差块,批量归一化使得研究人员能够训练 100 层以上的网络。
1.1 为什么要用批量归一化
为什么需要批量归一化层呢?让我们来回顾一下训练神经网络时出现的一些实际挑战。
(1)首先,数据预处理的方式通常会对最终结果产生巨大影响。回想一下我们应用多层感知机来预测房价的例子(kaggle_house
)。使用真实数据时,我们的第一步是标准化输入特征,使其平均值为0,方差为1。直观地说,这种标准化可以很好地与我们的优化器配合使用,因为它可以将参数的量级进行统一。
(2)第二,对于典型的多层感知机或卷积神经网络。当我们训练时,中间层中的变量(例如,多层感知机中的仿射变换输出)可能具有更广的变化范围:不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数的随着训练更新变幻莫测。批量归一化的发明者非正式地假设,这些变量分布中的这种偏移可能会阻碍网络的收敛。
直观地说,我们可能会猜想,如果一个层的可变值是另一层的 100 倍,这可能需要对学习率进行补偿调整。
(3)第三,更深层的网络很复杂,容易过拟合。这意味着正则化变得更加重要。
1.2 单个可选层中的批量归一化
批量归一化应用于单个可选层(也可以应用到所有层),其原理如下:在每次训练迭代中,我们首先归一化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。接下来,我们应用比例系数和比例偏移。正是由于这个基于批量统计的标准化,才有了批量归一化的名称。
请注意,如果我们尝试使用大小为 1 的小批量应用批量归一化,我们将无法学到任何东西。这是因为在减去均值之后,每个隐藏单元将为 0。所以,只有使用足够大的小批量,批量归一化这种方法才是有效且稳定的。
也请注意,在应用批量归一化时,批量大小的选择可能比没有批量归一化时更重要。
从形式上来说,用 x ∈ B \mathbf{x} \in \mathcal{B} x∈B 表示一个来自小批量 B \mathcal{B} B 的输入,批量归一化 B N \mathrm{BN} BN 根据以下表达式转换 x \mathbf{x} x:
B N ( x ) = γ ⊙ x − μ ^ B σ ^ B + β . \mathrm{BN}(\mathbf{x}) = \boldsymbol{\gamma} \odot \frac{\mathbf{x} - \hat{\boldsymbol{\mu}}_\mathcal{B}}{\hat{\boldsymbol{\sigma}}_\mathcal{B}} + \boldsymbol{\beta}. BN(x)=γ⊙σ^Bx−μ^B+β.
在上式中,
μ
^
B
\hat{\boldsymbol{\mu}}_\mathcal{B}
μ^B 是样本均值,
σ
^
B
\hat{\boldsymbol{\sigma}}_\mathcal{B}
σ^B 是小批量
B
\mathcal{B}
B 的样本标准差。应用标准化后,生成的小批量的平均值为 0 和单位方差为 1。由于单位方差(与其他一些魔法数)是一个任意的选择,因此我们通常包含拉伸参数
(scale)
γ
\boldsymbol{\gamma}
γ 和 偏移参数
(shift)
β
\boldsymbol{\beta}
β,它们的形状与
x
\mathbf{x}
x 相同。
请注意, γ \boldsymbol{\gamma} γ 和 β \boldsymbol{\beta} β 是需要与其他模型参数一起学习的参数。由于在训练过程中,中间层的变化幅度不能过于剧烈,而批量归一化将每一层主动居中,并将它们重新调整为给定的平均值和大小(通过 μ ^ B \hat{\boldsymbol{\mu}}_\mathcal{B} μ^B 和 σ ^ B {\hat{\boldsymbol{\sigma}}_\mathcal{B}} σ^B)。从形式上来看,我们计算出批量归一化参数中的 μ ^ B \hat{\boldsymbol{\mu}}_\mathcal{B} μ^B 和 σ ^ B {\hat{\boldsymbol{\sigma}}_\mathcal{B}} σ^B,如下所示:
μ ^ B = 1 ∣ B ∣ ∑ x ∈ B x , σ ^ B 2 = 1 ∣ B ∣ ∑ x ∈ B ( x − μ ^ B ) 2 + ϵ . \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} μ^Bσ^B2=∣B∣1x∈B∑x,=∣B∣1x∈B∑(x−μ^B)2+ϵ.
请注意,我们在方差估计值中添加一个小常量 ϵ > 0 \epsilon > 0 ϵ>0,以确保我们永远不会尝试除以零,即使在经验方差估计值可能消失的情况下也是如此。估计值 μ ^ B \hat{\boldsymbol{\mu}}_\mathcal{B} μ^B 和 σ ^ B {\hat{\boldsymbol{\sigma}}_\mathcal{B}} σ^B 通过使用平均值和方差的噪声(noise)估计来抵消缩放问题。
你可能会认为这种噪声是一个问题,而事实上它是有益的。事实证明,这是深度学习中一个反复出现的主题。由于理论上尚未明确表述的原因,优化中的各种噪声源通常会导致更快的训练和较少的过拟合:这种变化似乎是正则化的一种形式。在一些初步研究中,Teye.Azizpour.Smith.2018
和Luo.Wang.Shao.ea.2018
分别将批量归一化的性质与贝叶斯先验相关联。这些理论揭示了为什么批量归一化最适应
50
∼
100
50 \sim 100
50∼100 范围中的中等小批量尺寸的难题。
另外,批量归一化图层在”训练模式“(通过小批量统计数据归一化)和“预测模式”(通过数据集统计归一化)中的功能不同。在训练过程中,我们无法得知使用整个数据集来估计平均值和方差,所以只能根据每个小批次的平均值和方差不断训练模型。而在预测模式下,可以根据整个数据集精确计算批量归一化所需的平均值和方差。
- 最初的论文是想用批量归一化来减少内协变量的转移
- 后续有论指出,批量诡异化可能就是通过再每个小批量中加入噪音来控制模型的复杂度 x i + 1 = γ x i − μ ^ B σ ^ B + β x_{i+1} = \gamma \frac{x_i - \hat{\mu}_B}{\hat{\sigma}_B} + \beta xi+1=γσ^Bxi−μ^B+β
- 没有必要将批量归一化和丢弃法混合使用
- 批量归一化固定小批量中的均值和方差,然后学习适合的偏移和缩放
- 批量归一化可以加快收速度,但一般不会改变模型的精度
1.3 批量归一化层
批量归一化和其他图层之间的一个关键区别是,由于批量归一化在完整的小批次上运行,因此我们不能像以前在引入其他图层时那样忽略批处理的尺寸大小。我们在下面讨论这两种情况:全连接层和卷积层,他们的批量归一化实现略有不同。
1.3.1 全连接层
通常,我们将批量归一化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为 u ,权重参数和偏置参数分别为 W \mathbf{W} W 和 b \mathbf{b} b ,激活函数为 ϕ \phi ϕ ,批量归一化的运算符为 B N \mathrm{BN} BN 。那么,使用批量归一化的全连接层的输出的计算详情如下:
h = ϕ ( B N ( W x + b ) ) . \mathbf{h} = \phi(\mathrm{BN}(\mathbf{W}\mathbf{x} + \mathbf{b}) ). h=ϕ(BN(Wx+b)).
回想一下,均值和方差是在应用变换的"相同"小批量上计算的。
1.3.2 卷积层
同样,对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量归一化。当卷积有多个输出通道时,我们需要对这些通道的“每个”输出执行批量归一化,每个通道都有自己的拉伸(scale)和偏移(shift)参数,这两个参数都是标量。假设我们的微批次包含 m m m 个示例,并且对于每个通道,卷积的输出具有高度 p p p 和宽度 q q q。那么对于卷积层,我们在每个输出通道的 m ⋅ p ⋅ q m \cdot p \cdot q m⋅p⋅q 个元素上同时执行每个批量归一化。因此,在计算平均值和方差时,我们会收集所有空间位置的值,然后在给定通道内应用相同的均值和方差,以便在每个空间位置对值进行归一化。
1.4 预测过程中的批量归一化
正如我们前面提到的,批量归一化在训练模式和预测模式下的行为通常不同。首先,将训练好的模型用于预测时,我们不再需要样本均值中的噪声以及在微批次上估计每个小批次产生的样本方差了。其次,例如,我们可能需要使用我们的模型对逐个样本进行预测。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。可见,和 dropout 一样,批量归一化层在训练模式和预测模式下的计算结果也是不一样的。
2. 从零实现BatchNorm
2.1 定义批量归一化
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
2.2 创建正确的BatchNorm层
这个层将保持适当的参数:拉伸 gamma
和偏移 beta
, 这两个参数将在训练过程中更新。此外,我们的图层将保存均值和方差的移动平均值,以便在模型预测期间随后使用。
撇开算法细节,注意我们实现图层的基础设计模式。通常情况下,我们用一个单独的函数定义其数学原理,比如说 batch_norm
。然后,我们将此功能集成到一个自定义层中,其代码主要处理簿记问题,例如将数据移动到训练设备(如 GPU)、分配和初始化任何必需的变量、跟踪移动平均线(此处为均值和方差)等。
为了方便起见,我们并不担心在这里自动推断输入形状,因此我们需要指定整个特征的数量。不用担心,深度学习框架中的批量归一化 API 将为我们解决上述问题,我们稍后将展示这一点。
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.beta = 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
2.3 使用批量归一化层的LeNet
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4),
nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16,kernel_size=5), BatchNorm(16, num_dims=4),
nn.Sigmoid(), nn.MaxPool2d(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))
# 读取数据
from torchvision import transforms
import torchvision
from torch.utils import data
batch_size = 256
def get_dataloader_workers():
"""使用四个进程读取数据"""
return 4
def load_data_fashion_mnist(batch_size,resize=None):
"""下载Fashion-MNIST数据集,并将其保存至内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0,transforms.Resize(resize)) # transforms.Resize将图片最小的一条边缩放到指定大小,另一边缩放对应比例
trans = transforms.Compose(trans) # compose用于串联多个操作
mnist_train = torchvision.datasets.FashionMNIST(root="./data",
train=True,
transform=trans,
download=True)
mnist_test = torchvision.datasets.FashionMNIST(root="./data",
train=False,
transform=trans,
download=True)
return (data.DataLoader(mnist_train,batch_size,shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test,batch_size,shuffle=True,
num_workers = get_dataloader_workers()))
def evaluate_accuracy_gpu(net, data_iter, device=None): #@save
"""使用GPU计算模型在数据集上的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 设置为评估模式
if not device:
device = next(iter(net.parameters())).device
# 正确预测的数量,总预测的数量
metric = d2l.Accumulator(2)
for X, y in data_iter:
if isinstance(X, list):
# BERT微调所需的(之后将介绍)
X = [x.to(device) for x in X]
else:
X = X.to(device)
y = y.to(device)
metric.add(d2l.accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
#@save
def train(net, train_iter, test_iter, num_epochs, lr, device):
"""用GPU训练模型"""
def init_weights(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights)
print('training on', device)
net.to(device) # 将网络挪到gpu上
optimizer = torch.optim.SGD(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss()
animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
timer, num_batches = d2l.Timer(), len(train_iter)
for epoch in range(num_epochs):
# 训练损失之和,训练准确率之和,范例数
metric = d2l.Accumulator(3)
net.train()
for i, (X, y) in enumerate(train_iter):
timer.start()
optimizer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
optimizer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
timer.stop()
train_l = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
animator.add(epoch + (i + 1) / num_batches,
(train_l, train_acc, None))
test_acc = evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
f'on {str(device)}')
lr, num_epochs, batch_size = 1.0, 10, 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
train(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.259, train acc 0.905, test acc 0.869
39525.0 examples/sec on cuda:0
# 拉伸参数和偏移参数
net[1].gamma.reshape((-1,)),net[1].beta.reshape((-1,))
(tensor([1.2002, 2.3541, 1.5280, 1.2819, 1.9412, 1.0461], device='cuda:0',
grad_fn=<ViewBackward>),
tensor([ 0.7679, 0.2918, -0.3071, 0.1223, -1.8845, -1.0148], device='cuda:0',
grad_fn=<ViewBackward>))
2.4 简明实现
net = nn.Sequential(nn.Conv2d(1, 6, kernel_size=5), nn.BatchNorm2d(6),
nn.Sigmoid(), nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.BatchNorm2d(16),
nn.Sigmoid(), nn.MaxPool2d(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))
train(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.232, train acc 0.915, test acc 0.875
65306.8 examples/sec on cuda:0
3. 课后练习
- 在使用批量归一化之前,我们是否可以从全连接层或卷积层中删除偏置参数?为什么?
可以,因为BN会将直流分量移除,所以不需要bias
- 我们是否需要在每个层中进行批量归一化?尝试一下?
输出层中不需要做BN,因为softmax会选择最大的一个值作为分类结果,做不做BN都没有影响。
- 你可以通过批量归一化来替换 dropout 吗?行为如何改变?
可以使用批量归一化替代 dropout。(具体怎么改变还不会。。。)
- 确定参数
beta
和gamma
,并观察和分析结果
只有比较深的网络的
beta
和gamma
才有比较大的区别和变化,顶层
- 查看高级 API 中有关
BatchNorm
的在线文档,以查看其他批量归一化的应用。
- 研究思路:想想你可以应用的其他“归一化”转换?你可以应用概率积分变换吗?全秩协方差估计如何?