循环神经网络(RNN)及衍生LSTM、GRU详解

  我们之前所学的全连接神经网络(DNN)和卷积神经网络(CNN),他们的前一个输入和后一个输入是没有关系的。但是当我们处理序列信息的时候,某些前面的输入和后面的输入是有关系的,比如:当我们在理解一句话意思时,孤立的理解这句话的每个词是不够的,我们需要处理这些词连接起来的整个序列;这个时候我们就需要使用到循环神经网络(Recurrent Neural Network)

RNN在自然语言处理领域最先被使用起来,RNN可以为语言模型进行建模:

我没有完成上级布置给我的任务,所以  被开除了

让电脑来填写下划线的词最有可能的是『我』,而不太可能是『小明』,甚至是『吃饭』。

语言模型就是这样的东西:给定一个一句话前面的部分,预测接下来最有可能的一个词是什么。

基本循环神经网络

  我们首先来学习 基本的 循环神经网络,结构由 输入层、一个隐藏层和输出层 组成。

  $x$是输入向量,$o$是输出向量,$s$表示隐藏层的值;$U$是输入层到隐藏层的权重矩阵,$V$是隐藏层到输出层的权重矩阵循环神经网络的隐藏层的值$s$不仅仅取决于当前这次的输入$x$,还取决于上一次隐藏层的值$s$。权重矩阵$W$就是隐藏层上一次的值作为这一次的输入的权重。

  我们将上图的基本RNN结构在时间维度展开(RNN是一个链式结构,每个时间片使用的是相同的参数):

RNN时间线展开图

现在看上去就会清楚许多,这个网络在$t$时刻接收到输入$x_t$之后,隐藏层的值是$s_t$,输出值是$o_t$关键一点是,$s_t$的值不仅仅取决于$x_t$,还取决于$s_{t-1}$

$$公式1:s_t=f(U*x_t+W*s_{t-1})$$

$$公式2:o_t=g(V*s_t)$$

  式1是隐藏层的计算公式,它是循环层。$U$是输入$x$的权重矩阵,W是上一次隐藏层值$S_{t-1}$作为这一次的输入的权重矩阵,$f$是激活函数。

  式2是输出层的计算公式,V是输出层的权重矩阵,g是激活函数。

  隐含层有两个输入,第一是$U$与$x_t$向量的乘积,第二是上一隐含层输出的状态$s_{t-1}$和$W$的乘积。等于上一个时刻计算的$s_{t-1}$需要缓存一下,在本次输入$x_t$一起计算,共同输出最后的$o_t$。

如果反复把式1带入式2,我们将得到:

$$\begin{align} \mathrm{o}_t&=g(V\mathrm{s}_t)\\ &=Vf(U\mathrm{x}_t+W\mathrm{s}_{t-1})\\ &=Vf(U\mathrm{x}_t+Wf(U\mathrm{x}_{t-1}+W\mathrm{s}_{t-2}))\\ &=Vf(U\mathrm{x}_t+Wf(U\mathrm{x}_{t-1}+Wf(U\mathrm{x}_{t-2}+W\mathrm{s}_{t-3})))\\ &=Vf(U\mathrm{x}_t+Wf(U\mathrm{x}_{t-1}+Wf(U\mathrm{x}_{t-2}+Wf(U\mathrm{x}_{t-3}+...)))) \end{align}$$

从上面可以看出,循环神经网络的输出值$o_t$,是受前面历次输入值$x_t、x_{t-1}、x_{t-2}、x_{t-3}、...$影响的,这就是为什么循环神经网络可以往前看任意多个输入值的原因。这样其实不好,因为如果太前面的值和后面的值已经没有关系了,循环神经网络还考虑前面的值的话,就会影响后面值的判断。

双向循环神经网络

对于语言模型来说,很多时候光看前面的词是不够的,比如下面这句话:我的手机坏了,我打算  一部新的手机。 我们这个时候就需要双向循环神经网络。

从上图可以看出,双向卷积神经网络的隐藏层要保存两个值,一个A参与正向计算,另一个值A'参与反向计算。最终的输出值$y_2$取决于$A_2$和${A}'_2$。其计算方法为:

$$\mathrm{y}_2=g(VA_2+V'A_2')$$

$A_2$和${A}'_2$则分别计算:

$$\begin{align}
A_2&=f(WA_1+U\mathrm{x}_2)\\
A_2'&=f(W'A_3'+U'\mathrm{x}_2)\\
\end{align}$$

现在,我们已经可以看出一般的规律:正向计算时,隐藏层的值$S_t$与$S_{t-1}$有关;反向计算时,隐藏层的值${S}'_t$与${S}'_{t+1}$有关;最终的输出取决于正向和反向计算的加和。现在,我们仿照式1和式2,写出双向循环神经网络的计算方法:

$$\begin{align}
\mathrm{o}_t&=g(V\mathrm{s}_t+V'\mathrm{s}_t')\\
\mathrm{s}_t&=f(U\mathrm{x}_t+W\mathrm{s}_{t-1})\\
\mathrm{s}_t'&=f(U'\mathrm{x}_t+W'\mathrm{s}_{t+1}')\\
\end{align}$$

从上面三个公式我们可以看到,正向计算和反向计算不共享权重,也就是说U和U'、W和W'、V和V'都是不同的权重矩阵。

梯度爆炸和消失问题

实践中前面介绍的几种RNNs并不能很好的处理较长的序列,RNN在训练中很容易发生梯度爆炸和梯度消失,这导致梯度不能在较长序列中一直传递下去,从而使RNN无法捕捉到长距离的影响。

通常来说,梯度爆炸更容易处理一些。因为梯度爆炸的时候,我们的程序会收到NaN错误。我们也可以设置一个梯度阈值,当梯度超过这个阈值的时候可以直接截取。

梯度消失更难检测,而且也更难处理一些。总的来说,我们有三种方法应对梯度消失问题:

1、合理的初始化权重值。初始化权重,使每个神经元尽可能不要取极大或极小值,以躲开梯度消失的区域。

2、使用relu代替sigmoid和tanh作为激活函数。原理请参考上一篇文章零基础入门深度学习(4) - 卷积神经网络的激活函数一节。

3、使用其他结构的RNNs,比如长短时记忆网络(LTSM)和Gated Recurrent Unit(GRU),这是最流行的做法。

长短期记忆网络

  原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么如果我们再增加一个门(gate)机制用于控制特征的流通和损失,即c,让它来保存长期的状态,这就是长短时记忆网络(Long Short Term Memory,LSTM)。

新增加的状态c,称为单元状态。我们把LSTM按照时间维度展开:

可以看到在$t$时刻,

LSTM的输入有三个:当前时刻网络的输出值$x_t$、上一时刻LSTM的输出值$h_{t-1}$、以及上一时刻的记忆单元向量$c_{t-1}$

LSTM的输出有两个:当前时刻LSTM输出值$h_t$、当前时刻的隐藏状态向量$h_t$、和当前时刻的记忆单元状态向量$c_t$

注意:记忆单元$c$在LSTM 层内部结束工作,不向其他层输出。LSTM的输出仅有隐藏状态向量h。

  LSTM 的关键是单元状态,即贯穿图表顶部的水平线,有点像传送带。这一部分一般叫做单元状态(cell state)它自始至终存在于LSTM的整个链式系统中。

图  LSTM中的单元状态

记忆单元状态的计算公式:

$$公式1:c_{t}=f_{t} \odot c_{t-1}+i_{t} \odot g_t$$

遗忘门

  $f_t$叫做遗忘门,表示$C_{t-1}$的哪些特征被用于计算$C_t$。$f_t$是一个向量,向量的每个元素均位于(0~1)范围内。通常我们使用 sigmoid 作为激活函数,sigmoid 的输出是一个介于于(0~1)区间内的值,但是当你观察一个训练好的LSTM时,你会发现门的值绝大多数都非常接近0或者1,其余的值少之又少。

图7:LSTM的遗忘门

$$公式2:f_t=\sigma (x_tW_x^{(f)}+h_{h-1}W_h^{(f)}+b^{(f)})$$

输入门

  $\tilde{C}_t$ 表示单元状态更新值,由输入数据$x_t$和隐节点$h_{t-1}$经由一个神经网络层得到,单元状态更新值的激活函数通常使用$tanh$。 $i_t$叫做输入门,同 $f_t$ 一样也是一个元素介于(0~1)区间内的向量,同样由$x_t$和$h_{t-1}$经由$sigmoid$激活函数计算而成

LSTM的输入门和单元状态更新值的计算方式

$$公式3::i_t=\sigma (x_tW_x^{(i)}+h_{t-1}W_h^{i}+b^{(i)})$$

$$公式4:g_t=tanh(x_tW_x^{(g)}+h_{t-1}W_h^{(g)}+b^{(g)})$$

输出门

  最后,为了计算预测值$\hat{y}_t$和生成下个时间片完整的输入,我们需要计算隐节点的输出 $h_t$。

$$公式5:o_t=\sigma (x_tW_x^{(o)}+h_{t-1}W_h^{(o)}+b^{(o)})$$

$$公式6:h_t=o_t\odot tanh(c_t)$$

  我们来看公式2、3、4、5,都是型如$xW_x+hW_h+b$的格式,因此可以整合为通过一个式子进行,如下图所示:

4 个权重(或偏置)被整合为了1 个。如此,原本单独执行4 次的仿射变换通过1 次计算即可完成,可以加快计算速度。这是因为矩阵库计算“大矩阵”时通常会更快,而且通过将权重整合到一起管理,源代码也会更简洁。

  假设$W_x$、$W_h$和$b$分别包含4 个权重(或偏置),此时LSTM 的计算图如下图所示。

import tensorflow as tf
from tensorflow.keras import layers

inputs = tf.random.normal((64, 6, 10))
LSTM_layer = layers.LSTM(units=20, return_sequences=True, return_state=True)
outputs, final_memory_state, final_carry_state = LSTM_layer(inputs)
print(outputs.shape, final_memory_state.shape, final_carry_state.shape)
# (64, 6, 20) (64, 20) (64, 20)

print(len(LSTM_layer.weights))  # 3
print(LSTM_layer.weights[0].shape)  # Wx (10, 80)
print(LSTM_layer.weights[1].shape)  # Wh (20, 80)
print(LSTM_layer.weights[2].shape)  # bias (80,)

GRU

LSTM 的参数太多,计算需要很长时间。因此,最近业界又提出了 GRU(Gated RecurrentUnit,门控循环单元)。GRU 保留了 LSTM使用门的理念,但是减少了参数,缩短了计算时间。

相对于 LSTM 使用隐藏状态和记忆单元两条线,GRU只使用隐藏状态。异同点如下:

GRU的计算图

GRU计算图,$\sigma $节点和tanh节点有专用的权重,节点内部进行仿射变换(“1−”节点输入x,输出1 − x)

$$公式2.1:z=\sigma(x_t W_x^{(z)}+h_{t-1} W_h^{(z)}+b^{(z)})$$

$$公式2.2:r=\sigma(x_t W_x^{(r)}+h_{t-1} W_h^{(r)}+b^{(r)})$$

$$公式2.3:\tilde{h}=\tanh (x_t W_x+(r \odot h_{t-1}) W_h+b)$$

$$公式2.4:h_t=(1-z) \odot h_{t-1}+z \odot \tilde{h} $$

GRU 中进行的计算由上述 4 个式子表示(这里 xt和 ht−1 都是行向量),如图所示,GRU 没有记忆单元,只有一个隐藏状态$h$在时间方向上传播。这里使用$r$和$z$共两个门(LSTM 使用 3 个门),$r$称为 reset 门,$z$称为 update 门。

  $r$(reset门)决定在多大程度上“忽略”过去的隐藏状态。根据公式2.3,如果$r$是 0,则新的隐藏状态$\tilde{h}$仅取决于输入$x_t$。也就是说,此时过去的隐藏状态将完全被忽略。

  $z$(update门)是更新隐藏状态的门,它扮演了 LSTM 的 forget 门和input 门两个角色。公式2.4 的$(1-z)\odot h_{t-1}$部分充当 forget 门的功能,从过去的隐藏状态中删除应该被遗忘的信息。$z\odot \tilde{h} $的部分充当 input 门的功能,对新增的信息进行加权。

 

RNN的实现

由于用TensorFlow和Keras实现RNN模型篇幅过长,我分成了两篇文章去写:

TensorFlow中实现RNN,彻底弄懂time_step

Keras实现RNN模型

RNN项目

参考文献

【书籍】深度学习进阶——自然语言处理

【colah's blog】Understanding LSTM Networks

【知乎】详解LSTM

【 easyAI】循环神经网络 – Recurrent Neural Network | RNN

【Cmd Markdown】零基础入门深度学习(5)--循环神经网络

 【IndoML】Pengenalan Long Short Term Memory (LSTM) dan Gated Recurrent Unit (GRU) – RNN Bagian 2

posted @ 2019-02-17 15:16  凌逆战  阅读(6955)  评论(5编辑  收藏  举报