【标准化方法】(1) Batch Normalization 原理解析、代码复现,附Pytorch完整代码

大家好,今天和各位分享一下深度学习中常见的标准化方法,先介绍一下最常用的 Batch Normalization,从数学公式的角度复现一下代码。


1. 原理解析

深层网络训练时,前面层训练参数的更新将导致后面层输入数据分布的变化,以网络第二层为例:网络的第二层输入,是由第一层的参数和输入数据计算得到的,而第一层的参数在整个训练过程中一直在变化,因此必然会引起后面每一层输入数据分布的改变。

由于模型参数在不断修改且不断的往前传播,所以各层的输入分布在不断变化,即网络每一层的输入数据分布是一直在变化的。研究人员把网络中间层在训练过程中,数据分布的变化称为内部协方差变化(internal covariate shift,ICS),这要求模型训练时必须使用较小的学习率,且需要慎重地选择权重初值。ICS 导致训练速度减慢,同时也导致使用饱和的非线性激活函数(如sigmoid,正负两边都会饱和梯度为 0)时出现梯度消失问题。

针对内部协方差变化这一普遍现象,解决办法是:对每层的输入进行归一化。即批归一化算法(Batch Normalization,BN),BN 算法主要包含以下三个步骤:

(1)计算统计值。在小批次样本上计算归一化所需的统计值,包括均值和方差。假设输入为x \in R^{m*d},其中 m 指当前批次(batch size)的大小,即当前批次共有多少个训练样本。 d 指输入的特征图的大小

E\left(x^{k}\right)=\frac{1}{m} \sum_{i=1}^{m} x_{i}^{k}

\operatorname{Var}\left[x^{k}\right] \leftarrow \frac{1}{m} \sum_{i=1}^{m}\left(x_{i}^{k}-E\left[x^{k}\right]\right)^{2}

(2)归一化操作把输入向量中每个元素当成独立随机变量单独进行归一化,向量中各变量独立了,也就没有协方差矩阵了。这种归一化在各变量相关的情况下依然能加速收敛,使用了下面的公式进行处理,也就是近似白化处理。对于 d 维输入数据 x=(x^{(1)}...x^{(d)})归一化每一维

\hat x=\frac{x^k-E[x^{(k)}]}{\sqrt{Var[x^{(k)}]}}

(3)线性变换。只对输入进行归一化可能会改变输入本来所能表现的特性或者是分布,如在 sigmoid 函数中加入批归一化算法后可能会使得输入从非线性变成线性。为了解决这个问题,可以用可学习的参数增益 \gamma 和偏置 \beta 去拟合原先的分布

y^{(k)}=\gamma^{(k)}\cdot\hat{x}^{(k)}+\beta^{(k)}

扫描二维码关注公众号,回复: 15168629 查看本文章

其中,\gamma^{(k)}=Var[x^{(k)}] 并且 \beta^{(k)}=E[x^{(k)}] 时,理论上可以得到和输入相同的分布。在实验中一般初始化为增益 \gamma=1,偏置 \beta=0,这里加入线性变换的目的是为了让因训练而“刻意”加入的 BN 能够有可能还原最初的输入

基于以上三点,批归一化算法的整体流程如下所示:

输入:

x 代表一个小批次 B=x_{1...m} 共 m 个样本;需要学习的参数分别是增益 \gamma 和偏置 \beta

输出:

y_i=BN_{\gamma, \beta}(x_i)

\mu_B\leftarrow\frac{1}{m}\sum_{i=1}^m x_i

\sigma_B^2\leftarrow\frac{1}{m}\sum_{i=1}^m(x_i-\mu_B)^2

\hat{x}_{i}\leftarrow\frac{x_{i}-\mu_{B}}{\sqrt{\sigma_{p}^{2}+\mathcal{E}}}

y_i\leftarrow\gamma\hat{x_i}+\beta\equiv\operatorname{BN}_{\gamma,\beta}(x_i)


关于 BN 变换的参数更新,可以用链式法则求导来计算。

上述只涉及到了批归一化算法在训练时的操作,在测试时批归一化是采用训练时的均值和方差的无偏估计来进行归一化的

批归一化算法由原来的输入 x 变成 BN(x),训练时归一化输入需要考虑到小批次但在测试时只需要建立输入和输出的关系。于是最后测试阶段的 \mu 和 \sigma 计算公式如下:

\operatorname{E}[x]\leftarrow\operatorname{E}_\beta[u_\beta]

Var[x]\leftarrow\frac{m}{m-1}\text{E}_{\beta}[\sigma_{\beta}^2]

测试时直接计算训练时所有小批次均值的平均值当做测试集的均值,对于方差采用每个小批次方差的无偏估计。那么对于一个测试样本 x,最终测试阶段的批归一化操作为:

y=\frac{\gamma}{\sqrt{Var[x]+\varepsilon}}\cdot x+(\beta-\frac{\gamma E[x]}{\sqrt{Var[x]+\varepsilon}})


批归一化算法把输入用均值方差进行归一化处理,使输入的统计分布一致,它可以减少 ICS,加速神经网络训练。总体来说,BN 主要作用如下:

(1)BN 变换是可微的,通过 BN 变换,可以减弱输入分布的 ICS保持各层输入的均值和方差稳定,并且最后加入了线性变换让 BN 变换与网络本来的变换等价。

(2)让梯度受训练参数及其初值的影响变小

(3)批归一化算法使得输入到激活函数的数值卡在饱和区域的概率降低,以便可以使用 sigmoid 这种易饱和的函数。


批归一化算法可以用于前馈神经网络和卷积神经网络。在卷积神经网络中应用批归一化技术时应该注意使用的对象和位置。

通常来说,对于输入 x\in R^{m\times d\times h \times w }。其中 m 指当前批次(batchsize)的大小;d 指输入的特征图的大小;h、w 在图像分类任务中分别代表图像的高和宽。

权重 W \in R^{d\times n\times F_h \times F_w };d、n 代表权重连接的两个特征图的大小;F_hF_w 代表滤波器的大小。经过卷积操作后的输出为 conv(Wx)+b,在分布上更加符合对称、非稀疏的分布,也就是分布上更加符合高斯分布,所以在 conv(Wx)+b 上使用批归一化算法。

在使用批归一化算法时,考虑到偏置参数 b 经过 BN 层后会被均值归一化,而且 BN 层后面还有个 \beta 参数作为偏置项,所以参数 b 可被省略,也就是将原来的网络变形为下面的形式,其中 g 代表激活函数。

z=g(BN(conv(Wx)))


2. 代码实现

基于上述理论推导,Batch Normalization 的代码如下,构造一个 shape=[b,c,w,h]=[2,3,2,2] 的输入张量测试 BN 层,对输入图像的每个通道做标准化。

import torch
from torch import nn

class BN(nn.Module):
    # 初始化
    def __init__(self, channels:int, 
                 eps:float=1e-5, momentum:float=0.1,
                 affine:bool=True, track_running_stats:bool=True):
        super(BN, self).__init__()

        self.channels = channels  # 输入特征数
        self.eps = eps            # 防止分母为0
        self.momentum = momentum  # 指数平滑
        self.affine = affine      # 是否对norm值做缩放和平移
        self.track_running_stats = track_running_stats  # 是否计算移动平均值或均值或方差

        if self.affine:  # 为每个特征生成一个可训练的缩放参数和平移参数
            self.scale = nn.Parameter(torch.ones(channels))  # [c]
            self.shift = nn.Parameter(torch.zeros(channels))  # [c]

        # 定义一组参数,模型训练时不会更新(即调用 optimizer.step()后该组参数不会变化,只可人为地改变它们的值)
        if self.track_running_stats:  # 存放均值和方差的指数移动平均
            self.register_buffer('exp_mean', torch.zeros(channels))
            self.register_buffer('exp_var', torch.ones(channels))

    # 前向传播
    def forward(self, x: torch.Tensor, training=True):
        x_shape = x.shape  # 输入特征的维度[b,c,w,h]
        batch_size = x_shape[0]  # 每个step训练batch_size个样本
        # 如果输入特征的深度和预置的输入通道不同就报错
        assert self.channels == x.shape[1]
        # [b,c,w,h]-->[b,c,w*h]
        x = x.view(batch_size, self.channels, -1)
        
        # 训练模式下或没有跟踪指数移动平均,模型更新参数
        if self.training or not self.track_running_stats:
            # [b,c,w*h]-->[c]
            mean = x.mean(dim=[0,2])  # 计算每个通道的均值,一个通道一个num,计算axis=0和2的均值
            mean_x2 = (x**2).mean(dim=[0,2])  # 平方的均值
            var = mean_x2 - mean**2  # 每个通道的方差
        
            # 更新指数移动平均
            if self.training and self.track_running_stats:
                self.exp_mean = (1-self.momentum)*self.exp_mean + self.momentum*mean
                self.exp_var = (1-self.momentum)*self.exp_var + self.momentum*var
        
        else:  # 测试模式下不更新均值和方差
            mean = self.exp_mean
            var = self.exp_var

        # 标准化 --> [b,c,w*h]
        x_norm = (x-mean.view(1,-1,1)) / torch.sqrt(var+self.eps).view(1,-1,1)
        # 可学习的缩放参数和平移参数 [b,c,w*h]
        if self.affine:
            x_norm = self.scale.view(1,-1,1)  * x_norm + self.shift.view(1,-1,1)
        
        # [b,c,w*h]-->[b,c,w,h]
        return x_norm.view(x_shape)

# ------------------------------ #
# 测试
# ------------------------------ #

if __name__ == '__main__':

    x = torch.linspace(0, 23, 24, dtype=torch.float32)
    x = x.reshape([2,3,2,2])  # [b,c,w,h]
    bn = BN(channels=3)  # 实例化
    # 前向传播
    x = bn(x)
    print(x)

猜你喜欢

转载自blog.csdn.net/dgvv4/article/details/130567501