rnn 订阅
循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络(recursive neural network) [1]  。对循环神经网络的研究始于二十世纪80-90年代,并在二十一世纪初发展为深度学习(deep learning)算法之一 [2]  ,其中双向循环神经网络(Bidirectional RNN, Bi-RNN)和长短期记忆网络(Long Short-Term Memory networks,LSTM)是常见的的循环神经网络 [3]  。循环神经网络具有记忆性、参数共享并且图灵完备(Turing completeness),因此在对序列的非线性特征进行学习时具有一定优势 [4]  。循环神经网络在自然语言处理(Natural Language Processing, NLP),例如语音识别、语言建模、机器翻译等领域有应用,也被用于各类时间序列预报。引入了卷积神经网络(Convoutional Neural Network,CNN)构筑的循环神经网络可以处理包含序列输入的计算机视觉问题。 展开全文
循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络(recursive neural network) [1]  。对循环神经网络的研究始于二十世纪80-90年代,并在二十一世纪初发展为深度学习(deep learning)算法之一 [2]  ,其中双向循环神经网络(Bidirectional RNN, Bi-RNN)和长短期记忆网络(Long Short-Term Memory networks,LSTM)是常见的的循环神经网络 [3]  。循环神经网络具有记忆性、参数共享并且图灵完备(Turing completeness),因此在对序列的非线性特征进行学习时具有一定优势 [4]  。循环神经网络在自然语言处理(Natural Language Processing, NLP),例如语音识别、语言建模、机器翻译等领域有应用,也被用于各类时间序列预报。引入了卷积神经网络(Convoutional Neural Network,CNN)构筑的循环神经网络可以处理包含序列输入的计算机视觉问题。
信息
外文名
Recurrent Neural Network, RNN
提出时间
1986-1990年
提出者
M. I. Jordan,Jeffrey Elman
类    型
机器学习算法,神经网络算法
中文名
循环神经网络
应    用
自然语言处理,计算机视觉
所属学科
人工智能
循环神经网络历史
1933年,西班牙神经生物学家Rafael Lorente de Nó发现大脑皮层(cerebral cortex)的解剖结构允许刺激在神经回路中循环传递,并由此提出反响回路假设(reverberating circuit hypothesis) [5]  。该假说在同时期的一系列研究中得到认可,被认为是生物拥有短期记忆的原因 [6-7]  。随后神经生物学的进一步研究发现,反响回路的兴奋和抑制受大脑阿尔法节律(α-rhythm)调控,并在α-运动神经(α-motoneurones )中形成循环反馈系统(recurrent feedback system) [8-9]  。在二十世纪70-80年代,为模拟循环反馈系统而建立的一些数学模型为RNN带来了启发 [10-12]  。1982年,美国学者John Hopfield基于Little (1974) [12]  的神经数学模型使用二元节点建立了具有结合存储(content-addressable memory)能力的神经网络,即Hopfield神经网络 [13]  。Hopfield网络是一个包含递归计算和外部记忆(external memory)的神经网络,其内部所有节点都相互连接,并使用能量函数进行非监督学习 [14]  。1986年,Michael I. Jordan在分布式并行处理(parallel distributed processing)理论下提出了Jordan网络 [15]  。Jordan网络的每个隐含层节点都与一个状态单元(state units)相连以实现延时输入,并使用logistic函数作为激励函数 [15]  。Jordan网络使用反向传播算法(Back-Probagation, BP)进行学习,并在测试中提取了给定音节的语音学特征 [15]  。之后在1990年,Jeffrey Elman提出了第一个全连接的RNN,即Elman网络 [16]  。Jordan网络和Elman网络都从单层前馈神经网络出发构建递归连接,因此也被称为简单循环网络(Simple Recurrent Network, SRN) [4]  。在SRN出现的同一时期,RNN的学习理论也得到发展。在反向传播算法被提出后 [17]  ,学界开始尝试在BP框架下对循环神经网络进行训练 [15]  [2]  [18]  。1989年,Ronald Williams和David Zipser提出了RNN的实时循环学习(Real-Time Recurrent Learning, RTRL) [19]  。随后Paul Werbos在1990年提出了随时间反向传播算法(BP Through Time,BPTT) [20]  。1991年,Sepp Hochreiter发现了循环神经网络的长期依赖问题(long-term dependencies problem),即在对长序列进行学习时,循环神经网络会出现梯度消失(gradient vanishing)和梯度爆炸(gradient explosion)现象,无法掌握长时间跨度的非线性关系 [21-22]  。为解决长期依赖问题,RNN的改进不断出现,较重要的包括Jurgen Schmidhuber及其合作者在1992和1997年提出的神经历史压缩器(Neural History Compressor, NHC) [23]  和长短期记忆网络(Long Short-Term Memory networks, LSTM) [24]  ,其中包含门控的LSTM受到了关注。同在1997年,M. Schuster和K. Paliwal提出了具有深度结构的双向循环神经网络(Bidirectional RNN, BRNN),并对其进行了语音识别试验 [25]  。双向和门控构架的出现提升了RNN的学习表现,在一些综述性研究中,被认为是RNN具有代表性的研究成果 [2]  。十一世纪后,随着深度学习理论的出现和数值计算能力的提升,拥有更高复杂度的RNN开始在自然语言处理问题中得到关注。2005年,Alex Graves等将双向LSTM应用于语音识别,并得到了优于隐马尔可夫模型(Hidden Markov Model, HMM)的表现 [26]  。2014年,K. Cho提出了门控循环单元网络(Gated Recurrent Unit networks, GRU),该方法是LSTM之后另一个受到关注的RNN门控构架 [27]  。2010年,Tomas Mikolov及其合作者提出了基于RNN的语言模型 [28]  [29]  。2013-2015年, Y. Benjo、D. Bahdanau等提出了编码器-解码器、自注意力层等一系列RNN算法,并将其应用于机器翻译问题 [30-31]  。为语言模型设计的RNN算法在随后的研究中启发了包括ransformers、XLNet、ELMo、BERT等复杂构筑 [32-34]  。
收起全文
精华内容
参与话题
问答
  • RNN

    万次阅读 多人点赞 2018-06-06 23:40:03
    RNN(Recurrent Neural Network)是一类用于处理序列数据的神经网络。首先我们要明确什么是序列数据,摘取百度百科词条:时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化...

    本文部分参考和摘录了以下文章,在此由衷感谢以下作者的分享!
    https://zhuanlan.zhihu.com/p/28054589
    https://blog.csdn.net/qq_16234613/article/details/79476763
    http://www.cnblogs.com/pinard/p/6509630.html
    https://zhuanlan.zhihu.com/p/28687529
    http://www.cnblogs.com/pinard/p/6509630.html
    https://zhuanlan.zhihu.com/p/26892413
    https://zhuanlan.zhihu.com/p/21462488?refer=intelligentunit


    RNN(Recurrent Neural Network)是一类用于处理序列数据的神经网络。首先我们要明确什么是序列数据,摘取百度百科词条:时间序列数据是指在不同时间点上收集到的数据,这类数据反映了某一事物、现象等随时间的变化状态或程度。这是时间序列数据的定义,当然这里也可以不是时间,比如文字序列,但总归序列数据有一个特点——后面的数据跟前面的数据有关系。

    RNN的结构及变体

    我们从基础的神经网络中知道,神经网络包含输入层、隐层、输出层,通过激活函数控制输出,层与层之间通过权值连接。激活函数是事先确定好的,那么神经网络模型通过训练“学“到的东西就蕴含在“权值“中。
    基础的神经网络只在层与层之间建立了权连接,RNN最大的不同之处就是在层之间的神经元之间也建立的权连接。如图。
    这里写图片描述

    这是一个标准的RNN结构图,图中每个箭头代表做一次变换,也就是说箭头连接带有权值。左侧是折叠起来的样子,右侧是展开的样子,左侧中h旁边的箭头代表此结构中的“循环“体现在隐层。
    在展开结构中我们可以观察到,在标准的RNN结构中,隐层的神经元之间也是带有权值的。也就是说,随着序列的不断推进,前面的隐层将会影响后面的隐层。图中O代表输出,y代表样本给出的确定值,L代表损失函数,我们可以看到,“损失“也是随着序列的推荐而不断积累的。
    除上述特点之外,标准RNN的还有以下特点:
    1、权值共享,图中的W全是相同的,U和V也一样。
    2、每一个输入值都只与它本身的那条路线建立权连接,不会和别的神经元连接。

    以上是RNN的标准结构,然而在实际中这一种结构并不能解决所有问题,例如我们输入为一串文字,输出为分类类别,那么输出就不需要一个序列,只需要单个输出。如图。
    这里写图片描述
    同样的,我们有时候还需要单输入但是输出为序列的情况。那么就可以使用如下结构:
    这里写图片描述

    还有一种结构是输入虽是序列,但不随着序列变化,就可以使用如下结构:
    这里写图片描述

    原始的N vs N RNN要求序列等长,然而我们遇到的大部分问题序列都是不等长的,如机器翻译中,源语言和目标语言的句子往往并没有相同的长度。
    下面我们来介绍RNN最重要的一个变种:N vs M。这种结构又叫Encoder-Decoder模型,也可以称之为Seq2Seq模型。
    这里写图片描述
    从名字就能看出,这个结构的原理是先编码后解码。左侧的RNN用来编码得到c,拿到c后再用右侧的RNN进行解码。得到c有多种方式,最简单的方法就是把Encoder的最后一个隐状态赋值给c,还可以对最后的隐状态做一个变换得到c,也可以对所有的隐状态做变换。
    这里写图片描述

    除了以上这些结构以外RNN还有很多种结构,用于应对不同的需求和解决不同的问题。还想继续了解可以看一下下面这个博客,里面又介绍了几种不同的结构。但相同的是循环神经网络除了拥有神经网络都有的一些共性元素之外,它总要在一个地方体现出“循环“,而根据“循环“体现方式的不同和输入输出的变化就形成了多种RNN结构。

    https://blog.csdn.net/qq_16234613/article/details/79476763

    标准RNN的前向输出流程

    上面介绍了RNN有很多变种,但其数学推导过程其实都是大同小异。这里就介绍一下标准结构的RNN的前向传播过程。
    这里写图片描述

    再来介绍一下各个符号的含义:x是输入,h是隐层单元,o为输出,L为损失函数,y为训练集的标签。这些元素右上角带的t代表t时刻的状态,其中需要注意的是,因策单元h在t时刻的表现不仅由此刻的输入决定,还受t时刻之前时刻的影响。V、W、U是权值,同一类型的权连接权值相同。

    有了上面的理解,前向传播算法其实非常简单,对于t时刻:
    h(t)=ϕ(Ux(t)+Wh(t1)+b)h^{(t)}=\phi(Ux^{(t)}+Wh^{(t-1)}+b)
    其中ϕ()\phi()为激活函数,一般来说会选择tanh函数,b为偏置。

    t时刻的输出就更为简单:
    o(t)=Vh(t)+co^{(t)}=Vh^{(t)}+c

    最终模型的预测输出为:
    y^(t)=σ(o(t))\widehat{y}^{(t)}=\sigma(o^{(t)})

    其中σ\sigma为激活函数,通常RNN用于分类,故这里一般用softmax函数。

    RNN的训练方法——BPTT

    BPTT(back-propagation through time)算法是常用的训练RNN的方法,其实本质还是BP算法,只不过RNN处理时间序列数据,所以要基于时间反向传播,故叫随时间反向传播。BPTT的中心思想和BP算法相同,沿着需要优化的参数的负梯度方向不断寻找更优的点直至收敛。综上所述,BPTT算法本质还是BP算法,BP算法本质还是梯度下降法,那么求各个参数的梯度便成了此算法的核心。
    这里写图片描述

    再次拿出这个结构图观察,需要寻优的参数有三个,分别是U、V、W。与BP算法不同的是,其中W和U两个参数的寻优过程需要追溯之前的历史数据,参数V相对简单只需关注目前,那么我们就来先求解参数V的偏导数。
    L(t)V=L(t)o(t)o(t)V\frac{\partial L^{(t)}}{\partial V}=\frac{\partial L^{(t)}}{\partial o^{(t)}}\cdot \frac{\partial o^{(t)}}{\partial V}
    这个式子看起来简单但是求解起来很容易出错,因为其中嵌套着激活函数函数,是复合函数的求道过程。

    RNN的损失也是会随着时间累加的,所以不能只求t时刻的偏导。
    L=t=1nL(t)L=\sum_{t=1}^{n}L^{(t)} LV=t=1nL(t)o(t)o(t)V\frac{\partial L}{\partial V}=\sum_{t=1}^{n}\frac{\partial L^{(t)}}{\partial o^{(t)}}\cdot \frac{\partial o^{(t)}}{\partial V}

    W和U的偏导的求解由于需要涉及到历史数据,其偏导求起来相对复杂,我们先假设只有三个时刻,那么在第三个时刻 L对W的偏导数为:
    L(3)W=L(3)o(3)o(3)h(3)h(3)W+L(3)o(3)o(3)h(3)h(3)h(2)h(2)W+L(3)o(3)o(3)h(3)h(3)h(2)h(2)h(1)h(1)W\frac{\partial L^{(3)}}{\partial W}=\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial W}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial W}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial h^{(1)}}\frac{\partial h^{(1)}}{\partial W}
    相应的,L在第三个时刻对U的偏导数为:
    L(3)U=L(3)o(3)o(3)h(3)h(3)U+L(3)o(3)o(3)h(3)h(3)h(2)h(2)U+L(3)o(3)o(3)h(3)h(3)h(2)h(2)h(1)h(1)U\frac{\partial L^{(3)}}{\partial U}=\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial U}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial U}+\frac{\partial L^{(3)}}{\partial o^{(3)}}\frac{\partial o^{(3)}}{\partial h^{(3)}}\frac{\partial h^{(3)}}{\partial h^{(2)}}\frac{\partial h^{(2)}}{\partial h^{(1)}}\frac{\partial h^{(1)}}{\partial U}

    可以观察到,在某个时刻的对W或是U的偏导数,需要追溯这个时刻之前所有时刻的信息,这还仅仅是一个时刻的偏导数,上面说过损失也是会累加的,那么整个损失函数对W和U的偏导数将会非常繁琐。虽然如此但好在规律还是有迹可循,我们根据上面两个式子可以写出L在t时刻对W和U偏导数的通式:
    L(t)W=k=0tL(t)o(t)o(t)h(t)(j=k+1th(j)h(j1))h(k)W\frac{\partial L^{(t)}}{\partial W}=\sum_{k=0}^{t}\frac{\partial L^{(t)}}{\partial o^{(t)}}\frac{\partial o^{(t)}}{\partial h^{(t)}}(\prod_{j=k+1}^{t}\frac{\partial h^{(j)}}{\partial h^{(j-1)}})\frac{\partial h^{(k)}}{\partial W} L(t)U=k=0tL(t)o(t)o(t)h(t)(j=k+1th(j)h(j1))h(k)U\frac{\partial L^{(t)}}{\partial U}=\sum_{k=0}^{t}\frac{\partial L^{(t)}}{\partial o^{(t)}}\frac{\partial o^{(t)}}{\partial h^{(t)}}(\prod_{j=k+1}^{t}\frac{\partial h^{(j)}}{\partial h^{(j-1)}})\frac{\partial h^{(k)}}{\partial U}
    整体的偏导公式就是将其按时刻再一一加起来。

    前面说过激活函数是嵌套在里面的,如果我们把激活函数放进去,拿出中间累乘的那部分:
    j=k+1thjhj1=j=k+1ttanhWs\prod_{j=k+1}^{t}{\frac{\partial{h^{j}}}{\partial{h^{j-1}}}} = \prod_{j=k+1}^{t}{tanh^{'}}\cdot W_{s}或是
    j=k+1thjhj1=j=k+1tsigmoidWs\prod_{j=k+1}^{t}{\frac{\partial{h^{j}}}{\partial{h^{j-1}}}} = \prod_{j=k+1}^{t}{sigmoid^{'}}\cdot W_{s}

    我们会发现累乘会导致激活函数导数的累乘,进而会导致“梯度消失“和“梯度爆炸“现象的发生。

    至于为什么,我们先来看看这两个激活函数的图像。
    这是sigmoid函数的函数图和导数图。
    这里写图片描述
    这是tanh函数的函数图和导数图。
    这里写图片描述
    它们二者是何其的相似,都把输出压缩在了一个范围之内。他们的导数图像也非常相近,我们可以从中观察到,sigmoid函数的导数范围是(0,0.25],tanh函数的导数范围是(0,1],他们的导数最大都不大于1。

    这就会导致一个问题,在上面式子累乘的过程中,如果取sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。其实RNN的时间序列与深层神经网络很像,在较为深层的神经网络中使用sigmoid函数做激活函数也会导致反向传播时梯度消失,梯度消失就意味消失那一层的参数再也不更新,那么那一层隐层就变成了单纯的映射层,毫无意义了,所以在深层神经网络中,有时候多加神经元数量可能会比多家深度好。

    你可能会提出异议,RNN明明与深层神经网络不同,RNN的参数都是共享的,而且某时刻的梯度是此时刻和之前时刻的累加,即使传不到最深处那浅层也是有梯度的。这当然是对的,但如果我们根据有限层的梯度来更新更多层的共享的参数一定会出现问题的,因为将有限的信息来作为寻优根据必定不会找到所有信息的最优解。

    之前说过我们多用tanh函数作为激活函数,那tanh函数的导数最大也才1啊,而且又不可能所有值都取到1,那相当于还是一堆小数在累乘,还是会出现“梯度消失“,那为什么还要用它做激活函数呢?原因是tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。

    还有一个原因是sigmoid函数还有一个缺点,Sigmoid函数输出不是零中心对称。sigmoid的输出均大于0,这就使得输出不是0均值,称为偏移现象,这将导致后一层的神经元将上一层输出的非0均值的信号作为输入。关于原点对称的输入和中心对称的输出,网络会收敛地更好。

    RNN的特点本来就是能“追根溯源“利用历史数据,现在告诉我可利用的历史数据竟然是有限的,这就令人非常难受,解决“梯度消失“是非常必要的。这里说两种改善“梯度消失”的方法:
    1、选取更好的激活函数
    2、改变传播结构

    关于第一点,一般选用ReLU函数作为激活函数,ReLU函数的图像为:
    这里写图片描述
    ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了小数的连乘,但反向传播中仍有权值的累乘,所以说ReLU函数不能说完全解决了“梯度消失”现象,只能说改善。有研究表明,在RNN中使用ReLU函数配合将权值初始化到单位矩阵附近,可以达到接近LSTM网络的效果。但恒为1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。还有一点就是如果左侧横为0的导数有可能导致把神经元学死,不过设置合适的步长(学习率)也可以有效避免这个问题的发生。

    关于第二点,LSTM结构就是传统RNN的改善。

    总结一下,sigmoid函数的缺点:
    1、导数值范围为(0,0.25],反向传播时会导致“梯度消失“。tanh函数导数值范围更大,相对好一点。
    2、sigmoid函数不是0中心对称,tanh函数是,可以使网络收敛的更好。


    LSTM

    下面来了解一下LSTM(long short-term memory)。长短期记忆网络是RNN的一种变体,RNN由于梯度消失的原因只能有短期记忆,LSTM网络通过精妙的门控制将加法运算带入网络中,一定程度上解决了梯度消失的问题。只能说一定程度上,过长的序列还是会出现“梯度消失”(我记得有个老外的博客上说长度超过300就有可能出现),所以LSTM叫长一点的“短时记忆”。
    由于已经存在了一篇写得非常好的博客,我在这里就直接转载过来,再在其中夹杂点自己的理解。原文连接如下。

    作者:朱小虎Neil 链接:https://www.jianshu.com/p/9dc9f41f0b29 來源:简书

    在此感谢原作者!

    长期依赖(Long-Term Dependencies)问题

    RNN 的关键点之一就是他们可以用来连接先前的信息到当前的任务上,例如使用过去的视频段来推测对当前段的理解。如果 RNN 可以做到这个,他们就变得非常有用。但是真的可以么?答案是,还有很多依赖因素。

    有时候,我们仅仅需要知道先前的信息来执行当前的任务。例如,我们有一个语言模型用来基于先前的词来预测下一个词。如果我们试着预测 “the clouds are in the sky” 最后的词,我们并不需要任何其他的上下文 —— 因此下一个词很显然就应该是 sky。在这样的场景中,相关的信息和预测的词位置之间的间隔是非常小的,RNN 可以学会使用先前的信息。
    在这里插入图片描述
    不太长的相关信息和位置间隔
    但是同样会有一些更加复杂的场景。假设我们试着去预测“I grew up in France… I speak fluent French”最后的词。当前的信息建议下一个词可能是一种语言的名字,但是如果我们需要弄清楚是什么语言,我们是需要先前提到的离当前位置很远的 France 的上下文的。这说明相关信息和当前预测位置之间的间隔就肯定变得相当的大。

    不幸的是,在这个间隔不断增大时,RNN 会丧失学习到连接如此远的信息的能力。
    在这里插入图片描述
    相当长的相关信息和位置间隔
    在理论上,RNN 绝对可以处理这样的 长期依赖 问题。人们可以仔细挑选参数来解决这类问题中的最初级形式,但在实践中,RNN 肯定不能够成功学习到这些知识。Bengio, et al. (1994)等人对该问题进行了深入的研究,他们发现一些使训练 RNN 变得非常困难的相当根本的原因。

    然而,幸运的是,LSTM 并没有这个问题

    LSTM 网络

    Long Short Term 网络—— 一般就叫做 LSTM ——是一种 RNN 特殊的类型,可以学习长期依赖信息。LSTM 由Hochreiter & Schmidhuber (1997)提出,并在近期被Alex Graves进行了改良和推广。在很多问题,LSTM 都取得相当巨大的成功,并得到了广泛的使用。
    LSTM 通过刻意的设计来避免长期依赖问题。记住长期的信息在实践中是 LSTM 的默认行为,而非需要付出很大代价才能获得的能力!

    所有 RNN 都具有一种重复神经网络模块的链式的形式。在标准的 RNN 中,这个重复的模块只有一个非常简单的结构,例如一个 tanh 层。
    在这里插入图片描述

    LSTM 同样是这样的结构,但是重复的模块拥有一个不同的结构。不同于 单一神经网络层,整体上除了h在随时间流动,细胞状态c也在随时间流动,细胞状态c就代表着长期记忆。
    在这里插入图片描述
    不必担心这里的细节。我们会一步一步地剖析 LSTM 解析图。现在,我们先来熟悉一下图中使用的各种元素的图标。
    在这里插入图片描述

    • 黄色的矩形是学习得到的神经网络层
    • 粉色的圆形表示一些运算操作,诸如加法乘法
    • 黑色的单箭头表示向量的传输
    • 两个箭头合成一个表示向量的连接
    • 一个箭头分开表示向量的复制

    LSTM 的核心思想

    LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。

    细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。
    在这里插入图片描述
    LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。
    在这里插入图片描述

    Sigmoid 层输出 0 到 1 之间的数值,描述每个部分有多少量可以通过。0 代表“不许任何量通过”,1 就指“允许任意量通过”!

    LSTM 拥有三个门,来保护和控制细胞状态。

    逐步理解 LSTM

    在我们 LSTM 中的第一步是决定我们会从细胞状态中丢弃什么信息。这个决定通过一个称为遗忘门完成。该门会读取ht1h_{t-1}xtx_t,输出一个在 0 到 1 之间的数值给每个在细胞状态Ct1C_{t-1}中的数字。1 表示“完全保留”,0 表示“完全舍弃”。

    让我们回到语言模型的例子中来基于已经看到的预测下一个词。在这个问题中,细胞状态可能包含当前主语的性别,因此正确的代词可以被选择出来。当我们看到新的主语,我们希望忘记旧的主语。
    在这里插入图片描述
    这里可以抛出两个问题:这个门怎么做到“遗忘“的呢?怎么理解?既然是遗忘旧的内容,为什么这个门还要接收新的xtx_{t}?
    对于第一个问题,“遗忘“可以理解为“之前的内容记住多少“,其精髓在于只能输出(0,1)小数的sigmoid函数和粉色圆圈的乘法,LSTM网络经过学习决定让网络记住以前百分之多少的内容。对于第二个问题就更好理解,决定记住什么遗忘什么,其中新的输入肯定要产生影响。

    下一步是确定什么样的新信息被存放在细胞状态中。这里包含两个部分。第一,sigmoid 层称 “输入门层” 决定什么值我们将要更新。然后,一个 tanh 层创建一个新的候选值向量,C~t\tilde{C}_t,会被加入到状态中。下一步,我们会讲这两个信息来产生对状态的更新。

    在我们语言模型的例子中,我们希望增加新的主语的性别到细胞状态中,来替代旧的需要忘记的主语。
    这里写图片描述
    现在是更新旧细胞状态的时间了,Ct1C_{t-1} 更新为 CtC_t。前面的步骤已经决定了将会做什么,我们现在就是实际去完成。

    我们把旧状态与$ f_t 相乘,丢弃掉我们确定需要丢弃的信息。接着加上 i_t * \tilde{C}_t$。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。

    有了上面的理解基础输入门,输入门理解起来就简单多了。sigmoid函数选择更新内容,tanh函数创建更新候选。
    这里写图片描述
    最终,我们需要确定输出什么值。这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。首先,我们运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态通过 tanh 进行处理(得到一个在 -1 到 1 之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。
    这里写图片描述

    这三个门虽然功能上不同,但在执行任务的操作上是相同的。他们都是使用sigmoid函数作为选择工具,tanh函数作为变换工具,这两个函数结合起来实现三个门的功能。

    LSTM 的变体

    我们到目前为止都还在介绍正常的 LSTM。但是不是所有的 LSTM 都长成一个样子的。实际上,几乎所有包含 LSTM 的论文都采用了微小的变体。差异非常小,但是也值得拿出来讲一下。

    其中一个流行的 LSTM 变体,就是由 Gers & Schmidhuber (2000) 提出的,增加了 “peephole connection”。是说,我们让 门层 也会接受细胞状态的输入。
    在这里插入图片描述
    上面的图例中,我们增加了 peephole 到每个门上,但是许多论文会加入部分的 peephole 而非所有都加。

    另一个变体是通过使用 coupled 忘记和输入门。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记。我们仅仅输入新的值到那些我们已经忘记旧的信息的那些状态 。

    另一个变体是通过使用 coupled 忘记和输入门。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记。我们仅仅输入新的值到那些我们已经忘记旧的信息的那些状态 。
    在这里插入图片描述
    另一个改动较大的变体是 Gated Recurrent Unit (GRU),这是由 Cho, et al. (2014) 提出。它将忘记门和输入门合成了一个单一的 更新门。同样还混合了细胞状态和隐藏状态,和其他一些改动。最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。
    在这里插入图片描述
    这里只是部分流行的 LSTM 变体。当然还有很多其他的,如Yao, et al. (2015) 提出的 Depth Gated RNN。还有用一些完全不同的观点来解决长期依赖的问题,如Koutnik, et al. (2014) 提出的 Clockwork RNN。

    要问哪个变体是最好的?其中的差异性真的重要吗?Greff, et al. (2015) 给出了流行变体的比较,结论是他们基本上是一样的。Jozefowicz, et al. (2015) 则在超过 1 万种 RNN 架构上进行了测试,发现一些架构在某些任务上也取得了比 LSTM 更好的结果。
    在这里插入图片描述

    写在2021-01-13

    RNN/LSTM是一个伟大的算法,但终究也成为了过去式。目前最强的特征抽取器当属transformer,bert、gpt等预训练模型也是基于transformer。从序列长度问题上,transformer依靠其独特的self-attention解决了RNN长序列梯度消失的问题;从训练方式上,transformer结构上的非序列性也解决了RNN难以并行的问题,可谓从各方面吊打了RNN,称得上革命性的创举。感兴趣的朋友可以移步:深入理解transformer源码

    展开全文
  • rnn

    2018-06-26 19:12:52
    随手记录一些比较模糊的点1.dropout 作用是随机去除一些数据,在rnn中分为input和output的dropout ,output的dropout比较特殊, 其中input部分:if is_training and keep_prob < 1: inputs = tf.nn....

    随手记录一些比较模糊的点

    1.dropout

      作用是随机去除一些数据,在rnn中分为input和output的dropout ,output的dropout比较特殊,

      其中input部分:

    if is_training and keep_prob < 1:
        inputs = tf.nn.dropout(inputs, keep_prob)

    其中is_training 是判断是否是训练阶段,因为只有训练阶段需要dropout,跑test验证数据的时候要全量跑,keep_prob的意思是去除的百分比 0-1之间,0全去除,1不去除

     output部分

    if is_training and keep_prob < 1:
        lstm_cell = tf.nn.rnn_cell.DropoutWrapper(
            lstm_cell, output_keep_prob=keep_prob)

    与input不同的是调用了rnn_cell的方法,不是tf.nn的通用方法,具体区别可以百度下。

    2.

    inputRnn = tf.reshape(inputRnn, [-1, time_step, 10])  # 将tensor转成3维,作为lstm cell的输入
    
    cell = tf.nn.rnn_cell.BasicLSTMCell(10)
    dropout_lstm = tf.nn.rnn_cell.DropoutWrapper(cell, output_keep_prob=0.5)
    lstm_cell = tf.nn.rnn_cell.MultiRNNCell([dropout_lstm]*4, state_is_tuple=True)
    
    initState = lstm_cell.zero_state(1, dtype=tf.float32)
    output_rnn,_ = tf.nn.dynamic_rnn(lstm_cell, inputRnn, initial_state=initState,
                                   dtype=tf.float32)  # output_rnn是记录lstm每个输出节点的结果,final_states是最后一个cell的结果
    # output = tf.reshape(output_rnn, [-1, 10])  # 作为输出层的输入
    output = tf.reshape(output_rnn, [-1, 10])  # 作为输出层的输入

    终于闹明白这一段里面各个参数的含义了

    inoutRnn的维度,必须和下面初始化initState的维度一样

    initState = lstm_cell.zero_state(1, dtype=tf.float32)

    特别注意,要用MultiRNNCell的zero_state方法,不要用basic的了

    正常时间序列数据,这里的time_step应该取5-20,代表取这一段时间内的数据作为LSTM训练批次

    如果输入数据不是时序数据,这个time_step 设为1,不过这种数据效果不是很理想

    lstm_cell = tf.nn.rnn_cell.MultiRNNCell([dropout_lstm]*4, state_is_tuple=True)

    这里*4,表示网络深度为4层


    题外话:

    tf.variable_scope() and tf.name_scope() 用法

    https://morvanzhou.github.io/tutorials/machine-learning/tensorflow/5-12-scope/


    tensowflow 保存模型,加载模型

    https://blog.csdn.net/ying86615791/article/details/72731372

    官网rnn示例详细解释

    https://blog.csdn.net/diligent_321/article/details/53385508




    展开全文
  • 循环神经网络(RNN)原理通俗解释

    万次阅读 多人点赞 2017-11-30 15:15:37
    1.RNN怎么来的? 2.RNN的网络结构及原理 3.RNN的改进1:双向RNN 4.RNN的改进2:深层双向RNN 4.1 Pyramidal RNN 5.RNN的训练-BPTT 6.RNN与CNN的结合应用:看图说话 7.RNN项目练手 1.RNN怎么来的? 循环神经...

    1.RNN怎么来的?

    循环神经网络的应用场景比较多,比如暂时能写论文,写程序,写诗,但是,(总是会有但是的),但是他们现在还不能正常使用,学习出来的东西没有逻辑,所以要想真正让它更有用,路还很远。

    这是一般的神经网络应该有的结构:
    这里写图片描述

    既然我们已经有了人工神经网络和卷积神经网络,为什么还要循环神经网络?
    原因很简单,无论是卷积神经网络,还是人工神经网络,他们的前提假设都是:元素之间是相互独立的,输入与输出也是独立的,比如猫和狗。
    但现实世界中,很多元素都是相互连接的,比如股票随时间的变化,一个人说了:我喜欢旅游,其中最喜欢的地方是云南,以后有机会一定要去_____.这里填空,人应该都知道是填“云南“。因为我们是根据上下文的内容推断出来的,但机会要做到这一步就相当得难了。因此,就有了现在的循环神经网络,他的本质是:像人一样拥有记忆的能力。因此,他的输出就依赖于当前的输入和记忆。

    2.RNN的网络结构及原理

    它的网络结构如下:
    这里写图片描述
    其中每个圆圈可以看作是一个单元,而且每个单元做的事情也是一样的,因此可以折叠呈左半图的样子。用一句话解释RNN,就是一个单元结构重复使用

    RNN是一个序列到序列的模型,假设xt1,xt,xt+1是一个输入:“我是中国“,那么ot1,ot就应该对应”是”,”中国”这两个,预测下一个词最有可能是什么?就是ot+1应该是”人”的概率比较大。

    因此,我们可以做这样的定义:

    Xt:tot:tSt:t
    。因为我们当前时刻的输出是由记忆和当前时刻的输入决定的,就像你现在大四,你的知识是由大四学到的知识(当前输入)和大三以及大三以前学到的东西的(记忆)的结合,RNN在这点上也类似,神经网络最擅长做的就是通过一系列参数把很多内容整合到一起,然后学习这个参数,因此就定义了RNN的基础:
    St=f(UXt+WSt1)
    大家可能会很好奇,为什么还要加一个f()函数,其实这个函数是神经网络中的激活函数,但为什么要加上它呢?
    举个例子,假如你在大学学了非常好的解题方法,那你初中那时候的解题方法还要用吗?显然是不用了的。RNN的想法也一样,既然我能记忆了,那我当然是只记重要的信息啦,其他不重要的,就肯定会忘记,是吧。但是在神经网络中什么最适合过滤信息呀?肯定是激活函数嘛,因此在这里就套用一个激活函数,来做一个非线性映射,来过滤信息,这个激活函数可能为tanh,也可为其他。

    假设你大四快毕业了,要参加考研,请问你参加考研是不是先记住你学过的内容然后去考研,还是直接带几本书去参加考研呢?很显然嘛,那RNN的想法就是预测的时候带着当前时刻的记忆St去预测。假如你要预测“我是中国“的下一个词出现的概率,这里已经很显然了,运用softmax来预测每个词出现的概率再合适不过了,但预测不能直接带用一个矩阵来预测呀,所有预测的时候还要带一个权重矩阵V,用公式表示为:

    ot=softmax(VSt)
    其中ot就表示时刻t的输出。

    RNN中的结构细节:
    1.可以把St当作隐状态,捕捉了之前时间点上的信息。就像你去考研一样,考的时候记住了你能记住的所有信息。
    2.ot是由当前时间以及之前所有的记忆得到的。就是你考研之后做的考试卷子,是用你的记忆得到的。
    3.很可惜的是,St并不能捕捉之前所有时间点的信息。就像你考研不能记住所有的英语单词一样。
    4.和卷积神经网络一样,这里的网络中每个cell都共享了一组参数(U,V,W),这样就能极大的降低计算量了。
    5.ot在很多情况下都是不存在的,因为很多任务,比如文本情感分析,都是只关注最后的结果的。就像考研之后选择学校,学校不会管你到底怎么努力,怎么心酸的准备考研,而只关注你最后考了多少分。

    3.RNN的改进1:双向RNN

    在有些情况,比如有一部电视剧,在第三集的时候才出现的人物,现在让预测一下在第三集中出现的人物名字,你用前面两集的内容是预测不出来的,所以你需要用到第四,第五集的内容来预测第三集的内容,这就是双向RNN的想法。如图是双向RNN的图解:
    这里写图片描述

    St1=f(U1Xt+W1St1+b1)
    :St2=f(U2Xt+W2St1+b2)
    ot=softmax(V[St1;St2])
    这里的[St1;St2]做的是一个拼接,如果他们都是1000X1维的,拼接在一起就是1000X2维的了。

    双向RNN需要的内存是单向RNN的两倍,因为在同一时间点,双向RNN需要保存两个方向上的权重参数,在分类的时候,需要同时输入两个隐藏层输出的信息。

    4.RNN的改进2:深层双向RNN

    深层双向RNN 与双向RNN相比,多了几个隐藏层,因为他的想法是很多信息记一次记不下来,比如你去考研,复习考研英语的时候,背英语单词一定不会就看一次就记住了所有要考的考研单词吧,你应该也是带着先前几次背过的单词,然后选择那些背过,但不熟的内容,或者没背过的单词来背吧。

    深层双向RNN就是基于这么一个想法,他的输入有两方面,第一就是前一时刻的隐藏层传过来的信息ht1(i),和当前时刻上一隐藏层传过来的信息ht(i1)=[ht(i1);ht(i1)],包括前向和后向的。
    这里写图片描述

    我们用公式来表示是这样的:
    这里写图片描述
    然后再利用最后一层来进行分类,分类公式如下:
    这里写图片描述

    4.1 Pyramidal RNN

    其他类似的网络还有Pyramidal RNN:
    这里写图片描述
    我们现在有一个很长的输入序列,可以看到这是一个双向的RNN,上图是谷歌的W.Chan做的一个测试,它原先要做的是语音识别,他要用序列到序列的模型做语音识别,序列到序列就是说,输入一个序列然后就输出一个序列。

    由图我们发现,上一层的两个输出,作为当前层的输入,如果是非常长的序列的话,这样做的话,每一层的序列都比上一层要短,但当前层的输入f(x)也会随之增多,貌似看一起相互抵消,运算量并没有什么改进。

    但我们知道,对于一层来说,它是从前往后转的,比如要预测一个股市的变化,以天为单位,假如要预测明天的股市变化,你就要用今天,以及今天之前的所有数据,我们暂时无法只用昨天的数据,不用今天的数据,预测明天的数据,也即是说,预测必须具有连续性。
    但每一层的f运算是可以并行的,从这个角度来看,运算量还是可以接受的,特别是在原始输入序列较短的时候还是有优势的。

    5.RNN的训练-BPTT

    如前面我们讲的,如果要预测t时刻的输出,我们必须先利用上一时刻(t-1)的记忆和当前时刻的输入,得到t时刻的记忆:

    st=tanh(Uxt+Wst1)
    然后利用当前时刻的记忆,通过softmax分类器输出每个词出现的概率:
    y^t=softmax(Vst)
    为了找出模型最好的参数,U,W,V,我们就要知道当前参数得到的结果怎么样,因此就要定义我们的损失函数,用交叉熵损失函数:
    tEt(yt,y^t)=ytlogy^t
    其中ytt时刻的标准答案,是一个只有一个是1,其他都是0的向量;y^t是我们预测出来的结果,与yt的维度一样,但它是一个概率向量,里面是每个词出现的概率。因为对结果的影响,肯定不止一个时刻,因此需要把所有时刻的造成的损失都加起来:
    E(yt,y^t)=tEt(yt,y^t)=tytlogy^t

    如图所示,你会发现每个cell都会有一个损失,我们已经定义好了损失函数,接下来就是熟悉的一步了,那就是根据损失函数利用SGD来求解最优参数,在CNN中使用反向传播BP算法来求解最优参数,但在RNN就要用到BPTT,它和BP算法的本质区别,也是CNN和RNN的本质区别:CNN没有记忆功能,它的输出仅依赖与输入,但RNN有记忆功能,它的输出不仅依赖与当前输入,还依赖与当前的记忆。这个记忆是序列到序列的,也就是当前时刻收到上一时刻的影响,比如股市的变化。

    因此,在对参数求偏导的时候,对当前时刻求偏导,一定会涉及前一时刻,我们用例子看一下:

    假设我们对E3的W求偏导:它的损失首先来源于预测的输出y^3,预测的输出又是来源于当前时刻的记忆s3,当前的记忆又是来源于当前的输出和截止到上一时刻的记忆:s3=tanh(Ux3+Ws2)
    因此,根据链式法则可以有:

    E3W=E3y^3y^3s3s3W
    但是,你会发现,s2=tanh(Ux2+Ws1),也就是s2里面的函数还包含了W,因此,这个链式法则还没到底,就像图上画的那样,所以真正的链式法则是这样的:
    这里写图片描述
    我们要把当前时刻造成的损失,和以往每个时刻造成的损失加起来,因为我们每一个时刻都用到了权重参数W。和以往的网络不同,一般的网络,比如人工神经网络,参数是不同享的,但在循环神经网络,和CNN一样,设立了参数共享机制,来降低模型的计算量。

    6.RNN与CNN的结合应用:看图说话

    在图像处理中,目前做的最好的是CNN,而自然语言处理中,表现比较好的是RNN,因此,我们能否把他们结合起来,一起用呢?那就是看图说话了,这个原理也比较简单,举个小栗子:假设我们有CNN的模型训练了一个网络结构,比如是这个

    最后我们不是要分类嘛,那在分类前,是不是已经拿到了图像的特征呀,那我们能不能把图像的特征拿出来,放到RNN的输入里,让他学习呢?

    之前的RNN是这样的:

    St=tanh(UXt+WSt1)
    我们把图像的特征加在里面,可以得到:
    St=tanh(UXt+WSt1+VX)
    其中的X就是图像的特征。如果用的是上面的CNN网络,X应该是一个4096X1的向量。

    注:这个公式只在第一步做,后面每次更新就没有V了,因为给RNN数据只在第一次迭代的时候给。

    7.RNN项目练手

    RNN可以写歌词,写诗等,这有个项目可以玩玩,还不错。
    Tensorflow实现RNN

    展开全文
  • RNN与CharRNN

    2019-06-26 11:00:11
    本文学习循环神经网络(RNN)以及相关的项目,首先会介绍 RNN 经典的结构,以及它的几个变体 。 接着将在 TensorFlow 中使用经典的结构实现一个有趣的项目: CharRNN。 Char RNN 可以对文本的字符级概率进行建模,...

    本文学习循环神经网络(RNN)以及相关的项目,首先会介绍 RNN 经典的结构,以及它的几个变体 。 接着将在 TensorFlow 中使用经典的结构实现一个有趣的项目: CharRNN。 Char RNN 可以对文本的字符级概率进行建模,从而生成各种类型的文本 。

     

    一、RNN 的原理

    1、经典 RNN 的结构

    RNN 的英文全称是 Recurrent Neural Networks,即循环神经网络, 是一种对序列型数据进行建模的深度模型。在实际应用中有很多序列形的数据,例如:自然语言处理问题、语言处理、时间序列问题。序列形的数据不太好用原始的神经网络处理 。

     

    (1)隐状态h的计算

    为了处理建模序列问题 , 引入了隐状态 h ( hidden state )的概念,h 可以对序列形的数据提取特征,接着再转换为输出。

    在计算时,每一步使用的参数 U、W、b 都是一样的,即每个步骤的参数都是共享的,这是 RNN 的重要特点。图中的 U、 W是参数矩阵,b是偏置项参数,f是激活函数,在经典的RNN结构中,通常使用 tanh 作为激活函数 。

     

    (2)输出y的计算

    目前的 RNN 还没有输出,得到输出值的方法是直接通过 h 进行计算。一个箭头表示对相应的向量做一次类似于f(Wx+b)的变换。

               

    V 和 c 是新的参数,通常处理的是分类问题,因此使用 Softmax 函数将输出转换成各个类别的概率。此处的箭头表示对 hi 进行一次变换,得到输出 Yi。这就是经典的RNN结构,输入和输出序列必须是要等长的。由于输入和输出序列必须是要等长的,经典RNN的适用范围比较小 ,但也有一些问题适合用经典的RNN结构建模,如:计算视频中每一帧的分类标签;输入为字符 ,输出为下一个字符的概率。这是著名的 Char RNN 。

     

    (3)经典RNN数学定义

    设输入为 X1,X2,... ,对应的隐状态为 h1,h2,... , 输出为 Y1,Y2  ... ,则经典RNN的运算过程表示为

    其中, U,V,W,b,c均为参数,而 f 表示激活函数,一般为 tanh 函数。

     

    2、N VS 1 RNN的结构

    若问题的输入是一个序列,输出是一个单独的值而不是序列,此时应该如何建模呢?只在最后一个 h 上进行输出变换就可以了。这种结构通常用来处理序列分类问题 。如输入一段文字判别所属的类别,输入一个句子判断其情感倾向,输入一段视频并判断它的类别等等。

    (1)结构

     

    (2)数学定义

    设输入为 X1,X2,... ,对应的隐状态为 h1,h2,... ,输出为y,那么运算过程为:,输出时对最后一个隐状态做运算即可:

     

    3、1 VS N RNN的结构

    输入不是序列而输出为序列的情况怎么处理?

    (1)结构

    可以只在序列开始进行输入计算。

    还有一种结构是把输入信息X作为每个阶段的输入。

     

    (2)数学定义

    这种 1 vs N 的结构可以处理的问题有:从图像生成文字、从类别生成语言或音乐等。

     

    二、LSTM 的原理

    本文主要讲LSTM 的内部结构以及它相对于 RNN 的优点。LSTM ( Long Short-Term Memory,长短期记忆网络),是RNN 的改进版。一个 RNN 单元的输入由两部分组成,即前一步 RNN 单元的隐状态相当前这一步的外部输入, 此外还有一个输出 。 从外部结构看, LSTM 和 RNN 的输入输出一模一样, 同样是在每一步接受外部输入和前一阶段的隐状态,并输出一个值 。所以,将之前提到的RNN及其变体的每一种结构都无缝切换到 LSTM,而不会产生任何问题。

    从RNN的计算公式可以看出,RNN 每一层的隐状态都由前一层的隐状态经过变换和激活函数得到,反向传播求导时最终得到的导数会包含每一步梯度的连乘,这会引起梯度爆炸或梯度消失,所以 RNN 很难处理 “长程依赖”问题,即无法学到序列中蕴含的间隔时间较长的规律。LSTM 在隐状态计算时以加法代替了这里的迭代变换,可以避免梯度消失的问题 , 使网络学到长程的规律 。

     

    (1)RNN和LSTM的整体结构

     

    (2)隐状态 Ci 的传播

    和 RNN 有所不同, LSTM 的隐状态有两部分,一部分是hi ,一部分是 ci, ci 是在各个步骤间传递的主要信息,而水平线可以看作是 LSTM 的“主干道” 。 通过加法, ci可以无障碍地在这条主干道上传递 ,因此较远的梯度也可以在长程上传播 。 这是 LSTM 的核心思想 。

     

    (3)遗忘门的结构

    LSTM 的每一个单元中都有一个“遗忘门”,用来控制遗忘掉 Ci的那些部分。σ 是 Sigmoid 激活函数,它的输出在 0~1 之间。最终遗忘门的输出是和 Ci相同形状的矩阵,这个矩阵会和 Ci逐点相乘,决定要遗忘哪些东西 。遗忘门的输入是 Xi和Hi-1, Xi是当前时刻 的输入,而 hi-1为上一个时刻的隐状态。

     

    (4)记忆门的结构

    LSTM 的每一个单元中都有一个“记忆门”,用来控制记忆 Ci的那些部分。记忆门的输入同样是 Xi和Hi-1, 它的输出有2项,一项是it ,it同样经过 Sigmoid 函数运算得到,因此值都在 0~1 之间,还有一项是Ct,最终要记住的内容是Ct*it。

     

    (5)输出门的结构

    还需要一个“输出门”,用于输出内容。这里说是输出,其实是去计算另一个隐状态的值,真正的输出(如类别)需要通过 h1做进一步运算得到。同样是根据 Xi和Ht-1计算,Ot中每一个数值在 0~1 之间,ht通过Ot * tanh(Ct)得到 。

    总结一下, LSTM 每一步的输入是Xt,隐状态是ht和Ct , 最终的输出通过ht进一步变换得到 。 在大多数情况下, RNN 和 LSTM 都是可以相互替换的。

     

    三、CharRNN 的原理

    CharRNN 是用于学习 RNN 的一个非常好的例子。它使用的是最经典的 N vs N 的模型 , 即输入是长度为 N 的序列,输出是与之长度相等的序列 。对于 Char RNN,输入序列是句子中的字母,输出依次是该输入的下一个字母 , 换句话说,是用已经输入的字母去预测下一个字母的概率 。 如一个简单的英文句子 Hello! 输入序列是{H,e,l, l, o},输出序列依次是{e,I, I, o,!}。 注意到这两个序列是等长的,因此可以用 N VS N RNN来建模。

        

    使用独热向量来表示字母,然后依次输入网络。假设一共 26 个字母,那么字母 a的表示为第一位为1,其他 25位都是 0,即(1, 0, 0, 0,...,0), 字 母b的表示是第二位为 1,真他25位都是0,即(0,1,0,0,...,0)。输出相当 于一个 26 类分类问题,因此每一步输出的向量也是 26 维,每一维代表对应字母的概率,最后的损失使用交叉楠可以直接得到。

    相对于字母来说,汉字的种类比较多,可能会导致模型过大,对此有以下两种优化方法:

    (1)取最常用的 N 个汉字,将剩下的汉字变成单独一类。

    (2)在输入时,可以加入一层 embedding层,这个 embedding层可以将汉字转换为较为稠密的表示,它可以代替稀疏的独热表示,取得更好的效果。

     

    四、TensorFlow 中的 RNN 实现方式

    先是使用RNNCell对RNN模型进行单步建模。 RNNCell可以处理时间上的“一步“,即输入上一步的隐层状态和这一步的数据,计算这一步的输和隐层状态。接着,TensorFlow 使用 tf.nn.dynamic_rnn 方法在时间维度上多次运行RNN Cell。最后还需要对输出结果建立损失。

    1、基本单元 RNNCell

    RNNCell 是 TensorFlow 中的基本单元 。 它本身是一个抽象类,它有两个可以直接使用的子类,一个是 BasicRNNCell,还有一个 是 BasicLSTMCell,前者对应基本的 RNN ,后者是基本的 LSTM。学习 RNNCell 要重点关注三个地方:类方法 call、类属性state size、类属性 output_size。

    (1)类方法 call

    所有 RNNCell 的子类都会实现一个 call 函数 。利用 call 函数可以实现 RNN 的单步计算,它的调用形式为(output, next_state) = call(input, state)。例如:初始的输入为 x1,而初始的隐层状态为 h0,可以调用(output1, h1) = cell.call(x1, h0) 得到当前的隐层状态 h1。 接着调用 (output2, h2) = cell.call(x2,h1) 可以得到h2,依此类推。

    (2)类属性 state_size 和 output_size

    RNNCell 的类属性 state_size 和 output_size 分别规定了隐层的大小平日输 出向量的大小 。 通常是以 batch 形式输入数据 ,即 input 的形状~(batch_size, input_size), 1周用 call 函数时对应的隐层的形状是(batch_size, state_size),输 出的形状是(batch_size, output_size)。

    (3)定义一个RNN基本单元

    import tensorflow as tf
    import numpy as np
    
    # 在TensorFlow中定义一个基本RNN单元
    run_cell = tf.nn.run_cell.BasicRNNCell(num_units=128)
    print(run_cell.state_size)
    
    # 在TensorFlow中定义一个基本LSTM单元
    lstm_cell = tf.nn.run_cell.BasicLSTMCell(num_units=128)
    # lstm有两个隐状态c、h
    # state_size = LSTMStateTuple(c=l28, h=128)
    print(lstm_cell.state_size)
    
    # 可以通过state.h以及state.c进行访问
    lstm_cell= tf.nn.rnn_cell.BasicLSTMCell(num_units=128)
    inputs= tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size 
    hO = lstm_cell.zero_state(32, np.float32)
    output, hl = lstm_cell.call(inputs, hO)
    # shape=(32, 128)
    print (hl.h)
    # shape=(32, 128)
    print (hl.c) 

     

    2、多层RNNCell -- MultiRNNCell

    将 x输入第一层RNN 后得到隐层状态 h, 这个隐层状态相当于第二层 RNN 的输入,第二层 RNN的隐层状态、又相当于第三层RNN的输入,依此类推。在 TensorFlow 中可以使用 tf.nn.rnn_cell.multiRNNCell 函数对 RNN 进行堆叠。

    # 调用一次生成一个RNN单元
    def get_a_cell():
        return tf.nn.run_cell.BasicRNNCell(num_units=128)
    # 生成多层RNN单元
    cell= tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)])
    inputs= tf.placeholder(np.float32 , shape=(32, 100) )
    hO = cell.zero_state(32, np.float32)
    # 多层计算
    output, hl = cell.call(inputs, hO)

     

    3、call函数的返回值output

    调用 call 函数后得到(state_size,output_size),其中 h 对应了BasicRNNCell 的 state_size。 那么, y 是不是对应了 BasicRNNCell 的 output_size 呢 ?答案是否定的 。output 其实和隐状态的值是一样的。因此,还需要额外对输出定义新的变换,才能得到图中真正的输出 y。如果处理的是分类问题,那么还需要对 h 添加单独的 Softmax 层才能得到最后的分类概率输出。

     

    4、使用 tf.nn.dynamic_rnn 展开时间维度

    对于单个的RNNCell, 使用call函数进行运算时,只是在序列时间上前进了一步。如使用X1、h0得到h1, 通过X2、h1得到h2,也只在时间上前进了一步。如果序列长度为 n,要调用 n 次 call 函数,比较麻烦 。对此,TensorFlow 提供了一个 tf.nn.dynamic_mn 函数 , 使用该函数相当于调用了 n 次 call 函数。

    设输入数据的格式为(batch_size, time_steps, input_size),其中 batch_size 表示 batch 的大小,即一个 batch 中序列的个数。 time_steps 表示序列本身的长度。假设已经定义好了一个 RNNCell, 调用tf.nn.dynamic_rnn的代码是:

    # inputs: shape = (batch size, time steps, input size)
    # cell: RNNCell
    # initial_state: shape = (batch_size, cell.state_size)。
    outputs, state= tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)

    此时,得到的 outputs 是 time_steps 步里所有的输出。它的形状为 (batch_size, time_steps, cell.output_size)。 state 是最后一步的隐状态,它的形状为(batch_size, cell.state_size)。再对每一步的输出进行变换,可以得到损失并训练模型了。

     

    五、使用 TensorFlow 实现 CharRNN

    1、定义输入数据(model.py)

    def build_inputs(self):
        with tf.name_scope('inputs'):
            # 输入数据的形状为(self.num_seqs, self.num_steps)
            # self.num_seqs是句子的个数(相当于batch_size),而self.num_steps表示每个句子的长度
            self.inputs = tf.placeholder(tf.int32, shape=(self.num_seqs, self.num_steps), name='inputs')
            # seIf.targets是self.inputs对应的训练目标,形状和self.inputs相同,内容是self.inputs每个字母对应的下一个字母。
            self.targets = tf.placeholder(tf.int32, shape=(self.num_seqs, self.num_steps), name='targets')
            # self.keep_prob控制了Dropout层所需要的概率。在训练时,使用self.keep_prob=0.5,在测试时,self.keep_prob=1.0
            self.keep_prob = tf.placeholder(tf.float32, name='keep_prob')
    
            # 对于中文,需要使用embedding层
            # 英文字母没有必要用embedding层
            if self.use_embedding is False:
                self.lstm_inputs = tf.one_hot(self.inputs, self.num_classes)
            else:
                with tf.device("/cpu:0"):
                    embedding = tf.get_variable('embedding', [self.num_classes, self.embedding_size])
                    self.lstm_inputs = tf.nn.embedding_lookup(embedding, self.inputs)

     

    2、定义多层 LSTM 模型

    def build_lstm(self):
        # 创建单个cell
        def get_a_cell(lstm_size, keep_prob):
            lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_size)
            # 加入dropout,减少过拟合
            drop = tf.nn.rnn_cell.DropoutWrapper(lstm, output_keep_prob=keep_prob)
            return drop
    
        with tf.name_scope('lstm'):
            # 堆叠多层cell
            cell = tf.nn.rnn_cell.MultiRNNCell(
                [get_a_cell(self.lstm_size, self.keep_prob) for _ in range(self.num_layers)]
            )
            self.initial_state = cell.zero_state(self.num_seqs, tf.float32)
    
            # 通过dynamic_rnn对cell展开时间维度
            self.lstm_outputs, self.final_state = tf.nn.dynamic_rnn(cell, self.lstm_inputs, initial_state=self.initial_state)
    
            # 通过lstm_outputs得到概率
            seq_output = tf.concat(self.lstm_outputs, 1)
            x = tf.reshape(seq_output, [-1, self.lstm_size])
            with tf.variable_scope('softmax'):
                softmax_w = tf.Variable(tf.truncated_normal([self.lstm_size, self.num_classes], stddev=0.1))
                softmax_b = tf.Variable(tf.zeros(self.num_classes))
            # Wx+b变换
            self.logits = tf.matmul(x, softmax_w) + softmax_b
            # softmax层
            self.proba_prediction = tf.nn.softmax(self.logits, name='predictions')

     

    3、定义损失

    def build_loss(self):
        with tf.name_scope('loss'):
            y_one_hot = tf.one_hot(self.targets, self.num_classes)
            y_reshaped = tf.reshape(y_one_hot, self.logits.get_shape())
            # proba_prediction 和 targets 的独热编码做交叉熵作为损失
            loss = tf.nn.softmax_cross_entropy_with_logits(logits=self.logits, labels=y_reshaped)
            self.loss = tf.reduce_mean(loss)

     

    4、使用定义好的模型训练并生成英文

    训练文件 shakespeare.txt 保存在项目的 data/ 文件夹下,对应的训练的命令为:

    python train.py 
    --input_file data/shakespeare.txt # UTF-8格式
    --name shakespeare # 模型名称,决定模型保存位置,此时保存至model/shakespeare目录下
    --num_steps 50 # 序列长度
    --num_seqs 32 # 序列个数
    --learning_rate 0.01 
    --max_steps 20000

    运行后,模型保存至model/shakespeare目录下,使用该模型进行测试的命令为:

    python sample.py
    --converter_path model/shakespeare/converter.pkl 
    --checkpoint_path model/shakespeare/
    --max_length 1000

    运行报错:AttributeError: 'str' object has no attribute 'decode',str.decode('utf-8') 改为 str.encode('utf-8').decode('utf-8') 即可。

    参数converter_path含义:神经网络生成的是字母类别id,而不是字母,这些类别 id 在输入模型时是通过一个 converter 转换的,程序会自动把 converter保存在 model/shakespeare 目录下 。在输出时需要使用 converter 将类别 id 转换回字母。

     

    5、其他参数说明

    lstm_size: LSTM 隐层的大小,默认为 128。

    embedding_size : embedding 空间的大小,默认为 128。该参数只在使用use_embedding 时才会生效。

    num_layers: LSTM的层数,默认为2。

    max vocab: 使用的字母(汉字)的最大个数,默认为 3500。程序会自动挑选出使用最多的字,将剩下的字归为一类。

     

    六、总结

    本文介绍了 RNN 和 LSTM 的基本结构,使用 TensorFlow 实现 RNN ( LSTM )的基本步骤,最后实现了一个 CharRNN 项目。

     

    展开全文
  • 大白话循环神经网络RNN-从此爱上RNN

    千人学习 2020-04-20 22:57:50
    本系列讲解循环神经网络RNN和LSTM的所有知识点,学完本系列课程将对RNN和LSTM的理论知识有清晰的认识,同时能够将理论结合实践应用到工作中。

空空如也

1 2 3 4 5 ... 20
收藏数 21,007
精华内容 8,402
关键字:

rnn