李沐动手学深度学习V2-Batch Normalization批量规范化内容和代码从零实现

1.Batch Normalization 批量规范化

1.1 介绍

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

1.2 为什么需要批量规范化层呢?

  1. 首先,数据预处理的方式通常会对最终结果产生巨大影响。 回想一下我们应用多层感知机来预测房价的例子。 使用真实数据时,我们的第一步是标准化输入特征,使其平均值为0,方差为1。 直观地说,这种标准化可以很好地与我们的优化器配合使用,因为它可以将参数的量级进行统一。
  2. 对于典型的多层感知机或卷积神经网络。当我们训练时,中间层中的变量可能具有更广的变化范围:不论是沿着从输入到输出的层,跨同一层中的单元,或是随着时间的推移,模型参数的随着训练更新变幻莫测。 批量规范化的发明者非正式地假设,这些变量分布中的这种偏移可能会阻碍网络的收敛。 直观地说,我们可能会猜想,如果一个层的可变值是另一层的100倍,这可能需要对学习率进行补偿调整。
  3. 更深层的网络很复杂,容易过拟合。 这意味着正则化变得更加重要。

1.3 批量规范化公式

  1. 批量规范化应用于单个可选层(也可以应用到所有层),其原理如下:在每次训练迭代中,我们首先规范化输入,即通过减去其均值并除以其标准差,其中两者均基于当前小批量处理。 接下来,我们应用比例系数和比例偏移,来输出经过批量规范化的结果。 正是由于这个基于批量统计的标准化,才有了批量规范化的名称。
  2. 请注意,如果我们尝试使用大小为1的小批量应用批量规范化,我们将无法学到任何东西。 这是因为在减去均值之后,每个隐藏单元将为0。 所以,只有使用足够大的小批量,批量规范化这种方法才是有效且稳定的。 请注意,在使用批量规范化层时,批量大小的选择可能比没有使用批量规范化层时更重要。
  3. 从形式上来说,用 x ∈ B \mathbf{x} \in \mathcal{B} xB表示一个来自小批量 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+β.

  1. 在上面公式中, μ ^ B \hat{\boldsymbol{\mu}}_\mathcal{B} μ^B是小批量 B \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} β是需要与网络模型中其他参数一起学习的参数。

  2. 由于在训练过程中,中间层的变化幅度不能过于剧烈,而批量规范化将每一层主动居中,并将它们重新调整为给定的平均值和方差(0-1标准化分布)(通过 μ ^ B \hat{\boldsymbol{\mu}}_\mathcal{B} μ^B σ ^ B {\hat{\boldsymbol{\sigma}}_\mathcal{B}} σ^B

  3. 从下面公式中,我们计算出 μ ^ 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=B1xBx,=B1xB(xμ^B)2+ϵ.
    请注意,我们在方差估计值中添加一个小的常量 ϵ > 0 \epsilon > 0 ϵ>0,以确保我们永远不会尝试除以零,即使在经验方差估计值可能消失的情况下也是如此。
    7.批量规范化虽然当前无法用理论来解释,但是在一些初步研究中将批量规范化的性质与贝叶斯先验相关联。这些理论揭示了为什么批量规范化最适应 50 ∼ 100 50 \sim 100 50100范围中的中等批量大小的难题,可以作为正则化技术来使用

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

1.4 批量规范化应用

批量规范化和其他层之间的一个关键区别是,由于批量规范化在完整的小批量上运行,因此我们不能像以前在引入其他层时那样忽略批量大小。
下面讨论在全连接层和卷积层上面两种情况,他们的批量规范化实现略有不同。

  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)).
  2. 卷积层
    同样,对于卷积层,我们可以在卷积层之后和非线性激活函数之前应用批量规范化。当卷积有多个输出通道时,我们需要对这些通道的“每个通道”输出执行批量规范化(也即是把批量所有样本中1号通道里面每一个像素都加起来,然后再除以(样本数k x 每个通道的高 x每个通道的宽)得到在1号通道的均值,方差也是同样求出来),也即是将计算每个通道在当前批量中的均值和方差,也即是把1号通道上面的每一个像素加起来,然后在批量中每一个样本的1号通道里面每一个像素加起来,然后把批量样本中1号通道所有像素加起来除以(样本数k x 每个通道的高 x每个通道的宽)得到的均值作为这个1号通道的均值,方差也是这样求出来,然后对所有样本中1号通道每一个像素都减去均值再除以标准差,来进行批量标准化。每个通道都有自己的拉伸(scale)和偏移(shift)参数,这两个权重参数都是标量,需要在网络中学出来。
    假设我们的小批量包含 m m m个样本,并且对于每个通道,卷积的输出具有高度 p p p和宽度 q q q。那么对于卷积层,我们在每个输出通道的 m ⋅ p ⋅ q m \cdot p \cdot q mpq个元素上同时执行每个批量规范化。因此,在计算平均值和方差时,我们会收集所有空间位置的值,然后在给定通道内应用相同的均值和方差,以便在每个空间位置对值进行规范化。
  3. 预测过程中的批量规范化
    正如我们前面提到的,批量规范化在训练模式和预测模式下的行为通常不同。首先,将训练好的模型用于预测时,我们不再需要每个批量样本均值中的噪声以及在每个批量上面估计每个小批次产生的样本方差了。
    其次,例如,我们可能需要使用我们的模型对逐个样本进行预测。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。
    可见,和暂退法一样,批量规范化层在训练模式和预测模式下的计算结果也是不一样的。

2 从零实现批量规范层

首先创建一个BatchNorm层, 这个层将保持适当的参数:拉伸gamma和偏移beta,这两个参数将在训练过程中更新。 此外,我们的层将保存均值和方差的移动平均值,均值和方差的移动平均值这两个值在训练完后作为训练数据集在每一层(有批量规范层的地方)输出时的所有样本的平均值和方差,然后在模型预测期间在应用到批量规范层的地方使用。

import d2l.torch
import torch
from torch import nn
#批量规范化函数:计算输入X使用批量规范化后的输出Y,以及根据最终在训练数据集上面得出的移动平均值和方差(也即是在训练数据集上面所有样本的全局平均值和方差,用于预测数据集上面进行批量规范化)
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) #加上一个很小的eps防止每个批量方差会变0
        # 更新移动平均的均值和方差,也即是在训练数据集上面所有样本的全局平均值和方差
        moving_mean = momentum * moving_mean + (1-momentum) * mean
        moving_var = momentum * moving_var +(1-momentum) *var
    # 缩放和移位
    Y = gamma * X_hat + beta
    return Y,moving_mean.data,moving_var.data
#批量规范化层
class BatchNorm(nn.Module):
    # num_features:完全连接层的特征输出数量或卷积层的输出通道数。
    # num_dims:2表示完全连接层,4表示卷积层
    def __init__(self,num_features,num_dim):
        super(BatchNorm, self).__init__()
        if num_dim == 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所在显存上,因为模型都是在gpu上面计算的
        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,1e-5,0.9)
        return Y

在LeNet网络上面应用批量规范化层,并进行训练和测试数据集(代码与我们第一次训练LeNet时几乎完全相同,最主要区别在于学习率大得多,为1.0,而不使用批量规范化时,学习率为0.03,表明使用批量规范化后学习率可以调的更大一些,加快模型收敛速度,从而使模型训练的更快)

LeNet = nn.Sequential(nn.Conv2d(1,6,kernel_size=5),BatchNorm(num_features=6,num_dim=4),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      nn.Conv2d(6,16,kernel_size=5),BatchNorm(num_features=16,num_dim=4),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      nn.Flatten(),
                      nn.Linear(in_features=16*4*4,out_features=120),BatchNorm(num_features=120,num_dim=2),nn.Sigmoid(),
                      nn.Linear(in_features=120,out_features=84),BatchNorm(num_features=84,num_dim=2),nn.Sigmoid(),
                      nn.Linear(in_features=84,out_features=10))
lr,num_epochs,batch_size = 1.0,10,128
train_iter,test_iter = d2l.torch.load_data_fashion_mnist(128)
d2l.torch.train_ch6(LeNet,train_iter,test_iter,num_epochs,lr,device=d2l.torch.try_gpu())

模型训练和测试结果如下图所示:
模型训练和测试结果
查看从第一个批量规范化层中学到的拉伸参数gamma和偏移参数beta:

print(LeNet[1].gamma.data.reshape((-1,)))
print(LeNet[1].beta.reshape((-1,)))
'''
输出结果如下:
(tensor([3.6049, 1.3120, 3.3909, 6.1067, 2.5441, 2.2569], device='cuda:0'),
 tensor([ 3.0142, -0.3084, -0.7312,  2.7088, -2.8389, -2.3739], device='cuda:0',
        grad_fn=<ReshapeAliasBackward0>))
'''

3.使用Pytorch框架中批量规范化层简明实现

#使用pytorch框架里面的BatchNorm层
LeNet_Pytorch = nn.Sequential(nn.Conv2d(1,6,kernel_size=5),nn.BatchNorm2d(6),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      #在卷积层后面使用批量规范化层nn.BatchNorm2d()
                      nn.Conv2d(6,16,kernel_size=5),nn.BatchNorm2d(16),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      nn.Flatten(),
                      nn.Linear(in_features=16*4*4,out_features=120),nn.BatchNorm1d(120),nn.Sigmoid(),
                      #在全连接层后面使用批量规范化层nn.BatchNorm1d()
                      nn.Linear(in_features=120,out_features=84),nn.BatchNorm1d(84),nn.Sigmoid(),
                      nn.Linear(in_features=84,out_features=10))
#模型训练和测试
# 请注意,通常Pytorch中高级API变体运行速度快得多,因为它的代码已编译为C++或CUDA,而我们的自定义代码由Python实现。
lr,num_epochs,batch_size = 1.0,10,128
train_iter,test_iter = d2l.torch.load_data_fashion_mnist(128)
d2l.torch.train_ch6(LeNet_Pytorch,train_iter,test_iter,num_epochs,lr,device=d2l.torch.try_gpu())

4.小结

  1. 在模型训练过程中,批量规范化利用小批量的均值和标准差,不断调整神经网络的中间输出,使整个神经网络中间层的输出值更加稳定。
  2. 批量规范化在全连接层和卷积层的使用略有不同。
  3. 批量规范化层和暂退层一样,在训练模式和预测模式下计算不同。
  4. 批量规范化有许多有益的副作用,主要是正则化。另一方面,”减少内部协变量偏移“的原始动机似乎不是一个有效的解释。
  5. 批量规范化已经被证明是一种不可或缺的方法,它适用于几乎所有图像分类器。

5 批量规范化应用于LeNet模型上的全部代码(包含手动从零实现和使用Pytorch框架实现):

import d2l.torch
import torch
from torch import nn


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) #加上一个很小的eps防止每个批量方差会变0
        # 更新移动平均的均值和方差,也即是在训练数据集上面所有样本的全局平均值和方差
        moving_mean = momentum * moving_mean + (1-momentum) * mean
        moving_var = momentum * moving_var +(1-momentum) *var
    # 缩放和移位
    Y = gamma * X_hat + beta
    return Y,moving_mean.data,moving_var.data

class BatchNorm(nn.Module):
    # num_features:完全连接层的特征输出数量或卷积层的输出通道数。
    # num_dims:2表示完全连接层,4表示卷积层
    def __init__(self,num_features,num_dim):
        super(BatchNorm, self).__init__()
        if num_dim == 2:
            shape = (1,num_features)
        else:
            shape = (1,num_features,1,1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成10,为模型参数的变量
        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所在显存上,因为模型都是在gpu上面计算的
        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,1e-5,0.9)
        return Y

LeNet = nn.Sequential(nn.Conv2d(1,6,kernel_size=5),BatchNorm(num_features=6,num_dim=4),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      nn.Conv2d(6,16,kernel_size=5),BatchNorm(num_features=16,num_dim=4),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      nn.Flatten(),
                      nn.Linear(in_features=16*4*4,out_features=120),BatchNorm(num_features=120,num_dim=2),nn.Sigmoid(),
                      nn.Linear(in_features=120,out_features=84),BatchNorm(num_features=84,num_dim=2),nn.Sigmoid(),
                      nn.Linear(in_features=84,out_features=10))
lr,num_epochs,batch_size = 1.0,10,128
train_iter,test_iter = d2l.torch.load_data_fashion_mnist(128)
d2l.torch.train_ch6(LeNet,train_iter,test_iter,num_epochs,lr,device=d2l.torch.try_gpu())

print(LeNet[1].gamma.data.reshape((-1,)))
print(LeNet[1].beta.reshape((-1,)))
#使用pytorch框架里面的BatchNorm层
LeNet_Pytorch = nn.Sequential(nn.Conv2d(1,6,kernel_size=5),nn.BatchNorm2d(6),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      #在卷积层后面使用批量规范化层nn.BatchNorm2d()
                      nn.Conv2d(6,16,kernel_size=5),nn.BatchNorm2d(16),nn.Sigmoid(),nn.AvgPool2d(kernel_size=2,stride=2),
                      nn.Flatten(),
                      nn.Linear(in_features=16*4*4,out_features=120),nn.BatchNorm1d(120),nn.Sigmoid(),
                      #在全连接层后面使用批量规范化层nn.BatchNorm1d()
                      nn.Linear(in_features=120,out_features=84),nn.BatchNorm1d(84),nn.Sigmoid(),
                      nn.Linear(in_features=84,out_features=10))

6. 为什么批量标准化工作?

(1) 我们知道,规范化输入特征可以加快学习速度,一种直觉是,对隐藏层做同样的事情也应该有效。
(2) 解决协方差偏移问题
假设你已经训练你的猫识别网络使用黑猫,但在有色猫上进行评估,你会看到数据分布发生变化(称为协方差偏移)。即使存在猫和非猫之间的真正界限,你也不能指望只有黑猫才能知道这个界限。所以你可能需要对网络进行再训练。
对于神经网络,假设输入分布是常数,那么某个隐藏层的输出分布应该是常数。但是,当该层和前一层的权重在训练阶段发生变化时,输出分布将发生变化,这导致从后面一层的角度来看,协方差发生偏移,就像猫识别网络一样需要重新训练。为了解决这个问题,我们使用批量标准化来强制均值为0和方差为1的分布。它允许一层接一层地独立于前一层学习,并更专注于自己的任务,从而加快训练过程。
(3) 批量标准化可以作为模型正则化,避免模型过拟合,加快模型收敛速度,训练的更快
**在正常批次中,平均值和方差是在小批次上计算的,小批次不包含太多样本,所以均值和方差包含噪声。就像dropout一样,它会给隐藏层的激活层输入增加一些噪声(dropout随机将激活层的输入乘以0或1,因为dropout层作用于全连接层和激活层之间)。**这是一个额外的和轻微的影响,不要依赖它作为正则化。

7. 相关链接

https://zhuanlan.zhihu.com/p/24810318

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值