循环神经网络教程第三部分-BPTT和梯度消失

2023-10-11 13:10

本文主要是介绍循环神经网络教程第三部分-BPTT和梯度消失,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者:徐志强
链接:https://zhuanlan.zhihu.com/p/22338087
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本篇是 循环神经网络教程的第三部分。

在前一篇教程中,我们从头开始实现了RNN,但并没有深入到BPTT如何计算梯度的细节中去。在本部分,我们将对BPTT做一个简短的介绍,并解释它和传统的反向传播有什么不同。然后,我们会试着去理解梯度消失问题,它导致了LSTM和GRU这两个目前在NLP中最流行、最强大的模型的发明。梯度消失问题最初由Sepp Hochreiter在1991年发现,在最近由于深度结构的使用又引起了人们的关注。

为了完全理解本部分的内容,我建议先熟悉一下偏微分和基本的反向传播是怎么工作的。如果你不熟悉的话,在这里,这里和这里可以找打很好的资料,它们的难度逐级递增。

随时间的反向传播(BPTT)

让我们先迅速回忆一下RNN的基本公式,注意到这里在符号上稍稍做了改变(o变成\hat{y} ),这只是为了和我参考的一些资料保持一致。
s_{t}=tanh(Ux_{t}+Ws_{t-1})
\hat{y}_{t}=softmax(Vs_{t})

同样把损失值定义为交叉熵损失,如下:
E_{t}(y_{t}, \hat{y}_{t})=-y_{t}log(\hat{y}_{t})
E(y, \hat{y})=\sum_{t}{E_{t}(y_{t}, \hat{y}_{t})}= -\sum_{t}{y_{t}log\hat{y}_{t}}
这里,y_{t}表示时刻t正确的词,\hat{y}_{t}是我们的预测。通常我们会把整个句子作为一个训练样本,所以总体错误是每一时刻的错误的加和。

我们的目标是计算错误值相对于参数U, V, W的梯度以及用随机梯度下降学习好的参数。就像我们要把所有错误相加一样,我们同样会把每一时刻针对每个训练样本的梯度值相加:\frac{\partial{E}}{\partial{W}}=\sum_{t}{\frac{\partial{E}_{t}}{\partial{W}}}
为了计算梯度,我们使用链式求导法则,主要是用反向传播算法往后传播错误。下文使用E_{3}作为例子,主要是为了描述方便。

\frac{\partial{E_{3}}}{\partial{V}} =\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}} \frac{\partial{\hat{y}_{3}}}{\partial{V}}=\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}} \frac{​{\partial{\hat{y}_{3}}}}{\partial{z_{3}}}\frac{\partial{z_{3}}}{\partial{V}}=(\hat{y}_{3}-y_{3})\otimes s_{3}
上面z_{3}=Vs_{3}\otimes 是向量的外积。如果你不理解上面的公式,不要担心,我在这里跳过了一些步骤,你可以自己尝试来计算这些梯度值。这里我想说明的一点是梯度值只依赖于当前时刻的结果\hat{y}_{3}, y_{3}, s_{3}。根据这些,计算V的梯度就只剩下简单的矩阵乘积了。

但是对于梯度\frac{\partial{E}_{3}}{\partial{W}} 情况就不同了,我们可以像上面一样写出链式法则。
\frac{\partial{E_{3}}}{\partial{W}}=\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}\frac{\partial{s_{3}}}{\partial{W}}

注意到这里的s_{3}=tanh(Ux_{t}+Ws_{2})依赖于s_{2}s_{2}依赖于Ws_{1},等等。所以为了得到W的梯度,我们不能将s_{2}看作常量。我们需要再次使用链式法则,得到的结果如下:
\frac{\partial{E_{3}}}{\partial{W}}=\sum_{k=0}^{3}{\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}\frac{\partial{s_{3}}}{\partial{s_{k}}}\frac{\partial{s_{k}}}{\partial{W}}}

我们把每一时刻得到的梯度值加和,换句话说,W在计算输出的每一步中都使用了。我们需要通过将t=3时刻的梯度反向传播至t=0时刻。

注意到这里和我们在深度前向神经网络中使用的标准反向传播算法是一致的,关键不同在于我们把每一时刻针对W的不同梯度做了加和。在传统神经网络中,不需要在层之间共享参数,就不需要做任何加和。在我看来,BPTT是应用于展开的RNN上的标准反向传播的另一个名字。就像反向传播一样,你也可以定义一个反向传递的delta向量,例如,\delta ^{(3)}_{2}=\frac{\partial{E}_{3}}{z_{2}}                          =\frac{\partial{E}_{3}}{\partial{s_{3}}} \frac{\partial{s}_{3}}{\partial{s_{2}}} \frac{\partial{s}_{2}}{\partial{z_{2}}} ,其中z_{2}=Ux_{2}+Ws_{1}

在代码中,BPTT的一个简易实现如下:

def bptt(self, x, y):T = len(y)# Perform forward propagationo, s = self.forward_propagation(x)# We accumulate the gradients in these variablesdLdU = np.zeros(self.U.shape)dLdV = np.zeros(self.V.shape)dLdW = np.zeros(self.W.shape)delta_o = odelta_o[np.arange(len(y)), y] -= 1.# For each output backwards...for t in np.arange(T)[::-1]:dLdV += np.outer(delta_o[t], s[t].T)# Initial delta calculation: dL/dzdelta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))# Backpropagation through time (for at most self.bptt_truncate steps)for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:# print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)# Add to gradients at each previous stepdLdW += np.outer(delta_t, s[bptt_step-1])              dLdU[:,x[bptt_step]] += delta_t# Update delta for next step dL/dz at t-1delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)return [dLdU, dLdV, dLdW]

这会让你明白为什么标准RNN很难训练:序列会变得很长,可能有20个词或更多,因而就需要反向传播很多层。实践中,很多人会把发现传播截断至几步。

梯度消失问题

在教程前一部分,我提到RNN很难学到长范围的依赖——相隔几步的词之间的交互。这是有问题的因为英语中句子的意思通常由相距不是很近的词来决定:“The man who wore a wig on his head went inside”。这个句子讲的是一个男人走了进去,而不是关于假发。但是普通的RNN不可能捕捉这样的信息。要理解为什么,让我们先仔细看一下上面计算的梯度:
\frac{\partial{E_{3}}}{\partial{W}}=\sum_{k=0}^{3}{\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}\frac{\partial{s_{3}}}{\partial{s_{k}}}\frac{\partial{s_{3}}}{\partial{W}}}
注意到\frac{\partial{s_{3}}}{\partial{s_{k}}}也需要使用链式法则,例如,\frac{\partial{s_{3}}}{\partial{s_{1}}}=\frac{\partial{s_{3}}}{\partial{s_{2}}}\frac{\partial{s_{2}}}{\partial{s_{1}}}。注意到因为我们是用向量函数对向量求导数,结果是一个矩阵(称为Jacobian Matrix),矩阵元素是每个点的导数。我们可以把上面的梯度重写成:
\frac{\partial{E_{3}}}{\partial{W}}=\sum_{k=0}^{3}{\frac{\partial{E_{3}}}{\partial{\hat{y}_{3}}}\frac{\partial{\hat{y}_{3}}}{\partial{s_{3}}}(\prod_{j=k+1}^{3} \frac{\partial{s_{j}}}{\partial{s_{j-1}}})\frac{\partial{s_{k}}}{\partial{W}}}
可以证明上面的Jacobian矩阵的二范数(可以认为是一个绝对值)的上界是1。这很直观,因为激活函数tanh把所有制映射到-1和1之间,导数值得界限也是1:

你可以看到tanh和sigmoid函数在两端的梯度值都为0,接近于平行线。当这种情况出现时,我们就认为相应的神经元饱和了。它们的梯度为0使得前面层的梯度也为0。矩阵中存在比较小的值,多个矩阵相乘会使梯度值以指数级速度下降,最终在几步后完全消失。比较远的时刻的梯度值为0,这些时刻的状态对学习过程没有帮助,导致你无法学习到长距离依赖。消失梯度问题不仅出现在RNN中,同样也出现在深度前向神经网中。只是RNN通常比较深(例子中深度和句子长度一致),使得这个问题更加普遍。

很容易想到,依赖于我们的激活函数和网络参数,如果Jacobian矩阵中的值太大,会产生梯度爆炸而不是梯度消失问题。梯度消失比梯度爆炸受到了更多的关注有两方面的原因。其一,梯度爆炸容易发现,梯度值会变成NaN,导致程序崩溃。其二,用预定义的阈值裁剪梯度可以简单有效的解决梯度爆炸问题。梯度消失出现的时候不那么明显而且不好处理。

幸运的是,已经有一些方法解决了梯度消失问题。合适的初始化矩阵W可以减小梯度消失效应,正则化也能起作用。更好的方法是选择ReLU而不是sigmoid和tanh作为激活函数。ReLU的导数是常数值0或1,所以不可能会引起梯度消失。更通用的方案时采用长短项记忆(LSTM)或门限递归单元(GRU)结构。LSTM在1997年第一次提出,可能是目前在NLP上最普遍采用的模型。GRU,2014年第一次提出,是LSTM的简化版本。这两种RNN结构都是为了处理梯度消失问题而设计的,可以有效地学习到长距离依赖,我们会在教程的下一部分进行介绍。

PS:前两篇教程中的图都没有显示出来,目前不知道是咋回事,希望这一篇能显示出来。

这篇关于循环神经网络教程第三部分-BPTT和梯度消失的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

JAVA中while循环的使用与注意事项

《JAVA中while循环的使用与注意事项》:本文主要介绍while循环在编程中的应用,包括其基本结构、语句示例、适用场景以及注意事项,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录while循环1. 什么是while循环2. while循环的语句3.while循环的适用场景以及优势4. 注意

Python中的异步:async 和 await以及操作中的事件循环、回调和异常

《Python中的异步:async和await以及操作中的事件循环、回调和异常》在现代编程中,异步操作在处理I/O密集型任务时,可以显著提高程序的性能和响应速度,Python提供了asyn... 目录引言什么是异步操作?python 中的异步编程基础async 和 await 关键字asyncio 模块理论

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

poj 2976 分数规划二分贪心(部分对总体的贡献度) poj 3111

poj 2976: 题意: 在n场考试中,每场考试共有b题,答对的题目有a题。 允许去掉k场考试,求能达到的最高正确率是多少。 解析: 假设已知准确率为x,则每场考试对于准确率的贡献值为: a - b * x,将贡献值大的排序排在前面舍弃掉后k个。 然后二分x就行了。 代码: #include <iostream>#include <cstdio>#incl

poj3750约瑟夫环,循环队列

Description 有N个小孩围成一圈,给他们从1开始依次编号,现指定从第W个开始报数,报到第S个时,该小孩出列,然后从下一个小孩开始报数,仍是报到S个出列,如此重复下去,直到所有的小孩都出列(总人数不足S个时将循环报数),求小孩出列的顺序。 Input 第一行输入小孩的人数N(N<=64) 接下来每行输入一个小孩的名字(人名不超过15个字符) 最后一行输入W,S (W < N),用

笔记整理—内核!启动!—kernel部分(2)从汇编阶段到start_kernel

kernel起始与ENTRY(stext),和uboot一样,都是从汇编阶段开始的,因为对于kernel而言,还没进行栈的维护,所以无法使用c语言。_HEAD定义了后面代码属于段名为.head .text的段。         内核起始部分代码被解压代码调用,前面关于uboot的文章中有提到过(eg:zImage)。uboot启动是无条件的,只要代码的位置对,上电就工作,kern

✨机器学习笔记(二)—— 线性回归、代价函数、梯度下降

1️⃣线性回归(linear regression) f w , b ( x ) = w x + b f_{w,b}(x) = wx + b fw,b​(x)=wx+b 🎈A linear regression model predicting house prices: 如图是机器学习通过监督学习运用线性回归模型来预测房价的例子,当房屋大小为1250 f e e t 2 feet^

校验码:奇偶校验,CRC循环冗余校验,海明校验码

文章目录 奇偶校验码CRC循环冗余校验码海明校验码 奇偶校验码 码距:任何一种编码都由许多码字构成,任意两个码字之间最少变化的二进制位数就称为数据检验码的码距。 奇偶校验码的编码方法是:由若干位有效信息(如一个字节),再加上一个二进制位(校验位)组成校验码。 奇校验:整个校验码中1的个数为奇数 偶校验:整个校验码中1的个数为偶数 奇偶校验,可检测1位(奇数位)的错误,不可纠错。

项目实战系列三: 家居购项目 第四部分

购物车 🌳购物车🍆显示购物车🍆更改商品数量🍆清空购物车&&删除商品 🌳生成订单 🌳购物车 需求分析 1.会员登陆后, 可以添加家居到购物车 2.完成购物车的设计和实现 3.每添加一个家居,购物车的数量+1, 并显示 程序框架图 1.新建src/com/zzw/furns/entity/CartItem.java, CartItem-家居项模型 /***