References

  • Reading: Dive into Deep Learning (D2L), Modern Recurrent Neural Networks

回顾, 对于 Sequential 数据, 我们会维护一个隐藏状态 来存储过去的信息, 通过

的形式来更新隐藏状态.

1. Long Short-Term Memory (LSTM) Networks

RNN 在长序列上, 由于其梯度会层层累加, 造成 gradient explosion 或 gradient vanishing 问题. 因此我们需要一些更复杂的结构来维护长时间的依赖关系. LSTM 通过引入 Gate 来控制信息的流动, 使得信息可以在 cell state 中长期存储, 有效地解决了 Vanishing gradient 问题.

1.1 LSTM Cell

在 LSTM 中, 我们会维护一个 cell state 来对信息进行长期存储. 在其中, 我们会使用三个 Gate 来控制信息的流动:

  • Input Gate : 控制当前输入 进入 cell state 的程度.
  • Forget Gate : 控制 cell state 中的信息被遗忘 (趋近于 0) 的程度.
  • Output Gate : 控制 cell state 中的信息被输出的程度.

下面给出具体的公式:

假设当前的 batch size 为 , 输入维度为 , 隐藏状态维度为 . 假设输入为 , 隐藏状态为 . 则在 时刻, LSTM 的计算过程为:

  • 其中, , 类似一个 soft-switch, 控制信息的流动. 取 1 时表示完全通过, 取 0 时表示完全不通过.
  • Internal state 表示了 LSTM 的最核心更新规则: 我们会将历史的信息 进行一定程度的遗忘 , 然后将当前输入的信息 进行一定程度的保留 , 最后将两者相加.

最终这个 cell state 会通过一个输出 gate 来控制输出:

在 PyTorch 中, LSTM 已经被实现为一个内置的模块, 我们可以直接使用:

class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=1):
        super(LSTM, self).__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers)
 
    def forward(self, x):
        out, _ = self.lstm(x)
        return out

2. Gated Recurrent Unit (GRU)

GRU 是一个更简化的 LSTM 变种, 它将 input gate 和 forget gate 合并成一个 update gate , 通过一个 reset gate 来控制 cell state 的更新.

在 时刻, GRU 的计算过程为:

  • 当 Reset gate 趋近于 1 时, , 使得当前的 hidden state 退化为: , 也就是 GRU 退化为一个普通的 RNN.
  • 当 Reset gate 趋近于 0 时, 当前的 hidden state 会被重置为 0, 使得模型能够忘记之前的信息, 模型退化为 , 即一个普通的 MLP.

最终我们通过 update gate 来控制当前的 hidden state 的最终输出:

  • 当 趋近于 1 时, , 使得当前的 hidden state 保持不变.
  • 当 趋近于 0 时, , 使得当前的 hidden state 被重置为 .

GRU 的计算图

在 PyTorch 中, GRU 已经被实现为一个内置的模块, 我们可以直接使用:

class GRU(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=1):
        super(GRU, self).__init__()
        self.gru = nn.GRU(input_size, hidden_size, num_layers)
 
    def forward(self, x):
        out, _ = self.gru(x)
        return out

LSTM v.s. GRU

  • GRU 的模型设计更为简洁, 一般而言其训练速度更快
  • 然而由于没有 LSTM 中的 internal state, 在一些具有非常强的长时间依赖关系的任务上, GRU 的性能可能会稍差一些.

3. Deep RNNs

这里的 Deep RNNs 指的是在 RNN 的序列基础上, 我们把每一步的处理模块也换成一个更深层的处理网络. 一个一般的具有 层的长度为 的 RNN 的结构图如下:

Deep RNNs 示意图

可以看到, 这里的每一个 Hidden State (e.g. ) 都有两个输入: 同一时间步的上一层输入 (e.g. ) 和上一时间步的当前层输入 (e.g. ). 这样就形成了一个深度的 RNN 结构. 因此可以抽象为如下数学公式:

  • 其中 , , , .
  • 一般而言 , .

4. Bi-directional RNNs

Bi-directional RNNs 是在 RNN 的基础上, 将输入序列同时从前向和后向进行处理. 这样可以使得模型在每一个时间步都能获得前后文的信息. 这尤其在一些 NLP 任务中 (例如 Speech Detection, Missing Token Prediction 等) 非常有用, 我们常常需要前后文的信息来进行判断.

例如:

  • I am _.
  • I am _ hungry.
  • I am _ hungry and I can eat half a cow.

Bi-directional RNNs

具体而言, 我们会训练两个方向的 RNN:

其中注意, 这里的 是两个不同的 hidden state, 它们的维度是相同的. 但是它们的权重矩阵和偏置项是不同的. 各自相当于是两个独立的 RNN. 训练完成后, 我们会将两个 hidden state 进行整合输出:

5. Machine Translation

  • 机器翻译是一个典型的 Seq2Seq 任务, 其发展和 RNN 密切相关. 翻译中, 我们有一个 Source Language 和一个 Target Language, 两个 sequence 之间的长度, 以及每个 token 的位置等信息都是不一样的. 这样的问题也称为 Unaligned Sequence Problem.

  • 这里以 Tatoeba 数据集为例, 其数据集包含了多种语言的翻译对. 例如 English-French:

    # Go. Va!
    # Hi. Salut !
    # Run. Cous!
    # Run! Courez !
    

5.1 Data Preprocessing

首先我们将数据集进行 tokenization:

src, tgt = data._tokenize(text)
# [['go', '.', '<eos>'], ...]
# [['va', '!', '<eos>'], ...]
  • 这里 <eos> 是一个特殊的 token, 用来表示 end of sequence.
  • 为了进一步简化计算, 将稀有的 token 进行合并统一记为 <unk>.

5.2 Loading Sequence Data of Same Length

我们希望把每个 sequence 的长度都统一为 , 这样可以方便我们进行 batch 训练. 这里我们可以使用 pad_sequence 来实现. 具体而言其会通过 <pad> token 来填充每个 sequence, 使得每个 sequence 的长度都为 .

此外, 在针对 target sequence 的时候, 我们会在每个 sequence 前面加一个 <bos> token 来表示开始, 并且标记了 target.

5.3 Machine Translation 的 Encoder-Decoder 结构

对于机器翻译, 我们会使用一个 Encoder-Decoder 的结构. 其结构如下:

Machine Translation 的 Encoder-Decoder 结构

  • Encoder: 将输入的 source sequence 进行编码, 得到一个 hidden state .
  • Decoder: 将 hidden state 进行解码, 这个 hidden state 会被传入到 decoder 的每一个时间步中, 并联合 Target sequence 的上一个 token 作为输入, 进行解码预测. 模型在看到 <bos> token 后, 会开始进行预测, 直到预测到 <eos> token.

Teacher Forcing: Teacher forcing 会使用真实的 target token 作为输入 (当然是前移一个位置), 而不是使用模型预测的 token 作为下一个输入. 这样可以加快模型的收敛速度, 但是在测试时, 我们需要使用模型预测的 token 作为下一个输入.

5.3.1 Encoder

具体而言, 假设输入的序列为

使用如下的公式进行编码:

我们还会引入一个 context vector 来表示当前的 hidden state (一般是最后一个步骤对应的):

Encoder 的训练权重有两个参数:

  • No. of rows: vocab_size, 表示输入的 token 的个数.
  • No. of columns: embed_size, 表示每个 token 的 embedding 维度.

5.3.2 Decoder

在训练阶段, 我们希望 Decoder 能够和给定的 target sequence 进行对齐. 具体而言, Decoder 会输入三个变量: 上一步的 target token , 上一步的 hidden state , 以及 Encoder 的 context vector :

然后我们通过全连接层和 Softmax 层来进行预测:

5.3.3 Prediction

在预测阶段, 我们只有输入序列, 并不知道 target sequence. 在预测时, 我们永远会从 <bos> token 开始进行解码,直到遇到 <eos> token 为止. 并且此时我们会用预测的 token 作为下一个输入.

在理想状况中, 我们的目标是使得生成的 sequence 的联合概率分布最高:

  • Exhaustive Search: 想要枚举每一种可能进行计算是不现实的, 因为每一步我们都要从 个 token 中进行选择, 这会导致总的计算为 .

  • Greedy Search: 在 Greedy Search 中, Decoder 会通过 Softmax 生成一个概率分布, 每一步会选择最高的概率对应的 token 作为输出, 直到遇到 <eos> token 为止.

    • 然而 Greedy Search 并不保证能够收敛到全局最优.
  • Beam Search:

    • 在每个时间步, 选择出现概率最高的 个 token 作为候选; 在生成的所有下一步的候选中, 再次选择 个 token 作为候选, 直到遇到 <eos> token 为止.
    • 在得到所有的候选后, 通过
    • Beam Search 的时间复杂度

5.3.4 BLEU

引入 BLEU (Bilingual Evaluation Understudy) 来评估翻译的质量 (此时假设我们是有标准答案的). BLEU 是一个基于 n-gram 的评估指标, 其计算方式如下:

  • 其中 是 n-gram 的精确度, 其计算方式如下:
  • , 表示在 target 和prediction 中的 token 数量
  • 是能够匹配到的最大 n-gram 的长度 (因为如果到 -gram 时已经没有能够匹配到的模式了, 那么对于所有的 的 n-gram 都是 0. 我们需要避免将0引入乘积中).

下面以 A,B,C,D,E,F 作为 target, A,B,B,C,D 作为 prediction 为例对 BLEU 的计算进行说明.

  • 首先计算 n-gram precision:
    • 1-gram : 分母考察 prediction 中总的 1-gram 数量, 这里是5. 分子为依次遍历 target 的每个 1-gram, 看是否能够匹配到 prediction 中的 1-gram. 这里是 4 (因为 target 中只有一个 B, 因此对于 prediction 中的第二个 B 不会进行匹配). 因此 .
    • 2-gram : 分母考察 prediction 中总的 2-gram 数量, 这里为 AB,BB,BC,CD 共4 个. 分子为使用 target 的 AB,BC,CD,DE,EF 与 prediction 的 2-gram 进行匹配, 这里一共有 3 个匹配. 因此 .
    • 3-gram : Prediction 的 3-gram 为 ABB,BBC,BCD, 共 3 个. Target 的 3-gram 为 ABC,BCD,CDE,DEF, 共 4 个. 这里 prediction 中的 BCD 和 target 中的 BCD 匹配, 因此 .
    • 4-gram : Prediction 的 4-gram 为 ABBC,BBCD, 共 2 个. Target 的 4-gram 为 ABCD,BCDE,CDEF, 共 3 个. 这里没有匹配, 因此 .
  • 然后计算长度惩罚项:
    • , , 因此 .
  • 此时, 因为 .

若 prediction 和 target 是完全相同的, 则所有 , 对应的 BLEU 为 1. 并且能够参与匹配的 n-gram 越长, 则 BLEU 越高. 前面的惩罚项是为了避免模型输出的长度过短, 使得 BLEU 过高.