TensorFlow | TF修炼手册(六)——批标准化与梯度剪裁

批标准化

综合前文所述,为解决/缓解反向传播过程中出现的梯度爆炸或梯度消失问题,我们可以使用的方法主要有——

  1. 使用Xavier或He参数初始化策略;
  2. 使用ELU或ReLU及其变种激活函数。

但是人们发现这并不能完全解决这一问题,2015年Sergey Ioffe 和 Christian Szegedy提出了一种新的策略,称为Batch Normalization(批标准化)。这一技术在每一层的输入进入激活函数以前加入了一个标准化的操作,使其标准正态化。然后依据两个参数来放缩并平移结果。换句话说,这种操作使得模型中的每一层都可以学习理想分布的输入。

这一操作可以总结为以下几步——

《TensorFlow | TF修炼手册(六)——批标准化与梯度剪裁》
《TensorFlow | TF修炼手册(六)——批标准化与梯度剪裁》

需要解释的是,ε代表了一个极小的数字,通常是10的-3次方,目的是为了防止方差为0.

算法的提出者认为这一技术可以显著提高各种类型的深层神经网络的效果,梯度消失的问题被大大缓解了,甚至可以使用tanh或logistic作为激活函数了,神经网络对连接权重的初始化也不再十分敏感,更高的学习率可以被使用,这极大地加速了训练进程。此外,它还自带了正则化的效果。当然了,由于批标准化需要做的计算操作多了不少,因此训练时间将会多一些。所以如果你需要一个快捷反应的神经网络,不妨先试试ELU + 何式初始化。

在TensorFlow中使用批标准化

TensorFlow提供了一个batch_normalization()的功能,它可以简化标准正态化输入的过程,但是你必须自行计算上述最后一步的γ、ε和β,并将它们作为参数传给这个函数。

或者你也可以使用batch_norm()函数,它可以为你完成上述所有工作。

import tensorflow as tf
from tensorflow.contrib.layers import batch_norm
from tensorflow.contrib.layers import fully_connected

n_inputs = 28*28
n_hidden1 = 300
n_hidden2 = 100
n_outputs = 10

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
is_training = tf.placeholder(tf.bool, shape=(), name="is_training")
bn_params = {
        'is_training' : is_training,
        'decay' : 0.99,
        'updates_collections' : None
        }
hidden1 = fully_connected(X, n_hidden1, scope='hidden1', normalizer_fn=batch_norm, normalizer_params=bn_params)
hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2", normalizer_fn=batch_norm, normalizer_params=bn_params)
logits = fully_connected(hidden2, n_outputs, activation_fn=None, scope="outputs",normalizer_fn=batch_norm, normalizer_params=bn_params)

代码中定义了“is_training”这个布尔型的占位符用来表示该过程是否在训练中,这是由于在训练过程中需要使用当前批量的均值和标准差,而在test过程中则要使用移动平均。这种指数平均使用的是指数式衰减,这也就是为什么我们需要decay这个参数的原因了。移动平均的具体做法是(其中箭头左侧的v(hat)代表的是新的移动平均值,右侧的第一个v(hat)是原移动平均值,v是新的数值)——

《TensorFlow | TF修炼手册(六)——批标准化与梯度剪裁》
《TensorFlow | TF修炼手册(六)——批标准化与梯度剪裁》

updates_collections设置为None,这样移动平均值会在进行批标准化之前更新。否则默认情况下你需要手动进行该操作。

最后我们像之前一样使用fully_connected()构建了一层神经网络,只不过我们加入了normalizer_fn来确定批标准化的输入标准化方式。

需要注意的是默认情况下batchnorm()并不进行放缩(即γ被设置为1),这对于没有激活函数或激活函数为ReLU的层是没有问题的,但是对于其它类型的激活函数,就需要在bn_params中加入”scale”:”True”。

你可能已经发现了,在之前的代码中我们几乎重复了三次相同的神经网络层构建语句,为了防止这种重复,我们可以使用arg_scope()创建一个参数域(Argument Scope),第一个参数是一个函数的list,而其它的参数就会自动地被传入这些函数中。

with tf.contrib.framework.arg_scope([fully_connected], normalizer_fn=batch_norm, normalizer_params=bn_params):
    hidden1 = fully_connected(X, n_hidden1, scope="hidden1")
    hidden2 = fully_connected(hidden1, n_hidden2, scope="hidden2")
    logits = fully_connected(hidden2, n_outputs, scope="outputs", activation_fn=None)

流程图创建的剩余部分和之前是相同的,定义代价函数,创建一个Optimizer并令它最小化代价函数,定义评价的操作,然后创建Saver。

执行部分也大致相同,不同的是在feed_dict时需要根据情况设置is_training为True或False.

需要说明的是,在这个只有两个隐层的神经网络中,批量标准化不一定会有很好的效果,但是对于更深层的神经网络,它可以造成很大的影响。

梯度剪裁(Gradient Clipping)

剃度剪裁就是在反向传播过程中对梯度设置一个阈值,这一方法通常用于RNN。当前人们更喜欢上述批标准化的方法。

由于我们此前使用的minimize()函数会计算梯度以后直接应用,因此为了进行梯度剪裁我们需要使用compute_gradients(),然后对梯度进行剪裁,然后应用梯度为剪裁后的梯度。

threshold = 1.0
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
grads_and_vars = optimizer.compute_gradients(loss)
capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var) for gard, var in grads_and_vars]
training_op = optimizer.apply_gradients(capped_gvs)

阈值是一个可以调节的超参数。

总结——梯度问题的解决方法

  1. 使用Xavier或He参数初始化策略;
  2. 使用ELU或ReLU及其变种激活函数;
  3. 批标准化;
  4. 梯度裁剪。
    原文作者:高俊
    原文地址: https://zhuanlan.zhihu.com/p/35375703
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞