Google在ICML文中描述得非常清晰,即在每次SGD时,通过mini-batch来对相应的activation做标准化操作,使得结果(输出信号各个维度)的均值为0,方差为1。而最后的“scale and shift”操作则是为了让因训练所需而“刻意”加入的BN能够有可能还原最初的输入,从而保证整个network的capacity(非线性、表达能力)。
为什么深度神经网络随着网络深度加深,训练起来越困难,收敛越来越慢?这是个在DL领域很接近本质的好问题。很多论文都是解决这个问题的,比如ReLU激活函数,再比如Residual Network,BN本质上也是解释并从某个不同的角度来解决这个问题的。
BN是用来解决“Internal Covariate Shift”(内部协变量偏移)问题的,对于深度学习这种包含很多隐层的网络结构,在训练过程中因为各层参数不停在变化,所以每个隐层都会面临covariate shift的问题,也就是在训练过程中,隐层的输入分布老是变来变去。所以,“Covariate Shift”可以理解为,如果ML系统实例集合<X,Y>中的输入值X的分布老是变,这不符合IID假设,网络模型很难稳定的学规律。所谓的“Internal Covariate Shift”,Internal指的是深层网络的隐层,是发生在网络内部的事情,而不是covariate shift问题只发生在输入层。深层神经网络在做非线性变换前的激活输入值(x2=Wx1+B,x1是输入)随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,整体分布逐渐往非线性函数的取值区间的上下限两端靠近(对于Sigmoid函数来说,意味着激活输入值Wx1+B是大的负值或正值,其梯度接近0),所以这导致反向传播时低层神经网络的梯度消失。
NOTE:其实对于Sigmoid激活函数,其导数在(0,0.25),多个相乘的值越来越接近0,即使整体分布不往非线性函数的取值区间的上下限两端靠近,本身就很容易造成梯度消失。
BN的想法产生于白化思想,就是对输入数据分布变换到均值为0,方差为1(也可能不是1)的正态(高斯)分布——那么神经网络会较快收敛。其实对于深度网络来说,其中某个隐层的神经元是下一层的输入,意思是深度神经网络的每一个隐层都是输入层,不过是相对下一层来说而已。
基本思想:通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0,方差为1(或其它)的正态分布。可以看下面两个图:
如果对于每层的输入x服从正态分布,那么95%的值落在[-2,2]的范围内,对于激活函数sigmoid(x)在这个区间的导数不会接近于0,即梯度不会为0,这样的区域也被称为梯度非饱和区 。经过BN后,目前大部分的值落入非线性激活函数的线性区内(sigmoid函数自变量靠近0的范围内),这样来加速训练收敛过程,不会造成梯度消失问题。
NOTE:仔细想一下,当整体分布被强行拉到均值为0,方差为1时,会有什么问题呢???虽然避免了梯度接近0的情况,解决了梯度消失问题,但是这个操作会削弱模型的非线性能力,也就是标准化使特征数据完全满足正态分布,数据集中在sigmoid激活函数的线性区域。
要对每个隐层神经元的输出做BN,可以想象成每个隐层后又加上了一层BN操作层,它位于X2=W1+B线性变换之后,非线性激活函数变换之前,其图示如下:(比如在卷积神经网络中,BN位于卷积操作和激活函数之间)
对于Mini-Batch SGD来说,一次训练过程里面包含m个训练实例,其具体BN操作就是对于隐层内每个神经元来说,进行如下变换: 公式表示:某个神经元对应的原始x通过减去mini-Batch内m个实例获得的m个x求得的均值E(x)并除以求得的方差Var(x)来进行转换。目前BN主要分为四种(最后一种是最近提出来的):
前面说过,经过这个变换后某个神经元的激活x形成了均值为0,方差为1的正态分布,目的是把值往后续要进行的非线性变换的线性区拉动,增大导数值,增强反向传播信息流动性,加快训练收敛速度。但是这样会导致网络表达能力下降(即削弱了模型的非线性能力),为了防止这一点,每个神经元增加两个调节参数(scale和shift),这两个参数是通过训练来学习到的,用来对变换后的激活反变换,使得网络表达能力增强,即对变换后的激活进行如下的scale(参数被称为缩放因子)和shift(参数被称为偏移因子)操作,这其实是变换的反操作:
TensorFlow2代码实现:
class Baseline(Model):
def __init__(self):
super(Baseline, self).__init__()
self.c1 = Conv2D(filters=6, kernel_size=(5, 5), padding='same') # 卷积层
self.b1 = BatchNormalization() # BN层
self.a1 = Activation('relu') # 激活层
self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same') # 池化层
self.d1 = Dropout(0.2) # dropout层
self.flatten = Flatten()
self.f1 = Dense(128, activation='relu')
self.d2 = Dropout(0.2)
self.f2 = Dense(10, activation='softmax')
def call(self, x):
x = self.c1(x)
x = self.b1(x)
x = self.a1(x)
x = self.p1(x)
x = self.d1(x)
x = self.flatten(x)
x = self.f1(x)
x = self.d2(x)
y = self.f2(x)
return y
model = Baseline()
优点:
- 不仅仅极大提升了训练速度,收敛过程大大加快;
- 还能增加分类效果,一种解释是这是类似于Dropout的一种防止过拟合的正则化表达方式,所以不用Dropout也能达到相当的效果;
- 另外调参过程也简单多了,对于初始化要求没那么高,而且可以使用大的学习率等。
- 同时引入的 随机噪声 能够起到对模型参数进行 正则化 的作用,有利于增强模型泛化能力。
不足:
- 如果 Batch Size 太小,则 BN 效果明显下降。因为在小的 BatchSize意味着数据样本少,因而得不到有效统计量,也就是说噪音太大;
- 对于有些像素级图片生成任务来说,BN 效果不佳;对于图片分类等任务,只要能够找出关键特征,就能正确分类,这算是一种粗粒度的任务,因为在Mini-Batch 内多张无关的图片之间计算统计量,弱化了单张图片本身特有的一些细节信息;
- RNN 等动态网络使用 BN 效果不佳且使用起来不方便;
- 训练时和推理时统计量不一致。
一些讨论:
就目前来看,争议的重点在于归一化的位置,还有gamma与beta参数的引入,从理论上分析,论文中的这两个细节实际上并不符合ReLU的特性:ReLU后,数据分布重新回到第一象限,这时是最应当进行归一化的;gamma与beta对sigmoid函数确实能起到一定的作用(实际也不如固定gamma=2),但对于ReLU这种分段线性的激活函数,并不存在sigmoid的低scale呈线性的现象。
有一篇论文否定了BN是改善 内部协变量偏移(Internal Covariate Shift) 的观点,参考论文How Does Batch Normalization Help Optimization?