【深度学习笔记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

相关文章

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”

Unity新手入门学习殿堂级知识详细讲解(图文)

《Unity新手入门学习殿堂级知识详细讲解(图文)》Unity是一款跨平台游戏引擎,支持2D/3D及VR/AR开发,核心功能模块包括图形、音频、物理等,通过可视化编辑器与脚本扩展实现开发,项目结构含A... 目录入门概述什么是 UnityUnity引擎基础认知编辑器核心操作Unity 编辑器项目模式分类工程

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?

Python学习笔记之getattr和hasattr用法示例详解

《Python学习笔记之getattr和hasattr用法示例详解》在Python中,hasattr()、getattr()和setattr()是一组内置函数,用于对对象的属性进行操作和查询,这篇文章... 目录1.getattr用法详解1.1 基本作用1.2 示例1.3 原理2.hasattr用法详解2.

Mybatis-Plus 3.5.12 分页拦截器消失的问题及快速解决方法

《Mybatis-Plus3.5.12分页拦截器消失的问题及快速解决方法》作为Java开发者,我们都爱用Mybatis-Plus简化CRUD操作,尤其是它的分页功能,几行代码就能搞定复杂的分页查询... 目录一、问题场景:分页拦截器突然 “失踪”二、问题根源:依赖拆分惹的祸三、解决办法:添加扩展依赖四、分页

深度解析Spring Security 中的 SecurityFilterChain核心功能

《深度解析SpringSecurity中的SecurityFilterChain核心功能》SecurityFilterChain通过组件化配置、类型安全路径匹配、多链协同三大特性,重构了Spri... 目录Spring Security 中的SecurityFilterChain深度解析一、Security