循环神经网络
序列模型
前馈神经网络可以看作一个复杂的函数,每次输入都是独立的,即网络的输出只依赖于当前的输入。但是在很多现实任务中,网络的输出不仅和当前时刻的输入相关,也和其过去一段时间的输出相关。此外,前馈网络难以处理时序数据,比如视频、语音、文本等。时序数据的长度一般是不固定的,而前馈神经网络要求输入和输出的维数都是固定的,不能任意改变。因此,当处理这一类和时序数据相关的问题时,就需要一种能力更强的模型。
循环神经网络(Recurrent Neural Network, RNN)是一种专门用于处理序列数据的神经网络模型。其核心特点是通过隐藏层的循环连接,能够捕获序列中的时间依赖关系。RNN 的输入不仅依赖当前时间步的数据,还会结合前一时间步的隐藏状态,从而实现对序列信息的记忆和处理。
循环神经网络可以应用到很多不同类型的机器学习任务。根据这些任务的特点可以分为以下几种模式:
- One-to-one:最简单的结构,其实和全连接神经网络并没有什么区别。
- One-to-many:例如音乐生成,只需要输入类别,但是需要输出整个序列。
- Many-to-one:主要用于序列数据的分类问题。比如在文本分类中,输入数据为单词的序列,输出为该文本的类别。
- Many-to-many:输入序列和输出序列的长度相同。比如在词性标注中,每一个单词都需要标注其对应的词性标签。
- Many-to-many:也称为编码器-解码器 (Encoder-Decoder) 模型,即输入序列和输出序列不需要有严格的对应关系,也不需要保持相同的长度。比如在机器翻译中,输入为源语言的单词序列,输出为目标语言的单词序列。
实例:我们用一个完整的单词级示例来贯穿讲解循环神经网络。这个例子更贴近现实世界的 NLP 应用。
任务:我们有一个简单的电影评论 "The movie was incredibly good"
,判断是正面评价还是负面评价。这是一个典型的序列分类任务。
为了简化,我们假设词汇表(Vocabulary)只包含以下单词(实际中词汇表可能有数万个单词):{"<EOS>", "<UNK>", "the", "movie", "was", "incredibly", "good"}
。每个单词会分配到一个唯一的整数索引。
<EOS>
: (End Of Sequence) 表示序列结束
<UNK>
: 未知词标记(用于处理词汇表外的单词)
我们将词汇表中的每个单词转换为 one-hot 向量。我们的输入序列和目标输出是:
时间步 (t) |
输入 (xt) |
One-Hot 编码 |
目标 (情感分类) |
1 |
the |
[0, 0, 1, 0, 0, 0, 0] |
|
2 |
movie |
[0, 0, 0, 1, 0, 0, 0] |
|
3 |
was |
[0, 0, 0, 0, 1, 0, 0] |
|
4 |
incredibly |
[0, 0, 0, 0, 0, 1, 0] |
|
5 |
good |
[0, 0, 0, 0, 0, 0, 1] |
|
6 |
<EOS> |
[1, 0, 0, 0, 0, 0, 0] |
Positive |
基本结构
在传统的前馈神经网络中,数据是从输入层流向输出层的,而在 RNN 中,数据不仅沿着网络层级流动,还会在每个时间步骤上传播到当前的隐层状态,从而将之前的信息传递到下一个时间步骤。循环神经网络在处理序列数据时的展开视图如下:
左边是为了简便描述 RNN 的工作原理而画的缩略图,右边是展开之后,每个时间点之间的流程图。简单的循环单元通常是一个两层神经网络,它由输入层、一个隐藏层和一个输出层组成:
隐藏层的激活值称为隐藏状态(Hidden State)。通过循环连接(Recurrent Connection)将上一步的隐藏状态传递到下一步,形成“记忆”。给定一个输入序列{x1,x2,⋯,xT}。我们考虑一个基础循环神经网络(Vanilla RNN):
ht=gh(Whhht−1+Wxhxt+bh)
在时间步 t 上的输出取决于具体任务
y^t=go(Whoht+bo)
参数:假设他们的维度分别为 xt∈RD,ht∈RH,y^t∈RK
- Wxh:输入到隐藏的权重矩阵 (H×D)
- Whh:隐藏到隐藏的权重矩阵 (H×H)
- bh:隐藏层偏置向量 (H×1)
- Who:隐藏到输出的权重矩阵 (K×H)
- bo:输出层偏置向量 (K×1)
隐藏层通常使用 tanh 激活函数。输出层通常使用 Softmax(用于分类)或线性激活(用于回归)。
文献中习惯将权重 Whh 和 Wxh 横向拼接成一个更大的矩阵 Wh=[Whh∣Wxh]。隐藏状态公式简写为
ht=gh(Wh[ht−1,xt]+bh)
其中约定
[ht−1,xt]=[ht−1xt]
和卷积神经网络过滤器中参数共享类似,RNN 循环体中的参数在每个时间步也是共享的。这种结构大大减少了需要学习的参数量,并且让网络使其能够处理任意长度的序列。
实例:让我们手动计算前向传播,感受一下这个过程。我们为了演示,随机所有权重参数,偏置都是 0。
Wxh=⎣⎢⎢⎡0.0.70.50.51.1.0.70.70.20.20.80.20.80.60.20.30.10.70.20.50.0.10.30.80.20.0.20.7⎦⎥⎥⎤,Whh=⎣⎢⎢⎡0.20.61.0.20.0.10.60.50.10.40.40.50.10.20.30.4⎦⎥⎥⎤,Who=[0.30.11.0.20.70.90.71.]
时间步 t=1:h1=tanh(Whhh0+Wxhx1+bh)=[0.2,0.2,0.66,0.2]T
时间步 t=2:h2=tanh(Whhh1+Wxhx2+bh)=[0.73,0.78,0.69,0.69]T
依次计算到时间步 t=6:h6=tanh(Whhh5+Wxhx6+bh)=[0.27,0.92,0.96,0.94]T
预测值是 y^=[0.58,0.42]T
我们使用分类任务常用的交叉熵损失:L=−(0∗log(0.58)+1∗log(0.42))=0.86
随时间反向传播算法
循环神经网络的参数可以通过梯度下降方法来进行学习。这里我们以输入序列和输出序列的长度相同的模式为例来介绍循环神经网络的参数学习。
随时间反向传播 (BackPropagation Through Time,BPTT) 算法的主要思想是通过类似前馈神经网络的反向传播算法来计算梯度。 前向传播相当简单,一次一个时间步的遍历。
对于每一个时间步 t=1,2,⋯,T:
(1) 隐藏状态计算:
zt=Wh[xt,ht−1]+bhht=gh(zt)
初始隐藏状态 h0 通常初始化为零向量
(2) 输出计算:
ot=Whoht+boy^t=go(ot)
(3) 然后通过一个目标函数在所有 T 个时间步内评估输出 y^t 和对应的标签 yt 之间的损失:
L=t=1∑TLt=t=1∑Tℓ(y^t,yt)
其中 yt 是时间步 t 的真实标签。对于分类任务,ℓ 通常是交叉熵损失。
我们的目标是计算总损失 L 对所有参数 (Wh,bh,Who,bo) 的梯度。由于时间维度的存在,BPTT 的推导比标准 BP 更为复杂。
因为输出层的参数 (Who,bo) 在每个时间步是独立的,它们的梯度计算相对简单。
对于 Who
∂Who∂L=t=1∑T∂ot∂Lt∂Who∂ot=t=1∑TethtT
其中定义
et=∂ot∂Lt=∂y^t∂Lt⊙go′(ot)
对于 bo
∂bo∂L=t=1∑T∂ot∂Lt∂bo∂ot=t=1∑Tet
接下来,我们计算循环层参数 (Wh,bh) 的梯度。由于这些参数在所有时间步共享,它们的总梯度是所有时间步上各自梯度的总和。为了方便应用链式法则,我们将定义一个关键变量——时间步 t 的隐藏状态误差:
δt=∂zt∂L
这个误差项会沿着时间维度反向传播。这是 BPTT 最核心、最复杂的部分。
先观察 zt 如何影响损失 L:
- 直接影响:zt 影响 ht,进而影响当前时间步的输出 ot 和损失 Lt。
- 间接影响:zt 影响 ht,而 ht 又是下一个时间步 zt+1 的输入,因此会影响所有未来的损失 Lt+1,Lt+2,⋯,LT。
因此,根据多变量的链式法则,δt 由两部分组成:
δt=Current∂zt∂Lt+Future∂zt+1∂L∂ht∂zt+1∂zt∂ht
让我们分解这个公式:
(1) 当前贡献 Lt 通过 ot 和 ht 依赖于 zt
∂zt∂Lt=∂ot∂Lt∂ht∂ot∂zt∂ht=(WhoTet)⊙gh′(zt)
(2) 未来贡献:
根据前向公式 zt+1=Wxhxt+1+Whhht+bh,可得
∂ht∂zt+1=Whh
因此,未来贡献为
δt+1Whh⊙gh′(zt)
(3) 合并:将当前贡献和未来贡献相加,我们得到了 δt 的递归计算公式:
δt=(WhoTet+WhhTδt+1)⊙gh′(zt)
在最后一个时间步 T,没有未来贡献,所以:
δT=(WhoTeT)⊙gh′(zT)
这个递归方程允许我们从最后一个时间步 T 开始,一路反向迭代计算 δT,δT−1,...,δ1。
一旦我们得到了所有时间步的 δt,计算循环层梯度就变得非常简单。
对于权重 Wh:
∂Wh∂L=t=1∑T∂zt∂L∂Wh∂zt=t=1∑Tδt[xt,ht−1]T
对于偏置项 bh:
∂bh∂L=t=1∑T∂zt∂L∂bh∂zt=t=1∑Tδt
再来看下 δt 的递归公式,由于循环神经网络经常使用 σ 或 tanh 激活函数,其导数值都小于 1,并且权重矩阵也不会太大,因此如果时间间隔 δt 过大,来自时间步 t+δt 的损失影响 et+δt 会趋向于 0,这便是循环神经网络的梯度消失问题,也称长程依赖问题(Long-Term Dependencies Problem)。
要注意的是,在循环神经网络中的梯度消失不是说梯度 ∂Wh∂Lt 消失了,而是当 δt 比较大时, ∂ht+δt∂Lt 消失了。也就是说,参数 Wh 的更新主要靠当前时刻 t 的几个相邻状态来更新,长距离的状态对参数 Wh 没有影响。
长短期记忆网络
长短期记忆网络 (Long Short-Term Memory, LSTM)是 RNN 的一种改进架构,专门设计来解决长期依赖问题,是目前使用最广泛最成功的 RNN 模型。
在普通的 RNN 中,重复模块结构非常简单,例如只有一个 tanh 层。整体结构如图所示:

LSTM 引入了一个额外的状态叫细胞状态(Cell State) ct,它像一条“传送带”,只在上面进行轻微的线性操作,使得信息可以轻松地流过许多时间步。
ct=ft⊙ct−1+it⊙c~t
其中 c~t 是通过非线性函数得到的候选状态
c~t=tanh(Wc[ht−1,xt]+bc)
LSTM 引入门控机制(Gating Mechanism)来保护和控制细胞状态。门是一种让信息选择式通过的方法。它们包含一个 Sigmoid 层和一个逐元乘法操作。Sigmoid 函数取值在 (0, 1) 之间, 表示以一定的比例允许信息通过。
遗忘门:ft 决定从细胞状态中丢弃哪些信息
ft=σ(Wf[ht−1,xt]+bf)
输入门:it 决定哪些新信息要存入细胞状态
it=σ(Wi[ht−1,xt]+bi)
输出门:ot 基于细胞状态,决定输出什么隐藏状态
ot=σ(Wo[ht−1,xt]+b0)
隐藏状态
ht=ot⊙tanh(ct)
下图给出了 LSTM 网络的循环单元结构
通过 LSTM 循环单元,整个网络可以建立较长距离的时序依赖关系。
一般在深度网络参数学习时,参数初始化的值一般都比较小。但是在训练 LSTM 网络时,过小的值会使得遗忘门的值比较小。这意味着前一时刻的信息大部分都丢失了,这样网络很难捕捉到长距离的依赖信息。并且相邻时间间隔的梯度会非常小,这会导致梯度弥散问题。因此遗忘的参数初始值一般都设得比较大。
对于更长的评论,如 "I loved the movie, but the ending was terrible""
,简单 RNN 可能会遇到梯度消失问题。LSTM 的门控机制允许它:
- 在 “but” 处决定忘记之前的正面情感
- 在 “terrible” 处决定存储强烈的负面情感
- 在整个过程中保持细胞状态的稳定流动
门控循环单元
门控循环单元(Gated Recurrent Unit, GRU)是 LSTM 的简化版本,在保持相似性能的同时减少了参数数量。
GRU 的核心结构如下图所示:
GRU 不引入额外的细胞状态,将遗忘门和输入门合并为一个“更新门”,结构更简单,效果通常与 LSTM 类似。
ht=(1−ut)⊙h~t+ut⊙ht−1
更新门(update gate)决定保留多少旧信息
ut=σ(Wu[ht−1,xt]+bu)
在 LSTM 网络中,输入门和遗忘门是互补关系,具有一定的冗余性。GRU 网络直接使用一个更新门来控制输入和遗忘之间的平衡。
重置门(reset gate)决定如何组合新旧信息
rt=σ(Wr[ht−1,xt]+br)
候选激活基于重置门计算
h~t=tanh(Wc[rt⊙ht−1,xt]+bh)
每当重置门接近1时,我们恢复成普通的循环神经网络。对于重置门中所有接近0的项,候选激活是以 xt 为输入的全连接神经网络。因此,任何预先存在的隐状态都会被重置为默认值。
双向循环神经网络
在有些任务中,一个时刻的输出不但和过去时刻的信息有关,也和后续时刻的信息有关。比如给定一个句子,其中一个词的词性由它的上下文决定,即包含左右两边的信息。因此,在这些任务中,我们可以增加一个按照时间的逆序来传递信息的网络层,来增强网络的能力。
双向循环神经网络(Bidirectional Recurrent Neural Network,Bi-RNN)由两层循环神经网络组成,一层处理过去的训练信息,另一层处理将来的训练信息。
下图给出了按时间展开的双向循环神经网络
考虑一句有转折的评论:"This movie is not good."
。如果只从左向右看,看到 "good"
会容易误判为正面。双向 RNN 会同时从左右两个方向处理序列,右边的网络看到 "not"
后会修正对 "good"
的理解,从而得出“负面”的正确结论。
深层循环神经网络
前面我们介绍的循环神经网络只有一个隐藏层,我们当然也可以堆叠两个以上的隐藏层,这样就得到了深度循环神经网络。如下图所示: