BN(Batch Normalization) 的理论理解以及在tf.keras中的实际应用和总结

1. BN层的概念,其与LN、IN、GN的异同

BN层于2015年由谷歌提出,可以有效的提升网络训练效率。BN层可以加快收敛速度,可以缓解梯度消失,具有一定的正则化作用。BN层本质上使损失函数变得平滑,所以收敛速度变快且不易落入局部最优值【1】【2】。BN层针对全链接网络结构是计算前面输出层在一个batch内每一个节点的均值和方差,然后针对每一个节点分别减去对应均值和除以对应方差,然后针对每一个节点再进行一次线性变换(即先乘以一个参数再加上一个参数),所以每一个节点具有4个参数;针对CNN网络结构,为了缩减存储量是计算前面输出层在一个batch内每一个channel内所有元素的均值和方差,然后针对每一个channel层分别减去对应均值和除以对应方差,然后针对每一个channel再进行一次线性变换,所以每一个channel具有4个参数。可以这样粗略的理解,BN层将前面的输出分布强制拉到坐标原点附近,再由线性变换系数进行变换,这样速度远快于通过学习率一点点修正前面层的权重以得到类似分布。BN层与LN、IN、GN的不同由下图可知,区别就是求均值和方差的作用范围不同。

实际测试发现BN层是真的非常有用,例如测试mobilenetV2,随机初始化权重,同样的数据,如果网络结构中包含BN层,那第一个epoch就会开始收敛,迭代几个epoch就可以完成训练;如果把所有BN层去掉,那网络根本不收敛,再尝试仅仅保留前面20个左右的卷积层时,第一个epoch不会收敛,迭代几十个epoch后才开始收敛,此后学习速度也很慢,需要训练很多epoch才会完成训练。其实还同步测试了VGG16,VGG16比较好训练,但是再增加几个卷积层到达20几个后,就开始出现类似难以收敛难以训练的情况了,说明BN层确实有助于构建更深的网络。

上一段说的不收敛问题,推断可能是损失函数不够平滑,过于抖动,导致很难找到比较好的收敛方向,因为周围可能基本全是局部最小值,可能原因是在网络在训练时,按照梯度下降法训练前面的层时,权重的微变会带来后面层的训练失效,即当前面层的权重发生微变时,由于链式效应导致原本针对后面层计算的调整量失效,需要根据前面层调整后的权重重新计算,而BN层的存在,可能减小了这种链式影响,减小了前面层网络权重变化对后面层网络权重的影响,以此加快收敛速度。可以考虑是不是改善一下传统的不考虑前面层还是后面层的梯度下降法,抑制一下这种现象。关于网络不收敛的教程。【4】

随机初始化问题:一般情况下网络随机初始化,都不太可能得到一个比较好的最终结果,大牛训练网络时,肯定不是随机初始化然后一次训练搞定的,肯定是初始化有一定的规则,然后通过控制中间层的各种参数最终训练出来的,所以对于工程人员最优的选择就是迁移学习,这也解释了为何有些论文宁愿改变网络结构也要匹配别人训练好的参数,然后再训练,例如孔洞卷积就是为了匹配现有权重参数的感受野而修改的结构。自己的测试也验证了这一问题,同样的数据,同样的网络模型,随机初始化,最终测试集acc只有0.62,但是按照imagenet初始化前n-1层(训练时不冻结任何层,初始化后直接全部训练),最终测试集acc能够到0.96。

batchsize大小问题:batchsize不宜过大也不宜过小,BN层对batchsize大小有要求不能太小,那是不是就是设置的越大越好呢,其实也不是,batchsze也会影响目标函数的最优解,我们知道训练目标是网络最终收敛的时候得到目标函数的最优解,而在实际的训练过程中我们并不总是能找到绝对最小值区域,很多时候是陷入局部最优解。这时候,如果将batchsize调整的较小,其每次的迭代下降方向就不是最准确的,loss小范围震荡下降反而会跳出局部最优解,从而寻找loss更低的区域。

站在网络的角度减小类内间距增大类间间距,记住网络不智能只能感受视觉相似特征,不能推理和联想。

2. BN层在推理过程与训练过程的不同

上一节已经讲过了,BN层的最小模块包含4个参数,前两个是从数据集中统计而来,后两个是神经网络训练得到的。这个要分训练状态和推理状态分别考虑,在训练状态,前两个参数是在一个batchsize内统计得到,后两个参数是反向梯度下降训练得到;在推理状态,前两个参数是直接使用训练过程中采用滑动平均维护的均值和方差,并不对当前数据集进行统计,后两个参数不发生任何改变,直接使用训练过程中得到的数值。滑动平均公式如下:

u_{new} = decay * u_{old} + (1 - decay) * u

其中decay是衰减系数。即总均值u_{new}是前一个mini-batch统计的总均值u_{old}​和本次mini-batch的u加权求和。至于衰减率 decay在区间0~1之间,decay越接近1,结果u_{new}​越稳定,越受较远的大范围的样本影响;decay越接近0,结果u_{new}​越波动,越受较近的小范围的样本影响。

3. BN层滑动平均系数decay与batchsize和steps的关系

BN层不管如何始终要坚持一个原则,就是训练时每个batch得到的统计均值和方差要尽量接近最终的滑动平均均值和方差(也即推理时使用的均值和方差),如果直接相等也完全可以,但是本着增加抖动噪声增加正则化的原则,可以适度不相等。

为了满足上面说的条件,需要调整初始化值、总数据量、batchsize、steps和decay,否则很容易出现不相等的情况。下面罗列的几种情况:

当steps不是很大,但是decay=0.999时,会出现最终滑动平均值很接近初始值,应该减小decay;

当总数据量不足,但是batchsize很大时,造成steps很小,有可能造成不一致性,如果单个batch内的统计值很接近滑动平均值则不会造成不一致性,可以尝试适度减小batchsize;

在tf.keras中由于对training参数和trainable属性设置不合理,造成不一致性。

4. BN层的一些问答【2】

  • BN层的位置能不能调整?如果能调整哪个位置更好?能。原因:由第二章BN的反向传播可知,BN不管放在网络的哪个位置,都可以实现这两个功能:训练后两个参数、传递梯度到前一层,所以位置并不限于ReLU之前。原始论文中,BN被放在本层ReLU之前,也有其它测试表明BN放在上一层ReLU之后,效果更好。但是由于这些都是试验证明,而非理论证明,因此无法肯定BN放在ReLU后就一定更好。在实践中可以都试试。如果激活函数是sigmoid函数,建议BN放在激活函数前面,感觉BN层有可能会把数值网中心拉一下,以增大梯度。
  • 在训练时为什么不直接使用整个训练集的均值/方差?使用 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的缺点,在训练时前向传播的时间将增大。(但是迭代次数变少了,总的时间反而少了)

5. BN层在tf.keras中的应用

在tf.keras中存在training参数(可选值True/False/None,默认None)和trainable属性(可选值True/False,默认True)。training参数可以针对每一层分别设置也可以针对整个模型进行设置,分别表示某一层或整个模型是处于训练状态还是推理状态;trainable属性同样可以针对每一层分别设置也可以针对整个模型进行设置,分别表示某一层或整个模型是否可以被训练即BN层四个参数是否可以被改变。除了BN层外training参数和trainable属性相互独立互不影响,BN层中四种设置方式有相互影响,详见后面结论。四种设置方式如下:

  • training参数针对每一层分别设置:x = layers.BatchNormalization(axis=channel_axis, epsilon=1e-3, momentum=0.99, name='Conv/BatchNorm')(x, training=True)
  • training参数针对整个模型进行设置:output = model(image, training=True)
  • trainable属性针对每一层分别设置:model.get_layer(name="Conv_1/BatchNorm").trainable = True
  • trainable属性针对整个模型进行设置:model.trainable = True

为了研究内外training参数设置(『内』表示针对BN层进行设置,『外』表示针对模型进行设置)、内外trainable属性设置和内外trainable属性设置顺序对BN层的影响(『内』表示针对BN层进行设置,『外』表示针对模型进行设置),我们组织了如下实验,分别研究在不同设置组合下,推理单张图片时,BN层前两个参数是否改变,推理图片结果是否正确;以及训练网络(fit函数)时,BN层前两个参数和后两个参数是否改变。前两个参数表示滑动平均均值和方差,后两个参数表示训练得来的线性变换系数。下面表格中绿色、蓝色和灰色表示不同实验设置下的结果,每一列是一种实验结果,每种颜色中的每一列的四个值是对应某种实验设置下的最终结果,四个值按照顺序分别为:推理时前两个参数是否改变(变/否)、推理时单张图片推理结果是否正确(对/错)、训练时(使用fit函数,所以外部training肯定是True,跟下方表格不一致,下面表格中的外training设置只是针对推理过程)前两个参数是否改变(变/否)、训练时后两个参数是否改变(变/否)。『空』表示默认没有设置,『T/F』表示True/False,『正/反』表示trainable属性是先设置内部再设置外部,还是先设置外部再设置内部,training参数不存在顺序,一定是先内后外,『变/否』表示参数是否发生改变,『对/错』表示单次图片推理结果是否正确。以下结果是在tensorflow2.6上测试的。

由以上实验结果可以得到实验结论:

1. 在内外部trainable属性均无False的情况下,如果内部training参数设置为True/False,则不管外部training参数的设置,遵循内部设置;如果内部training参数设置为None,则遵循外部training参数的设置外部设置不可能为None,predict()函数默认为False,evaluate()函数默认为False,fit()函数默认为True,model(image)函数默认为True,model(image, training=True)函数显式设置为True;注意:training参数为True则推理使用当前batch的均值和方差,修改滑动均值和方差,training参数为False则推理使用滑动均值和方差,不修改滑动均值和方差;

2. 在最后一步是外部trainable属性被设置为False的情况下(不针对内部再次设置),不管BN层最终training参数的设置结果,training参数一律变为False,即BN层所有参数不能改变,且BN层永远处在推理状态;在最后一步是内部trainable属性被设置为False的情况下(不针对外部再次设置),不会修改BN层最终training参数的设置结果,即BN层所有参数不能改变,BN层的状态取决于training参数的设置;注意:外部trainable属性设置为False则模型不参与训练,内部trainable属性设置为False,则所有参数不可被修改;

3. 外部trainable属性的设置可以递归的改变内部所有层的trainable属性,内部trainable属性的设置不会影响外部trainable属性的设置所以当首先设置外部trainable属性为False,然后设置内部某层的trainable属性为True时,整个模型不会发生梯度下降训练,后两个参数不会发生改变,原因是网络在进行训练前首先会检查模型外部的trainable属性,False则不训练,但是在正向推理时,该层的trainable属性为True,所以前两个参数允许改变,所以当training参数为True时,前两个参数会改变,且图片推理错误,例如第12和15列绿色和蓝色,当training参数为False时,前两个参数不会改变,且图片推理正确,例如第12和15列灰色;

4. BN层之所以在tf中有如此复杂的规律,原因是在tf中,不管是training参数还是trainable属性都存在内部设置和外部设置,且内外部设置并不是遵守谁最后设置听谁的,且针对BN层trainable属性的设置还会影响到内部training参数的设置。在torch中就没有这么复杂的规律,因为torch相对应的设置是model.train()/model.eval()和reguires_grad=True/False,两个属性相互之间没有影响且每个属性的内外设置都遵守谁后设置听谁的。

5. 总之(重点):

  • 要看训练时后两个参数是否发生改变,首先看外部trainable属性,False则不可改变,然后看内部trainable属性,False则不可改变,True则可以改变;
  • 要看训练时前两个参数是否发生改变,首先看外部trainable属性,False则不可改变,然后看内部trainable属性,False则不可改变,True则可以改变;
  • 要看单张图片推理是否正确,只需要看最终的推理状态,如果最终是推理状态,则图片推理结果正确,如果最终是训练状态,则图片推理错误(错误原因是,图片太少,单张图片求出的均值和方差与滑动均值和方差相差太大);
  • 如果内外trainable属性不一致,那么肯定是先设置模型的属性或没有设置模型参数,然后设置层的属性;
  • 以上实验结果均为两个参数和两个属性只能设置一次的情况下得到的,没有反复设置,且内部设置是针对所有BN层同时设置,所以不能包含所有实验可能,实际应用建议还是通过实际测试验证为好。

6. BN层导致的各种奇怪实验现象

1. 在采用迁移学习时,首先冻结0~n-1层,只训练最后一层,训练几轮后,完成第一阶段训练,再放开所有层,开始第二阶段训练,发现放开后第一个epoch,acc和loss都剧降,然后重新上升。 

原因:0 ~ n-1层网络中,BN层的training参数没有设置,默认是None;当第一阶段训练时,由于BN层的trainable属性被设置为False,则BN层的training参数被修改为False,所以滑动平均均值和方差均不会改变,训练时正向推理用的是之前的滑动平均均值和方差,当第二阶段训练时,由于BN层的trainable属性被设置为True,则BN层的training参数变回之前的None,所以滑动平均均值和方差会发生改变,训练时正向推理用的是当前batch的平均均值和方差,所以两个阶段所用均值和方差有较大差别,造成第一阶段学习的特征崩塌。所以,迁移学习最好的办法是,在定义网络时直接把BN层的training属性显式的定义为False,如此不管第一阶段还是第二阶段,不管训练还是测试都是使用之前数据集的滑动均值和方差,滑动均值和方差不会改变,不存在特征崩塌问题,第二种解决方案是迁移学习恢复权重后,手动修改BN层的前两个参数为新数据集对应的滑动均值和方差,然后再训练,这种方法操作比较困难。

2. 当没有采用迁移学习时,整个网络随机初始化权重然后训练,fit()函数在训练集上进行训练,acc逐步提升很正常,但是同步在验证集上acc完全不增加,用evaluate()函数测试也是完全不增加,在训练7~8轮后才开始缓慢增加,最后虽然增加了,但也增加的相当有限。

原因:原因很简单,训练和测试时BN层使用的前两个均值和方差(用于归一化的均值和方差)不同造成的,训练时采用的是当前batch的均值和方差,由于batchsize设置不是太小,所以这个均值和方差接近整体数据集的均值和方差,所以训练时acc效果正常;但是测试时采用的是滑动均值和方差,由于随机初始化的均值和方差与整体数据集的均值和方差相差巨大,且滑动系数默认0.99,所以要想使得滑动均值和方差与整体数据集的均值和方差相似,需要巨大数量的steps才可以,而我们由于把batchsize设置的偏大,造成每个epoch中的steps偏小,所以造成几个epoch后,滑动均值和方差与整体数据集的均值和方差依然相差比较大,这就是以上现象的原因。解决办法是适当减小batchsize,增大每个epoch的steps,但是BN本身也是要求batchsize要大一点,所以batchsize不能太小,此外还可以适度减小滑动系数,以减小随机初始化均值和方差对最终滑动均值和方差的影响,例如改为0.9,那么只需要50个steps,就可以把初始值在最终值中的占比降低到千分之五以下。

参考:

1. How Does Batch Normalization Help Optimization?

2. 理解Batch Normalization系列3——为什么有效及11个问题(清晰解释)​​​​​​

3. 深度学习中的BN,LN,IN,GN总结

4. 神经网络不收敛的原因

下面代码在tensorflow出现了init() missing 1 required positional argument: 'cell'报错: class Model(): def init(self): self.img_seq_shape=(10,128,128,3) self.img_shape=(128,128,3) self.train_img=dataset # self.test_img=dataset_T patch = int(128 / 2 ** 4) self.disc_patch = (patch, patch, 1) self.optimizer=tf.keras.optimizers.Adam(learning_rate=0.001) self.build_generator=self.build_generator() self.build_discriminator=self.build_discriminator() self.build_discriminator.compile(loss='binary_crossentropy', optimizer=self.optimizer, metrics=['accuracy']) self.build_generator.compile(loss='binary_crossentropy', optimizer=self.optimizer) img_seq_A = Input(shape=(10,128,128,3)) #输入图片 img_B = Input(shape=self.img_shape) #目标图片 fake_B = self.build_generator(img_seq_A) #生成的伪目标图片 self.build_discriminator.trainable = False valid = self.build_discriminator([img_seq_A, fake_B]) self.combined = tf.keras.models.Model([img_seq_A, img_B], [valid, fake_B]) self.combined.compile(loss=['binary_crossentropy', 'mse'], loss_weights=[1, 100], optimizer=self.optimizer,metrics=['accuracy']) def build_generator(self): def res_net(inputs, filters): x = inputs net = conv2d(x, filters // 2, (1, 1), 1) net = conv2d(net, filters, (3, 3), 1) net = net + x # net=tf.keras.layers.LeakyReLU(0.2)(net) return net def conv2d(inputs, filters, kernel_size, strides): x = tf.keras.layers.Conv2D(filters, kernel_size, strides, 'same')(inputs) x = tf.keras.layers.BatchNormalization()(x) x = tf.keras.layers.LeakyReLU(alpha=0.2)(x) return x d0 = tf.keras.layers.Input(shape=(10, 128, 128, 3)) out= ConvRNN2D(filters=32, kernel_size=3,padding='same')(d0) out=tf.keras.layers.Conv2D(3,1,1,'same')(out) return keras.Model(inputs=d0, outputs=out) def build_discriminator(self): def d_layer(layer_input, filters, f_size=4, bn=True): d = tf.keras.layers.Conv2D(filters, kernel_size=f_size, strides=2, padding='same')(layer_input) if bn: d = tf.keras.layers.BatchNormalization(momentum=0.8)(d) d = tf.keras.layers.LeakyReLU(alpha=0.2)(d) return d img_A = tf.keras.layers.Input(shape=(10, 128, 128, 3)) img_B = tf.keras.layers.Input(shape=(128, 128, 3)) df = 32 lstm_out = ConvRNN2D(filters=df, kernel_size=4, padding="same")(img_A) lstm_out = tf.keras.layers.LeakyReLU(alpha=0.2)(lstm_out) combined_imgs = tf.keras.layers.Concatenate(axis=-1)([lstm_out, img_B]) d1 = d_layer(combined_imgs, df)#64 d2 = d_layer(d1, df * 2)#32 d3 = d_layer(d2, df * 4)#16 d4 = d_layer(d3, df * 8)#8 validity = tf.keras.layers.Conv2D(1, kernel_size=4, strides=1, padding='same')(d4) return tf.keras.Model([img_A, img_B], validity)
05-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值