于是,作者通过对每层加权和进行标准化,然后再通过缩放平移来“适度还原”。这样,做到了既不过分破坏输入信息,又抑制了各batch之间各位置点像素分布的剧烈变化带来的学习难度。
在原作中,最主要的思想就是下面这个公式。
一个经典的神经网络,它的某一个隐层如下图所示:
为了和原始论文统一,将之前常见的加权和符号$\vecz$改用$\vecx$表示。即上一层输出的激活值为$\veca$,那么经过本层加权和$W\veca+\vecb$处理后,获得加权和$\vecx$,然后经过本层激活后即输出$\sigma(\vecx)$。
加入BN之后的网络结构如图所示。
总体上来说,对于本层的加权和$\vecx$,
-BN先进行标准化求出$\hat{\vecx}$;
-再进行缩放和平移求出$\vecy$,这个$\vecy$取代了原始的$\vecx$,然后进行激活。
下图用一个究极简明的例子,说明了BN到底在干啥。
标准化即对一组数据中的每个数字,减均值再除以标准差,就可把一个该组数据转换为一个均值为$0$方差为$1$的标准正态分布。
BatchNormalization的数据组的构造方法:一个batch上所有m个样本分别进行前向传播时,传到这个隐层时所有m个$\vecx$的每个维度,分别构成一个数据组。
在原始论文里,用下标B指的正是一个batch(也就是我们常说的mini-batch),包含m个样本。这也就是为啥叫BatchNormalization的原因。
只要我们求得均值$\mu$和方差$\sigma^2$,就可以进行标准化了:$\hat{x_i}=\frac{x_i-\mu}{\sqrt{\sigma^2}}$
为避免分母为$0$的极端情况,工程上可以给分母增加一个非常小的小数$\epsilon$(例如$10^{-8}$):$\hat{x_i}=\frac{x_i-\mu}{\sigma^2+\epsilon}$
由标准化公式可以反推出:$x_i=\sigma\hat{x_i}+\mu$,仿照这个公式,作者构造了scaleandshift公式:$y_i=\gamma\hat{x_i}+\beta$。
很直觉就能看出来,$\gamma$是对$\hat{x_i}$的缩放,\beta是对$\gamma\hat{x_i}$的平移。可以增加可学习的参数$\gamma,beta$,如果$\gamma=\sigma,\beta=\mu$,那么必然有$y_i=x_i$,即我们就能够完全地还原成功!
我们可以通过反向传播来训练这两个参数(推导表明这是可以训练的),而至于$\gamma$多大程度上接近$\sigma$,$\beta$多大程度上接近$\mu$,让损失函数对它们计算出的梯度决定!注意,$\gamma$和$\beta$都是向量。
因此,
又回到了这张图:
BN实现的效果是:对于某一层$\vecx$来说,它的每个元素$x_i$的数值,在一个batch上的分布是一个任意的未知分布,BN首先把它标准化为了一个标准正态分布。
这样是否太暴力了?如果所有输入样本被层层改分布,相当于输入信息都损失掉了,网络是没法训练的。所以需要第二步对标准正态分布再进行一定程度的还原操作,即缩放平移。
最终使得这个数值分布,兼顾保留有效信息、加速梯度训练。
引入BN,增加了$\mu,\sigma,\gamma,\beta$四个参数。
这四个参数的引入,能否计算梯度?它们分别是如何初始化与更新的?
神经网络的训练,离不开反向传播,必须保证BN引入的两个操作(标准化、缩放平移)均可导。
上图可见:
狗尾续貂,对这个反传大致做了一个流程图,如下图所示,帮助理解。
注意,均值的梯度、方差的梯度的计算,只是为了保证梯度的反向传播链路的通畅,而不是为了更新自己(没明白下文还会解释);缩放因子$\gamma$和平移因子$\beta$的梯度传播则和权重$W$一样,不影响反向传播链路的通畅,只是为了更新自己。
最后的结果就是原论文中表述:
讨论一下参数的初始化及更新问题。
$\gamma$作为“准方差”,初始化为一个全1向量;而$\beta$作为"准均值”,初始化为一个全$0$向量,他俩的初始值对于刚刚完成标准正态化的$\hat{\vecx}$来说,没起任何作用。至于将要变成什么值,起多大作用,那就交给后续的训练,即采用梯度下降进行更新,方式同$W$。
$\gamma,\beta$是在整个训练集上训练出来的,与$W$一样,训练结束就可获得。
然而,$\mu$和$\sigma$是靠每一个mini-batch的统计得到,因为评估时只有一条样本,batch_size相当于是1,在只有1个向量的数据组上进行标准化后,成了一个全0向量,这可咋办?
做法是用训练集来估计总体均值$\mu$和总体标准差$\sigma$。
事实上,简单平均可能更好,简单平均本质上是平均权重,但是简单平均需要保存所有BN层在所有mini-batch上的均值向量和方差向量,如果训练数据量很大,会有较可观的存储代价。移动指数平均在实际的框架中更常见(例如tensorflow),可能的好处是EMA不需要存储每一个mini-batch的值,永远只保存着三个值:总统计值、本batch的统计值,$decay$系数。
在训练阶段同步获得了$\mu_{total}$和$\sigma_{total}$后,在评估时即可对样本进行BN操作。
为避免分母不为0,增加一个非常小的常数$\epsilon$,并为了计算优化,被转换为:
这样,只要训练结束,$\frac{\gamma}{\sqrt{\sigma^2_{total}+\epsilon}}、\mu_{total}、\beta$就已知了,1个BN层对一条测试样本的前向传播只是增加了一层线性计算而已。
一张图小结:
原作者认为BN是旨在解决了ICS(InternalCovariateShift)问题。原文是这样解释:
所谓CovariateShift,是指相比于训练集数据的特征,测试集数据的特征分布发生了变化。
而原作者定义的InternalCovariateShift,设想把每层神经网络看做一个单独的模型,它有着自己对应的输入与输出。如果这个“模型”越靠近输出层,由于训练过程中前面多层的权重的更新频繁,导致它每个神经元的输入(即上一层的激活值)的数值分布,总在不停地变化,这导致训练困难。
【更详细通俗地讲,网络一旦train起来,那么参数就要发生更新,除了输入层的数据外(因为输入层数据,我们已经人为地为每个样本归一化),后面网络每一层的输入数据分布是一直在发生变化的。因为在训练的时候,前面层训练参数的更新将导致后面层输入数据分布的变化。以网络第二层为例:网络的第二层输入,是由第一层的参数和input计算得到的,而第一层的参数在整个训练过程中一直在变化,因此必然会引起后面每一层输入数据分布的改变。我们把网络中间层在训练过程中,数据分布的改变称之为:“InternalCovariateShift”。
对于深度网络的训练是一个复杂的过程,只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。一旦网络某一层的输入数据的分布发生改变,那么这一层网络就需要去适应学习这个新的数据分布,所以如果训练过程中,训练数据的分布一直在发生变化,那么将会影响网络的训练速度。】
然而,一个启发性的解释很容易被推翻,又有人做了更进一步的解释。
(然而,我的理解是:如果每个BN层后叠加噪音,下一层的BN也会进行标准化,层层抵消,相当于仅最后一个BN层后叠加的噪音增大了ICS)然而两种情况下,左图BN的表现依然非常稳定。即BN并没有减少ICS。那么,BN是为什么有效?
上图论文的作者定义了一个描述损失函数平滑度的函数,观察加入BN的前后,损失函数平滑性的变化。如下图所示,纵轴的数值越小,表明损失函数曲面越平滑;纵轴数值越大,表明损失函数曲面越颠簸。蓝色线为加入BN后的损失函数的平滑度,可以看到,加入BN后,损失函数曲面的平滑程度得到了显著改善。
对平滑性的理解,我想没有比下图更合适的了:
能。原因:由前述BN的反向传播可知,BN不管放在网络的哪个位置,都可以实现这两个功能:训练$\gamma$和$\beta$、传递梯度到前一层,所以位置并不限于ReLU之前。原始论文中,BN被放在本层ReLU之前,即$$\veca^{l+1}=ReLU[BN(W^{l+1}\veca^l+\vecb^{l+1})]$$
除此之外,从上述也可以看出,batchnormalization依赖于batch的大小,当batch值很小时,计算的均值和方差不稳定。研究表明对于ResNet类模型在ImageNet数据集上,batch从16降低到8时开始有非常明显的性能下降,在训练过程中计算的均值和方差不准确,而在测试的时候使用的就是训练过程中保持下来的均值和方差。这一个特性,导致batchnormalization不适合以下的几种场景:
(1)batch非常小,比如训练资源有限无法应用较大的batch,也比如在线学习等使用单例进行模型参数更新的场景。
针对BN依赖于batch的这个问题,BN的作者亲自现身提供了改进,即在原来的基础上增加了一个仿射变换:$$x_i'=\frac{x_i-\mu_{\beta}}{\sigma_{\beta}}\cdotr+d$$
其中参数$r,d$就是仿射变换参数,它们本身是通过如下的方式进行计算的$r=\frac{\sigma_{\beta}}{\sigma},d=\frac{\mu_{\beta}-\mu}{\sigma}$,其中参数都是通过滑动平均的方法进行更新:$$\begin{align}\mu&:=\mu+\alpha(\mu_{\beta}-\mu)\\\sigma&:=\sigma+\alpha(\sigma_{\beta}-\sigma)\end{align}$$
在实际使用的时候,先使用BN进行训练得到一个相对稳定的移动平均,网络迭代的后期再使用刚才的方法,称为BatchRenormalization,当然$r$和$d$的大小必须进行限制。
Normalization思想非常简单,为深层网络的训练做出了很大贡献。对于CNN,BN的操作是在各个特征维度之间单独进行,也就是说各个通道是分别进行BatchNormalization操作的。如果输出的blob大小为$(N,C,H,W)$,那么在每一层normalization就是基于$N*H*W$个数值进行求平均以及方差的操作。而因为有依赖于样本数目的缺陷,所以也被研究人员盯上进行改进。说的比较多的就是LayerNormalization与InstanceNormalization,GroupNormalization了。
前面说了BatchNormalization各个通道之间是独立进行计算,如果抛弃对batch的依赖,也就是每一个样本都单独进行normalization,同时各个通道都要用到,就得到了LayerNormalization。跟BatchNormalization仅针对单个神经元不同,LayerNormalization考虑了神经网络中一层的神经元。如果输出的blob大小为(N,C,H,W),那么在每一层LayerNormalization就是基于$C*H*W$个数值进行求平均以及方差的操作。
LayerNormalization把每一层的特征通道一起用于归一化,如果每一个特征层单独进行归一化呢?也就是限制在某一个特征通道内,那就是instancenormalization了。如果输出的blob大小为(N,C,H,W),那么在每一层InstanceNormalization就是基于$H*W$个数值进行求平均以及方差的操作。对于风格化类的图像应用,InstanceNormalization通常能取得更好的结果,它的使用本来就是风格迁移应用中提出。
GroupNormalization是LayerNormalization和InstanceNormalization的中间体,GroupNormalization将channel方向分group,然后对每个Group内做归一化,算其均值与方差。如果输出的blob大小为(N,C,H,W),将通道C分为G个组,那么GroupNormalization就是基于$G*H*W$个数值进行求平均以及方差的操作。我只想说,你们真会玩,要榨干所有可能性。
在BatchNormalization之外,有人提出了通用版本GeneralizedBatchNormalization,有人提出了硬件更加友好的L1-NormBatchNormalization等,不再一一讲述。
另一方面,以上的BatchNormalization,LayerNormalization,InstanceNormalization都是将规范化应用于输入数据$x$,Weightnormalization则是对权重进行规范化,感兴趣的可以自行了解,使用比较少,也不在我们的讨论范围。
这么多的Normalization怎么使用呢?有一些基本的建议吧,不一定是正确答案。
(1)正常的处理图片的CNN模型都应该使用BatchNormalization。只要保证batchsize较大(不低于32),并且打乱了输入样本的顺序。如果batch太小,则优先用GroupNormalization替代。
(2)对于RNN等时序模型,有时候同一个batch内部的训练实例长度不一(不同长度的句子),则不同的时态下需要保存不同的统计量,无法正确使用BN层,只能使用LayerNormalization。
(3)对于图像生成以及风格迁移类应用,使用InstanceNormalization更加合适。
(1)主流观点,BatchNormalization调整了数据的分布,不考虑激活函数,它让每一层的输出归一化到了均值为0方差为1的分布,这保证了梯度的有效性,目前大部分资料都这样解释,比如BN的原始论文认为的缓解了InternalCovariateShift(ICS)问题。
(2)可以使用更大的学习率,Bjorck论文《Understandingbatchnormalization》指出BN有效是因为用上BN层之后可以使用更大的学习率,从而跳出不好的局部极值,增强泛化能力,在它们的研究中做了大量的实验来验证。
(3)损失平面平滑。Santurkar论文《Howdoesbatchnormalizationhelpoptimization》的研究提出,BN有效的根本原因不在于调整了分布,因为即使是在BN层后模拟ICS,也仍然可以取得好的结果。它们指出,BN有效的根本原因是平滑了损失平面。之前我们说过,Z-score标准化对于包括孤立点的分布可以进行更平滑的调整。
构建两个全连接神经网络:
定义输入占位符,定义三个层的权重,方便后面使用
w1_initial=np.random.normal(size=(784,100)).astype(np.float32)w2_initial=np.random.normal(size=(100,100)).astype(np.float32)w3_initial=np.random.normal(size=(100,10)).astype(np.float32)#为BN层准备一个非常小的数字,防止出现分母为0的极端情况。epsilon=1e-3x=tf.placeholder(tf.float32,shape=[None,784])y_=tf.placeholder(tf.float32,shape=[None,10])Layer1层:无BN
w1=tf.Variable(w1_initial)b1=tf.Variable(tf.zeros([100]))z1=tf.matmul(x,w1)+b1l1=tf.nn.sigmoid(z1)Layer1层:有BN(自定义BN层)
w2=tf.Variable(w2_initial)b2=tf.Variable(tf.zeros([100]))z2=tf.matmul(l1,w2)+b2#l2=tf.nn.sigmoid(z2)l2=tf.nn.relu(z2)Layer2层:有BN(使用tensorflow创建BN层)
w3=tf.Variable(w3_initial)b3=tf.Variable(tf.zeros([10]))y=tf.nn.softmax(tf.matmul(l2,w3)+b3)Layer3层:有BN(使用tensorflow创建BN层)
#w3_BN=tf.Variable(w3_initial)#b3_BN=tf.Variable(tf.zeros([10]))#y_BN=tf.nn.softmax(tf.matmul(l2_BN,w3_BN)+b3_BN)w3_BN=tf.Variable(w3_initial)z3_BN=tf.matmul(l2_BN,w3_BN)batch_mean3,batch_var3=tf.nn.moments(z3_BN,[0])scale3=tf.Variable(tf.ones([10]))beta3=tf.Variable(tf.zeros([10]))BN3=tf.nn.batch_normalization(z3_BN,batch_mean3,batch_var3,beta3,scale3,epsilon)#print(BN3.get_shape())y_BN=tf.nn.softmax(BN3)针对普通网络和BN网络,分别定义损失、优化器、精度三个op。
#普通网络的损失cross_entropy=-tf.reduce_sum(y_*tf.log(y))#BN网络的损失cross_entropy_BN=-tf.reduce_sum(y_*tf.log(y_BN))#普通网络的优化器train_step=tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)#BN网络的优化器train_step_BN=tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy_BN)#普通网络的accuracycorrect_prediction=tf.equal(tf.arg_max(y,1),tf.arg_max(y_,1))accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))#BN网络的accuracy_BNcorrect_prediction_BN=tf.equal(tf.arg_max(y_BN,1),tf.arg_max(y_,1))accuracy_BN=tf.reduce_mean(tf.cast(correct_prediction_BN,tf.float32))WARNING:tensorflow:From
zs来自无BN网络的第二个隐层的输入加权和向量,即下一步将喂给本层的激活函数。BNs来自有BN网络的第二个隐层的输入加权和经过BN层处理后的向量,即下一步也将喂给本层的激活函数。
predictions=[]correct=0foriinrange(100):pred,corr=sess.run([tf.arg_max(y_BN,1),accuracy_BN],feed_dict={x:[mnist.test.images[i]],y_:[mnist.test.labels[i]]})#累加,最终用于求取100次预测的平均精度correct+=corr#保存每次预测的结果predictions.append(pred[0])print("PREDICTIONS:",predictions)print("ACCURACY:",correct/100)sess.close()#结果将是:不管输入的是什么照片,结果都将相同。因为每个图片在仅有自己的mini-batch上都被标准化为了全0向量。PREDICTIONS:[8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8]ACCURACY:0.02