这两天造的轮子每次训练上来几个epoch就爆,试过lr降到1e-5;将所有分母/log都加了极小值;加了梯度裁剪,改了损失函数,lr衰减改成指数形式,加载预训练权重,修改权重初始化方式等等全失败,一度怀疑是我代码逻辑哪里写的有问题,检查数遍无果。就在这时随手加了个warmup,卧槽居然不飞了,个人觉得原因是加载的预训练模型最后几个输出层类别个数和自己的任务类别个数存在差异,直接采用温和的lr衰减方式导致梯度飘了,需要在训练初期快速下降是模型稳定后再采用普通的衰减训练。被爆多次干脆这篇把梯度爆炸和弥散过一遍。
什么是梯度弥散和梯度爆炸?
直观上说深度学习的优化是基于反向传播和链式求导,每层的梯度会进行连乘,如果层数太深就容易造成>1的梯度连乘后变得很大,<1的梯度连乘后变得很小。所以每层中相乘的两个数:一个是初始化权重的值,一个的激活函数的导数就会影响传播值。
梯度弥散通常出现在以下两种情况:
- 神经网络层次太深;
- 采用了不合适的激活函数;
梯度爆炸通常出现在以下情况:
- 神经网络层次太深;
- 采用了不合适的初始化权重方式
在深层网络中,不同的层学习的速度差异很大,靠近输出的层学习的情况通常很好,靠近输入的层学习的通常很慢,有时甚至训练了很久,前几层的权值和刚开始随机初始化的值差不多。因此,梯度消失和梯度爆炸的根本原因在于反向传播训练法则,本质在于反向传播这个方法问题。
如何解决梯度弥散和爆炸?
方法一:恒等映射,根据上面提到的问题,无论是梯度弥散还是梯度爆炸都是由于网络层次太深造成连乘的缩小/放大。造成越靠近输入端的权重更新越缓慢,参考残差结构的思想采用恒等映射的方式设计神经网络跳跃连接,使反向传播的梯度值能直接传递到早期的层更新权重,残差结构如下图所示:
![v2-3357767cea2e8f4327474abf3675b362_b.jpg](https://i-blog.csdnimg.cn/blog_migrate/7a0735c3fc2af352ac569ca1a96c4d6c.png)
方法二:激活函数,每层的梯度值可以选择合适的激活函数来缓解梯度弥散问题;比如下图sigmoid函数的梯度随着x的增大或减小会进入饱和区,公式如下:
![sigmoid(x) = \frac{1}{1+e^{-x}}](https://i-blog.csdnimg.cn/blog_migrate/9857878f731beb9e3224ba87ba139445.png)
![v2-6060134f5f7320b35c36769ce3eba9bd_b.jpg](https://i-blog.csdnimg.cn/blog_migrate/9433e22182444cae2d6790d02dbd0a8b.png)
relu相比sigmoid属于非饱和激活函数,其导数在正数部分是恒等于1的,因此在深层网络中使用relu激活函数就不会导致梯度消失和爆炸的问题,每层的网络都可以得到相同的更新速度,公式如下:
![relu(x) = max(0, x)](https://i-blog.csdnimg.cn/blog_migrate/ad6d63b23152fece9fb6fdbe555238d1.png)
![v2-cfa15735728ecc43f5a333b25ffbb4c3_b.jpg](https://i-blog.csdnimg.cn/blog_migrate/21c51dd3e8b876709a19f628cb6f10cd.png)
但是relu激活函数由于负数部分为0,会导致神经元死亡,所以后来出现了很多变种形式,比如:leakrelu,elu等解决了relu的0区间带来的影响。
方法三:BatchNorm,把每层神经网络任意神经元这个输入值的分布强行拉回到接近均值为0方差为1的标准正太分布,即严重偏离的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,使得让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,能大大加快训练速度,提升训练稳定性。
![v2-aba48f222cee8320a6a81dc281640165_b.jpg](https://i-blog.csdnimg.cn/blog_migrate/a0c07b31826c4e8f4342c5da8989890f.jpeg)
BN应作用在非线性映射前,即对x=Wu+b做规范化。将激活规范为均值和方差一致的手段使得原本会减小的activation的scale变大。
方法四:梯度裁剪,检查误差梯度的值是否超过阈值,如果超过,则截断梯度,将梯度设置为阈值,可以一定程度上缓解梯度爆炸问题。TF代码如下:
gvs = optimizer.compute_gradients(loss[0] + l2_loss, var_list=update_vars)
clip_grad_var = [gv if gv[0] is None else [tf.clip_by_norm(gv[0], 100.), gv[1]] for gv in gvs]
train_op = optimizer.apply_gradients(clip_grad_var, global_step=global_step)
####### tensorflow 提供的 API函数 ######
tf.clip_by_value(t, clip_value_min, clip_value_max)
tf.clip_by_norm(t, clip_norm)
tf.clip_by_average_norm(t, clip_norm)
tf.clip_by_global_norm(t_list, clip_norm)
方法五:权重初始化,上面提到如果初始化权重太大,进过多层的连续相乘回传到输入端会造成梯度值指数级增长,所以选择合适的初始化方式至关重要,一般推荐He初始化和Xavier初始化方式。也可以采用预训练的方法先寻找局部最优,站在一个较好的位置进行微调。如果使用 relu,推荐采用 he_initialization, 即 tf.contrib.layers.variance_scaling_initializer( ),在 relu 网络中,假定每一层有一半的神经元被激活,另一半为 0 ,所以要保持 variance 不变,只需要在 xavier 的基础上再除以 2 。如果激活函数使用 sigmoid 和 tanh,则最好使用 xavier initialization, 即 tf.contrib.layers.xavier_initializer_conv2d( ),保持输入和输出的方差一致,避免了所有的输出值都趋向于0。
方法六:权重正则化,检查网络权重的大小,并惩罚产生较大权重值的损失函数。该过程被称为权重正则化,通常使用的是 L1 惩罚项(权重绝对值)或 L2 惩罚项(权重平方)。TF代码如下:
l2_loss = tf.add_n([tf.nn.l2_loss(var) for var in tf.trainable_variables() if 'weights' in var.name])
方法七:长短时记忆网络,在循环神经网络中,梯度爆炸的发生可能是因为某种网络的训练本身就存在不稳定性,如随时间的反向传播本质上将循环网络转换成深度多层感知机神经网络。使用长短期记忆(LSTM)单元和相关的门类型神经元结构可以减少梯度爆炸问题。采用 LSTM 单元是适合循环神经网络的序列预测的较好方式。