目前主流的训练深度神经网络的算法是梯度下降算法,简而言之该过程就是通过将网络输出值与真实值之间的误差信号逐层传递至神经网络的每个节点,进而更新节点与节点之间的参数。但深度神经网络的调参过程一直是个问题。
随着梯度下降算法的不断改进,已经有越来越多的算法尝试减少调参的工作量,比如减小学习率、选取合适的权重初始化函数、应用Dropout等等,而Batch Normalization也是一个加速神经网络训练过程的算法,帮助减少调参的弯路。这个算法在2015年由Google提出,一提出便被广泛接纳采用,可以发现,现如今基本上所有的深度神经网络模型中都会加入Batch Normalization技巧。
参考论文:Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
Batch_normalization即在每次SGD时,通过mini-batch来对相应的activation做规范化操作,使得结果(输出信号各个维度)的均值为0,方差为1。而最后的“scale and shift”操作则是为了让因训练所需而“刻意”加入的BN能够有可能还原最初的输入(即当),从而保证整个network的capacity。(有关capacity的解释:实际上BN可以看作是在原模型上加入的“新操作”,这个新操作很大可能会改变某层原来的输入。当然也可能不改变,不改变的时候就是“还原原来输入”。如此一来,既可以改变同时也可以保持原输入,那么模型的容纳能力(capacity)就提升了。(来源知乎:魏秀参)对此过程感兴趣的可以去看链接论文中的推导 和 深度学习中 Batch Normalization为什么效果好?
批量规范化指通过减少内部协变量转换来加速深度网络训练。在论文中,Batch normalization解决的是一个Internal covariate shift问题,论文中将这个问题定义为在训练过程中由于网络参数的改变而引起的网络激活分布的改变。
covariate shift导致了神经网络训练起来比较缓慢,而ReLU的发明也是为了减少covariate shift,较少covariate shift就可以提高神经网络的训练速度。
internal covariate shift:对于深度神经网络而言(比如微软提出152层的residual network),由于其参数是逐层启发性的结构,由于第一层参数的改变,导致了传递给第二层的输入的分布也会发生改变,也就是说在更新参数的过程中,无形中发生了internal covariate shift。因此,这样网络就很难较好的范化,训练起来也就非常缓慢了。
tensorflow中batch normalization的实现主要有下面三个:
tf.nn.batch_normalization
tf.layers.batch_normalization
tf.contrib.layers.batch_norm
封装程度逐个递进,建议使用tf.layers.batch_normalization或tf.contrib.layers.batch_norm,因为在tensorflow官网的解释比较详细。这里我们主要使用tf.layers.batch_normalization--批量规范化层的功能接口.
函数详解:https://www.w3cschool.cn/tensorflow_python/tensorflow_python-13p92sws.html
tf.layers.batch_normalization(
inputs,
axis=-1,
momentum=0.99,
epsilon=0.001,
center=True,
scale=True,
beta_initializer=tf.zeros_initializer(),
gamma_initializer=tf.ones_initializer(),
moving_mean_initializer=tf.zeros_initializer(),
moving_variance_initializer=tf.ones_initializer(),
beta_regularizer=None,
gamma_regularizer=None,
beta_constraint=None,
gamma_constraint=None,
training=False,
trainable=True,
name=None,
reuse=None,
renorm=False,
renorm_clipping=None,
renorm_momentum=0.99,
fused=None,
virtual_batch_size=None,
adjustment=None
)
BN在training和inference时使用的方法是不一样的。
μ和σ,在训练的时候,使用的是batch内的统计值,测试/预测的时候,采用的是训练时计算出的滑动平均值
1、training:
我们需要逐个神经元逐个样本地来计算,这个batch在某一层输出的均值和标准差,然后再对该层的输出进行标准化。同时还要学习gamma和beta两个参数。
这是非常非常耗时的,显然,我们不能在inference的时候使用这种方法。
解决方案就是,在训练时使用滑动平均维护population均值和方差:
running_mean = momentum * running_mean + (1 - momentum) * sample_mean
running_std = momentum * running_std + (1 - momentum) * sample_std
在训练结束保存模型时,running_mean、running_var、trained_gamma和trained_beta一同被保存下来。
2、inference:
output = (input - running_mean) / running_std
output = trained_gamma * output + trained_beta
也就是说,在inference时,BN对应的操作不再是公式里提到的那样,计算该batch的各种统计量,而是直接使用在训练时保存下来的population均值和方差,进行一次线性变换。这样效率提升了很多。但是缺点也显而易见,如果训练集和验证集不平衡的时候,验证的效果会一直一直很差
3、实例:
1) 训练的时候需要注意两点,(1)输入参数training=True,(2)计算loss时,要添加以下代码(即添加update_ops到最后的train_op中)。这样才能计算μ和σ的滑动平均(测试时会用到)
update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
train_op = optimizer.minimize(loss)
2) 保存模型的时候不能只保存trainable_variables,因为BN的参数不属于trainable_variables。为了方便,可以用tf.global_variables()。使用姿势如下
saver = tf.train.Saver(var_list=tf.global_variables())
savepath = saver.save(sess, 'here_is_your_personal_model_path’)
3) 读取模型与保存类似,读的时候变量也需要为global_variables。如下:
saver = tf.train.Saver()
or saver = tf.train.Saver(tf.global_variables())
saver.restore(sess, 'here_is_your_personal_model_path')
4) 测试的时候还需要把tf.layers.batch_normalization(x, training=is_training,name=scope) 这里的training设为False