循环神经网络

序列模型

前馈神经网络可以看作一个复杂的函数,每次输入都是独立的,即网络的输出只依赖于当前的输入。但是在很多现实任务中,网络的输出不仅和当前时刻的输入相关,也和其过去一段时间的输出相关。此外,前馈网络难以处理时序数据,比如视频、语音、文本等。时序数据的长度一般是不固定的,而前馈神经网络要求输入和输出的维数都是固定的,不能任意改变。因此,当处理这一类和时序数据相关的问题时,就需要一种能力更强的模型。

循环神经网络(Recurrent Neural Network, RNN)是一种专门用于处理序列数据的神经网络模型。其核心特点是通过隐藏层的循环连接,能够捕获序列中的时间依赖关系。RNN的输入不仅依赖当前时间步的数据,还会结合前一时间步的隐藏状态,从而实现对序列信息的记忆和处理。

循环神经网络可以应用到很多不同类型的机器学习任务。根据这些任务的特点可以分为以下几种模式:

  • One-to-one:最简单的结构,其实和全连接神经网络并没有什么区别。
  • One-to-many:例如音乐生成,只需要输入类别,但是需要输出整个序列。
  • Many-to-one:主要用于序列数据的分类问题。比如在文本分类中,输入数据为单词的序列,输出为该文本的类别。
  • Many-to-many:输入序列和输出序列的长度相同。比如在词性标注中,每一个单词都需要标注其对应的词性标签。
  • Many-to-many:也称为编码器-解码器 (Encoder-Decoder) 模型,即输入序列和输出序列不需要有严格的对应关系,也不需要保持相同的长度。比如在机器翻译中,输入为源语言的单词序列,输出为目标语言的单词序列。

基本结构

在传统的前馈神经网络中,数据是从输入层流向输出层的,而在 RNN 中,数据不仅沿着网络层级流动,还会在每个时间步骤上传播到当前的隐层状态,从而将之前的信息传递到下一个时间步骤。循环神经网络在处理序列数据时的展开视图如下:

左边是为了简便描述RNN的工作原理而画的缩略图,右边是展开之后,每个时间点之间的流程图。简单的循环单元通常是一个两层神经网络,它由输入层、一个隐藏层和一个输出层组成:

隐藏层的激活值称为隐藏状态(Hidden State)。通过循环连接(Recurrent Connection)将上一步的隐藏状态传递到下一步,形成“记忆”。给定一个输入序列{x1,x2,,xT}\{\mathbf{x}_1, \mathbf{x}_2, \cdots, \mathbf{x}_T\}。我们考虑一个基础循环神经网络(Vanilla RNN):

ht=gh(Whhht1+Wxhxt+bh)\mathbf h_t=g_h(\mathbf W_{hh}\mathbf h_{t-1}+\mathbf W_{xh}\mathbf x_t+\mathbf b_h)

在时间步 tt 上的输出取决于具体任务

y^t=go(Whoht+bo)\hat{\mathbf y}_t=g_o(\mathbf W_{ho}\mathbf h_t+\mathbf b_o)

参数:假设他们的维度分别为 xtRD,htRH,y^tRK\mathbf{x}_t \in \R^{D},\mathbf{h}_t \in\R^{H},\hat{\mathbf y}_t\in\R^{K}

  • Wxh\mathbf{W}_{xh}:输入到隐藏的权重矩阵 (H×D)(H \times D)
  • Whh\mathbf{W}_{hh}:隐藏到隐藏的权重矩阵 (H×H)(H \times H)
  • bh\mathbf{b}_h:隐藏层偏置向量 (H×1)(H \times 1)
  • Who\mathbf{W}_{ho}:隐藏到输出的权重矩阵 (K×H)(K \times H)
  • bo\mathbf{b}_o:输出层偏置向量 (K×1)(K \times 1)

隐藏层通常使用 tanh\tanh 激活函数。输出层通常使用 Softmax(用于分类)或线性激活(用于回归)。

文献中习惯将权重 Whh\mathbf W_{hh}Wxh\mathbf W_{xh} 横向拼接成一个更大的矩阵 Wh=[WhhWxh]\mathbf W_h=[\mathbf W_{hh}\mid\mathbf W_{xh}]。隐藏状态公式简写为

ht=gh(Wh[ht1,xt]+bh)\mathbf h_t=g_h(\mathbf W_h[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_h)

其中约定

[ht1,xt]=[ht1xt][\mathbf h_{t-1},\mathbf x_t]=\begin{bmatrix}\mathbf h_{t-1} \\ \mathbf x_t\end{bmatrix}

如何设计循环体的结构是 RNN 解决实际问题的关键。和卷积神经网络过滤器中参数共享类似,RNN 循环体中的参数在每个时间步也是共享的。这种结构使其能够处理任意长度的序列数据。

随时间反向传播算法

循环神经网络的参数可以通过梯度下降方法来进行学习。这里我们以输入序列和输出序列的长度相同的模式为例来介绍循环神经网络的参数学习。

随时间反向传播 (BackPropagation Through Time,BPTT) 算法的主要思想是通过类似前馈神经网络的反向传播算法来计算梯度。 前向传播相当简单,一次一个时间步的遍历。

对于每一个时间步 t=1,2,,Tt = 1, 2, \cdots, T

(1) 隐藏状态计算:

zt=Wh[xt,ht1]+bhht=gh(zt)\mathbf{z}_t = \mathbf{W}_{h}[\mathbf{x}_t,\mathbf{h}_{t-1}]+\mathbf{b}_h \\ \mathbf{h}_t=g_h(\mathbf{z}_t)

初始隐藏状态 h0\mathbf{h}_0 通常初始化为零向量

(2) 输出计算:

ot=Whoht+boy^t=go(ot)\mathbf{o}_t = \mathbf{W}_{ho} \mathbf{h}_t + \mathbf{b}_o \\ \hat{\mathbf{y}}_t=g_o(\mathbf{o}_t)

(3) 然后通过一个目标函数在所有 TT 个时间步内评估输出 y^t\hat{\mathbf y}_t 和对应的标签 yt\mathbf y_t 之间的损失:

L=t=1TLt=t=1T(y^t,yt)L=\sum_{t=1}^TL_t=\sum_{t=1}^T\ell(\hat{\mathbf y}_t,\mathbf y_t)

其中 yt\mathbf{y}_t 是时间步 tt 的真实标签。对于分类任务,\ell 通常是交叉熵损失。

我们的目标是计算总损失 LL 对所有参数 (Wh,bh,Who,bo)(\mathbf{W}_{h}, \mathbf{b}_h, \mathbf{W}_{ho}, \mathbf{b}_o) 的梯度。由于时间维度的存在,BPTT 的推导比标准BP更为复杂。

因为输出层的参数 (Who,bo)(\mathbf{W}_{ho}, \mathbf{b}_o) 在每个时间步是独立的,它们的梯度计算相对简单。

对于 Who\mathbf{W}_{ho}

LWho=t=1TLtototWho=t=1TethtT\frac{\partial L}{\partial \mathbf{W}_{ho}}=\sum_{t=1}^{T} \frac{\partial L_t}{\partial \mathbf{o}_t}\frac{\partial \mathbf{o}_t}{\partial \mathbf{W}_{ho}}=\sum_{t=1}^{T} \mathbf{e}_t\mathbf{h}_t^T

其中定义

et=Ltot=Lty^tgo(ot)\mathbf{e}_t = \frac{\partial L_t}{\partial \mathbf{o}_t} =\frac{\partial L_t}{\partial \hat{\mathbf{y}}_t}\odot g'_o(\mathbf{o}_t)

对于 bo\mathbf{b}_o

Lbo=t=1TLtototbo=t=1Tet\frac{\partial L}{\partial \mathbf{b}_o}=\sum_{t=1}^{T} \frac{\partial L_t}{\partial \mathbf{o}_t}\frac{\partial \mathbf{o}_t}{\partial \mathbf{b}_o} = \sum_{t=1}^{T} \mathbf{e}_t

接下来,我们计算循环层参数 (Wh,bh)(\mathbf{W}_{h},\mathbf{b}_h) 的梯度。由于这些参数在所有时间步共享,它们的总梯度是所有时间步上各自梯度的总和。为了方便应用链式法则,我们将定义一个关键变量——时间步 tt 的隐藏状态误差:

δt=Lzt\boldsymbol{\delta}_t = \frac{\partial L}{\partial \mathbf{z}_t}

这个误差项会沿着时间维度反向传播。这是 BPTT 最核心、最复杂的部分。

先观察 zt\mathbf{z}_t 如何影响损失 LL

  1. 直接影响:zt\mathbf{z}_t 影响 ht\mathbf{h}_t,进而影响当前时间步的输出 ot\mathbf{o}_t 和损失 LtL_t
  2. 间接影响:zt\mathbf{z}_t 影响 ht\mathbf{h}_t,而 ht\mathbf{h}_t 又是下一个时间步 zt+1\mathbf{z}_{t+1} 的输入,因此会影响所有未来的损失 Lt+1,Lt+2,,LTL_{t+1}, L_{t+2}, \cdots, L_{T}

因此,根据多变量的链式法则,δt\boldsymbol{\delta}_t 由两部分组成:

δt=LtztCurrent+Lzt+1zt+1hthtztFuture\boldsymbol{\delta}_t = \underbrace{\frac{\partial L_t}{\partial \mathbf{z}_t}}_{\text{Current}} + \underbrace{\frac{\partial L}{\partial \mathbf{z}_{t+1}}\frac{\partial \mathbf{z}_{t+1}}{\partial \mathbf{h}_t}\frac{\partial \mathbf{h}_t}{\partial \mathbf{z}_t}}_{\text{Future}}

让我们分解这个公式:

(1) 当前贡献 LtL_t 通过 ot\mathbf{o}_tht\mathbf{h}_t 依赖于 zt\mathbf{z}_t

Ltzt=Ltotoththtzt=(WhoTet)gh(zt)\frac{\partial L_t}{\partial \mathbf{z}_t} = \frac{\partial L_t}{\partial \mathbf{o}_t}\frac{\partial \mathbf{o}_t}{\partial \mathbf{h}_t}\frac{\partial \mathbf{h}_t}{\partial \mathbf{z}_t} = (\mathbf{W}_{ho}^T \mathbf{e}_t) \odot g_h'(\mathbf{z}_t)

(2) 未来贡献:

根据前向公式 zt+1=Wxhxt+1+Whhht+bh\mathbf{z}_{t+1} = \mathbf{W}_{xh}\mathbf{x}_{t+1} + \mathbf{W}_{hh}\mathbf{h}_t + \mathbf{b}_h,可得

zt+1ht=Whh\frac{\partial \mathbf{z}_{t+1}}{\partial \mathbf{h}_t} = \mathbf{W}_{hh}

因此,未来贡献为

δt+1Whhgh(zt)\boldsymbol{\delta}_{t+1} \mathbf{W}_{hh} \odot g_h'(\mathbf{z}_t)

(3) 合并:将当前贡献和未来贡献相加,我们得到了 δt\boldsymbol{\delta}_t 的递归计算公式:

δt=(WhoTet+WhhTδt+1)gh(zt)\boldsymbol{\delta}_t = \left( \mathbf{W}_{ho}^T \mathbf{e}_t + \mathbf{W}_{hh}^T \boldsymbol{\boldsymbol{\delta}}_{t+1} \right) \odot g_h'(\mathbf{z}_t)

在最后一个时间步 TT,没有未来贡献,所以:

δT=(WhoTeT)gh(zT)\boldsymbol{\delta}_T = (\mathbf{W}_{ho}^T \mathbf{e}_T) \odot g_h'(\mathbf{z}_T)

这个递归方程允许我们从最后一个时间步 TT 开始,一路反向迭代计算 δT,δT1,...,δ1\boldsymbol{\delta}_T, \boldsymbol{\boldsymbol{\delta}}_{T-1}, ..., \boldsymbol{\boldsymbol{\delta}}_1

一旦我们得到了所有时间步的 δt\boldsymbol{\delta}_t,计算循环层梯度就变得非常简单。

对于权重 Wh\mathbf{W}_{h}:

LWh=t=1TLztztWh=t=1Tδt[xt,ht1]T\frac{\partial L}{\partial \mathbf{W}_{h}} = \sum_{t=1}^{T} \frac{\partial L}{\partial \mathbf{z}_t}\frac{\partial \mathbf{z}_t}{\partial \mathbf{W}_{h}} =\sum_{t=1}^{T} \boldsymbol{\delta}_t[\mathbf{x}_t,\mathbf{h}_{t-1}]^T

对于偏置项 bh\mathbf{b}_h:

Lbh=t=1TLztztbh=t=1Tδt\frac{\partial L}{\partial \mathbf{b}_h} = \sum_{t=1}^{T} \frac{\partial L}{\partial \mathbf{z}_t}\frac{\partial \mathbf{z}_t}{\partial \mathbf{b}_h} =\sum_{t=1}^{T} \boldsymbol{\delta}_t

再来看下 δt\boldsymbol{\delta}_t 的递归公式,由于循环神经网络经常使用 σ\sigmatanh\tanh 激活函数,其导数值都小于 1,并且权重矩阵也不会太大,因此如果时间间隔 δt\delta t 过大,来自时间步 t+δtt+\delta t 的损失影响 et+δt\mathbf e_{t+\delta t} 会趋向于0,这便是循环神经网络的梯度消失问题,也称长程依赖问题(Long-Term Dependencies Problem)。

要注意的是,在循环神经网络中的梯度消失不是说梯度 LtWh\frac{\partial L_t}{\partial \mathbf{W}_h} 消失了,而是当 δt\delta t 比较大时, Ltht+δt\frac{\partial L_t}{\partial \mathbf{h}_{t+\delta t}} 消失了。也就是说,参数 Wh\mathbf{W}_h 的更新主要靠当前时刻 tt 的几个相邻状态来更新,长距离的状态对参数 Wh\mathbf{W}_h 没有影响。

长短期记忆网络

长短期记忆网络 (Long Short-Term Memory, LSTM)是 RNN 的一种改进架构,专门设计来解决长期依赖问题,是目前使用最广泛最成功的RNN模型。

在普通的RNN中,重复模块结构非常简单,例如只有一个tanh层。整体结构如图所示:

LSTM 引入了三个门控机制(Gating Mechanism)和一个记忆单元。门是一种让信息选择式通过的方法。它们包含一个 Sigmoid 层和一个逐元乘法操作。Sigmoid 函数取值在 (0, 1) 之间, 表示以一定的比例允许信息通过。

下图给出了 LSTM 网络的循环单元结构

记忆单元:LSTM的主要思想是采用一个叫做记忆单元的通道来贯穿整个时间序列。上面承载的信息可以很容易地流过而不改变。

ct=ftct1+itc~t\mathbf c_t=\mathbf f_t\odot\mathbf c_{t-1}+\mathbf i_t\odot\tilde{\mathbf c}_t

其中 c~t\tilde{\mathbf c}_t 是通过非线性函数得到的候选状态

c~t=tanh(Wc[ht1,xt]+bc)\tilde{\mathbf c}_t=\tanh(\mathbf W_c[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_c)

输出门ot\mathbf o_t 控制当前时刻的记忆单元 ct\mathbf c_t 中有多少信息需要输出给隐藏状态

ot=σ(Wo[ht1,xt]+b0)\mathbf o_t=\sigma(\mathbf W_o[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_0)

隐藏状态

ht=ottanh(ct)\mathbf h_t=\mathbf o_t\odot\tanh(\mathbf c_t)

遗忘门ft\mathbf f_t 控制上一个时刻的记忆单元 ct1\mathbf c_{t-1} 需要遗忘多少信息

ft=σ(Wf[ht1,xt]+bf)\mathbf f_t=\sigma(\mathbf W_f[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_f)

输入门it\mathbf i_t 控制当前时刻的候选状态 c~t\tilde{\mathbf c}_t 有多少信息需要保存在记忆单元 ct\mathbf c_t

it=σ(Wi[ht1,xt]+bi)\mathbf i_t=\sigma(\mathbf W_i[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_i)

通过 LSTM 循环单元,整个网络可以建立较长距离的时序依赖关系。

一般在深度网络参数学习时,参数初始化的值一般都比较小。但是在训练 LSTM 网络时,过小的值会使得遗忘门的值比较小。这意味着前一时刻的信息大部分都丢失了,这样网络很难捕捉到长距离的依赖信息。并且相邻时间间隔的梯度会非常小,这会导致梯度弥散问题。因此遗忘的参数初始值一般都设得比较大。

门控循环单元

门控循环单元(Gated Recurrent Unit, GRU)是 LSTM 的简化版本,在保持相似性能的同时减少了参数数量。

GRU 的核心结构如下图所示:

和 LSTM 不同, GRU 不引入额外的记忆单元,而是引入一个更新门来控制当前状态需要从历史状态中保留多少信息,以及需要从候选状态中接受多少新信息,即

ht=(1ut)h~t+utht1\mathbf h_t=(1-\mathbf u_t)\odot\tilde{\mathbf h}_t+\mathbf u_t\odot\mathbf h_{t-1}

更新门(update gate)决定保留多少旧信息

ut=σ(Wu[ht1,xt]+bu)\mathbf u_t=\sigma(\mathbf W_u[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_u)

在LSTM网络中,输入门和遗忘门是互补关系,具有一定的冗余性。GRU网络直接使用一个更新门来控制输入和遗忘之间的平衡。

重置门(reset gate)决定如何组合新旧信息

rt=σ(Wr[ht1,xt]+br)\mathbf r_t=\sigma(\mathbf W_r[\mathbf h_{t-1},\mathbf x_t]+\mathbf b_r)

候选激活基于重置门计算

h~t=tanh(Wc[rtht1,xt]+bh)\tilde{\mathbf h}_t=\tanh(\mathbf W_c [\mathbf r_t\odot\mathbf h_{t-1},\mathbf x_t]+\mathbf b_h)

双向循环神经网络

在有些任务中,一个时刻的输出不但和过去时刻的信息有关,也和后续时刻的信息有关。比如给定一个句子,其中一个词的词性由它的上下文决定,即包含左右两边的信息。因此,在这些任务中,我们可以增加一个按照时间的逆序来传递信息的网络层,来增强网络的能力。

双向循环神经网络(Bidirectional Recurrent Neural Network,Bi-RNN)由两层循环神经网络组成,一层处理过去的训练信息,另一层处理将来的训练信息。

下图给出了按时间展开的双向循环神经网络

深层循环神经网络

前面我们介绍的循环神经网络只有一个隐藏层,我们当然也可以堆叠两个以上的隐藏层,这样就得到了深度循环神经网络。如下图所示: