文章出处:
https://zhongqiang.blog.csdn.net/article/details/106299252
1. 写在前面
最近用深度学习做一些时间序列预测的实验, 用到了一些循环神经网络的知识, 而当初学这块的时候,只是停留在了表面,并没有深入的学习和研究,只知道大致的原理, 并不知道具体的细节,所以导致现在复现一些经典的神经网络会有困难, 所以这次借着这个机会又把RNN, GRU, LSTM以及Attention的一些东西复习了一遍,真的是每一遍学习都会有新的收获,之前学习过也没有整理, 所以这次也借着这个机会把这一块的基础内容进行一个整理和总结, 顺便了解一下这些结构底层的逻辑。
当然,这次的整理是查缺补漏, 类似于知识的串联, 一些很基础的内容可能不会涉及到, 这一部分由于篇幅很长,所以打算用三篇基础文章来整理, 分别是重温循环神经网络RNN, 重温LSTM和GRU和重温Seq2Seq与Attention机制。 整理完了这些基础知识 , 然后会总结一篇用于时间序列预测非线性自回归模型的论文,这篇论文用的就是带有双阶段注意力机制的LSTM,后面也会使用keras尝试复现并用于时间序列预测的任务,通过这样的方式,可以把这些基础知识从理论变成实践, 这也是先整理三篇基础文章的原因, 因为复现过程中发现一些细节懵懵懂懂, 所以还是先温习遍基础 😉
第一篇就是重温RNN, 这里面会先从全连接神经网络开始, 看一下神经网络到底长什么样子以及如何进行计算, 然后针对一些特殊的任务说一下全连接神经网络的局限性引出循环神经网络架构, 然后根据这个结构说一些基础知识和运算细节, 并用numpy简单实现一下RNN的前向传播过程, 最后分析一下传统RNN的局限性, 通过反向传播的公式看一下RNN为什么会存在梯度消失和爆炸, 而有了梯度消失为啥又不能捕捉到长期关联, 如何解决梯度消失问题等。 通过解决方法引出LSTM和变体GRU, 再去探索这两个的原理和一些实现细节。
大纲如下:
- 理解RNN? 我们先从全连接神经网络开始
- 关于RNN结构的基础知识和计算细节
- RNN前向传播的numpy实现与RNN的局限性
- 总结
Ok, let’s go!
2. RNN? 我们还是先从神经网络开始说起吧
说到神经网络, 我们肯定是不陌生了, 并且也非常熟悉它的运算过程, 拿我整理Pytorch的时候的一张图再回顾一下神经网络:
上面其实就是全连接网络的一个总体计算过程,左上就是一个全连接神经网络示意图, 一个全连接神经网络有一个输入层, 若干个隐藏层和一个输出层, 它的计算步骤包括前向传播, 计算损失, 反向传播, 更新参数,然后重复这个过程。 具体细节就不再这里展开了, 这种网络功能也是非常强大, 由于激活函数的存在,也善于学习很复杂的非线性关系。
但是有些任务, 比如我们的输入是一个句子: Cat is beautiful! 让这个神经网络进行翻译, 我们一般要这么做, 首先,会把上面这3个单词转成向量的形式,要不然模型不认识, 可以通过one-hot或者embedding等, 然后我们喂入神经网络, 得到输出:
应该是一个这样的过程, 上面这个图得好好理解一下, 这就是如果基于全连接网络的话会是这样的一个图, 这里之所以画成3步,就是为了后面更好的理解循环神经网络, 如果看过吴恩达老师的深度学习, 这里画的是这样的一个图:
这里也拿来做个对比吧, 这个图的话很容易把特征和不同时间步的序列给搞混了, 并且不利于后面和递归神经网络进行对比,所以我把每个单词的翻译给分开了, 分别通过神经网络进行翻译。
但是上面这种网络存在一些问题, 很大的一个问题就是单词和单词之间的翻译孤立起来了, 没有关联了, 但是我们知道句子翻译很大程度上是依赖于上下文的, 如果不看上下文, 很容易把某个词翻译错的。比如我前面的cat换成cats, 后面的is就需要换成are, 但是在上面的神经网络里面, 是学习不到这种词与词之间的关联关系, 所以这种神经网络对于这种时序性的任务不擅长, 也就是说如果我的输入是一串序列,并且这串序列前后之间有关联关系, 比如一个句子, 一段音乐, 一段语音,一段视频, 一段随时间变化的数据(股票,温度)等这样的数据, 如果想用一个网络对这样的数据进行建模, 比如捕捉这些前后的关联关系,全连接神经网络是不行的,什么? 还有CNN?CNN1D确实可以处理一些简单的时间序列数据,卷积神经网络也确实可以采用滑动窗口的那种思想去捕捉局部的一些特征, 但是功能比较受限(长距离依赖没法学习), 于是循环神经网络诞生了。
3. 关于RNN结构的基础知识和计算细节
啥叫循环神经网络呢? 这里的循环到底干什么事情呢? 下面这个就是循环神经网络的图, 通过这个图很容易看到循环吧, 但是对于初学者来说,这个图并不是那么好理解:
其实,虽然这个图不是那么好理解, 那还是这个图能够真正的表示循环神经网络,更能看出一种循环, 简单的说, 循环神经网络在做一件这样的事情:
我们的输入序列不是说有时间的先后关系吗?我们不是说要捕捉不同时间步中输入数据的关联吗? 看看RNN是如何做的:
第二个过程, 前一时间步处理的结果要传递到下一个时间步
所以上面这个过程我们可以用下面的公式表示:
如果是把我上面举得那个例子拿下来的话,就是这样的一个感觉
所以这里要注意一些细节:
不要以为这是很多个全连接神经网络,其实这就是一个神经网络,只不过不同的时间步用了不同的输入而已。
这里的前向传播过程是一气呵成的, 就是在一个时间步的循环中,直接进行每个时间步的前向传播,得到最后的结果。
注意这里的可学习参数W , V , U 不同的时间步里面都使用的这一套参数, 所以这里的参数是共享的, 参数共享有很多好处, 比如减少计算量, 比如特征提取, 也可以让模型更好的泛化, 比如我去年去了北京, 和去年我去了北京, 这两个句子意思一样, 但是文字位置不同,共享的参数有利于学习词义本身而不是每个位置的规则。
这里还要注意几个名词, 第一个就是timesteps, 表示时间步长, 也就是时间序列的长度, 需要循环迭代的次数, 第二个就是input_dim, 这个表示的每个时间步的输入数据有多少个特征, 第三个是units, 这个指的是上面隐藏层有多少个神经单元, 为什么要说这三个名词呢? 因为在使用实际用RNN或者LSTM的时候,这三个是核心参数,后面整理LSTM的时候,会看看keras的LSTM层如何用,那时候会再次看到这三个名词
下面我们把上面按照时间线展开的RNN换一种形式表示,就是把那个圆圈给它再放大放大,进来看看细节:
这个就是RNN按照时间线展开的图了,这里的符号可能和上面表示的不一样,这里我就先不统一符号了,毕竟参考的资料不一样, 如果真懂了运算原理, 就不会在乎符号的问题, 并且这里主要也是说明计算原理,上面这个图是取自吴恩达老师的深度学习课程, 这里的RNN-cell, 可以理解成那个隐藏层, 里面当然很多个隐藏单元, 我们可以看一下这里面的整体计算:
这里与上面不同的是,指明了具体的激活函数g , f 了,这个公式和上面循环神经网络的计算公式一样, 无非是符号换了一下。
下面我们可以基于上面的这个RNN的运算过程, 用numpy简单的写一下。为了看清楚这个过程, 还找了张动图:
动图后面会给出参考链接。
4. RNN前向传播的numpy实现与RNN的局限性
根据上面的图, 我们就用numpy代码简单实现一下RNN的前向传播,这样更容易里面RNN的前向传播过程, 首先,依然是先定义上面细节中的三个名词:timesteps, input_dim和units, 这里我们假设时间步长是4, input_dim是3, units是5, 然后10个样本。 实现过程,我们先看看一个RNN-cell里面的计算, 把上面的图拿下来:
1 | def rnn_cell_forward(xt, a_prev, parameters): |
这就是cell的前向传播, 当然这里面有一些细节, 比如像那些参数, a_next, a_prev, xt这些东西, 最好都保存一下,反向传播的时候会用到。
一个cell的前向传播完毕, 那么整个RNN的前向传播应该咋写呢? 还是看图
有了一个cell的计算, 整个RNN其实就是时间步的一个循环, 所以可以用一个时间步循环解决这个问题, 还是先分析一下, 接收的数据是a0和整个x, 这个x的维度就是(input_dim, m, timesteps), 而a0的维度就是(units, m), 而这里的输出有最后一步的a, 这个维度是(units, m, timesteps), y_pred, 维度是(n_y, m, timesteps)。 过程就是遍历每个时间步, 得到本时间步的输出y和下一步的输入a_next, 把这个加入到最后的y和a里面即可。
1 | def rnn_forward(x, a0, parameters): |
这就是RNN的前向传播过程, 这样理解这个循环神经网络的计算过程为啥是一气呵成了吧, 但是这里还要注意一下, 这个和普通的全连接前向传播的循环可不一样, 这里是只有一层隐藏层, 然后这里的循环是时间步的循环, 而全连接网络那里的循环是多个隐藏层, 循环是隐藏层的循环计算, 如果不理解的话很容易就搞乱了。这里是一层的RNN, 但是有一个时间步的循环计算, 而普通的一层全连接网络,是不用循环计算的。
那么这里又要看一个问题了, 我们知道全连接那分析的时候,如果层数很多, 就会出现梯度消失或者爆炸, 这是因为在反向传播的时候, 通过链式法则的推导,会用到上一层正向传播过程中的输出, 而这个输出,又依赖于前面层数的输出,这是一个连乘的计算过程, 所以如果前面某一层某个值很大或者很小的时候,就会导致后面某些层的输出很小, 这样就会导致梯度消失或者爆炸, 如果不知道我在说啥的,建议补一下基础,或者看看系统学习Pytorch笔记六:模型的权值初始化与损失函数介绍, 这里面解释了一点梯度消失和爆炸现象。
就拿一个三个时间步的RNN来看, 通过上面的分析,我们可以写一下它的前向传播过程:
5. 总结
好了, 这篇基础知识的内容就整理到这里, 如果后面加上LSTM就会太多了, 所以趁热乎快速回顾一下: 这篇文章就是围绕着时序序列的任务进行展开, 从全连接网络开始,复习了一下DNN的步骤和处理这种时序序列任务的局限性, 引出了RNN, 然后重点说了一下RNN的运算原理和几个细节部分, 纠正一下初学者对RNN的理解误差, 然后为了更加详细的理解RNN的计算原理,用numpy实现了一下前向传播的过程, 并有一个例子写了一下反向传播的公式, 并解释了一下为什么RNN会存在梯度消失和爆炸现象, 为什么不能捕捉长期依赖关系, 最后又分析了这两个问题的解决关键在什么地方。
而RNN的这两个问题到底是如何解决的呢? 下一篇重温LSTM及其变体GRU中告诉你 😉
参考: