赞
踩
上一章节(循环神经网络)介绍了循环神经网络的基础知识,这种网络可以更好的处理序列数据。我们在文本数据上实现了基于循环神经网络的语言模型,但是对于当今各种各样的序列学习问题,这些技术可能不够用。
例如,循环神经网络在实践中一个常见的问题是数值不稳定。尽管我们已经应用了梯度裁剪等技巧来缓解这个问题,但是仍需要通过设计更复杂的序列模型来进一步处理它。具体来说,可以引用两个广泛使用的网络,即门控循环单元(gated recurrent units, GRU)和长短时记忆网络(long short-term memory, LSTM)。然后,基于一个单向隐藏层来扩展循环神经网络架构。将描述具有多个隐藏层的深层架构,并讨论基于前向和后向循环计算的双向设计。现代循环网络经常采用这种扩展。
事实上,语言建模只揭示了序列学习能力的冰山一角。在各种序列学习问题中,如自动语音识别、文本到语音转换和机器翻译,输入和输出都是任意长度的序列。为了阐述如何拟合这种类型的数据,将以机器翻译为例介绍基于循环神经网络的“编码器-解码器”架构和束搜索,并用他们来生成序列。
梯度异常在实践中的意义:
门控循环单元与普通的循环神经网络之间的关键区别在于:前者支持隐状态的门控。门控循环单元有专门的机制来确定应该何时更新隐状态,以及应该何时更新隐状态。这些机制是可以学习的,并且能够解决了上面列出的问题。例如,如果第一个词元非常重要,模型将学会在第一次观测之后不更新隐状态。同样,模型也可以学会跳过不相关的临时观测。最后,模型还将学会在需要的时候重置隐状态。
将重置门和更新门设计成(0,1)区间中的向量,这样就可以进行凸组合。重置门允许控制“可能还想记住”的过去状态的数量;更新门将允许控制新状态中有多少个是旧状态的副本。
门控循环单元的数学表达。对于给定的时间步 t t t,假设输入是一个小批量 X t ∈ R n × d \mathbf{X}_t \in \mathbb{R}^{n \times d} Xt∈Rn×d(样本个数 n n n,输入个数 d d d),上一个时间步的隐状态是 H t − 1 ∈ R n × h \mathbf{H}_{t-1} \in \mathbb{R}^{n \times h} Ht−1∈Rn×h(隐藏单元个数 h h h)。那么,重置门 R t ∈ R n × h \mathbf{R}_t \in \mathbb{R}^{n \times h} Rt∈Rn×h和更新门 Z t ∈ R n × h \mathbf{Z}_t \in \mathbb{R}^{n \times h} Zt∈Rn×h的计算如下:
R
t
=
σ
(
X
t
W
x
r
+
H
t
−
1
W
h
r
+
b
r
)
,
Z
t
=
σ
(
X
t
W
x
z
+
H
t
−
1
W
h
z
+
b
z
)
,
其中 W x r , W x z ∈ R d × h \mathbf{W}_{xr}, \mathbf{W}_{xz} \in \mathbb{R}^{d \times h} Wxr,Wxz∈Rd×h和 W h r , W h z ∈ R h × h \mathbf{W}_{hr}, \mathbf{W}_{hz} \in \mathbb{R}^{h \times h} Whr,Whz∈Rh×h是权重参数, b r , b z ∈ R 1 × h \mathbf{b}_r, \mathbf{b}_z \in \mathbb{R}^{1 \times h} br,bz∈R1×h是偏置参数。在求和过程中会发生广播机制。
将重置门 R t \mathbf{R}_t Rt和RNN中的常规隐状态 H t \mathbf H_t Ht更新机制集成,得到时间步 t t t的候选隐状态 H ~ t ∈ R n × h \tilde{\mathbf{H}}_t \in \mathbb{R}^{n \times h} H~t∈Rn×h
H ~ t = tanh ( X t W x h + ( R t ⊙ H t − 1 ) W h h + b h ) , \tilde{\mathbf{H}}_t = \tanh(\mathbf{X}_t \mathbf{W}_{xh} + \left(\mathbf{R}_t \odot \mathbf{H}_{t-1}\right) \mathbf{W}_{hh} + \mathbf{b}_h), H~t=tanh(XtWxh+(Rt⊙Ht−1)Whh+bh),
其中 W x h ∈ R d × h \mathbf{W}_{xh} \in \mathbb{R}^{d \times h} Wxh∈Rd×h 和 W h h ∈ R h × h \mathbf{W}_{hh} \in \mathbb{R}^{h \times h} Whh∈Rh×h是权重参数, b h ∈ R 1 × h \mathbf{b}_h \in \mathbb{R}^{1 \times h} bh∈R1×h是偏置项, 符号⊙是Hadamard积(按元素乘积)运算符。 在这里,我们使用tanh非线性激活函数来确保候选隐状态中的值保持在区间(−1,1)中。
当重置门 R t \mathbf{R}_t Rt中的项接近1时,候选隐状态 H ~ t \tilde{\mathbf{H}}_t H~t就和普通的RNN网络中的隐状态 H t \mathbf H_t Ht一样了
当重置门 R t \mathbf R_t Rt中所有项接近0时,候选隐状态是以 X t \mathbf X_t Xt作为输入的多层感知机的结果。因此,任何预先存在的隐状态都会被重置为默认值。
上面介绍的是候选隐状态,我们需要结合更新门 Z t \mathbf Z_t Zt的效果。这一步确定新的隐状态 H t ∈ R n × h \mathbf{H}_t \in \mathbb{R}^{n \times h} Ht∈Rn×h在多大程度上来自旧的状态 H t − 1 \mathbf H_{t-1} Ht−1和新的候选状态 H ~ t \tilde{\mathbf{H}}_t H~t。更新门 Z t \mathbf Z_t Zt仅需要在 H t − 1 \mathbf{H}_{t-1} Ht−1和 H ~ t \tilde{\mathbf{H}}_t H~t之间进行按元素的凸组合就可以实现。门控循环单元的最终更新公式:
H t = Z t ⊙ H t − 1 + ( 1 − Z t ) ⊙ H ~ t . \mathbf{H}_t = \mathbf{Z}_t \odot \mathbf{H}_{t-1} + (1 - \mathbf{Z}_t) \odot \tilde{\mathbf{H}}_t. Ht=Zt⊙Ht−1+(1−Zt)⊙H~t.
每当更新门 Z t \mathbf Z_t Zt接近1时,模型就倾向只保留旧状态。此时,来自 X t \mathbf X_t Xt的信息就会被忽略,从而有效的跳过了依赖链条中的时间步 t t t。相反,当更新门 Z t \mathbf Z_t Zt接近0时,新的隐状态 H t \mathbf H_t Ht就会接近候选隐状态 H ~ t \tilde{\mathbf{H}}_t H~t。这些设计可以帮助我们处理循环神经网络中的梯度消失问题,并更好地捕获时间步距离很长的序列的依赖关系。例如,如果整个子序列的所有时间步的更新门都接近1,则无论序列长度如何,在序列起始时间步的旧隐状态都将很容易保留并传递到序列结束。
门控循环单元的两个显著特征:
读取数据集:
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_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_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, device=device) # 附加梯度 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。此函数返回一个形状为(批量大小、隐藏单元个数)的张量,张量的值全部为零。
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,)
训练与预测的工作方式与RNN完全相同。训练结束后,分别打印输出训练集的困惑度,以及前缀“time traveler”和“traveler”的预测序列上的困惑度。
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 filby
高级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
长短期记忆网络引入了记忆元,或简称为单元。有些文献认为记忆元是隐状态的一种特殊类型,它们与隐状态具有相同的形状,其目的是用于记录附加的信息。为了控制记忆元,需要许多门。一个门用来从单元中输出条目,称为输出门。另外一个门用来决定把何时将数据读入单元,称为输入门。还需要一种机制来重置单元的内容,由遗忘门来管理,这种设计的动机与门控循环单元相同,能够通过专用机制决定什么时候记忆或忽略隐状态的输入。
就如在门控循环单元中一样,当前时间步的输入和前一个时间步的隐状态作为数据送入长短期记忆网络的门中,如下图所示。它们由三个具有sigmoid激活函数的全连接层处理,以计算输入门、遗忘门和输出门的值。因此这三个门的值都在(0,1)的范围内。
细化一下长短期记忆网络的数学表达。 假设有 h h h个隐藏单元,批量大小为 n n n,输入数为 d d d(即时间步数)。 因此,输入为 X t ∈ R n × d \mathbf{X}_t \in \mathbb{R}^{n \times d} Xt∈Rn×d, 前一时间步的隐状态为 H t − 1 ∈ R n × h \mathbf{H}_{t-1} \in \mathbb{R}^{n \times h} Ht−1∈Rn×h。 相应地,时间步 t t t的门被定义如下: 输入门是 I t ∈ R n × h \mathbf{I}_t \in \mathbb{R}^{n \times h} It∈Rn×h, 遗忘门是 F t ∈ R n × h \mathbf{F}_t \in \mathbb{R}^{n \times h} Ft∈Rn×h, 输出门是 O t ∈ R n × h \mathbf{O}_t \in \mathbb{R}^{n \times h} Ot∈Rn×h。 它们的计算方法如下:
I
t
=
σ
(
X
t
W
x
i
+
H
t
−
1
W
h
i
+
b
i
)
,
F
t
=
σ
(
X
t
W
x
f
+
H
t
−
1
W
h
f
+
b
f
)
,
O
t
=
σ
(
X
t
W
x
o
+
H
t
−
1
W
h
o
+
b
o
)
,
其中 W x i , W x f , W x o ∈ R d × h \mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h} Wxi,Wxf,Wxo∈Rd×h和 W h i , W h f , W h o ∈ R h × h \mathbf{W}_{hi}, \mathbf{W}_{hf}, \mathbf{W}_{ho} \in \mathbb{R}^{h \times h} Whi,Whf,Who∈Rh×h是权重参数, b i , b f , b o ∈ R 1 × h \mathbf{b}_i, \mathbf{b}_f, \mathbf{b}_o \in \mathbb{R}^{1 \times h} bi,bf,bo∈R1×h是偏置参数。
** W x i , W x f , W x o ∈ R d × h \mathbf{W}_{xi}, \mathbf{W}_{xf}, \mathbf{W}_{xo} \in \mathbb{R}^{d \times h} Wxi,Wxf,Wxo∈Rd×h训练过程中是将批量大小×输入长度为输入 下图中的列数是由时间步数决定的 然后进行训练 所以输入和参数的维度是上面所示 输出是因为每一个输入会有一个输出 所以输入和输出的维度是一样的 最后使用时是保留最后一个输出值 **
由于还没有指定各种门的操作,所以先介绍候选记忆元 C ~ t ∈ R n × h \tilde{\mathbf{C}}_t \in \mathbb{R}^{n \times h} C~t∈Rn×h。它的计算与上述描述的三个门的计算类似,但是使用tanh函数作为激活函数,函数的值范围为(-1,1).下面导出在时间步t出的方程:
C ~ t = tanh ( X t W x c + H t − 1 W h c + b c ) , \tilde{\mathbf{C}}_t = \text{tanh}(\mathbf{X}_t \mathbf{W}_{xc} + \mathbf{H}_{t-1} \mathbf{W}_{hc} + \mathbf{b}_c), C~t=tanh(XtWxc+Ht−1Whc+bc),
其中 W x c ∈ R d × h \mathbf{W}_{xc} \in \mathbb{R}^{d \times h} Wxc∈Rd×h和 W h c ∈ R h × h \mathbf{W}_{hc} \in \mathbb{R}^{h \times h} Whc∈Rh×h是权重参数,\mathbf{b}_c \in \mathbb{R}^{1 \times h}是偏置参数
在门控循环单元中,有一种机制来控制输入和遗忘(跳过)。类似地,在长短期记忆网络中,也有两个门用于这样的目的:输入门 I t \mathbf I_t It控制采用多少来自 C ~ t \tilde{\mathbf{C}}_t C~t的新数据,而遗忘门 F t \mathbf F_t Ft控制保留多少过去的记忆元 C t − 1 ∈ R n × h \mathbf{C}_{t-1} \in \mathbb{R}^{n \times h} Ct−1∈Rn×h的内容。使用按元素乘法,得出:
C t = F t ⊙ C t − 1 + I t ⊙ C ~ t . \mathbf{C}_t = \mathbf{F}_t \odot \mathbf{C}_{t-1} + \mathbf{I}_t \odot \tilde{\mathbf{C}}_t. Ct=Ft⊙Ct−1+It⊙C~t.
如果遗忘门始终为1且输入门始终为0,则过去的记忆元 C t − 1 \mathbf C_{t-1} Ct−1将随时间被保存并传递到当前时间步。引入这种设计是为了缓解梯度消失问题,并更好的捕获序列中的长距离依赖关系。
计算记忆元的流程图:
最后,我们需要定义如何计算隐状态 H t ∈ R n × h \mathbf{H}_t \in \mathbb{R}^{n \times h} Ht∈Rn×h,这就是输出门发挥作用的地方。在长短时记忆网络中,他仅仅是记忆元的tanh的门控版本。这就确保了 H t \mathbf H_t Ht的值始终在区间(-1,1)内:
H t = O t ⊙ tanh ( C t ) . \mathbf{H}_t = \mathbf{O}_t \odot \tanh(\mathbf{C}_t). Ht=Ot⊙tanh(Ct).
只要输出门接近1,就能够有效地将所有记忆信息传递给预测部分,而对于输出门接近0,只保留级医院内的所有信息,而不需要更新隐状态。
加载数据集:
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_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
在初始化函数中,长短期记忆网络的隐状态需要返回一个额外的记忆元,单元的值为0,形状为(批量大小,隐藏单元数)。因此,我们得到以下的状态初始化。
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))
实际模型的定义与前面讨论的一样:提供三个门和一个额外的记忆元。注意:只有隐状态会传递到输出层,而记忆元
C
t
\mathbf C_t
Ct不直接参与输出计算 这是下面代码中输出计算部分Y = (H @ W_hq) + b_q
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)
通过实例化RNN中引入的RNNModelScratch
类来训练一个长短期记忆网络
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 dime
使用高级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。
之前的章节只讲了具有一个单向隐藏层的循环神经网络。其中,隐变量和观测值与具体的函数形式的交互方式是相当随意的。只要交互类型建模具有足够的灵活性就可以。然而 ,对一个单层来说,这可能具有相当的挑战性。之前在线性模型中,我们通过添加更多层来解决这个问题。而在循环神经网络中,需要确定如何添加更多的层,以及在哪里添加额外的非线性。
深度循环神经网络主要是为了增强其预测的能力。
将多层循环神经网络堆叠在一起,通过几个简单的组合,产生一个灵活的机制。特别是,数据可能与不同层的堆叠有关。 例如,我们可能希望保持有关金融市场状况 (熊市或牛市)的宏观数据可用, 而微观数据只记录较短期的时间动态。
下图描述了一个具有 L \mathbf L L个隐藏层的深度循环神经网络,每个隐状态都连续地传递到当前层地下一个时间步和下一层的当前时间步。
假设在时间步 t t t有一个小批量的输入数据 X t ∈ R n × d \mathbf{X}_t \in \mathbb{R}^{n \times d} Xt∈Rn×d(样本数: n n n,每个样本中的输入数: d d d)。同时,将 l t h l^\mathrm{th} lth隐藏层 ( l = 1 , … , L ) (l=1,\ldots,L) (l=1,…,L)的隐状态设为 H t ( l ) ∈ R n × h \mathbf{H}_t^{(l)} \in \mathbb{R}^{n \times h} Ht(l)∈Rn×h(隐藏单元数: h h h),输出层变量设为 O t ∈ R n × q \mathbf{O}_t \in \mathbb{R}^{n \times q} Ot∈Rn×q(输出数: q q q)。设置 H t ( 0 ) = X t \mathbf{H}_t^{(0)} = \mathbf{X}_t Ht(0)=Xt,第 l l l个隐藏层的隐状态使用激活函数 ϕ l \phi_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 ) ) , \mathbf{H}_t^{(l)} = \phi_l(\mathbf{H}_t^{(l-1)} \mathbf{W}_{xh}^{(l)} + \mathbf{H}_{t-1}^{(l)} \mathbf{W}_{hh}^{(l)} + \mathbf{b}_h^{(l)}), Ht(l)=ϕl(Ht(l−1)Wxh(l)+Ht−1(l)Whh(l)+bh(l)),
其中,权重 W x h ( l ) ∈ R h × h \mathbf{W}_{xh}^{(l)} \in \mathbb{R}^{h \times h} Wxh(l)∈Rh×h, W h h ( l ) ∈ R h × h \mathbf{W}_{hh}^{(l)} \in \mathbb{R}^{h \times h} Whh(l)∈Rh×h和 偏置 b h ( l ) ∈ R 1 × h \mathbf{b}_h^{(l)} \in \mathbb{R}^{1 \times h} bh(l)∈R1×h都是第l个隐藏层的模型参数。
最后,输出层的计算仅基于第 l l l个隐藏层最终的隐状态:
O t = H t ( L ) W h q + b q , \mathbf{O}_t = \mathbf{H}_t^{(L)} \mathbf{W}_{hq} + \mathbf{b}_q, Ot=Ht(L)Whq+bq,
其中,权重 W h q ∈ R h × q \mathbf{W}_{hq} \in \mathbb{R}^{h \times q} Whq∈Rh×q和偏置 b q ∈ R 1 × q \mathbf{b}_q \in \mathbb{R}^{1 \times q} bq∈R1×q都是输出层的模型参数。
与多层感知机一样,隐藏层数目 L \mathbf L L和隐藏单元数目 h h h都是超参数。也就是说,它们可以由我们调整。另外,用门控循环单元或长短期记忆网络的隐状态来代替上式中的隐状态进行计算,可以很容易地得到深度门控循环神经网络或深度长短期记忆神经网络。
实现多层循环神经网络所需的许多逻辑细节在高级API中都是现成的。 简单起见,我们仅示范使用此类内置函数的实现方式。 以长短期记忆网络模型为例, 该代码与之前在第二节中使用的代码非常相似, 实际上唯一的区别是我们指定了层的数量, 而不是使用单一层这个默认值。 像往常一样,我们从加载数据集开始。
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
。 隐藏单元的数量仍然是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)
由于使用了长短期记忆网络模型来实例化两个层,因此训练速度被大大降低了
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
主要可以应用在完形填空中
希望循环神经网络拥有一种机制,使之能够提供前瞻能力。需要增加“从最后一个词元开始从后向前运行”的循环神经网络,而不是只有一个在前向模式下“从第一个词元开始运行”的循环神经网络。*双向循环神经网络(RNNs)*添加了反向传递信息的隐藏层,以便灵活的处理此类信息。
单个隐藏层的双向循环神经网络架构:
对于任意时间步 t t t,给定一个小批量的输入数据 X t ∈ R n × d \mathbf{X}_t \in \mathbb{R}^{n \times d} Xt∈Rn×d(样本数 n n n,每个实例中的输入数 d d d),并且领隐藏层激活函数为 ϕ \phi ϕ。在双向架构中,设该时间步的前向和反向隐状态分别是 H → t ∈ R n × h \overrightarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h} H t∈Rn×h和 H ← t ∈ R n × h \overleftarrow{\mathbf{H}}_t \in \mathbb{R}^{n \times h} H t∈Rn×h,其中 h h h是隐藏单元的数目。前向和反向隐状态的更新如下:
KaTeX parse error: {split} can be used only in display mode.
其中,权重 W x h ( f ) ∈ R d × h , W h h ( f ) ∈ R h × h , W x h ( b ) ∈ R d × h , W h h ( b ) ∈ R h × h \mathbf{W}_{xh}^{(f)} \in \mathbb{R}^{d \times h}, \mathbf{W}_{hh}^{(f)} \in \mathbb{R}^{h \times h}, \mathbf{W}_{xh}^{(b)} \in \mathbb{R}^{d \times h}, \mathbf{W}_{hh}^{(b)} \in \mathbb{R}^{h \times h} Wxh(f)∈Rd×h,Whh(f)∈Rh×h,Wxh(b)∈Rd×h,Whh(b)∈Rh×h和偏置 b h ( f ) ∈ R 1 × h , b h ( b ) ∈ R 1 × h \mathbf{b}_h^{(f)} \in \mathbb{R}^{1 \times h}, \mathbf{b}_h^{(b)} \in \mathbb{R}^{1 \times h} bh(f)∈R1×h,bh(b)∈R1×h都是模型参数。
**接下来,将前向隐状态 H → t \overrightarrow{\mathbf{H}}_t H t和反向隐状态 H ← t \overleftarrow{\mathbf{H}}_t H t连接起来,获得需要送入输出层的隐状态 H t ∈ R n × 2 h \mathbf{H}_t \in \mathbb{R}^{n \times 2h} Ht∈Rn×2h。**在具有多个隐藏层的深度双向循环网络中,该信息作为输入传递到下一个双向层。最后,输出层计算得到的输出为 O t ∈ R n × q \mathbf{O}_t \in \mathbb{R}^{n \times q} Ot∈Rn×q( q q q是输出单元的数目):
O t = H t W h q + b q . \mathbf{O}_t = \mathbf{H}_t \mathbf{W}_{hq} + \mathbf{b}_q. Ot=HtWhq+bq.
这里,权重矩阵 W h q ∈ R 2 h × q \mathbf{W}_{hq} \in \mathbb{R}^{2h \times q} Whq∈R2h×q和偏置 b q ∈ R 1 × q \mathbf{b}_q \in \mathbb{R}^{1 \times q} bq∈R1×q是输出层的模型参数。事实上,这两个方向可以拥有不同数量的隐藏单元。
双向循环网络的一个关键特性是:使用来自序列两端的信息来估计输出。也就是说,使用来自过去和未来的观测信息来预测当前的观测。但是在对下一个词元进行预测的情况中,这样的模型并不是所需的。因为在预测下一个词元时,无法知道下一个词元的下文是什么,所以不会得到很好的精度。具体来说,在训练期间,能够利用过去和未来的数据来估计现在空缺的词;而在测试期间,只有过去的数据,所以精度会将很差。
另一个非常严重的问题是,双向循环神经网络的计算速度非常慢。主要原因是网络的前向传播需要在双向层中进行前向和后向递归,并且网络的反向传播还依赖于前向传播的结果。因此,梯度求解将有一个非常长的链。
由于双向循环神经网络使用了过去的和未来的数据,所以不能盲目地将这一语言模型应用于任何预测任务。尽管模型产出的困惑度是合理的,该模型预测未来次元的能力却存在严重缺陷。下面的示例代码引以为戒,以防在错误的环境中使用它们。
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) # 通过设置“bidirective=True”来定义双向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, bidirectional=True) 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
语言模型是自然语言处理的关键,而机器翻译是语言模型最成功的基准测试。因为机器翻译正是将输入序列转换成输出序列的序列转换模型的核心问题。序列转换模型在各类现代人工智能应用中发挥着至关重要的作用。、
机器翻译指的是将序列从一种语言自动翻译成另一种语言。
神经网络机器翻译方法强调的是端到端学习。与循环神经网络中语言模型和数据集中的语料库是单一语言的语言模型问题存在不同,机器翻译的数据集是由源语言和目标语言的文本序列对组成的。因此,需要一种完全不同的方法来预处理机器翻译数据集,而不是复用语言模型的预处理程序。下面将预处理后的数据加载到小批量中用于训练。
import os
import torch
from d2l import torch as d2l
下载一个“英-法”数据集,数据集中的每一行都是制表符分隔的文本序列对,序列对由英文文本序列和翻译后的法语文本序列组成。每个文本序列可以是一个句子也可以是包含多个句子的一个段落。在这个将英语翻译成法语的机器翻译问题中,英语是源语言,法语是目标语言。
#@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 != ' ' # 使用空格替换不间断空格 # 使用小写字母替换大写字母 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 !
与循环神经网络中的字符级词元化不同,在机器翻译中,将单词词元化(最先进的模型可能使用更高级的词元化技术)。下面的tokenize_nmt函数对前num_examples个文本序列进行词元,其中每个词元要么是一个词,要么是一个标点符号。此函数返回两个词元列表:source和target:source[i]是源语言第 i i i个文本序列的词元列表,target[i]是目标语言第 i 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(' ')) # 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);
由于机器翻译数据集由语言对组成,因此可以分别为源语言和目标语言构建两个词表。使用单词级词元化时,词表大小将明显大于使用字符级词元化时的词表大小。为了缓解这一问题,将出现次数少于2的低频词词元视为相同的未知(“”)词元。此外还指定了额外的特定词元,例如在小批量时用于将序列填充到相同长度的填充词元(“”),以及序列的开始词元(“”)和结束词元(“”)。
src_vocab = d2l.Vocab(source, min_freq=2,
reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)
输出:
10012
语言模型中的序列样本都有一个固定的长度,无论这个样本是一个句子的一部分还是跨越了多个句子的一个片段。这个固定长度是由时间步数或词元数量的参数指定的。在机器翻译中,每个样本都是由源和目标组成的文本序列对,其中每个文本序列可能具有不同的长度。
为了提高效率,仍然可以通过截断和填充方式实现一次只处理一个小批量的文本序列。假设同一个小批量中的每个序列都应该具有相同的长度num_steps,那么如果文本序列的词元数目少于num_steps时,将继续在其末尾添加特定的""词元,知道其长度达到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]
定义一个函数,可以将文本序列转换成小批量数据集用于训练。将特定的"“词元添加到所有序列的末尾,用于表示序列的结束。当模型通过一个词元接一个词元地生成序列进行预测时,生成的”"词元说明完成了序列输出工作。此外,记录了每个文本序列的长度,统计长度时排除了填充词元。
#@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([[ 7, 43, 4, 3, 1, 1, 1, 1],
[44, 23, 4, 3, 1, 1, 1, 1]], dtype=torch.int32)
X的有效长度: tensor([4, 4])
Y: tensor([[ 6, 7, 40, 4, 3, 1, 1, 1],
[ 0, 5, 3, 1, 1, 1, 1, 1]], dtype=torch.int32)
Y的有效长度: tensor([5, 3])
其实这是一个迭代器 里面还有很多数据 因为循环的最后是一个break所以指数出了一个结果 因为batch_size是2 所以有两行
CNN理解编码器-解码器
RNN理解编码器-解码器
隐藏层相当于编码器,最后一个时刻的输出相当于解码器
编码器-解码器架构
一个模型被分成两块:
在编码器接口中,指定长度可变的序列作为编码器的输入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 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)
本节将使用两个循环网络的编码器和解码器,并将其应用到序列到序列(sequence to sequence,seq2seq)类的学习任务
遵循编码器-解码器架构的设计原则,循环神经网络编码器使用长度可变的序列作为输入,将其转换为固定形状的隐状态。换言之,输入序列的信息被编码到循环网络编码器的隐状态中。为了连续生成输出序列的词元,独立的循环神经网络解码器是基于输入序列的编码信息和输出序列已经看见的或者生成的词元来预测下一个词元。下图演示了在机器翻译中使用两个循环神经网络进行序列到序列学习。
特定的“”表示序列结束词元,一旦输出序列生成此词元,模型就会停止预测。在循环神经网络解码器的初始化时间步,有两个特定的设计决定:首先,特定的""表示序列开始词元,他是解码器的输入序列的第一个词元。其次,使用循环神经网络编码器最终的隐状态来初始化解码器的隐状态。
使用“英-法”数据集来训练这个机器翻译模型
import collections
import math
import torch
from torch import nn
from d2l import torch as d2l
机器翻译:
编码器将长度可变的输入序列转换成形状固定的上下文变量 c \mathbf c c,并且将输入序列的信息在该上下文变量中进行编码。
考虑有一个序列组成的样本(批量大小是1)。假设输入序列是 x 1 , … , x T x_1, \ldots, x_T x1,…,xT,其中 x t x_t xt是输入文本序列中的第 t t t个词元。在时间步 t t t,循环神经网络将词元 x t x_t xt的输入特征向量 x t \mathbf{x}_t xt和 h t − 1 \mathbf{h} _{t-1} ht−1(即上一时间步的隐状态)转换为 h t \mathbf{h}_t ht(即当前步的隐状态)。使用一个函数 f f f来描述循环的循环层所做的变换:
h t = f ( x t , h t − 1 ) . \mathbf{h}_t = f(\mathbf{x}_t, \mathbf{h}_{t-1}). ht=f(xt,ht−1).
总之,编码器通过选定的函数 q q q,将所有时间步的隐状态转换为上下文变量:
c = q ( h 1 , … , h T ) . \mathbf{c} = q(\mathbf{h}_1, \ldots, \mathbf{h}_T). c=q(h1,…,hT).
比如,当选择 q ( h 1 , … , h T ) = h T q(\mathbf{h}_1, \ldots, \mathbf{h}_T) = \mathbf{h}_T q(h1,…,hT)=hT时,上下文变量仅仅是输入序列在最后时间的隐状态 h T \mathbf{h}_T hT。
到目前为止,使用的是单向循环神经网络来设计编码器,其中隐状态只依赖于输入子序列,这个子序列是由输入序列的开始为指导隐状态所在的时间步的位置(包括隐状态所在的时间步)组成。也可以使用双向循环神经网络构造编码器,其中隐状态依赖于两个输入子序列,两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列,因此隐状态对整个序列的信息都进行了编码。
现在,实现循环神经网络编码器。使用嵌入层来获得输入序列中每个词元的特征向量。嵌入层的权重是一个矩阵,其行数等于输入词表的大小(vocab_size),其列数等于特征向量的维度(embed_size)。对于任意输入词元的索引 i i i,嵌入层获取权重矩阵的第 i i i行(从0开始)以返回其特征向量。另外,选择一个多层门控循环单元来实现编码器。
Embedding是用一个低维稠密的向量来“表示”一个对象(这里的对象泛指一切可推荐的事物,比如商品、电影、音乐、新闻等),同时表示一词意味着Embedding能够表达相应对象的某些特征,同时向量之间的距离也能够反应对象之间的相似性。
在词向量空间内,甚至完全不知道一个词的向量的情况下,仅靠语义关键加词向量运算就可以推荐出这个词的词向量。
Embedding技术对于深度学习推荐系统的重要性
(详情参考https://zhuanlan.zhihu.com/p/164502624)
#@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'的形状:(batch_size,num_steps,embed_size) X = self.embedding(X) # 在循环神经网络模型中,第一个轴对应于时间步 X = X.permute(1, 0, 2) # 如果未提及状态,则默认为0 output, state = self.rnn(X) # output的形状:(num_steps,batch_size,num_hiddens) # state的形状:(num_layers,batch_size,num_hiddens) return output, state
输出的output的是每个时间步的输出,state是最后一个时间步每一层的输出
output会输出隐藏层的是因为 没有进行输出y的计算
使用一个两层门控循环单元编码器,其隐藏单元数为16。 给定一小批量的输入序列X
(批量大小为4,时间步为7)。 在完成所有时间步后, 最后一层的隐状态的输出是一个张量(output
由编码器的循环层返回), 其形状为(时间步数,批量大小,隐藏单元数)
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
由于这里使用的是门控循环单元, 所以在最后一个时间步的多层隐状态的形状是 (隐藏层的数量,批量大小,隐藏单元的数量)。 如果使用长短期记忆网络,state
中还将包含记忆单元信息。
state.shape
输出:
torch.Size([2, 4, 16])
编码器输出的上下文变量 c \mathbf c c对整个输入序列 x 1 , … , x T x_1, \ldots, x_T x1,…,xT进行编码。来自训练数据集的输出序列 y 1 , y 2 , … , y T ′ y_1, y_2, \ldots, y_{T'} y1,y2,…,yT′,对每个时间步 t ′ t' t′(与输入序列或编码器的时间步 t t t)不同,解码器输出 y t ′ y_{t'} yt′的概率取决于先前的输出序列 y 1 , … , y t ′ − 1 y_1, \ldots, y_{t'-1} y1,…,yt′−1和上下文变量 c \mathbf c c,即 P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}) P(yt′∣y1,…,yt′−1,c)。
上面这段话的意思就是说 每一个时间步的翻译结果都和上下文变量c(即编码器的最后一个隐藏层的输出 ) 和上一个时间步的翻译结果(上一个时间步解码器的输出)有关
为了在序列上模型化这种条件概率,可以使用另一个循环神经网络作为解码器。在输出序列上的任意时间步 t ′ t' t′,循环神经网络将来自上一时间步的输出 y t ′ − 1 y_{t'-1} yt′−1和上下文变量 c \mathbf 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 ) . \mathbf{s}_{t^\prime} = g(y_{t^\prime-1}, \mathbf{c}, \mathbf{s}_{t^\prime-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^\prime} \mid y_1, \ldots, y_{t^\prime-1}, \mathbf{c}) P(yt′∣y1,…,yt′−1,c)
当实现解码器时,直接使用编码器最后一个时间步的隐状态来初始化解码器的隐状态。这就要求使用循环神经网络的编码器和解码器具有相同数量的层和隐藏单元。为了进一步包含经过编码的输入序列的信息,上下文变量在所有的时间步与解码器的输入(在训练的时候输入是翻译的准确值 在使用或者说预测的时候输入的是上一个时间步的结果)进行拼接(concatenate)。为了预测输出词元的概率分布,在循环神经网络解码器的最后一层使用全连接层来变换隐状态。
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) self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout) # 将编码器的隐藏状态和输入拼接在一起作为解码器的输入 所以需要的长度是embed_size + num_hiddens 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) # 将x中的列进行交换 # 广播context,使其具有与X相同的num_steps context = state[-1].repeat(X.shape[0], 1, 1) # 将最后一个隐藏层的最后一个隐藏状态进行复制为和x的时间步数一样的大小 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_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
在每个时间步,解码器预测了输出词元的概率分布。类似于语言模型,可以使用softmax来获得分布,并通过计算交叉熵损失函数来进行优化。特定的填充词元被添加到序列的末尾,因此不同长度的序列可以以相同的形状的小批量加载。但是,应该将填充词元的预测排除在损失函数的计算之外。
使用下面的sequence_mask函数通过零值化屏蔽不相关的项,以便后面任何不相关预测的计算都是与零的乘积,结果都等于零。例如,如果两个序列的有效长度(不包括填充词元)分别为1和2,则第一个序列的第一项和第二序列的前两项之后的剩余项将被清除为零。
#@save
def sequence_mask(X, valid_len, value=0):
"""在序列中屏蔽不相关的项"""
maxlen = X.size(1)
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
我们可以创建三个相同的序列来进行代码健全性检查, 然后分别指定这些序列的有效长度为4、2和0。 结果就是,第一个序列的损失应为第二个序列的两倍,而第三个序列的损失应为零。
loss = MaskedSoftmaxCELoss()
loss(torch.ones(3, 4, 10), torch.ones((3, 4), dtype=torch.long),
torch.tensor([4, 2, 0]))
输出:
tensor([2.3026, 1.1513, 0.0000])
**训练:**将编码器每一层的隐状态传递给解码器为其隐状态的初始状态,编码器的输入为源语言,解码器的输入是编码器隐状态最后一个时间步最后一层的隐状态和翻译的结果进行训练。
循环训练过程中,特定的序列开始词元(“”)和原始的输出序列(不包括序列结束词元"")拼接在一起作为解码器的输入。这被称为强制教学(teach 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(), 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, 12745.1 tokens/sec on cuda:0
为了采用一个接着一个词元的方式预测输出序列,每个解码器当前时间步的输入都将来自于前一时间步的预测词元。与训练类似,开始词元(“”)在初始时间步被输入到解码器中。该预测过程如下图所示,当输出序列的预测遇到序列结束词元(“”)时,预测就结束了。
#@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
对于预测序列中的任意n元语法(n-grams), BLEU的评估都是这个n元语法是否出现在标签序列中。
将BLEU定义为:
exp ( min ( 0 , 1 − l e n label l e n pred ) ) ∏ n = 1 k p n 1 / 2 n , \exp\left(\min\left(0, 1 - \frac{\mathrm{len}_{\text{label}}}{\mathrm{len}_{\text{pred}}}\right)\right) \prod_{n=1}^k p_n^{1/2^n}, exp(min(0,1−lenpredlenlabel))∏n=1kpn1/2n,
其中 l e n label \mathrm{len}_{\text{label}} lenlabel表示标签序列中的词元数和 l e n pred \mathrm{len}_{\text{pred}} lenpred表示预测序列中的词元数, k k k是用于匹配的最长的 n n n元语法。 另外,用 p n p_n pn表示 n n n元语法的精确度,它是两个数量的比值: 第一个是预测序列与标签序列中匹配的 n n n元语法的数量, 第二个是预测序列中 n n n元语法的数量的比率。 具体地说,给定标签序列 A 、 B 、 C 、 D 、 E 、 F A、B、C、D、E、F A、B、C、D、E、F 和预测序列 A 、 B 、 B 、 C 、 D A、B、B、C、D A、B、B、C、D, 我们有 p 1 = 4 / 5 、 p 2 = 3 / 4 、 p 3 = 1 / 3 p1=4/5、p2=3/4、p3=1/3 p1=4/5、p2=3/4、p3=1/3和 p 4 = 0 p4=0 p4=0。
p 1 p_1 p1就是用预测到的结果和标签序列进行对比 只要有一个是一样的加一 然后除以预测序列的总长度
p 2 p_2 p2是用预测到的结果中两个连续一样的和标签序列中两个连续一样的加一 然后除以预测序列中两个连续组合的数
以此类推
当预测序列与标签序列完全相同时,BLEU为1。 此外,由于 n n n元语法越长则匹配难度越大, 所以BLEU为更长的 n n n元语法的精确度分配更大的权重。 具体来说,当 p n p_n pn固定时, p n 1 / 2 n p_n^{1/2^n} pn1/2n 会随着 n n n的增长而增加(原始论文使用 p n 1 / n p_n^{1/n} pn1/n。 而且,由于预测的序列越短获得的 p n p_n pn值越高, 所以中乘法项之前的系数用于惩罚较短的预测序列。 例如,当 k = 2 k=2 k=2时,给定标签序列 A 、 B 、 C 、 D 、 E 、 F A、B、C、D、E、F A、B、C、D、E、F 和预测序列 A 、 B A、B A、B,尽管 p 1 = p 2 = 1 p_1=p_2=1 p1=p2=1, 惩罚因子 exp ( 1 − 6 / 2 ) ≈ 0.14 \exp(1-6/2) \approx 0.14 exp(1−6/2)≈0.14会降低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 !, bleu 1.000
i lost . => j'ai perdu ., bleu 1.000
he's calm . => il est riche ., bleu 0.658
i'm home . => je suis en retard ?, bleu 0.447
在上一节中,逐个预测输出序列,直到预测序列中出现特定的序列结束词元""。本节将介绍贪心搜索策略(greedy search)策略,并探讨其存在的问题,然后对比其他替代策略:穷举搜索(exhaustive search)和束搜索(beam search)
定义搜索问题,在任意时间步 t ′ t' t′,解码器输出 y t ′ y_{t'} yt′的概率取决于时间步 t ′ t' t′之前的输出子序列 y 1 , … , y t ′ − 1 y_1, \ldots, y_{t'-1} y1,…,yt′−1和对输入序列的信息进行编码得到的上下文变量 c \mathbf c c。为了量化计算代价,用 Y \mathcal{Y} Y表示输出词表,其中包含"“,所以这个词汇集合的基数 ∣ Y ∣ \left|\mathcal{Y}\right| ∣Y∣就是此表的大小(所有词)。将输出序列的最大词元次数指定为 T ′ T' T′(输出句子的长度)。因此,目标是从所有 O ( ∣ Y ∣ T ′ ) \mathcal{O}(\left|\mathcal{Y}\right|^{T'}) O(∣Y∣T′)个可能的输出序列中寻找理想的输出。当然,对于所有输出序列,在”"之后的部分(非本句)将在实际输出中丢弃。
对于输出序列的每一个时间步 t ′ t' t′,我们都将基于贪心搜索从 Y \mathcal{Y} Y中找到具有最高条件概率的词元,即:
y t ′ = argmax y ∈ Y P ( y ∣ y 1 , … , y t ′ − 1 , c ) y_{t'} = \operatorname*{argmax}_{y \in \mathcal{Y}} P(y \mid y_1, \ldots, y_{t'-1}, \mathbf{c}) yt′=argmaxy∈YP(y∣y1,…,yt′−1,c)
一旦输出序列包含了""或者达到其最大长度 T ′ T' T′,则输出完成。
假设输出中有四个词元"A"“B”“C"和”“。每个时间步下的四个数字分别表示在该时间步生成"A”“B”“C"和”"的条件概率。在每个时间步,贪心搜索选择具有最高条件概率的词元。因此,将在上图中预测序列“A”“B”“C”和“”。这个输出序列的条件概率是 0.5 × 0.4 × 0.4 × 0.6 = 0.048 0.5\times0.4\times0.4\times0.6 = 0.048 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_{t'=1}^{T'} P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}) ∏t′=1T′P(yt′∣y1,…,yt′−1,c)值的输出序列,这是基于输入序列生成输出序列的条件概率。 然而,贪心搜索无法保证得到最优序列。
上图在时间步2中, 我们选择词元“C”, 它具有第二高的条件概率。 由于时间步3所基于的时间步1和2处的输出子序列已从贪心搜索中的“A”和“B”改变为上图中的“A”和“C”, 因此时间步3处的每个词元的条件概率也在上图中改变。 假设我们在时间步3选择词元“B”, 于是当前的时间步4基于前三个时间步的输出子序列“A”“C”和“B”为条件, 这与贪心搜索中的“A”“B”和“C”不同。 因此,在上图中的时间步4生成 每个词元的条件概率也不同于贪心搜索中的条件概率。 结果,上图中的输出序列 “A”“C”“B”和“”的条件概率为 0.5×0.3×0.6×0.6=0.054, 这大于贪心搜索的条件概率。 这个例子说明:贪心搜索获得的输出序列 “A”“B”“C”和“” 不一定是最佳序列。
如果目标是获得最优序列,可以考虑使用穷举搜索:穷举地列举所有可能的输出序列及其条件概率,然后计算输出条件概率最高的一个。
虽然可以使用穷举搜索来获得最优序列,但是其计算量 O ( ∣ Y ∣ T ′ ) \mathcal{O}(\left|\mathcal{Y}\right|^{T'}) O(∣Y∣T′)可能高的惊人。例如,当 ∣ Y ∣ = 10000 |\mathcal{Y}|=10000 ∣Y∣=10000和 T ′ = 10 T'=10 T′=10时,需要评估 1000 0 10 = 1 0 40 10000^{10} = 10^{40} 1000010=1040序列,这是一个极大的数,现在的计算机几乎不可能计算它。然而,贪心搜索的计算量 O ( ∣ Y ∣ T ′ ) \mathcal{O}(\left|\mathcal{Y}\right|T') O(∣Y∣T′)要显著地小于穷举搜索。例如,当 ∣ Y ∣ = 10000 |\mathcal{Y}|=10000 ∣Y∣=10000和 T ′ = 10 T'=10 T′=10时,只需要评估 10000 × 10 = 1 0 5 10000\times10=10^5 10000×10=105个序列。
如果精度最重要,则显然是穷举搜索。 如果计算成本最重要,则显然是贪心搜索。 而束搜索的实际应用则介于这两个极端之间。
束搜索是贪心策略的一个改进版本。它有一个超参数,名为束宽 k k k。在时间步1,选择具有最高条件概率 k k k个词元。这 k k k个词元将分别是 k k k个候选输出序列的第一个词元。在随后的每个时间步,基于上一时间步的 k k k个候选输出序列,将继续从 k ∣ Y ∣ k\left|\mathcal{Y}\right| k∣Y∣个可能的选择中挑出具有最高条件概率的 k k k个候选输出序列。
最后选择其中条件概率乘积最高的序列作为输出序列:
1 L α log P ( y 1 , … , y L ∣ c ) = 1 L α ∑ t ′ = 1 L log P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) , \frac{1}{L^\alpha} \log P(y_1, \ldots, y_{L}\mid \mathbf{c}) = \frac{1}{L^\alpha} \sum_{t'=1}^L \log P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}), Lα1logP(y1,…,yL∣c)=Lα1∑t′=1LlogP(yt′∣y1,…,yt′−1,c),
其中 L L L是最终候选序列的长度, α \alpha α通常设置为0.75。因为一个较长的序列在上式的求和中会有更多的对数项,因此分母中的 L α L^\alpha Lα用于惩罚长序列。
束搜索的计算量为 O ( k ∣ Y ∣ T ′ ) \mathcal{O}(k\left|\mathcal{Y}\right|T') O(k∣Y∣T′),这个结果介于贪心搜索和穷举搜索之间。实际上,贪心搜索可以看作一种束宽为1的特殊类型的束搜索。通过灵活地选择束宽,束搜索可以在正确率和计算代价之间进行权衡。
中的输出序列 “A”“C”“B”和“”的条件概率为 0.5×0.3×0.6×0.6=0.054, 这大于贪心搜索的条件概率。 这个例子说明:贪心搜索获得的输出序列 “A”“B”“C”和“” 不一定是最佳序列。
如果目标是获得最优序列,可以考虑使用穷举搜索:穷举地列举所有可能的输出序列及其条件概率,然后计算输出条件概率最高的一个。
虽然可以使用穷举搜索来获得最优序列,但是其计算量 O ( ∣ Y ∣ T ′ ) \mathcal{O}(\left|\mathcal{Y}\right|^{T'}) O(∣Y∣T′)可能高的惊人。例如,当 ∣ Y ∣ = 10000 |\mathcal{Y}|=10000 ∣Y∣=10000和 T ′ = 10 T'=10 T′=10时,需要评估 1000 0 10 = 1 0 40 10000^{10} = 10^{40} 1000010=1040序列,这是一个极大的数,现在的计算机几乎不可能计算它。然而,贪心搜索的计算量 O ( ∣ Y ∣ T ′ ) \mathcal{O}(\left|\mathcal{Y}\right|T') O(∣Y∣T′)要显著地小于穷举搜索。例如,当 ∣ Y ∣ = 10000 |\mathcal{Y}|=10000 ∣Y∣=10000和 T ′ = 10 T'=10 T′=10时,只需要评估 10000 × 10 = 1 0 5 10000\times10=10^5 10000×10=105个序列。
如果精度最重要,则显然是穷举搜索。 如果计算成本最重要,则显然是贪心搜索。 而束搜索的实际应用则介于这两个极端之间。
束搜索是贪心策略的一个改进版本。它有一个超参数,名为束宽 k k k。在时间步1,选择具有最高条件概率 k k k个词元。这 k k k个词元将分别是 k k k个候选输出序列的第一个词元。在随后的每个时间步,基于上一时间步的 k k k个候选输出序列,将继续从 k ∣ Y ∣ k\left|\mathcal{Y}\right| k∣Y∣个可能的选择中挑出具有最高条件概率的 k k k个候选输出序列。
[外链图片转存中…(img-hotdxK6a-1709711164139)]
最后选择其中条件概率乘积最高的序列作为输出序列:
1 L α log P ( y 1 , … , y L ∣ c ) = 1 L α ∑ t ′ = 1 L log P ( y t ′ ∣ y 1 , … , y t ′ − 1 , c ) , \frac{1}{L^\alpha} \log P(y_1, \ldots, y_{L}\mid \mathbf{c}) = \frac{1}{L^\alpha} \sum_{t'=1}^L \log P(y_{t'} \mid y_1, \ldots, y_{t'-1}, \mathbf{c}), Lα1logP(y1,…,yL∣c)=Lα1∑t′=1LlogP(yt′∣y1,…,yt′−1,c),
其中 L L L是最终候选序列的长度, α \alpha α通常设置为0.75。因为一个较长的序列在上式的求和中会有更多的对数项,因此分母中的 L α L^\alpha Lα用于惩罚长序列。
束搜索的计算量为 O ( k ∣ Y ∣ T ′ ) \mathcal{O}(k\left|\mathcal{Y}\right|T') O(k∣Y∣T′),这个结果介于贪心搜索和穷举搜索之间。实际上,贪心搜索可以看作一种束宽为1的特殊类型的束搜索。通过灵活地选择束宽,束搜索可以在正确率和计算代价之间进行权衡。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。