循环神经网络
前面,我们介绍了适用于图像和视觉领域的 卷积神经网络。本文,我们来介绍适用于时序数据处理场景的 循环神经网络(Recurrent Neural Network,RNN)。
前馈神经网络的局限性
我们首先来介绍一下前馈神经网络的局限性。什么是前馈神经网络?其特征是数据在神经网络中单向流动,从输入层经过隐藏层达到输出层,中间不存在反馈连接。前面我们介绍的入门级神经网络、卷积神经网络都属于前馈神经网络。
前馈神经网络的一个缺点是无法处理时序数据,通常只适用于一次输入,一次输出的场景。根本原因在于其不存在记忆能力,类似函数式编程中的纯函数,其内部不存在状态,当输入相同时,则输出也相同。
循环神经网络
循环神经网络解决了前馈神经网络无法解决的问题,经过训练后可以处理时序数据。时序数据是指具有顺序或时间关联性的数据,比如:视频流、音频流、构成文章或句子的单词序列等。因此,RNN 非常适合语言翻译、语音识别、图像字幕、自然语言处理等场景。
类似于 CNN 包含卷积层、池化层,RNN 的特征是包含一个循环层(Recurrent Layer),不过大多数情况下称为 “RNN 层”。下面,我们来重点介绍一下 RNN 层的工作原理。
RNN 与 Time RNN
下图所示,是 RNN 层的两种数据流向示意图。经典的表示法采用水平流向表示,为了方便分析,后续我们将使用垂直流向表示。
RNN 层内部具有环路,数据在层内循环。下标 \(t\) 表示时间,当时序数据 \((x_0, x_1, ..., x_t, ...)\) 输入到 RNN 层后,将输出 \((h_0, h_1, ..., h_t, ...)\) 等 隐藏状态(Hidden State)。
为了进一步分析环路的数据走向,我们对 RNN 层进行循环展开,如下所示。可以看出,各个时刻的 RNN 层接收两个输入,分别是:
- 当前时刻的时序数据输入
- 前一时刻的隐藏状态输出
很显然,循环展开后包含了各个时刻的 RNN 层。为了区分,我们重新定义 RNN 的不同结构。
- RNN 层:表示单一时刻的 RNN 层
- Time RNN 层:表示由多个单一时刻 RNN 层所构成的 RNN 层
正向传播
我们先来看下 RNN 层的正向传播计算图,如下所示。其内部包含三种运算,分别是:
- 矩阵乘法:MatMul 节点
- 矩阵加法:Sum 节点
- 激活函数:tanh 节点,即双曲正切函数
可以看出,其内部包含两个权重,分别是将输入 \(x\) 转化为输出 \(h\) 的权重 \(W_x\) 和将前一时刻的输出转换为当前时刻的输出的权重 \(W_h\)。此外,还有偏置 \(b\)。这里的 \(h_{t-1}\) 和 \(x_t\) 都是行向量。根据计算图,我们最终可以得出如下的计算公式。
\[\begin{aligned} h_t = tanh(h_{t-1}W_h + x_t W_x + b) \end{aligned}\]由此,我们可以认为 RNN 具有 “状态” \(h\),并基于此进行运算,这也是 RNN 层是 “具有状态的层” 或 “具有记忆的层” 的原因。
相比 RNN 层,Time RNN 层的正向传播在输入和输出侧有所不同。Time RNN 将 \((x_0, x_1, ..., x_{T-1})\) 捆绑为 \(xs\) 作为输入,将 \((h_0, h_1, ..., h_{T-1})\) 捆绑为 \(hs\) 作为输出,内部则是由多个 RNN 层连接而成。
反向传播
下图所示为 RNN 层的反向传播计算图。结合 《神经网络的分层设计原理》 中基于计算图计算反向传播梯度的方法,我们可以很容易求解 RNN 层的反向传播梯度。
下图所示为 Time RNN 层的反向传播计算图。上游的梯度记为 \(dhs\),下游的梯度记为 \(dxs\)。这是 Time RNN 层作为整体在外部的反向传播路径。
关于 Time RNN 层内部各个时刻的 RNN 层的反向传播路径,具体方法有两种,分别是:
- 基于时间的反向传播(Backpropagation Through Time,BPTT),简称 BPTT
- 基于 BPTT 进行分段截断,简称 Truncated BPTT
BPTT
BPTT 的核心思想很简单,对 Time RNN 层进行循环展开,由此求各个时刻的目标梯度,即按时间顺序展开的神经网络的误差传播法。
随着时序数据的时间跨度增大,BPTT 反向传播会出现以下几个问题:
- 内存成比例增大,因为各个时刻的 RNN 层会存储中间数据。
- 时间成比例变长,因为误差(梯度)会按顺序反向传播。
- 梯度逐渐不稳定,比如:梯度爆炸、梯度消失。
Truncated BPTT
为了解决反向传播的长时序数据问题,一种自然而然的方法是 将 Time RNN 层循环展开后的反向传播路径进行分段截断,这就是 Truncated BPTT 的核心思想。这里要注意的是,我们只截断反向传播,而不截断正向传播,如下所示。
下图所示是 Truncated BPTT 的训练示意图。我们来看一下它是如何对时序数据过大产生的问题进行优化的。
- 对于时间问题,由于反向传播依赖正向传播的中间数据,即使是 Truncated BPTT,也需要按序的正向传播。只不过对于反向传播,每个分段可以并行处理。从而在一定程度上提高训练速度。
- 对于梯度问题,截断后也能够在一定程度上缓解梯度不稳定的情况,前提是要选择合适的截断长度。
- 对于内存问题,此时我们无需积累完整路径的所有中间数据后才进行反向传播,因此也有了一定程度的内存优化。
RNN 语言模型
RNN 语言模型(Language Model),简称 RNNLM,是一个基于 RNN 层实现的神经网络,用于理解和生成自然语言文本。
我们考虑一个问题:当输入的内容为 “Tom was watching TV in his room. Mary came into the room. Mary said hi to _” 时,如何预测最后一个单词是什么?很显然,最后一个单词应该是 “Tom”。但是,由于 “Tom” 单词相距预测点太远,常规的神经网络无法有效地进行预测。对此,基于 RNN 的语言模型 RNNLM 可以有效解决这个问题。
下图所示是一个简单的 RNNLM 的神经网络模型。每一个 RNN 层的上下游都有对应的神经网络层,比如:Embedding、Affine、Softmax 等,用于处理单一时刻的时序数据。类似于 RNN 和 Time RNN 的关系,这里我们将整体处理含有 \(T\) 个时序数据的层称为 “Time XX 层”。
对于学习阶段,RNNLM 中也有对应的 Time Softmax with Loss 层,如下所示。\(x_0\)、\(x_1\) 等数据是推理输出的得分,\(t_0\)、\(t_1\) 等数据是正确解标签。\(T\) 个 Softmax with Loss 层各自计算损失,求和取平均值,作为最终的损失。
梯度爆炸和梯度消失
前面我们提到 RNN 在学习时序数据的长期依赖时,会存在梯度爆炸和梯度消失的问题。这里我们来介绍一下深层次的原因。
如下图所示,我们仅关注 Time RNN 层中的梯度传播。这里考虑长度为 \(T\) 的时序数据,在反向传播过程中,梯度会反复经历多次 tanh、sum、MatMul 的运算:
- sum:加法的导数是常数 1
- tanh:当 \(y = tanh(x)\) 时,其导数为 \(\frac{dy}{dx} = 1 - y^2\)
- MatMul:矩阵乘法的导数计算可以参考 《神经网络的分层设计原理》
首先,我们来看 tanh 对于反向传播梯度的影响。下图所示为 tanh 及其导数的曲线图,导数值的范围为 [0, 1]。当多次经过 tanh 节点时,梯度值会不断进行乘积,会逐渐减小,趋近于 0。这正是梯度消失的原因之一。这里主要是因为激活函数 tanh 导致的,如果将其改为 ReLU,可以有效抑制梯度消失的问题。
然后,我们考虑 MatMul 对于反向传播梯度的影响。简单起见,我们忽略 tanh 节点的影响。下图所示为 MatMul 的反向传播计算图,其中 \(W_h\) 保持不变,当时序数据越长,\(W_h^T\) 的乘积次数越多。
由此可以看出梯度爆炸或者梯度消失,取决于矩阵,更专业的术语是矩阵奇异值。矩阵奇异值本质上表示数据的离散程度,根据奇异值(更准确地说是多个奇异值中的最大值)是否大于 1。如果奇异值的最大值大于 1,可以预测梯度很有可能会呈指数级增加,即梯度爆炸;如果奇异值的最大值小于 1,可以预测梯度会呈指数级减少,即梯度消失。当然,并不是说奇异值比 1 大就一定会出现梯度爆炸。
梯度爆炸优化
梯度裁剪(Gradient Clipping)可以用于解决梯度爆炸,其基本思想可以用如下所示的伪代码表示。
\[\begin{aligned} if \space \space \space || \hat g || \geq & \space threshold \\ \hat g = & \space \frac{threshold}{|| \hat g ||} \hat g \end{aligned}\]这里将神经网络所有参数的梯度整合成一个,用符合 \(\hat g\) 表示。\(threshold\) 表示阈值。当 \(\hat g\) 大于或等于阈值,则对 \(\hat g\) 进行裁剪。
梯度消失优化
Gated RNN 可以用于解决梯度消失,其从根本上改变了 RNN 结构。目前业界已经出现了诸多 Gated RNN 网络结构,其中具有代表性的有 LSTM 和 GRU。下面,我们分别来进行介绍。
LSTM
LSTM(Long Short-Term Memory),长短期记忆网络。类似于 RNN 和 Time RNN,这里也存在 LSTM 和 Time LSTM 的区别。这里,我们主要介绍单个时刻的 LSTM 结构。
如下所示,LSTM 与 RNN 的区别在于 LSTM 具有额外的记忆单元 \(c\),其只存在于 Time LSTM 内部,不会其它层进行传输。
LSTM 内部包含多种结构,包括:输出门、遗忘门、记忆单元、输入门等,下面我们先从 LSTM 的基本结构进行介绍。
基本结构
LSTM 中的记忆单元 \(c_t\) 存储了过去到时刻 \(t\) 的所有记忆,并基于此向外部层和下一时刻的 LSTM 输出隐藏状态 \(h_t\)。
下图所示,LSTM 中当前记忆单元 \(c_t\) 基于 3 个输入 \(c_{t-1}\)、\(h_{t-1}\)、\(x_t\) 经过“某种计算”得出。最后输出的隐藏状态 \(h_t\) 是在 \(c_t\) 的基础上应用 tanh 函数计算得到的。
这里提到的“某种计算”是下面要介绍的各种运算,它们都会涉及到一个概念——门(Gate)。门的本质就是阀门,可以控制流过数据的大小。在神经网络中,激活函数的概念与它非常相似。因此,LSTM
使用经典的激活函数 sigmoid
作为各种门结构,后续将使用 \(\sigma\) 符号进行表示。
输出门
首先,LSTM 增加了 输出门(Output Gate)。输入 \(x_t\) 有权重 \(W_x\),输入隐藏状态 \(h_{t-1}\) 有权重 \(W_h\)。将它们的矩阵乘积和偏置 \(b\) 之和传入 \(sigmoid\) 函数,结果就是输门的输出值 \(o\)。
\[\begin{aligned} o = \sigma (x_t W_x + h_{t-1} W_h + b) \end{aligned}\]输出门的输出值 \(o\) 进一步与 \(tanh(c_t)\) 进行乘积。注意,这里的乘积并不是矩阵乘积,而是 阿达玛乘积,即矩阵对应元素之间的乘积。如果使用 \(\circ\) 表示阿达玛乘积,其计算式如下。
\[\begin{aligned} h_t = o \circ tanh(c_t) \end{aligned}\]遗忘门
为了模拟更真实的记忆效果,LSTM 加入了 遗忘门(Forget Gate),其计算图如下所示。
遗忘门的计算与输出门类似,区别在于两者在计算图中的位置不同,其计算式如下。
\[\begin{aligned} f = \sigma (x_t W_x + h_{t-1} W_h + b) \end{aligned}\]记忆单元优化
如果针对记忆单元,我们只增加遗忘门,那么会导致记忆逐步遗忘。因此,我们还要增加强化记忆的结构,由此对记忆单元进行优化。这里,我们增加 tanh 节点,如下所示。
tanh 节点计算与其他门的计算类似,如下所示。
\[\begin{aligned} g = tanh(x_t W_x + h_{t-1} W_h + b) \end{aligned}\]基于 tanh 节点计算得到的结果 \(g\) 最终与加到上上一时刻的记忆单元 \(c_{t-1}\) 中,从而形成新的记忆。
输入门
输入门判断新增信息 \(g\) 的各个元素的价值,会对待添加的信息进行取舍,其计算图如下所示。
输入门的输出值计算如下所示。
\[\begin{aligned} i = \sigma (x_t W_x + h_{t-1} W_h + b) \end{aligned}\]完整结构
最后,我们可以得到如下所示的 LSTM 完整结构,可以看出 LSTM 是在 RNN 的基础上增加了一系列门结构以及相关运算实现的。
反向传播
接下来,我们来看一下为什么 LSTM 能够解决梯度消失的问题。其原因可以通过观察记忆单元 \(c\) 的反向传播来了解,如下所示。
记忆单元的反向传播会反复经过 “+” 和 “×” 节点。“+” 节点的导数是 1,会直接传递上游梯度至下游。“×” 节点并不是矩阵乘积,而是阿达玛乘积。阿达玛乘积是矩阵中对应元素的乘积运算,而且每次都会基于不同的门值进行乘积运算,因此不会发生梯度消失或梯度爆炸。
在反向传播过程中,遗忘门认为应该忘记的记忆单元元素,其梯度会变小;不能忘记的记忆单元元素,其梯度并不会退化。因此,可以期待记忆单元能够保存学习长期的依赖关系。
GRU
GRU(Gated Recurrent Unit),门控循环单元,其继承了 LSTM 的思路,但是减少了参数,缩短了计算时间。
GRU 的计算图和计算式如下所示。
\[\begin{aligned} z = & \space \sigma (x_t W_x + h_{t-1} W_h + b) \\ r = & \space \sigma (x_t W_x + h_{t-1} W_h + b) \\ \widetilde{h} = & \space tanh (x_t W_x + h_{t-1} W_h + b) \\ h_t = & \space (1 - z) \circ h_{t-1} + z \circ \widetilde{h} \end{aligned}\]GRU 没有记忆单元,只有一个隐藏状态 \(h\) 在时间方向上传播。其使用了两个门结构:\(r\)(reset 门)、\(z\)(update 门)。
\(r\) 决定在多大程度上“忽略”过去的隐藏状态。如果 \(r\) 为 0,则新的隐藏状态 \(\widetiled{h}\) 只取决于 \(x_t\),即过去的隐藏状态被忽略。
\(z\)是更新隐藏状态的门,其包含了 LSTM 中遗忘门和输入门的作用。\(\widetilde{h}\) 中的 \((1-z)\circ h_{t-1}\) 表示遗忘门的功能;\(z \circ \widetilde{h}\) 表示输入门的功能。
整体而言,GRU 是简化版的 LSTM 架构,相比之下,GRU 的参数和计算成本更少。关于 GRU 和 LSTM 的选择,根据不同的任务和超参数的设置,结论可能不同。由于 GRU 超参数少、计算量小,因此比较适合于数据集较小,设计模型需要反复实验的场景。
总结
本文我们主要介绍了循环神经网络的结构,其内部根据时序数据可以展开成多个循环结构。为了区分,我们将整体称为 Time RNN,将内部的单个循环结构称为 RNN。
Time RNN 的反向传播主要有两种方法,分别是:BPTT 和 Truncated BPTT。后者是在前者的基础上对反向传播路径进行分段截断。从而在一定程度上缓解时序数据过长带来的时间问题、梯度问题、内存问题。
之后,我们进一步介绍了梯度问题中,梯度爆炸和梯度消失出现的原因。对于梯度爆炸,我们介绍了梯度裁剪的方法。对于梯度消失,我们介绍了 Gate RNN 的两种结构:LSTM 和 GRU。
参考
- 《深度学习进阶:自然语言处理》