当前位置:   article > 正文

Tensorflow神经网络训练(Nan)问题实践分析_tensorflow训练神经网络loss: nan

tensorflow训练神经网络loss: nan

我们在设计、训练Tensorflow神经网络时,无论是简易的BP神经网络,还是复杂的卷积神经网络、循环神经网络,都要面临梯度爆炸、梯度消失,以及数据越界等一系列问题,这也是计算机资源和数学原理所决定。

通常,我们在模型训练过程中,特别是非图像识别模型中,经常会出现Loss(损失)与gradients(梯度)的Nan情况,接下来我们一起讨论此实践所遇到的情况,以及解决方案

1. 现象

Tensorflow模型训练过程中,很多情况下,在训练一段时间后,输出的loss为nan,幸运的可能是一闪而过,接着正常训练,多数是持续nan下去,训练失败。

如果你也输出了gradient(梯度),也可能gradient先输出为nan。

2. 原因

2.1. 简明通俗原理

通俗来讲,神经网络最基本节点的神经单元,数学表示为 y i = f ( w i x i + b i ) y_{i}=f(w_{i}x_{i}+b_{i}) yi=f(wixi+bi),一般通过反向求导模式追踪每一个节点如何影响一个输出。这是以误差为主导的反向传播(Back Propagation),旨在得到最优的全局参数矩阵(权重参数)。

当神经网络的层数较多、神经元节点较多时,模型的数值稳定性容易变差。假设一个层数为 L L L的网络,第 l l l H l H_{l} Hl的权重参数为 W l W_{l} Wl,为了便于讨论,略去偏置参数 b b b。对于给定 X X X输入的网络,第 l l l层的输出 H l = X W 1 W 2 . . . W l H_{l}=XW_{1}W_{2}...W_{l} Hl=XW1W2...Wl。此时, H l H_{l} Hl很容易出现衰减或爆炸。

例如第30层的输出将出现 0. 2 30 ≈ 1 × 1 0 − 21 0.2^{30} \approx 1 \times 10^{-21} 0.2301×1021,衰减掉了。

当我们训练神经网络时,把“损失“ 看作 ”权重参数“ 的函数。因此,出现nan现象就是 ∑ w i x i + b i \sum w_{i}x_{i}+b_{i} wixi+bi越界造成的。

那么,我们要做的就是如何防止出现 ∑ w i x i + b i \sum w_{i}x_{i}+b_{i} wixi+bi越界。

反向传播是以目标的负梯度方向对参数进行调整,参数的更新为: w i ← w i + Δ w w_{i} \leftarrow w_{i} + \Delta w wiwi+Δw

给定学习率 α \alpha α,得出: Δ w = − α ∂ L o s s ∂ w \Delta w = -\alpha \frac{\partial Loss}{\partial w} Δw=αwLoss

由于深度网络是多层非线性函数的堆砌,整个深度网络可以视为是一个复合的非线性多元函数(这些非线性多元函数其实就是每层的激活函数),那么对loss函数求不同层的权值偏导,相当于应用梯度下降的链式法则,链式法则是一个连乘的形式,所以当层数越深的时候,梯度将以指数传播。

如果接近输出层的激活函数求导后梯度值大于1,那么层数增多的时候,最终求出的梯度很容易指数级增长,也就是梯度爆炸;相反,如果小于1,那么经过链式法则的连乘形式,也会很容易衰减至0,也就是梯度消失。

2.2. 原因

神经网络训练过程中所有nan的原因:一般是正向计算时节点数值越界,或反向传播时gradient数值越界。无论正反向,数值越界基本只有三种操作会导致:

  1. 节点权重参数或梯度的数值逐渐变大直至越界;
  2. 有除零操作,包括0除0,这种比较常见于趋势回归预测;或者,交叉熵对0或负数取log;
  3. 输入数据存在异常,过大/过小的输入,导致瞬间NAN。

3. 解决方案

3.1. 减小学习率

减少学习率(learning_rate)是最直接的、简易的方法。

学习率较高的情况下,直接影响到每次更新值的程度比较大,走的步伐因此也会大起来。通常过大的学习率会导致无法顺利地到达最低点,稍有不慎就会跳出可控制区域,此时我们将要面对的就是损失成倍增大(跨量级)。

训练过程中,我们可以尝试把学习率减少10倍、100倍,乃至更多,该类问题绝大部分可以得到解决(注意学习率也别太小,否则收敛过慢浪费时间,要在速度和稳定性之间平衡)

3.2. 权重参数初始化

神经网络的训练过程,实质就是自动调整网络中参数的过程。在训练的起初,网络的参数总要从某一状态开始,而这个初始状态的设定,就是神经网络的初始化。

合适网络初始值,不仅有助于梯度下降法在一个好的“起点”上去寻找最优值。

通常,我们常见的初始化是随机正态分布初始化,权重和偏置的初始化采用的是符合均值为0、标准差为1的标准正态分布(Standard Noraml Distribution)随机化方法。但它是最佳的初始化策略吗?

        #标准差为0.1的正态分布
        def weight_variable(shape,name=None):
            initial = tf.truncated_normal(shape,stddev=0.1)
            return tf.Variable(initial,name=name)
        #0.1的偏差常数,为了避免死亡节点
        def bias_variable(shape,name=None):
            initial = tf.constant(0.1, shape=shape)
            return tf.Variable(initial,name=name)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果使用relu,则最好使用方差缩放初始化(he initial)。

在 TensorFlow 中,方差缩放方法写作 tf.contrib.layers.variance_scaling_initializer()。根据我们的实验,这种初始化方法比常规高斯分布初始化、截断高斯分布初始化及 Xavier 初始化的泛化/缩放性能更好。

通俗的讲,方差缩放初始化根据每一层输入或输出的数量(在 TensorFlow 中默认为输入的数量)来调整初始随机权重的方差,从而帮助信号在不需要其他技巧(如梯度裁剪或批归一化)的情况下在网络中更深入地传播。Xavier 和方差缩放初始化类似,只不过 Xavier 中每一层的方差几乎是相同的;但是如果网络的各层之间规模差别很大(常见于卷积神经网络),则这些网络可能并不能很好地处理每一层中相同的方差。

        #def weight_variable(shape,name=None):
        #    return tf.get_variable(name, shape ,tf.float32 ,xavier_initializer())
        
        def weight_variable(shape,name=None):
            return tf.get_variable(name, shape ,tf.float32 ,tf.variance_scaling_initializer())
  • 1
  • 2
  • 3
  • 4
  • 5

tf.variance_scaling_initializer()
参数为(scale=1.0,mode=“fan_in”,distribution=“normal”,seed=None,dtype=dtypes.float32)

其训练效果有所提升:

step: 11800, total_loss: 161.1809539794922,accuracy: 0.78125
step: 11850, total_loss: 46.051700592041016,accuracy: 0.9375
step: 11900, total_loss: 92.10340118408203,accuracy: 0.875
step: 11950, total_loss: 23.025850296020508,accuracy: 0.96875
  • 1
  • 2
  • 3
  • 4

如果激活函数使用sigmoid和tanh,最好使用xavir。

xavier_initializer()法:这个初始化器是用来保持每一层的梯度大小都差不多相同

W ∼ U [ − 6 n j + n j + 1 , 6 n j + n j + 1 ] W \sim U [ - \frac {\sqrt{6}} {\sqrt{n_{j}+n_{j+1}}} , \frac{\sqrt{6}}{\sqrt{n_{j}+n_{j+1}}}] WU[nj+nj+1 6 ,nj+nj+1 6 ]

常用初始化方法:

  1. tf.constant_initializer() 常数初始化
  2. tf.ones_initializer() 全1初始化
  3. tf.zeros_initializer() 全0初始化
  4. tf.random_uniform_initializer() 均匀分布初始化
  5. tf.random_normal_initializer() 正态分布初始化
  6. tf.truncated_normal_initializer() 截断正态分布初始化
  7. tf.uniform_unit_scaling_initializer() 这种方法输入方差是常数
  8. tf.variance_scaling_initializer() 自适应初始化
  9. tf.orthogonal_initializer() 生成正交矩阵

有时候,为了确保w初始值足够小,常用初始化|w|远远小于1,一般推荐使用 tf.truncated_normal_initializer(stddev=0.02)

3.3. 预测结果裁剪

交叉熵Loss计算中出现Nan值,我们看交叉熵:
l o s s = − ∑ i = 1 n y i l o g ( y ^ i ) loss=-\sum_{i=1}^{n} y_{i} log(\hat{y}_{i}) loss=i=1nyilog(y^i)

虽然 y ^ \hat{y} y^是经过tf.nn.sigmoid函数得到的,但在输出的参数非常大,或者非常小的情况下,会给出边界值1或者0。

所以,对如下代码:
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv), name=‘cost_func’)
修改为:
cross_entropy = -tf.reduce_sum(y_*tf.log(tf.clip_by_value(y_conv, 1e-10, 1.0)), name=‘cost_func’) # 最大值是1,归一化后0~1

            #第二全连接层,即输出层,使用柔性最大值函数softmax作为激活函数
            y_conv = tf.nn.softmax(tf.matmul(h_fc2_drop,W_fc3) + b_fc3, name='y_')
            
            # 使用TensorFlow内置的交叉熵函数避免出现权重消失问题
            cross_entropy = -tf.reduce_sum(y_*tf.log(tf.clip_by_value(y_conv, 1e-10, 1.0)), name='cost_func') # 最大值是1,归一化后0~1
            
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.4. 梯度修剪

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

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

            #使用优化器
            lr = tf.Variable(learning_rate,dtype=tf.float32)
            #train_step = tf.train.AdamOptimizer(lr).minimize(cross_entropy)           
            optimizer = tf.train.AdamOptimizer(lr)
            gradients = optimizer.compute_gradients(cross_entropy)
            #梯度优化裁剪   
            capped_gradients = [(tf.clip_by_value(grad, -0.5, 0.5), var) for grad, var in gradients if grad is not None]
            train_step = optimizer.apply_gradients(capped_gradients) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

另外,网络推荐clip_by_global_norm:
tf.clip_by_global_norm 重新缩放张量列表,以便所有范数的向量的总范数不超过阈值。但是它可以一次作用于所有梯度,而不是分别作用于每个梯度(即,如果有必要,将全部按相同的因子进行缩放,或者都不进行缩放)。这样会更好,因为可以保持不同梯度之间的平衡。

3.5. 其他

  1. 缩小Bathsize
    通过缩小batch_size,相当于缩小输入,达到缩减权重集合目标,有一定效果,但是训练速度可能减慢。

  2. 输入归一化
    对于输入数据,图片类比较好解决,把0—255归一化到0~1,而其他数据,需要根据数据分布分析处理,采用合适的归一化方案。

  3. 调整激活函数
    神经网络,现在经常使用relu做为激活函数,但是容易出现Nan的问题,可以根据实际情况换激活函数。

  4. batch normal
    BN具有提高网络泛化能力的特性。采用BN算法后,就可以移除针对过拟合问题而设置的dropout和L2正则化项,或者采用更小的L2正则化参数。

4. 小结

Tensorflow神经网络训练比较容易出现Nan等问题,通过实践分析,在实际应用中,需要采用综合解决方案,采用上面提到的所有方法,综合解决。

万事开头难,首先从设计的学习率、初始权重开始、激活函数等方面入手优化,尽量减少中间干预修剪。

即使这样,也会出现Nan等问题,怎么办?
中断训练,重新开始新的一轮训练。

由于笔者水平有限,欢迎交流反馈。

参考:

1.《Tensorflow LSTM选择Relu激活函数与权重初始化、梯度修剪解决梯度爆炸问题实践》 CSDN博客, 肖永威 ,2021年3月
2.《梯度消失和梯度爆炸的数学原理及解决方案》 CSDN博客 , ty44111144ty ,2019.08
3.《关于 tf.nn.softmax_cross_entropy_with_logits 及 tf.clip_by_value》 博客园 ,微信公众号–共鸣圈 ,2017.03
4.《tensorflow NAN常见原因和解决方法》 CSDN博客 ,苏冉旭 ,2019年2月
5.《一文搞懂反向传播算法》 简书,张小磊啊,2017年8月
6.《TensorFlow从0到1 - 15 - 重新思考神经网络初始化》 知乎 ,袁承兴 ,2020年7月
7.《深入认识深度神经网络》 CSDN博客 ,肖永威 ,2020年6月
8.《TensorFlow实现Batch Normalization》 脚本之家,marsjhao ,2018年3月

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/313835
推荐阅读
相关标签
  

闽ICP备14008679号