Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift,2015
强调BN 的目的
首先在最前面强调一下,BN 的本意是为了消除 internal covariate shift 以达到加速训练的目的,只不过同时还有正则化的效果。所以,在解决过拟合过程中可以考虑 BN,但是要清楚一点,它最大的作用应该是加速训练。那么笼统来说,怎么才能加速训练呢?减少不必要的干扰、震荡,更顺利地接近最优表现,所以安全有效地提高学习率就可以加速。之所以说安全有效,是因为有时候加大学习率是偷鸡不成蚀把米的操作,比如引起严重的震荡。
BN 解决什么问题
然后单独再说一下 BN 主要解决的问题,也就是全篇多次提到的 covariate shift。在很多时候,我们都是假设输入到模型的数据分布是稳定的,但是很遗憾,这个假设很多时候都不成立,也就是输入到模型的数据分布是会变化的,这种现象就是协变量偏移 covariate shift,可能发生在训练集和测试集之间,也可以发生在整个训练过程中。论文主要在训练过程中的协变量偏移现象上发力,训练过程中模型的各个子模块(对于网络来说每层都可以叫作一个子网络)的输入数据变化就叫 internal covariate shift。
对于深度学习网络来说,其深度一般都较大。对于现在常用的 mini-batch gradient descent 方法来说,每次进来一个 mini-batch,直观上来说,模型就会“讨好”这个 mini-batch,也就是照着 mini-batch 的“指引”进行学习。那如果下一个 mini-batch 来了其数据分布和上一个 mini-batch 有些区别,那网络参数还是很乖,它会再根据当前这个“老师”的教导再进行更新。两次更新之间的差异,造成的震荡就会降低学习的效率。论文里面没有重点讨论 mini-batch 之间的差异,而是一开始在整个 batch 上讨论,然后“简化近似”到 mini-batch 上。这段是我自己的理解,因为激活之后是不是会饱和,肯定和激活前的值,以及具体的激活变换都有关系,增大 mini-batch 的 size 可以减少这种效应。
不乐观的是,深度学习网络层数很多,靠后层的参数更新时的输入是前面层的输出,也就是说前面层的操作比如非线性变换在饱和区域,就会让后面层的参数更加“难办”。类似长尾车,你坐在后面,前面每次的变动会对你和对最前排的扰动程度是不一样的。也可以类比“口口相传”,消息经过多次“转述”,可能变味儿就很严重了。所以在 BN 之前,有各种 trick 来降低这种作用,比如减少使用 sigmoid 或者 tanh 激活函数,而是使用 Relu;在初始化时更加小心和精细;每次更新参数都更谨慎也就是使用更小的学习率(无疑也就降低了学习的步伐)。
BN 怎么解决的
BN 就是针对前面的问题提出的解决方法,总的来说,BN 能够让模型的各个子模块(单层或多层)之间更加相对地独立,也就是说前面层的参数更新发生变化后,其向下一层传递的数据变化不会产生过大的变化,从而减少 internal covariate shift 的作用。这样做之后,整个训练过程会更加稳定,学习率也可以放开一点,从而加速学习,深层网络会体现得更加明显。
在具体实现过程中,BN 论文采用了两种近似操作:
- 不同维度之间,BN 操作互相独立;
- 适应 mini-batch,在一个 mini-batch 上而不是整个 batch(训练集大小)。
关于第一点,以图像数据为例,一般输入网络 [batchsize, height, width, channel] ,经过一层有 n 个神经元/隐藏单元 padding 为 same 之后变为 [batchsize, height, width, n] 的特征,在 channel 和 n 那个维度之间 BN 操作是独立的,其实就是每层网络的各个隐藏单元之间互相独立。论文说的是针对每个 “activation” 做 BN,激活函数不就在每个隐藏单元上嘛。
关于第二点,就不再多说了,照着深度学习的训练集大小,谁招架得住整个 batch,mini-batch 就很好。
BN 考虑更深的一点是,设置两个可以学习的参数
β
\beta
β 和
γ
\gamma
γ,分别对应平移/均值和缩放/方差。如果按照前面说的,每个维度上分开做 BN 的话,以维度 k 为例就有:
y
(
k
)
=
γ
(
k
)
x
(
k
)
−
μ
(
k
)
(
σ
(
k
)
)
2
+
ϵ
+
β
(
k
)
y^{(k)} = \gamma^{(k)} \frac{x^{(k)} - \mu^{(k)}}{\sqrt{(\sigma^{(k)})^2 + \epsilon}} + \beta^{(k)}
y(k)=γ(k)(σ(k))2+ϵx(k)−μ(k)+β(k)
- x ( k ) x^{(k)} x(k) 就是当前这个隐藏单元一个 mini-batch 的输入;
- y ( k ) y^{(k)} y(k) 就是这个隐藏单元 BN 之后的输出;
- μ ( k ) \mu^{(k)} μ(k) 和 σ ( k ) \sigma^{(k)} σ(k) 就是在 mini-batch 上的均值和标准差;
- β ( k ) \beta^{(k)} β(k) 和 γ ( k ) \gamma^{(k)} γ(k) 分别是科学系的平移参数和缩放参数。
这两个可学习的参数有什么作用呢?
- 如果没有,每层都是标准的标准化,均值为 0 方差为 1,你就学不到东西了呀,因为特征之所以叫特征,肯定各自有各自“特点”的。我们 BN 的原本目的是让特征分布不要变化得太离谱,而不是完全让它们都木讷地 0 1 正太。
- 跟第一条有点关联,就是标准 0 1 标准化,总是容易落在激活函数的线性区域,那模型表达能力就被“阉割”了,同样这不是 BN 的本意。
- 当 β ( k ) \beta^{(k)} β(k) 和 γ ( k ) \gamma^{(k)} γ(k) 分别取均值和标准差的时候,可以直接关掉 BN 的作用。
具体算法如下图,需要注意的是,图中的
x
i
x_i
xi 完全可以是向量哦,m 个特征向量做均值和方差,而不是单独数字做。
BN 应该放在哪儿
一直有争议的是:BN 到底应该放在哪儿?BN 虽然是在一个隐藏单元上做的,那到底是激活前的 z = W x + b z = Wx + b z=Wx+b 呢还是激活后 a = g ( z ) a = g(z) a=g(z) 呢?这个问题大家各有理解,因为本来 BN 本意就是加速训练、适度地保留特征的分布避免深层网络陷入死局。这个度本来就很难把握,随着模型其他方面的设置比如层深、激活函数等的变化,这个度也会变。不过目前工业界不少人放在后面,比如 mobilenetv2。
- 放在前面,可以有效避免 BN 破坏非线性的成果(我好不容易非线性激活完了,你一棒子给我打回去了),放在前面可以有控制地避免严重饱和。
- 放在后面,只要搭配 Relu(本来现在 Relu 出镜就很高),减少使用 sigmoid 或者 tanh,就可以避免过度的 BN,也就是保留一些激活效果。
如何看待 BN 的正则化能力
最后想讨论下 BN 的正则化能力,朴素一点来说,正则化其实就是让过于“复杂”的模型变得“简单”。那什么叫“简单”或者“复杂”?这里不能单纯只通过层数或者隐藏层节点数来说。我在其他的学习记录里面也总结过,深度学习网络的“复杂”度主要来自非线性,也就是激活函数,尤其是 sigmoid 或者 tanh 这种曲线函数的“饱和区域”。而 BN 的操作,能够让每个子模块(单层/多层)的输入都在一定的掌控范围内,不会严重“饱和”,而是相对有控制地落在线性区域或者饱和区域。这个操作也就达成了“简化”模型的作用,自然也就有正则化的效果了。