1. 梯度爆炸问题

我最近研究多层LSTM在时序业务场景中的应用,如果基于Keras框架实现的时候,激活函数使用Relu,训练速度比较快而且效果也好,但是基于Tensorflow框架实现的时候,如果把激活函数由默认tanh换成Relu时,训练过程中出现了如下问题:
在这里插入图片描述
深度学习模型训练中途出现cost突然变大,或许几经周折降下来,不过大多数还是暴涨,出现了“nan”。

cost:  0.00532
......
cost:  1097.2125
cost:  nan
cost:  nan

其中,激活函数设置如下:

        #更换默认tanh激活函数
        cell_list = tf.contrib.rnn.BasicLSTMCell(self.cell_size, 
                                                 forget_bias=1.0, 
                                                 state_is_tuple=True, 
                                                 activation=tf.nn.relu) 

模型初始化权重:

    def _weight_variable(self, shape, name='weights'):
        initializer = tf.random_normal_initializer(mean=0., stddev=1.0,)
        return tf.get_variable(shape=shape, initializer=initializer, name=name)

    def _bias_variable(self, shape, name='biases'):
        initializer = tf.constant_initializer(0.1)
        return tf.get_variable(name=name, shape=shape, initializer=initializer)

其实,出现这种问题是深度学习训练中常见的典型梯度爆炸问题。

2. 解决方案

2.1. 换回tanh激活函数?

本实践案例中,目标是采用的激活函数是Relu。

r e l u ( x ) = m a x ( x , 0 ) = { x , x ⩾ 0 0 x < 0 relu(x)=max(x,0)= \left\{\begin{matrix} x, & x\geqslant 0\\ 0 & x<0 \end{matrix}\right. relu(x)=max(x,0)={x,0x0x<0

其图形效果如下所示。
在这里插入图片描述
使用Relu函数有什么优势?

  • 没有饱和区,不存在梯度消失问题。
  • 没有复杂的指数运算,计算简单、效率提高。
  • 实际收敛速度较快,比 Sigmoid/tanh 快很多。
  • 比 Sigmoid 更符合生物学神经激活机制。

Relu的缺点:

  • 在训练的时候,ReLU单元比较脆弱并且可能“死掉”。举例来说,当一个很大的梯度,流过ReLU的神经元的时候,可能会导致梯度更新到一种特别的状态,在这种状态下神经元将无法被其他任何数据点再次激活。如果这种情况发生,那么从此所以流过这个神经元的梯度将都变成0。也就是说,这个ReLU单元在训练中将不可逆转的死亡,因为这导致了数据多样化的丢失。

  • 如果学习率设置得太高,可能会发现网络中40%的神经元都会死掉(在整个训练集中这些神经元都不会被激活)。通过合理设置学习率,这种情况的发生概率会降低。

在神经网络中,隐含层的激活函数,最好选择ReLU。而在LSTM中,默认激活函数Tanh,如果解决不了梯度爆炸问题,只能换回tanh激活函数。

t a n h ( x ) = 2 1 + e − 2 x − 1 tanh(x) = \frac{2} {1+e^{-2x}}-1 tanh(x)=1+e2x21

其图形效果如下图所示。
在这里插入图片描述
sigmoid和tanh:

  • sigmoid在输入处于[-1,1]之间时,函数值变化敏感,一旦接近或者超出区间就失去敏感性,处于饱和状态,影响神经网络预测的精度值;
  • tanh的变化敏感区间较宽,导数值渐进于0、1,符合人脑神经饱和的规律,比sigmoid函数延迟了饱和期;
  • tanh在原点附近与y=x函数形式相近,当激活值较低时,可以直接进行矩阵运算,训练相对容易;
  • tanh和sigmoid都是全部激活(fire),使得神经网络较重(heavy)。

relu时比tanh收敛更快,且准确率更高

2.2. 优化初始化权重

我们知道神经网络基本构成可以描述为由线性函数 f ( x ) = w x + b f(x)=wx+b f(x)=wx+b构成,在累加 ∑ i m w i x i + b \sum_{i}^{m}w_{i}x_{i} + b imwixi+b过程中,如果权重’weights’过大,可能影响loss过大,进而引起梯度爆炸的问题。

根据输入、输出的特点(本案例归一化输入为0~1之间),设置参数如下所示:

    def _weight_variable(self, shape, name='weights'):
        initializer = tf.random_normal_initializer(mean=0., stddev=0.1,)
        return tf.get_variable(shape=shape, initializer=initializer, name=name)

    def _bias_variable(self, shape, name='biases'):
        initializer = tf.constant_initializer(0.01)
        return tf.get_variable(name=name, shape=shape, initializer=initializer)

通过设置小些的标准差和偏置,梯度爆炸发生减少,但是不能杜绝。

附注:初始化函数,正态分布
tf.random_normal_initializer(mean=0.0, stddev=0.1, seed=3)
参数:

  • mean:正态分布的均值,默认值 0,一个 python 标量或一个标量张量.要生成的随机值的均值.
  • stddev:正态分布的标准差, 默认值 1,一个 python 标量或一个标量张量.要生成的随机值的标准偏差.
  • seed:随机种子,指定seed的值相同生成同样的数据,一个 Python 整数.用于创建随机种子.查看 tf.set_random_seed 行为.
  • dtype:数据类型,只支持浮点类型

一个较小的标准差,代表这些数值较接近平均值,一般来说标准差较小为好,这样代表比较稳定。下面模拟标准差对神经元计算的影响。

import matplotlib.pyplot as plt
import math
import numpy as np

def testInitWeight():
    x = np.random.uniform(0,1,30*25)
    t = 10000
    z_lst = np.empty(t)
    mu = [0,0,0]
    sigma = [1.0,0.1,0.01]    
    for j in range(3):
        for i in range(t):
            w = np.random.normal(mu[j], sigma[j], 30*25)         
            b = 0
            # z为加权和
            z = np.sum(x * w) + b           
            z_lst[i] = z                            
            
        print ('z 均值:', np.mean(z_lst))
        print ('z 方差:', np.var(z_lst))   
        plt.subplot(1,3,(j+1))
        plt.grid()   
        
        plt.hist(z_lst, bins=10)  
    
    plt.show()            

if __name__ == '__main__':
    testInitWeight()

在这里插入图片描述通过实践,标准差设置小些,如上所示“stddev=0.1”,训练过程中较稳定,减少了梯度爆炸的发生。

2.3. 梯度修剪

不过本文的重点在于在tensorflow中解决梯度爆炸问题,原理很简单就是梯度修剪。把大于1的导数修剪为1。

Tensorflow梯度修剪函数为tf.clip_by_value(A, min, max):
输入一个张量A,把A中的每一个元素的值都压缩在min和max之间。小于min的让它等于min,大于max的元素的值等于max。

    def compute_cost(self):
        losses = tf.contrib.legacy_seq2seq.sequence_loss_by_example(
            [tf.reshape(self.pred, [-1], name='reshape_pred')], 
            [tf.reshape(self.ys, [-1], name='reshape_target')],       
            [tf.ones([self.batch_size * self.n_steps*self.output_size], dtype=tf.float32)], 
            average_across_timesteps=True,
            softmax_loss_function=self.ms_error,
            name='losses'
        )
        
        with tf.name_scope('average_cost'):
            self.cost = tf.div(
                tf.reduce_sum(losses, name='losses_sum'),
                self.batch_size_,
                name='average_cost')
            tf.summary.scalar('cost', self.cost)
    
    def train_optimizer(self):   
        # 使用Adam梯度下降
        optimizer = tf.train.AdamOptimizer(LR)     
        # 裁剪一下Gradient输出,最后的gradient都在[-1, 1]的范围内
        # 计算导数,cost为损失函数
        gradients = optimizer.compute_gradients(self.cost)
        # 限定导数值域-1到1
        capped_gradients = [(tf.clip_by_value(grad, -1., 1.), var) for grad, var in gradients if grad is not None]
        # 将处理后的导数继续应用到LSTM算法中
        self.train_op = optimizer.apply_gradients(capped_gradients)

默认值为 clipnorm=1.0 、clipvalue=0.5。

3. 原理说明

3.1. 什么是梯度爆炸

梯度爆炸指神经网络训练过程中大的误差梯度不断累积,导致模型权重出现重大更新。会造成模型不稳定,无法利用训练数据学习。

误差梯度是神经网络训练过程中计算的方向和量的大小,用于以正确的方向和以合适的量更新网络权重。 在深度网络或RNN中,更新过程中可能会累积误差梯度,并最终累积成非常大的梯度。这会导致网络权重的大幅更新,从而导致网络不稳定。在极端情况下,权重的值可能会大到溢出导致出现NaN值。

网络层之间的梯度(值大于 1.0)重复相乘导致的指数级增长会产生梯度爆炸。 梯度爆炸引发的问题 在深度多层感知机网络中,梯度爆炸会引起网络不稳定,最好的结果是无法从训练数据中学习,而最坏的结果是出现无法再更新的 NaN 权重值。

如何确定是否出现梯度爆炸?

  • 模型无法从训练数据中获得更新(如低损失)。
  • 模型不稳定,导致更新过程中的损失出现显著变化。
  • 训练过程中,模型损失变成 NaN。

本质是梯度传递的链式法则所导致的矩阵高次幂(反向传播会逐层对函数求偏导相乘)。

RNN结果出现nan值?梯度爆炸,导致结果不收敛。都是梯度太大惹的祸,所以可以通过减小学习率(梯度变化直接变小)、减小batch size(累积梯度更小)、 features规格化(避免突然来一个大的输入)。

3.2. 链式法则

链式法则应用广泛,神经网络中的反向传播算法就是链式法则为基础。微积分中的链式法则(为了不与概率中的链式法则相混) 用于计算复合函数的导数。反向传播是一种计算链式法则的算法,使用高效的特定运算顺序。

x x x是实数, f f f g g g是从实数映射到实数的函数。假设 y = g ( x ) y=g(x) y=g(x),并且 z = f ( g ( x ) ) = f ( y ) z=f(g(x))=f(y) z=f(g(x))=f(y)。那么链式法则是说:

d z d x = d z d y d y d x \frac {dz}{dx}=\frac {dz}{dy}\frac {dy}{dx} dxdz=dydzdxdy

或另种形式:

d z d x = f ′ ( g ( x ) ) g ′ ( x ) \frac {dz}{dx}=f'(g(x))g'(x) dxdz=f(g(x))g(x)

链式法则就是 f f f g g g两个函数组合起来的复合函数,导数等于里面函数代入外函数值的导乘以里面函数之导数。

Hinton在它的IRNN论文里面(arxiv:[1504.00941] A Simple Way to Initialize Recurrent Networks of Rectified Linear Units)是很明确的提到的:

也就是说在RNN中直接把激活函数换成ReLU会导致非常大的输出值。

首先,由于前向传播时计算的结果会变成多个W连乘:

假设采用ReLU替代传统RNN中的激活函数,并且假设ReLU函数一直处于激活区域(即输入大于0),

则有:
f ( x ) = x f(x)=x f(x)=x
n e t t = U x t + W ( U x t − 1 + W h t − 2 ) net_{t} = Ux_{t} + W(Ux_{t-1}+Wh_{t-2}) nett=Uxt+W(Uxt1+Wht2)

将其继续展开, n e t t net_{t} nett中最终将包含t个W连成。如果W不是单位举证, n e t t net_{t} nett则的结果最终将趋于0或者无穷大,引发严重的数值问题。

同时,假设采用ReLU激活函数,且一开始所有的神经元都处于激活状态,在梯度传递了n层之后,有:

n e t t n e t t 1 = W n \frac{net_{t}}{net_{t1}}=W^{n} nett1nett=Wn

可以看到,只要W不是单位矩阵,梯度还是会出现消失或者爆炸的问题。

综上所述,当采用ReLU作为激活函数时,只有当W的取值在单位矩阵附近的时候,才能取得比较好的效果。

3.3. 梯度修剪原理

基于梯度修剪原理解决梯度爆炸的方法,是在一个只有一个隐藏节点的网络中,损失函数和权值w偏置b构成error surface,就好比其中有一堵墙,如下所示。
在这里插入图片描述
损失函数每次迭代都是每次一小步,但是当遇到这堵墙时,在墙上的某点计算梯度,梯度会瞬间增大,指向某处不理想的位置。如果我们使用缩放,可以把误导控制在可接受范围内,如虚线箭头所示。

3.4. 权重正则化

目标函数 = 损失函数 + 正则化项。通过目标函数中的正则化项,“惩罚”过大的权重,从而使权重不会过大,进而缓解梯度爆炸的问题。

什么是正则化(regularization)

​ 如果用一句话解释:正则化就是通过增加权重惩罚(penalty)项到损失函数,让网络倾向于学习小一点的权重,从而达到抑制过拟合,增加模型泛化能力的效果。常见的正则化方法有 L 1 L1 L1正则化, L 2 L2 L2正则化和Dropout正则化等。其中, L 2 L2 L2正则化的公式:

L = L 0 + λ 2 ∑ i = 1 n ( w 2 ) L = L_{0} + \frac{ \lambda}{2} \sum_{i=1}^{n}(w^{2}) L=L0+2λi=1n(w2)

式中 L 0 L_{0} L0是原始代价损失函数, λ 2 ∑ i = 1 n ( w 2 ) \frac{ \lambda}{2} \sum_{i=1}^{n}(w^{2}) 2λi=1n(w2) L 2 L2 L2正则化损失函数, 其中 λ \lambda λ是权重因子, w w w为权重。

在tensorflow 中,计算图(graph)通过集合(collection)来管理包括张量(tensor)、变量(variable)、资源:

  • tf.add_to_collection 将资源添加到特定的集合中
  • tf.get_collection 从特定集合中取出对应的资源

4. 小结

通常在使用LSTM组成的神经网络层数比较少的时候,一般默认用其tanh函数作为激活函数,比Relu要好很多。

近些年来,在卷机神经网络中使用了Relu函数,发现解决了深度神经网络梯度消失的问题,在LSTM中,随着LSTM组成的网络加深,再继续使用tanh函数,就存在了梯度消失的的风险,导致一直徘徊在一个点无法搜索最优解,这种情况下,可以采用Relu函数进行调整,注意学习率需要变地更小一点防止进入死神经元。

为了解决基于Tensorflow多层LSTM模型中激活函数采用Relu出现梯度爆炸的问题,采用梯度修剪为核心的解决方案,并在神经网络及输入数据的参数权重设置为正态分布,std=0.1,减缓梯度爆炸发生。
在这里插入图片描述

在训练方法也很重要,选取合适的batch、学习率也是解决梯度爆炸问题的入手点。

参考:
《深度学习–解决梯度爆炸方法(含TensorFlow代码)》 CSDN博客, 超屌的温jay ,2018年6月
《激活函数 sigmoid、tanh、relu》 简书 , SpikeKing ,2019年1月
《Tensorflow LSTM实现多维输入输出预测实践详解》 CSDN博客 ,肖永威 ,2021年3月
《基于tensorflow的正则化实现》 简书 , AlexChung16 ,2020年5月

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐