【深度学习】TensorFlow学习之路三

  • 一、为什么会有梯度消失和爆炸
  • 二、参数初始化
  • 三、选择激活函数
  • 四、Batch标准化
  • 五、梯度修剪



本系列文章主要是对OReilly的Hands-On Machine Learning with Scikit-learn and TensorFlow一书深度学习部分的阅读摘录和笔记。

一、为什么会有梯度消失和爆炸

如我们上一章提到的,深度神经网络优化方法为求出损失函数对每一个参数的梯度,然后让每个参数沿着梯度一步步下降。由于我们后向传播求解梯度的时候,遵循的是求导的链式法则,即一层层的导数相乘。如果我们初始化时的权重是小于1的值,到我们求解到比较下层的层次时,小于1的值被不断相乘,就会导致梯度变得非常小,从而让比较下层的参数在梯度下降过程中不怎么变化,这就是梯度消失问题;反之,如果一开始权重是大于1的值,会导致比较下层的参数梯度变的很大,从而让其不能拟合到最优解,这就是梯度爆炸问题。
梯度消失爆炸本质上是一样的,都是因为层数太深而引发的梯度反向传播中的连乘效应。

二、参数初始化

Xavier初始化(使用sigmoid激活函数时)可以解决梯度消失和爆炸问题并极大提高训练的速度。Xavier初始化要求w的初始值从以下分布中随机选取:

均值为0,标准差为tensorflow 内存泄漏 排查 tensorflow内存爆炸_神经网络的正态分布或

-r到r之间的均匀分布,tensorflow 内存泄漏 排查 tensorflow内存爆炸_tensorflow 内存泄漏 排查_02

当然对于其它激活函数,也有相应的初始化策略如下:

tensorflow 内存泄漏 排查 tensorflow内存爆炸_深度学习_03


Relu激活函数下的初始化策略被称为He初始化。

TensorFlow中的dense函数默认使用的是Xavier初始化,如果想要更改为He初始化:

he_init = tf.variance_scaling_initializer()
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.relu,
                          kernel_initializer=he_init, name="hidden1")

三、选择激活函数

相比于sigmoid函数,Relu函数不会有梯度平原的问题,而且计算更快;但Relu也有自己的问题:dying Relus,即当输入神经元的值小于0时,该神经元开始输出0,此时Relu的梯度也为0,所以从此开始该神经元就一直输出0,像死了一样。

为了解决这个问题,可以使用Relu的变种leaky-Relu:

tensorflow 内存泄漏 排查 tensorflow内存爆炸_tensorflow 内存泄漏 排查_04

tensorflow 内存泄漏 排查 tensorflow内存爆炸_深度学习_05通常取0.01,当然也可以取大一点如0.2,效果相对更好一些;当然,tensorflow 内存泄漏 排查 tensorflow内存爆炸_深度学习_05的值也可以在训练过程中随机取(RRelu),或者作为一个参数在训练过程中去学习(PRelu)。

还有另一种选择ELU:

tensorflow 内存泄漏 排查 tensorflow内存爆炸_tensorflow_07

tensorflow 内存泄漏 排查 tensorflow内存爆炸_深度学习_08


ELU有几个优势:

  1. 它处理了z<0部分,让神经元的输出平均值在0附近;
  2. z<0的部分梯度不为0,防止了dying unit的问题;
  3. 这个函数处处平滑可导,使得它在0附近不会有太大波动,梯度下降更快。

当然ELU也有不足之处,计算涉及到指数运算,有点慢。
经验上整体效果来讲: ELU > leaky ReLU > ReLU > tanh > sigmoid,如果想要更短的训练时间,建议选用leaky-Relu。
TensorFlow中可以通过dense函数中的activation参数更改激活函数:

def leaky_relu(z, name=None):
    return tf.maximum(0.01 * z, z, name=name)

hidden1 = tf.layers.dense(X, n_hidden1, activation=leaky_relu, name="hidden1")
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.elu, name="hidden1")

四、Batch标准化

虽然选择He正则化和ELU激活函数可以在训练初期极大改善梯度消失和梯度爆炸问题,但不能保证训练后期这个问题再次出现。因为在训练过程中,每个中间层的输入项的分布都会随着上一层参数的改变而改变。为了解决这样一个问题,我们可以在激活函数作用之前,先对每层中间层的输入项进行一个尺度和均值的校正,由于这种校正都是在每个batch样本上进行的,所以这种方法被称为Batch标准化。具体公式如下:

tensorflow 内存泄漏 排查 tensorflow内存爆炸_深度学习_09


其中均值tensorflow 内存泄漏 排查 tensorflow内存爆炸_神经网络_10,标准差tensorflow 内存泄漏 排查 tensorflow内存爆炸_深度学习_11,尺度项tensorflow 内存泄漏 排查 tensorflow内存爆炸_机器学习_12和截距项tensorflow 内存泄漏 排查 tensorflow内存爆炸_神经网络_13都是需要在每次mini-batch中学习的参数。这种方法可以明显提高神经网络的效果,很大程度上改善梯度消失和爆炸问题,即便使用sigmoid激活函数。而且这种方法也会使得神经网络对初始参数不敏感,并可以通过增大学习率来提高训练速度。另外,Batch标准化方法同样能起到正则化的作用。

但这种方法也有弊端,因为它增加了神经网络的复杂度,因为每一层都需要做标准化,所以用这样的神经网络来预测时也会明显计算偏慢。

TensorFlow里面的tf.layers.batch_normalization可以帮我们实现batch标准化:

from functools import partial
import tensorflow as tf

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

X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")

training = tf.placeholder_with_default(False, shape=(), name='training')

#利用python里面的partial函数把batch_normalization的参数给固定下来,方便每次使用
my_batch_norm_layer = partial(tf.layers.batch_normalization,
                              training=training, momentum=0.9)

hidden1 = tf.layers.dense(X, n_hidden1, name="hidden1")
bn1 = my_batch_norm_layer(hidden1)
bn1_act = tf.nn.elu(bn1)
hidden2 = tf.layers.dense(bn1_act, n_hidden2, name="hidden2")
bn2 = my_batch_norm_layer(hidden2)
bn2_act = tf.nn.elu(bn2)
logits_before_bn = tf.layers.dense(bn2_act, n_outputs, name="outputs")
logits = my_batch_norm_layer(logits_before_bn)

这里的tf.layers.batch_normalization函数有两个参数:

  1. training参数:布鲁尔值,默认为False,表示使用batch样本中的均值和方差来做batch标准化(训练时使用),True表示用计算得到的滑动平均值来做batch标准化(预测时使用);
  2. momentum:计算滑动平均时的一个动量参数,一般取接近于1的一个值,如0.99,当给出一个新的值V时,滑动平均值tensorflow 内存泄漏 排查 tensorflow内存爆炸_神经网络_14的计算公式为:
    tensorflow 内存泄漏 排查 tensorflow内存爆炸_神经网络_15

对于比较浅层的网络,batch标准化不会起到明显的作用,batch正则化会在比较深层次的网络中效果显著。

五、梯度修剪

另外一种解决梯度消失和爆炸问题的方法为梯度修剪,即在后向传播过程中不断对梯度进行修剪,让它不要超过某个阈值。这种方法通常对循环神经网络效果较好。

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 grad, var in grads_and_vars]
training_op = optimizer.apply_gradients(capped_gvs)