1 总体结构
不论是编码器还是解码器,它们都会首先通过嵌入层(Embedding层)处理外部输入,以便转换成相应的词向量。
2 编码器结构
vocab_size:词汇表的大小,用于定义嵌入层。embed_size:嵌入层输出的维度,即每个词汇的向量表示大小。num_hiddens:循环神经网络(RNN)隐藏层的大小。num_layers:RNN的层数。dropout:dropout概率,用于防止过拟合。
class Seq2SeqEncoder(nn.Module):
"""用于序列到序列学习的循环神经网络编码器"""
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
3 解码器结构
- 前向传播方法:
- 输入
X是一个形状为(batch_size, num_steps)的张量,代表一批目标输入序列,通过嵌入层将输入序列转换为形状为(batch_size, num_steps, embed_size)的张量,并重新排列维度为(num_steps, batch_size, embed_size)。 - 然后,将编码器的最后一个隐藏状态(
context)广播到与输入序列相同的步数,以便在每个时间步上都可以使用。 - 接着,将嵌入后的输入序列和广播后的
context在特征维度上连接,形成新的输入X_and_context。 - 将
X_and_context和初始状态传递给GRU层,得到输出output和新的隐藏状态state。 - 最后,通过全连接层
dense将GRU层的输出转换为形状为(num_steps, batch_size, vocab_size)的张量,并再次重新排列维度为(batch_size, num_steps, vocab_size)。
- 输入
- 注意:
- 在前向传播中,
context即上下文,在这种非注意力解码器中实际上并不存在上下文的概念,它仅使用最新的隐藏状态作为context。
- 在前向传播中,
class Seq2SeqDecoder(nn.Module):
"""用于序列到序列学习的循环神经网络解码器"""
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)
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_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的形状:(num_layers,batch_size,num_hiddens)
return output, state
4 组合
将编码器解码器组合起来,形成整体结构
- 注意:
- enc_outputs包含了enc_output以及隐藏状态state
- 而解码器的隐藏状态初始化只使用了编码器的隐藏状态state
enc_X一般是输入的序列,dec_X一般是输出的目标序列
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)
5 损失函数
其中sequence_mask的valid_len(可以是熟组)表示的是每一个序列保留的长度
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
在损失函数中,weights实际上起到了遮罩的作用,它遮盖了交叉熵损失中不需要计算的部分,以防止计算出错误的损失值
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
6 训练(只展示关键部分)
- 权重初始化
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])
- 梯度裁剪函数
def grad_clipping(net, theta):
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
- 训练的关键部分
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) # 使用 <bos> 作为每一个解码器输入的起始词元
optimizer.zero_grad()
Y_hat, _ = net(X, dec_input, X_valid_len)
l = loss(Y_hat, Y, Y_valid_len)
l.sum().backward() # 计算梯度
grad_clipping(net, 1) # 梯度裁剪
optimizer.step()
7 评估
计算BLEU来作为评估依据
def bleu(pred_seq, label_seq, k):
"""计算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