系列文章目录
文章目录
- 系列文章目录
- 一、理论知识
- 比喻
- 机器翻译
- Seq2seq
- 编码器-解码器细节
- 训练
- 衡量生成序列的好坏的BLEU(值越大越好)
- 总结
- 二、代码
- 编码器
- 解码器
- 损失函数
- 训练
- 预测
- 预测序列的评估
- 小结
- 练习
一、理论知识
比喻
seq2seq就像RNN的转录工作一样,非常形象的比喻。
机器翻译
Seq2seq
seq2seq是一个EncoderDecoder的架构。在编码器,我们对一个句子可以正着看反着看,解码器部分给定编码器的隐藏状态,和一个开始翻译的标记< bos >,表示开始翻译吧,第一部分翻译结果作为下一部分翻译结果的输入,直到输出了< eos >。此时,我们就没有要求原句子和target句子的长度要相同。
编码器-解码器细节
把Encoder中最后一层的RNN在最后那个时刻的隐藏状态(也就是编码器的输出)和句子的Embedding的输入一起作为Decoder的输入。
训练
衡量生成序列的好坏的BLEU(值越大越好)
p
3
p_3
p3就代表,预测序列中长度为3的序列,ABBCD中就是ABB、BBC、BCD,其中只有BCD在标签序列中存在,所以是1/3。
解释一下长匹配有高权重:因为pn是个小于1的数,所以他的幂越小,整体值越大。
总结
- Seq2seq从一个句子生成另一个句子
- 编码器和解码器都是RNN
- 将编码器最后时间隐状态来初始解码器隐状态来完成信息传递
- 常用BLEU来衡量生成序列的好坏
二、代码
import collections
import math
import torch
from torch import nn
from d2l import torch as d2l
编码器
从技术上讲,编码器将长度可变的输入序列转换成形状固定的上下文变量
c
\mathbf{c}
c,并且将输入序列的信息在该上下文变量中进行编码。如上文所示,可以使用循环神经网络来设计编码器。
考虑由一个序列组成的样本(批量大小是
1
1
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。
到目前为止,我们使用的是一个单向循环神经网络来设计编码器,其中隐状态只依赖于输入子序列,这个子序列是由输入序列的开始位置到隐状态所在的时间步的位置(包括隐状态所在的时间步)组成。
我们也可以使用双向循环神经网络构造编码器,其中隐状态依赖于两个输入子序列,两个子序列是由隐状态所在的时间步的位置之前的序列和之后的序列(包括隐状态所在的时间步),因此隐状态对整个序列的信息都进行了编码。
现在,让我们[实现循环神经网络编码器]。
注意,我们使用了嵌入层(embedding layer)来获得输入序列中每个词元的特征向量。
嵌入层的权重是一个矩阵,其行数等于输入词表的大小(vocab_size
),其列数等于特征向量的维度(embed_size
)。
对于任意输入词元的索引
i
i
i,嵌入层获取权重矩阵的第
i
i
i行(从
0
0
0开始)以返回其特征向量。
另外,本文选择了一个多层门控循环单元来实现编码器。
#@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) 最后与之前的普通RNN不同,之前是len(vocab)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state
循环层返回变量的说明可以参考博客RNN的简介实现。
下面,我们实例化[上述编码器的实现]:
我们使用一个两层门控循环单元编码器,其隐藏单元数为
16
16
16。
给定一小批量的输入序列X
(批量大小为
4
4
4,时间步为
7
7
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
torch.Size([7, 4, 16])
由于这里使用的是门控循环单元,所以在最后一个时间步的多层隐状态的形状是(隐藏层的数量,批量大小,隐藏单元的数量)。
如果使用长短期记忆网络,state
中还将包含记忆单元信息。
state.shape
torch.Size([2, 4, 16])
解码器
????sec_seq2seq_decoder
正如上文提到的,编码器输出的上下文变量 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)。
为了在序列上模型化这种条件概率,我们可以使用另一个循环神经网络作为解码器。
在输出序列上的任意时间步
t
′
t^\prime
t′,循环神经网络将来自上一时间步的输出
y
t
′
−
1
y_{t^\prime-1}
yt′−1和上下文变量
c
\mathbf{c}
c作为其输入,然后在当前时间步将它们和上一隐状态
s
t
′
−
1
\mathbf{s}_{t^\prime-1}
st′−1转换为隐状态
s
t
′
\mathbf{s}_{t^\prime}
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).
:eqlabel:eq_seq2seq_s_t
在获得解码器的隐状态之后,我们可以使用输出层和softmax操作来计算在时间步 t ′ t^\prime t′时输出 y t ′ y_{t^\prime} yt