归一化那些事

为什么要归一化?

在对神经网络训练的过程中,未归一化的数据可能会导致陡峭的优化曲面,导致其在优化参数的过程中对学习率敏感,从而难以收敛,也就难以训练。

举个简单的例子,如果数据有两个特征维度,其中一个其值在 0-1 之间,另一个在 0-10000 之间,为了优化第一个特征,显然其学习率应该低,而为了优化第二个特征,其学习率应该高,这就产生了一个矛盾。而一般情况下为了训练能够收敛,只能使学习率低,那么就会导致第二个特征的训练收敛较慢,从而拖慢整个训练过程。

归一化就是将数据的分布进行重新映射,映射到适合神经网络进行训练,通常有以下两个步骤:

  • 移除均值:通过对数据分布进行平移,可以使数据均值变为 0
  • 缩放:调整方差,通过对数据值进行缩放,调整其分布范围和方差

由于移除均值和缩放对数据本身分布进行了调整,所以会消除数据的量纲,在推理阶段为了还原数据的量纲,通常会记录归一化的调整,在推理阶段进行还原。

对数据归一化之后还有以下几个好处:

  • 避免梯度消失和梯度爆炸:这是因为所有特征都进行了归一化,对学习率的敏感程度可以视为一样,不会有某一个维度单独出现梯度消失和梯度爆炸,一定程度上可以避免
  • 方便参数初始化:由于数据可以统一到某一个数据范围中,更容易对权重参数进行初始化

批量归一化 Batch Normalization

归一化对输入数据的分布进行了调整,使其更适合学习和训练。但在深层神经网络中,内部协变量偏移(Internal Covariate Shift)会导致输入归一化逐渐失效。

假设我们将输入数据归一化为均值 0,方差 1 的数据分布。对于输入来讲,其归一化确实是做到了,但是在深层神经网络中,每一层都会对数据分布产生一定的偏移。例如数据前向传播至第十层,经过前九层的计算,可能输入给第十层的数据的均值和方差已经变得很大,此时归一化就失去效果了。

在往深层网络传递的过程中,又会出现未归一化产生的问题。所以为了解决层与层之间会发送数据分布偏移的问题,引入了批量归一化,它作为一个层,对内部中间的数据分布再次进行调整,使其满足归一化。

同时它还能解决深层网络训练不稳定的问题,数据在深层网络传播的过程中,浅层数据的微小变化会在传导过程中不断放大,也就是深层网络对浅层数据非常敏感。浅层数据分布的微小改变会导致在深层数据分布的较大改变,也就导致深层的权重参数难以收敛,它需要不停地调整权重来适应这种数据分布的变化。

举个简单的例子,在神经网络的训练中,我们通常都是基于 batch 进行训练,这样可以大大利用 GPU 的并行计算能力。假设第一个批次的数据分布在 (-1,1) 之间,也许这批数据传导至深层其数据分布为 (-13, 25),第二个批次的数据分布在 (-0.9, 1.1) 之间,这批数据传导至深层的数据分布为 (12, 36)。这就导致了深层网络难以适应这么大的数据分布变化,也就难以收敛,一直处在不断的调整过程中。

每个 batch 的数据分布不会完全相同,因为 batch 只是完整数据集的一部分。

所以 Batch Normalization 做的就是对中间某一层的输出进行归一化,归一化之后作为下一层的输入。其归一化的依据是基于 batch 的,所以叫做 Batch Normalization。具体来讲,其公式如下:

x^=xμσ2+ϵ\hat{x} = \frac{x-\mu}{\sqrt{\sigma^2}+\epsilon}

其中 xx 是某一层的输出,μ\mu 是该批次该层输出的均值,σ\sigma 是该批次该层输出的方差,三者均为向量,其长度为数据特征维度,ϵ\epsilon 是一个极小值,防止除零。

在 Batch Normalization 的过程中,由于均值和方差的计算是基于 batch 的,而不是完整数据集的,所以 batch 的方差和均值和完整数据集的方差和均值会略有不同,这相当于引入噪声,模型为了适应这种噪声在一定程度上也会提高泛化能力。

但是这样简单的归一化有可能会带来一定的问题,直接将方差减小为 1 有可能会损失一定的表达能力,一些更高级的特征可能需要更宽的数值范围来表示。另外由于 batch 之间方差和均值并不完全一样,引入噪点的同时也可能带来一定的不稳定性,但是数据分布又是千差万别的,没有一个方法可以很好地处理所有数据。

所以 Batch Normalization 将调整数据分布的职责交还给模型,让他自己学如何调整分布,于是就引入了 γ\gammaβ\beta 两个可学习的参数,于是就可以最终得到 Batch Normalization 层的输出:

y=γx^+βy = \gamma \hat{x} + \beta

其中 γ\gammaβ\beta 都是长为特征维度的向量,因为每个特征维度可能需要不同的调整方式。γ\gamma 负责缩放,β\beta 负责平移。

总结来说,Batch Normalization 可以看作是一种人为提供给模型调整内部数据分布的网络层,模型可以根据实际情况自行裁决内部数据分布应该怎么调整。而首先将数据调整为均值 0 方差 1 可以看作是一种初始化,将数据先统一预处理为这样的分布,再交由模型调整,更方便模型进行学习。这样既实现了规范化的效果,又不损失模型的表达能力。

训练

在训练时,Batch Normalization 就如同上述所说的,基于每个 batch 计算出均值和方差,对内部数据调整之后,进行前向传播。其中 γ\gammaβ\beta 可以简单初始化为 1 和 0,即将数据分布调整为均值 0 方差 1。

前向传播完成后计算损失和反向传播,γ\gammaβ\beta 也将参与反向传播的计算。

推理

在模型训练完成后,在推理阶段,Batch Normalization 的均值和方差是基于所有 batch 计算的。

输入形状的影响

上述假设输入的 xx 是一个一维的向量,在 xx 是向量的情况下,某一层 batch 的数据形状是 (batch_size, channel),其均值的计算就是对每一行计算均值,其形状为 (1, channel),σ\sigma 也同理。

假设输入的 xx 是一个三维张量,例如图像(长、宽、通道)。此时某个 batch 的数据形状就是 (batch_size, height, width, channel),其均值和方差的计算就是对于每一个 channel,统计该 batch 中所有像素特征值的平均。计算过后的 μ\muσ\sigma 形状就是 (1, 1, 1, channel),也就是说每个通道都有一个独立的分布调整。

实际上也就反应了 Batch Normalization 是针对每一个特征维度(通道)进行数据分布的调整的。

缺点

由于 Batch Normalization 高度依赖于 batch,所以不适合于样本量太小的数据,一个 batch 如果样本量不够多,会影响最终效果。

代码

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

层归一化 Layer Normalization

由于 Batch Normalization 依赖于 batch,而 RNN、LSTM 这类网络很难应用,因为其输入的长度是不固定的,无法打包成统一的 batch。所以提出 Layer Normalization,希望将类似的思想应用在这类循环神经网络中,实际上就是让归一化的操作不依赖于 batch。

顾名思义,Layer Normalization 是基于层的,而不是基于 batch 中的数据的。简单来说,就是对某一层的输出进行归一化。

我们假设某一层某一个神经元的输出是 aia_i,那么就有如下的均值和方差计算方法:

μ=1Hi=1Haiσ=1Hi=1H(aiμ)2\mu = \frac{1}{H} \sum_{i=1}^H{a_i}\quad \sigma = \sqrt{\frac{1}{H}\sum_{i=1}^H{(a_i-\mu)^2}}

也就是说,均值为该层所有神经元输出的均值,方差为该层所有神经元输出的方差,那么经过层归一化之后的数据就变为:

ai^=aiμσ2+ϵ\hat{a_i} = \frac{a_i - \mu}{\sqrt{\sigma^2} + \epsilon}

其中 ϵ\epsilon 是一个极小值,防止除零,用 aa 表示该层的输出(向量),式子也可以简单写为:

a^=aμσ2+ϵ\hat{a} = \frac{a - \mu}{\sqrt{\sigma^2} + \epsilon}

同样借鉴了 Batch Normalization,为了归一化不会破坏模型的表达能力,仍然给了两个参数让模型可以自己决定数据分布如何调整,最终就有公式:

h=ga^+bh = g\odot \hat{a} + b

其中 gg 称为增益(gain),bb 为偏置,\odot 代表对应元素相乘。

训练和推理

由于是基于层的,所以也就没有什么特殊的处理,不需要进行其他额外的计算。

也由于是基于层的,和普通的全连接层并没有什么区别,所以可以很方便地嵌入到各种模型中,例如在 MLP 中,直接嵌入即可。在 RNN 或 LSTM 中,可以在每一个时间步之间添加一个 Layer Normalization 实现归一化。

所以 Layer Normalization 就适合于 batch 比较小的训练中,或是 RNN、LSTM 这种循环结构中,不会被 batch 所限制。

代码

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

虽然 RNN、LSTM 这类循环结构的网络不好使用 batch 进行训练,但是仍然有很多方法把时序数据打包成 batch,至于打包成 batch 之后为什么不能用 Batch Normalization 是因为其要求固定的 batch 大小(RNN 在训练时 batch 大小可以不一样),且 RNN、LSTM 这类循环网络对输入数据的顺序和时间步长比较敏感,引入 Batch Normalization 反而会引起不稳定。另外在推理时,输入长度的不稳定也会导致引入 Batch Normalization 的模型输出不稳定。

权重归一化 Weight Normalization

在 Batch Normalization 和 Layer Normalization 的归一化方法中,都是对数据进行归一化。归一化是为了优化算法更好地进行梯度下降,加速收敛、防止梯度爆炸和梯度消失。

但对数据进行归一化不适合于 LSTM、RNN 这类循环模型,因为其输入长度是不固定的,不容易进行归一化的计算;也不适用于噪声敏感的模型,例如强化学习,这是因为 BN 和 LN 是基于部分样本进行的归一化,其数据分布会和完整的训练集样本有偏差,这就相当于引入了噪声。

Weight Normalization 是直接对权重矩阵进行归一化,不需要对数据进行操作,类似的还有正则化技术,例如 L1 正则化、L2 正则化、Dropout 等,但其对权重的影响并不直接。

所以 Weight Normalization 一方面通过约束权重的大小,可以防止梯度爆炸和梯度消失,同时加速收敛;另一方面由于权重值被约束,也可以增加模型的泛化能力,防止过拟合。另外由于其不对数据进行操作,也可以应用在 LSTM、RNN 这类循环模型上。

原理

在标准的神经网络中,单层网络可以简单描述为:

y=ϕ(wx+b)y = \phi(wx+b)

其中 yy 是该层的输出,ww 是该层参数,xx 是输入,ϕ\phi 是激活函数,bb 是偏置。权重归一化通过将 ww 分解为 ggvv,其中 gg 是标量,代表缩放系数,vv 是单位向量,代表方向:

w=gvvw = g\frac{v}{||v||}

也就是相当于将权重向量分解为方向和大小,使得优化算法可以分别从方向和大小进行优化,从而梯度下降。换句话说,权重归一化使得优化有更多选择的空间,从而可以选择到更优的优化方向。

具体来讲,如果使 gg 固定为 w||w||,那么原式就等于:

w=wvvw=||w||\frac{v}{||v||}

只优化 vv,由于 v/vv/||v|| 仅代表方向,所以相当于只优化 ww 的方向,而保留其大小

如果使 vv 固定为 ww,那么原式等于:

w=gwww = g\frac{w}{||w||}

只优化 gg,相当于只优化 ww 的方向,而保留其大小。所以权重归一化通过优化 ggvv 两个参数,可以使得优化选择相比 ww 更多。个人直观理解是通过将 ww 解耦,将 ww 的优化难度降低,因为之前一次优化要同时考虑方向和大小,现在可以分开计算。

所以权重归一化并没有一个真正归一化的过程,而是给模型一个调整权重的方法,通过解耦,模型可以单独调整权重的大小和方向,从数据分布上来看,权重整体的大小方向的调整就可以对数据的分布产生整体的影响,从而实现类似于归一化的效果。

所以我觉得 Normalization 翻译成规范化更合适一些,它是对权重进行一种规范化的处理,它本身并没有对数据或权重做归一化的操作。

训练

由于 ww 解耦了,其反向传播的更新方式会略有不同,但具体推导本文就不涉及了。除此之外,和普通的权重更新没有其他不同,但是需要注意的是,由于参数变为两个,所以权重初始化会稍微不一样一点。

可以采取的一个方式是使用均值为 0,标准差为 0.05 的正态分布初始化 vvgg 和偏置 bb 使用第一批训练样本的统计量进行初始化。(具体可以参考原文:Weight Normalization: A Simple Reparameterization to Accelerate Training of Deep Neural Networks)

代码

在 Pytorch 中,Weight Normalization 实际上就是一个函数,具体实现细节需要参照 Pytorch 源码,这里就不涉及了。

weight_norm(module, name='weight', dim=0)

Args:
	module (Module): Pytorch 的 Module
	name (str, optional): 权重参数名
	dim (int, optional): 在哪个维度上计算范数

Returns:
	进行 weight_norm 之后的原模块

Example:

>>> m = weight_norm(nn.Linear(20, 40), name='weight')
>>> m
	Linear(in_features=20, out_features=40, bias=True)
>>> m.weight_g.size()
	torch.Size([40, 1])
>>> m.weight_v.size()
	torch.Size([40, 20])

Reference

[1] Batch Normalization 原理与实战
[2] 深度学习中的归一化技术全面总结
[3] 深度解析Batch normalization(批归一化)
[4] 模型优化之Weight Normalization
[5] Review — Weight Normalization: A Simple Reparameterization to Accelerate Training of Deep Neural Networks
[6] Weight Normalization Explained | Papers With Code
[7] 模型优化之Layer Normalization

上一篇 下一篇

评论 | 0条评论