9. 现代循环神经网络

9.1. 门控循环单元(GRU)

在观察 一个序列时,并非所有的观测值都是同等重要的。可能存在下述情况:

  • 早期观测值对预测所有未来观测值具有非常重要的意义。 希望存储重要的早期信息。
  • 一些词元没有相关的观测值。希望跳过此类词元。
  • 序列的各个部分之间存在逻辑中断。 例如,书的章节之间可能会有过渡存在。 希望重置状态表示。

解决这些问题最早期的方法是“长短期记忆”(long-short-term memory,LSTM) [Hochreiter & Schmidhuber, 1997],门控循环单元(gated recurrent unit,GRU) [Cho et al., 2014a] 是一个简化的变体,通常能够提供同等的效果, 并且计算 [Chung et al., 2014]的速度明显更快。

9.1.1. 门控隐状态

门控循环单元有专门的门控机制来确定何时更新隐状态, 以及何时重置隐状态,这些机制是可学习的。 例如,如果第一个词元非常重要, 模型将学会在第一次观测之后不更新隐状态。 同样,模型也可以学会跳过不相关的临时观测。 最后,模型还将学会在需要的时候重置隐状态。
gru-3.svg

9.1.1.1. 重置门和更新门

重置门控制从过去状态中获取多少信息; 更新门控制新状态中有多少是旧状态。
对于给定的时间步 9. 现代循环神经网络 - 图2 , 假设输入是一个小批 量 $\mathbf{X}{t} \in \mathbb{R}^{n \times d} $ , 上一个时间步的隐状态是 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D&id=VqllO)。 那么, 重置门 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BR%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BR%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D&id=tQkoF) 和 更新门 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BZ%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BZ%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D&id=mk5f5) 的计算如下所示:
![](https://g.yuque.com/gr/latex?%5Cbegin%7Baligned%7D%0A%5Cmathbf%7BR%7D
%7Bt%7D%20%26%3D%5Csigma%5Cleft(%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20r%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20r%7D%2B%5Cmathbf%7Bb%7D%7Br%7D%5Cright)%20%5C%5C%0A%5Cmathbf%7BZ%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft(%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20z%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20z%7D%2B%5Cmathbf%7Bb%7D%7Bz%7D%5Cright)%0A%5Cend%7Baligned%7D%0A#card=math&code=%5Cbegin%7Baligned%7D%0A%5Cmathbf%7BR%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20r%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20r%7D%2B%5Cmathbf%7Bb%7D%7Br%7D%5Cright%29%20%5C%5C%0A%5Cmathbf%7BZ%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20z%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20z%7D%2B%5Cmathbf%7Bb%7D%7Bz%7D%5Cright%29%0A%5Cend%7Baligned%7D%0A&id=sXyDi)
其中 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BW%7D
%7Bx%20r%7D%2C%20%5Cmathbf%7BW%7D%7Bx%20z%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bd%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BW%7D%7Bx%20r%7D%2C%20%5Cmathbf%7BW%7D%7Bx%20z%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bd%20%5Ctimes%20h%7D&id=wYcaB) 和 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BW%7D%7Bh%20r%7D%2C%20%5Cmathbf%7BW%7D%7Bh%20z%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bh%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BW%7D%7Bh%20r%7D%2C%20%5Cmathbf%7BW%7D%7Bh%20z%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bh%20%5Ctimes%20h%7D&id=rPuuJ) 是权重参数, ![](https://g.yuque.com/gr/latex?%5Cmathbf%7Bb%7D%7Br%7D%2C%20%5Cmathbf%7Bb%7D%7Bz%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B1%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7Bb%7D%7Br%7D%2C%20%5Cmathbf%7Bb%7D_%7Bz%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B1%20%5Ctimes%20h%7D&id=ZvwyF) 是 偏置参数。使用 sigmoid函数将输入值转换到区间 (0,1),便于进行凸组合(门控的内部原理)。

9.1.1.2. 候选隐状态

将重置门 9. 现代循环神经网络 - 图3 与隐状态更新机制集成, 得到在时间步 9. 现代循环神经网络 - 图4 的候选隐状态 (candidate hidden state) 9. 现代循环神经网络 - 图5
9. 现代循环神经网络 - 图6%20%5Cmathbf%7BW%7D%7Bh%20h%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5Cright)%0A#card=math&code=%5Ctilde%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%3D%5Ctanh%20%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%2B%5Cleft%28%5Cmathbf%7BR%7D%7Bt%7D%20%5Codot%20%5Cmathbf%7BH%7D%7Bt-1%7D%5Cright%29%20%5Cmathbf%7BW%7D%7Bh%20h%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5Cright%29%0A&id=SEmZV)
其中符号 9. 现代循环神经网络 - 图7 是 Hadamard积(按元素乘积)。tanh确保候选隐状态中的值保持在区间 (-1,1) 中。$\left(\mathbf{R}
{t} \odot \mathbf{H}{t-1}\right) $可以减少以往状态的影响,每当重置门 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BR%7D%7Bt%7D#card=math&code=%5Cmathbf%7BR%7D%7Bt%7D&id=zbMj7) 中的项接近 1 时, 我们恢复一个普通的循环神经网络。对于 重置门 $\mathbf{R}{t} $ 中所有接近 0 的项,候选隐状态是以 9. 现代循环神经网络 - 图8 作为输入的多层感知机的结果。因此任何预先存在的隐状态都会被重置为默认值.

9.1.1.3. 隐状态

新的隐状态 9. 现代循环神经网络 - 图9 在多大程度上来自旧的状态 9. 现代循环神经网络 - 图10 和 新的候选状态 9. 现代循环神经网络 - 图11。更新门9. 现代循环神经网络 - 图12 仅需要在 9. 现代循环神经网络 - 图139. 现代循环神经网络 - 图14 之间进行按元素的凸组合就可以实现这个目标。这就得出了门控循环单元的最终更新公式:
9. 现代循环神经网络 - 图15%20%5Codot%20%5Ctilde%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%0A#card=math&code=%5Cmathbf%7BH%7D%7Bt%7D%3D%5Cmathbf%7BZ%7D%7Bt%7D%20%5Codot%20%5Cmathbf%7BH%7D%7Bt-1%7D%2B%5Cleft%281-%5Cmathbf%7BZ%7D%7Bt%7D%5Cright%29%20%5Codot%20%5Ctilde%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%0A&id=vxKqF)
每当更新门 9. 现代循环神经网络 - 图16 接近 1 时, 模型就倾向只保留旧状态不更新。此时, 来自 9. 现代循环神经网络 - 图17 的信息基本上被忽略。 相反, 当 9. 现代循环神经网络 - 图18 接近 0 时, 新的隐状态 9. 现代循环神经网络 - 图19 就会仅使用候选隐状态 9. 现代循环神经网络 - 图20 。这些设计可以帮助我们处理循环神经网络中的梯度消失问题, 并更好地捕获时间步距离很长的序列的依赖关系。
9. 现代循环神经网络 - 图21
总之,门控循环单元在学习后会决定是多看过去的信息9. 现代循环神经网络 - 图22,还是当下的信息9. 现代循环神经网络 - 图23。具有以下两个显著特征:

  • 重置门有助于捕获序列中的短期依赖关系。
  • 更新门有助于捕获序列中的长期依赖关系。

    9.1.2. 从零开始实现

    ```python import torch from torch import nn from d2l import torch as d2l

batch_size, num_steps = 32, 35 train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

初始化参数

def get_params(vocab_size, num_hiddens, device): num_inputs = num_outputs = vocab_size

  1. def normal(shape):
  2. return torch.randn(size=shape, device=device)*0.01
  3. def three():
  4. return (normal((num_inputs, num_hiddens)),
  5. normal((num_hiddens, num_hiddens)),
  6. torch.zeros(num_hiddens, device=device))
  7. W_xz, W_hz, b_z = three() # 更新门参数
  8. W_xr, W_hr, b_r = three() # 重置门参数
  9. W_xh, W_hh, b_h = three() # 候选隐状态参数
  10. # 输出层参数
  11. W_hq = normal((num_hiddens, num_outputs))
  12. b_q = torch.zeros(num_outputs, device=device)
  13. # 附加梯度
  14. params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
  15. for param in params:
  16. param.requires_grad_(True)
  17. return params

隐状态的初始化函数

def init_gru_state(batch_size, num_hiddens, device): return (torch.zeros((batch_size, num_hiddens), device=device), )

定义门控循环单元模型, 模型的架构与基本的循环神经网络单元是相同的, 权重更新公式更为复杂。

def gru(inputs, state, params): W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params H, = state outputs = [] for X in inputs: Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z) R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r) H_tilda = torch.tanh((X @ W_xh) + ((R H) @ W_hh) + b_h) H = Z H + (1 - Z) * H_tilda Y = H @ W_hq + b_q outputs.append(Y) return torch.cat(outputs, dim=0), (H,)

训练与预测

vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu() num_epochs, lr = 500, 1 model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,init_gru_state, gru) d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

![output_gru_b77a34_54_1.svg](https://cdn.nlark.com/yuque/0/2022/svg/25432231/1652693525116-ba142a70-15a3-41f9-b61f-3bd1d4ed78b1.svg#clientId=uc3b702d7-5ddc-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u6d9f7ff8&margin=%5Bobject%20Object%5D&name=output_gru_b77a34_54_1.svg&originHeight=301&originWidth=421&originalType=binary&ratio=1&rotation=0&showTitle=false&size=49032&status=done&style=none&taskId=uda04e696-8eb8-4fbc-bf7b-bb40ea1f606&title=)
<a name="7fca0085"></a>
### 9.1.3. 简洁实现
```python
num_inputs = vocab_size
gru_layer = nn.GRU(num_inputs, num_hiddens)
model = d2l.RNNModel(gru_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

output_gru_b77a34_66_1.svg

9.2. 长短期记忆网络(LSTM)

9.2.1. 门控记忆元

长短期记忆网络(long short-term memory,LSTM)引入了与隐状态具有相同形状的记忆元(memory cell),目的是用于记录附加的信息。其通过专用机制决定什么时候记忆或忽略隐状态中的输入。

  • 输出门(output gate)决定新的隐状态将从新的记忆元中获取多少信息。
  • 输入门(input gate)决定将输入的哪部分添加到记忆元中。
  • 遗忘门(forget gate)决定从上一时刻的记忆元中获取多少信息。

lstm-3.svg
遗忘门、输入门和输出门的值都通过sigmoid激活函数处理,结果均在(0,1)范围内。输入为 9. 现代循环神经网络 - 图26 , 前一时间步的隐状态为 9. 现代循环神经网络 - 图27 , 时间步 9. 现代循环神经网络 - 图28 的门被定义如下:
9. 现代循环神经网络 - 图29%20%5C%5C%0A%5Cmathbf%7BF%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft(%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20f%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20f%7D%2B%5Cmathbf%7Bb%7D%7Bf%7D%5Cright)%20%5C%5C%0A%5Cmathbf%7BO%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft(%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20o%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20o%7D%2B%5Cmathbf%7Bb%7D%7Bo%7D%5Cright)%0A%5Cend%7Baligned%7D%0A#card=math&code=%5Cbegin%7Baligned%7D%0A%5Cmathbf%7BI%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20i%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20i%7D%2B%5Cmathbf%7Bb%7D%7Bi%7D%5Cright%29%20%5C%5C%0A%5Cmathbf%7BF%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20f%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20f%7D%2B%5Cmathbf%7Bb%7D%7Bf%7D%5Cright%29%20%5C%5C%0A%5Cmathbf%7BO%7D%7Bt%7D%20%26%3D%5Csigma%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20o%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20o%7D%2B%5Cmathbf%7Bb%7D%7Bo%7D%5Cright%29%0A%5Cend%7Baligned%7D%0A&id=fWmTs)
其中 9. 现代循环神经网络 - 图309. 现代循环神经网络 - 图319. 现代循环神经网络 - 图32
候选记忆元 (candidate memory cell) 9. 现代循环神经网络 - 图33使用tanh函数作为激活函数,函数的值范围为 (-1,1) 。
9. 现代循环神经网络 - 图34%0A#card=math&code=%5Ctilde%7B%5Cmathbf%7BC%7D%7D%7Bt%7D%3D%5Ctanh%20%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20c%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20c%7D%2B%5Cmathbf%7Bb%7D%7Bc%7D%5Cright%29%0A&id=wLNVJ)
在门控循环单元中,有一种机制来控制输入和遗忘(或跳过)。 在长短期记忆网络中: 输入门9. 现代循环神经网络 - 图35控制采用多少来自9. 现代循环神经网络 - 图36的新数据, 而遗忘门9. 现代循环神经网络 - 图37控制保留多少过去的记忆元9. 现代循环神经网络 - 图38的内容:
9. 现代循环神经网络 - 图39
如果遗忘门始终为1且输入门始终为0, 则过去的记忆元9. 现代循环神经网络 - 图40 将随时间被保存并传递到当前时间步。 引入这种设计是为了缓解梯度消失问题, 并更好地捕获序列中的长距离依赖关系。
隐状态是记忆元的tanh的门控版本,确保了隐状态9. 现代循环神经网络 - 图41的值始终在区间(-1,1)内:
9. 现代循环神经网络 - 图42%0A#card=math&code=%5Cmathbf%7BH%7D%7Bt%7D%3D%5Cmathbf%7BO%7D%7Bt%7D%5Codot%20tanh%28%5Cmathbf%7BC%7D_%7Bt%7D%29%0A&id=UPbDP)
只要输出门接近1,我们就能够有效地将所有记忆信息传递给预测部分, 而对于输出门接近0,我们只保留记忆元内的所有信息,而不需要更新隐状态。

9.2.2. 从零开始实现

import torch
from torch import nn
from d2l import torch as d2l

# 数据加载
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

# 初始化模型参数
def get_lstm_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size

    def normal(shape):
        return torch.randn(size=shape, device=device)*0.01

    def three():
        return (normal((num_inputs, num_hiddens)),
                normal((num_hiddens, num_hiddens)),
                torch.zeros(num_hiddens, device=device))

    W_xi, W_hi, b_i = three()  # 输入门参数
    W_xf, W_hf, b_f = three()  # 遗忘门参数
    W_xo, W_ho, b_o = three()  # 输出门参数
    W_xc, W_hc, b_c = three()  # 候选记忆元参数
    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # 附加梯度
    params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
              b_c, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params

# 隐状态需要返回额外的记忆元,形状为(批量大小,隐藏单元数)。LSTM中有两个隐状态输出Ct和Ht
def init_lstm_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),
            torch.zeros((batch_size, num_hiddens), device=device))

# 模型定义
def lstm(inputs, state, params):
    [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
     W_hq, b_q] = params
    (H, C) = state
    outputs = []
    for X in inputs:
        I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
        F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
        O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
        C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
        C = F * C + I * C_tilda
        H = O * torch.tanh(C)
        Y = (H @ W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H, C)

# 训练和预测
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model=d2l.RNNModelScratch(len(vocab),num_hiddens,device,get_lstm_params,init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

output_lstm_86eb9f_54_1.svg

9.2.3. 简洁实现

num_inputs = vocab_size
lstm_layer = nn.LSTM(num_inputs, num_hiddens)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

output_lstm_86eb9f_66_1.svg
由于序列的长距离依赖性,训练长短期记忆网络 和其他序列模型(例如门控循环单元)的成本是相当高的。 在后面的内容中,我们将讲述更高级的替代模型,如transformer。

9.3. 深度循环神经网络

下图描述具有9. 现代循环神经网络 - 图45个隐藏层的深度循环神经网络。这些讨论也适应于其他序列模型。
deep-rnn.svg
小批量的输入数据为 9. 现代循环神经网络 - 图479. 现代循环神经网络 - 图48 层的隐状态为 9. 现代循环神经网络 - 图49%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D#card=math&code=%5Cmathbf%7BH%7D%7Bt%7D%5E%7B%28l%29%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D&id=CrK3T) , 输出层变量设为![](https://g.yuque.com/gr/latex?%5Cmathbf%7BO%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20q%7D#card=math&code=%5Cmathbf%7BO%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20q%7D&id=Km0CN) 。 设 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BH%7D%7Bt%7D%5E%7B(0)%7D%3D%5Cmathbf%7BX%7D%7Bt%7D#card=math&code=%5Cmathbf%7BH%7D%7Bt%7D%5E%7B%280%29%7D%3D%5Cmathbf%7BX%7D%7Bt%7D&id=TbPES) , 第 9. 现代循环神经网络 - 图50 个隐藏层的隐状态使用激活函数 ![](https://g.yuque.com/gr/latex?%5Cphi%7Bl%7D#card=math&code=%5Cphi%7Bl%7D&id=NPuAM) , 则:
![](https://g.yuque.com/gr/latex?%5Cmathbf%7BH%7D
%7Bt%7D%5E%7B(l)%7D%3D%5Cphi%7Bl%7D%5Cleft(%5Cmathbf%7BH%7D%7Bt%7D%5E%7B(l-1)%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%5E%7B(l)%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%5E%7B(l)%7D%20%5Cmathbf%7BW%7D%7Bh%20h%7D%5E%7B(l)%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5E%7B(l)%7D%5Cright)%0A#card=math&code=%5Cmathbf%7BH%7D%7Bt%7D%5E%7B%28l%29%7D%3D%5Cphi%7Bl%7D%5Cleft%28%5Cmathbf%7BH%7D%7Bt%7D%5E%7B%28l-1%29%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%5E%7B%28l%29%7D%2B%5Cmathbf%7BH%7D%7Bt-1%7D%5E%7B%28l%29%7D%20%5Cmathbf%7BW%7D%7Bh%20h%7D%5E%7B%28l%29%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5E%7B%28l%29%7D%5Cright%29%0A&id=TyVBw)
输出层的计算仅基于第 9. 现代循环神经网络 - 图51 个隐藏层最终的隐状态:
![](https://g.yuque.com/gr/latex?%5Cmathbf%7BO%7D
%7Bt%7D%3D%5Cmathbf%7BH%7D%7Bt%7D%5E%7B(L)%7D%20%5Cmathbf%7BW%7D%7Bh%20q%7D%2B%5Cmathbf%7Bb%7D%7Bq%7D%0A#card=math&code=%5Cmathbf%7BO%7D%7Bt%7D%3D%5Cmathbf%7BH%7D%7Bt%7D%5E%7B%28L%29%7D%20%5Cmathbf%7BW%7D%7Bh%20q%7D%2B%5Cmathbf%7Bb%7D_%7Bq%7D%0A&id=klOX6)
隐藏层数目9. 现代循环神经网络 - 图52和隐藏单元数目9. 现代循环神经网络 - 图53都是超参数。以LSTM模型为例,与先前代码的唯一区别是指定了层数。

import torch
from torch import nn
from d2l import torch as d2l

# 加载数据集
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
# 设定模型
vocab_size, num_hiddens, num_layers = len(vocab), 256, 2    # 设定了层的数量
num_inputs = vocab_size
device = d2l.try_gpu()
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers)
model = d2l.RNNModel(lstm_layer, len(vocab))
model = model.to(device)
# 训练和预测
num_epochs, lr = 500, 2
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)

output_deep-rnn_d70a11_24_1.svg
深度循环神经网络需要大量的调参(如学习率和修剪) 来确保合适的收敛,模型的初始化也需要谨慎。

9.4. 双向循环神经网络

以往假设的目标是: 在给定观测的情况下, 对下一个输出进行建模。然而,还有许多需要考虑上下文的应用场景。例如,根据上下文传达的信息进行完形填空,或者根据上下文信息进行命名实体识别。
隐变量模型: 在任意时间步9. 现代循环神经网络 - 图55,假设存在某个隐变量9. 现代循环神经网络 - 图56, 通过概率9. 现代循环神经网络 - 图57#card=math&code=P%28x%7Bt%7D%E2%88%A3h%7Bt%7D%29&id=ek4uE)控制观测到的9. 现代循环神经网络 - 图58。 此外,任何9. 现代循环神经网络 - 图59转移都是由一些状态转移概率9. 现代循环神经网络 - 图60#card=math&code=P%28h%7Bt%2B1%7D%E2%88%A3h%7Bt%7D%29&id=GBzeh)给出:
hmm.svg
因此,对于有9. 现代循环神经网络 - 图62个观测值的序列, 我们在观测状态和隐状态上具有以下联合概率分布:
9. 现代循环神经网络 - 图63%3D%5Cprod%7Bt%3D1%7D%5E%7BT%7D%20P%5Cleft(h%7Bt%7D%20%5Cmid%20h%7Bt-1%7D%5Cright)%20P%5Cleft(x%7Bt%7D%20%5Cmid%20h%7Bt%7D%5Cright)%2C%20%5Ctext%20%7B%20where%20%7D%20P%5Cleft(h%7B1%7D%20%5Cmid%20h%7B0%7D%5Cright)%3DP(h%7B1%7D)%0A#card=math&code=P%5Cleft%28x%7B1%7D%2C%20%5Cldots%2C%20x%7BT%7D%2C%20h%7B1%7D%2C%20%5Cldots%2C%20h%7BT%7D%5Cright%29%3D%5Cprod%7Bt%3D1%7D%5E%7BT%7D%20P%5Cleft%28h%7Bt%7D%20%5Cmid%20h%7Bt-1%7D%5Cright%29%20P%5Cleft%28x%7Bt%7D%20%5Cmid%20h%7Bt%7D%5Cright%29%2C%20%5Ctext%20%7B%20where%20%7D%20P%5Cleft%28h%7B1%7D%20%5Cmid%20h%7B0%7D%5Cright%29%3DP%28h%7B1%7D%29%0A&id=I2S5T)
假设观测到所有的 9. 现代循环神经网络 - 图64 , 除了 9. 现代循环神经网络 - 图65 , 目标是计算 9. 现代循环神经网络 - 图66#card=math&code=P%5Cleft%28x%7Bj%7D%20%5Cmid%20x%7B-j%7D%5Cright%29&id=FtfJi),其中,9. 现代循环神经网络 - 图67%20%5Ctext%20%7B%20%E3%80%82%20%7D#card=math&code=x%7B-j%7D%3D%5Cleft%28x%7B1%7D%2C%20%5Cldots%2C%20x%7Bj-1%7D%2C%20x%7Bj%2B1%7D%2C%20%5Cldots%2C%20x%7BT%7D%5Cright%29%20%5Ctext%20%7B%20%E3%80%82%20%7D&id=NYe8B)
如果希望循环神经网络具有前瞻能力,需要增加反向传递信息的隐藏层,下图述了具有单个隐藏层的双向循环神经网络(bidirectional RNNs) 的架构。
birnn.svg
双向循环神经网络不好解释, 只能把它们当作通用的、可学习的函数。 这种转变集中体现了现代深度网络的设计原则: 首先使用经典统计模型的函数依赖类型,然后将其参数化为通用形式。
对于任意时间步 9. 现代循环神经网络 - 图69 , 给定一个小批量的输入数据 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BX%7D
%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20d%7D#card=math&code=%5Cmathbf%7BX%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20d%7D&id=iQgk2) ,设该时间步的 前向和反向隐状态分别为 ![](https://g.yuque.com/gr/latex?%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D#card=math&code=%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D&id=S0ijO) 和 ![](https://g.yuque.com/gr/latex?%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D#card=math&code=%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20h%7D&id=SvlKC) 。 更新如下:
![](https://g.yuque.com/gr/latex?%5Cbegin%7Barray%7D%7Bl%7D%0A%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D
%7Bt%7D%3D%5Cphi%5Cleft(%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%5E%7B(f)%7D%2B%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20h%7D%5E%7B(f)%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5E%7B(f)%7D%5Cright)%20%5C%5C%0A%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%3D%5Cphi%5Cleft(%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%5E%7B(b)%7D%2B%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%2B1%7D%20%5Cmathbf%7BW%7D%7Bh%20h%7D%5E%7B(b)%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5E%7B(b)%7D%5Cright)%0A%5Cend%7Barray%7D%0A#card=math&code=%5Cbegin%7Barray%7D%7Bl%7D%0A%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%3D%5Cphi%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%5E%7B%28f%29%7D%2B%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D%7Bt-1%7D%20%5Cmathbf%7BW%7D%7Bh%20h%7D%5E%7B%28f%29%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5E%7B%28f%29%7D%5Cright%29%20%5C%5C%0A%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D%3D%5Cphi%5Cleft%28%5Cmathbf%7BX%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bx%20h%7D%5E%7B%28b%29%7D%2B%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%2B1%7D%20%5Cmathbf%7BW%7D%7Bh%20h%7D%5E%7B%28b%29%7D%2B%5Cmathbf%7Bb%7D%7Bh%7D%5E%7B%28b%29%7D%5Cright%29%0A%5Cend%7Barray%7D%0A&id=A2E3C)
接下来, 将前向隐状态 ![](https://g.yuque.com/gr/latex?%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D
%7Bt%7D#card=math&code=%5Coverrightarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D&id=bIusX) 和反向隐状态 ![](https://g.yuque.com/gr/latex?%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D#card=math&code=%5Coverleftarrow%7B%5Cmathbf%7BH%7D%7D%7Bt%7D&id=LEMXG) 连接起来, 获得需要送入输出层的隐状态 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BH%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%202%20h%7D#card=math&code=%5Cmathbf%7BH%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%202%20h%7D&id=oZHsz) 。在具有多个隐藏层的深度双向循环神经网络中, 该信息作为输入传递到下一个双向层。 最后, 输出层计算得到的输出为 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BO%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20q%7D#card=math&code=%5Cmathbf%7BO%7D%7Bt%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7Bn%20%5Ctimes%20q%7D&id=vKSx0):
![](https://g.yuque.com/gr/latex?%5Cmathbf%7BO%7D
%7Bt%7D%3D%5Cmathbf%7BH%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bh%20q%7D%2B%5Cmathbf%7Bb%7D%7Bq%7D%0A#card=math&code=%5Cmathbf%7BO%7D%7Bt%7D%3D%5Cmathbf%7BH%7D%7Bt%7D%20%5Cmathbf%7BW%7D%7Bh%20q%7D%2B%5Cmathbf%7Bb%7D%7Bq%7D%0A&id=zonNN)
这里, 权重矩阵 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7BW%7D
%7Bh%20q%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B2%20h%20%5Ctimes%20q%7D#card=math&code=%5Cmathbf%7BW%7D%7Bh%20q%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B2%20h%20%5Ctimes%20q%7D&id=AFYgh) 、偏置 ![](https://g.yuque.com/gr/latex?%5Cmathbf%7Bb%7D%7Bq%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B1%20%5Ctimes%20q%7D#card=math&code=%5Cmathbf%7Bb%7D_%7Bq%7D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B1%20%5Ctimes%20q%7D&id=Aj9Uj) 。事实上, 这两个方向可以拥有不同数量的隐藏单元。
双向循环神经网络的一个关键特性是:使用来自序列两端的信息来估计输出。但是在对下一个词元进行预测的情况中,这样的模型并不是我们所需的。因为在训练期间,我们利用过去和未来的数据来估计现在空缺的词; 而在测试期间,我们只有过去的数据,因此精度将会很差。
另一个严重问题是,双向循环神经网络的计算速度非常慢。 其主要原因是网络的前向传播需要在双向层中进行前向和后向递归, 并且网络的反向传播还依赖于前向传播的结果。 因此,梯度求解将有一个非常长的链。
双向层的使用在实践中非常少,并且仅仅应用于部分场合。 例如,填充缺失的单词、词元注释(例如,用于命名实体识别)以及作为序列处理流水线中的一个步骤对序列进行编码(例如,用于机器翻译)。

# 通过设置“bidirective=True”来定义双向LSTM模型
lstm_layer = nn.LSTM(num_inputs, num_hiddens, num_layers, bidirectional=True)

9.5. 机器翻译与数据集

机器翻译是序列转换模型(sequence transduction)的核心问题。机器翻译的数据集是由源语言和目标语言的文本序列对组成的。
首先,下载一个由Tatoeba项目的双语句子对 组成的“英-法”数据集,数据集中的每一行都是制表符分隔的文本序列对。 每个文本序列可以是一个句子, 也可以是包含多个句子的一个段落。 英语是源语言(source language), 法语是目标语言(target language)。

#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip',
                           '94646ad1522d915e7b0f9296181140edcf86a4f5')

#@save
def read_data_nmt():
    """载入“英语-法语”数据集"""
    data_dir = d2l.download_extract('fra-eng')
    with open(os.path.join(data_dir, 'fra.txt'), 'r',
             encoding='utf-8') as f:
        return f.read()

raw_text = read_data_nmt()
print(raw_text[:75])
"""
Go. Va !
Hi. Salut !
Run!        Cours !
Run!        Courez !
Who?        Qui ?
Wow!        Ça alors !
"""

原始文本数据需要经过几个预处理步骤。

#@save
def preprocess_nmt(text):
    """预处理“英语-法语”数据集"""
    def no_space(char, prev_char):# 是否为符号且先前位置没有空格
        return char in set(',.!?') and prev_char != ' '

    # 1.使用空格替换不间断空格
    # 2.使用小写字母替换大写字母
    text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
    # 3.在单词和标点符号之间插入空格
    out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
           for i, char in enumerate(text)]
    return ''.join(out)    # 将所有的字符链接起来

text = preprocess_nmt(raw_text)
print(text[:80])
"""
go .        va !
hi .        salut !
run !       cours !
run !       courez !
who ?       qui ?
wow !       ça alors !
"""

在机器翻译中,我们更喜欢单词级词元化。 下面的tokenize_nmt函数对前num_examples个文本序列对进行词元, 其中每个词元要么是一个词,要么是一个标点符号。 此函数返回两个词元列表:sourcetargetsource[i]是源语言第i个文本序列的词元列表, target[i]是目标语言第i个文本序列的词元列表。

#@save
def tokenize_nmt(text, num_examples=None):
    """词元化“英语-法语”数据数据集"""
    source, target = [], []
    for i, line in enumerate(text.split('\n')):
        if num_examples and i > num_examples:
            break
        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' '))    # 分成了一个个单词
            target.append(parts[1].split(' '))
    return source, target

source, target = tokenize_nmt(text)
source[:3], target[:3]
"""
([['go', '.'],
  ['hi', '.'],
  ['run', '!']],
 [['va', '!'],
  ['salut', '!'],
  ['cours', '!']])
"""

分别为源语言和目标语言构建词表。 使用单词级词元化时,词表大小将明显大于使用字符级词元化时的词表大小。 为了缓解这一问题,将出现次数少于2次的低频率词元视为未知<unk>词元。 除此之外,还指定了额外的特定词元, 例如在小批量时用于将序列填充到相同长度的填充词元<pad>, 以及序列的开始词元<bos>和结束词元<eos>

src_vocab = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)
# 10012

语言模型中的序列样本都有一个固定的长度,以便以相同形状的小批量进行加载,这个固定长度是由 8.3节中的 num_steps(时间步数或词元数量)参数指定的。 在机器翻译中,可以通过截断(truncation)和 填充(padding)使得每个文本序列具有相同的长度。

#@save
def truncate_pad(line, num_steps, padding_token):
    """截断或填充文本序列"""
    if len(line) > num_steps:
        return line[:num_steps]  # 截断
    return line + [padding_token] * (num_steps - len(line))  # 填充

truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])
# [47, 4, 1, 1, 1, 1, 1, 1, 1, 1]

现在定义函数将文本序列转换成小批量数据集用于训练。 将“”词元添加到所有序列的末尾。 当模型通过一个词元接一个词元地生成序列进行预测时, 生成的“”词元说明完成了序列输出工作。 此外,我们还记录了未填充时每个文本序列的长度,在稍后将要介绍的一些模型会需要这个长度信息。

#@save
def build_array_nmt(lines, vocab, num_steps):
    """将机器翻译的文本序列转换成小批量"""
    lines = [vocab[l] for l in lines]    # 将文本转化为数字
    lines = [l + [vocab['<eos>']] for l in lines]    # 每行添加结束符
    array = torch.tensor([truncate_pad(l, num_steps, vocab['<pad>']) for l in lines])    # 转为长度相同的张量
    valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
    return array, valid_len

最后,我们定义load_data_nmt函数来返回数据迭代器, 以及源语言和目标语言的两种词表。

#@save
def load_data_nmt(batch_size, num_steps, num_examples=600):
    """返回翻译数据集的迭代器和词表"""
    text = preprocess_nmt(read_data_nmt())
    source, target = tokenize_nmt(text, num_examples)
    src_vocab = d2l.Vocab(source, min_freq=2,
                          reserved_tokens=['<pad>', '<bos>', '<eos>'])
    tgt_vocab = d2l.Vocab(target, min_freq=2,
                          reserved_tokens=['<pad>', '<bos>', '<eos>'])
    src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
    tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
    data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
    data_iter = d2l.load_array(data_arrays, batch_size)
    return data_iter, src_vocab, tgt_vocab

下面我们读出“英语-法语”数据集中的第一个小批量数据。

train_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
    print('X:', X.type(torch.int32))
    print('X的有效长度:', X_valid_len)
    print('Y:', Y.type(torch.int32))
    print('Y的有效长度:', Y_valid_len)
    break
"""
X: tensor([[164,  12,   4,   3,   1,   1,   1,   1],
        [ 44,  12,   4,   3,   1,   1,   1,   1]], dtype=torch.int32)
X的有效长度: tensor([4, 4])
Y: tensor([[0, 5, 3, 1, 1, 1, 1, 1],
        [0, 5, 3, 1, 1, 1, 1, 1]], dtype=torch.int32)
Y的有效长度: tensor([3, 3])
"""

9.6. 编码器-解码器架构

机器翻译是序列转换模型的一个核心问题, 其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出, 可以设计一个包含两个主要组件的架构: 编码器(encoder)接受一个长度可变的序列作为输入, 并将其转换为具有固定形状的编码状态。解码器(decoder)将固定形状的编码状态映射到长度可变的序列。 这被称为编码器-解码器(encoder-decoder)架构。
encoder-decoder.svg

from torch import nn


#@save
class Encoder(nn.Module):
    """编码器-解码器架构的基本编码器接口"""
    def __init__(self, **kwargs):
        super(Encoder, self).__init__(**kwargs)

    def forward(self, X, *args):
        raise NotImplementedError

解码器接口新增一个init_state函数, 用于将编码器的输出(enc_outputs)转换为特定的状态。 注意,此步骤可接收额外的输入,例如:输入序列的有效长度。 为了逐个地生成长度可变的词元序列, 解码器在每个时间步都会将输入和编码后的状态 映射成当前时间步的输出词元。

#@save
class Decoder(nn.Module):
    """编码器-解码器架构的基本解码器接口"""
    def __init__(self, **kwargs):
        super(Decoder, self).__init__(**kwargs)

    def init_state(self, enc_outputs, *args):
        raise NotImplementedError

    def forward(self, X, state):
        raise NotImplementedError

#@save
class EncoderDecoder(nn.Module):
    """编码器-解码器架构的基类"""
    def __init__(self, encoder, decoder, **kwargs):
        super(EncoderDecoder, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, enc_X, dec_X, *args):
        enc_outputs = self.encoder(enc_X, *args)
        dec_state = self.decoder.init_state(enc_outputs, *args)
        return self.decoder(dec_X, dec_state)

9.7. 序列到序列学习(seq2seq)

下面使用两个循环神经网络的编码器和解码器, 并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务 [Sutskever et al., 2014][Cho et al., 2014b]
seq2seq.svg
表示序列结束词元,一旦输出序列生成此词元,模型就会停止预测。在循环神经网络解码器的初始化时间步,有两个特定的设计决定: 首先,表示序列开始词元,它是解码器的输入序列的第一个词元。 其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。
下面构建上图的设计,并基于先前的机器翻译数据集进行训练。

9.7.1 编码器

假设输入序列是9. 现代循环神经网络 - 图72, 其中9. 现代循环神经网络 - 图73是输入文本序列中的第9. 现代循环神经网络 - 图74个词元。 在时间步9. 现代循环神经网络 - 图75,循环神经网络将词元9. 现代循环神经网络 - 图76的输入特征向量 9. 现代循环神经网络 - 图779. 现代循环神经网络 - 图78转换为9. 现代循环神经网络 - 图79,即9. 现代循环神经网络 - 图80#card=math&code=%5Cmathbf%7Bh%7D%7Bt%7D%3Df%28%5Cmathbf%7Bx%7D%7Bt%7D%2C%5Cmathbf%7Bh%7D%7Bt-1%7D%29&id=ia8gV)
我们使用的是一个单向循环神经网络来设计编码器, 其中隐状态只依赖于输入子序列, 这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置组成。 我们也可以使用双向循环神经网络构造编码器, 其中隐状态依赖于两个输入子序列, 两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列, 隐状态对整个序列的信息都进行了编码。由于编码器并不进行预测,因此其总是会观测到后面的信息。
现在,让我们实现循环神经网络编码器。 注意,我们使用了
嵌入层_(embedding layer) 来获得输入序列中每个词元的特征向量。 嵌入层的权重是一个矩阵, 其行数等于输入词表的大小(vocab_size), 其列数等于特征向量的维度(embed_size)。 另外,本文选择了一个多层门控循环单元来实现编码器。

#@save
class Seq2SeqEncoder(d2l.Encoder):
    """用于序列到序列学习的循环神经网络编码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
                          dropout=dropout)

    def forward(self, X, *args):
        X = self.embedding(X)    # 对输入(batch_size,num_steps)执行查询操作
        # 输出'X'的形状:(batch_size,num_steps,embed_size)

        # `nn.Embedding`是一种查询表,vocab_size指定了查询表的大小,embed_size指定了每个查询向量的维度。可以理解为每个词对应特征向量的维度

        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)
        # 如果未提及状态,则默认为0
        output, state = self.rnn(X)
        # output的形状:(num_steps,batch_size,num_hiddens)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state
encoder=Seq2SeqEncoder(vocab_size=10,embed_size=8,num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape    # torch.Size([7, 4, 16])

由于这里使用的是门控循环单元, 所以在最后一个时间步的多层隐状态的形状是 (隐藏层的数量,批量大小,隐藏单元的数量)。 如果使用长短期记忆网络,state中还将包含记忆单元信息。

torch.Size([2, 4, 16])

9.7.2 解码器

编码器输出的上下文变量9. 现代循环神经网络 - 图81对整个输入序列9. 现代循环神经网络 - 图82进行编码。 对于每个时间步9. 现代循环神经网络 - 图83, 解码器输出9. 现代循环神经网络 - 图84的概率取决于先前的输出子序列9. 现代循环神经网络 - 图85和上下文变量9. 现代循环神经网络 - 图86。为了在序列上模型化这种条件概率,可以将上一时间步的输出9. 现代循环神经网络 - 图87 和上下文变量9. 现代循环神经网络 - 图88作为循环神经网络解码器的输入。然后在当前时间步将它们和上一隐状态9. 现代循环神经网络 - 图89转换为隐状态9. 现代循环神经网络 - 图90。即9. 现代循环神经网络 - 图91#card=math&code=%5Cmathbf%7Bs%7D%7Bt%27%7D%3Dg%28y%7Bt%27-1%7D%2C%5Cmathbf%7Bc%7D%2C%5Cmathbf%7Bs%7D%7Bt%27-1%7D%29&id=Vll8B). 在获得解码器的隐状态之后, 我们可以使用全连接层和softmax操作 来计算在时间步9. 现代循环神经网络 - 图92时输出![](https://g.yuque.com/gr/latex?y%7Bt’%7D#card=math&code=y%7Bt%27%7D&id=OKifL)的条件概率分布 ![](https://g.yuque.com/gr/latex?P(y%7Bt%E2%80%B2%7D%E2%88%A3y%7B1%7D%2C%E2%80%A6%2Cy%7Bt%E2%80%B2%E2%88%921%7D%2C%5Cmathbf%7Bc%7D)#card=math&code=P%28y%7Bt%E2%80%B2%7D%E2%88%A3y%7B1%7D%2C%E2%80%A6%2Cy_%7Bt%E2%80%B2%E2%88%921%7D%2C%5Cmathbf%7Bc%7D%29&id=rG1Kh)。
我们直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。 这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元

class Seq2SeqDecoder(d2l.Decoder):
    """用于序列到序列学习的循环神经网络解码器"""
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)    # 输入的查询表,Encoder与Decoder使用不同的语言词表
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,
                          dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)    # 用于预测的全连接层

    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]    # 获取Encoder返回变量中的state

    def forward(self, X, state):
        # 输出'X'的形状:(batch_size,num_steps,embed_size)
        X = self.embedding(X).permute(1, 0, 2)
        # 广播context,使其具有与X相同的num_steps
        context = state[-1].repeat(X.shape[0], 1, 1)    # 上下文变量为最后一层隐状态的输出,因为我们认为,有效信息都是存储在隐状态之中的
        X_and_context = torch.cat((X, context), 2)
        output, state = self.rnn(X_and_context, state)
        output = self.dense(output).permute(1, 0, 2)
        # output的形状:(batch_size,num_steps,vocab_size)
        # state[0]的形状:(num_layers,batch_size,num_hiddens)
        return output, state

下面,我们用与前面提到的编码器中相同的超参数来实例化解码器。 如我们所见,解码器的输出形状变为(批量大小,时间步数,词表大小), 其中张量的最后一个维度存储预测的词元分布。

decoder=Seq2SeqDecoder(vocab_size=10,embed_size=8,num_hiddens=16,num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape    # (torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))

seq2seq-details.svg

9.7.3. 损失函数

类似于语言模型,解码器可以使用softmax来获得分布, 并通过计算交叉熵损失函数来进行优化。我们应该将填充词元的预测排除在损失函数的计算之外。

#@save
def sequence_mask(X, valid_len, value=0):
    """在序列中使用0屏蔽不相关的项"""
    maxlen = X.size(1)    # (seq_num,seq_len)
    mask = torch.arange((maxlen), dtype=torch.float32,
                        device=X.device)[None, :] < valid_len[:, None]
    X[~mask] = value
    return X

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))
"""
tensor([[1, 0, 0],
        [4, 5, 0]])
"""

扩展softmax交叉熵损失函数来遮蔽不相关的预测。 最初,所有预测词元的掩码都设置为1。 一旦给定了有效长度,与填充词元对应的掩码将被设置为0。 最后,将所有词元的损失乘以掩码,以过滤掉损失中填充词元产生的不相关预测。

#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""
    # pred的形状:(batch_size,num_steps,vocab_size)
    # label的形状:(batch_size,num_steps)
    # valid_len的形状:(batch_size,)
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)
        self.reduction='none'
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
            pred.permute(0, 2, 1), label)
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss

9.7.4. 训练

#@save
def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型"""
    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])

    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    loss = MaskedSoftmaxCELoss()
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                     xlim=[10, num_epochs])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        metric = d2l.Accumulator(2)  # 训练损失总和,词元数量
        for batch in data_iter:
            optimizer.zero_grad()
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
                          device=device).reshape(-1, 1)    # 为解码器加入序列开始词元
            dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学
            Y_hat, _ = net(X, dec_input, X_valid_len)
            l = loss(Y_hat, Y, Y_valid_len)
            l.sum().backward()      # 损失函数的标量进行“反向传播”
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
        f'tokens/sec on {str(device)}')

# 模型训练
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
# loss 0.019, 10006.3 tokens/sec on cuda:0

output_seq2seq_13725e_138_1.svg

9.7.5. 预测

与训练类似,序列开始词元<bos> 在初始时间步被输入到解码器中。
seq2seq-predict.svg

#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为评估模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    # 添加批量轴
    enc_X = torch.unsqueeze(torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X, enc_valid_len)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

9.7.6. 预测序列的评估

预测句子长度与真实句子长度可能并不相同,可以用BLEU来衡量预测序列的好坏。原则上说,对于预测序列中的任意n元语法(n-grams), BLEU的评估都是这个n元语法是否出现在标签序列中。我们将BLEU定义为:
9. 现代循环神经网络 - 图96%5Cright)%20%5Cprod%7Bn%3D1%7D%5E%7Bk%7D%20p%7Bn%7D%5E%7B1%20%2F%202%5E%7Bn%7D%7D%0A#card=math&code=%5Cexp%20%5Cleft%28%5Cmin%20%5Cleft%280%2C1-%5Cfrac%7B%5Coperatorname%7Blen%7D%7B%5Ctext%20%7Blabel%20%7D%7D%7D%7B%5Coperatorname%7Blen%7D%7B%5Ctext%20%7Bpred%20%7D%7D%7D%5Cright%29%5Cright%29%20%5Cprod%7Bn%3D1%7D%5E%7Bk%7D%20p%7Bn%7D%5E%7B1%20%2F%202%5E%7Bn%7D%7D%0A&id=g5w2w)
其中9. 现代循环神经网络 - 图97表示标签序列中的词元数和9. 现代循环神经网络 - 图98表示预测序列中的词元数, 9. 现代循环神经网络 - 图99是用于匹配的最长的9. 现代循环神经网络 - 图100元语法。 另外,用9. 现代循环神经网络 - 图101表示9. 现代循环神经网络 - 图102元语法的精确度,它是两个数量的比值: 预测序列与标签序列中匹配的n元语法的数量, 预测序列中n元语法的数量。 具体地说,给定标签序列A、B、C、D、E、F 和预测序列A、B、B、C、D, 我们有p1=4/5、p2=3/4、p3=1/3和p4=0。
QQ图片20220509174502.png
根据BLEU的定义, 当预测序列与标签序列完全相同时,BLEU为1。此外,由于n元语法越长则匹配难度越大, 所以BLEU为更长的n元语法的精确度分配更大的权重。 具体来说,当9. 现代循环神经网络 - 图104固定时,9. 现代循环神经网络 - 图105 会随着n的增长而增加(9. 现代循环神经网络 - 图106恒小于等于1)。而且,由于预测的序列越短获得的9. 现代循环神经网络 - 图107值越高, 所以上式中乘法项之前的系数用于惩罚较短的预测序列。 例如,当k=2时,给定标签序列A、B、C、D、E、F和预测序列A、B,尽管p1=p2=1, 惩罚因子9. 现代循环神经网络 - 图108%E2%89%880.14#card=math&code=exp%281%E2%88%926%2F2%29%E2%89%880.14&id=ZLFI2)会降低BLEU。BLEU的代码实现如下。

def bleu(pred_seq, label_seq, k):  #@save
    """计算BLEU"""
    pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    score = math.exp(min(0, 1 - len_label / len_pred))
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)
        for i in range(len_label - n + 1):
            label_subs[' '.join(label_tokens[i: i + n])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    return score

最后,利用训练好的循环神经网络“编码器-解码器”模型, 将几个英语句子翻译成法语,并计算BLEU的最终结果。

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, attention_weight_seq = predict_seq2seq(
        net, eng, src_vocab, tgt_vocab, num_steps, device)
    print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')
"""
go . => va au <unk> !, bleu 0.000
i lost . => j'ai perdu perdu ., bleu 0.783
he's calm . => il est essaye il partie paresseux ., bleu 0.418
i'm home . => je suis chez tom chez triste pas pas pas <unk>, bleu 0.376
"""

9.8. 束搜索

上一节我们逐个预测输出序列, 直到预测序列中出现特定的序列结束词元“”。 在本节中,我们将探讨一些搜索策略。
在任意时间步 9. 现代循环神经网络 - 图109 , 解码器输出 9. 现代循环神经网络 - 图110 的概率取决于 时间步 9. 现代循环神经网络 - 图111 之前的输出子序列 9. 现代循环神经网络 - 图112 对输入序列的信息进行编码得到的上下文变量9. 现代循环神经网络 - 图113。 为了量化计算代价,用9. 现代循环神经网络 - 图114表示输出词表, 其中包含“”, 所以这个词汇集合的基数 9. 现代循环神经网络 - 图115 就是词表的大小。 我们还将输出序列的最大词元数指定为 9. 现代循环神经网络 - 图116 。 因此, 我们的目标是从所有 9. 现代循环神经网络 - 图117#card=math&code=%5Cmathcal%7BO%7D%5Cleft%28%7C%5Cmathcal%7BY%7D%7C%5E%7BT%5E%7B%5Cprime%7D%7D%5Cright%29&id=eXefR) 个可能的输 出序列中寻找理想的输出。当然, 对于所有输出序列, 在<eos>之后的部分将在实际输出中丢弃。

9.8.1. 贪心搜索

该策略已用于 9.7节的序列预测。对 于输出序列的每一时间步 9. 现代循环神经网络 - 图118, 从9. 现代循环神经网络 - 图119 词元中找到具有最高条件概率的词元,即:
9. 现代循环神经网络 - 图120%0A#card=math&code=y%7Bt%5E%7B%5Cprime%7D%7D%3D%5Cunderset%7By%20%5Cin%20%5Cmathcal%7BY%7D%7D%7B%5Coperatorname%7Bargmax%7D%7D%20P%5Cleft%28y%20%5Cmid%20y%7B1%7D%2C%20%5Cldots%2C%20y%7Bt%5E%7B%5Cprime%7D-1%7D%2C%20%5Cmathbf%7Bc%7D%5Cright%29%0A&id=sM0O7)
s2s-prob1.svg
上图中预测输出序列“A”、“B”、“C”和<eos>。 这个输出序列的条件概率是 9. 现代循环神经网络 - 图122。在先前的讨论中,最有序列应该是最大化 ![](https://g.yuque.com/gr/latex?%E2%88%8F
%7Bt’-1%7D%5E%7BT%5E%7B’%7D%7DP(y%7Bt%E2%80%B2%7D%E2%88%A3y1%2C%E2%80%A6%2Cy%7Bt%E2%80%B2%E2%88%921%7D%2C%5Cmathbf%7Bc%7D)#card=math&code=%E2%88%8F%7Bt%27-1%7D%5E%7BT%5E%7B%27%7D%7DP%28y%7Bt%E2%80%B2%7D%E2%88%A3y1%2C%E2%80%A6%2Cy_%7Bt%E2%80%B2%E2%88%921%7D%2C%5Cmathbf%7Bc%7D%29&id=G5dhE) 值的输出序列,而贪心搜索无法保证得到最优序列,这是因为在每一个时间步进行选择后,都会影响后面词元的可能性,例如:
s2s-prob2.svg
上图在第二步中没有采用贪心策略,因此时间步3的预测概率发生了变化,在选择3后,时间步4处的概率也发生了变化。因此,最终的条件概率为9. 现代循环神经网络 - 图124,大于仅使用贪心搜索的概率。因此贪心搜索可能有较高的效率,但其结果不一定是最优的。

9.8.2. 穷举搜索

穷举地列举所有可能的输出序列及其条件概率, 然后计算输出条件概率最高的一个。穷举搜索一定能找到最优路径,但其计算量可能高的惊人。

9.8.3. 束搜索

如果精度最重要,则显然是穷举搜索。 如果计算成本最重要,则显然是贪心搜索。 而束搜索的实际应用则介于这两个极端之间。束搜索(beam search)是贪心搜索的一个改进版本。 它有一个超参数,名为束宽(beam size)9. 现代循环神经网络 - 图125。 在时间步1,我们选择具有最高条件概率的9. 现代循环神经网络 - 图126个词元。 这9. 现代循环神经网络 - 图127个词元将分别是9. 现代循环神经网络 - 图128个候选输出序列的第一个词元。 在随后的每个时间步,基于上一时间步的9. 现代循环神经网络 - 图129个候选输出序列, 我们将继续从9. 现代循环神经网络 - 图130个可能的选择中 挑出具有最高条件概率的9. 现代循环神经网络 - 图131个候选输出序列。下图展示束宽为2,词典长度为5,输出序列最大长度为3的情况。
beam-search.svg
束搜索的计算量为9. 现代循环神经网络 - 图133#card=math&code=%5Cmathcal%7BO%7D%28k%7C%5Cmathcal%7BY%7D%7CT%5E%7B%27%7D%29&id=D4BWs), 这个结果介于贪心搜索和穷举搜索之间。 实际上,贪心搜索可以看作是一种束宽为1的特殊类型的束搜索。 通过灵活地选择束宽,束搜索可以在正确率和计算代价之间进行权衡。