LSTM和GRU真是NLP的神器,太好用了,不过得学好了才能用好。本文整合了网上关于RNN的资料,然后自己用python实现了经典的RNN,LSTM和GRU单元以便更加深入的理解。
RNN
你在阅读这个句子时,你是一个词一个词地阅读(或者说,眼睛一次扫视一次扫视地阅读),同时会记住之前的内容。这让你能够动态理解这个句子所传达的含义。生物智能以渐进的方式处理信息,同时保存一个关于所处理内容的内部模型,这个模型是根据过去的信息构建的,并随着新信息的进入而不断更新。
循环神经网络(RNN, recurrent neural network)采用同样的原理,不过是一个极其简化的版本:它处理序列的方式是,遍历所有序列元素,并保存一个状态(state),其中包含与已查看内容相关的信息。实际上, RNN 是一类具有内部环的神经网络。在处理两个不同的独立序列之间, RNN 状态会被重置,因此,你仍可以将一个序列看作单个数据点,即网络的单个输入。真正改变的是,数据点不再是在单个步骤中进行处理,相反,网络内部会对序列元素进行遍历。 RNN示意如下图:
RNN详细的结构图如下:
这是一个标准的RNN结构图,图中每个箭头代表做一次变换,也就是说箭头连接带有权值。左侧是折叠起来的样子,右侧是展开的样子,左侧中h旁边的箭头代表此结构中的“循环“体现在隐层。
在展开结构中我们可以观察到,在标准的RNN结构中,隐层的神经元之间也是带有权值的。也就是说,随着序列的不断推进,前面的隐层将会影响后面的隐层。
上图中O代表输出,y代表样本给出的确定值,L代表损失函数,我们可以看到,“损失“也是随着序列的推荐而不断积累的。标准RNN的还有以下特点:1、权值共享,图中的W全是相同的,U和V也一样。2、每一个输入值都只与它本身的那条路线建立权连接,不会和别的神经元连接。
RNN单元的代码实现:
import numpy as np time_step=5 #你 可 真是 个 帅比 input_feature=6 #输入特征数 out_feature=7 #输出特征数 #输入数据 input_data=np.array([ [1,0,0,0,0,0],#你 [0,1,0,0,0,0],#可 [0,0,1,0,0,0],#真是 [0,0,0,1,0,0],#个 [0,0,0,0,1,0],#帅比 ]) result=[] #创建初始状态矩阵 state_t=np.zeros((out_feature,)) #创建随机的权重矩阵 W=np.random.random((out_feature,out_feature))#状态转移权重矩阵 U=np.random.random((out_feature,input_feature))#输入权重矩阵 b=np.random.random((out_feature,)) #偏置 for input_t in input_data: #维度说明 #input_t:(1,6),U:(7,6),矩阵乘法np.dot(U,input_t)得到的维度为(1,7) #W:(7,7),state_t:(1,7),矩阵乘法np.dot(W,state_t)得到的维度为(1,7) #偏置矩阵b的的维度是(1,7),所以输出也是(1,7) output_t=np.tanh(np.dot(U,input_t)+np.dot(W,state_t)+b) result.append(output_t) state_t=output_t output_data=np.stack(result,axis=0) print(output_data)
运行结果:
接下来引用一下博主SandaG的文章中的动图:
上图中是原始输入文本为:What time is it ?,经过RNN的编码后,可以得到保存了前面输入的单词信息的一个状态。但是也可以看到,在后面的状态中,前面单词的信息所占的比例越来越小,这就是所谓的梯度消失。
训练神经网络有三个主要步骤。首先,它进行前向传递并进行预测。其次,它使用损失函数将预测与基础事实进行比较。损失函数输出一个误差值。最后,它使用该误差值进行反向传播,计算网络中每个节点的梯度。
梯度是用于调整网络内部权重的值从而更新整个网络。梯度越大,调整越大。在进行反向传播时,图层中的每个节点都会根据渐变效果计算它在其前面的图层中的渐变。因此,如果在它之前对层的调整很小,那么对当前层的调整将更小。这会导致渐变在向后传播时呈指数级收缩。由于梯度极小,内部权重几乎没有调整,因此较早的层无法进行任何学习。这就是消失的梯度问题。
LSTM和GRU可以解决梯度消失的问题。
LSTM
长短期记忆网络——通常也被简称为 LSTMs——是一种特殊类型的 RNN,能够学习长期的依赖关系。经典的RNN如下图所示:
而LSTM如下图所示:
黄色的矩形是学习得到的神经网络层,粉色的圆形表示一些运算操作,比如加法或乘法,黑色的单箭头表示向量的传输,两个箭头合成一个表示向量的连接,一个箭头分开表示向量的复制。
核心思想
LSTMs 的核心所在是 cell 的状态(cell state),也就是下图这条向右的线。Cell 的状态就像是传送带,它的状态会沿着整条链条传送,而只有少数地方有一些线性交互。信息如果以这样的方式传递,实际上会保持不变。
LSTM 通过一种名为「门」(gate)的结构控制 cell 的状态,并向其中删减或增加信息。可以把门理解为一种控制信息通过的方式。门由一个 sigmoid 网络层与一个按位乘操作构成。
S igmoid 层的输出值在 0 到 1 间,表示每个部分所通过的信息。0 表示「对所有信息关上大门」;1 表示「我家大门常打开」。一个 LSTM 有三个这样的门,控制 cell 的状态。
遗忘门
LSTM 的第一步需要决定我们需要从 cell 中抛弃哪些信息,这个决定是从 sigmoid 中的「遗忘层」来实现的。它的输入是ht-1(前一时间步的输出)和 xt(当前输入),输出为一个0到1之间的数。Ct1 就是每个在 cell 中所有在 0 和 1 之间的数值,越接近0意味着忘记,越接近1意味着要保持。
输入门
下一步,我们需要决定什么样的信息应该被存储起来。这个过程主要分两步。首先是 sigmoid 层(输入门)决定我们需要更新哪些值;随后,tanh 层生成了一个新的候选向量 C`,它能够加入状态中。最后,我们将这两个值结合起来,并更新 cell 的状态。
细胞状态
接下来,我们就可以更新 cell 的状态了。将旧状态与 ft 相乘,忘记此前我们想要忘记的内容,然后加上 C`。得到的结果便是新的候选值,依照我们决定的值进行缩放。
输出门
最后,我们需要确定输出的内容。这个输出内容取决于我们的 cell 状态,但这是一个经过过滤的版本。首先,我们会运行一个 sigmoid 层决定 cell 状态输出哪一部分。随后,我们把 cell 状态通过 tanh 函数,将输出值保持在-1 到 1 间。之后,我们再乘以 sigmoid 门的输出值,就可以得到结果了。
代码实现
import numpy as np def sigmoid(x): return 1/(1+np.exp(-x)) time_step=5 #你 可 真是 个 帅比 input_feature=6 #输入特征数 out_feature=7 #输出特征数 #输入数据 input_data=np.array([ [1,0,0,0,0,0],#你 [0,1,0,0,0,0],#可 [0,0,1,0,0,0],#真是 [0,0,0,1,0,0],#个 [0,0,0,0,1,0],#帅比 ]) result=[] #创建初始状态矩阵 Ct=np.zeros((out_feature,)) #创建上一次输出矩阵 h_t_1=np.zeros((out_feature,)) #创建权重矩阵 Wf=np.random.random((out_feature,out_feature+input_feature)) bf=np.random.random((out_feature,)) Wi=np.random.random((out_feature,out_feature+input_feature)) bi=np.random.random((out_feature,)) Wc_=np.random.random((out_feature,out_feature+input_feature)) bc_=np.random.random((out_feature,)) Wo=np.random.random((out_feature,out_feature+input_feature)) bo=np.random.random((out_feature,out_feature)) for x in input_data: #遗忘门 t=np.dot(Wf,np.concatenate([h_t_1,x],axis=0)) ft=sigmoid(t+bf) #输入门 it=sigmoid(np.dot(Wi,np.concatenate([h_t_1,x],axis=0))+bi) Ct_=np.tanh(np.dot(Wc_,np.concatenate([h_t_1,x],axis=0))+bc_) #细胞状态 Ct=ft*Ct+it*Ct_ #输出门 ot=sigmoid(np.dot(Wo,np.concatenate([h_t_1,x],axis=0))+bo) ht=np.dot(ot,np.tanh(Ct)) result.append(ht) h_t_1=ht result=np.array(result) print(result)
程序运行结果:
LSTM变体
Gers & Schmidhuber (2000) 提出的「猫眼连接」(peephole connections)的神经网络,也就是说,门连接层能够接收到 cell 的状态。
另一种变体就是采用一对门,分别叫遗忘门(forget)及输入门(input)。与分开决定遗忘及输入的内容不同,现在的变体会将这两个流程一同实现。我们只有在将要输入新信息时才会遗忘,而也只会在忘记信息的同时才会有新的信息输入。
最流行的变体就是GRU了,下面接着介绍。
GRU
GRU(Gated Recurrent),由 Cho, et al. (2014) 提出。他将遗忘门与输入门结合在一起,名为「更新门」(update gate),并将 cell 状态与隐藏层状态合并在一起,此外还有一些小的改动。这个模型比起标准 LSTM 模型简单一些,因此也变得更加流行了。
GRU摆脱了细胞状态并使用隐藏状态来传输信息。它只有两个门,一个重置门和一个更新门。zt和rt分别表示更新门和重置门。 GRU的张量操作较少;因此,它比LSTM更快一点。
重置门rt
重置门是另一个门,它决定忘记过去的信息量。
更新门zt
更新门的作用类似于LSTM的遗忘门和输入门。它决定了要丢弃哪些信息以及要添加的新信息。从上图可以看到,更新门采用一对门,只有在将要输入新信息时才会遗忘,而也只会在忘记信息的同时才会有新的信息输入。
代码实现
import numpy as np def sigmoid(x): return 1/(1+np.exp(-x)) time_step=5 #你 可 真是 个 帅比 input_feature=6 #输入特征数 out_feature=7 #输出特征数 #输入数据 input_data=np.array([ [1,0,0,0,0,0],#你 [0,1,0,0,0,0],#可 [0,0,1,0,0,0],#真是 [0,0,0,1,0,0],#个 [0,0,0,0,1,0],#帅比 ]) result=[] #创建上一次输出矩阵 h_t_1=np.zeros((out_feature,)) #创建权重矩阵 Wr=np.random.random((out_feature,out_feature+input_feature)) br=np.random.random((out_feature,)) Wz=np.random.random((out_feature,out_feature+input_feature)) bz=np.random.random((out_feature,)) Wh_=np.random.random((out_feature,out_feature+input_feature)) bh_=np.random.random((out_feature,)) for x in input_data: #重置门 t=np.dot(Wr,np.concatenate([h_t_1,x],axis=0)) rt=sigmoid(t+bf) h_t_2=rt*h_t_1 #更新门 zt=sigmoid(np.dot(Wz,np.concatenate([h_t_1,x],axis=0))+bz) t=np.concatenate([h_t_2,x],axis=0) temp=Wh_*t h_t_=np.tanh(+bh_) ht=h_t_1*(1-zt)+h_t_*zt result.append(ht) h_t_1=ht result=np.array(result) print(result)
运行结果:
参考资料
[1]Francois Chollet.Deep Learning with Python.张亮(译)Python深度学习.人民邮电出版社.2018-8
[2]SandaG.如何深度理解RNN?——看图就好!.https://baijiahao.baidu.com/s?id=1612358810937334377&wfr=spider&for=pc. 2018-09-23
[2]了不起的赵队.RNN.https://blog.csdn.net/zhaojc1995/article/details/80572098. 2018-06-06
[3]集智俱乐部.gru深度丨目前最受欢迎的 LSTM 教程:谷歌大脑科学家亲解.http://dy.163.com/v2/article/detail/D3PM6QIO0511D05M.html. 2017-11-21