【深度学习笔记1.2】梯度消失与梯度爆炸

2024-06-06 05:58

本文主要是介绍【深度学习笔记1.2】梯度消失与梯度爆炸,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

梯度下降

  梯度下降法(Gradient descent)是一种基于函数一阶性质的优化算法,其本质是在某个位置将目标函数一阶展开,利用其一阶性质持续向函数值下降最快的方向前进,以期找到函数的全局最小解。梯度下降属于梯度优化方法大类,此外还有最速下降法,共轭梯度法等等。还有其他方法基于目标函数的二阶性质,比如牛顿法、拟牛顿法等[1]。

注意:梯度下降法就是最速下降法,很多地方、很多人、包括维基百科、百度百科都这么说的,这导致大家都认为,梯度的反方向就是下降最快的方向,然后事实并非如此,其细微差别参见[2]。

反向传播

  反向传播(BackPropagation,缩写为BP)是“误差反向传播”的简称,是一种与最优化方法(如梯度下降法)结合使用的,用来训练人工神经网络的常见方法[3]。其基本思想就是根据当前网络参数计算出当前输入样本的输出y,再根据y和实际结果的误差来计算各层神经元的梯度项,最后按梯度的反方向更新网络参数,直到达到停止条件。
  人工神经网络的参数多,梯度计算比较复杂。在人工神经网络模型提出几十年后才有研究者提出了反向传播算法来解决深层参数的训练问题。

梯度消失/爆炸

什么是梯度消失/爆炸

  反向传播算法的工作原理是从输出层向输入层传播误差的梯度。不幸的是,梯度下降更新使梯度往往越变得越来越小,以至于使得低层连接权重实际上保持不变,并且训练永远不会收敛到良好的解决方案,这被称为梯度消失问题。在某些情况下,可能会发生相反的情况:梯度可能变得越来越大,许多层得到了非常大的权重更新,算法发散。这是梯度爆炸的问题,在循环神经网络中最为常见。更一般地说,深度神经网络受梯度不稳定之苦,不同的层次可能以非常不同的速度学习。

产生梯度消失/爆炸的原因

  梯度消失/爆炸是造成深度神经网络大部分时间都被抛弃的原因之一。直到2010年才有所缓解,Xavier Glorot 和 Yoshua Bengio 发表的题为《Understanding the Difficulty of Training Deep Feedforward Neural Networks》的论文提出了一些疑问,sigmoid 激活函数和当时最受欢迎的权重随机初始化(即随机初始化时使用平均值为 0,标准差为 1 的正态分布)这个方案组合,使得每层输出的方差远大于其输入的方差(为什么?)。网络正向计算时,每层的方差持续增加,直到激活函数在顶层饱和[4],即在顶层激活函数的导数接近0,因此当反向传播开始时,它几乎没有梯度通过网络传播回来。而且由于反向传播通过顶层向下传递,所以有些较小的梯度也会不断地被稀释,因此较低层确实没有任何东西可用。

如何解决梯度消失/爆炸

改变权重初始化方法

  Glorot和Xavier在他们的论文中提出了一种能显著缓解这个问题的方法。我们需要信号在两个方向上正确地流动:即在进行预测时是正向的,在反向传播梯度时是反向的。 我们不希望信号消失,也不希望它爆炸并饱和。为了使信号能够正确流动,作者认为,我们需要每层输出的方差等于其输入的方差,也需要梯度在相反方向上流过一层之前和之后有相同的方差。然而实际上不可能保证两者都是一样的,除非这个层具有相同数量的输入和输出连接,但是他们提出了一个很好的折衷办法,在实践中证明这个折中办法非常好:随机初始化连接权重必须如下图1公式所描述的那样。其中n_inputs和n_outputs是权重正在被初始化的层(也称为扇入和扇出)的输入和输出连接的数量。 这种初始化策略通常被称为Xavier初始化,或者有时是 Glorot 初始化。

![enter image description here](https://lh3.googleusercontent.com/-Ct5ypbJsA40/W87lWrvh79I/AAAAAAAAAGc/nVMkkzv-ATcJV4H2ZByWRz0bIqPM_A7WQCLcBGAs/s0/Xavier%25E5%2588%259D%25E5%25A7%258B%25E5%258C%2596.jpg "Xavier初始化.jpg")
图1   Xavier 初始化

当输入连接的数量大致等于输出连接的数量时,可以得到更简单的等式:
e.g. σ = 1 n i n p u t s o r r = 3 n i n p u t s \quad \sigma = \dfrac{1}{\sqrt{n_{inputs}}} \quad or \quad r = \dfrac{\sqrt{3}}{\sqrt{n_{inputs}}} σ=ninputs 1orr=ninputs 3

  使用 Xavier 初始化策略可以大大加快训练速度,这是导致深度学习目前取得成功的技巧之一。 另外,最近的一些论文也针对不同的激活函数提供了类似的策略,如下图2所示。 ReLU 激活函数(及其变体,包括简称为 ELU 的激活)的初始化策略有时也称为 He 初始化。

![enter image description here](https://lh3.googleusercontent.com/-e5T_6_feghs/W87q48fykuI/AAAAAAAAAGs/PSRccyXZQFsYGO6iE4tJflzsNzbkfUFBwCLcBGAs/s0/WeightInit_about_activation_function.jpg "WeightInit_about_activation_function.jpg")
图2   不同激活函数的参数初始化方法

  默认情况下,fully_connected() 函数使用 Xavier 初始化(具有统一的分布)。 你也可以通过使用如下所示的variance_scaling_initializer() 函数来将其更改为 He 初始化[4]:

he_init = tf.contrib.layers.variance_scaling_initializer()
hidden1 = tf.layers.dense(X, n_hidden1, activation=tf.nn.relu,kernel_initializer=he_init, name="hidden1")

疑问,文献[4]中说 “He 初始化只考虑了扇入,而不是像 Xavier 初始化那样扇入和扇出之间的平均值。”,但是从图2中公式来看,He初始化(对应这ReLU激活函数)也是同时考虑了扇入和扇出的呀。这是为何?

使用非饱和激活函数

  Glorot 和 Bengio 在 2010 年的论文中的一个见解是,梯度消失/爆炸部分是由于激活函数的选择不好造成的。 在那之前,大多数人都认为,如果大自然选择在生物神经元中使用 sigmoid 激活函数,它们必定是一个很好的选择。 但事实证明,其他激活函数在深度神经网络中表现得更好,特别是 ReLU 激活函数,主要是因为它对正值不会饱和(也因为它的计算速度很快)[4]。

  不幸的是,ReLU激活函数并不完美。 它有一个被称为 “ReLU 死区” 的问题:在训练期间,如果神经元的权重得到更新,并且使得神经元输入的加权和为负时,则它将一直输出 0(ReLU函数的梯度为0),神经元不可能恢复生机。在某些情况下,你可能会发现你网络的一半神经元已经死亡,特别是如果你使用大学习率 [4]。

  为了解决这个问题,你可能需要使用 ReLU 函数的一个变体,比如 leaky ReLU。这个函数定义为 L e a k y R e L U α ( z ) = m a x ( α z , z ) LeakyReLU_α(z)= max(αz,z) LeakyReLUα(z)=max(αzz)(见图3)。超参数α定义了函数“leaks”的程度:它是z < 0时函数的斜率,通常设置为 0.01。这个小斜坡确保 leaky ReLU 永不死亡,他们可能会长期昏迷,但他们有机会最终醒来。事实上,设定α= 0.2(巨大 leak)似乎导致比α= 0.01(小 leak)更好的性能。他们还评估了随机化 leaky ReLU(RReLU),其中α在训练期间在给定范围内随机挑选,并在测试期间固定为平均值。它表现相当好,似乎是一个正则项(减少训练集的过拟合风险)。最后,他们还评估了参数 leaky ReLU(PReLU),其中α被授权在训练期间被学习(而不是超参数,它变成可以像任何其他参数一样被反向传播修改的参数)。据报道这在大型图像数据集上的表现强于 ReLU,但是对于较小的数据集,其具有过度拟合训练集的风险 [4]。

![enter image description here](https://lh3.googleusercontent.com/-PKheMt9KTEI/W8_I8Pc_fAI/AAAAAAAAAG8/zCnftIQWoL0oEp5IM5JANzdwvicRqrK2QCLcBGAs/s0/leakyReLU.png "leakyReLU.png")
图3   Leaky ReLU

  最后,Djork-Arné Clevert 等人在 2015 年的一篇论文中提出了一种称为指数线性单元(exponential linear unit,ELU)的新的激活函数(见图4),在他们的实验中表现优于所有的 ReLU 变体:训练时间减少,神经网络在测试集上表现的更好 [4]。

![enter image description here](https://lh3.googleusercontent.com/-6T6P6f9SwtM/W8_JBtYUjrI/AAAAAAAAAHE/OvG_B8zoM_s1Mitq-DpzgTQZgOaQTg7UgCLcBGAs/s0/elu.png "elu.png")
图4   ELU activation function

  ELU 激活函数的主要缺点是计算速度慢于 ReLU 及其变体(由于使用指数函数),但是在训练过程中,这可以换来更快的收敛速度。 然而,在测试期间,ELU 网络将比 ReLU 网络慢。

  那么你应该使用哪个激活函数来处理深层神经网络的隐藏层? 一般 ELU > leaky ReLU(及其变体) > ReLU > tanh > sigmoid,详见文献[4]。

TensorFlow 提供了一个可以用来建立神经网络的elu()函数。 调用fully_connected()函数时,只需设置activation_fn参数即可:

from tensorflow.contrib.layers import fully_connected
hidden1 = fully_connected(X, n_hidden1, activation_fn=tf.nn.elu)

TensorFlow 没有针对 leaky ReLU 的预定义函数,但是很容易定义:

import tensorflow as tf
from tensorflow.contrib.layers import fully_connected
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)
批量标准化

  尽管使用 He初始化和 ELU(或任何 ReLU 变体)可以显著减少训练开始阶段的梯度消失/爆炸问题,但不保证在训练期间问题不会回来。

  在 2015 年的一篇论文中,Sergey Ioffe 和 Christian Szegedy 提出了一种称为批量标准化(Batch Normalization,BN)的技术来解决梯度消失/爆炸问题 [4]。(论文名称:《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》)

  BatchNorm就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的,关于BN的思想在文献 [5] 中已经描述的很清楚了(文献[4]描述的不是很清楚),我这里不再赘述。

  作者证明,这项技术大大改善了他们试验的所有深度神经网络。梯度消失问题大大减少了,他们可以使用饱和激活函数,如 tanh 甚至 sigmoid 激活函数。网络对权重初始化也不那么敏感。他们能够使用更大的学习率,显著加快了学习过程。批量标准化也像一个正则化项一样,减少了对其他正则化技术的需求(如 dropout)[4]。

  然而,批量标准化的确会增加模型的复杂性,您可能会发现,训练起初相当缓慢,而渐变下降正在寻找每层的最佳尺度和偏移量,但一旦找到合理的好值,它就会加速 [4]。

代码示例1
# He初始化,ELU激活函数
# Batch⧸⧸Batch⧸⧸ Normalization, 批量标准化
# 梯度裁剪
from functools import partial
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_datadatapath = "/home/xiajun/res/MNIST_data"
mnist = input_data.read_data_sets(datapath, validation_size=0, one_hot=True)if __name__ == '__main__':n_inputs = 28 * 28n_hidden1 = 300n_hidden2 = 100n_outputs = 10batch_norm_momentum = 0.9learning_rate = 0.01X = tf.placeholder(tf.float32, shape=(None, n_inputs), name = 'X')y = tf.placeholder(tf.int64, shape=None, name = 'y')training = tf.placeholder_with_default(False, shape=(), name = 'training')#给Batch norm加一个placeholderwith tf.name_scope("dnn"):he_init = tf.contrib.layers.variance_scaling_initializer()#对权重的初始化my_batch_norm_layer = partial(tf.layers.batch_normalization,training=training,momentum=batch_norm_momentum)my_dense_layer = partial(tf.layers.dense,kernel_initializer=he_init)hidden1 = my_dense_layer(X, n_hidden1, name='hidden1')bn1 = tf.nn.elu(my_batch_norm_layer(hidden1))hidden2 = my_dense_layer(bn1, n_hidden2, name='hidden2')bn2 = tf.nn.elu(my_batch_norm_layer(hidden2))logists_before_bn = my_dense_layer(bn2, n_outputs, name='outputs')logists = my_batch_norm_layer(logists_before_bn)with tf.name_scope('loss'):xentropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=y, logits=logists)loss = tf.reduce_mean(xentropy, name='loss')# 训练操作1, ok'''with tf.name_scope('train'):optimizer = tf.train.GradientDescentOptimizer(learning_rate)training_op = optimizer.minimize(loss)'''# 训练操作2, ok''with tf.name_scope("train"):optimizer = tf.train.GradientDescentOptimizer(learning_rate)extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)with tf.control_dependencies(extra_update_ops):training_op = optimizer.minimize(loss)''# 梯度裁剪, ok'''threshold = 1.0optimizer = tf.train.GradientDescentOptimizer(learning_rate)grads_and_vars = optimizer.compute_gradients(loss)capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var) for grad, var in grads_and_vars]  # 将梯度裁剪到 -1.0 和 1.0 之间training_op = optimizer.apply_gradients(capped_gvs)'''with tf.name_scope("eval"):correct = tf.nn.in_top_k(logists, y, 1)accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))init = tf.global_variables_initializer()saver = tf.train.Saver()n_epoches = 20batch_size = 200
# 注意:由于我们使用的是 tf.layers.batch_normalization() 而不是 tf.contrib.layers.batch_norm()(如本书所述),
# 所以我们需要明确运行批量规范化所需的额外更新操作(sess.run([ training_op,extra_update_ops], ...)。extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)with tf.Session() as sess:init.run()for epoch in range(n_epoches):for iteraton in range(mnist.train.num_examples//batch_size):X_batch, y_batch = mnist.train.next_batch(batch_size)y_batch = np.argmax(y_batch, 1)# 训练操作1 用下面的sess.run# sess.run([training_op, extra_update_ops], feed_dict={training: True, X: X_batch, y: y_batch})# 训练操作2 用下面的sess.runsess.run(training_op, feed_dict={training: True, X: X_batch, y: y_batch})y_batch = np.argmax(mnist.test.labels, 1)accuracy_val = accuracy.eval(feed_dict={X: mnist.test.images, y: y_batch})print(epoch, 'Test accuracy:', accuracy_val)

使用BN优化时的准确率:

0 Test accuracy: 0.8815
1 Test accuracy: 0.9044

18 Test accuracy: 0.9651
19 Test accuracy: 0.9652

这对 MNIST 来说不是一个很好的准确性。 当然,如果你训练的时间越长,准确性就越好,但是由于这样一个浅的网络,批量范数和 ELU 不太可能产生非常积极的影响:它们大部分都是为了更深的网络而发光[4]。

梯度裁剪

  减少梯度爆炸问题的一种常用技术是在反向传播过程中简单地剪切梯度,使它们不超过某个阈值。 这就是所谓的梯度裁剪。一般来说,人们更喜欢批量标准化,但了解梯度裁剪以及如何实现它仍然是有用的[4]。

threshold = 1.0  # threshold是可以调整的超参数optimizer = tf.train.GradientDescentOptimizer(learning_rate)
grads_and_vars = optimizer.compute_gradients(loss)
capped_gvs = [(tf.clip_by_value(grad, -threshold, threshold), var)for grad, var in grads_and_vars]  # 将梯度裁剪到 -1.0 和 1.0 之间
training_op = optimizer.apply_gradients(capped_gvs)

将上述梯度裁剪代码添加到代码示例1中即可检验其效果。

使用梯度裁剪时的准确率:

0 Test accuracy: 0.6869
1 Test accuracy: 0.7291

18 Test accuracy: 0.8823
19 Test accuracy: 0.8957

参考文献

[1] 梯度下降
[2] 梯度下降法和最速下降法的细微差别
[3] 反向传播算法
[4] hands_on_Ml_with_Sklearn_and_TF.第10章.训练深层神经网络
[5] 【深度学习】深入理解Batch Normalization批标准化

这篇关于【深度学习笔记1.2】梯度消失与梯度爆炸的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1035268

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

usaco 1.2 Palindromic Squares(进制转化)

考察进制转化 注意一些细节就可以了 直接上代码: /*ID: who jayLANG: C++TASK: palsquare*/#include<stdio.h>int x[20],xlen,y[20],ylen,B;void change(int n){int m;m=n;xlen=0;while(m){x[++xlen]=m%B;m/=B;}m=n*n;ylen=0;whi

usaco 1.2 Name That Number(数字字母转化)

巧妙的利用code[b[0]-'A'] 将字符ABC...Z转换为数字 需要注意的是重新开一个数组 c [ ] 存储字符串 应人为的在末尾附上 ‘ \ 0 ’ 详见代码: /*ID: who jayLANG: C++TASK: namenum*/#include<stdio.h>#include<string.h>int main(){FILE *fin = fopen (

usaco 1.2 Milking Cows(类hash表)

第一种思路被卡了时间 到第二种思路的时候就觉得第一种思路太坑爹了 代码又长又臭还超时!! 第一种思路:我不知道为什么最后一组数据会被卡 超时超了0.2s左右 大概想法是 快排加一个遍历 先将开始时间按升序排好 然后开始遍历比较 1 若 下一个开始beg[i] 小于 tem_end 则说明本组数据与上组数据是在连续的一个区间 取max( ed[i],tem_end ) 2 反之 这个

usaco 1.2 Transformations(模拟)

我的做法就是一个一个情况枚举出来 注意计算公式: ( 变换后的矩阵记为C) 顺时针旋转90°:C[i] [j]=A[n-j-1] [i] (旋转180°和270° 可以多转几个九十度来推) 对称:C[i] [n-j-1]=A[i] [j] 代码有点长 。。。 /*ID: who jayLANG: C++TASK: transform*/#include<

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss