野路子程序猿
  • 博客介绍
  • TensorFlow
    • TensorFlow数据读取
    • TensorBoard的使用
  • 弱监督学习
    • 论文: Snorkel DryBell: A Case Study in Deploying Weak Supervision at Industrial Scale
  • NLP之语言模型
    • 统计语言模型
    • 论文: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
  • NLP之关键词提取
    • 改进TF-IDF算法
    • 论文: 基于文档主题结构的关键词抽取方法研究
      • 1. 引言
      • 2. 基于文档内部信息构建主题的关键词抽取方法
      • 3. 基于隐含主题模型构建主题的关键词抽取方法
      • 4. 利用隐含主题模型和文档结构的关键词抽取方法
      • 5. 基于文档与关键词主题一致性的关键词抽取方法
  • 深度学习
    • 《深度学习与神经网络》笔记
      • 1. 使用神经网络识别手写数字
      • 2. 反向传播算法
      • 3. 如何提高神经网络学习算法的效果
      • 4. 神经网络可以实现任意函数的直观解释
      • 5. 深度神经网络学习过程中的梯度消失问题
      • 6. 深度学习
  • RNN
    • RNN常见结构
  • attention机制
    • 简述Attention机制及其在深度学习中的应用
    • 论文: Attention Is All You Need
  • Spark
    • Spark ML下实现的多分类AdaBoost + NaiveBayes算法
  • Python
    • Python函数式编程
  • 线性代数
    • 《Immersive Linear Algebra》笔记
      • 4. 向量叉乘(外积)
  • 机器学习
    • 指数分布族和广义线性回归
    • 条件随机场CRF
    • 信息论基本概念
  • 过拟合
    • Early Stopping
Powered by GitBook
On this page
  • 1. 介绍
  • 2. 基本RNN
  • 3. RNN的反向传播 -- BPTT
  • 4. RNN的梯度爆炸/消失
  • 5. BRNN
  • 6. LSTM
  • 7. GRU
  • 8. 结构的选择
  • 9. 结论
  • 参考资料

Was this helpful?

  1. RNN

RNN常见结构

PreviousRNNNextattention机制

Last updated 5 years ago

Was this helpful?

1. 介绍

深度学习中,虽然CNN除了被经常用来进行图像相关的任务外,也可以作为一种特征提取的方法用在NLP任务中。但是在NLP任务中,更多的我们还是使用RNN模型,本文就简单介绍几种常用的RNN结构。

2. 基本RNN

不同于在CNN模型中,网络的状态只决定于输入。在RNN中,最明显的一个特征就是它还决定于上一个时刻的状态,因此RNN经常被用来处理序列问题,被用在序列性质非常明显的NLP任务上。

基本的RNN结构如下:

它跟CNN和DNN区别最大的地方就在于这些前馈神经网络是个有向无环图的模型(DAG),而在RNN中是至少包含一个环的,在时间上进行展开我们可以更明显的看到这种特性:

公式可以表示为:

ht=tanh(Whxxt+Whhht−1+bh)ot=softmax(Wohht+bo)\begin{aligned}& h^t = tanh(W_{hx}x^t + W_{hh}h^{t-1}+b_h) \\\\ &o^t = softmax(W_{oh}h^t + b_o) \end{aligned}​ht=tanh(Whx​xt+Whh​ht−1+bh​)ot=softmax(Woh​ht+bo​)​

其中$x^t$是t时刻的输入,在NLP任务中,通常代表一句话中第t位置处的字。隐藏层的状态h不光决定于当前的输入,还决定于前一个时间t-1的状态,通过矩阵 WhhW_{hh}Whh​ 联系t-1的状态和当前状态,通过 WhxW_{hx}Whx​ 联系当前输入 xtx^txt 和当前状态,这里的激活函数通常可以使用tanh函数。对于t时刻的输出通常可以使用一个softmax回归,参数矩阵为 WohW_{oh}Woh​ 。

3. RNN的反向传播 -- BPTT

可以看到,类似于CNN在不同位置上共享卷积矩阵的参数,RNN在不同时刻t是共享参数的。对于不同时刻t,使用的是同样的 Whh,WohW_{hh}, W_{oh}Whh​,Woh​ 。对于RNN的反向传播有自己的一套算法BPTT,BP是反向传播,TT是Through Time。在介绍BPTT之前,先回忆一下在DNN中的反向传播。DNN的结构如下:

可以看到,前向过程的公式为: z1=W1x+b1a1=σ(z1)...zL=WLaL−1+bL−1aL=σ(zL)\begin{aligned} &z^1 = W^1x+b^1 \\\\ & a^1 = \sigma(z^1) \\\\ &... \\\\&z^L = W^La^{L-1}+b^{L-1}\\\\&a^L = \sigma(z^L) \end{aligned}​z1=W1x+b1a1=σ(z1)...zL=WLaL−1+bL−1aL=σ(zL)​ 方向传播的话先定义一个中间变量: δl=∂C∂zl\delta^l = \frac{\partial{C}}{\partial{z^l}}δl=∂zl∂C​ 代表损失函数C关于第l层的未激活输出$z^l$的偏导数,然后对于任意一层的参数 WlW^lWl 的偏导数为: ∂C∂Wl=∂C∂zl∂zl∂wl\frac{\partial{C}}{\partial{W^l}} = \frac{\partial{C}}{\partial{z^l}}\frac{\partial{z^l}}{\partial{w^l}}∂Wl∂C​=∂zl∂C​∂wl∂zl​ 其中 ∂zl∂wl\frac{\partial{z^l}}{\partial{w^l}}∂wl∂zl​ 为 al−1a^{l-1}al−1 ,这个在一次前向过程中已经全部求得,反向传播要做的事就是通过一次反向过程求得所有的 ∂C∂zl\frac{\partial{C}}{\partial{z^l}}∂zl∂C​ ,也就是所有层的 δl\delta^{l}δl 。

通过求导的链式法则: δl−1=∂C∂zl−1=∂al−1∂zl−1∂zl∂al−1∂C∂zl=σ′(zl−1)⋅(Wl)Tδl\delta^{l-1} = \frac{\partial{C}}{\partial{z^{l-1}}} = \frac{\partial{a^{l-1}}}{\partial{z^{l-1}}}\frac{\partial{z^l}}{\partial{a^{l-1}}}\frac{\partial{C}}{\partial{z^l}}=\sigma'(z^{l-1})\cdot(W^l)^T\delta^lδl−1=∂zl−1∂C​=∂zl−1∂al−1​∂al−1∂zl​∂zl∂C​=σ′(zl−1)⋅(Wl)Tδl 于是可以得到类似于前向过程的反向过程公式: δL=σ′(zL)⋅▽C(aL)δL−1=σ′(zL−1)⋅(WL)TδL\begin{aligned} &&\delta^L = \sigma'(z^L)\cdot \triangledown C(a^L)\\\\ && \delta^{L-1} = \sigma'(z^{L-1})\cdot(W^L)^T\delta^L\end{aligned}​​δL=σ′(zL)⋅▽C(aL)δL−1=σ′(zL−1)⋅(WL)TδL​ 可以看到,前向过程是从第1层一直到第L层进行计算,而这里是从第L层到第1层计算每层的 δl\delta^lδl ,所以这种算法被称为反向传播算法。

讲完了DNN的情况,这里再来理解RNN的情况。RNN的特殊在于时间t引入。将RNN沿时间进行展开:

可以发现,RNN展开后的结构和DNN的结构原理上是一样的。只是在DNN中,链接发生在相邻的隐藏层上面。而在RNN中,链接发生在相邻时间上。

然后DNN每一层的参数是不一样的,而在RNN中一个序列样本下,不同时间上的参数是一致的。在这种情况下,如果我们最后考虑的损失函数只和最末尾的t时刻的输出 oto^tot 相关的话(情感分析,文本分类等都属于这种情况),在进行参数 β\betaβ 的更新时需要考虑每个时刻t上的损失函数对 β\betaβ 的梯度之和,即: ∂Ct∂β=∑k∂Ct∂βk\frac{\partial{C^t}}{\partial{\beta}} = \sum_k \frac{\partial{C^t}}{\partial{\beta^k}}∂β∂Ct​=∑k​∂βk∂Ct​ 这里的 βk\beta^kβk 是参数在时间k上的状态,可以看作是DNN中的第k层的参数,于是利用反向传播可以求出每个时刻k的偏导数。但是实际上不同时刻k的参数 β\betaβ 是一个统一的参数,因此需要进行累加作为对参数 β\betaβ 总的偏导数。

如果我们考虑的损失函数和每个时刻的输出都有关(序列标注等属于这种情况),即 C=f(C1,C2,...,Ct−1,Ct)C = f(C^1, C^2, ..., C^{t-1}, C^t)C=f(C1,C2,...,Ct−1,Ct) ,则有: ∂C∂β=∑k=1t∂Ck∂β∂C∂Ck\frac{\partial{C}}{\partial{\beta}} = \sum_{k=1}^t\frac{\partial{C^k}}{\partial{\beta}}\frac{\partial C}{\partial{C^k}}∂β∂C​=∑k=1t​∂β∂Ck​∂Ck∂C​ 对于其中的每一时刻 ∂Ck∂β\frac{\partial C^k}{\partial \beta}∂β∂Ck​ ,都需要考虑k时刻以前所有的对 β\betaβ 的偏导数的和: ∂Ck∂β=∑j=1k∂Ck∂βj\frac{\partial C^k}{\partial \beta}= \sum_{j=1}^k \frac{\partial C^k}{\partial \beta^j}∂β∂Ck​=∑j=1k​∂βj∂Ck​ 于是得到,关于 β\betaβ 的总的导数为: ∂C∂β=∑k=1t∑j=1k∂Ck∂βj∂C∂Ck\frac{\partial{C}}{\partial{\beta}} = \sum_{k=1}^t\sum_{j=1}^k \frac{\partial C^k}{\partial \beta^j}\frac{\partial C}{\partial{C^k}}∂β∂C​=∑k=1t​∑j=1k​∂βj∂Ck​∂Ck∂C​

4. RNN的梯度爆炸/消失

可以看到RNN的BPTT算法与DNN的BP算法非常相似,只是前者发生在时间上,后者发生在隐藏层上。那么对于时间跨度很长的情况,BPTT就很可能会发生梯度爆炸或者梯度消失的情况。

激活函数:

像DNN一样,如果激活函数是 σ\sigmaσ 函数或者tanh函数,进行反向传播的时候很容易就会导致梯度很小,产生梯度消失的问题。

参数 WhhW_{hh}Whh​ :

不同于DNN中每层的参数是不一样的,RNN中的参数 WhhW_{hh}Whh​ 每个时刻是一个参数,所以在进行反向传播的时候会进行 WhhW_{hh}Whh​ 的累乘。

当 WhhW_{hh}Whh​ 为对角阵时,我们就有结论:

  • 当对角线元素小于1,则其幂次会趋近于0,进而导致梯度消失

  • 当对角线元素大雨1,则其幂次会趋近于无穷大,进而导致梯度爆炸

当 WhhW_{hh}Whh​ 不是对角阵时,对矩阵进行随机初始化。观察累乘后的分布如下:

可以看到,经过一定次数的相乘以后,大部分的数值都是趋近于绝对值大的数,要么趋近于0。这就分别对应了梯度爆炸和梯度消失的情况。

理论上, WhhW_{hh}Whh​ 是个方阵,简化问题,假设它是可以进行对角化的,那么可以分解为 Q∑Q−1Q\sum Q^{-1}Q∑Q−1 ,其中的 ∑\sum∑ 也是对角矩阵,累乘的话同样会发生上面讨论的对角阵相乘的情况。

处理梯度消失/爆炸的方法

梯度消失:

传统的使用RELU等激活函数的方法有效,但是存在更好的RNN架构直接就可以解决这样的问题,比如下文中要介绍的LSTM,GRU等。

梯度爆炸:

通常还是使用Gradient Clipping,在梯度大于一个阈值的时候,进行动态的放缩,将它限制在一定范围内。

5. BRNN

BRNN就是Bi-directional RNN,双向的RNN。前面的讨论都是单向的,从前到后,双向就是多了一层,从后到前:

对于每一个时刻t,它隐藏层的状态不光决定于前向层经过该时刻带来的状态h1,还决定于后向层经过该时刻带来的状态h2,然后进行链接[h1, h2]得到的就是在时刻t上的状态。两层可以分别保留各自的参数 WhhW_{hh}Whh​ ,虽然使用不同的维度在理论上是可以的,但是实际上通常前向层和反向层保持维度一致。

这种模型带来的好处是显而易见的,它不光能够考虑当前时间受前面时间的影响,还能考虑受后面时间的影响,看几个简单的例子来感受一下:

  • 在命名实体识别中,“我们爱吃红烧肉”,根据前向层很容易根据爱吃推断红烧肉是一道菜名,但是在“红烧肉很好吃”中,如果只根据前向,就比较难判断了,这个时候如果加上后向层,就可以根据很好吃判断它前面的字段红烧肉是菜名了。

  • 在情感分析中,“这个公园好美啊,尽管有一点拥挤”,如果只看前向可能会比较倾向于把这个句子判断为负情感,但是如果加上反向的特征的话,就更容易判断争取,识别为正情感。

当然上面的例子只是简要说明一下双向的好处,实际的算法肯定不是这么浅显直观的计算的。

既然BRNN只是在RNN的基础上反向加了一层,本质是一样的,只是训练的时候考虑两层的参数,这里就不再重复介绍了。

6. LSTM

前面的讨论表明理论上RNN在考虑前后文联系的时候可以发挥不错的作用,尤其是当相关信息的位置间隔比较短的时候:

但是根据前一小节的讨论,当相关信息的位置间隔越来越长的时候,由于存在梯度消失的问题,在基础RNN上进行这样的参数学习非常困难的。

LSTM,long short term网络的出现就解决了这样的问题,它可以非常轻松的学习到序列中的长期依赖信息。

LSTM通常指的是里面的RNN网络中的LSTM Cell,类似于下图:

普通的RNN Cell,并没有中间那么复杂的链接,基本就是一个隐藏层+激活函数(通常使用tanh),而在在上图的LSTM Cell中有四个进行交互的层,后面可以看到这些是LSTM中的各种功能的门函数。

6.1 LSTM核心思想

LSTM中很重要的信息载体就是其每时刻的细胞状态,它会沿着水平线上传播。它在整个链上传播的过程中,会进行一些简单的线性操作。

这些线性操作会使得信息可以增加或者减少,而决定信息是如何增加或者减少的话就要依靠LSTM中定义的各种门函数。每个门可以理解为一个开关,0表示不通过,1表示通过,0到1之间的数表示部分通过,因此门函数使用sigmoid神经网络层代替,在sigmoid输出一个值后进行pointwise的操作:

6.2 LSTM中的三个门

遗忘门

在LSTM的第一步中要决定过去的信息需要遗忘多少,保留多少,遗忘们就起到这样的作用。它通过一个参数矩阵 WfW_fWf​ (f代表forget),作用于上一个隐藏层的状态 ht−1h_{t-1}ht−1​ 和当前输入 xtx_txt​ ,然后通过sigmoid函数得到一个[0, 1]的数:

回到具体的语言模型情况,考虑当前的细胞状态 Ct−1C_{t-1}Ct−1​ 包含当前主语的性别,我们想要预测合适的代词。如果当前的输入 xtx_txt​ 包含新的主语,这个时候就要通过遗忘门来忘掉旧的主语。

输入门

知道了要忘掉哪些信息后,就要决定记住哪些新的输入信息了。

首先新的信息依靠一个参数矩阵 WCW_CWC​ 和tanh激活函数,然后哪些信息需要记忆依靠的是类似遗忘门的sigmoid神经层计算的一个输入门函数。

其中 iti_tit​ 是输入门层,决定要更新什么值,就是要记住哪些新的信息,然后 C~t\tilde{C}_tC~t​ 是候选的新的状态。

然后依靠遗忘门和输入门的线性组合就可以得到新的细胞状态了:

可以看到新的细胞状态有两个部分,一个是有旧的细胞状态带来的,一个是有新的输入信息带来的。

语言模型的例子里,我们希望模型新的细胞状态能记住新的主语的性别。

输出门

在得到了新的细胞状态 CtC_tCt​ 后,就能够直接通过激活函数(tanh)得到我们的输出了。不过这里我们仍然要一个门控制输出哪些信息,同样使用sigmoid神经层:

在语言模型的例子里,有可能是看到一个代词就选择输出一个跟动词有关的信息。

LSTM结构的个人理解

通过上面LSTM结构的解析,我们可以看到网络的输出和计算三个门的值所使用的都是hth_tht​。于是可以这样理解,在LSTM网络沿着时间传播的过程中,细胞状态 CtC_tCt​ 时刻保存着最重要的信息。然后上一时刻的网络输出 ht−1h_{t-1}ht−1​ 和当前的网络输入 xtx_txt​ 通过对应的矩阵计算得到三个门的值。输入门和遗忘门负责合并过去细胞状态和当前新信息,输出门负责控制对更新后的细胞状态的选择性输出。当前时刻的输出又被用来进行下一时刻三个门的计算。

6.3 LSTM的变体

上面的LSTM是标准的LSTM结构,下面讨论几个变体。

peephole connection

从上面的结构可以看出peephole connection就是让门的计算也同样受细胞状态的影响:遗忘门和输入门受上一时刻细胞状态 Ct−1C_{t-1}Ct−1​ 的影响,计算输出门时当前的时刻的细胞状态 CtC_tCt​ 已经计算好了,于是收到 CtC_tCt​ 的影响。从公式中也可以很容易看出三个门计算中对于细胞状态的引入。

当然三个peephole connection可以有选择的添加,并非一定要三个一起加入。

coupled forget gate and input gate

不同一开始遗忘门和输入门分别计算,coupled就是只计算遗忘门 ftf_tft​ ,然后输入门就是 1−ft1-f_t1−ft​ 。

在这种情况下,当遗忘门确定的时候,输入门也同时被确定。当我们要保留旧细胞状态的时候( ft=1f_t = 1ft​=1 ),新信息就完全不会被加入。当我们要遗忘旧细胞状态的时候( ft=0f_t = 0ft​=0 ),新的细胞状态完全由新信息决定。

6.4 LSTM与梯度消失

在描述LSTM的一开始,就说了它可以处理在长期记忆上产生的梯度消失问题,它具体是怎们做的呢?

先看一下LSTM反向传播时 δk=∂Ct∂ck\delta^k = \frac{\partial C^t}{\partial c^k}δk=∂ck∂Ct​ 是怎么传播的: δk−1=∂Ct∂ck−1=∂Ct∂ck∂ck∂ck−1=δk∂ck∂ck−1\delta^{k-1} = \frac{\partial C^t}{\partial c^{k-1}} = \frac{\partial C^t}{\partial c^k} \frac{\partial c^k}{\partial c^{k-1}} = \delta^k \frac{\partial c^k}{\partial c^{k-1}}δk−1=∂ck−1∂Ct​=∂ck∂Ct​∂ck−1∂ck​=δk∂ck−1∂ck​ 根据LSTM的公式 ct=ft⋅ct−1+it⋅gtc^t = f^t \cdot c^{t-1} + i^t \cdot g^tct=ft⋅ct−1+it⋅gt 可以的到上式可以转变为: δk−1=δk(ft+...)\delta^{k-1} = \delta^k (f^t + ...)δk−1=δk(ft+...) 上面的省略号是一些无关部分,不影响我们的分析。可以看到,当 ft=1f^t = 1ft=1 时,省略号那部分无论多小,梯度都是可以很容易反向传播的。此时,即使是学习长期记忆也不会发生梯度消失问题。当 ft=0f^t=0ft=0 时,上一时刻的信号不影响到当前时刻,ftf^tft 在这里控制着梯度传递到前一时刻的衰减程度,和它在遗忘门上的功能是一致的。

7. GRU

GRU和LSTM有着一定的相似性,算是一个简化版本的LSTM。它将遗忘门和输入门合并为更新门,因此只有两个门。而且它没有细胞状态 CtC_tCt​ 的流动,只保留隐藏状态 HtH_tHt​ 。

上图中的 rtr_trt​ 是reset gate 重置门,它负责对过去的隐藏状态 ht−1h_{t-1}ht−1​ 进行取舍,以用来计算新的候选隐藏状态 ht~\tilde{h_t}ht​~​ 。不同于LSTM中,门函数是作用于tanh激活的计算结果,这里的重置门直接作用于计算过程中的 ht−1h_{t-1}ht−1​ ,可以看到如果 rt=0r_t=0rt​=0 ,在计算 ht~\tilde{h_t}ht​~​ 时,完全受当前输入 xtx_txt​ 的影响。

ztz_tzt​ 是更新门,它负责选择 ht−1h_{t-1}ht−1​ 在新的输出 hth_tht​ 中所要记住的信息,注意到这里旧的信息和新的信息的合并是coupled。

8. 结构的选择

至于LSTM和GRU的选择,通常效果也差不多,不过GRU有更少的参数,相对来说容易训练,过拟合的情况也会好很多,更适用于训练数据较少的情形。

9. 结论

目前看来RNN架构的发展是要弱于CNN的,不过LSTM和GRU解决了RNN中长期依赖的问题,在RNN中取得了巨大的成功。由于RNN常用的结构少于CNN,这里就用一篇文章总结了一下常见RNN的结构。不过论模型的难度的话,无论是理论还是应用,RNN都是要难于CNN的。

LSTM的出现算得上是RNN中一个突破性的进展,接下来可能attention机制是一个很重要的研究热点。自己今后也要多多关注和学习这一方面的内容。

参考资料

上面讲了LSTM,GRU及其很多变体,给出了一些流行变体的比较,结论是它们基本一致。其它的一些论文比较,也通常只是发现在具体任务上,某些结构可能会优于某些结构,但是总的来说,并没有哪个结构一定是最好的。

[1] - 李宏毅

[2] - 余文毅

[3] - Christopher Olah

[4] - Klaus Greff, et al

[5] - Alex Graves, et al

Greff, et al.(2015)
Understanding Deep Learning in One Day
当我们在谈论数据挖掘
Understanding LSTM Networks
LSTM: A Search Space Odyssey
Framewise Phoneme Classification with Bidirectional LSTM and Other Neural Network Architectures