深入理解深度学习中的Normalization操作

Normalization, 即标准化, 和普通的数据标准化类似, 是将分散的数据统一的一种做法, 也是优化神经网络的一种方法。Normalization 可以将数据统一规格, 能让机器学习更容易学习到数据之中的规律。

在深度学习中,Normalization已经成为了标准技术。2015 年Google首先提出了Batch Normalization(BN),源论文地址。自 BN 之后, Layer Norm / Weight Norm / Cosine Norm 等也横空出世。

如果我们仅仅停留在使用Normalization上,那么现成的框架只需要一行就可以添加到模型中。我们真正想知道的是,隐藏在BN的背后深度学习的问题,以及这样简单的操作是如何奏效的

本文在整理参考文献【1-4】的基础上,加入了一些自己的理解,试图记录一下自己是如何理解并应用Normalization的。

一、深度学习的Normalization具体做什么?

由于有多种Normalization的方法,如Batch Normalization(BN),Layer Norm(LN), Weight Norm(WN), Cosine Norm(CN)等,我这里以最经典的Batch Normalization为例说明Normalization到底是做的什么操作。

需要知道得是,BN可以在激活函数 σ \sigma σ之前,也可以在 σ \sigma σ之后,本篇博客默认Normalization在激活函数 σ \sigma σ之前。

有如下图的神经网络:

我们定义 z 1 2 z_1^2 z12
z 1 2 = w 11 2 x 1 + w 12 2 x 2 + w 13 2 x 3 + b 1 2 z_1^2 =w_{11}^2x_1 + w_{12}^2x_2 + w_{13}^2x_3 + b_1^{2} z12=w112x1+w122x2+w132x3+b12

因此有:
a 1 2 = σ ( z 1 2 ) = σ ( w 11 2 x 1 + w 12 2 x 2 + w 13 2 x 3 + b 1 2 ) a_1^2=\sigma(z_1^2) = \sigma(w_{11}^2x_1 + w_{12}^2x_2 + w_{13}^2x_3 + b_1^{2}) a12=σ(z12)=σ(w112x1+w122x2+w132x3+b12)

我们做得BN,就是对 z 1 2 z_1^2 z12进行标准化。假设Batch size为1000,那么就会有1000个 z 1 2 z_1^2 z12,我们对这1000个 z 1 2 z_1^2 z12求标准化,使得这1000个值满足均值为0,标准差为1的高斯分布。然后进行激活,激活后的值,继续前向传播。

这就是BN的过程,很简单。

我们再举一个例子,形象地说明做Normalization的好处。

我们知道,在神经网络中, 数据分布对训练会产生影响. 比如某个神经元 x x x 的值为1, 某个 Weights 的初始值为 0.1, 这样后一层神经元计算结果就是 W x = 0.1 Wx = 0.1 Wx=0.1; 又或者 x = 20 x = 20 x=20, 这样 W x Wx Wx 的结果就为 2. 现在还不能看出什么问题, 但是, 当我们加上一层激励函数, 激活这个 W x Wx Wx 值的时候, 问题就来了. 如果使用 像 tanh 的激励函数, W x Wx Wx 的激活值就变成了 ~0.1 和 ~1, 接近于 1 的部分已经处在了激励函数的饱和阶段, 也就是如果 x x x 无论再怎么扩大, tanh 激励函数输出值也还是接近1. 换句话说, 神经网络在初始阶段已经不对那些比较大的 x x x特征范围 敏感了. 这样很糟糕, 想象我轻轻拍自己的感觉和重重打自己的感觉居然没什么差别, 这就证明我的感官系统失效了.

当然, x x x不仅可以是输入层,在隐含层也可能出现这样的情况。

通过下图我们可以看到BN的效果:

之前说过, 计算结果在进入激励函数前的值很重要, 如果我们不单单看一个值, 我们可以说, 计算结果值的分布对于激励函数很重要. 对于数据值大多分布在这个区间的数据, 才能进行更有效的传递. 对比这两个在激活之前的值的分布. 上者没有进行 normalization, 下者进行了 normalization, 这样当然是下者能够更有效地利用 tanh 进行非线性化的过程.

没有 normalize 的数据 使用 tanh 激活以后, 激活值大部分都分布到了饱和阶段, 也就是大部分的激活值不是-1, 就是1, 而 normalize 以后, 大部分的激活值在每个分布区间都还有存在. 再将这个激活后的分布传递到下一层神经网络进行后续计算, 每个区间都有分布的这一种对于神经网络就会更加有价值.

我们需要知道得是,Batch normalization 不仅仅 normalize 了一下数据, 它还进行了反 normalize。关于这点,我们在第三章会讲到。

二、为什么深度学习中需要Normalization?

第一章我们已经看到了Normalization的好处。这里我们深入解释一下为什么深度学习中需要Normalization。

其中一个很重要的原因是在深度学习中会存在internal covariate shift(ICS)。

我们先从covariate shift说起,covariate shift是缘于统计学的一个概念,它描述了源域(S)和目标域(T)边缘分布的不一致,即 P ( X t e s t ) ≠ P ( X t r a i n ) P(X_{test})\neq P(X_{train}) P(Xtest)̸=P(Xtrain),但是他们的条件分布却是相同的,即 P T ( y ∣ x ) = P S ( y ∣ x ) P_T(y|x)= P_S(y|x) PT(yx)=PS(yx)

简单在机器学习中来说,从概率的视角,条件分布 P ( y ∣ x ) P(y|x) P(yx)是我们得到的模型,如果我们的训练集的 X t r a i n X_{train} Xtrain分布与测试集的 X t e s t X_{test} Xtest分布存在差异,那么就会出现covariate shift,此时会出现两个结果:

  • 我们利用从训练集得到的模型,去在测试集上做性能评估,得到的并不会是模型的真实水平。

  • 训练集和测试集的分布差异太大,我们训练出的模型并不是真实的模型。

CSDN图标

如图,在样本空间中,红色点表示训练集,黑色点表示测试集,真实的拟合直线是黑线,而我们学习到的却是红线。

我们期望数据是“独立同分布”的,即independent and identically distributed,简称为 i.i.d. 独立同分布并非所有机器学习模型的必然要求(比如 Naive Bayes 模型就建立在特征彼此独立的基础之上,而Logistic Regression 和 神经网络 则在非独立的特征数据上依然可以训练出很好的模型),但独立同分布的数据可以简化常规机器学习模型的训练、提升机器学习模型的预测能力,已经是一个共识。

独立同分布要求训练集和测试集的样本都从同一个分布独立采样而来,这在理论上是一个强力的保证。但在实际过程中,我们无法做出完全的iid分布,我们一般会采用权重分布来参与学习,使得训练集和测试集分布差异较小的样本点来占据更大的权重。

最重要的是,所谓的标准化就是减小分布差异的一种方式,因为预处理会让每个特征服从标准高斯分布。我们在数据预处理的时候,往往会得到训练集的均值和标准差,并将其直接用在测试集上,这种信息共享的方式可以说是将独立同分布用到了极致。

但在深度学习中,这个现象加剧为internal covariate shift。从表示学习的角度来看,神经网络前面的所有层,都可以看做获得一个更好的表示,隐藏单元的最大作用是非线性,使得神经网络在最基本的乘法中获得足够的复杂性,只有最后一层将表示转化为输出,所以只有最后一层可以看作统计学习中的学习器

正因为前面所有的层都是在获得一个更好的表示,而非直接做学习,所以经过层和激活函数的处理,我们获得的还是 P ( x ) P(x) P(x),而非 P ( y ∣ x ) P(y|x) P(yx),而这样是非常有可能加剧covariate shift的程度(当然,也有可能减弱),这就是Internal covariate shift中Internal (内部的)的含义。

大家细想便会发现,的确,对于神经网络的各层输出,由于它们经过了层内操作作用,其分布显然与各层对应的输入信号分布不同,而且差异会随着网络深度增大而增大,可是它们所能“指示”的样本标记(label)仍然是不变的,这便符合了internal covariate shift的定义。

三、Normalization 的通用框架与基本思想

这里我们借用参考文献【1】的思路,该文的作者整理出了一个理解Normalization 的通用框架。

我们以神经网络中的一个普通神经元为例。神经元接收一组输入向量 x = ( x 1 , x 2 , . . . x d ) \bold{x}=(x_1,x_2,...x_d) x=(x1,x2,...xd),在将 x \bold{x} x送给神经元之前,先对其做平移和伸缩变换, 将 x \bold{x} x 的分布标准化成在固定区间范围的标准分布。

通用变换框架就如下所示:
h = f ( g ⋅ x − μ σ + b ) (1) h=f(\bold{g}\cdot \frac{\bold{x}-\mu}{\sigma}+\bold{b})\qquad \text{(1)} h=f(gσxμ+b)(1)

我们来看看这个公式中的各个参数。
(1) μ \bold{\mu} μ平移参数(shift parameter), σ \bold{\sigma} σ缩放参数(scale parameter)。通过这两个参数进行 shift 和 scale 变换: x ^ = x − μ σ \bold{\hat{x}}=\frac{\bold{x}-\bold{\mu}}{\bold{\sigma}} x^=σxμ 得到的数据符合均值为 0、方差为 1 的标准分布。

(2) b \bold{b} b再平移参数(re-shift parameter), g \bold{g} g再缩放参数(re-scale parameter)。将 上一步得到的 x ^ \bold{\hat{x}} x^ 进一步变换为: y = g ⋅ x ^ + b \bold{y}=\bold{g}\cdot \bold{\hat{x}} + \bold{b} y=gx^+b

最终得到的数据符合均值为 b \bold{b} b 、方差为 g 2 \bold{g}^2 g2 的分布。

可能有些读者会有疑问,第一步都已经得到了标准分布,第二步怎么又给变走了?

答案是——为了保证模型的表达能力不因为标准化而下降

我们可以看到,第一步的变换将输入数据限制到了一个全局统一的确定范围(均值为 0、方差为 1)。下层神经元可能很努力地在学习,但不论其如何变化,其输出的结果在交给上层神经元进行处理之前,将被粗暴地重新调整到这一固定范围。

所以,为了尊重底层神经网络的学习结果,我们将标准化后的数据进行再平移和再缩放,使得每个神经元对应的输入范围是针对该神经元量身定制的一个确定范围(均值为 b \bold{b} b 、方差为 g 2 \bold{g}^2 g2 )。rescale 和 reshift 的参数都是可学习的,这就使得 Normalization 层可以学习如何去尊重底层的学习结果。

除了充分利用底层学习的能力,另一方面的重要意义在于保证获得非线性的表达能力。Sigmoid 等激活函数在神经网络中有着重要作用,通过区分饱和区和非饱和区,使得神经网络的数据变换具有了非线性计算能力。而第一步的标准化会将几乎所有数据映射到激活函数的非饱和区(线性区),仅利用到了线性变化能力,从而降低了神经网络的表达能力。而进行再变换,则可以将数据从线性区变换到非线性区,恢复模型的表达能力。

那么问题又来了——经过这么的变回来再变过去,会不会跟没变一样?

不会。因为,再变换引入的两个新参数 g g g b b b,可以表示旧参数作为输入的同一族函数,但是新参数有不同的学习动态。在旧参数中, x \bold{x} x 的均值取决于上一层神经网络的复杂关联;但在新参数中, y = g ⋅ x ^ + b \bold{y}=\bold{g}\cdot \bold{\hat{x}} + \bold{b} y=gx^+b 仅由 b \bold{b} b 来确定,去除了与上一层计算的密切耦合。简单来说,原始的 μ \bold{\mu} μ σ \bold{\sigma} σ将输入数据限制到了一个均值为 0、方差为 1的范围,这太严格了,会使得模型的表达能力下降,现在的 g g g b b b将输入数据限制到了一个均值为 b \bold{b} b、方差为 g 2 \bold{g}^2 g2 的范围,既使得数据在一定的范围内,缓解了ICS的问题,又在一定程度上保证了模型的表达能力。

四、主流 Normalization 方法梳理

对照公式(1),我们来梳理主流的四种标准化方法。
h = f ( g ⋅ x − μ σ + b ) (1) h=f(\bold{g}\cdot \frac{\bold{x}-\mu}{\sigma}+\bold{b})\qquad \text{(1)} h=f(gσxμ+b)(1)

4.1 Batch Normalization —— 纵向标准化

Batch Normalization 其标准化针对单个神经元进行,利用网络训练时一个 mini-batch 的数据来计算该神经元 z i z_i zi 的均值和方差,因而称为 Batch Normalization。符号与第一章的符号定义相同。
μ i = 1 M ∑ z i \mu_i=\frac{1}{M}\sum z_i μi=M1zi

σ i = 1 M ∑ ( z i − μ i ) 2 + ϵ \sigma_i=\sqrt{\frac{1}{M}\sum (z_i-\mu_i)^2+\epsilon } σi=M1(ziμi)2+ϵ
其中 M M M 是mini-batch 的大小。

如果把一层神经元看成是水平排列,BN 可以看做一种纵向的标准化,即mini-batch 个数据叠在一起进行的标准化。由于 BN 是针对单个神经元定义的,因此标准公式中的计算均为 element-wise 的。

BN 独立地标准化每一个神经元的 z i z_i zi ,但标准化的参数是一个 mini-batch 的均值和标准差。这就要求 每一个 mini-batch 的统计量是整体统计量的近似估计,或者说每一个 mini-batch 彼此之间,以及和整体数据,都应该是近似同分布的。如果每个 mini-batch的原始分布差别很大,那么不同 mini-batch 的数据将会进行不一样的数据变换,这就增加了模型训练的难度。

因此,BN 比较适用的场景是:每个 mini-batch 比较大,数据分布比较接近。在进行训练之前,要做好充分的 shuffle. 否则效果会差很多

4.2 Layer Normalization —— 横向标准化

层标准化就是针对 BN 的上述不足而提出的。与 BN 不同,LN 是一种横向的标准化,即对一层的神经元进行的标准化。它综合考虑一层所有神经元的输入,计算该层的平均输入值和输入方差,然后用同一个标准化操作来转换各个神经元的输入。
μ i = ∑ i z i \mu_i=\sum_{i} z_i μi=izi

σ i = ∑ i ( z i − μ i ) 2 + ϵ \sigma_i=\sqrt{\sum_{i} (z_i-\mu_i)^2+\epsilon } σi=i(ziμi)2+ϵ

其中 i i i枚举了该层所有的输入神经元。对应到标准公式(1)中,四大参数 μ \bold{\mu} μ, σ \bold{\sigma} σ, g \bold{g} g, b \bold{b} b 均为标量(BN中是向量),所有输入共享一个标准化变换。

LN 针对单个训练样本进行,不依赖于其他数据,因此可以避免 BN 中受 mini-batch 数据分布影响的问题,可以用于 小mini-batch场景、动态网络场景和 RNN,特别是自然语言处理领域。此外,LN 不需要保存 mini-batch 的均值和方差,节省了额外的存储空间。

但是,BN 的转换是针对单个神经元可训练的——不同神经元的输入经过再平移和再缩放后分布在不同的区间,而 LN 对于一整层的神经元训练得到同一个转换——所有的输入都在同一个区间范围内。如果不同输入特征不属于相似的类别(比如颜色和大小),那么 LN 的处理可能会降低模型的表达能力

4.3 Weight Normalization —— 参数标准化

前面我们讲的模型框架
h = f ( g ⋅ x − μ σ + b ) (1) h=f(\bold{g}\cdot \frac{\bold{x}-\mu}{\sigma}+\bold{b})\qquad \text{(1)} h=f(gσxμ+b)(1)

中,最普遍的变换是线性变换,即 f w ( x ) = w ⋅ x f_\bold{w}(\bold{x})=\bold{w}\cdot\bold{x} fw(x)=wx。 这里特别需要注意的是: f w ( x ) f_\bold{w}(\bold{x}) fw(x)中的 x \bold{x} x是已经激活后的值,而公式(1)的 x \bold{x} x是激活前的值,这是2个不同的 x \bold{x} x,或者说公式(1)的 x \bold{x} x先进行标准化,再进行激活,就形成了 f w ( x ) f_\bold{w}(\bold{x}) fw(x)中的 x \bold{x} x

BN 和 LN 均将标准化应用于输入的特征数据 x \bold{x} x ,而 WN 则另辟蹊径,将标准化应用于线性变换函数的权重 w \bold{w} w ,这就是 WN 名称的来源。

具体而言,WN 提出的方案是,将权重向量 w \bold{w} w 分解为向量方向 v ^ \hat{\bold{v}} v^ 和向量模 g g g 两部分:
w = g ⋅ v ^ = g ⋅ v ∣ ∣ v ∣ ∣ {\bold{w}} = g\cdot\hat{\bold{v}} = g\cdot\frac{\bold{v}}{\bold{||v||}}\\ w=gv^=gvv

其中 v \bold{v} v 是与 w \bold{w} w 同维度的向量, ∣ ∣ v ∣ ∣ \bold{||v||} v是二范数,因此 v ^ \hat{\bold{v}} v^是单位向量,决定了 w \bold{w} w 的方向; g g g 是标量,决定了 w \bold{w} w 的长度。由于 ∣ ∣ w ∣ ∣ ≡ ∣ g ∣ ||{\bold{w}}|| \equiv |g| wg(恒等于) ,因此这一权重分解的方式将权重向量的欧氏范数进行了固定,从而实现了正则化的效果。

乍一看,这一方法似乎脱离了我们前文所讲的通用框架(1)?

并没有。其实从最终实现的效果来看,异曲同工。我们来推导一下看。
f w ( W N ( x ) ) = w ⋅ W N ( x ) = g ⋅ v ∣ ∣ v ∣ ∣ ⋅ x = v ⋅ g ⋅ x ∣ ∣ v ∣ ∣ = f v ( g ⋅ x ∣ ∣ v ∣ ∣ ) f_\bold{w}(WN(\bold{x}))=\bold{w}\cdot WN(\bold{x}) = g\cdot\frac{\bold{v}}{\bold{||v||}} \cdot\bold{x} \\= \bold{v}\cdot g\cdot\frac{\bold{x}}{\bold{||v||}}=f_\bold{v}(g\cdot\frac{\bold{x}}{\bold{||v||}}) fw(WN(x))=wWN(x)=gvvx=vgvx=fv(gvx)

对照一下前述框架:

h = f ( g ⋅ x − μ σ + b ) h=f\left(\bold{g}\cdot\frac{\bold{x}-\bold{\mu}}{\bold{\sigma}}+\bold{b}\right) h=f(gσxμ+b)

我们只需令:

σ = ∣ ∣ v ∣ ∣ , μ = 0 , b = 0 \bold{\sigma} = \bold{||v||}, \quad \bold{\mu}=0, \quad \bold{b}=0 σ=v,μ=0,b=0

就完美地对号入座了

回忆一下,BN 和 LN 是用输入的特征数据的方差对输入数据进行 scale,而 WN 则是用 神经元的权重的欧氏范式对输入数据进行 scale。虽然在原始方法中分别进行的是特征数据标准化和参数的标准化,但本质上都实现了对数据的标准化,只是用于 scale 的参数来源不同。

另外,我们看到这里的标准化只是对数据进行了 scale,而没有进行 shift,因为我们简单地令 μ = 0 \bold{\mu}=0 μ=0. 但事实上,这里留下了与 BN 或者 LN 相结合的余地——那就是利用 BN 或者 LN 的方法来计算输入数据的均值 \bold{\mu} 。

WN 的标准化不直接使用输入数据的统计量,因此避免了 BN 过于依赖 mini-batch 的不足,以及 LN 每层唯一转换器的限制,同时也可以用于动态网络结构。

4.4 Cosine Normalization —— 余弦标准化

Normalization 还能怎么做?

我们再来看看神经元的经典变换 f w ( x ) = w ⋅ x f_\bold{w}(\bold{x})=\bold{w}\cdot\bold{x} fw(x)=wx.

对输入数据 x \bold{x} x 的变换已经做过了,横着来是 LN,纵着来是 BN。

对模型参数 w \bold{w} w 的变换也已经做过了,就是 WN。

好像没啥可做的了。

然而天才的研究员们盯上了中间的那个点,对,就是 ⋅ \cdot

他们说,我们要对数据进行标准化的原因,是数据经过神经网络的计算之后可能会变得很大,导致数据分布的方差爆炸,而这一问题的根源就是我们的计算方式——点积,权重向量 w \bold{w} w 和 特征数据向量 x \bold{x} x 的点积。向量点积是无界(unbounded)的啊!

那怎么办呢?我们知道向量点积是衡量两个向量相似度的方法之一。哪还有没有其他的相似度衡量方法呢?有啊,很多啊!夹角余弦就是其中之一啊!而且关键的是,夹角余弦是有确定界的啊, [ − 1 , 1 ] [-1, 1] [1,1] 的取值范围,多么的美好!

于是,Cosine Normalization 就出世了。他们不处理权重向量 w \bold{w} w ,也不处理特征数据向量 x \bold{x} x ,就改了一下线性变换的函数:
f w ( x ) = c o s θ = w ⋅ x ∣ ∣ w ∣ ∣ ⋅ ∣ ∣ x ∣ ∣ f_\bold{w}(\bold{x})=cos \theta = \frac{\bold{w}\cdot\bold{x}}{\bold{||w||}\cdot\bold{||x||}}\\ fw(x)=cosθ=wxwx

其中 θ \theta θ w \bold{w} w x \bold{x} x 的夹角。然后就没有然后了,所有的数据就都是 [ − 1 , 1 ] [-1, 1] [1,1] 区间范围之内了。

不过,回过头来看,CN 与 WN 还是很相似的。我们看到上式中,分子还是 w \bold{w} w x \bold{x} x 的内积,而分母则可以看做用 w \bold{w} w x \bold{x} x 二者的模之积进行标准化。对比一下 WN 的公式:
f w ( W N ( x ) ) = f v ( g ⋅ x ∣ ∣ v ∣ ∣ ) f_\bold{w}(WN(\bold{x}))=f_\bold{v}(g\cdot\frac{\bold{x}}{\bold{||v||}})\\ fw(WN(x))=fv(gvx)

一定程度上可以理解为,WN 用 权重的模 ∣ ∣ v ∣ ∣ \bold{||v||} v 对输入向量进行 scale,而 CN 在此基础上用输入向量的模 ∣ ∣ x ∣ ∣ \bold{||x||} x 对输入向量进行了进一步的 scale.

CN 通过用余弦计算代替内积计算实现了标准化,但是这其中又一些隐患。原始的内积计算,其几何意义是 输入向量在权重向量上的投影,既包含 二者的夹角信息,也包含 两个向量的scale信息。去掉scale信息,可能导致表达能力的下降,因此也引起了一些争议和讨论。具体效果如何,可能需要在特定的场景下深入实验。

五、Normalization 为什么会有效?

我们从反向传播的梯度上着手说明Normalization的有效性。

下面的公式符号与我前面介绍反向传播博客的符号保持一致。

:因为BN的使用,通常是独立于激活函数的,所以,出于简洁性,这里只介绍基于线性激活函数的前向过程,也就是 σ ( x ) = x \sigma(x)=x σ(x)=x。同时,我们这里也去除了偏置项 b b b

5.1 不使用Batch Normalization

1.首先,对某层的前向传播过程有:
z l + 1 = W l + 1 σ ( z l ) = W l + 1 z l (1) z^{l+1} = W^{l+1} \sigma(z^l)=W^{l+1} z^l \qquad \text{(1)} zl+1=Wl+1σ(zl)=Wl+1zl(1)

2.针对该层的反向传播过程为(由于我们关心的是梯度的连续反向传播过程,故不关注权重的梯度):
∂ z l + 1 ∂ z l = ( W l + 1 ) T (2) \frac{\partial z^{l+1}}{\partial z^l} = (W^{l+1})^T\qquad \text{(2)} zlzl+1=(Wl+1)T(2)

3.进一步推导可得,连续多层的梯度反向传播过程为:
∂ z l + 1 ∂ z k = ∂ z l + 1 ∂ z l ∂ z l ∂ z l − 1 . . . ∂ z k + 1 ∂ z k = ∏ i = k + 1 l + 1 ( W i ) T (3) \frac{\partial z^{l+1}}{\partial z^{k}}=\frac{\partial z^{l+1}}{\partial z^l}\frac{\partial z^{l}}{\partial z^{l-1}}...\frac{\partial z^{k+1}}{\partial z^{k}} = \prod_{i=k+1}^{l+1} (W^{i})^T\qquad \text{(3)} zkzl+1=zlzl+1zl1zl...zkzk+1=i=k+1l+1(Wi)T(3)

由此我们可以初步看出,在梯度的连续反向传播过程中,是通过权重 W i W^{i} Wi的连乘进行的。因此,如果权重 W i W^{i} Wi的值总是较小的(广义上与1相比),则在反向过程中,梯度呈指数级衰减,就出现了梯度消失的问题;反之,如果如果权重 总是较大,则相应的就会出现梯度爆炸的问题。结论就是,在反向传播过程中,权值 W i W^{i} Wi的大小会极大的影响梯度的有效传播,而在训练过程中,权重并不总是受人为控制的。因此,我们有必要在一定程度上限制甚至消除权值 W i W^{i} Wi对梯度反向传播的不良影响,BN就可以起到这么一个作用。

5.2 使用Batch Normalization

1.带有BN的前向传播过程如下所示:

z l + 1 = B N ( W l + 1 z l ) = 1 σ l + 1 ( W l + 1 z l − μ l + 1 ) (4) z^{l+1} = BN(W^{l+1} z^l )=\frac{1}{\sigma^{l+1}}(W^{l+1} z^l-\mu^{l+1}) \qquad \text{(4)} zl+1=BN(Wl+1zl)=σl+11(Wl+1zlμl+1)(4)

其中 μ l + 1 \mu^{l+1} μl+1为向量, σ l + 1 \sigma^{l+1} σl+1为对角矩阵 d i a g ( 1 σ 1 l + 1 , 1 σ 2 l + 1 . . . . 1 σ n l + 1 ) diag(\frac{1}{\sigma_1^{l+1}},\frac{1}{\sigma_2^{l+1}}....\frac{1}{\sigma_n^{l+1}}) diag(σ1l+11,σ2l+11....σnl+11)

2.则其反向传播有:
∂ z l + 1 ∂ z l = 1 σ l + 1 ( W l + 1 ) T (5) \frac{\partial z^{l+1}}{\partial z^l} = \frac{1}{\sigma^{l+1}}(W^{l+1})^T\qquad \text{(5)} zlzl+1=σl+11(Wl+1)T(5)

3.相应的,连续多层的梯度反向传播过程为:
∂ z l + 1 ∂ z k = ∂ z l + 1 ∂ z l ∂ z l ∂ z l − 1 . . . ∂ z k + 1 ∂ z k = ∏ i = k + 1 l + 1 1 σ i ( W i ) T (6) \frac{\partial z^{l+1}}{\partial z^{k}}=\frac{\partial z^{l+1}}{\partial z^l}\frac{\partial z^{l}}{\partial z^{l-1}}...\frac{\partial z^{k+1}}{\partial z^{k}} = \prod_{i=k+1}^{l+1} \frac{1}{\sigma^{i}}(W^{i})^T\qquad \text{(6)} zkzl+1=zlzl+1zl1zl...zkzk+1=i=k+1l+1σi1(Wi)T(6)

可以看出,与不使用BN相比,每层的反向传播过程的,增加了一个基于标准差的矩阵 1 σ i \frac{1}{\sigma^{i}} σi1对权重 W i W^i Wi进行缩放。这样的缩放能够产生什么效果?让我们分析一下,如果权重 W i W^i Wi较小,那必然 W i z l W^iz^l Wizl较小,从而使得其标准差 σ i \sigma^{i} σi较小,相对的 1 σ i \frac{1}{\sigma^{i}} σi1较大,所以 1 σ i ( W i ) T \frac{1}{\sigma^{i}}(W^{i})^T σi1(Wi)T相对于原本的 W i W^i Wi就放大了,避免了梯度的衰减;同样的,如果权重 W i W^i Wi较大,可以很容易得到 1 σ i ( W i ) T \frac{1}{\sigma^{i}}(W^{i})^T σi1(Wi)T相对于原本的 W i W^i Wi缩小了,避免了梯度的膨胀。于是,加入了BN的反向传播过程中,就不易出现梯度消失或梯度爆炸,梯度将始终保持在一个合理的范围内。而这样带来的好处就是,基于梯度的训练过程可以更加有效的进行,即加快收敛速度,减轻梯度消失或爆炸导致的无法训练的问题。

一句话总结Normalization 为什么会有效:就是BN解决了反向传播过程中的梯度问题(梯度消失和爆炸),同时使得不同scale的 W W W 整体更新步调更一致。

六、对Batch Normalization的实践及分析

我们针对BN做一些实验,来看下BN的效果。

6.1 BN在Keras的使用

在很多框架内,批标准化(以下简称为BN)的操作在具体的实践中都非常简单,在keras中也不例外,我们只需要在模型中添加BN层,就表示对这一层做了标准化。我们看下keras中BatchNormalization的接口

keras.layers.BatchNormalization(axis=-1, momentum=0.99, epsilon=0.001, center=True, scale=True, beta_initializer=‘zeros’, gamma_initializer=‘ones’, moving_mean_initializer=‘zeros’, moving_variance_initializer=‘ones’, beta_regularizer=None, gamma_regularizer=None, beta_constraint=None, gamma_constraint=None)

在这里主要讲解与训练过程有关的参数。我们采用官方默认参数设置,其中 γ \gamma γ β \beta β就是公式(1)中的 b b b 和 g 和g g,分别表示re-shift和re-scale。设置为True,就表示我们会使用这两个可学习参数。

接下来,我们直接贴出完整代码。代码很简单,我们主要说明问题。

6.2 BN加速训练

代码如下:

import numpy as np
from keras.datasets import mnist
from sklearn.model_selection import KFold
from keras.models import Sequential
from keras.layers import Dense
from keras import optimizers
from keras.utils import to_categorical
from keras.layers import BatchNormalization as BN

# 导入数据。对其做one-hot编码,归一化。
(X_train,y_train),(X_test,y_test)=mnist.load_data()

train_labels = to_categorical(y_train)
test_labels = to_categorical(y_test)

X_train_normal = X_train.reshape(60000,28*28)
X_train_normal = X_train_normal.astype('float32') / 255
X_test_normal = X_test.reshape(10000, 28*28)
X_test_normal = X_test_normal.astype('float32') / 255

# 定义模型

def normal_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)


def BN2_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    #这里使用默认的BN参数
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)


# 两个模型训练至10个epochs,观察其表现
model_1=normal_model('sigmoid') # relu sigmoid
his_1=model_1.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w1=his_1.history

model_2=BN2_model('sigmoid') # relu sigmoid
his_2=model_2.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w2=his_2.history

import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style='whitegrid')
plt.plot(range(10),w1['loss'],'-.',label='Without BN ')
plt.plot(range(10),w2['loss'],'-.',label=' With BN_1')
plt.title('Sigmoid')
plt.xlabel('epochs')
plt.ylabel('Loss')
plt.legend() 

当激活函数是sigmoid的时候,有下图:
在这里插入图片描述
可以看到只是添加了一层BN,模型的收敛速度变得比未添加快了不少

我们知道,使用ReLU代替了sigmoid,收敛速度一般变快,那么我们的BN层是否也会对ReLU这样已经表现良好的隐藏单元再次加速呢?我们接下来就使用ReLU,观察其效果:
在这里插入图片描述
如图,因为ReLU是通过近似线性化的方式来缓解sigmoid的梯度消失问题,而BN是通过削弱层之间的依赖性来起作用,所以BN可以在ReLU的基础上进一步加速收敛

6.3 增加BN层数

基于我们对BN的认识,如果它真的在表示过程中削弱internal covariate shfit的影响,对于我们的4层模型来说,那么只添加一层BN,是将底层与其他三层相互独立开来,这三层的优化仍然相互影响。所以,可以预想到,如果我们添加BN层,收敛速度也会变快。

在6.2节中我们只在第一个隐含层使用了BN。为了说明BN层数的影响,具体实践上,我们依次搭建四个模型,分别含有不同的BN层数,并分别训练得到Loss随着epochs的变化图。代码如下:

import numpy as np
from keras.datasets import mnist
from sklearn.model_selection import KFold
from keras.models import Sequential
from keras.layers import Dense
from keras import optimizers
from keras.utils import to_categorical
from keras.layers import BatchNormalization as BN

# 导入数据
(X_train,y_train),(X_test,y_test)=mnist.load_data()

train_labels = to_categorical(y_train)
test_labels = to_categorical(y_test)

X_train_normal = X_train.reshape(60000,28*28)
X_train_normal = X_train_normal.astype('float32') / 255
X_test_normal = X_test.reshape(10000, 28*28)
X_test_normal = X_test_normal.astype('float32') / 255

# 1个BN层
def BN_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

# 2个BN层
def BN2_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(BN())
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

# 3个BN层
def BN3_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(BN())
    model.add(Dense(128,activation=a))
    model.add(BN())
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

# 4个BN层
def BN4_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(BN())
    model.add(Dense(128,activation=a))
    model.add(BN())
    model.add(Dense(64,activation=a))
    model.add(BN())
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

model_1=BN_model('sigmoid')
his_1=model_1.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w1=his_1.history

model_2=BN2_model('sigmoid')
his_2=model_2.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w2=his_2.history

model_3=BN3_model('sigmoid')
his_3=model_3.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w3=his_3.history

model_4=BN4_model('sigmoid')
his_4=model_4.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w4=his_4.history

import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style='whitegrid')
plt.plot(range(10),w1['loss'],'-.',label='With BN_1')
plt.plot(range(10),w2['loss'],'-.',label='With BN_2')
plt.plot(range(10),w3['loss'],'-.',label='With BN_3')
plt.plot(range(10),w4['loss'],'-.',label='With BN_4')
plt.title('sigmoid')
plt.xlabel('epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

分别使用sigmoid和ReLU激活函数:
在这里插入图片描述
在这里插入图片描述

如上图,使用sigmoid激活函数的模型,收敛速度和效果随着层数的增加而增加,使用ReLU的模型似乎对BN层的数量并不敏感,使用一层与使用多层并没有显著的区别,这很可能是因为网络不够深,ReLU加一层BN似乎已经到达了这个模型优化的极限。

6.4 自适应学习率算法与BN比较

我们接下来的代码将不会讨论ReLU,因为sigmoid激活函数的模型似乎有着更大的调优空间,我们也方便看出其效果。
要实现自适应学习率算法与BN比较,我们需要更改为SGD算法为Adam算法,一方面,我们可以对不使用BN层的模型添加自适应学习率优化算法,另一方面,我们可以对使用BN层的模型添加自适应学习率优化算法,看看是否加快收敛。

我们知道,BN和自适应学习率是更改参数更新幅度的两种手段,都是尽可能保持参数的更新幅度在一个量级内,并没有什么高下之分。

具体实践上,我们在上述模型的基础上新定义两个模型,一个是不使用BN,但使用Adam,另一个使用BN,也使用Adam算法,并与最开始两个模型进行比对,需要注意,我们保持Adam和SGD的学习率应该一致,均为0.01,keras中两者的默认参数并不相同,需要我们手动设置。

代码如下:

import numpy as np
from keras.datasets import mnist
from sklearn.model_selection import KFold
from keras.models import Sequential
from keras.layers import Dense
from keras import optimizers
from keras.utils import to_categorical
from keras.layers import BatchNormalization as BN

# 导入数据
(X_train,y_train),(X_test,y_test)=mnist.load_data()

train_labels = to_categorical(y_train)
test_labels = to_categorical(y_test)

X_train_normal = X_train.reshape(60000,28*28)
X_train_normal = X_train_normal.astype('float32') / 255
X_test_normal = X_test.reshape(10000, 28*28)
X_test_normal = X_test_normal.astype('float32') / 255

# 定义模型
def normal_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

def BN_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

def BN_adam_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(BN())
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    # lr=0.01
    model.compile(optimizer=optimizers.Adam(lr=0.01),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

def normal_adam_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    # lr=0.01
    model.compile(optimizer=optimizers.Adam(lr=0.01),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)

model_1=normal_model('sigmoid')
his_1=model_1.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w1=his_1.history

model_2=BN_model('sigmoid')
his_2=model_2.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w2=his_2.history

model_3=BN_adam_model('sigmoid')
his_3=model_3.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w3=his_3.history

model_4=normal_adam_model('sigmoid')
his_4=model_4.fit(X_train_normal,train_labels,batch_size=128,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w4=his_4.history


import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style='whitegrid')
plt.plot(range(10),w1['loss'],'-.',label='Normal')
plt.plot(range(10),w2['loss'],'-.',label='BN_1')
plt.plot(range(10),w3['loss'],'-.',label='BN_1+Adam')
plt.plot(range(10),w4['loss'],'-.',label='Normal+Adam')
plt.title('Sigmoid')
plt.xlabel('epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

结果:
在这里插入图片描述

我们发现在使用BN的模型基础上再添加Adam算法,使得模型的收敛进一步加快,而未使用BN的模型,添加Adam算法,也取得了与BN类似的效果。

6.5 Batch的影响

我们从前面可以发现,BN是基于batch进行的,batch是训练集的子集,它的大小直接会影响BN中方差和均值的计算,很可能每一个批次都有着不同的均值和方差。

假定对于确定的批次,标准化参数是固定的。减小batch size会让批次之间的方差(不是BN的方差)增加,使得网络有着更大的波动。但需要注意,即便不使用BN,batch size仍然会影响梯度的估计,大的batch迭代更快,但容易陷入局部最小值,小的batch随机性更高,但遍历一个epochs需要更多的时间。所以我们更改batch size之后,无法有效的确定收敛的效果到底是因为BN层还是因为梯度估计。

但是我们可以将这个问题转化为另一个问题,即,在对添加BN的模型和未添加BN的模型,比较其在不同batch上的表现,如果在小batch上,添加BN的模型相较于未添加BN的模型,并未取得很好的效果,就说明,在小batch上,BN层是不发挥作用的。

按照这个思路,我们采用4,64,256,1024四种batch size分别对BN模型和未添加BN的模型进行训练,代码如下:

import numpy as np
from keras.datasets import mnist
from sklearn.model_selection import KFold
from keras.models import Sequential
from keras.layers import Dense
from keras import optimizers
from keras.utils import to_categorical
from keras.layers import BatchNormalization as BN

# 导入数据。对其做one-hot编码,归一化。
(X_train,y_train),(X_test,y_test)=mnist.load_data()

train_labels = to_categorical(y_train)
test_labels = to_categorical(y_test)

X_train_normal = X_train.reshape(60000,28*28)
X_train_normal = X_train_normal.astype('float32') / 255
X_test_normal = X_test.reshape(10000, 28*28)
X_test_normal = X_test_normal.astype('float32') / 255

# 定义模型

def normal_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)


def BN2_model(a):
    model=Sequential()
    model.add(Dense(512,activation=a,input_shape=(28*28,)))
    #这里使用默认的BN参数
    model.add(BN())
    model.add(Dense(256,activation=a))
    model.add(Dense(128,activation=a))
    model.add(Dense(64,activation=a))
    model.add(Dense(10,activation='softmax'))
    model.compile(optimizer=optimizers.SGD(momentum=0.9,nesterov=True),loss='categorical_crossentropy',\
						metrics=['accuracy'])
    return(model)



model_2=BN2_model('sigmoid')
his_2=model_2.fit(X_train_normal,train_labels,batch_size=64,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w2=his_2.history


model_4=BN2_model('sigmoid')
his_4=model_4.fit(X_train_normal,train_labels,batch_size=256,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w4=his_4.history


model_6=BN2_model('sigmoid')
his_6=model_6.fit(X_train_normal,train_labels,batch_size=1024,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w6=his_6.history

model_7=BN2_model('sigmoid')
his_7=model_7.fit(X_train_normal,train_labels,batch_size=4,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w7=his_7.history



model_2n=normal_model('sigmoid')
model_2n.summary()
his_2n=model_2n.fit(X_train_normal,train_labels,batch_size=64,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w2n=his_2n.history


model_4n=normal_model('sigmoid')
his_4n=model_4n.fit(X_train_normal,train_labels,batch_size=256,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w4n=his_4n.history


model_6n=normal_model('sigmoid')
his_6n=model_6n.fit(X_train_normal,train_labels,batch_size=1024,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w6n=his_6n.history

model_7n=normal_model('sigmoid')
his_7n=model_7n.fit(X_train_normal,train_labels,batch_size=4,validation_data=(X_test_normal,test_labels),verbose=1,epochs=10)
w7n=his_7n.history


import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style='whitegrid')
plt.plot(range(10),w2['loss'],'b-.',label='BN_batch64')
plt.plot(range(10),w4['loss'],'r-.',label='BN_batch256')
plt.plot(range(10),w6['loss'],'g-.',label='BN_batch1024')
plt.plot(range(10),w7['loss'],'k-.',label='BN_batch4')
plt.plot(range(10),w2n['loss'],'b-',label='Normal_batch64')
plt.plot(range(10),w4n['loss'],'r-',label='Normal_batch256')
plt.plot(range(10),w6n['loss'],'g-',label='Normal_batch1024')
plt.plot(range(10),w7n['loss'],'k-',label='Normal_batch4')
plt.title('Sigmoid')
plt.xlabel('epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

结果:
在这里插入图片描述
如图,我们可以发现很多信息,对于添加BN层的模型(虚线),batch size为64和4的时候,收敛的效果和速度是最好,对于未添加BN的模型(实线),在batch为4的时候效果也最好。但是在小batch时,BN的使用与否几乎不会对性能造成影响,BN层反而使得性能不佳。这就证明我们4.1节中的观点,如果batch-size较少,那么batch的数目就会多,就更无法保证这些batch是独立同分布了。

6.6 总结

通过前面一系列的实验,我们知道了什么时候用BN比较好。例如,在神经网络训练时遇到收敛速度很慢,或梯度爆炸等无法训练的状况时可以尝试BN来解决。另外,在一般使用情况下也可以加入BN来加快训练速度,提高模型精度

参考文献

【1】详解深度学习中的Normalization,BN/LN/WN

【2】「周末AI课堂」认识批标准化的三种境界(代码篇)

【3】深度学习中 Batch Normalization为什么效果好?

【4】批标准化 (Batch Normalization)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值