目录
批量归一化
训练深层神经网络是十分困难的,特别是在较短的时间内使他们收敛更加棘手。
因为数据在网络最开始,而损失在结尾。训练的过程是一个前向传播的过程,而参数更新是一个从后往前的更新方式。会导致越靠近损失的参数,梯度更新越大(因为是一些很小的值不断的乘,会变得越来越小),而最终导致后面的层训练的比较快。
虽然底部层训练的慢,但是底部层一变化,所有的都得跟着变。导致最后的那些层需要重新学习多次!从而导致收敛变慢。
批量归一化(batch normalization),这是一种流行且有效的技术,可持续加速深层网络的收敛速度。 再结合 残差块,批量归一化 使得研究人员能够训练100层以上的网络。
虽然这个思想不新了,但是这个层确实是近几年出来的,大概在16年左右。当你要做很深的神经网络之后,会发现加入批量归一化,效果很好。基本成为现在不可避免的一个层了。
核心想法
当我们训练时,中间层中的变量(例如,多层感知机中的仿射变换输出)可能具有更广的变化范围:不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数的随着训练更新变幻莫测。
所以批量归一化的思想就是,我固定住分布,不管哪一层的 输出 还是 梯度,都符合某一个分布。使得网络没有特别大的转变,那么在学习细微的数值的时候就比较容易。当然具体什么分布,分布细微的东西可以再调整。
- 固定小批量里面的 均值 和 方差
- 然后在做额外的调整(可学习的参数)
式子中的
和
是根据数据学出来的,而
和
是一个可学习的参数
这两个参数的意义是 假设直接把数据设为均值为0,方差为1 不是那么适合,那就可以去需欸一个新的均值和方差去更加适应网络
但是会限制住
和
不要变化的过于猛烈
- 可学习的参数为
和
- 作用在
- 全连接层和卷积层输出上,激活函数前
- 全连接层和卷积层输入上,对输入做一个均值变化,使得输入的 方差、均值 比较好
为什么要放在激活函数之前:ReLU把你所有东西都变成正数,如果放在ReLU之后,批归一化层又给你算的奇奇怪怪的
可以认为批归一化是个线性变换
- 对全连接层,作用在特征维度
- 对于卷积层,作用在通道维度
批归一化在做什么
- 最初论文是想用它来减少内部协变量转移
- 后续有论文指出它可能就是通过在每个小批量里加入噪音来控制模型复杂度
认为
是随机偏移(当前样本计算而来),
是随机缩放(当前样本计算而来)
- 因此没必要和丢弃法混合使用
按照上面的思路的话,本来批归一化就是一个控制模型复杂度的方法,丢弃法也是。在 批归一化 上再加 丢弃,可能就没那么有用了。
总结
- 批量归一化:固定小批量中的均值和方差,然后学习出适合的偏移和缩放
- 可以加速收敛速度,但一般不改变模型的精度
- 在模型训练过程中,批量归一化不断调整神经网络的中间输出,使整个神经网络各层的中间输出值更加稳定。
- 批量归一化在全连接层和卷积层的使用略有不同。
- 批量归一化层 和 丢弃法 一样,在训练模式和预测模式下计算不同。
- 批量归一化 有许多有益的副作用,主要是正则化。另一方面,”减少内部协变量偏移“的原始动机似乎不是一个有效的解释。
代码实现
从零实现
详细注释版
import torch
from torch import nn
from d2l import torch as d2l
# 参数(X, 学习的参数:gamma、beta,预测用的全局的均值和方差:moving_mean、moving_var,极小值:eps,用来更新全局均值和方差的参数:momentum,通常取0.9 or 固定数字)
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
if not torch.is_grad_enabled():
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差,因为预测的时候可能没有批量,只有一张图片 or 一个样本
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2: #X.shape = 2:全连接层
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0) #(1,n)的行向量,按行求均值 = 计算每一列的均值
var = ((X - mean) ** 2).mean(dim=0) # 依旧是按行,所以我们的方差也是行向量
else: # X.shape = 4:卷积层
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
# 这里我们需要保持X的形状以便后面可以做广播运算
mean = X.mean(dim=(0, 2, 3), keepdim=True) #(1,n,1,1)的形状
var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True) #(1,n,1,1)的形状
# 训练模式下,用当前的均值和方差做标准化
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层
我们现在可以创建一个正确的 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
应用BatchNorm于LeNet模型
回想一下,批量规范化是在卷积层或全连接层之后、相应的激活函数之前应用的。
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)) # 没有必要对输出计算归一化
简单实现
除了使用我们刚刚定义的BatchNorm
,我们也可以直接使用深度学习框架中定义的BatchNorm
。 该代码看起来几乎与我们上面的代码相同。
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))
QA
-
Xavier 和 batch normalization 以及其他正则化手段有什么区别
Xavier 是选取比较好的初始化方法,使得网络在开始的时候比较稳定,但不能保证之后
BN 保证在整个模型训练的时候都强行的在每一层后面做归一化(其实不应该叫normalization,学深度学习的数学没学好,应该是归一化,不是正则化)
-
BN是不是一般用于深层网络,浅层MLP加上BN效果好像不好
BN对深度网络效果更好,对于浅层网络没有太多太多用处,因为只有网络深度起来了才会出现我们上面提到的后面的层更快的训练好,从而被反复作废、训练、作废、训练的情况
-
BN是做了线性变换,和加一个线性层有什么区别?
没啥太大的区别,只能说如果加了一个线性层,线性层可能不一定能学到 BN 学到的那些东西。只是一个线性层,做一个线性变换,没办法给数值做变化(均值为1,方差为0)
-
layerNorm 和 batchNorm的区别
一般来说,layerNorm 用于比较大的网络,作用在图上,batchNorm就为1,做不了batchNorm