归一化总结(Normalization)

神经网络的学习的本质就是学习数据的分布,使模型收敛,得到学习数据的特性。如果没有对数据进行归一化处理,那么每一批次训练的数据的分布就有可能不一样。从大的方面来讲,神经网络需要在多个分布中找到一个合适的平衡点;从小的方面来说,由于每层网络的输入数据在不断的变化,这会导致不容易找到合适的平衡点,最终使得构建的神经网络模型不容易收敛。当然,如果只是对输入数据做归一化,这样只能保证数据在输入层是一致的,并不能保证每层网络的输入数据分布是一致的,所以在神经网络模型的中间层也需要加入归一化处理。利用随机梯度下降更新参数时,每次参数更新都会导致网络中间每一层的输入的分布发生改变。越深的层,其输入分布会改变的越明显

内部协变量偏移(Internal Covariate Shift):也就是在训练过程中,隐层的输入分布老是变来变去, 每一层的参数在更新过程中,会改变下一层输入的分布,神经网络层数越多,变现的越明显。为了解决内部协变量偏移问题,这就要使得每一个神经网络层的输入的分布在训练过程保持一致。

批量归一化(Batch Normalization, BN)

为什么需要BN?

在深层网络中的训练中,由于反向传播算法,模型的参数在发生指数型变化(因为是链式传播),从而导致每一层的输入分布会发生剧烈变化,这就会引起两个问题:

  • 网络需要不断调整来适应输入数据分布的变化,导致网络学习速度的降低

  • 网络的训练过程容易陷入梯度饱和区,减缓网络收敛速度,训练不稳定。

  • 什么是梯度饱和区?

    当我们在神经网络中采用饱和激活函数(saturated activation function)时,例如sigmoid,tanh激活函数,很容易使得模型训练陷入梯度饱和区,此时梯度会变得很小接近于0,从而导致网络收敛很慢。有两种解决方法:

    1. 线性整流函数ReLU可以在一定程度上解决训练进入梯度饱和区的问题。

    2. 我们可以让激活函数的输入分布保持在一个稳定的状态来尽可能避免他们陷入梯度饱和区,这就是BN的想法。

解决上述问题
  • 固定网络每一层输入值的分布来缓解这两个问题。比如归一化到标准正态分布。
BN基本原理和公式

  • $\gamma $ 和 $\beta $ : 是两个可训练参数,主要是在一定程度上恢复数据本身的表达能力,对规范化后的数据进行线性处理。
BN的计算

选择 torch.nn.BatchNorm2d为例子,给定特征图 【N, H, W, C】,其中N是batch_size, HW特征图的宽高,C是通道数,那么上面公式的B就是下图【N,C,HW】中的蓝色部分:保留通道 C 计算均值和方差。

  • 也就是说 BN是对不同样本里面的同一个特征通道进行归一化处理,逐特征维度归一化,可训练参数 $\gamma $ 和 $\beta $ 的维度是 C。保留通道C计算均值和方差,故有2C个可训练参数(C个 $\gamma $ 和C个 $\beta $ ),2C个不可训练参数(均值mean和方差var)。

  • BN训练时的均值和方差该批次内数据相应维度的均值与方差

  • BN测试时的均值和方差基于所有批次的期望(无偏估计)计算所得,训练阶段根据 mini-batch 的数据计算均值和方差,并使用滑动平均法计算全局均值和方差;推理阶段使用训练阶段计算的全局均值和方差参与计算。

  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # torch版本
    def BatchNorm(x, gamma, beta, eps=1e-5):
    # x: input shape [N, C, H, W]
    N, C, H, W = x.shape
    mean = torch.mean(input=x, dim=[0,2,3], keepdim=True)
    var = torch.var(input=x, dim=[0,2,3], keepdim=True)
    # mean, var shape : [1, C, 1, 1]
    x = (x - mean) / torch.sqrt(var + eps)
    return x * gamma + beta

    # numpy版本 更对(两个可学习的参数)
    def batch_normalize2d(x, alpha=1.0, beta=0.0, eps=1e-5):
    # x: input shape [N, C, H, W]
    mean = np.mean(x, axis=(0, 2, 3), keepdims=True) # [1, C, 1, 1]
    variance = np.var(x, axis=(0, 2, 3), keepdims=True) # [1, C, 1, 1]
    alpha = np.zeros_like(mean) + alpha #[1,C,1,1]
    beta = np.zeros_like(mean) + beta #[1,C,1,1]
    x = (data - mean) / np.sqrt(variance + eps)
    return x * alpha + beta
BN的优点缺点
  • 优点:加快模型训练时的收敛速度,使得模型训练过程更加稳定,避免梯度爆炸或者梯度消失,同时起到一定的正则化作用。

    • BN将每层的数据进行标准化,并通过可学习参数 $\gamma $ 和 $\beta $ 两个学习参数来调整这种分布。使得网络中每层输入数据的分布相对稳定,加速模型的训练速度,解决“internal covariate shift”的问题。
    • 允许网络使用饱和性激活函数(sigmoid,tanh),缓解了梯度消失的问题避免了梯度弥散和爆炸。BN可以控制数据的分布范围,在遇到sigmoid或者tanh等激活函数时,不会使数据落在饱和区域导致梯度弥散。并且BN可以避免ReLU激活函数数据死亡的问题。
    • 降低权重初始化的困难,在深度网络中,网络的最终训练效果也受到初始化的影响,初始化决定了神经网络最终会收敛到哪个局部最小值中,具有随机性。通过BN来标准化分布,可以降低初始化权重的影响
    • 因为不同的mini_batch均值和方差都有所不同,这就为网络的学习过程增加了随机噪音,与dropout随机关闭神经元给网络带来的噪音类似,一定程度上起到了正则化的作用。在正则化方面,一般全连接层用dropout,卷积层用BN。
    • BN中的 $\gamma $ 和 $\beta $ 的作用:
      • 保证了模型的capacity,意思就是,γ和β作为调整参数可以调整被BN刻意改变过后的输入,即能够保证能够还原成原始的输入分布。BN对每一层输入分布的调整有可能改变某层原来的输入,当然有了这两个参数,经过调整也可以不发生改变,既可以改变同时也可以保持原输入,那么模型的容纳能力(capacity)就提升了。
      • 适应激活函数,如果是sigmoid函数,那么BN后的分布在0-1之间,由于sigmoid在接近0的地方趋于线性,非线性表达能力可能会降低,因此通过γ和β可以自动调整输入分布,使得非线性表达能力增强。
      • 如果激活函数为ReLU,那么意味着将有一半的激活函数无法使用,那么通过β可以进行调整参与激活的数据的比例,防止dead-Relu问题。
  • 缺点

    • BN特别依赖于大的batch_size,而由于显卡等硬件限制,我们大多数batch_size都设置的较小,性能会急剧下降。

    • 对于序列化数据的网络不太适用,尤其是序列样本长度不同时。如RNN,LSTM。

层归一化(Layer Normalization)

如果一个神经元的净输入分布在神经网络中是动态变化的,比如循环神经网络,那么无法应用批归一化操作。

层归一化和批归一化不同的是,层归一化是对一个中间层的所有神经元进行归一化

注意:LayerNorm的均值和方差是根据单个数据计算的,所以不需要计算全局均值和全局方差。类似BN的计算公式,只不过是在计算均值和方差是在不同的维度上进行的。

LN基本原理和公式

基本原理和公式与 BN的差不多,只不过是计算均值和方差在不一样的维度罢了。

LN的计算

选择 torch.nn.LayerNorm为例(normalized_shape=(C,H,W)),对于【N, C, H, W】的特征图(下图可以理解为【N ,C, H*W】,保留通道 N 计算均值和方差。

  • 注意与BatchNorm2d的不同:

    • 当torch.nn.LayerNorm的elementwise_affine为True时, $\gamma $ 和 $\beta $ 的参数数量分别时 C*H*W 。
    • 当torch.nn.LayerNorm的elementwise_affine为False时,没有 $\gamma $ 和 $\beta $ 参数
  • 训练和推理阶段:均值和方差根据输入数据计算,不需要在训练集上用滑动平均方法计算,所以不保存均值和方差。

  • 代码:实现的时候γ和β参数的维度和一开始想的不一样(一开始以为和batch_norm2d一样,那应该是2N个可学习参数,实际上是根据元素数量来的),看了源码才发现没有均值和方差这2个参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # torch版本
    def LayerNorm(x, gamma, beta, eps=1e-5):
    # x: input shape [N, C, H, W]
    N, C, H, W = x.shape
    mean = torch.mean(input=x, dim=[1,2,3], keepdim=True)
    var = torch.var(input=x, dim=[1,2,3], keepdim=True)
    # mean, var shape: [N, 1, 1, 1]
    x = (x - mean) / torch.sqrt(var + eps)
    return x * gamma + beta

    # numpy版本 更对(两个可学习的参数)
    def layer_normalize2d(x, alpha=1.0, beta=0.0, eps=1e-5):
    # x: input shape [N, C, H, W]
    mean = np.mean(x, axis=(1, 2, 3), keepdims=True) # [N, 1, 1, 1]
    variance = np.var(x, axis=(1, 2, 3), keepdims=True) # [N, 1, 1, 1]
    alpha = np.zeros(shape=(1,) + x.shape[1:]) + alpha #[1,C,H,W]
    beta = np.zeros(shape=(1,) + x.shape[1:]) + beta #[1,C,H,W]
    x = (x- mean) / np.sqrt(variance + eps)
    return x * alpha + beta

    在这种方法中,batch(N) 中的每个示例都在 [C, H, W] 维度上进行了归一化。 与 BN 一样,它可以加速和稳定训练,并且不受批次的限制。 此方法可用于批量为 1 的在线学习任务。

实例归一化(Instance Normalization)

IN是针对图像像素做归一化处理,适用于生成模型中,例如图像的风格化迁移等。

IN基本原理和公式

基本原理和公式与 BN的差不多,只不过是计算均值和方差在不一样的维度罢了。

IN的计算

以 torch.nn.InstanceNorm2d(track_running_stats=True, affine=True)为例,对于【N, C, H, W】的特征图(下图可以理解为[N ,C, H*W]),保留通道N和C计算均值和方差。

  • 相信有很多朋友都是根据这张图来理解Instance Norm,一眼看上去均值mean和方差variance的维度应该是[N, C],但是打印出runing_mean和runing_var维度一看,竟然是[C]!What?

    好家伙,经过多次测试,终于搞明白了,原来在归一化计算(一、归一化 中的公式)的时候,均值和方差维度是[N, C],但是因为batch_size可能会变化,所以running_mean和running_var保存的时候把[N,C]的均值和方差取了个均值,所以runing_mean和runing_var维度是[C]。

  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #torch版本
    def InstanceNorm(x, gamma, beta, eps=1e-5):
    # x: input shape [N, C, H, W]
    N, C, H, W = x.shape
    mean = torch.mean(input=x, dim=[2,3], keepdim=True)
    var = torch.var(input=x, dim=[2,3], keepdim=True)
    # mean, var shape: [N, C, 1, 1]
    x = (x - mean) / torch.sqrt(var + eps)
    return x * gamma + beta

    #numpy版本
    def instance_normalize2d(x, alpha=1.0, beta=0.0, eps=1e-5):
    mean = np.mean(x, axis=(2, 3), keepdims=True) # [N, C, 1, 1]
    variance = np.var(x, axis=(2, 3), keepdims=True) # [N, C, 1, 1]
    alpha = np.zeros_like(mean) + alpha
    beta = np.zeros_like(mean) + beta
    # print(mean[:, :, 0, 0].mean(axis=0), variance[:, :, 0, 0].mean(axis=0))
    x = (x - u) / np.sqrt(variance + eps)
    return x * alpha + beta

组归一化(Group Normalization)

InstanceNorm就是GroupNorm的特例,当Group Norm分组和channels相同时,就是instance norm,当分组为1时,就是LayerNorm。 GN 将通道分成组并在它们之间进行标准化。 该方案使计算独立于批量大小。

GN基本原理和公式

基本原理和公式与 BN的差不多,只不过是计算均值和方差在不一样的维度罢了。

GN计算

以 torch.nn.GroupNorm为例,对于【N, C, H, W】的特征图(下图可以理解为[N ,C, H*W]),需要先将通道C维度划分为G个组得到新的特征图【N, G,C/G H, W】,然后保留通道N和G计算均值和方差。

  • 代码:

    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
    def GroupNorm(x, gamma, beta, G, eps=1e-5): 
    # x: input features with shape [N, C, H, W]
    # G : number of groups
    N, C, H, W = x.shape
    x = torch.reshape(input=x, shape=[N, G, C // G, H, W])
    mean = torch.mean(input=x, dim=[2,3,4], keepdim=True)
    var = torch.var(input=x, dim=[2,3,4], keepdim=True)
    # mean, var shape : [N, G, 1, 1, 1]
    x = (x - mean) / torch.sqrt(var + eps)
    x = torch.reshape(input=x, shape=[N, C, H, W])
    return x * gamma + beta
    # 可学习参数的不同(更好)
    def GroupNorm(x, G, gamma=1.0, beta=0.0, eps=1e-5):
    # x: input features with shape [N, C, H, W]
    # G : number of groups
    N, C, H, W = x.shape
    x = torch.reshape(input=x, shape=[N, G, C // G, H, W])
    mean = torch.mean(input=x, dim=[2,3,4], keepdim=True)
    var = torch.var(input=x, dim=[2,3,4], keepdim=True)
    # mean, var shape : [N, G, 1, 1, 1]
    gamma = np.zeros_like(mean) + gamma
    beta = np.zeros_like(mean) + beta
    x = (x - mean) / torch.sqrt(var + eps)
    x = torch.reshape(input=x, shape=[N, C, H, W])
    return x * gamma + beta

可切换归一化(Switchable Normalization)

BN、LN、IN这些归一化方法往往能提升模型性能,但当你接收一个任务时,具体选择哪个归一化方法仍然需要人工选择,这往往需要大量的对照实验或者开发者本身优秀的经验才能选出最合适的归一化方法,因此SN出场了。

它的算法核心在于提出了一个可微的归一化层,可以让模型根据数据来学习到每一层该选择的归一化方法,或是三个归一化方法的加权和。所以SN是一个任务无关的归一化方法,在分类,检测,分割,IST,LSTM等各个方向的任务中,均取得了非常好的效果。

SN算法是为三组不同的γ和β分别学习共六个标量值$(W_k,W_k^{‘})$,得到他们的加权和:

其中=$W={IN,LN,BN}$。这样仅仅通过增加六个参数,由三种不同统计方法的共同优化,得到更好的归一化结果,但训练过程比较复杂。因此其比其他的归一化方法存在更高的鲁棒性、通用性和多样性。

权重归一化(Weight Normalization)

权重归一化(Weight Normalization)是对神经网络的连接权重进行归一化,通过再参数化(Reparameterization)方法,将连接权重分解为长度和方向两种参数。

已经对输入和层输出进行了标准化,唯一剩下的就是权重。因为它们可以在没有任何控制的情况下变大,尤其是当我们无论如何都要标准化输出时。 通过标准化权重,我们实现了更平滑的损失和更稳定的训练。

  • 代码

    1
    2
    3
    4
    5
    6
    7
    def WeightStand(w, eps=1e-5): 
    # w: input features shape [Cin, Cout, kernel_size, kernel_size]
    mean = torch.mean(input=w, dim=[0,2,3], keepdim=True)
    var = torch.var(input=w, dim=[0,2,3], keepdim=True)
    # mean, var shape : [1, Cout, 1, 1]
    w = (w - mean) / torch.sqrt(var + eps)
    return w

归一化对比

  • BN:取整个Batch-size,多个样本做归一化

  • LN:取同一个样本的不同通道做归一化,逐样本归一化。是由Hinton及其学生提出,可以很好的用在序列型网络如RNN中,同时LN在训练和预测时均值方差都由当前样本确定,这与BN不同。可以不进行批训练。

  • IN:仅仅对每一个样本的每一个通道做归一化。主要用于生成模型中, feature map 的各个 channel 的均值和方差会影响到最终生成图像的风格,如图片风格迁移,图片生成结果主要依赖于某个图像实例,所以BN不行

  • GN:介于LN和IN之间的一种方法,对每个样本的多个通道进行归一化。用由何凯明团队提出,优化了BN在batch_size较小时的劣势,适用于占用显存较大的任务,如图像分割,一般为16个通道为一组(经验)

目前BN使用最为广泛,能加快模型收敛速度,提高网络泛化性,但是存在小batch时错误率的问题。LN多用于RNN中,且不需要批训练,但在输入的特征区别较大时不建议使用。

IN用于图像的风格化迁移方面,不会受到通道数和batch size的影响,但特征通道之间的存在相关性时,则不建议使用。GN避免了BN的问题,训练时与batch size大小无关,但验证效果比BN差一些。SN是让模型根据数据来学习到每一层该选择的归一化方法或是BN、IN、GN归一化方法的加权和,但计算复杂。WN在噪声较大时能取得更好的效果,不受限于batch,但是对初始参数较为敏感,目前使用极少。