批标准化 tf.keras.layers.BatchNormalization 参数解析与应用分析

Table of Contents

函数调用

设置training=None时可能存在的问题 :tf.keras.backend.learning_phase()的特点

批标准化函数产生的变量是可训练的吗?

在使用批标准化时,要保存所有变量,而不仅仅是可训练变量

应用分析

滑动平均的计算公式:

滑动平均计算节点在批标准化中的作用

滑动平均的计算时机与注意事项:添加依赖控制

如果没有添加依赖控制会怎样? 

批标准化过程

 


说明:

1. 我用的tensorflow是1.14.0。 tensorflow的一些参数含义在1.*版本和2.*版本上是不同的。这点需要注意。以下面代码中的参数“trainable=True/False”为例,在1.*版本和2.*版本上是完全相反的含义。(劝大家早日逃离tensorflow).

2. trainable参数:是在批标准化层的类对象参数。

   training参数:是批标准化的类对象的调用函数call()的参数。

3. trainable参数:

    上面提到,在tensorflow 2.0和tensorflow 1.*中,对于批标准化层的trainable参数的相同设置有不同的含义。下面第二个代码框内有介绍。

    所应用的到底是哪种含义,建议直接去源码查看说明,我用的是tf 1.14.0, 在批标准化层的说明。


参数介绍

基类的定义如下:

class BatchNormalizationBase(Layer):
  def __init__(self,
               axis=-1,# 指向[NHWC]的channel维度,当数据shape为[NCHW]时,令axis=1
               momentum=0.99,# 计算均值与方差的滑动平均时使用的参数(滑动平均公式中的beta,不要与这里混淆)
               epsilon=1e-3,
               center=True,# bool变量,决定是否使用批标准化里的beta参数(是否进行平移)
               scale=True,# bool变量,决定是否使用批标准化里的gamma参数(是否进行缩放)
               beta_initializer='zeros',# 调用init_ops.zeros_initializer(),beta参数的0初始化,beta参数是平移参数
               gamma_initializer='ones',# 调用init_ops.ones_initializer(),gamma参数的1初始化,gamma参数是缩放参数
               moving_mean_initializer='zeros',# 均值的滑动平均值的初始化,初始均值为0
               moving_variance_initializer='ones',# 方差的滑动平均值的初始化,初始均值为1# 可见初始的均值与方差是标准正态分布的均值与方差
               beta_regularizer=None,# beta参数的正则化向,一般不用
               gamma_regularizer=None,# gamma 参数的正则化向,一般不用
               beta_constraint=None,# beta参数的约束项,一般不用
               gamma_constraint=None,# gamma 参数的约束项,一般不用
               renorm=False,
               renorm_clipping=None,
               renorm_momentum=0.99,
               fused=None,
               trainable=True,# 默认为True,这个我觉得就不要改了,没必要给自己找麻烦,
                              # 就是把我们标准化公式里面的参数添加到
                              # GraphKeys.TRAINABLE_VARIABLES这个集合里面去,
                              # 因为只有添加进去了,参数才能更新,毕竟γ和β是需要学习的参数。
                              # 但是,tf.keras.layers.BatchNormalization中并没有做到这一点,
                              # 所以需要手工执行这一操作。
               virtual_batch_size=None,
               adjustment=None,
               name=None,
               **kwargs):
    ########################
    ##只介绍参数,具体执行代码省略
    #####################

  def _get_training_value(self, training=None):
    #######
    ###该函数说明了training在不同取值时的处理,把输入的training参数转为bool变量输出,
    ###这里主要关注对training=None的处理
    #######
    if training is None:
      training = K.learning_phase() # K表示keras.backend,learning_phase()函数返回当前状态flag,是train还是test阶段,供keras使用
    if self._USE_V2_BEHAVIOR:
      if isinstance(training, int):
        training = bool(training)
      if base_layer_utils.is_in_keras_graph():
        training = math_ops.logical_and(training, self._get_trainable_var())
      else:
        training = math_ops.logical_and(training, self.trainable)
    return training

  def call(self, inputs,# 就是输入数据,默认shape=[NHWC],如果是其它shape,要对上面的axis值进行修改
         training=None  # 有三种选择:True,False,None,用于判断网络是处于训练阶段还是测试阶段。
                        # `training=True`: 网络处于训练阶段,The layer will normalize its inputs 
                        #    using the mean and variance of the current batch of inputs.
                        #  `training=False`: 网络处于测试阶段或inference阶段,The layer will normalize its inputs using 
                        #    the mean and variance of its moving statistics, learned during training.
                        # 即,training=True:使用当前批次的均值与方差进行标准化;training=False,使用滑动均值,滑动方差进行标准化。
                       
          ):
   
    training = self._get_training_value(training)

    ###
    ###只介绍参数,具体执行代码省略
    ###

关于trainable的设置,以下是keras的说明:

"""
class BatchNormalization(normalization.BatchNormalizationBase):

  __doc__ = normalization.replace_in_base_docstring([
      ('{{TRAINABLE_ATTRIBUTE_NOTE}}',
       '''
  **About setting `layer.trainable = False` on a `BatchNormalization layer:**
关于 BatchNormalization 层中 layer.trainable = False 的设置:

  The meaning of setting `layer.trainable = False` is to freeze the layer,
  i.e. its internal state will not change during training:
  its trainable weights will not be updated
  during `fit()` or `train_on_batch()`, and its state updates will not be run.
对于一个一般的层,设置layer.trainable = False表示冻结这一层的参数,使这一层的内部状态不随着训练过程改变,即这一层的可训练参数不被更新,也即,在`fit()` or `train_on_batch()`过程中,这一层的状态不会被更新。

  Usually, this does not necessarily mean that the layer is run in inference
  mode (which is normally controlled by the `training` argument that can
  be passed when calling a layer). "Frozen state" and "inference mode"
  are two separate concepts.
通常,设置layer.trainable = False并不一定意味着这一层处于inference状态(测试状态),(模型是否处于inference状态,通常调用该层的call函数时用一个叫training的参数控制。)所以,“冻结状态”和“推断模式”是两种不同的概念。

  However, in the case of the `BatchNormalization` layer, **setting
  `trainable = False` on the layer means that the layer will be
  subsequently run in inference mode** (meaning that it will use
  the moving mean and the moving variance to normalize the current batch,
  rather than using the mean and variance of the current batch).
但是,在BatchNormalization中,设置trainable = False 意味着这一层会以“推断模式”运行。
这就意味着,如果在训练过程中设置批标准化层的trainable = False,就意味着批标准化过程中会使用滑动均值与滑动方差来执行当前批次数据的批标准化,而不是使用当前批次的均值与方差。
----》个人理解:对于批标准化,我们希望的是,在训练过程中使用每个minibatch自己的均值与方差执行标准化,同时保持一个滑动均值与滑动方差在测试过程中使用。如果在训练过程中,设置trainable = False的话,会导致,在训练过程中,批标准化层就会使用滑动均值与方差进行批标准化。


  This behavior has been introduced in TensorFlow 2.0, in order
  to enable `layer.trainable = False` to produce the most commonly
  expected behavior in the convnet fine-tuning use case.
这一操作已经被引入到TensorFlow 2.0中,目的是使`layer.trainable = False`产生最期待的行为:以便在网络fine-tune中使用。
---》个人理解:在网络fine-tune中,我们希望冻结一些层的参数,仅仅训练个别层的参数。对于批标准化层来说,我们希望这一层在训练过程中仍旧使用已经训练好的滑动均值和滑动方差,而不是当前批次的均值和方差。

  Note that:
    - This behavior only occurs as of TensorFlow 2.0. In 1.*,
      setting `layer.trainable = False` would freeze the layer but would
      not switch it to inference mode.
注意:这一行为仅仅发生在TensorFlow 2.0上。在1.*版本上,设置标准化层的`layer.trainable = False`,仍旧只会冻结标准化层的gamma和beta,仍旧使用当前批次的均值和方差标准化。
--》个人理解:在1.*版本上,设置标准化层的`layer.trainable = False`,得到的操作是:
    1)标准化层的gamma和beta不被训练
    2)执行标准化时,使用的是当前批次的均值和方差,而不是滑动均值和滑动方差。
    3)滑动均值和滑动方差仍旧会被计算吗?这有待确定。
    - Setting `trainable` on an model containing other layers will
      recursively set the `trainable` value of all inner layers.
当给一整个model设置trainable参数时,相当于给其内部的每个层都设置了这一相同的参数。
    - If the value of the `trainable`
      attribute is changed after calling `compile()` on a model,
      the new value doesn't take effect for this model
      until `compile()` is called again.
如果,model在调用“compile()”时改变了trainable参数,新的trainable参数值并不影响这个model,直到再次调用“compile()”函数。
      ''')])
"""

函数调用

综上,在调用tf.keras.layers.BatchNormalization 时,我们几乎不需要设定任何参数,只需要输入数据就好。

但是

1. tf.keras.layers.BatchNormalization有一个bug:无论“trainable=True"还是“trainable=False",tf.keras.layers.BatchNormalization都不会把批标准化中的变量放到 tf.GraphKeys.UPDATE_OPS, bn_update_ops中去,所以需要手动添加。

示例:

import tensorflow as tf

input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization(trainable=None)(input,training=True)
#output = tf.keras.layers.BatchNormalization(trainable=True)(input,training=True)
#output = tf.keras.layers.BatchNormalization(trainable=False)(input,training=True)

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
print(update_ops)
"""
以上三种情况都是返回 []
"""

根据打印结果可见,keras没有把批标准化中的变量添加到tf.GraphKeys.UPDATE_OPS中,所以不能直接调用tf.GraphKeys.UPDATE_OPS 去进行更新节点的提取。

因此,需要 手动把BN的更新节点到tf.GraphKeys.UPDATE_OPS,的方法如下:

import tensorflow as tf

input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization()(input,training=True)

# 手动添加方法
ops = tf.get_default_graph().get_operations()
bn_update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")]
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS,bn_update_ops)

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
print(update_ops)

2. 批标准化的一个比较重要的参数是"training",在一些其他批标准化函数中需要手工设定,在keras里既可以通过手工设定,也可以通过另外一种方式设定:用tf.keras.backend.set_learning_phase()来设定。

from tensorflow.keras import backend as K
# 设置keras的训练状态,模拟训练或测试状态
K.set_learning_phase(1) # 1 代表训练状态, 0 代表测试状态
is_training = K.learning_phase()
print(is_training)
"""
打印: 1
"""

用tf.keras.backend.set_learning_phase()设定训练状态(一个全局变量)后,tf.keras.layers.BatchNormalization可以识别这一状态,然后对training=None进行自动处理:令training=True或False.这个操作在tf.keras.layers.BatchNormalization的源码中有所体现,这里不再赘述。

示例:

import tensorflow as tf
from keras import backend as K

# 设置keras的训练状态,模拟训练或测试状态
K.set_learning_phase(0) # 1 代表训练状态, 0 代表测试状态
is_training = K.learning_phase()
print ("is_training =",K.learning_phase())

input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization()(input,training=None)

# 手动添加方法
ops = tf.get_default_graph().get_operations()
bn_update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")]
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS,bn_update_ops)


update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
print(”update_ops=“,update_ops)
"""
当设置 K.set_learning_phase(0) 时,打印:
is_training = 0
update_ops= [[]]


当设置 K.set_learning_phase(1) 时,打印:
is_training = 1
update_ops= [[
, 

]]

"""

设置training=None时可能存在的问题 :tf.keras.backend.learning_phase()的特点

tf.keras.backend.learning_phase()的设定要出现在所有节点(尤其批标准化层)的定义之前

tf.keras.backend.learning_phase()会获取一个全局变量,是一个tensorflow的bool型tensor。在使用前需要预先设定。 如果不预先设定,在执行全部变量初始化时会被初始化为False.如下:

import tensorflow as tf
from tensorflow.keras import backend as K

is_training = K.learning_phase()
print(is_training)

with tf.Session() as sess:
	tf.global_variables_initializer().run()
	print(sess.run(is_training))
"""
打印:
Tensor("keras_learning_phase:0", shape=(), dtype=bool)
False
"""

多说一句,如果不用 tensorflow.keras.backend.set_learning_phase()预先设定神经网络的训练或测试状态,keras或tensorflow是不会通过你的其他行为代码任务判断你是在训练还是在测试的,它还没那么智能,而且也很难有一个很明显的行为特征来指示这一点。因此,如果在用tf.keras.layers.BatchNormalization批标准化时打算用training=None的参数设置,一定要预先用 tf.keras.backend.set_learning_phase()设定好程序的运行状态。

再多说一句,tf.keras.backend.set_learning_phase()一定要出现在所有计算节点之前才有效,否则,如果在set_learning_phase()之前定义了BatchNormalization()(training=None)层,此时keras不知道程序的训练或测试状态,会默认生成滑动均值的更新计算节点,即使后面设置了tf.keras.backend.set_learning_phase(0)也不会改变。

示例代码如下:

示例1:set_learning_phase()出现在BatchNormalization()(training=None)之前

import tensorflow as tf
from tensorflow.keras import backend as K

# 设置keras的训练状态,模拟训练或测试状态
K.set_learning_phase(0) # 1 代表训练状态, 0 代表测试状态
is_training = K.learning_phase()
print ("is_training =",is_training)

# 定义计算节点及BN层
input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization()(input,training=None)

# 检测update_ops的生成手动添加方法
ops = tf.get_default_graph().get_operations()
bn_update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")]

print("bn_update_ops=",bn_update_ops)
"""
实验结果:
K.set_learning_phase(0)时打印:
is_training = 0
bn_update_ops= []

K.set_learning_phase(1)时打印:
is_training = 1
bn_update_ops= [
, 
]

结果符合预期
"""

示例2:set_learning_phase()出现在BatchNormalization()(training=None)之后

import tensorflow as tf
from tensorflow.keras import backend as K

# 定义计算节点及BN层
input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization()(input,training=None)

# 设置keras的训练状态,模拟训练或测试状态
K.set_learning_phase(0) # 1 代表训练状态, 0 代表测试状态
is_training = K.learning_phase()
print ("is_training =",is_training)

# 检测update_ops的生成手动添加方法
ops = tf.get_default_graph().get_operations()
bn_update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")]

print("bn_update_ops=",bn_update_ops)

"""
实验结果:
K.set_learning_phase(0)时打印:
is_training = 0
bn_update_ops= [
, 
]

K.set_learning_phase(1)时打印:
is_training = 1
bn_update_ops= [
, 
]

结果不符合预期, keras的训练与测试的状态设定对批标准化无影响
"""

还有一种情况就是,即设置了training=True又设置了K.set_learning_phase(0),或者反过来,这种自相矛盾的事希望大家还是不要发生。

综上,training=None/True/False的设定问题,看大家的习惯,不要造成紊乱。个人觉得手工设定training=True/False好一些。这个习惯与其他的批标准化函数也统一一些。毕竟K.set_learning_phase(0)是keras独有的

批标准化函数产生的变量是可训练的吗?

批标准化的流程如下:

批标准化 tf.keras.layers.BatchNormalization 参数解析与应用分析_第1张图片

由流程图可见:

1)在训练时(training=True),

           (a)  使用的是当前批次数据的均值与方差做数据标准化,

            (b)  同时维持了一个均值的滑动平均值与方差的滑动平均值。这两个数值在训练期间不起作用。

             (c)  训练了两个“可训练”的变量:放缩变量gamma, 平移变量beta.

             备注:训练时的更新节点,是对“均值的滑动平均值”与“方差的滑动平均值”进行更新。与当前批次的均值与方差的计算,beta与gamma的更新没有任何关系。

2) 在测试时(training=False):

        (a)用训练过程中的“均值的滑动平均值”与“方差的滑动平均值”对测试数据进行标准化。

          (b)  由于测试时,不再执行“参数更新节点”,要注意也不要再执行“均值与方差的滑动平均节点”,所以此时滑动均值,滑动方差,放缩变量,平移变量均不会再进行变化。

如果:在训练时,设置了training=False,会导致什么结果?

1) 设置 training =False,会导致 “均值与方差的滑动均值节点”不会被创建,从而均值与方差的滑动平均值永远保持初始值:均值的滑动均值等于0,方差的滑动均值等于1.

2)设置training=False,会导致批标准化算法不会使用当前批次的均值与方差对数据进行标准化,而是使用均值与方差的滑动平均值进行标准化。

3)如1)和2)所描述的,当在训练过程中设置training=False时,批标准化算法会以均值与方差的滑动平均值进行数据标准化,而这两个数值又因为training=False时不存在滑动平均的更新节点而不会被更新。所以,最终导致,如果在训练过程中设置training=False,批标准化算法不会执行数据标准化操作。

4)但是,无论training=True或False,批标准化层都会产生两个可训练的变量:平移变量beta和放缩变量gamma.这两个变量会在可训练参数的更新过程中被实时更新。

5)综上,如果在训练过程中设置了training =False,最终会导致,批标准化层消除了批标准化操作,但是保留了数据的放缩与平移。

总结:批标准化算法会带来4个变量:moving-mean,moving_variance, gamma(放缩变量),beta(平移变量)。在训练过程中设置training = False, 会导致moving_mean,moving_variance永远维持初始值,即 moving_mean=0,moving_variance=1。 而gamma与beta是可训练变量,这两个变量仍旧会随着模型的训练过程被优化/改变。所以,在训练过程中设置training = False,会导致数据不会被执行标准化,但是仍旧会执行放缩与平移。所以说,也不能说这种情况下算法完全没有使用BN。我觉得,在实践中,这种方式也可以作为一种尝试。这种操作可能对内部协变量漂移的问题没有解决作用,但是毕竟增加了两个变量,可能仍旧会对改善模型有帮助。 所以:在训练与测试的过程中都设置training=False,也可能有利于模型的改善。

 

无论training=True 或者False, tf.keras.layers.BatchNormalization都会产生四个变量:

  • moving_mean : 参数的滑动均值,以上流程中的\mu _{B}, 为不可训练变量
  • moving_variance: 参数的滑动方差,以上流程中的 \sigma _{B}^{2}, 为不可训练变量
  • gamma : 放缩变量,初始化为1 ,是可训练变量
  • beta : 平移变量,初始化为0 ,是可训练变量

其中 gamma与beta是可训练的, moving_mean 与moving_variance是不可训练的,示例如下:

import tensorflow as tf
from keras import backend as K

# 设置keras的训练状态,模拟训练或测试状态
K.set_learning_phase(0) # 1 代表训练状态, 0 代表测试状态
is_training = K.learning_phase()
print ("is_training =",K.learning_phase())

input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization()(input,training=None)

# 手动添加方法
ops = tf.get_default_graph().get_operations()
bn_update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")]
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS,bn_update_ops)

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
gl_var_list = tf.global_variables()
tr_var_list = tf.trainable_variables()
print("update_ops=",update_ops)
print("gl_var_list=",gl_var_list)
print("tr_var_list=",tr_var_list)
"""
# 在测试状态下
is_training = 0
update_ops= [[]]
gl_var_list= [
    , 
    ,
     , 
    ]
tr_var_list= [
    , 
    ]


# 在训练状态下
is_training = 1
update_ops= [[
    ,
     
]]
gl_var_list= [
    , 
    ,
     , 
    ]
tr_var_list= [
    ,
     
]

"""

在使用批标准化时,要保存所有变量,而不仅仅是可训练变量

因为在测试过程中,要用到

  • moving_mean : 不可训练变量
  • moving_variance: 不可训练变量

这两个不可训练的变量,所以在保存模型时要保存tf.global_variables() 而不是 tf.trainable_variables()

否则,可能会存在模型加载错误,或者,更严重的,可能会导致moving_mean与moving_variance又被初始化为0和1被使用,这与我们训练时得到的参数不同,会导致测试结果远差于训练效果。

具体会发生那种情况,我还没测试过,但很明显,我们不能让这种情况发生。

 

应用分析

由以上分析,不论 training=True 或 training=False, tf.keras.layers.BatchNormalization都会产生四个变量:

  • gamma
  • beta
  • moving_mean
  • moving_variance
[, 
, 
, 
]

不同的是, training=False没有产生计算滑动平均的两个计算节点: 

[, 
]

这两个节点一个用于计算mean的滑动平均,一个用于计算var的滑动平均。

滑动平均的计算公式:

假设m_1,m_2,...,m_n是 n 个mini-batch数据的均值,n 代表迭代次数,则第 t 次迭代时的滑动均值为

{m^{'}_t} = \beta* {m^{'}_{t-1}} + (1-\beta)m_t,

对应的,假设var_1,var_2,...,var_n是 n 个mini-batch数据的var,var_n 代表第n次迭代时输入的mini-batch数据的var,则第 t 次迭代时var的滑动均值var^{'}_{t}为:

var^{'}_{t} = \beta*var^{'}_{t-1} + (1-\beta)*var_{t}

滑动平均计算节点在批标准化中的作用

训练过程中,通常用mean与var的滑动平均值去标准化当前minibatch的数据,当training=False或未执行滑动平均的更新操作时,批标准化操作是以 mean 与var的滑动平均值的初始化值进行标准化,在整个过程中,mean的滑动均值与var的滑动均值不会被改变。

  • 在训练过程中, mean 与var的滑动平均值的分别被初始化为0和1,由参数设置决定,如果training=False则永不更新。根据批标准化公式,相当于未执行批标准化。
  • 在测试过程中,mean 与var的滑动平均值的分别被初始化为模型保存的对应变量值,如果training=False则永不更新。
  • 如果在训练过程中,training被设置为False, mean 与var的滑动平均值的分别被初始化为0和1,且永不更新,最终保存到模型里的值也是0和1,在测试时仍会以0和1被加载。

所以,正确设置training的值很重要。或者,更本质一些,在恰当的时候执行 mean与var的滑动平均值的更新,对批标准化操作很重要。

滑动平均的计算时机与注意事项:添加依赖控制

在每一次minibatch数据输入之前,即,在每一次迭代训练之前更新mean与var的滑动平均值,要用tf.control_dependencies添加依赖控制,具体如下:


import tensorflow as tf
 
input = tf.ones([1, 2, 2, 3])
output = tf.keras.layers.BatchNormalization()(input,training=None)

# 手动添加滑动平均更新节点到tf.GraphKeys.UPDATE_OPS,bn_update_ops中
ops = tf.get_default_graph().get_operations()
bn_update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")]
tf.add_to_collection(tf.GraphKeys.UPDATE_OPS,bn_update_ops)

# 在训练中添加对滑动平均更新节点的依赖控制
update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
	train_op = optimizer.minimize(loss)
 
# 执行训练
with tf.Session() as sess:
 	sess.run(tf.global_variables_initializer())
        # 保存模型时要保存所有变量,因为批标准化操作产生的变量是不可训练的。
 	saver = tf.train.Saver(var_list=tf.global_variables())
 	saver.save(sess, "batch_norm_layer/Model")

什么时候进行update_ops?

如上面介绍,批标准化操作会带来两个更新节点:一个用于计算moving_mean的滑动平滑计算节点,一个用于计算moving_variance的滑动平滑计算节点。这两个节点在优化过程中不会被主动调用。如何执行这两个节点呢?以及什么时候运行这两个节点呢?上面提到了用控制依赖法,用with tf.control_dependencies(update_ops):  train_op = optimizer.minimize(loss),使update_ops节点运行与变量优化同步运行,或者严格意义上是先于参数更新运行。

个人理解:在BN的流程图中,什么时候执行滑动平均计算操作?按照定义是在一次迭代完成之后。

在训练过程中,滑动平均并不参与任何模型参数与批标准化过程,滑动平均的计算仅仅是为了在测试阶段使用。所以严格来说,滑动平均值,只需要在当次小批量的BN的均值与方差计算完之后就可以计算。或者在下一次迭代开始之前就可以。在以上的例子中是与参数优化一起执行的,这肯定是在计算完梯度之后了,即前向计算完成之后了。所以肯定可以。

我自己有一个想法:是不是可以在sess.run()完批量数据的优化之后用sess.run()去运行滑动平均节点?事实证明,这样也能改变这四个变量:moving_mean.moving_variance,gamma,beta 也会改变,但是好像效果不如用控制依赖好,不知道为什么?

批标准化 tf.keras.layers.BatchNormalization 参数解析与应用分析_第2张图片

左图是用sess.run()更新,右图是用控制依赖更新。右图看着下降更平滑一些,而且下降的也更快一些。不知道为什么?

还有个实验,在训练时设置training=False好像也收敛的更快。个人猜测是:我用的relu激活函数,在批标准化时仅仅使用平移与放缩也能达到批标准化的效果,而不用非得执行标准化。

 

如果没有添加依赖控制会怎样? 

我在一次实验中用了批标准化层,但是没有在训练时添加依赖控制,即没有执行mean与var的滑动平均更新,训练得到的模型中,BN层的变量数值如下:

 1.0191431
 0.01283348
 0.0
 1.0


 1.0
 0.0
 0.0
 1.0


 1.0107784
 -0.0097986935
 0.0
 1.0


 1.074267
 0.005866247
 0.0
 1.0


 1.065175
 -0.017884836
 0.0
 1.0

可见,与上面分析的一致,mean与var的滑动平均值保持了其初始化数值,没有进行更新,而beta与gamma两个变量的值有所改变,因为这两个变量是可训练的变量,所以在执行trainable_variable优化时会被更新。这可能危害不是特别大,根据批标准化的流程公式,这相当于没有执行BN,或只进行了放缩与平移。安慰下自己,至少测试时用的BN参数与训练时相同。没有带来特别负面的影响。

模型中变量值的查看代码如下:

import tensorflow as tf

model_path = "/media/***/model.ckpt-18000"

tf.reset_default_graph() # 清空图,防止图上存在干扰节点
with tf.Session(graph=tf.get_default_graph()) as sess:
	saver = tf.train.import_meta_graph(model_path+".meta")# 从model.meta文件中加载保存的图结构
	saver.restore(sess, model_path)# 从model中加载保存的变量数据
	
	var_list = tf.global_variables() # 获取图上的所有变量
	bn_var_list = [v for v in var_list if "batch_normalization" in v.name ] # 筛选与BatchNormalization相关的变量
	for v in bn_var_list:
		print(v,sess.run(v)[1]) # 打印变量名与对应数值,为了便于观察,打印变量张量的第一个数值

 

批标准化过程

算法1:训练过程的批标准化:用当前minibatch的均值与方差对当前minibatch的数据进行标准化,然后进行平移与缩放。

                                                 有两个需要学习的参数:缩放因子\gamma和平移因子\beta。这两个变量应该是随着训练进行更新的。

批标准化 tf.keras.layers.BatchNormalization 参数解析与应用分析_第3张图片

算法2:训练与测试过程的批标准化

其中K表示K个标准化层。

步骤6表示:具有批标准化算法的神经网络的可训练变量,除了已有的可训练变量外,每个批标准化层都会带来额外的两个可训练变量\gamma\beta 所以用了一个并集的方式表示网络的所有可训练参数。

步骤10中: E(x)Var(x)可理解为训练过程中训练样本的moving_mean和moving_var.

步骤11表示:用训练集的moving_mean和moving_var对测试集样本进行批标准化,然后用训练集训练得到的放缩变量gamma和平移变量beta进行放缩和平移变换。

批标准化 tf.keras.layers.BatchNormalization 参数解析与应用分析_第4张图片

其他几个注意点:

1. batch-size要尽可能大,这样训练过程中计算得到的均值与方差才会尽可能稳定。

2. 由于训练时批标准化使用的均值与方差是当前batch的信息,测试时使用的是所有训练batch的滑动平均。所以二者会存在差异。可能会导致测试时的结果与训练时的结果不同。为了解决这一个问题,我们可以使momentum参数的值尽量小一些,这样计算得到的滑动平均对历史的记忆比较少,最后得到的滑动平均也与训练时用于批标准化的信息更接近。momentum=0时就使滑动平均完全保留当前的信息,完全抛弃历史信息了。

self.moving_mean = momentum * self.moving_mean + (1-momentum) * mean

参考:TensorFlow使用之tf.layers.batch_normalization函数详解

对tensorflow 的BatchNormalization的坑的理解与测试

对 BatchNormalization 中 Internal Convariate Shift 的理解

 

你可能感兴趣的