李宏毅深度学习笔记:Batch Normalization
本文是学习李宏毅老师讲解batch normalization公开课的笔记。
Batch Normalization的paper:Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
最后补充pytorch里面的nn.BatchNorm2d使用方法和注意事项。
Feature Scaling
Feature Scaling指特征缩放,是将不同的feature缩放到相同的scale上。这里scale指的是取值范围。
- Feature Scaling的对training的影响
下图是一个简单的例子:
在上图中, x 1 x_1 x1和 x 2 x_2 x2是模型输入的不同feature, w 1 w_1 w1和 w 2 w_2 w2是weight, b b b是bias, a a a是模型的输出,即 a = w 1 ∗ x 1 + w 2 ∗ x 2 + b a = w_1 * x_1 + w_2 * x_2 + b a=w1∗x1+w2∗x2+b 。那么 a a a最终会影响到Loss。模型的输入 x 1 x_1 x1和 x 2 x_2 x2的scale差别很大, x 2 x_2 x2的各个元素的取值都远大于 x 1 x_1 x1,容易理解,这会导致 w 1 w_1 w1和 w 2 w_2 w2有相同变化时, w 2 w_2 w2对 a a a的影响远大于 w 1 w_1 w1,并最终导致 w 2 w_2 w2对Loss的影响远大于 w 1 w_1 w1,error surface如下图:
上图说明了 w 1 w_1 w1和 w 2 w_2 w2对Loss的影响,可以看出,Loss在 w 2 w_2 w2方向的梯度gradient即斜率大于在 w 1 w_1 w1方向上的,说明 w 2 w_2 w2对Loss的影响大于 w 1 w_1 w1 。
如果现在对 x 1 x_1 x1和 x 2 x_2 x2做Feature Scaling,如下图:
现在 x 1 x_1 x1和 x 2 x_2 x2有了相同的scale,所以 w 1 w_1 w1和 w 2 w_2 w2对 a a a的影响也就是对最终Loss的影响就相同了,error surface如下图:
可以看出error surface比较接近正圆形。
那么椭圆形和正圆形有什么不同呢?在做梯度下降的时候,如果是椭圆形,gradient在不同方向上差别比较大,这会让training变得不容易。为了让gradient能到达尽快到达0,也就是Loss局部最优,需要让gradient 尽量“走直线”,如下图红线所示:
为了让gradient尽量“走直线”,需要在横向和纵向上设置不同的learning rate,并且learning rate的比例应该和对应feature的比例相同,如果有很多feature(远多于两个)的话,这是一件非常麻烦的事情。而error surface为正圆形的话,在所有方向上设置一个learning rate就可以让gradient“走直线”,会让training容易得多。 - 如何做Feature Scaling
从上面我们知道了Feature Scaling带来的好处,接下来我们介绍经典的Feature Scaling的做法。先给出一组training data,如下图:
现在给定 R R R个 N N N维的training data,分别为 x 1 x^1 x1, x 2 x^2 x2, x 3 x^3 x3, … , x R x^R xR。 x i j x_i^j xij 表示第 j j j个training data( x j x^j xj)的第 i i i维元素 ( i = 1 , . . . , N , j = 1 , . . . , R ) (i = 1, ..., N , j = 1, ... ,R) (i=1,...,N,j=1,...,R),如下图:
对第 i i i 维 ( i = 1 , . . . , N ) ( i = 1, ... , N ) (i=1,...,N),先求所有training data这一维(即绿色框圈住的部分,绿色框从上向下平移求每一维)的平均值 m i m_i mi 和标准差 σ i \sigma_i σi,即 m i = 1 R ∑ j = 1 R x i j m_i =\frac{1}{R}\sum_{j=1}^R x_i^j mi=R1∑j=1Rxij, σ i = ∑ j = 1 N ( x i j − m i ) 2 R \sigma_i = \sqrt{\frac{\sum_{j=1}^N(x_i^j - m_i)^2}{R}} σi=R∑j=1N(xij−mi)2,这样就求出了 N N N个平均值和标准差。
求出每一维的平均值和标准差后,对所有training data的所有元素按公式 x i j = x i j − m i σ i , ( i = 1 , . . . , N , j = 1 , . . . , R ) x_i^j = \frac{x_i^j - m_i}{\sigma_i}, (i = 1, ..., N , j = 1, ... ,R) xij=σixij−mi,(i=1,...,N,j=1,...,R)进行更新,更新之后所有维度的平均值均为 0 0 0,标准差均为 1 1 1。 - 总结,做了Feature Scaling之后梯度下降会比不做会收敛地更快。
Internal Covariate Shift
机器学习前提假设:独立同分布假设,即假设训练数据和测试数据独立同分布。
我们将训练和测试数据记为
X
X
X,对应的label记为
Y
Y
Y。
- Internal Covariate Shift现象
我们知道神经网络有很多层,在训练过程中,除了最底层(input layer)的输入数据为training set,输入数据的分布不会变化之外,其他层(hidden layer 和 output layer,下文我们称为高层)的输入数据都是前一层的输出数据,而随着反向传播使得每一层的参数改变,他们的输出数据分布也会发生变化,导致高层的输入数据分布发生改变,且越高层的输入数据分布变化会越剧烈,这使得高层需要不断去重新适应低层的参数更新。Google将这一现象总结为Internal Covariate Shift,简称ICS。
这个现象导致:为了训练好模型,我们需要非常谨慎地去设定学习率、初始化权重以及尽可能细致的参数更新策略。(例如学习率设定的小一点,那高层输入数据分布的shift也会小一点。)
用数学语言来描述就是,高层的每一层输入数据的label对于输入数据分布的条件概率是一致的,但输入数据分布每一轮训练的边缘概率不同,即对所有 x ∈ X x \in X x∈X,有 P t r a i n ( Y ∣ x = X ) = P t e s t ( Y ∣ x = X ) P_{train}(Y|x=X) = P_{test}(Y|x = X) Ptrain(Y∣x=X)=Ptest(Y∣x=X),但是, P t r a i n ( X ) ≠ P t e s t ( Y ) P_{train}(X) \ne P_{test}(Y) Ptrain(X)=Ptest(Y)。其中Y是X的标签。也就是对于高层,输入数据分布每一轮训练都会改变,也就是不再独立同分布了,但是对应label不会改变。
注意:Internal corvariate shift和covariate shift是两回事,前者是网络内部,后者是针对输入数据,比如我们在训练数据前做归一化等预处理操作。 - Internal Covariate Shift的影响
- 高层网络需要不断适应新的输入数据分布,降低学习速度。
- 底层输入的变化可能趋向于变大或变小,导致上层落入饱和区,使得学习过早停止。
- 每层的更新都会影响到其它层,因此每层的参数更新策略需要尽可能谨慎。
- 前面我们对输入数据做Feature Scaling,使输入层更加容易train。那我们现在对高层也做Feature Scaling,使得高层的输入数据的平均值和标准差一直为0和1,这样虽然输入数据在每一次反向传播后都会改变,但是最起码它的statistic是不变的,从而在一定程度上减轻Internal Covariate Shift的问题。
- 对高层的输入做Feature Scaling的问题是,每次做的时候都需要先求出输入的statistics,但是由于上一层的参数变化,每轮训练的statistics都会改变,没有办法用很简单的方法很快求出他们的statistics。解决的方法是使用Batch Normalization。
训练过程的Batch Normalization
大部分work都会先做BN,再通过activation function,这样做得好处是:一般我们采用的激活函数,如hyperbolic tangent(双曲正切,tanh),sigmoid,都会有饱和区域(saturation region),如果activation function的输入落在了饱和区域,在反向传播时很可能会造成梯度消失(gradient vanish)的问题。所以我们会希望输入落在微分比较大也就是0的附近。而在激活层之前做normalization使得平均值为0标准差为1,在一定程度上可以使得输入落在0附近的概率更大。
下面我们以 b a t c h _ s i z e = 3 batch\_size = 3 batch_size=3 为例说明如何做Batch Normalization.
- 如下图,我们对输入层的输出,也就是第二层的输入,做Batch Normalizaiton
首先计算 z 1 z^1 z1, z 2 z^2 z2, z 3 z^3 z3的平均值 μ \mu μ和标准差 σ \sigma σ: μ = 1 3 ∑ i = 1 3 z i \mu = \frac{1}{3}\sum_{i=1}^3z^i μ=31∑i=13zi, σ = 1 3 ∑ i = 1 3 ( z i − μ ) 2 \sigma=\sqrt{\frac{1}{3}\sum_{i=1}^3(z^i-\mu)^2} σ=31∑i=13(zi−μ)2.
这里我们可以先注意一下, μ \mu μ和 σ \sigma σ都是依赖于 z 1 z^1 z1, z 2 z^2 z2, z 3 z^3 z3的,后文需要用到这个点。
由于每一个bacth都会有一次反向传播改变 W 1 W^1 W1的值,使得对整个训练集得到的第一层的整个输出做Normalization是不切实际的,所以这里只对一个batch的输出做Normalization。我们是希望做 normalization时计算出的 μ \mu μ和 σ \sigma σ能尽可能的近似于整个输出的平均值和标准差,所以batch_size不能太小。 - 将 z 1 z^1 z1, z 2 z^2 z2, z 3 z^3 z3按公式: z ~ i = z i − μ σ ( i = 1 , 2 , 3 ) \tilde z^i = \frac{z^i-\mu}{\sigma} (i=1, 2, 3) z~i=σzi−μ(i=1,2,3) 进行更新,得到 z ~ 1 , z ~ 2 , z ~ 3 \tilde z^1, \tilde z^2, \tilde z^3 z~1,z~2,z~3,这样得到的 z ~ 1 , z ~ 2 , z ~ 3 \tilde z^1, \tilde z^2, \tilde z^3 z~1,z~2,z~3的平均值为0,标准差为1,。如下图所示:
- 做了Normalization之后,反向传播也会通过 μ \mu μ和 σ \sigma σ这两条路径对 W 1 W^1 W1进行更新,这是因为 W 1 W^1 W1的变化会影响到 z 1 , z 2 , z 3 z^1, z^2, z^3 z1,z2,z3的值从而影响到 μ \mu μ和 σ \sigma σ的值,也就是这两个值是随着训练而不断变化的,所以反向传播时也会通过他们。
- 在某些情况下我们可能不希望
z
~
1
,
z
~
2
,
z
~
3
\tilde z^1, \tilde z^2, \tilde z^3
z~1,z~2,z~3的平均值和标准差为0和1,而是为其他值,那我们将
z
~
1
,
z
~
2
,
z
~
3
\tilde z^1, \tilde z^2, \tilde z^3
z~1,z~2,z~3按公式:
z
^
i
=
γ
⊙
z
~
i
+
β
(
i
=
1
,
2
,
3
)
\hat z^i = \gamma \odot \tilde z^i+\beta (i=1,2,3)
z^i=γ⊙z~i+β(i=1,2,3)进行更新,这样得到的
z
^
1
,
z
^
2
,
z
^
3
\hat z^1, \hat z^2, \hat z^3
z^1,z^2,z^3的平均值和标准差分别为
γ
\gamma
γ和
β
\beta
β。并且我们也可以把
γ
\gamma
γ和
β
\beta
β当做网络的参数,让网络自己去学习
γ
\gamma
γ和
β
\beta
β的取值分别为多少才最适用。如下图:
测试过程的Batch Normalization
- 下图是我们在training过程做Batch Normalization的过程:
但是这个过程在测试阶段会有问题,那就是测试过程没有batch,那么我们怎么去计算用于Batch Normalization的 μ \mu μ和 σ \sigma σ呢? - 理想的做法:在训练过程中我们计算每个batch的
μ
\mu
μ和
σ
\sigma
σ是为了代替整个training data的平均值和标准差,所以我们在测试的时候也可以采用整个training data的平均值和标准差。
但是问题是有可能training data很大,去计算平均值和方差并不方便;也有可能training data是分batch进入的,很可能并没有留下来,那根本没办法计算平均值和方差。 - 可实操的做法:将训练过程中所有batch的
μ
\mu
μ和
σ
\sigma
σ都保存下来,然后按权重求和得到用于testing的
μ
\mu
μ和
σ
\sigma
σ,如下图:
由于随着updata,accuracy会不断增大,所以一般我们会给后面的 μ \mu μ和 σ \sigma σ更大的权重,前面的权重就小一些。
Batch Normalization的好处
- 由于BN解决了Internal Covariate Shift的问题,使得网络训练可以设置更大的learning rate,从而可以减少网络训练时间。
- 在一定程度上可以防止gradient vanishing(梯度消失)。
- 减小参数的初始化对网络学习的影响。例如我们将某层参数的初始化扩大
K
K
K倍,那对应的这一层的输出也增大
K
K
K倍,但是这层的输出做了BN之后的输出不会改变,如下图:、
- BN会减少regularization(正则化)的需求,即在一定程度上对抗overfitting。
PyTorch中的BatchNorm2d
- 语法:
计算公式: y = x − E [ x ] V a r [ x ] + ϵ ∗ γ + β y = \frac{x - \mathrm{E}[x]}{ \sqrt{\mathrm{Var}[x] + \epsilon}} * \gamma + \beta y=Var[x]+ϵx−E[x]∗γ+βCLASS torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
其中 ϵ \epsilon ϵ 是参数中的eps, E [ x ] \mathrm{E}[x] E[x]和 V a r [ x ] \mathrm{Var}[x] Var[x]分别为均值和方差, γ \gamma γ和 β \beta β是上文中提到过的可学习到的新的均值和标准差。 - 参数的意思:
- num_features:即输入的channel;
- eps:就是计算公式中的 ϵ \epsilon ϵ,为保证数值稳定性(分母不能趋近或取0),给分母加上的值,默认为 1 e − 5 1e-5 1e−5;
- momentum:这个参数很多博客都解释错了,上文我们讲过BN在test时没有batch,无法计算均值和标准差,实际的做法是将训练过程中每轮的均值和标准差保存下来,然后按权相加,得到test时使用的均值和标准差。而momentum表示的就是权值。计算过程如下:
假设 x ^ \hat x x^是test时的均值或标准差,- 第一轮的的 x ^ \hat x x^就是这一轮训练数据计算出的statistic;
- 第二轮到最后一轮的每一轮,假设第 t t t轮的statistic为 x t x_t xt, x ^ \hat x x^都按公式 x ^ new = ( 1 − momentum ) × x ^ + momentum × x t , x ^ = x ^ n e w \hat{x}_\text{new} = (1 - \text{momentum}) \times \hat{x} + \text{momentum} \times x_t, \hat x = \hat{x}_{new} x^new=(1−momentum)×x^+momentum×xt,x^=x^new 进行更新;
- affine:当设为true时,使normalization后的均值和标准差设置为可学习的参数:缩放变量 γ \gamma γ和和平移变量 β \beta β;否则为0和1。缩放加平移就是仿射,affine的意思就是仿射。
- 举例说明如何计算
E
[
x
]
\mathrm{E}[x]
E[x]和
V
a
r
[
x
]
\mathrm{Var}[x]
Var[x]。假如BN的输入维度是
B
∗
C
∗
H
∗
W
=
4
∗
3
∗
2
∗
2
B*C*H*W = 4 * 3 * 2 * 2
B∗C∗H∗W=4∗3∗2∗2 ,那么最终会求得
C
=
3
C = 3
C=3 个
E
[
x
]
\mathrm{E}[x]
E[x]和
V
a
r
[
x
]
\mathrm{Var}[x]
Var[x],下图是一个
E
[
x
]
\mathrm{E}[x]
E[x]和
V
a
r
[
x
]
\mathrm{Var}[x]
Var[x]的计算过程: