梯度消失与梯度爆炸

Author Avatar
YaHei 4月 08, 2018


BGM:《钢之炼金术师FA》OP1
这个吉他混音版真是太棒了~


参考:

  1. Hands-On Machine Learning with Scikit-Learn and TensorFlow(2017)》Chap11
    《Hands-On Machine Learning with Scikit-Learn and TensorFlow》笔记
  2. 卷积神经网络——深度学习实践手册(2017.05)

解决梯度爆炸和消失的常用技术

  1. 随机初始化(Xavier Initialization、He Initialization等)
  2. 使用nonsaturating函数(如relu)
  3. 批量归一化(Batch Normalization, BN)
  4. 梯度裁剪(Gradient Clipping)

Xavier Initialization(Glorot Initialization)

论文:Understanding the difficulty of training deep feedforward neural networks(2010)
作者Xavier建议:使每一层的输入输出的方差相等,而且正反向传播的梯度也相等

并针对sigmoid激活函数(logistic激活函数)提出一种初始化方式:

  1. 各权重用均值为0的正态分布随机数进行初始化,并且标准差根据输入、输出的维度确定——
    $$ \sigma = \sqrt{ \frac{2}{n_{inputs} + n_{outputs} } } $$
  2. 用[-r, r]的均匀分布随机数进行初始化——
    $$ r = \sqrt{ \frac{6}{n_{inputs} + n_{outputs}} } $$

此外,另外一篇论文Delving Deep into Rectifiers:Surpassing Human-Level Performance on ImageNet Classification(2015)
作者He提出类似的关于其他激活函数的初始化建议:

  • tanh
    $$ \sigma = 4 \sqrt{ \frac{2}{n_{inputs} + n_{outputs} } } $$
    $$ r = 4 \sqrt{ \frac{6}{n_{inputs} + n_{outputs}} } $$
  • relu及其变体(He Initialization)
    $$ \sigma = \sqrt{2} \sqrt{ \frac{2}{n_{inputs} + n_{outputs} } } $$
    $$ r = \sqrt{2} \sqrt{ \frac{6}{n_{inputs} + n_{outputs}} } $$

数据敏感的参数初始化方式:
是一种根据自身任务数据集量身定制的参数初始化方式;
论文:Data-dependent Initializations of Convolutional Neural Networks(2016)
代码:philkr/magic_init | github

relu——nonsaturating activation function

  • 优势
  • 存在问题(dying relus)
    当输入的加权和为负数时,relu将出现“死亡”而开始不停地输出0(因为当输入为负数时relu的梯度一直是0),最终导致网络崩溃;
  • 变体(解决die问题)
    论文 Empirical Evaluation of Rectified Activations in Convolution Network(2015) 比较了各种不同变体的表现

    • leaky relu
      $$ LeakyReLU_\alpha(z) = max(\alpha z, z) $$
      其中 $\alpha$ 使 $z<0$ 时有一个小梯度,使得relu不会彻底“死亡”,在接下来的训练中有可能被“复活”;
      通常 $\alpha$ 取0.01
    • randomized leaky relu(RReLU)
      leaky relu的变体,其中 $\alpha$ 在训练中是一个随机数,最终测试时固定为一个平均值
    • parametric leaky relu(PReLU)
      leaky relu的变体,其中 $\alpha$ 作为模型的一个参数参与训练
      论文指出,PReLU在大型图片数据集上有很好的表现,但在小数据集上很快就过拟合
    • exponential linear unit(ELU)
      论文 FAST AND ACCURATE DEEP NETWORK LEARNING BY EXPONENTIAL LINEAR UNITS (ELUS)(2016) 指出,ELU可以减少训练时间,并且在测试集上有更好的表现
      $$\begin{equation}
      ELU_\alpha (z) = \left\{
      \begin{array}{rcl}
      \alpha (e^z - 1) & & ,z < 0\\
      z & & ,z>=0
      \end{array} \right.
      \end{equation}$$

      • ELU在 $z<0$ 时有负数输出,使得单元的平均输出更接近于0,这有助于缓解梯度消失的问题
      • $\alpha$ 通常取1,但也可以采用其他方式来确定它的值
      • 当 $z<0$ 时有非0梯度,有助于避免dying relu问题
      • 函数光滑,有助于加速梯度下降(而且当 $\alpha=1$ 时,函数在 $z=0$ 上可导)
      • ELU因为多了指数运算,其计算要比relu慢,但是因为能更快收敛,所以在训练有一定的补偿;不过在测试时,还是会比较慢
  • 选择
    • 一般来说:ELU > leaky ReLU(及其变体) > ReLU > tanh > logistic
    • 优先使用ELU,但如果需要考虑预测的开销,那么可以使用leaky ReLU及其变体
    • 如果还要进一步节省时间和计算力,可以使用交叉验证来评估不同的激活函数
    • 如果网络出现过拟合而不想花时间去调参和测试,可以使用RReLU
    • 如果你拥有非常大的数据集,那也可以直接使用PReLU
  • 使用
    tensorflow有预置的elu函数
      hidden1 = fully_connected(X, n_hidden1, activation_fn=tf.nn.elu)
    
    虽然tensorflow没有预置的leaky relus,但是定义起来很容易
      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)
    

批量归一化(Batch Normalization, BN)

随机初始化可以在训练的开始显著减少梯度爆炸和消失的问题,但它不能保证训练过程中不再出现;
论文 Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift(2015) 提出了批量归一化技术来解决这个问题,以及Internal Covariate Shift问题(训练过程中,随着前一层参数的变化,本层的输入分布也随之发生变化);

BN有一种特征归一化(Feature Normalization, FN)的变体;
作用于网络最后一层的特征表示上(FN随后接的是目标函数层),用于提高习得特征的分辨能力;
可用于人脸识别、行人重检测、车辆重检测等任务上;
论文:DeepVisage: Making face recognition simple yet with powerful generalization skills(2017)

区分:归一化、正则化、标准化

参考文章:机器学习里的黑色艺术:normalization, standardization, regularization
归一化(Normalization):数据预处理,将数据限定在特定范围内,消除量纲对建模的影响;
标准化(Standardization):数据预处理,使数据符合标准正态分布;
正则化(Regularization):在损失函数中添加惩罚项,增加建模模糊性,将建模关注点转移到整体趋势上;

具体操作

归一化的方式有很多种:

  1. 最大最小值归一化 $x’ = \frac{x - min}{max - min}$
  2. 对数归一化 $ x’ = \frac{lg(x)}{lg(max)} $
  3. 反正切归一化 $ x’= \frac{2arctan(x)}{\pi} $
  4. 零平均归一化 $ x’ = \frac{x - mean}{std} $

根据论文,该技术在每层的激活函数之前添加一个BN操作,使得输入的数据以零点为中心、归一化;并且在每一层中使用两个新的参数来调整输出的范围,使得模型的每一层能够学习到适宜的表示范围和平均值;
该算法需要评估每一个输入的mini-batch的平均值和标准差;

对于某个mini-batch的输入,归一化的具体过程如下:
$$ \mathbb{ x^{(i)} } = \frac{ x^{(i)} - \mu_B }{ \sqrt{\sigma^2_B + \epsilon} } $$
$$ z^{(i)} = \gamma \mathbb{ x^{(i)} } + \beta $$
其中,
$\mu_B$ 和 $\sigma_B$ 分别是该mini-batch的平均数和标准差;
$\epsilon$ 是一个很小的数(通常取0.001),用于避免 $\sigma = 0$ 导致分母为0的情况;
$\gamma$ 和 $\beta$ 分别是每一层中的两个新的参数,调整后的 $\mathbb{x^{(i)}}$ 通过线性变化后得到归一化的 $z^{(i)}$ 输出

在预测阶段因为不再有mini-batch,所以直接使用在整个训练集的平均数、标准差进行计算即可;
所以在训练时每层会增加四个参数:$\gamma, \beta, \mu, \sigma$ ,训练时采用移动平均的方式可以高效地获得在整个训练集各层的平均值和标准差;

效果

  1. 可以很好的解决梯度爆炸和消失的问题,甚至saturate函数如sigmoid和tanh都可以在深度网络中正常使用;
  2. 网络对权重初始化不再那么敏感
  3. 可以使用更大的学习率,提高训练速度
  4. 具有正则化的效果,可以减少dropout等其他正则化技术的使用
  5. 缺点:BN操作增加了模型的复杂度,预测时间将不可避免地增加
    但是,在训练完成后,BN层可以合并到前一层的卷积层或全连接层,具体参见《MobileNet-SSD网络解析 - BN层合并 | Hey~YaHei!

在tensorflow中使用BN

tensorflow提供了现成的BN层操作: tensorflow.contrib.layers.batch_norm
可以直接作为参数传递给 fully_connected 全连接网络:

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

# ...

# 训练标志
# ... 如前所述,BN在训练和预测时模型略有不同
# ... 训练时,模型需要每次都对mini-batch计算平均值和标准差,并用滑动平均的方式记录整个训练集上的平均值和标准差
# ... 预测时则直接使用整个训练集上的平均值和标准差
is_training = tf.placeholder(tf.bool, shape=(), name='is_training')

# BN操作的参数,包括——
# ... is_training:标志训练与否状态的占位符
# ... decay:使学习率下降的一个因子,v <- v*decay + v*(1-decay)
# ... updates_collections:
bn_params = {
    'is_training': is_training,
    'decay': 0.99,
    'updates_collections': None
}

# 指定全连接网络使用BN操作
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)

如果觉得每次都需要指定归一化的函数和参数太麻烦,可以借助 tf.contrib.framework.arg_scope() 来简化代码:

bg_params = {# ...}

with tf.contrib.framework.arg_scope(
        [fully_connected],          # 第一个参数,为with块内统一参数的函数列表(这里只为fully_connected函数统一参数)
        normalizer_fn=batch_norm,     # 对于其他参数,关键字为函数对应的关键字参数名,值为对该参数赋予的值
        normalizer_params=bn_params):
    # 在该with块中的fully_connected函数就无需再重复指明归一化的函数和参数
    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)

这里batch_norm 层默认是不带scale的,如果有需要,可以在参数 bn_params 添加一项 "scale": True 开启;

梯度裁剪(Gradient Clipping):简单粗暴

论文:On the difficulty of training recurrent neural networks(2012)
直接限制梯度不能超过一个阈值,在RNN中用的比较多;
具体实现:

import tensorflow as tf
# 限制阈值
threshold = 1.0
# 创建优化器
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
# 用optimizer.compute_gradients计算梯度
grads_and_vars = optimizer.compute_gradients(loss)
# 用tf.clip_by_value裁剪梯度grad,如果小于-threshold则取-threshold,如果大于threshold则取threshold
# 变量var不变
capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var)
          for grad, var in grads_and_vars]
# 用optimizer.apply_gradients更新梯度
training_op = optimizer.apply_gradients(capped_gvs)

对于优化器,minimize 方法包含 compute_gradientsapply_gradients,计算梯度后直接更新;
由于需要裁剪梯度,所以需要单独地使用 compute_gradientsapply_gradients