【深度学习笔记1.2】梯度消失与梯度爆炸

梯度下降

  梯度下降法(Gradient descent)是一种基于函数一阶性质的优化算法,其本质是在某个位置将目标函数一阶展开,利用其一阶性质持续向函数值下降最快的方向前进,以期找到函数的全局最小解。梯度下降属于梯度优化方法大类,此外还有最速下降法,共轭梯度法等等。还有其他方法基于目标函数的二阶性质,比如牛顿法、拟牛顿法等[1]。

注意:梯度下降法就是最速下降法,很多地方、很多人、包括维基百科、百度百科都这么说的,这导致大家都认为,梯度的反方向就是下降最快的方向,然后事实并非如此,其细微差别参见[2]。

反向传播

  反向传播(BackPropagation,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法[3]。其基本思想就是根据当前网络参数计算出当前输入样本的输出y,再根据y和实际结果的误差来计算各层神经元的梯度项,最后按梯度的反方向更新网络参数,直到达到停止条件。
  人工神经网络的参数多,梯度计算比较复杂。在人工神经网络模型提出几十年后才有研究者提出了反向传播算法来解决深层参数的训练问题。

梯度消失/爆炸

什么是梯度消失/爆炸

  反向传播算法的工作原理是从输出层向输入层传播误差的梯度。不幸的是,梯度下降更新使梯度往往越变得越来越小,以至于使得低层连接权重实际上保持不变,并且训练永远不会收敛到良好的解决方案,这被称为梯度消失问题。在某些情况下,可能会发生相反的情况:梯度可能变得越来越大,许多层得到了非常大的权重更新,算法发散。这是梯度爆炸的问题,在循环神经网络中最为常见。更一般地说,深度神经网络受梯度不稳定之苦,不同的层次可能以非常不同的速度学习。

产生梯度消失/爆炸的原因

  梯度消失/爆炸是造成深度神经网络大部分时间都被抛弃的原因之一。直到2010年才有所缓解,Xavier Glorot 和 Yoshua Bengio 发表的题为《Understanding the Difficulty of Training Deep Feedforward Neural Networks》的论文提出了一些疑问,sigmoid 激活函数和当时最受欢迎的权重随机初始化(即随机初始化时使用平均值为 0,标准差为 1 的正态分布)这个方案组合,使得每层输出的方差远大于其输入的方差(为什么?)。网络正向计算时,每层的方差持续增加,直到激活函数在顶层饱和[4],即在顶层激活函数的导数接近0,因此当反向传播开始时,它几乎没有梯度通过网络传播回来。而且由于反向传播通过顶层向下传递,所以有些较小的梯度也会不断地被稀释,因此较低层确实没有任何东西可用。

如何解决梯度消失/爆炸

改变权重初始化方法

  Glorot和Xavier在他们的论文中提出了一种能显著缓解这个问题的方法。我们需要信号在两个方向上正确地流动:即在进行预测时是正向的,在反向传播梯度时是反向的。 我们不希望信号消失,也不希望它爆炸并饱和。为了使信号能够正确流动,作者认为,我们需要每层输出的方差等于其输入的方差,也需要梯度在相反方向上流过一层之前和之后有相同的方差。然而实际上不可能保证两者都是一样的,除非这个层具有相同数量的输入和输出连接,但是他们提出了一个很好的折衷办法,在实践中证明这个折中办法非常好:随机初始化连接权重必须如下图1公式所描述的那样。其中n_inputs和n_outputs是权重正在被初始化的层(也称为扇入和扇出)的输入和输出连接的数量。 这种初始化策略通常被称为Xavier初始化,或者有时是 Glorot 初始化。

![enter image description here](https://lh3.googleusercontent.com/-Ct5ypbJsA40/W87lWrvh79I/AAAAAAAAAGc/nVMkkzv-ATcJV4H2ZByWRz0bIqPM_A7WQCLcBGAs/s0/Xavier%25E5%2588%259D%25E5%25A7%258B%25E5%258C%2596.jpg "Xavier初始化.jpg")
图1   Xavier 初始化

当输入连接的数量大致等于输出连接的数量时,可以得到更简单的等式:
e.g. σ = 1 n i n p u t s o r r = 3 n i n p u t s \quad \sigma = \dfrac{1}{\sqrt{n_{inputs}}} \quad or \quad r = \dfrac{\sqrt{3}}{\sqrt{n_{inputs}}} σ=ninputs 1orr=ninputs 3

  使用 Xavier 初始化策略可以大大加快训练速度,这是导致深度学习目前取得成功的技巧之一。 另外,最近的一些论文也针对不同的激活函数提供了类似的策略,如下图2所示。 ReLU 激活函数(及其变体,包括简称为 ELU 的激活)的初始化策略有时也称为 He 初始化。

![enter image description here](https://lh3.googleusercontent.com/-e5T_6_feghs/W87q48fykuI/AAAAAAAAAGs/PSRccyXZQFsYGO6iE4tJflzsNzbkfUFBwCLcBGAs/s0/WeightInit_about_activation_function.jpg "WeightInit_about_activation_function.jpg")
图2   不同激活函数的参数初始化方法

  默认情况下,fully_connected() 函数使用 Xavier 初始化(具有统一的分布)。 你也可以通过使用如下所示的variance_scaling_initializer() 函数来将其更改为 He 初始化[4]:

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

疑问,文献[4]中说 “He 初始化只考虑了扇入,而不是像 Xavier 初始化那样扇入和扇出之间的平均值。”,但是从图2中公式来看,He初始化(对应这ReLU激活函数)也是同时考虑了扇入和扇出的呀。这是为何?

使用非饱和激活函数

  Glorot 和 Bengio 在 2010 年的论文中的一个见解是,梯度消失/爆炸部分是由于激活函数的选择不好造成的。 在那之前,大多数人都认为,如果大自然选择在生物神经元中使用 sigmoid 激活函数,它们必定是一个很好的选择。 但事实证明,其他激活函数在深度神经网络中表现得更好,特别是 ReLU 激活函数,主要是因为它对正值不会饱和(也因为它的计算速度很快)[4]。

  不幸的是,ReLU激活函数并不完美。 它有一个被称为 “ReLU 死区” 的问题:在训练期间,如果神经元的权重得到更新,并且使得神经元输入的加权和为负时,则它将一直输出 0(ReLU函数的梯度为0),神经元不可能恢复生机。在某些情况下,你可能会发现你网络的一半神经元已经死亡,特别是如果你使用大学习率 [4]。

  为了解决这个问题,你可能需要使用 ReLU 函数的一个变体,比如 leaky ReLU。这个函数定义为 L e a k y R e L U α ( z ) = m a x ( α z , z ) LeakyReLU_α(z)= max(αz,z) LeakyReLUα(z)=max(αzz)(见图3)。超参数α定义了函数“leaks”的程度:它是z < 0时函数的斜率,通常设置为 0.01。这个小斜坡确保 leaky ReLU 永不死亡,他们可能会长期昏迷,但他们有机会最终醒来。事实上,设定α= 0.2(巨大 leak)似乎导致比α= 0.01(小 leak)更好的性能。他们还评估了随机化 leaky ReLU(RReLU),其中α在训练期间在给定范围内随机挑选,并在测试期间固定为平均值。它表现相当好,似乎是一个正则项(减少训练集的过拟合风险)。最后,他们还评估了参数 leaky ReLU(PReLU),其中α被授权在训练期间被学习(而不是超参数,它变成可以像任何其他参数一样被反向传播修改的参数)。据报道这在大型图像数据集上的表现强于 ReLU,但是对于较小的数据集,其具有过度拟合训练集的风险 [4]。

![enter image description here](https://lh3.googleusercontent.com/-PKheMt9KTEI/W8_I8Pc_fAI/AAAAAAAAAG8/zCnftIQWoL0oEp5IM5JANzdwvicRqrK2QCLcBGAs/s0/leakyReLU.png "leakyReLU.png")
图3   Leaky ReLU

  最后,Djork-Arné Clevert 等人在 2015 年的一篇论文中提出了一种称为指数线性单元(exponential linear unit,ELU)的新的激活函数(见图4),在他们的实验中表现优于所有的 ReLU 变体:训练时间减少,神经网络在测试集上表现的更好 [4]。

![enter image description here](https://lh3.googleusercontent.com/-6T6P6f9SwtM/W8_JBtYUjrI/AAAAAAAAAHE/OvG_B8zoM_s1Mitq-DpzgTQZgOaQTg7UgCLcBGAs/s0/elu.png "elu.png")
图4   ELU activation function

  ELU 激活函数的主要缺点是计算速度慢于 ReLU 及其变体(由于使用指数函数),但是在训练过程中,这可以换来更快的收敛速度。 然而,在测试期间,ELU 网络将比 ReLU 网络慢。

  那么你应该使用哪个激活函数来处理深层神经网络的隐藏层? 一般 ELU > leaky ReLU(及其变体) > ReLU > tanh > sigmoid,详见文献[4]。

TensorFlow 提供了一个可以用来建立神经网络的elu()函数。 调用fully_connected()函数时,只需设置activation_fn参数即可:

from tensorflow.contrib.layers import fully_connected
hidden1 = fully_connected(X, n_hidden1, activation_fn=tf.nn.elu)

TensorFlow 没有针对 leaky ReLU 的预定义函数,但是很容易定义:

import tensorflow as tf
from tensorflow.contrib.layers import fully_connected
def leaky_relu(z, name=None):
    return tf.maximum(0.01 * z, z, name=name)
    
hidden1 = fully_connected(X, n_hidden1, activation_fn=leaky_relu)
批量标准化

  尽管使用 He初始化和 ELU(或任何 ReLU 变体)可以显著减少训练开始阶段的梯度消失/爆炸问题,但不保证在训练期间问题不会回来。

  在 2015 年的一篇论文中,Sergey Ioffe 和 Christian Szegedy 提出了一种称为批量标准化(Batch Normalization,BN)的技术来解决梯度消失/爆炸问题 [4]。(论文名称:《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》)

  BatchNorm就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的,关于BN的思想在文献 [5] 中已经描述的很清楚了(文献[4]描述的不是很清楚),我这里不再赘述。

  作者证明,这项技术大大改善了他们试验的所有深度神经网络。梯度消失问题大大减少了,他们可以使用饱和激活函数,如 tanh 甚至 sigmoid 激活函数。网络对权重初始化也不那么敏感。他们能够使用更大的学习率,显著加快了学习过程。批量标准化也像一个正则化项一样,减少了对其他正则化技术的需求(如 dropout)[4]。

  然而,批量标准化的确会增加模型的复杂性,您可能会发现,训练起初相当缓慢,而渐变下降正在寻找每层的最佳尺度和偏移量,但一旦找到合理的好值,它就会加速 [4]。

代码示例1
# He初始化,ELU激活函数
# Batch⧸⧸Batch⧸⧸ Normalization, 批量标准化
# 梯度裁剪
from functools import partial
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

datapath = "/home/xiajun/res/MNIST_data"
mnist = input_data.read_data_sets(datapath, validation_size=0, one_hot=True)


if __name__ == '__main__':
    n_inputs = 28 * 28
    n_hidden1 = 300
    n_hidden2 = 100
    n_outputs = 10

    batch_norm_momentum = 0.9
    learning_rate = 0.01

    X = tf.placeholder(tf.float32, shape=(None, n_inputs), name = 'X')
    y = tf.placeholder(tf.int64, shape=None, name = 'y')
    training = tf.placeholder_with_default(False, shape=(), name = 'training')#给Batch norm加一个placeholder

    with tf.name_scope("dnn"):
        he_init = tf.contrib.layers.variance_scaling_initializer()
        #对权重的初始化

        my_batch_norm_layer = partial(
            tf.layers.batch_normalization,
            training=training,
            momentum=batch_norm_momentum
        )

        my_dense_layer = partial(
            tf.layers.dense,
            kernel_initializer=he_init
        )

        hidden1 = my_dense_layer(X, n_hidden1, name='hidden1')
        bn1 = tf.nn.elu(my_batch_norm_layer(hidden1))
        hidden2 = my_dense_layer(bn1, n_hidden2, name='hidden2')
        bn2 = tf.nn.elu(my_batch_norm_layer(hidden2))
        logists_before_bn = my_dense_layer(bn2, n_outputs, name='outputs')
        logists = my_batch_norm_layer(logists_before_bn)

    with tf.name_scope('loss'):
        xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logists)
        loss = tf.reduce_mean(xentropy, name='loss')

    # 训练操作1, ok
    '''
    with tf.name_scope('train'):
        optimizer = tf.train.GradientDescentOptimizer(learning_rate)
        training_op = optimizer.minimize(loss)
    '''

    # 训练操作2, ok
    ''
    with tf.name_scope("train"):
        optimizer = tf.train.GradientDescentOptimizer(learning_rate)
        extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
        with tf.control_dependencies(extra_update_ops):
            training_op = optimizer.minimize(loss)
    ''

    # 梯度裁剪, ok
    '''
    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]  # 将梯度裁剪到 -1.0 和 1.0 之间
    training_op = optimizer.apply_gradients(capped_gvs)
    '''

    with tf.name_scope("eval"):
        correct = tf.nn.in_top_k(logists, y, 1)
        accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))

    init = tf.global_variables_initializer()
    saver = tf.train.Saver()

    n_epoches = 20
    batch_size = 200
# 注意:由于我们使用的是 tf.layers.batch_normalization() 而不是 tf.contrib.layers.batch_norm()(如本书所述),
# 所以我们需要明确运行批量规范化所需的额外更新操作(sess.run([ training_op,extra_update_ops], ...)。
    extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)

    with tf.Session() as sess:
        init.run()
        for epoch in range(n_epoches):
            for iteraton in range(mnist.train.num_examples//batch_size):
                X_batch, y_batch = mnist.train.next_batch(batch_size)
                y_batch = np.argmax(y_batch, 1)

                # 训练操作1 用下面的sess.run
                # sess.run([training_op, extra_update_ops], feed_dict={training: True, X: X_batch, y: y_batch})

                # 训练操作2 用下面的sess.run
                sess.run(training_op, feed_dict={training: True, X: X_batch, y: y_batch})

            y_batch = np.argmax(mnist.test.labels, 1)
            accuracy_val = accuracy.eval(feed_dict={X: mnist.test.images, y: y_batch})
            print(epoch, 'Test accuracy:', accuracy_val)

使用BN优化时的准确率:

0 Test accuracy: 0.8815
1 Test accuracy: 0.9044

18 Test accuracy: 0.9651
19 Test accuracy: 0.9652

这对 MNIST 来说不是一个很好的准确性。 当然,如果你训练的时间越长,准确性就越好,但是由于这样一个浅的网络,批量范数和 ELU 不太可能产生非常积极的影响:它们大部分都是为了更深的网络而发光[4]。

梯度裁剪

  减少梯度爆炸问题的一种常用技术是在反向传播过程中简单地剪切梯度,使它们不超过某个阈值。 这就是所谓的梯度裁剪。一般来说,人们更喜欢批量标准化,但了解梯度裁剪以及如何实现它仍然是有用的[4]。

threshold = 1.0  # threshold是可以调整的超参数

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]  # 将梯度裁剪到 -1.0 和 1.0 之间
training_op = optimizer.apply_gradients(capped_gvs)

将上述梯度裁剪代码添加到代码示例1中即可检验其效果。

使用梯度裁剪时的准确率:

0 Test accuracy: 0.6869
1 Test accuracy: 0.7291

18 Test accuracy: 0.8823
19 Test accuracy: 0.8957

参考文献

[1] 梯度下降
[2] 梯度下降法和最速下降法的细微差别
[3] 反向传播算法
[4] hands_on_Ml_with_Sklearn_and_TF.第10章.训练深层神经网络
[5] 【深度学习】深入理解Batch Normalization批标准化

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值