Tensorflow LSTM选择Relu激活函数与权重初始化、梯度修剪解决梯度爆炸问题实践
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,0x⩾0x<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+e−2x2−1
其图形效果如下图所示。
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(Uxt−1+Wht−2)
将其继续展开, 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月
更多推荐
所有评论(0)