深度学习 《LSTM和GRU模型》

前言:
前面我们学习了标准的单向单层和单向多层的RNN,这一博文我来介绍RNN的改进版本LSTM和GRU,至于为什么有这个改进的方案,以及如何理解它们,我会尽量用最通俗的语言俩表达。
学习自博客https://colah.github.io/posts/2015-08-Understanding-LSTMs/ 以及Andrew Ng的DL视频,我脑子比较笨,学了一整天才搞明白LSTM的设计初衷,于是赶紧整理下来免得遗忘,为了可快速书写,我就不自己画图了,截图来自上述博客。
以下全是通俗的话。

一:简单回顾下RNN
为了简单期起见,我们使用单层单向的RNN示意图,如下:
在这里插入图片描述

RNN的结构就是,每一时刻的输出都源自于当前时刻的输入以及和之前的输出信息的加权激活,每一层都是单个tanh函数激活,一旦得到输入,本单元就会不假思索,啥也不保留,直接和之前的信息做线性变换再tanh激活,激活的结果直接输出,这个就是RNNCell的工作机制。
总而言之,RNNCell能使用之前时刻的信息,参与当前任务的计算。
在处理一些简单的序列问题的时候,有时候我们确实想要根据之前时刻的输出来执行当前的任务,举个简单的例子,当存在这么一句话, “中国的航天之父是钱学森”,当模型学到了这句话,下次我们再输入这样话,输入到“航天之父”的时候,那么模型就会大概率输出“钱学森”。所以处理简单的任务还是和有用的,为什么说是简单的呢?当它处理相关信息和当前信息位置距离还不是很远的时候,确实就很管用。
在这里插入图片描述

但是当它处理序列较长,相关信息的距离比较远的时候就会显得很吃力,序列就得变得长,相关信息比较远,就需要增加网络深度,然而我们在模型中能看出来,标准的RNN比较 “短视”,它在某时刻执行任务都是只去看到上一时刻的输出来作为本时刻任务一个决策条件,时间越远,也就是距离越远,信息越模糊,影响程度就越小,因为每个RNNCell就是一个非常非常简单纯粹的计算单元,计算后啥也没保留住,它就只知道给它一个输入值和上一时刻的输出值,它就立马激活输出,一旦它需要某一段比较远的时刻的值(这个信息很关键)作为本时刻的参考数据,诶!,懵逼了吧,数据太久了,影响快消失没了,找不到了吧,那就没办法了,所以标准的RNN考虑的序列时间范围是相当有限的。
在这里插入图片描述

举个例子,“我出生在中国,………,(很长一个句子),我能说流利的中文”,很难有个RNN网络能把最后的“中文”预测正确,因为最关键的信息在很远之前的“中国”,也就是一旦相关性的信息位置距离本次任务太久远,就无法很好的工作了,这里有个比较专业的说法就是“RNN不擅长捕获长期的依赖关系,适合于捕获短期的依赖关系,它只能得到附近的时刻影响”。

产生上述问题的原因就是梯度消失,我们在之前的博文中推导了反向传播的公式,我们会发现,当网络很深的时候,一方面从最后一层到最前面一层的导数是不断相乘的计算,因此,存在梯度消失和梯度爆炸的可能性,另一方面还受到激活函数的影响,Sigmoid函数和tanh函数会出现梯度为0 的区域,一旦发生了梯度消失,越前面的网络的参数越得不到更新,因此越后面的网络越难去和前面的网络层联系上,这个在BP神经网络里面也是一样的存在的问题,在RNN里呢就是越后面的时刻越难联系上之前的时刻,因此导致前面的关键信息和后面的任务脱节。

然而,办法总比困难多,LSTM和一些变种可以解决这个问题,我们需要通过某种方式记住每个时刻的一些东西,这样在以后时刻用起来的时候我们就能随时去取用,起码是想用的时候信息还在,不至于消失了啊。

二:LSTM
还是拿那句话举例,“我出生在中国,………,(很长一个句子),我能说流利的中文”,当我们听到了“出生在中国”这个信息的时候,我们的大脑其实已经把这个信息通过某个途径留存了下来,之后巴拉巴拉…………听了一堆,最后预测他能说“能说流利的XXXX”的时候,我们就会用到之前存储的那个关键信息“出生在中国”来预测出很大概率是“中文”。

我们重新审视RNNCell的结构,将内部的逻辑画出来如下:
在这里插入图片描述

除了梯度消失,造成RNN无法得到长期依赖的原因还有就是,它仅仅是一个灰常灰常简单的计算单元,唯一做的事情就是将上一时刻的输出和本时刻的输入做个tanh激活就直接传给了下一层了,完了之后自己什么也没有留下,特别的呆萌和朴素,而且很“短视”,最多只看清楚前面距离本时刻最近的某几时刻的输出,时刻越远,信息越模糊,也就是记忆能力有限吧。

于是LSTM就针对这些点,形成了自己独特的结构:先给出图示,再通俗深入一下,最后再给出数学的计算过程。
在这里插入图片描述

扫描二维码关注公众号,回复: 11989122 查看本文章

我知道肯定有一部分人,看到这个图肯定会相当一会儿是不明白为什么要这么设计的?要是直接给出数学计算过程,大家都能懂计算,但是为什么要这么设计呢?比如,怎么每个Cell的输出多了一条线呢?计算中多了三个sigmoid函数是干嘛的?怎么又两个tanh激活函数?下面我用通俗的话描述一遍,毕竟思考了大半天了的(脑子比较笨,花了大半天,聪明的人可以尽情嘲讽我)。

下面这几段通俗话相当关键,上述我们也说了标准的RNN就是个简单的计算单元,不假思索的接受输入,不假思索的计算,完了之后片叶不沾身什么也没有留下,最后再不假思索的给后面激活输出了,请注意这三个“不假思索”和一个“片叶不沾身”。LSTM呢就是专门针对这四个成语点改进的。

1)首先最重要的,先解决无法记忆问题,那就多设计一个结构呗,这个结构作为本节点的存储,也就是说,类似于神经元,接收到所有输入后,在总得形成点儿自己东西吧,这个形成的东西,姑且称之为记忆值,这就是图中多出来的那个C,这个C呢是可以传递给下一时刻的,因为你不知道什么时候就可能被使用了,也许这个记忆值会被传递到很远,这时候Cell就不再 “片叶不沾身”了。但是一方面也很慷慨,不吝啬自己已经得到的东西,依然把自己得到的记忆值分享输出到下一层去,横穿Cell,至于接受不接受是一层的事儿。

2)对于上一时刻的记忆,我也不是全盘照收啊,给增加个门通道,然后选择性的接受,看我训练的心情,心情好了,大门完全敞开,上一时刻的记忆值全部过来,心情不好,接收上一时刻记忆值的部分,如果心情特别差,门全部给关了,不好意思,相当于直接抛弃和忽略了上一时刻的记忆值,这个门就叫起个名做“遗忘门”,起到一个过滤的作用,得到上一时刻记忆有什么用呢,就是为了参与本时刻的模型计算嘛,万一通过模型训练,这个值来自很远的某些时刻呢,岂不是得到了很远时刻的信息输入,岂不就是存在解决长期依赖问题的可能性么。

3)对于当前的输入(包含上一时刻的激励输出和当前时刻的数据输入),该做激活就做激活,但是呢激活完了后我也不是全盘接受,我也给设置个门,也是看心情,选择性地通过,也起到了过滤的作用,这个门叫做“输入门”。通过门后去哪里呢?别着急输出嘛,等一下,先来和经过过滤后的上一次时刻记忆值进行融合,形成一下我当前时刻的记忆值。这就是一个记忆值生成的过程。也就是说,本单元的Cell进行了一些思考,存储形成了自己本时刻的记忆值。这个叫做 “记忆更新”。

4)总该轮到我Cell输出了吧?别急,先弄清楚你输出啥啊?之前说了标准RNN没办法获取到久远的Cell的信息,我们现在不已经产生了这个信息了么,第一段也讲了,不吝啬,那就往外输出啊。没错,也就是说,产生的C记忆值就直接往下一时刻送了,至于下一时刻接受与否或者接受多少,由下一时刻决定,反正我是输出了。

除此之外,输出记忆值,只是为了能让将来不知道的某一时刻得到该时刻的值,这个路径是人为加上去的啊,解决长期依赖问题的嘛,没有它,顶多就是后面的Cell没办法获得长期依赖嘛,但是如果仅仅设计这么一条路,取消后,层与层之间就完全断了啊,那是不行的。因此,主流程还是得生成一个激活输出的啊,这个跟标准的RNN一样,隐藏层的激活输出是不能少的,这才是传递的主流程,向下输出记忆值单纯就是打辅助的。总结下就是,也就是在标准RNN的基础上多了一条记忆值的输出通道,这就解释了为啥是两条线的原因。

5)激活输出怎么生成的呢?为什么要用生成的记忆值去决定激活输出呢?因为本时刻的X输入值已经做过一次tanh的激活了,再用X去做激活就有点过度计算了的感觉,略微显得自己笨笨哒,又因为生成的记忆值C已经包含了输入X的和上一时刻的tanh激活输出,又混杂了上一时刻的记忆值,因此信息会显得更加饱满,用这个更加饱满的信息,去做激活可能效果会更好吧,总结一下,Cell不再“片叶不沾身了”了,每次都是先得到自己的记忆值(自己的东西),再去决定向外的激活输出是多少,但是一方面也很慷慨,不吝啬自己得到的东西,依然把自己得到的记忆值输出到下一层去,至于接受不接受是一层的事儿。

6)此时呢,对于输出我得到了,但是真正王爱输出多少呢?我又设置了个门,还是看心情,开心了,全部输出,不开心了,部分通过,心情很差的时候,就大门关闭,别想输出了。这个时候呢,这个门就叫做“输出门”。

至此,讲故事结束,故事里面已经把上图LSTM的大致计算过程其实讲了遍,也顺带把LSTM的关键结构顺道讲了,不知道大家有没有看懂。剩下一些细枝末节,我想用图和数学来展示。

1) 门是啥?
在这里插入图片描述

这里说的门,在数学上就是一个比例的意思,一个Sigmoid函数计算后是0~1之间的数字,乘上某个信息值,不就是起到了一个过滤的作用么,如果Sigmoid函数输出是1,信息全量通过;如果Sigmoid函数输出是0,全部关闭,忽略信息;其他的情况就是适当通过一些信息。(多说一点,大于1 可以做放大器,小于1可做抑制器,等于1叫做单位函数,对频域信号的某些波段乘以极小值,就起到了滤波的作用,数字滤波器)

2) Cell记忆,或者叫做Cell状态,是每时每刻计算到的一个值,横贯整个Cell
在这里插入图片描述

通过一个遗忘门控制它被下一个单元所能接受的程度。简单举个现实的例子,有的人就是喜欢说废话,我就是要屏蔽他的话,忽略掉之前产生的记忆信息,屏蔽多少看实际训练的结果。或者,发现之前的信息没有价值,这一层就直接给全部屏蔽。

3) 遗忘门
在这里插入图片描述

这就是遗忘门的部分,计算过程如图所示,是用来对上一时刻的记忆值进行程度选择的。

4) 根据所有的输出进行当前的任务计算,以及生成输入门。
在这里插入图片描述

这里先把上一时刻的输入和当前时刻的输入X做激活运算,但是同时生产了一个输入门,用来对当前的计算结果进行通量选择。也就是这哥们控制当前输入的重要程度,如果一个人一直说废话,我就每时每刻屏蔽他,如果是大神讲课,我就每时每刻全量接受过来。 再比如,遇到停用词,基本没有什么作用的话,当前输入就可以屏蔽。

5) 更新Cell状态
在这里插入图片描述

前面也说了,LSTM中的Cell聪明的很,会先自己留下点东西,把任务计算的结果先留下给自己,再分享给下一层。这里需要更新当前时刻的Cell的记忆值,通过计算式,我们能发现,当前Cell的计算式是输入门和遗忘门共同控制的,遗忘门控制上一时刻的记忆值,输入门控制当前时刻的任务产生的记忆值,二者分别经过各自的门的通量过滤后,累加起来就是当前细胞的记忆值。
可以画出门逻辑电路图:

在这里插入图片描述

6) 最后就是激活输出了
在这里插入图片描述

这里还整了个输出门,用已经产生的Cell记忆值去产生这个激活输出值,还要经输出门的通量过滤一番,如果一个人前言不接后语,这哥们就不让你的废话传播了,免得影响后面的反应。最后就全部结束了。

这里我分别用了通俗的话,和截图图示算式进行了讲解。

三:GRU
这个作为LSTM的改进版本,目前流行的RNN使用可能通常就是LSTM和GRU了,GRU是在几年前提出来,LSTM是在20几年前提出来的,可想而知时间的跨度还是很大的。那么他改进了什么东西呢?有什么特点呢?我们一起来看看结构

在这里插入图片描述

第一个改动:
将遗忘门和输入门合二为一,作者可能觉得太多的门感觉太繁琐了。
这样一来,门的数目变少了一个,计算的程序也少了一次门的计算。

第二个改动:
将记忆值和隐藏层的激活输出合二为一,LSTM中是输出两条线路(一个是记忆值,一个是隐藏层的激活输出),在GRU中做了个加法后合而为一统一对外输出。诶?等下,这样怎么传递记忆值呢?且看图中所示,下一层的Cell接受到了上一层的输出后,copy了三份,一份直接跟LSTM的记忆路线一样,横穿Cell,用的是比例加权输出(公式最后条),这条路上没有门,所以没有长期依赖功能上的丢失,依然能解决长期依赖的问题。

另外两份与X共同作用,产生门,产生输出。特征上也没有没有损失。看图中结构,结构变得简单了一些。

第三个改动:
tanh函数也少了,最后输出的时候不再是LSTM的tanh激活输出,而是按照Zt和(1-Zt)的比例输出,计算变得少了。

GRU的运算过程是:
1) 先使用上一时刻的输出和当前时刻的输入X做出两个独立的门出来。一个是Zt(重置门),一个Rt(更新门),两个门都是在0~1之间的值,代表过滤程度。

2) 先生成临时激活值h_head,可以发现Rt越大,之前的信息就带入越多。Rt是为了h_head服务的,所以Rt控制之前时刻的输出值对当前计算h_head的参与程度。

3) 最后加权生成最后的输出,可以发现Zt越大,(1-Zt)越小,也就是对之前的信息带入越少,当前的计算带入越多。比例综合刚好是1。计算情况就是,要么全部输出当前值且忽略之前值,要么全部忽略当前值且全部输出之前值。两个极端。所以Zt是控制最后输出时候两个时刻值的占比(有一定的比例关系)。

所以总体而言,结构变得简单了。计算过程变少了。总体功能特征上没有什么损失。

至于在Pytorch上怎么实现,由于篇幅有点长了,后面还有几个要写的点,下下次补上在Pytorch上实现简单的LSTM和GRU的代码。

最后骚话一下:我觉得学习就是要知道所以然,这样才能举一反三,不然创新都是别人的,人家出了个啥就屁颠屁颠去学,这样也不好,学会了大神思考方式,才能试着取去走自己的路嘛。

第一次写,写的不好的地方请尽情指正我。我会改正的。

猜你喜欢

转载自blog.csdn.net/qq_29367075/article/details/108958791