• 深度学习之解密Batch Normalization


    本文主要是对知乎、CSDN平台上BN相关文章的转载、整理与汇总,记录以方便自己日后的复习巩固,并分享给同样渴望知识的你们。

    主体来自知乎soplars理解Batch Normalization系列文章,感谢作者的耕耘!

    一图解释BN作用:

    简单粗暴地说,BN就是按批次对网络输出的数据分布先进行一个标准化,再进行一个“还原化”。(归一化 、标准化 和中心化/零均值化

    原理

    初始idea

    如果做神经网络训练前,对输入的像素进行标准化处理,将有效降低模型的训练难度(见神经网络训练技巧的初始化部分)。受此启发,作者想到,既然输入层可以加标准化有好处,那么网络里的隐层为什么不可以标准化?

    于是,作者通过对每层加权和进行标准化,然后再通过缩放平移来“适度还原”。这样,做到了既不过分破坏输入信息,又抑制了各batch之间各位置点像素分布的剧烈变化带来的学习难度

    在原作中,最主要的思想就是下面这个公式。

    原始神经网络的结构

    一个经典的神经网络,它的某一个隐层如下图所示:

    为了和原始论文统一,将之前常见的加权和符号$vec z$改用$vec x$表示。即上一层输出的激活值为$vec a$ ,那么经过本层加权和$Wvec a + vec b$处理后,获得加权和$vec x$,然后经过本层激活后即输出$sigma(vec x)$。

    BN的神经网络结构

    加入BN之后的网络结构如图所示。

    总体上来说,对于本层的加权和$vec x$,

    - BN先进行标准化求出$hat{vec x}$;

    - 再进行缩放和平移求出$vec y$,这个$vec y$取代了原始的$vec x$,然后进行激活。

    BN的前向传播

    认识BN的困难在于维度太多了!大脑里至少能联想到三个维度:batch_size维度(时间顺序维度)、网络层维度(结构横向维度)、向量维度(结构纵向维度)。

    下图用一个究极简明的例子,说明了BN到底在干啥。

    标准化

    标准化即对一组数据中的每个数字,减均值再除以标准差,就可把一个该组数据转换为一个均值为$0$方差为$1$的标准正态分布

    Batch Normalization的数据组的构造方法:一个batch上所有m个样本分别进行前向传播时,传到这个隐层时所有m个$vec x$的每个维度,分别构成一个数据组。

    在原始论文里,用下标B指的正是一个batch(也就是我们常说的mini-batch),包含m个样本。这也就是为啥叫Batch Normalization的原因。

    • 对这m个$vec x$,每一个维度上的标量们,分别求均值和方差。
    • 得到的均值$mu$和方差$sigma^2$分别对应该层的每个神经元维度。

    只要我们求得均值$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$,仿照这个公式,作者构造了scale and shift公式:$y_i = gammahat{x_i} + eta$。

    很直觉就能看出来,$gamma$是对$hat{x_i}$的缩放,eta是对$gammahat{x_i}$的平移。可以增加可学习的参数$gamma, beta$,如果$gamma = sigma, eta = mu$,那么必然有$y_i = x_i$,即我们就能够完全地还原成功!

    我们可以通过反向传播来训练这两个参数(推导表明这是可以训练的),而至于$gamma$多大程度上接近$sigma$,$eta$多大程度上接近$mu$,让损失函数对它们计算出的梯度决定!注意,$gamma$ 和 $eta$都是向量。

    因此,

    • 只要损失函数有需要,scale and shift公式赋予了它左右BN层还原程度的能力,而且上限是完全还原;
    • 具体对每一层还原多少,则是由损失函数对每一层这两个系数的梯度来决定;
    • 损失通过梯度来控制还原的程度,较好利于减少损失,就多还原;较少利于减少损失,就少还原。

    BN实现的效果

    又回到了这张图:

    BN实现的效果是:对于某一层$vec x$来说,它的每个元素$x_i$的数值,在一个batch上的分布是一个任意的未知分布,BN首先把它标准化为了一个标准正态分布。

    这样是否太暴力了?如果所有输入样本被层层改分布,相当于输入信息都损失掉了,网络是没法训练的。所以需要第二步对标准正态分布再进行一定程度的还原操作,即缩放平移。

    最终使得这个数值分布,兼顾保留有效信息、加速梯度训练

    训练及评估

    训练阶段

    引入BN,增加了$mu, sigma, gamma, eta$四个参数。

    这四个参数的引入,能否计算梯度?它们分别是如何初始化与更新的?

    反向传播

    神经网络的训练,离不开反向传播,必须保证BN引入的两个操作(标准化、缩放平移)均可导。

    缩放平移就是一个线性公式,求导很简单。而对于标准化时的统计量,可以绘制计算图,如下图所示。Frederik Kratzert 在这篇博文中有详细的计算,对每一个环节都进行了详细的描述。

    上图可见:

    • 每个环节都可导
    • 只要求出各个环节的导数
    • 用链式法则(串联关系就相乘,并联关系就相加)求出总梯度。

    狗尾续貂,对这个反传大致做了一个流程图,如下图所示,帮助理解。

    注意,均值的梯度、方差的梯度的计算,只是为了保证梯度的反向传播链路的通畅,而不是为了更新自己(没明白下文还会解释);缩放因子$gamma$和平移因子$eta$的梯度传播则和权重$W$一样,不影响反向传播链路的通畅,只是为了更新自己。

    最后的结果就是原论文中表述:

    参数的初始化及更新

    讨论一下参数的初始化及更新问题。

    • $W$
      初始化用标准正态分布,更新用梯度下降
      与经典网络的初始化相同,初始化一个标准正态分布(即Xavier方法)。
    • $b$
      省略掉该参数
      在经典的神经网络里,$b$作为偏置,用于解决那些$W$无法通过与$x$相乘搞定的"损失减少要求",即对于本层所有神经元的加权和进行各自的平移。而加入BN后,$eta$的作用正是进行平移。$b$的作用被$eta$所完全替代了,因此省略掉$b$。
      了解过ResNet结构的朋友会发现该网络中的卷积,都没有偏置,为什么?下面截图是Kaiming He在github上回答原话。(踩坑无数必须体会深刻)
    • $mu$和$sigma$
      初始化取决于统计量,仅更新梯度,但不更新值本身
      在训练阶段,每个mini-batch上进行前向传播时,通过对本batch上的$m$个样本进行统计得到;
      在反向传播时,计算出它们的梯度$ell$对$mu$的梯度、$ell$对$sigma$的梯度,用于进行梯度传播。
      但是$mu$和$sigma$这两个值本身不必进行更新,因为在下一个mini-batch会计算自己的统计量,所以前一个mini-batch获得的$mu$和$sigma$没意义。$gamma$和$eta$
    • 初始化为1、0,更新用梯度下降

    $gamma$作为“准方差”,初始化为一个全1向量;而$eta$作为"准均值”,初始化为一个全$0$向量,他俩的初始值对于刚刚完成标准正态化的$hat{vec x}$来说,没起任何作用。
    至于将要变成什么值,起多大作用,那就交给后续的训练,即采用梯度下降进行更新,方式同$W$。

    评估阶段

    $gamma, eta$是在整个训练集上训练出来的,与$W$一样,训练结束就可获得。

    然而,$mu$和$sigma$是靠每一个mini-batch的统计得到,因为评估时只有一条样本,batch_size相当于是1,在只有1个向量的数据组上进行标准化后,成了一个全0向量,这可咋办?

    来自训练集的均值和方差

    做法是用训练集来估计总体均值$mu$和总体标准差$sigma$。

    • 简单平均法
      把每个mini-batch的均值和方差都保存下来,然后训练完了求均值的均值,方差的均值即可。
    • 移动指数平均(Exponential Moving Average)
      这是对均值的近似。
      仅以$mu$举例:$mu_{total} = decay * mu_{total} + (1 - decay) * mu$,其中$decay$是衰减系数。即总均值$mu_{total}$是前一个mini-batch统计的总均值和本次mini-batch的$mu$加权求和。至于衰减率$decay$在区间$[0, 1]$之间,$decay$越接近1,结果$mu_{total}$越稳定,越受较远的大范围的样本影响;$decay$越接近0,结果$mu_{total}$越波动,越受较近的小范围的样本影响。

    事实上,简单平均可能更好,简单平均本质上是平均权重,但是简单平均需要保存所有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}、eta$就已知了,1个BN层对一条测试样本的前向传播只是增加了一层线性计算而已。

    一张图小结:

    补充解答

    BN改善了ICS吗?

    原作者认为BN是旨在解决了 ICS(Internal Covariate Shift)问题。原文是这样解释:

    什么是ICS?

    所谓Covariate Shift,是指相比于训练集数据的特征,测试集数据的特征分布发生了变化。

    而原作者定义的Internal Covariate Shift,设想把每层神经网络看做一个单独的模型,它有着自己对应的输入与输出。如果这个“模型”越靠近输出层,由于训练过程中前面多层的权重的更新频繁,导致它每个神经元的输入(即上一层的激活值)的数值分布,总在不停地变化,这导致训练困难。

    【更详细通俗地讲,网络一旦train起来,那么参数就要发生更新,除了输入层的数据外(因为输入层数据,我们已经人为地为每个样本归一化),后面网络每一层的输入数据分布是一直在发生变化的。因为在训练的时候,前面层训练参数的更新将导致后面层输入数据分布的变化。以网络第二层为例:网络的第二层输入,是由第一层的参数和input计算得到的,而第一层的参数在整个训练过程中一直在变化,因此必然会引起后面每一层输入数据分布的改变。我们把网络中间层在训练过程中,数据分布的改变称之为:“Internal  Covariate Shift”。

    对于深度网络的训练是一个复杂的过程,只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。一旦网络某一层的输入数据的分布发生改变,那么这一层网络就需要去适应学习这个新的数据分布,所以如果训练过程中,训练数据的分布一直在发生变化,那么将会影响网络的训练速度。】

    然而,一个启发性的解释很容易被推翻,又有人做了更进一步的解释。

    BN与ICS无关

    2018年的文章《How Does Batch Normalization Help Optimization?》做了实验,如下图所示。

    左图表明,三个网络训练曲线,最终都达成了较高的精度;右图是三个网络中抽出3个层的激活值,绘制9个HISTOGRAMS图,每层激活值的分布都在训练过程中不断变化(HISTOGRAMS图),尤其是网络中更深的层,这导致了ICS问题(根据上文的ICS定义)。
    • 应用了BN,观察到的右图(Standard + BatchNorm)的激活值分布变化很明显,理论上将引起明显的ICS问题。
    • 在BN层后叠加噪音(输入到后面的非线性激活,相当于BN白干了),观察到的右图(Standard+"Noisy" BatchNorm)的激活值分布变化更为突出,理论上将引起更为明显的ICS问题。

    (然而,我的理解是:如果每个BN层后叠加噪音,下一层的BN也会进行标准化,层层抵消,相当于仅最后一个BN层后叠加的噪音增大了ICS)然而两种情况下,左图BN的表现依然非常稳定。即BN并没有减少ICS。
    那么,BN是为什么有效?

    BN改善了损失的平滑性

    上图论文的作者定义了一个描述损失函数平滑度的函数,观察加入BN的前后,损失函数平滑性的变化。如下图所示,纵轴的数值越小,表明损失函数曲面越平滑;纵轴数值越大,表明损失函数曲面越颠簸。蓝色线为加入BN后的损失函数的平滑度,可以看到,加入BN后,损失函数曲面的平滑程度得到了显著改善。

    因此得到的结论是:BN的加入使得损失函数曲面变得平滑,而平滑的损失函数进行梯度下降法变得非常容易。

    什么是平滑性?

    对平滑性的理解,我想没有比下图更合适的了:

    图中所展示的是,ResNet中引入的shortcut connection,实际上是对损失函数的平滑作用。显然,对于左侧的损失函数,梯度下降将是异常困难;而对于右侧,即经过平滑的损失函数,将大大提升训练效率由于权重参数动辄千万,必然将权重数映射成2个,因此绘制损失函数曲面相当需要技巧与计算代价,尚未找到BN的平滑性3D图对比,但不影响上述论文中BN对平滑性改善效果的证明。

    其他值得讨论的问题

    BN层的位置能不能调整?如果能调整哪个位置更好?

    能。原因:由前述BN的反向传播可知,BN不管放在网络的哪个位置,都可以实现这两个功能:训练$gamma$和$eta$、传递梯度到前一层,所以位置并不限于ReLU之前。原始论文中,BN被放在本层ReLU之前,即$$vec a^{l+1} = ReLU[BN(W^{l+1}vec a^l + vec b^{l+1})]$$

    也有[测试](https://github.com/ducha-aiki/caffenet-benchmark/blob/master/batchnorm.md)表明,BN放在上一层ReLU之后,效果更好,即$$vec a^{l+1} = ReLU[W^{l+1}BN(vec a^l) + vec b^{l+1}]$$

    但是由于这些都是试验证明,而非理论证明,因此无法肯定BN放在ReLU后就一定更好。在实践中可以都试试。

    在训练时为什么不直接使用整个训练集的均值/方差?

    使用 BN 的目的就是为了保证每批数据的分布稳定,使用全局统计量反而违背了这个初衷。

    在预测时为什么不直接使用整个训练集的均值/方差?

    完全可以。由于神经网络的训练数据量一般很大,所以内存装不下,因此用指数滑动平均方法去近似值,好处是不占内存,计算方便,但其结果不如整个训练集的均值/方差那么准确。

    batch_size的配置

    不适合batch_size较小的学习任务。因为batch_size太小,每一个step里前向计算中所统计的本batch上的方差和均值,噪音声量大,与总体方差和总体均值相差太大。前向计算已经不准了,反向传播的误差就更大了。尤其是最极端的在线学习(batch_size=1),原因为无法获得总体统计量。

    对学习率有何影响?

    由于BN对损失函数的平滑作用,因此可以采用较大的学习率。

    BN是正则化吗?

    在深度学习中,正则化一般是指为避免过拟合而限制模型参数规模的做法。即正则化=简化。BN能够平滑损失函数的曲面,显然属于正则化。不过,除了在过拟合时起正则作用,在欠拟合状况下,BN也能提升收敛速度。

    与Dropout的有何异同?

    BN由于平滑了损失函数的梯度函数,不仅使得模型训练精度提升了,而且收敛速度也提升了;Dropout是一种集成策略,只能提升模型训练精度。因此BN更受欢迎。

    能否和Dropout混合使用?

    虽然混合使用较麻烦,但是可以。不过现在主流模型已经全面倒戈BN。Dropout之前最常用的场合是全连接层,也被全局池化日渐取代。既生瑜何生亮。

    BN可以用在哪些层?

    所有的层。从第一个隐藏层到输出层,均可使用,而且全部加BN效果往往最好。

    BN可以用在哪些类型的网络?

    MLP、CNN均ok,几乎成了这类网络的必选项。RNN网络不ok,因为无论训练和测试阶段,每个batch上的输入序列的长度都不确定,均值和方差的统计非常困难。

    BN的缺点

    训练时前向传播的时间将增大。(但是迭代次数变少了,总的时间反而少了)

    除此之外,从上述也可以看出,batch normalization依赖于batch的大小,当batch值很小时,计算的均值和方差不稳定。研究表明对于ResNet类模型在ImageNet数据集上,batch从16降低到8时开始有非常明显的性能下降,在训练过程中计算的均值和方差不准确,而在测试的时候使用的就是训练过程中保持下来的均值和方差。这一个特性,导致batch normalization不适合以下的几种场景

    (1)  batch非常小,比如训练资源有限无法应用较大的batch,也比如在线学习等使用单例进行模型参数更新的场景。

    (2)  RNN,因为它是一个动态的网络结构,同一个batch中训练实例有长有短,导致每一个时间步长必须维持各自的统计量,这使得BN并不能正确的使用。在rnn中,对bn进行改进也非常的困难。不过,困难并不意味着没人做,事实上现在仍然可以使用的,不过这超出了咱们初识境的学习范围。

    BN的改进

    针对BN依赖于batch的这个问题,BN的作者亲自现身提供了改进,即在原来的基础上增加了一个仿射变换:$$x_i' = frac{x_i - mu_{eta}}{sigma_{eta}}cdot r + d$$

    其中参数$r, d$就是仿射变换参数,它们本身是通过如下的方式进行计算的$r = frac{sigma_{eta}}{sigma}, d = frac{mu_{eta} - mu}{sigma}$,其中参数都是通过滑动平均的方法进行更新:$$egin{align}mu & := mu + alpha(mu_{eta} - mu) \ sigma & := sigma + alpha(sigma_{eta} - sigma)end{align}$$

    所以$r$ 和 $d$就是一个跟样本有关的参数,通过这样的变换来进行学习,这两个参数在训练的时候并不参与训练。

    在实际使用的时候,先使用BN进行训练得到一个相对稳定的移动平均,网络迭代的后期再使用刚才的方法,称为Batch Renormalization,当然$r$ 和 $d$的大小必须进行限制。

    Batch Normalization的变种

    Normalization思想非常简单,为深层网络的训练做出了很大贡献。对于CNN,BN的操作是在各个特征维度之间单独进行,也就是说各个通道是分别进行Batch Normalization操作的。如果输出的blob大小为$(N,C,H,W)$,那么在每一层normalization就是基于$N*H*W$个数值进行求平均以及方差的操作。而因为有依赖于样本数目的缺陷,所以也被研究人员盯上进行改进。说的比较多的就是Layer NormalizationInstance NormalizationGroup Normalization了。

    Layer Normalization

    前面说了Batch Normalization各个通道之间是独立进行计算,如果抛弃对batch的依赖,也就是每一个样本都单独进行normalization,同时各个通道都要用到,就得到了Layer Normalization。跟Batch Normalization仅针对单个神经元不同,Layer Normalization考虑了神经网络中一层的神经元。如果输出的blob大小为(N,C,H,W),那么在每一层Layer Normalization就是基于$C*H*W$个数值进行求平均以及方差的操作。

    Instance Normalization

    Layer Normalization把每一层的特征通道一起用于归一化,如果每一个特征层单独进行归一化呢?也就是限制在某一个特征通道内,那就是instance normalization了。如果输出的blob大小为(N,C,H,W),那么在每一层Instance Normalization就是基于$H*W$个数值进行求平均以及方差的操作。对于风格化类的图像应用,Instance Normalization通常能取得更好的结果,它的使用本来就是风格迁移应用中提出。

    Group Normalization

    Group Normalization是Layer Normalization和Instance Normalization 的中间体, Group Normalization将channel方向分group,然后对每个Group内做归一化,算其均值与方差。如果输出的blob大小为(N,C,H,W),将通道C分为G个组,那么Group Normalization就是基于$G*H*W$个数值进行求平均以及方差的操作。我只想说,你们真会玩,要榨干所有可能性。

    在Batch Normalization之外,有人提出了通用版本Generalized Batch Normalization,有人提出了硬件更加友好的L1-Norm Batch Normalization等,不再一一讲述。

    另一方面,以上的Batch Normalization,Layer Normalization,Instance Normalization都是将规范化应用于输入数据$x$,Weight normalization则是对权重进行规范化,感兴趣的可以自行了解,使用比较少,也不在我们的讨论范围。

    这么多的Normalization怎么使用呢?有一些基本的建议吧,不一定是正确答案。

    (1)正常的处理图片的CNN模型都应该使用Batch Normalization。只要保证batch size较大(不低于32),并且打乱了输入样本的顺序。如果batch太小,则优先用Group Normalization替代

    (2)对于RNN等时序模型,有时候同一个batch内部的训练实例长度不一(不同长度的句子),则不同的时态下需要保存不同的统计量,无法正确使用BN层,只能使用Layer Normalization

    (3)对于图像生成以及风格迁移类应用,使用Instance Normalization更加合适

    BN的优点总结

    (1) 主流观点,Batch Normalization调整了数据的分布,不考虑激活函数,它让每一层的输出归一化到了均值为0方差为1的分布,这保证了梯度的有效性,目前大部分资料都这样解释,比如BN的原始论文认为的缓解了Internal Covariate Shift(ICS)问题。

    (2) 可以使用更大的学习率,Bjorck论文《Understanding batch normalization》指出BN有效是因为用上BN层之后可以使用更大的学习率,从而跳出不好的局部极值,增强泛化能力,在它们的研究中做了大量的实验来验证。

    (3) 损失平面平滑。Santurkar论文《How does batch normalization help optimization?》的研究提出,BN有效的根本原因不在于调整了分布,因为即使是在BN层后模拟ICS,也仍然可以取得好的结果。它们指出,BN有效的根本原因是平滑了损失平面。之前我们说过,Z-score标准化对于包括孤立点的分布可以进行更平滑的调整。

    【后面补充部分摘选自言友三的知乎回答

    实践

    【感谢作者的代码分享与详细讲解!】

    构造网络

    构建两个全连接神经网络:

    • 一个是普通网络,包括2个隐层,1个输出层。
    • 一个是有BN的网络,包括2个隐层,1个输出层。
      第1层中的BN是我们自定义的,第2层和第3层中的BN是调用tensorflow实现。

    定义输入占位符,定义三个层的权重,方便后面使用

    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-3
    
    x = tf.placeholder(tf.float32, shape=[None, 784])
    y_ = tf.placeholder(tf.float32, shape=[None, 10])

    Layer 1 层:无BN

    w1 = tf.Variable(w1_initial)
    b1 = tf.Variable(tf.zeros([100]))
    z1 = tf.matmul(x,w1)+b1
    l1 = tf.nn.sigmoid(z1)

    Layer 1 层:有BN(自定义BN层)

    w1_BN = tf.Variable(w1_initial)
    
    # 因为BN的引入,b的作用被BN层替代,省略。
    z1_BN = tf.matmul(x,w1_BN)
    
    # 计算加权和的均值和方差,0是指batch这个维度
    batch_mean1, batch_var1 = tf.nn.moments(z1_BN,[0])
    
    # 正则化
    z1_hat = (z1_BN - batch_mean1) / tf.sqrt(batch_var1 + epsilon)
    
    # 新建两个变量scale and beta
    scale1 = tf.Variable(tf.ones([100]))
    beta1 = tf.Variable(tf.zeros([100]))
    
    # 计算被还原的BN1,即BN文章里的y
    BN1 = scale1 * z1_hat + beta1
    
    # l1_BN = tf.nn.sigmoid(BN1)
    l1_BN = tf.nn.relu(BN1)

    Layer 2 层:无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)

    Layer 2 层:有BN(使用tensorflow创建BN层)

    w2_BN = tf.Variable(w2_initial)
    z2_BN = tf.matmul(l1_BN,w2_BN)
    
    # 计算加权和的均值和方差,0是指batch这个维度
    batch_mean2, batch_var2 = tf.nn.moments(z2_BN,[0])
    
    # 新建两个变量scale and beta
    scale2 = tf.Variable(tf.ones([100]))
    beta2 = tf.Variable(tf.zeros([100]))
    
    # 计算被还原的BN2,即BN文章里的y。使用
    BN2 = tf.nn.batch_normalization(z2_BN,batch_mean2,batch_var2,beta2,scale2,epsilon)
    
    # l2_BN = tf.nn.sigmoid(BN2)
    l2_BN = tf.nn.relu(BN2)

    Layer 3 层:无BN

    w3 = tf.Variable(w3_initial)
    b3 = tf.Variable(tf.zeros([10]))
    y  = tf.nn.softmax(tf.matmul(l2,w3)+b3)

    Layer 3 层:有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。

    • 损失使用交叉熵,因为我们输出层的激活函数为softmax。
    • 优化器用梯度下降
    # 普通网络的损失
    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)
    
    # 普通网络的accuracy
    correct_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_BN
    correct_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 <ipython-input-9-175577f60212>:12: arg_max (from tensorflow.python.ops.gen_math_ops) is deprecated and will be removed in a future version.
    Instructions for updating:
    Use `argmax` instead

    训练网络

    • 训练普通网络、有BN网络,
    • 对比训练阶段的学习曲线。
    • 对比BN对输入加权和的影响。

    首先是训练普通网络、有BN网络

    # zs存放普通网络第2个隐层的非线性激活前的加权和向量。
    # BNs存放BN网络第2个隐层的非线性激活前的加权和向量(经过了BN)。
    # acc, acc_BN存放训练阶段,通过测试集测得的精度序列。用于绘制learning curve
    zs, BNs, acc, acc_BN = [], [], [], []
    
    # 开一个sess,同时跑train_step、train_step_BN
    sess = tf.InteractiveSession()
    sess.run(tf.global_variables_initializer())
    for i in tqdm.tqdm(range(40000)):
        batch = mnist.train.next_batch(60)
        
        # 运行train_step,训练无BN网络
        train_step.run(feed_dict={x: batch[0], y_: batch[1]})
        # 运行train_step_BN,训练有BN的网络
        train_step_BN.run(feed_dict={x: batch[0], y_: batch[1]})
        
        if i % 50 is 0:
            # 每50个batch,测一测精度,并把第二层的加权输入,没进BN层之前的z2,被BN层处理过的BN2,都算一遍
            res = sess.run([accuracy,accuracy_BN,z2,BN2],feed_dict={x: mnist.test.images, y_: mnist.test.labels})
            
            # 保存训练阶段的精度记录,acc是无BN网络的记录,acc_BN是有BN网络的记录,
            acc.append(res[0])
            acc_BN.append(res[1])
            
            # 保存训练阶段的z2,BN2的历史记录
            zs.append(np.mean(res[2],axis=0)) 
            BNs.append(np.mean(res[3],axis=0))
    
    zs, BNs, acc, acc_BN = np.array(zs), np.array(BNs), np.array(acc), np.array(acc_BN)
    100%|████████████████████████████████████| 40000/40000 [06:54<00:00, 96.44it/s]

    对学习曲线的影响

    对比训练阶段的学习曲线:
    绘制精度训练曲线的结果表明BN的加入,大大提升了训练效率。

    fig, ax = plt.subplots()
    
    ax.plot(range(0,len(acc)*50,50),acc, label='Without BN')
    ax.plot(range(0,len(acc)*50,50),acc_BN, label='With BN')
    ax.set_xlabel('Training steps')
    ax.set_ylabel('Accuracy')
    # ax.set_ylim([0.8,1])
    ax.set_title('Batch Normalization Accuracy')
    ax.legend(loc=4)
    plt.show()

    对加权和的影响

    zs 来自无BN网络的第二个隐层的输入加权和向量,即下一步将喂给本层的激活函数。
    BNs 来自有BN网络的第二个隐层的输入加权和经过BN层处理后的向量,即下一步也将喂给本层的激活函数。

    • 效果:没有BN,则网络的加权和完全跑飞了;有BN,则加权和会被约束在0附近。
    # 显示在无BN和有BN两个网络里,800次前向传播中第2个隐层的5个神经元的加权和的输入范围。
    fig, axes = plt.subplots(5, 2, figsize=(6,12))
    # fig, axes = plt.subplots(5, 2)
    fig.tight_layout()
    
    for i, ax in enumerate(axes):
        ax[0].set_title("Without BN")
        ax[1].set_title("With BN")
        # [:,i]表示取其中一列,也就是对应神经网络中
        # print(zs[:,i].shape)
        ax[0].plot(zs[:,i])
        ax[1].plot(BNs[:,i])
    plt.show()

    测试阶段的问题

    带有BN的网络,不能直接用于测试。
    因为测试阶段每个样本如果是逐个输入,相当于batch_size=1,那么均值为自己,方差为0,正则化后将为0。
    导致模型的输入永远是一个0值。因此预测将根据训练的权重输出一个大概率是错误的预测。
    predictions = []
    correct = 0
    for i in range(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

    实际应用的细节

    为了避免推理(or预测)时出现的问题,需要注意一下几点:

    构造BN层

    batch_norm_wrapper将实现在更为高级的功能,合二为一:

    • 训练阶段,统计训练集均值和方差;
    • 推理阶段,直接使用训练阶段的统计结果。
    # batch_norm_wrapper 是对tensorflow中BN层实现的一个核心功能的重现。
    # https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/layers/python/layers/layers.py#L102
    # 其功能是:对于每个batch的每一层的加权输入
    # 在训练阶段,统计方差和均值,一边记录和更新总体方差和均值。
    # 在测试/评估阶段,直接使用训练时统计好的总体方差和均值。
    def batch_norm_wrapper(inputs, is_training, decay = 0.999):
    
        # 每个BN层,引入了4个变量 scale beta pop_mean pop_var,其中:
        # scale beta 是可训练的,训练结束后被保存为模型参数
        # pop_mean pop_var 是不可训练,只在训练中进行统计,
        # pop_mean pop_var 最终保存为模型的变量。在测试时重构的计算图会接入该变量,只要载入训练参数即可。
        scale = tf.Variable(tf.ones([inputs.get_shape()[-1]]))
        beta = tf.Variable(tf.zeros([inputs.get_shape()[-1]]))
        pop_mean = tf.Variable(tf.zeros([inputs.get_shape()[-1]]), trainable=False)
        pop_var = tf.Variable(tf.ones([inputs.get_shape()[-1]]), trainable=False)
    
        if is_training:
            # 以下为训练时的BN计算图构造
            # batch_mean、batch_var在一个batch里的每一层,在前向传播时会计算一次,
            # 在反传时通过它来计算本层输入加权和的梯度,仅仅作为整个网络传递梯度的功能。在训练结束后被废弃。
            batch_mean, batch_var = tf.nn.moments(inputs,[0])
            
            # 通过移动指数平均的方式,把每一个batch的统计量汇总进来,更新总体统计量的估计值pop_mean、pop_var
            # assign构建计算图一个operation,即把pop_mean * decay + batch_mean * (1 - decay) 赋值给pop_mean
            train_mean = tf.assign(pop_mean,pop_mean * decay + batch_mean * (1 - decay))
            train_var = tf.assign(pop_var,pop_var * decay + batch_var * (1 - decay))
    
            # 确保本层的train_mean、train_var这两个operation都执行了,才进行BN。
            with tf.control_dependencies([train_mean, train_var]):
                return tf.nn.batch_normalization(inputs,batch_mean, batch_var, beta, scale, epsilon)
        else:
            # 以下为测试时的BN计算图构造,即直接载入已训练模型的beta, scale,已训练模型中保存的pop_mean, pop_var
            return tf.nn.batch_normalization(inputs,pop_mean, pop_var, beta, scale, epsilon)

    构造计算图

    其中通过调用上面定义好的BN包装器,实现BN层的简洁添加。
    def build_graph(is_training):
        x = tf.placeholder(tf.float32, shape=[None, 784],name="x")
        y_ = tf.placeholder(tf.float32, shape=[None, 10],name="y_")
    
        w1 = tf.Variable(w1_initial)
        z1 = tf.matmul(x,w1)
        bn1 = batch_norm_wrapper(z1, is_training)
        l1 = tf.nn.sigmoid(bn1)
    
        w2 = tf.Variable(w2_initial)
        z2 = tf.matmul(l1,w2)
        bn2 = batch_norm_wrapper(z2, is_training)
        l2 = tf.nn.sigmoid(bn2)
    
        w3 = tf.Variable(w3_initial)
        # b3 = tf.Variable(tf.zeros([10]))
        # y  = tf.nn.softmax(tf.matmul(l2, w3))
        z3 = tf.matmul(l2,w3)
        bn3 = batch_norm_wrapper(z3, is_training)
        y  = tf.nn.softmax(bn3)
    
    
        cross_entropy = -tf.reduce_sum(y_*tf.log(y))
        train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
        correct_prediction = tf.equal(tf.arg_max(y,1),tf.arg_max(y_,1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32),name='accuracy')
    
        return (x, y_), train_step, accuracy, y

    训练阶段

    • 通过传入is_training=True,开启计算图的方差和均值统计操作。
    • 训练结束后,保存模型,包含计算图和参数,实际上只有参数会被用到,因为在预测时会新建计算图。
    tf.reset_default_graph()
    (x, y_), train_step, accuracy, _,= build_graph(is_training=True)
    
    acc = []
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for i in tqdm.tqdm(range(10000)):
            batch = mnist.train.next_batch(60)
            train_step.run(feed_dict={x: batch[0], y_: batch[1]})
            if i % 200 is 0:
                res = sess.run([accuracy],feed_dict={x: mnist.test.images, y_: mnist.test.labels})
                acc.append(res[0])
                # print('batch:',i,'    accuracy:',res[0])
        # 保存模型,注意该模型是不可用的。因为其计算图是训练的计算图。
        saver=tf.train.Saver()
        # saved_model = saver.save(sess, './temp-bn-save')
        saver.save(sess, './bn_test/temp-bn-save')
        writer=tf.summary.FileWriter('./improved_graph2',sess.graph)
        writer.flush()
        writer.close()
    print("Final accuracy:", acc[-1])
    100%|███████████████████████████████████| 10000/10000 [00:38<00:00, 260.31it/s]
    
    
    Final accuracy: 0.9538

    测试阶段

    先构造推理的计算图,再把训练好的模型参数载入到这个计算图中。

    tf.reset_default_graph()
    # (x, y_), _, accuracy, y, saver = build_graph(is_training=False)
    (x, y_), _, accuracy, y = build_graph(is_training=False)
    
    predictions = []
    correct = 0
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        
        # 读取训练时模型,将学到的权重参数、估计的总体均值、方差,通过restore,载入到运行的计算图中。
        saver=tf.train.Saver()
        saver.restore(sess, './bn_test/temp-bn-save')
        saver.save(sess, './bn_release/temp-bn-save')
        
        for i in range(100):
            pred, corr = sess.run([tf.arg_max(y,1), accuracy],
                                 feed_dict={x: [mnist.test.images[i]], y_: [mnist.test.labels[i]]})
            correct += corr
            predictions.append(pred[0])
    print("PREDICTIONS:", predictions)
    print("ACCURACY:", correct/100)
    INFO:tensorflow:Restoring parameters from ./bn_test/temp-bn-save
    PREDICTIONS: [7, 2, 1, 0, 4, 1, 4, 9, 6, 9, 0, 6, 9, 0, 1, 5, 9, 7, 3, 4, 9, 6, 6, 5, 4, 0, 7, 4, 0, 1, 3, 1, 3, 4, 7, 2, 7, 1, 2, 1, 1, 7, 4, 2, 3, 5, 1, 2, 4, 4, 6, 3, 5, 5, 6, 0, 4, 1, 9, 7, 7, 8, 9, 3, 7, 4, 1, 4, 3, 0, 7, 0, 2, 9, 1, 7, 3, 2, 9, 7, 7, 6, 2, 7, 8, 4, 7, 3, 6, 1, 3, 6, 9, 3, 1, 4, 1, 7, 6, 9]
    ACCURACY: 0.97

    其他可以做的试验

    如果感兴趣,不妨基于上面的代码继续进行试验

    BN到添加位置试验

    在加权和后 vs 在加权和前
    靠近输入层 vs 靠近输出层

    BN的添加量试验

    在每一层都加BN vs 在少数几层加BN

    BN对学习率的影响

    大学习率 vs 小学习率

    Batch_size对BN效果的影响

    小batch_size vs 大batch_size

    Min是清明的茗
  • 相关阅读:
    阿里云ecs服务器wamp内网可以访问,外网ip、域名无法访问
    python- 粘包 struct,socketserver
    python-网络编程
    python-模块-包
    python- 异常
    python-模块 time, os, sys
    python_模块 collections,random
    python_模块 hashlib ,configparser, logging
    python_ 模块 json pickle shelve
    python-面向对象中的特殊方法 ,反射,与单例模式
  • 原文地址:https://www.cnblogs.com/MinPage/p/14087216.html
Copyright © 2020-2023  润新知