中企动力科技股份有限公司做网站,字体设计在线生成免费,微信广告投放推广平台多少费用,所有关键词本系列为《动手学深度学习》学习笔记 
书籍链接#xff1a;动手学深度学习 笔记是从第四章开始#xff0c;前面三章为基础知识#xff0c;有需要的可以自己去看看 关于本系列笔记#xff1a; 书里为了让读者更好的理解#xff0c;有大篇幅的描述性的文字#xff0c;内容很…本系列为《动手学深度学习》学习笔记 
书籍链接动手学深度学习 笔记是从第四章开始前面三章为基础知识有需要的可以自己去看看 关于本系列笔记 书里为了让读者更好的理解有大篇幅的描述性的文字内容很多笔记只保留主要内容同时也是对之前知识的查漏补缺 9. 现代循环神经网络 前一章中我们介绍了循环神经网络的基础知识这种网络可以更好地处理序列数据。我们在文本数据上实现了基于循环神经网络的语言模型但是对于当今各种各样的序列学习问题这些技术可能并不够用。仍需要通过设计更复杂的序列模型来进一步处理它。具体来说将引入两个广泛使用的网络即门控循环单元gated recurrent unitsGRU 和 长短期记忆网络long short‐term memoryLSTM。 
9.1 门控循环单元GRU 在 8.7节中讨论了如何在循环神经网络中计算梯度以及矩阵连续乘积可以导致梯度消失或梯度爆炸的问题。下面简单思考一下这种梯度异常在实践中的意义 
早期观测值对预测所有未来观测值具有非常重要的意义。一些词元没有相关的观测值。序列的各个部分之间存在逻辑中断。 在学术界已经提出了许多方法来解决这类问题。其中 
最早的方法是 “长短期记忆”long‐short‐term memoryLSTM (Hochreiter and Schmidhuber, 1997)将在 9.2节中讨论。门控循环单元gated recurrent unitGRU (Cho et al., 2014)是一个稍微简化的变体通常能够提供同等的效果并且计算 (Chung et al., 2014)的速度明显更快。 
9.1.1 门控隐状态 门控循环单元与普通的循环神经网络之间的关键区别在于前者支持隐状态的门控。 这意味着模型有专门的机制来确定应该何时更新隐状态以及应该何时重置隐状态。 这些机制是可学习的并且能够解决了上面列出的问题。 例如如果第一个词元非常重要模型将学会在第一次观测之后不更新隐状态。同样模型也可以学会跳过不相关的临时观测。最后模型还将学会在需要的时候重置隐状态。 重置门reset gate和更新门update gate 
我们把它们设计成(0, 1)区间中的向量这样我们就可以进行凸组合。 
重置门允许我们控制“可能还想记住”的过去状态的数量更新门将允许我们控制新状态中有多少个是旧状态的副本。 从构造这些门控开始  图9.1.1: 在门控循环单元模型中计算重置门和更新门 门控循环单元的数学表达 
对于给定的时间步 t t t假设输入是一个小批量  X t ∈ R n × d X_t ∈ R^{n×d} Xt∈Rn×d 样本个数n输入个数d上一个时间步的隐状态是  H t − 1 ∈ R n × h H_{t−1} ∈ R^{n×h} Ht−1∈Rn×h 隐藏单元个数h。 重置门 R t  σ ( X t W x r  H t − 1 W h r  b r ) ∈ R n × h , ( 9.1.1 ) 重置门R_t  σ(X_tW_{xr}  H_{t−1}W_{hr}  b_r)∈ R^{n×h},(9.1.1) 重置门Rtσ(XtWxrHt−1Whrbr)∈Rn×h,(9.1.1)  更新门 Z t  σ ( X t W x z  H t − 1 W h z  b z ) ∈ R n × h , ( 9.1.1 ) 更新门Z_t  σ(X_tW_{xz}  H_{t−1}W_{hz}  b_z)∈ R^{n×h},(9.1.1) 更新门Ztσ(XtWxzHt−1Whzbz)∈Rn×h,(9.1.1) 其中 W x r , W x z ∈ R d × h W_{xr} , W_{xz} ∈ R^{d×h} Wxr,Wxz∈Rd×h 和 W h r , W h z ∈ R h × h W_{hr}, W_{hz} ∈ R^{h×h} Whr,Whz∈Rh×h是权重参数 b r , b z ∈ R 1 × h b_r, b_z ∈ R_{1×h} br,bz∈R1×h是偏置参数。注意在求和过程中会触发广播机制。使用sigmoid函数如 4.1节中介绍的将输入值转换到区间(0, 1)。 
候选隐状态 将重置门 R t R_t Rt与 (8.4.5) 中的常规隐状态更新机制集成得到在时间步 t t t的候选隐状态candidatehidden state  H ^ t ∈ R n × h \hat{H}_t ∈ R^{n×h} H^t∈Rn×h。  H ^ t  t a n h ( X t W x h  ( R t ⊙ H t − 1 ) W h h  b h ) , ( 9.1.2 ) \hat{H}_t  tanh(X_tW_{xh}  (R_t ⊙ H_{t−1}) W_{hh}  b_h), (9.1.2) H^ttanh(XtWxh(Rt⊙Ht−1)Whhbh),(9.1.2) 其中 W x h ∈ R d × h W_{xh} ∈ R^{d×h} Wxh∈Rd×h 和 W h h ∈ R h × h W_{hh} ∈ R^{h×h} Whh∈Rh×h是权重参数 b h ∈ R 1 × h b_h ∈ R^{1×h} bh∈R1×h是偏置项符号⊙是Hadamard积按元素乘积运算符。 使用 t a n h tanh tanh非线性激活函数来确保候选隐状态中的值保持在区间(−1, 1)中。与 (8.4.5)相比(9.1.2)中的 R t R_t Rt和 H t − 1 H_{t−1} Ht−1 的元素相乘可以减少以往状态的影响。 
重置门 R t R_t Rt中的项接近1时恢复一个如 (8.4.5)中的普通的循环神经网络保留隐状态信息。重置门 R t Rt Rt中所有接近0的项候选隐状态是以 X t X_t Xt作为输入的多层感知机的结果任何预先存在的隐状态都会被重置为默认值 应用重置门之后的计算流程  图9.1.2: 在门控循环单元模型中计算候选隐状态 
隐状态 上述的计算结果只是候选隐状态仍然需要结合更新门 Z t Z_t Zt的效果。这一步确定 新的隐状态  H t ∈ R n × h H_t ∈ R^{n×h} Ht∈Rn×h 在多大程度上来自旧的状态  H t − 1 H_{t-1} Ht−1 和新的候选状态  H ^ t \hat{H}_t H^t。更新门 Z t Z_t Zt仅需要在 H t − 1 H_{t-1} Ht−1和 H ^ t \hat{H}_t H^t 之间进行按元素的凸组合就可以实现这个目标。这就得出了门控循环单元的最终更新公式  H t  Z t ⊙ H t − 1  ( 1 − Z t ) ⊙ H ^ t . ( 9.1.3 ) H_t  Z_t ⊙ H_{t-1}  (1 − Z_t) ⊙ \hat{H}_t. (9.1.3) HtZt⊙Ht−1(1−Zt)⊙H^t.(9.1.3) 
每当更新门 Z t Z_t Zt接近1时模型就倾向只保留旧状态。此时来自Xt的信息基本上被忽略当 Z t Z_t Zt接近0时新的隐状态 H t H_t Ht就会接近候选隐状态 H ^ t \hat{H}_t H^t。 这些设计可以处理循环神经网络中的梯度消失问题并更好地捕获时间步距离很长的序列的依赖关系。  图9.1.3: 计算门控循环单元模型中的隐状态 总之门控循环单元具有以下两个显著特征 
重置门有助于捕获序列中的短期依赖关系更新门有助于捕获序列中的长期依赖关系。 
9.1.2 从零开始实现 从零开始实现门控循环单元模型。读取 8.5节中使用的时间机器数据集 
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)初始化模型参数 下一步是初始化模型参数。我们从标准差为0.01的高斯分布中提取权重并将偏置项设为0超参数num_hiddens定义隐藏单元的数量实例化与更新门、重置门、候选隐状态和输出层相关的所有权重和偏置。 
def get_params(vocab_size, num_hiddens, device):num_inputs  num_outputs  vocab_sizedef normal(shape):return torch.randn(sizeshape, devicedevice)*0.01def three():return (normal((num_inputs, num_hiddens)),normal((num_hiddens, num_hiddens)),torch.zeros(num_hiddens, devicedevice))W_xz, W_hz, b_z  three() # 更新门参数W_xr, W_hr, b_r  three() # 重置门参数W_xh, W_hh, b_h  three() # 候选隐状态参数# 输出层参数W_hq  normal((num_hiddens, num_outputs))b_q  torch.zeros(num_outputs, devicedevice)# 附加梯度params  [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]for param in params:param.requires_grad_(True)return params定义模型 定义隐状态的初始化函数init_gru_state。此函数返回一个形状为批量大小隐藏单元个数的张量张量的值全部为零与 8.5节中定义的init_rnn_state函数一样 
def init_gru_state(batch_size, num_hiddens, device):return (torch.zeros((batch_size, num_hiddens), devicedevice), )定义门控循环单元模型模型的架构与基本的循环神经网络单元是相同的只是权重更新公式更为复杂 
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  paramsH,  stateoutputs  []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_tildaY  H  W_hq  b_qoutputs.append(Y)return torch.cat(outputs, dim0), (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)perplexity 1.1, 19911.5 tokens/sec on cuda:0
time traveller firenis i heidfile sook at i jomer and sugard are
travelleryou can show black is white by argument said filby9.1.3 简洁实现 高级API包含了前文介绍的所有配置细节所以我们可以直接实例化门控循环单元模型。这段代码的运行速度要快得多因为它使用的是编译好的运算符而不是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)perplexity 1.0, 109423.8 tokens/sec on cuda:0
time travelleryou can show black is white by argument said filby
traveller with a slight accession ofcheerfulness really thi小结 
门控循环神经网络可以更好地捕获时间步距离很长的序列上的依赖关系。重置门有助于捕获序列中的短期依赖关系。更新门有助于捕获序列中的长期依赖关系。重置门打开时门控循环单元包含基本循环神经网络更新门打开时门控循环单元可以跳过子序列。 
9.2 长短期记忆网络LSTM 长期以来隐变量模型存在着长期信息保存和短期输入缺失的问题。解决这一问题的最早方法之一是长短期存储器long short‐term memoryLSTM(Hochreiter and Schmidhuber, 1997)。它有许多与门控循环单元9.1节一样的属性。有趣的是长短期记忆网络的设计比门控循环单元稍微复杂一些却比门控循环单元早诞生了近20年。 
9.2.1 门控记忆元 可以说长短期记忆网络的设计灵感来自于计算机的逻辑门。长短期记忆网络引入了记忆元memory cell或简称为单元cell。有些文献认为记忆元是隐状态的一种特殊类型它们与隐状态具有相同的形状其设计目的是用于记录附加的信息。为了控制记忆元需要许多门。 
其中一个门用来从单元中输出条目将其称为输出门output gate。另外一个门用来决定何时将数据读入单元将其称为输入门inputgate。还需要一种机制来重置单元的内容由**遗忘门forget gate**来管理这种设计的动机与门控循环单元相同能够通过专用机制决定什么时候记忆或忽略隐状态中的输入。 输入门、忘记门和输出门就如在门控循环单元中一样由三个具有sigmoid激活函数的全连接层处理以计算输入门、遗忘门和输出门的值。因此这三个门的值都在(0, 1)的范围内。 细化一下长短期记忆网络的数学表达。假设有 h h h个隐藏单元批量大小为 n n n输入数为 d d d。因此 
输入为 X t ∈ R n × d X_t ∈ R^{n×d} Xt∈Rn×d前一时间步的隐状态为 H t − 1 ∈ R n × h H_{t−1} ∈ R^{n×h} Ht−1∈Rn×h。相应地时间步t的门被定义如下输入门是 I t ∈ R n × h I_t ∈ R^{n×h} It∈Rn×h遗忘门是 F t ∈ R n × h F_t ∈ R^{n×h} Ft∈Rn×h输出门是 O t ∈ R n × h O_t ∈R^{n×h} Ot∈Rn×h。 它们的计算方法如下 I t  σ ( X t W x i  H t − 1 W h i  b i ) , I_t  σ(X_tW_{xi}  H_{t−1}W_{hi}  b_i), Itσ(XtWxiHt−1Whibi),  F t  σ ( X t W x f  H t − 1 W h f  b f ) , F_t  σ(X_tW_{xf} H_{t−1}W_{hf}  b_f ), Ftσ(XtWxfHt−1Whfbf),  O t  σ ( X t W x o  H t − 1 W h o  b o ) , O_t  σ(X_tW_{xo}  H_{t−1}W_{ho}  b_o), Otσ(XtWxoHt−1Whobo), 
候选记忆元 由于还没有指定各种门的操作所以先介绍候选记忆元candidate memory cell  C ^ t ∈ R n × h \hat{C}_t ∈ R^{n×h} C^t∈Rn×h。它的计算与上面描述的三个门的计算类似但是使用tanh函数作为激活函数函数的值范围为(−1, 1)。下面导出在时间步 t t t处的方程  C ^ t  t a n h ( X t W x c  H t − 1 W h c  b c ) , \hat{C}_t tanh(X_tW_{xc}  H_{t−1}W_{hc}  b_c), C^ttanh(XtWxcHt−1Whcbc),  图9.2.2: 长短期记忆模型中的候选记忆元 
记忆元 在长短期记忆网络中也有两个门用于这样的目的 
输入门 I t I_t It控制采用多少来自 C ^ t \hat{C}_t C^t的新数据遗忘门 F t F_t Ft控制保留多少过去的记忆元 C t − 1 ∈ R n × h C_{t−1} ∈ R^{n×h} Ct−1∈Rn×h的内容。 使用按元素乘法得出  C t  F t ⊙ C t − 1  I t ⊙ C ^ t . ( 9.2.3 ) C_t  F_t ⊙ C_{t−1}  I_t ⊙ \hat{C}_t. (9.2.3) CtFt⊙Ct−1It⊙C^t.(9.2.3)   如果遗忘门始终为1且输入门始终为0则过去的记忆元 C t − 1 C_{t−1} Ct−1 将随时间被保存并传递到当前时间步。引入这种设计是为了缓解梯度消失问题并更好地捕获序列中的长距离依赖关系。这样我们就得到了计算记忆元的流程图如 图9.2.3 图9.2.3: 在长短期记忆网络模型中计算记忆元 
隐状态 最后定义如何计算隐状态 H t ∈ R n × h H_t ∈ R^{n×h} Ht∈Rn×h这就是输出门发挥作用的地方。在长短期记忆网络中它仅仅是记忆元的tanh的门控版本。这就确保了 H t H_t Ht的值始终在区间(−1, 1)内  H t  O t ⊙ t a n h ( C t ) . ( 9.2.4 ) H_t  O_t ⊙ tanh(C_t). (9.2.4) HtOt⊙tanh(Ct).(9.2.4) 
只要输出门接近1我们就能够有效地将所有记忆信息传递给预测部分输出门接近0只保留记忆元内的所有信息而不需要更新隐状态。  
图9.2.4提供了数据流的图形化演示。 
9.2.2 从零开始实现 从零开始实现长短期记忆网络 (与 8.5节中的实验相同我们首先加载时光机器数据集) 
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)初始化模型参数 定义和初始化模型参数。超参数num_hiddens定义隐藏单元的数量。按照标准差0.01的高斯分布初始化权重并将偏置项设为0。 
def get_lstm_params(vocab_size, num_hiddens, device):num_inputs  num_outputs  vocab_sizedef normal(shape):return torch.randn(sizeshape, devicedevice)*0.01def three():return (normal((num_inputs, num_hiddens)),normal((num_hiddens, num_hiddens)),torch.zeros(num_hiddens, devicedevice))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, devicedevice)# 附加梯度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定义模型 在初始化函数中长短期记忆网络的隐状态需要返回一个额外的记忆元单元的值为0形状为批量大小隐藏单元数。因此得到以下的状态初始化。 
def init_lstm_state(batch_size, num_hiddens, device):return (torch.zeros((batch_size, num_hiddens), devicedevice),torch.zeros((batch_size, num_hiddens), devicedevice))实际模型的定义与前面讨论的一样提供三个门和一个额外的记忆元。请注意只有隐状态才会传递到输出层而记忆元 C t C_t Ct不直接参与输出计算。 
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)  stateoutputs  []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_tildaH  O * torch.tanh(C)Y  (H  W_hq)  b_qoutputs.append(Y)return torch.cat(outputs, dim0), (H, C)训练和预测 通过实例化 8.5节中引入的RNNModelScratch类来训练一个长短期记忆网络就如我们在 9.1节中所做的一样。 
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)perplexity 1.3, 17736.0 tokens/sec on cuda:0
time traveller for so it will leong go it we melenot ir cove i s
traveller care be can so i ngrecpely as along the time dime9.2.3 简洁实现 使用高级API可以直接实例化LSTM模型。高级API封装了前文介绍的所有配置细节。这段代码的运行速度要快得多因为它使用的是编译好的运算符而不是Python来处理之前阐述的许多细节。 
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)perplexity 1.1, 234815.0 tokens/sec on cuda:0
time traveller for so it will be convenient to speak of himwas e
travelleryou can show black is white by argument said filby长短期记忆网络是典型的具有重要状态控制的隐变量自回归模型。多年来已经提出了其许多变体例如多层、残差连接、不同类型的正则化。然而由于序列的长距离依赖性训练长短期记忆网络和其他序列模型例如门控循环单元的成本是相当高的。在后面的内容中我们将讲述更高级的替代模型如Transformer。 
小结 
长短期记忆网络有三种类型的门输入门、遗忘门和输出门。长短期记忆网络的隐藏层输出包括“隐状态”和“记忆元”。只有隐状态会传递到输出层而记忆元完全属于内部信息。长短期记忆网络可以缓解梯度消失和梯度爆炸。 
9.3 深度循环神经网络 到目前为止只讨论了具有一个单向隐藏层的循环神经网络。其中隐变量和观测值与具体的函数形式的交互方式是相当随意的。只要交互类型建模具有足够的灵活性这就不是一个大问题。然而对一个单层来说这可能具有相当的挑战性。 之前在线性模型中通过添加更多的层来解决这个问题。而在循环神经网络中首先需要确定如何添加更多的层以及在哪里添加额外的非线性因此这个问题有点棘手。 事实上可以将多层循环神经网络堆叠在一起通过对几个简单层的组合产生了一个灵活的机制。 图9.3.1描述了一个具有L个隐藏层的深度循环神经网络每个隐状态都连续地传递到当前层的下一个时间步和下一层的当前时间步。  图9.3.1: 深度循环神经网络结构 
9.3.1 函数依赖关系 可以将深度架构中的函数依赖关系形式化这个架构是由 图9.3.1中描述了L个隐藏层构成。 
假设在时间步 t t t有一个小批量的输入数据  X t ∈ R n × d X_t ∈ R^{n×d} Xt∈Rn×d 样本数n每个样本中的输入数d。同时将 l t h l^{th} lth隐藏层l  1, . . . , L的隐状态设为 H t ( l ) ∈ R n × h H^{(l)}_t ∈ R^{n×h} Ht(l)∈Rn×h隐藏单元数h输出层变量设为 O t ∈ R n × q O_t ∈ R^{n×q} Ot∈Rn×q 输出数q。设置 H t ( 0 )  X t H^{(0)}_t  X_t Ht(0)Xt第 l l l个隐藏层的隐状态使用激活函数 ϕ l ϕ_l ϕl 则  H t ( l )  ϕ l ( H t ( l − 1 ) W x h ( l )  H t − 1 ( l ) W h h ( l )  b h ( l ) ) H^{(l)}_t  ϕ_l(H^{(l-1)}_t W^{(l)}_{xh}  H^{(l)}_{t-1}W^{(l)}_{hh}  b^{(l)}_h) Ht(l)ϕl(Ht(l−1)Wxh(l)Ht−1(l)Whh(l)bh(l))   输出层的计算仅基于第l个隐藏层最终的隐状态  O t  H t ( l ) W h q  b q O_t  H^{(l)}_tW_{hq}b_q OtHt(l)Whqbq   与多层感知机一样隐藏层数目L和隐藏单元数目h都是超参数。也就是说它们可以由我们调整的。另外用门控循环单元或长短期记忆网络的隐状态来代替 (9.3.1)中的隐状态进行计算可以很容易地得到深度门控循环神经网络或深度长短期记忆神经网络。 
9.3.2 简洁实现 实现多层循环神经网络所需的许多逻辑细节在高级API中都是现成的。简单起见仅示范使用此类内置函数的实现方式。以长短期记忆网络模型为例该代码与之前在 9.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)像选择超参数这类架构决策也跟 9.2节中的决策非常相似。因为有不同的词元所以输入和输出都选择相同数量即vocab_size。隐藏单元的数量仍然是256。唯一的区别是现在通过num_layers的值来设定隐藏层数。 
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)9.3.3 训练与预测 由于使用了长短期记忆网络模型来实例化两个层因此训练速度被大大降低了。 
num_epochs, lr  500, 2
d2l.train_ch8(model, train_iter, vocab, lr*1.0, num_epochs, device)perplexity 1.0, 186005.7 tokens/sec on cuda:0
time traveller for so it will be convenient to speak of himwas e
travelleryou can show black is white by argument said filby小结 
在深度循环神经网络中隐状态的信息被传递到当前层的下一时间步和下一层的当前时间步。有许多不同风格的深度循环神经网络如长短期记忆网络、门控循环单元、或经典循环神经网络。这些模型在深度学习框架的高级API中都有涵盖。总体而言深度循环神经网络需要大量的调参如学习率和修剪来确保合适的收敛模型的初始化也需要谨慎。 
9.4 双向循环神经网络 在序列学习中我们以往假设的目标是在给定观测的情况下例如在时间序列的上下文中或在语言模型的上下文中对下一个输出进行建模。虽然这是一个典型情景但不是唯一的。还可能发生什么其它的情况呢我们考虑以下三个在文本序列中填空的任务。 • 我___。   • 我___饿了。   • 我___饿了我可以吃半头猪。 根据可获得的信息量可以用不同的词填空如“很高兴”“happy”、“不”“not”和“非常”“very”。很明显每个短语的“下文”传达了重要信息如果有的话而这些信息关乎到选择哪个词来填空所以无法利用这一点的序列模型将在相关任务上表现不佳。例如如果要做好命名实体识别例如识别“Green”指的是“格林先生”还是绿色不同长度的上下文范围重要性是相同的。为了获得一些解决问题的灵感让我们先迂回到概率图模型。 
9.4.1 隐马尔可夫模型中的动态规划 这一小节是用来说明动态规划问题的具体的技术细节对于理解深度学习模型并不重要但它有助于我们思考为什么要使用深度学习以及为什么要选择特定的架构。 如果我们想用概率图模型来解决这个问题可以设计一个隐变量模型在任意时间步 t t t假设存在某个隐变量 h t h_t ht通过概率 P ( x t ∣ h t ) P(x_t | h_t) P(xt∣ht)控制观测到的 x t x_t xt。此外任何 h t → h t  1 h_t → h_{t1} ht→ht1转移都是由一些状态转移概率 P ( h t  1 ∣ h t ) P(h_{t1} | h_t) P(ht1∣ht)给出。这个概率图模型就是一个隐马尔可夫模型hidden Markov modelHMM如图9.4.1所示。    因此对于有 T T T个观测值的序列在观测状态和隐状态上具有以下联合概率分布    现 在 假 设 观 测 到 所 有 的 x i x_i xi 除 了 x j x_j xj 并 且 目 标 是 计 算 P ( x j ∣ x − j ) P(x_j | x_{−j} ) P(xj∣x−j) 其 中 x − j  ( x 1 , . . . , x j − 1 , x j  1 , . . . , x T ) x_{−j} (x_1, . . . , x_{j−1}, x_{j1}, . . . , x_T ) x−j(x1,...,xj−1,xj1,...,xT) 由于 P ( x j ∣ x − j ) P(x_j | x_{−j} ) P(xj∣x−j)中没有隐变量因此考虑对 h 1 , . . . , h T h_1, . . . , h_T h1,...,hT选择构成的所有可能的组合进行求和。如果任何 h i h_i hi可以接受 k k k个不同的值有限的状态数这意味着我们需要对 k T k^T kT个项求和这个任务显然难于登天。幸运的是有个巧妙的解决方案动态规划dynamic programming。 要了解动态规划的工作方式我们考虑对隐变量 h 1 , . . . , h T h_1, . . . , h_T h1,...,hT的依次求和。根据 (9.4.1)将得出    通常我们将前向递归forward recursion写为    递归被初始化为 π 1 ( h 1 )  P ( h 1 ) π_1(h_1)  P(h_1) π1(h1)P(h1)。符号简化也可以写成 π t  1  f ( π t , x t ) π_{t1}  f(π_t, x_t) πt1f(πt,xt)其中 f f f是一些可学习的函数。这看起来就像我们在循环神经网络中讨论的隐变量模型中的更新方程。与前向递归一样我们也可以使用后向递归对同一组隐变量求和。这将得到    因此我们可以将后向递归backward recursion写为 初始化ρT (hT )  1。前  初始化 ρ T ( h T )  1 ρT (hT )  1 ρT(hT)1。前向和后向递归都允许我们对T个隐变量在 O ( k T ) O(kT) O(kT) 线性而不是指数时间内对 ( h 1 , . . . , h T ) (h_1, . . . , h_T) (h1,...,hT)的所有值求和。这是使用图模型进行概率推理的巨大好处之一。它也是通用消息传递算法 (Aji and McEliece, 2000)的一个非常特殊的例子。 结合前向和后向递归能够计算:    因为符号简化的需要后向递归也可以写为 ρ t − 1  g ( ρ t , x t ) ρ_{t−1}  g(ρ_t, x_t) ρt−1g(ρt,xt)其中 g g g是一个可以学习的函数。同样这看起来非常像一个更新方程只是不像我们在循环神经网络中看到的那样前向运算而是后向计算。事实上知道未来数据何时可用对隐马尔可夫模型是有益的。信号处理学家将是否知道未来观测这两种情况区分为内插和外推有关更多详细信息请参阅 (Doucet et al., 2001)。 
9.4.2 双向模型 如果希望在循环神经网络中拥有一种机制使之能够提供与隐马尔可夫模型类似的前瞻能力就需要修改循环神经网络的设计。幸运的是这在概念上很容易只需要增加一个“从最后一个词元开始从后向前运行”的循环神经网络而不是只有一个在前向模式下“从第一个词元开始运行”的循环神经网络。双向循环神经网络bidirectional RNNs添加了反向传递信息的隐藏层以便更灵活地处理此类信息。图9.4.2描述了具有单个隐藏层的双向循环神经网络的架构。  图9.4.2: 双向循环神经网络架构   事实上这与隐马尔可夫模型中的动态规划的前向和后向递归没有太大区别。其主要区别是在隐马尔可夫模型中的方程具有特定的统计意义。双向循环神经网络没有这样容易理解的解释我们只能把它们当作通用的、可学习的函数。这种转变集中体现了现代深度网络的设计原则首先使用经典统计模型的函数依赖类型然后将其参数化为通用形式。 
定义 双向循环神经网络是由 (Schuster and Paliwal, 1997)提出的关于各种架构的详细讨论请参阅 (Graves and Schmidhuber, 2005)。 对于任意时间步t给定一个小批量的输入数据  X t ∈ R n × d X_t ∈ R_{n×d} Xt∈Rn×d 样本数n每个示例中的输入数d并且令隐藏层激活函数为 ϕ ϕ ϕ。在双向架构中设该时间步的前向和反向隐状态分别为 H → t ∈ R n × h \overrightarrow{H}_t ∈ R^{n×h} H   t∈Rn×h和 H ← t ∈ R n × h \overleftarrow{H}_t∈ R^{n×h} H   t∈Rn×h其中h是隐藏单元的数目。前向和反向隐状态的更新如下  H → t  ϕ ( X t W x h ( f )  H → t − 1 W h h ( f )  b h ( f ) ) , \overrightarrow{H}_t  ϕ(X_tW^{(f)}_{xh} \overrightarrow{H}_{t-1} W^{(f)}_{hh}  b^{(f)}_h), H   tϕ(XtWxh(f)H   t−1Whh(f)bh(f)),  H ← t  ϕ ( X t W x h ( f )  H ← t − 1 W h h ( f )  b h ( f ) ) , \overleftarrow{H}_t  ϕ(X_tW^{(f)}_{xh} \overleftarrow{H}_{t-1} W^{(f)}_{hh}  b^{(f)}_h), H   tϕ(XtWxh(f)H   t−1Whh(f)bh(f)),   接下来将前向隐状态 H → t ∈ R n × h \overrightarrow{H}_t ∈ R^{n×h} H   t∈Rn×h和反向隐状态 H ← t ∈ R n × h \overleftarrow{H}_t∈ R^{n×h} H   t∈Rn×h连接起来获得需要送入输出层的隐状态  H t ∈ R n × 2 h {H}_t ∈ R^{n×2h} Ht∈Rn×2h。在具有多个隐藏层的深度双向循环神经网络中该信息作为输入传递到下一个双向层。最后输出层计算得到的输出为  O t ∈ R n × q Ot ∈ R^{n×q} Ot∈Rn×qq是输出单元的数目  O t  H t W h q  b q . O_t  H_tW_{hq} b_q. OtHtWhqbq. 
模型的计算代价及其应用 双向循环神经网络的一个关键特性是使用来自序列两端的信息来估计输出。也就是说我们使用来自过去和未来的观测信息来预测当前的观测。但是在对下一个词元进行预测的情况中这样的模型并不是我们所需的。因为在预测下一个词元时我们终究无法知道下一个词元的下文是什么所以将不会得到很好的精度。具体地说在训练期间我们能够利用过去和未来的数据来估计现在空缺的词而在测试期间我们只有过去的数据因此精度将会很差。 下面的实验将说明这一点。 另一个严重问题是双向循环神经网络的计算速度非常慢。其主要原因是网络的前向传播需要在双向层中进行前向和后向递归并且网络的反向传播还依赖于前向传播的结果。因此梯度求解将有一个非常长的链。 
9.4.3 双向循环神经网络的错误应用 由于双向循环神经网络使用了过去的和未来的数据所以我们不能盲目地将这一语言模型应用于任何预测任务。尽管模型产出的困惑度(perplexity)是合理的该模型预测未来词元的能力却可能存在严重缺陷。 
import torch
from torch import nn
from d2l import torch as d2l
# 加载数据
batch_size, num_steps, device  32, 35, d2l.try_gpu()
train_iter, vocab  d2l.load_data_time_machine(batch_size, num_steps)
# 通过设置“bidirectiveTrue”来定义双向LSTM模型
vocab_size, num_hiddens, num_layers  len(vocab), 256, 2
num_inputs  vocab_size
lstm_layer  nn.LSTM(num_inputs, num_hiddens, num_layers, bidirectionalTrue)
model  d2l.RNNModel(lstm_layer, len(vocab))
model  model.to(device)
# 训练模型
num_epochs, lr  500, 1
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)perplexity 1.1, 131129.2 tokens/sec on cuda:0
time travellerererererererererererererererererererererererererer
travellerererererererererererererererererererererererererer小结 
在双向循环神经网络中每个时间步的隐状态由当前时间步的前后数据同时决定。双向循环神经网络与概率图模型中的**“前向‐后向”算法**具有相似性。双向循环神经网络主要用于序列编码和给定双向上下文的观测估计。由于梯度链更长因此双向循环神经网络的训练代价非常高。 
9.5 机器翻译与数据集 语言模型是自然语言处理的关键而机器翻译是语言模型最成功的基准测试。因为机器翻译正是将输入序列转换成输出序列的序列转换模型sequence transduction的核心问题。 机器翻译machine translation 指的是将序列从一种语言自动翻译成另一种语言。在使用神经网络进行端到端学习的兴起之前统计学方法在这一领域一直占据主导地位 (Brown et al., 1990, Brown et al., 1988)。因为统计机器翻译statistical machine translation涉及了翻译模型和语言模型等组成部分的统计分析因此基于神经网络的方法通常被称为 神经机器翻译neural machine translation用于将两种翻译模型区分开来。 下面我们看一下如何将预处理后的数据加载到小批量中用于训练。 
import os
import torch
from d2l import torch as d2l9.5.1 下载和预处理数据集 首先下载一个由Tatoeba项目的双语句子对113 组成的“英法”数据集数据集中的每一行都是制表符分隔的文本序列对序列对由英文文本序列和翻译后的法语文本序列组成。 请注意每个文本序列可以是一个句子也可以是包含多个句子的一个段落。在这个将英语翻译成法语的机器翻译问题中英语是源语言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,encodingutf-8) as f:return f.read()raw_text  read_data_nmt()
print(raw_text[:75])
Downloading ../data/fra-eng.zip from http://d2l-data.s3-accelerate.amazonaws.com/fra-eng.zip...
Go. Va !
Hi. Salut !
Run! Cours !
Run! Courez !
Who? Qui ?
Wow! Ça alors !下载数据集后原始文本数据需要经过几个预处理步骤。例如我们用空格代替不间断空格non‐breaking space使用小写字母替换大写字母并在单词和标点符号之间插入空格。 
#save
def preprocess_nmt(text):预处理“英语法语”数据集def no_space(char, prev_char):return char in set(,.!?) and prev_char !  # 使用空格替换不间断空格# 使用小写字母替换大写字母text  text.replace(\u202f,  ).replace(\xa0,  ).lower()# 在单词和标点符号之间插入空格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 !9.5.2 词元化 与 8.3节中的字符级词元化不同在机器翻译中更喜欢单词级词元化最先进的模型可能使用更高级的词元化技术。 下面的tokenize_nmt函数对前num_examples个文本序列对进行词元其中每个词元要么是一个词要么是一个标点符号。 此函数返回两个词元列表source和target 
source[i] 是源语言这里是英语第i个文本序列的词元列表target[i] 是目标语言这里是法语第i个文本序列的词元列表。 
#save
def tokenize_nmt(text, num_examplesNone):词元化“英语法语”数据数据集source, target  [], []for i, line in enumerate(text.split(\n)):if num_examples and i  num_examples:breakparts  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[:6], target[:6]([[go, .],
[hi, .],
[run, !],
[run, !],
[who, ?],
[wow, !]],
[[va, !],
[salut, !],
[cours, !],
[courez, !],
[qui, ?],
[ça, alors, !]])绘制每个文本序列所包含的词元数量的直方图。在这个简单的“英法”数据集中大多数文本序列的词元数量少于20个。 
#save
def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):绘制列表长度对的直方图d2l.set_figsize()_, _, patches  d2l.plt.hist([[len(l) for l in xlist], [len(l) for l in ylist]])d2l.plt.xlabel(xlabel)d2l.plt.ylabel(ylabel)for patch in patches[1].patches:patch.set_hatch(/)d2l.plt.legend(legend)show_list_len_pair_hist([source, target], # tokens per sequence,count, source, target);9.5.3 词表 由于机器翻译数据集由语言对组成因此可以分别为源语言和目标语言构建两个词表。使用单词级词元化时词表大小将明显大于使用字符级词元化时的词表大小。 为了缓解这一问题将出现次数少于2次的低频率词元视为 相同的未知 “  u n k  ” “unk” “unk”词元 。除此之外还指定了额外的特定词元例如在小批量时用于将序列填充到相同长度的填充词元 “  p a d  ” “pad” “pad”以及序列的开始词元 “  b o s  ” “bos” “bos”和结束词元 “  e o s  ” “eos” “eos”。 这些特殊词元在自然语言处理任务中比较常用。 
src_vocab  d2l.Vocab(source, min_freq2,reserved_tokens[pad, bos, eos])
len(src_vocab)
100129.5.4 加载数据集 回想一下语言模型中的序列样本都有一个固定的长度无论这个样本是一个句子的一部分还是跨越了多个句子的一个片断。这个固定长度是由 8.3节中的 num_steps时间步数或词元数量参数指定的。 在机器翻译中每个样本都是由源和目标组成的文本序列对其中的每个文本序列可能具有不同的长度。为了提高计算效率我们仍然可以通过截断truncation 和 填充padding方式实现一次只处理一个小批量的文本序列。 假设同一个小批量中的每个序列都应该具有相同的长度num_steps 如果文本序列的词元数目少于num_steps时我们将继续在其末尾添加特定的 “  p a d  ” “pad” “pad”词元直到其长度达到num_steps反之将截断文本序列时只取其前num_steps 个词元并且丢弃剩余的词元。 这样每个文本序列将具有相同的长度以便以相同形状的小批量进行加载。 如前所述下面的truncate_pad函数将截断或填充文本序列。 
#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]现在定义一个函数可以将文本序列转换成小批量数据集用于训练。 
将特定的 “  e o s  ” “eos” “eos”词元添加到所有序列的末尾用于表示序列的结束。当模型通过一个词元接一个词元地生成序列进行预测时生成的 “  e o s  ” “eos” “eos”词元说明完成了序列输出工作。此外还记录了每个文本序列的长度统计长度时排除了填充词元在稍后将要介绍的一些模型会需要这个长度信息。 
#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_len9.5.5 训练模型 最后定义load_data_nmt函数来返回数据迭代器以及源语言和目标语言的两种词表。 
#save
def load_data_nmt(batch_size, num_steps, num_examples600):返回翻译数据集的迭代器和词表text  preprocess_nmt(read_data_nmt())source, target  tokenize_nmt(text, num_examples)src_vocab  d2l.Vocab(source, min_freq2,reserved_tokens[pad, bos, eos])tgt_vocab  d2l.Vocab(target, min_freq2,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_size2, num_steps8)
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)breakX: tensor([[ 7, 43, 4, 3, 1, 1, 1, 1],[44, 23, 4, 3, 1, 1, 1, 1]], dtypetorch.int32)
X的有效长度: tensor([4, 4])
Y: tensor([[ 6, 7, 40, 4, 3, 1, 1, 1],[ 0, 5, 3, 1, 1, 1, 1, 1]], dtypetorch.int32)
Y的有效长度: tensor([5, 3])小结 
机器翻译指的是将文本序列从一种语言自动翻译成另一种语言。使用单词级词元化时的词表大小将明显大于使用字符级词元化时的词表大小。为了缓解这一问题我们可以将低频词元视为相同的未知词元。通过截断和填充文本序列可以保证所有的文本序列都具有相同的长度以便以小批量的方式加载。 
9.6 编码器-解码器架构 正如我们在 9.5节中所讨论的机器翻译是序列转换模型的一个核心问题其输入和输出都是长度可变的序列。 为了处理这种类型的输入和输出可以设计一个包含两个主要组件的架构 
第一个组件是一个编码器encoder它接受一个长度可变的序列作为输入并将其转换为具有固定形状的编码状态。第二个组件是解码器decoder它将固定形状的编码状态映射到长度可变的序列。 这被称为编码器-解码器encoder‐decoder架构如 图9.6.1 所示。  图9.6.1: 编码器‐解码器架构 以英语到法语的机器翻译为例给定一个英文的输入序列“They”“are”“watching”“.”。 
首先这种“编码器解码器”架构将长度可变的输入序列编码成一个“状态”然后对该状态进行解码一个词元接着一个词元地生成翻译后的序列作为输出“Ils”“regordent”“.”。 
9.6.1 编码器 在编码器接口中只指定长度可变的序列作为编码器的输入X。任何继承这个Encoder基类的模型将完成代码实现。 
from torch import nn
#save
class Encoder(nn.Module):编码器-解码器架构的基本编码器接口def __init__(self, **kwargs):super(Encoder, self).__init__(**kwargs)def forward(self, X, *args):raise NotImplementedError9.6.2 解码器 在下面的解码器接口中新增一个init_state函数用于将编码器的输出enc_outputs转换为编码后的状态。 注意此步骤可能需要额外的输入例如输入序列的有效长度这在 9.5.4节中进行了解释。为了逐个地生成长度可变的词元序列解码器在每个时间步都会将输入例如在前一时间步生成的词元和编码后的状态映射成当前时间步的输出词元。 
#save
class Decoder(nn.Module):编码器-解码器架构的基本解码器接口def __init__(self, **kwargs):super(Decoder, self).__init__(**kwargs)def init_state(self, enc_outputs, *args):raise NotImplementedErrordef forward(self, X, state):raise NotImplementedError9.6.3 合并编码器和解码器 总而言之“编码器‐解码器”架构包含了一个编码器和一个解码器并且还拥有可选的额外的参数。在前向传播中编码器的输出用于生成编码状态这个状态又被解码器作为其输入的一部分。 
#save
class EncoderDecoder(nn.Module):编码器-解码器架构的基类def __init__(self, encoder, decoder, **kwargs):super(EncoderDecoder, self).__init__(**kwargs)self.encoder  encoderself.decoder  decoderdef 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 正如在 9.5节中看到的机器翻译中的输入序列和输出序列都是长度可变的。为了解决这类问题在 9.6节中设计了一个通用的”编码器解码器“架构。 遵循编码器解码器架构的设计原则循环神经网络编码器使用长度可变的序列作为输入将其转换为固定形状的隐状态。 为了连续生成输出序列的词元独立的循环神经网络解码器是基于输入序列的编码信息和输出序列已经看见的或者生成的词元来预测下一个词元。图9.7.1演示了如何在机器翻译中使用两个循环神经网络进行序列到序列学习。 图9.7.1: 使用循环神经网络编码器和循环神经网络解码器的序列到序列学习 特定的 “  e o s  ” “eos” “eos”表示序列结束词元一旦输出序列生成此词元模型就会停止预测。 在循环神经网络解码器的初始化时间步有两个特定的设计决定 
首先特定的 “  b o s  ” “bos” “bos”表示序列开始词元它是解码器的输入序列的第一个词元。其次使用循环神经网络编码器最终的隐状态 来初始化解码器的隐状态。 例如在 (Sutskever et al., 2014)的设计中正是基于这种设计将输入序列的编码信息送入到解码器中来生成输出序列的。在其他一些设计中 (Cho et al., 2014)如 图9.7.1所示编码器最终的隐状态在每一个时间步都作为解码器的输入序列的一部分。 类似于 8.3节中语言模型的训练可以允许标签成为原始的输出序列从源序列词元 “  b o s  ” “bos” “bos” “Ils”“regardent”“.”到新序列元“Ils”“regardent”“.” “  e o s  ” “eos” “eos”来移动预测的位置。 
import collections
import math
import torch
from torch import nn
from d2l import torch as d2l9.7.1 编码器 从技术上讲编码器将长度可变的输入序列转换成形状固定的上下文变量c并且将输入序列的信息在该上下文变量中进行编码。 如 图9.7.1所示可以使用循环神经网络来设计编码器。考虑由一个序列组成的样本批量大小是1。 
假设输入序列是 x 1 , . . . , x T x_1, . . . , x_T x1,...,xT其中 x t x_t xt是输入文本序列中的第 t t t个词元。在时间步 t t t循环神经网络将词元 x t x_t xt的输入特征向量  x t x_t xt和 h t − 1 h_{t−1} ht−1即上一时间步的隐状态转换为 h t h_t ht即当前步的隐状态。使用一个函数 f f f来描述循环神经网络的循环层所做的变换 h t  f ( x t , h t − 1 ) . h_t  f(x_t, ht−1). htf(xt,ht−1). 总之编码器通过选定的函数 q q q将所有时间步的隐状态转换为上下文变量  c  q ( h 1 , . . . , h T ) . c  q(h_1, . . . , h_T ). cq(h1,...,hT). 比如当选择 q ( h 1 , . . . , h T )  h T q(h_1, . . . , h_T )  h_T q(h1,...,hT)hT时就像 图9.7.1中一样上下文变量仅仅是输入序列在最后时间步的隐状态 h T h_T hT。 
到目前为止使用的是一个单向循环神经网络来设计编码器其中隐状态只依赖于输入子序列这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置包括隐状态所在的时间步组成。也可以使用双向循环神经网络构造编码器其中隐状态依赖于两个输入子序列两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列包括隐状态所在的时间步因此隐状态对整个序列的信息都进行了编码。 现在实现循环神经网络编码器。注意使用了嵌入层embedding layer 来获得输入序列中每个词元的特征向量。 
嵌入层的权重是一个矩阵其行数等于输入词表的大小vocab_size其列数等于特征向量的维度embed_size。 对于任意输入词元的索引i嵌入层获取权重矩阵的第i行从0开始以返回其特征向量。另外本文选择了一个多层门控循环单元来实现编码器。 
#save
class Seq2SeqEncoder(d2l.Encoder):用于序列到序列学习的循环神经网络编码器def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout0, **kwargs):super(Seq2SeqEncoder, self).__init__(**kwargs)# 嵌入层self.embedding  nn.Embedding(vocab_size, embed_size) # 输入词表的大小vocab_size特征向量的维度embed_sizeself.rnn  nn.GRU(embed_size, num_hiddens, num_layers,dropoutdropout)def forward(self, X, *args):# 输出X的形状(batch_size,num_steps,embed_size)X  self.embedding(X)# 在循环神经网络模型中第一个轴对应于时间步X  X.permute(1, 0, 2)# 如果未提及状态则默认为0output, state  self.rnn(X)# output的形状:(num_steps,batch_size,num_hiddens) 时间步数批量大小隐藏单元数# state的形状:(num_layers,batch_size,num_hiddens) num_layers批量大小隐藏单元数return output, state循环层返回变量的说明可以参考 8.6节。 实例化上述编码器的实现 
使用一个两层门控循环单元编码器其隐藏单元数为16。给定一小批量的输入序列X批量大小为4时间步为7。在完成所有时间步后最后一层的隐状态的输出是一个张量output由编码器的循环层返回其形状为时间步数批量大小隐藏单元数。 
encoder  Seq2SeqEncoder(vocab_size10, embed_size8, num_hiddens16,num_layers2)
encoder.eval()
X  torch.zeros((4, 7), dtypetorch.long)
output, state  encoder(X)
output.shapetorch.Size([7, 4, 16]) # 时间步为7 批量大小为4隐藏单元数为16由于这里使用的是门控循环单元所以在最后一个时间步的多层隐状态的形状是隐藏层的数量批量大小隐藏单元的数量。如果使用长短期记忆网络state中还将包含记忆单元信息。 
state.shape
torch.Size([2, 4, 16])9.7.2 解码器 正如上文提到的编码器输出的上下文变量 c c c对整个输入序列 x 1 , . . . , x T x_1, . . . , x_T x1,...,xT进行编码。来自训练数据集的输出序列 y 1 , y 2 , . . . , y T ′ y_1, y_2, . . . , y_{T^′} y1,y2,...,yT′对于每个时间步 t ′ t′ t′与输入序列或编码器的时间步t不同解码器输出 y t ′ y_{t^′} yt′的概率 取决于先前的输出子序列 y 1 , . . . , y t ′ − 1 y1, . . . , yt′−1 y1,...,yt′−1和上下文变量c即 P ( y t ′ ∣ y 1 , . . . , y t ′ − 1 , c ) P(yt′ | y1, . . . , yt′−1, c) P(yt′∣y1,...,yt′−1,c)。 为了在序列上模型化这种条件概率可以使用另一个循环神经网络作为解码器。在输出序列上的任意时间步 t ′ t^′ t′循环神经网络将来自上一时间步的输出 y t ′ − 1 y_{t^′−1} yt′−1 和上下文变量 c c c作为其输入然后在当前时间步将它们和上一隐状态  s t ′ − 1 s_{t^′−1} st′−1转换为隐状态 s t ′ s_{t^′} st′。因此可以使用函数 g g g来表示解码器的隐藏层的变换  s t ′  g ( y t ′ − 1 , c , s t ′ − 1 ) . s_{t^′}  g(y_{t^′−1}, c, s_{t^′−1}). st′g(yt′−1,c,st′−1).   在获得解码器的隐状态之后可以使用输出层和softmax操作来计算在时间步 t ′ t^′ t′时输出 y t ′ y_{t^′} yt′的条件概率分布  P ( y t ′ ∣ y 1 , . . . , y t ′ − 1 , c ) P(y_{t^′} | y_1, . . . , y_{t^′−1}, c) P(yt′∣y1,...,yt′−1,c)。根据 图9.7.1当实现解码器时直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。这就要求使用循环神经网络实现的编码器和解码器具有相同数量的层和隐藏单元。为了进一步包含经过编码的输入序列的信息上下文变量在所有的时间步与解码器的输入进行拼接concatenate。为了预测输出词元的概率分布在循环神经网络解码器的最后一层使用全连接层来变换隐状态。 
class Seq2SeqDecoder(d2l.Decoder):用于序列到序列学习的循环神经网络解码器def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout0, **kwargs):super(Seq2SeqDecoder, self).__init__(**kwargs)self.embedding  nn.Embedding(vocab_size, embed_size)self.rnn  nn.GRU(embed_size  num_hiddens, num_hiddens, num_layers,dropoutdropout)self.dense  nn.Linear(num_hiddens, vocab_size)def init_state(self, enc_outputs, *args):return enc_outputs[1]def forward(self, X, state):# 输出X的形状(batch_size,num_steps,embed_size)X  self.embedding(X).permute(1, 0, 2)# 广播context使其具有与X相同的num_stepscontext  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的形状:(num_layers,batch_size,num_hiddens)return output, state下面用与前面提到的编码器中相同的超参数来实例化解码器。解码器的输出形状变为批量大小时间步数词表大小其中张量的最后一个维度存储预测的词元分布。 
decoder  Seq2SeqDecoder(vocab_size10, embed_size8, num_hiddens16,
num_layers2)
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]))图9.7.2: 循环神经网络编码器‐解码器模型中的层 
9.7.3 损失函数 在每个时间步解码器预测了输出词元的概率分布。类似于语言模型可以使用softmax来获得分布并通过计算交叉熵损失函数来进行优化。 9.5节中特定的填充词元被添加到序列的末尾因此不同长度的序列可以以相同形状的小批量加载。但是应该将填充词元的预测排除在损失函数的计算之外。为此可以使用下面的sequence_mask函数通过零值化屏蔽不相关的项以便后面任何不相关预测的计算都是与零的乘积结果都等于零。例如如果两个序列的有效长度不包括填充词元分别为1和2则第一个序列的第一项和第二个序列的前两项之后的剩余项将被清除为零。 
#save
def sequence_mask(X, valid_len, value0):在序列中屏蔽不相关的项maxlen  X.size(1)mask  torch.arange((maxlen), dtypetorch.float32,deviceX.device)[None, :]  valid_len[:, None]X[~mask]  valuereturn X
X  torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))tensor([[1, 0, 0],[4, 5, 0]])还可以使用此函数屏蔽最后几个轴上的所有项。 
X  torch.ones(2, 3, 4)
sequence_mask(X, torch.tensor([1, 2]), value-1)tensor([[[ 1., 1., 1., 1.],
[-1., -1., -1., -1.],
[-1., -1., -1., -1.]],
[[ 1., 1., 1., 1.],
[ 1., 1., 1., 1.],
[-1., -1., -1., -1.]]])通过扩展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.reductionnoneunweighted_loss  super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)weighted_loss  (unweighted_loss * weights).mean(dim1)return weighted_loss创建三个相同的序列来进行代码健全性检查然后分别指定这些序列的有效长度为4、2和0。结果就是第一个序列的损失应为第二个序列的两倍而第三个序列的损失应为零。 
loss  MaskedSoftmaxCELoss()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtypetorch.long),torch.tensor([4, 2, 0]))tensor([2.3026, 1.1513, 0.0000])9.7.4 训练 在下面的循环训练过程中如 图9.7.1所示特定的序列开始词元 “  b o s  ” “bos” “bos”和原始的输出序列不包括序列结束词元 ( “  e o s  ” (“eos” (“eos”拼接在一起作为解码器的输入。这被称为强制教学teacher forcing因为原始的输出序列词元的标签被送入解码器。或者将来自上一个时间步的预测得到的词元作为解码器的当前输入。 
#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(), lrlr)loss  MaskedSoftmaxCELoss()net.train()animator  d2l.Animator(xlabelepoch, ylabelloss,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],devicedevice).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(floss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} ftokens/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, 12745.1 tokens/sec on cuda:09.7.5 预测 为了采用一个接着一个词元的方式预测输出序列每个解码器当前时间步的输入都将来自于前一时间步的预测词元。与训练类似序列开始词元 “  b o s  ” “bos” “bos”在初始时间步被输入到解码器中。该预测过程如 图9.7.3所示当输出序列的预测遇到序列结束词元 “  e o s  ” “eos” “eos”时预测就结束了。    图9.7.3: 使用循环神经网络编码器‐解码器逐词元地预测输出序列。   将在 9.8节中介绍不同的序列生成策略。 
#save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,device, save_attention_weightsFalse):序列到序列模型的预测# 在预测时将net设置为评估模式net.eval()src_tokens  src_vocab[src_sentence.lower().split( )]  [src_vocab[eos]]enc_valid_len  torch.tensor([len(src_tokens)], devicedevice)src_tokens  d2l.truncate_pad(src_tokens, num_steps, src_vocab[pad])# 添加批量轴enc_X  torch.unsqueeze(torch.tensor(src_tokens, dtypetorch.long, devicedevice), dim0)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]], dtypetorch.long, devicedevice), dim0)output_seq, attention_weight_seq  [], []for _ in range(num_steps):Y, dec_state  net.decoder(dec_X, dec_state)# 我们使用具有预测最高可能性的词元作为解码器在下一时间步的输入dec_X  Y.argmax(dim2)pred  dec_X.squeeze(dim0).type(torch.int32).item()# 保存注意力权重稍后讨论if save_attention_weights:attention_weight_seq.append(net.decoder.attention_weights)# 一旦序列结束词元被预测输出序列的生成就完成了if pred  tgt_vocab[eos]:breakoutput_seq.append(pred)return  .join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq小结 
根据“编码器‐解码器”架构的设计我们可以使用两个循环神经网络来设计一个序列到序列学习的模型。在实现编码器和解码器时我们可以使用多层循环神经网络。可以使用遮蔽来过滤不相关的计算例如在计算损失时。在“编码器解码器”训练中强制教学方法将原始输出序列而非预测结果输入解码器。BLEU是一种常用的评估方法它通过测量预测序列和标签序列之间的n元语法的匹配度来评估预测。 
9.8 束搜索 在 9.7节中逐个预测输出序列直到预测序列中出现特定的序列结束词元 “  e o s  ” “eos” “eos”。本节将首先介绍**贪心搜索greedy search**策略并探讨其存在的问题然后对比其他替代策略穷举搜索exhaustive search和束搜索beam search。 在正式介绍贪心搜索之前使用与 9.7节中相同的数学符号定义搜索问题。 在任意时间步 t ′ t^′ t′解码器输出 y t ′ y_{t^′} yt′的概率取决于时间步 t ′ t^′ t′之前的输出子序列 y 1 , . . . , y t ′ − 1 y_1, . . . , y_{t^′−1} y1,...,yt′−1 和对输入序列的信息进行编码得到的上下文变量 c c c。为了量化计算代价用 Y Y Y表示输出词表其中包含 “  e o s  ” “eos” “eos”所以这个词汇集合的基数 ∣ Y ∣ |Y| ∣Y∣就是词表的大小。还将输出序列的最大词元数指定为 T ′ T′ T′。因此目标是从所有 O ( ∣ Y ∣ T ′ ) O(|Y|T′) O(∣Y∣T′)个可能的输出序列中寻找理想的输出。当然对于所有输出序列在 “  e o s  ” “eos” “eos”之后的部分非本句将在实际输出中丢弃。 
9.8.1 贪心搜索 首先看一个简单的策略贪心搜索该策略已用于 9.7节的序列预测。对于输出序列的每一时间步t′都将基于贪心搜索从Y中找到具有最高条件概率的词元即    一旦输出序列包含了 “  e o s  ” “eos” “eos”或者达到其最大长度T′则输出完成。    图9.8.1: 在每个时间步贪心搜索选择具有最高条件概率的词元 如 图9.8.1中假设输出中有四个词元“A”“B”“C”和 “  e o s  ” “eos” “eos”。每个时间步下的四个数字分别表示在该时间步生成“A”“B”“C”和 “  e o s  ” “eos” “eos”的条件概率。在每个时间步贪心搜索选择具有最高条件概率的词元。因此将在 图9.8.1中预测输出序列“A”“B”“C”和 “  e o s  ” “eos” “eos”。这个输出序列的条件概率是 0.5×0.4×0.4×0.6  0.048。 **那么贪心搜索存在的问题是什么呢**现实中最优序列optimal sequence应该是最大化 ∏ t ′  1 T ′ P ( y t ′ ∣ y 1 , . . . , y t ′ − 1 , c ) \prod \limits_{t^′1}^{T^′}P(yt^′ | y1, . . . , y_{t′−1}, c) t′1∏T′P(yt′∣y1,...,yt′−1,c) 值的输出序列这是基于输入序列生成输出序列的条件概率。然而贪心搜索无法保证得到最优序列。    图9.8.2: 在时间步2选择具有第二高条件概率的词元“C”而非最高条件概率的词元 与 图9.8.1不同在时间步2中选择 图9.8.2中的词元“C”它具有第二高的条件概率。 
由于时间步3所基于的时间步1和2处的输出子序列已从 图9.8.1中的“A”和“B”改变为图9.8.2中的“A”和“C”因此时间步3处的每个词元的条件概率也在 图9.8.2中改变。假设在时间步3选择词元“B”于是当前的时间步4基于前三个时间步的输出子序列“A”“C”和“B”为条件这与 图9.8.1中的“A”“B”和“C”不同。因此在 图9.8.2中的时间步4生成每个词元的条件概率也不同于 图9.8.1中的条件概率。结果图9.8.2中的输出序列“A”“C”“B”和 “  e o s  ” “eos” “eos”的条件概率为 0.5 × 0.3 × 0.6 × 0.6  0.054这大于 图9.8.1中的贪心搜索的条件概率。这个例子说明贪心搜索获得的输出序列“A”“B”“C”和 “  e o s  ” “eos” “eos”不一定是最佳序列。 
9.8.2 穷举搜索 如果目标是获得最优序列可以考虑使用穷举搜索exhaustive search穷举地列举所有可能的输出序列及其条件概率然后计算输出条件概率最高的一个。虽然可以使用穷举搜索来获得最优序列但其计算量 O ( ∣ Y ∣ T ′ ) O(|Y|T^′) O(∣Y∣T′)可能高的惊人。 
例如当 ∣ Y ∣  10000 |Y|  10000 ∣Y∣10000和 T ′  10 T^′ 10 T′10时需要评估 1000010  1040 1000010  1040 10000101040序列这是一个极大的数现有的计算机几乎不可能计算它。然而贪心搜索的计算量 O(|Y| T^′) 通它要显著地小于穷举搜索。例如当 ∣ Y ∣  10000 |Y|  10000 ∣Y∣10000和 T ′  10 T^′  10 T′10时我们只需要评估 10000 × 10  105 10000 × 10  105 10000×10105个序列。 
9.8.3 束搜索 那么该选取哪种序列搜索策略呢如果精度最重要则显然是穷举搜索。 如果计算成本最重要则显然是贪心搜索。而束搜索的实际应用则介于这两个极端之间。 束搜索beam search 是贪心搜索的一个改进版本。它有一个超参数名为束宽beam sizek。在时间步1我们选择具有最高条件概率的 k k k个词元。这k个词元将分别是k个候选输出序列的第一个词元。在随后的每个时间步基于上一时间步的k个候选输出序列我们将继续从 k ∣ Y ∣ k |Y| k∣Y∣个可能的选择中挑出具有最高条件概率的k个候选输出序列。    图9.8.3: 束搜索过程束宽2输出序列的最大长度3。候选输出序列是A、C、AB、CE、ABD和CED 图9.8.3演示了束搜索的过程。假设输出的词表只包含五个元素Y  {A, B, C, D, E}其中有一个是 “  e o s  ” “eos” “eos”。 设置束宽为2输出序列的最大长度为3。 
在时间步1假设具有最高条件概率 P(y1 | c)的词元是A和C。在时间步2我们计算所有y2 ∈ Y为 从这十个值中选择最大的两个比如P(A, B | c)和P(C, E | c)。然后在时间步3我们计算所有y3 ∈ Y为    从这十个值中选择最大的两个即 P ( A , B , D ∣ c ) P(A, B, D | c) P(A,B,D∣c)和 P ( C , E , D ∣ c ) P(C, E, D | c) P(C,E,D∣c)我们会得到六个候选输出序列  1  A  2  C  3  A , B  4  C , E  5  A , B , D  6  C , E , D 1A2 C3A, B4C, E5A, B, D6C, E, D 1A2C3A,B4C,E5A,B,D6C,E,D。 最后基于这六个序列例如丢弃包括 “  e o s  ” “eos” “eos”和之后的部分我们获得最终候选输出序列集合。然后 我们选择其中条件概率乘积最高的序列作为输出序列    其中L是最终候选序列的长度α通常设置为0.75。因为一个较长的序列在 (9.8.4) 的求和中会有更多的对数项因此分母中的 L α L^α Lα用于惩罚长序列。束搜索的计算量为 O ( k ∣ Y ∣ T ′ ) O(k |Y| T^′) O(k∣Y∣T′)这个结果介于贪心搜索和穷举搜索之间。实际上贪心搜索可以看作一种束宽为1的特殊类型的束搜索。通过灵活地选择束宽束搜索可以在正确率和计算代价之间进行权衡。 
小结 
序列搜索策略包括贪心搜索、穷举搜索和束搜索。贪心搜索所选取序列的计算量最小但精度相对较低。穷举搜索所选取序列的精度最高但计算量最大。束搜索通过灵活选择束宽在正确率和计算代价之间进行权衡。