【2020华为云AI实战营】ModelArts上seq2seq+attention模型实现中英文翻译
Seq2seq是一个Encoder-Decoder结构的网络,Encoder和Decoder均由RNN组成,Encoder将输入编码变为向量,decoder根据向量和上一个时间步的预测结果作为输入,预测完整的结果。
Attention机制
整个输入序列的信息被Encoder编码为固定长度的向量,这个向量无法完全表达整个输入序列的信息。另外,随着输入长度的增加,这个固定长度的向量,会逐渐丢失更多信息。
以中英文翻译任务为例,我们翻译的时候,虽然要考虑上下文,但每个时间步的输出,不同单词的贡献是不同的。考虑下面这个句子对:
当我们翻译“knowledge”时,只需将注意力放在源句中“知识”的部分,当翻译“power”时,只需将注意力集中在"力量“。这样,当我们decoder预测目标翻译的时候就可以看到encoder的所有信息,而不仅局限于原来模型中定长的隐藏向量,并且不会丧失长程的信息。
数据集可以在http://www.manythings.org/anki/下载
cmn.txt为中英文数据文本
样本示例:
'Hi.\t嗨。\tCC-BY 2.0 (France) Attribution: tatoeba.org #538123 (CM) & #891077 (Martha)'
# 分割英文数据和中文数据 en_data = [line.split('\t')[0] for line in data] ch_data = [line.split('\t')[1] for line in data]
# 按字符级切割,并添加<eos> en_token_list = [[char for char in line]+["<eos>"] for line in en_data] ch_token_list = [[char for char in line]+["<eos>"] for line in ch_data]
[['H', 'i', '.', '<eos>'], ['H', 'i', '.', '<eos>']] [['嗨', '。', '<eos>'], ['你', '好', '。', '<eos>']]
# 基本字典 # pad保留填充标识,unk未知符号保留标识,bos句子开头保留标识,eos句尾标记保留id basic_dict = {'<pad>':0, '<unk>':1, '<bos>':2, '<eos>':3} # 分别生成中英文字典 en_vocab = set(''.join(en_data)) en2id = {char:i+len(basic_dict) for i, char in enumerate(en_vocab)} en2id.update(basic_dict) id2en = {v:k for k,v in en2id.items()} # 分别生成中英文字典 ch_vocab = set(''.join(ch_data)) ch2id = {char:i+len(basic_dict) for i, char in enumerate(ch_vocab)} ch2id.update(basic_dict) id2ch = {v:k for k,v in ch2id.items()}
["hello", "how", "are", "you", "?", <pad>, <pad>]->[1, 1, 1, 1, 1, 0, 0]
# 利用字典,映射数据 en_num_data = [[en2id[en] for en in line ] for line in en_token_list] ch_num_data = [[ch2id[ch] for ch in line] for line in ch_token_list] char: Hi. index: [62, 72, 42, 3]
为什么要在Encoder进行padding处理?
def forward(self, input_seqs, input_lengths, hidden): embedded = self.embedding(input_seqs) packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths, enforce_sorted=False) outputs, hidden = self.gru(packed, hidden) outputs, output_lengths = torch.nn.utils.rnn.pad_packed_sequence(outputs) return outputs, hidden
当我们进行batch训练数据一起计算的时候,我们会遇到多个训练样例长度不同的情况,padding将短句子为跟最长的句子一样。
但是这时会出现一个问题,只包含一个单词的句子和包含20个单词的句子,padding之后长度一样,前者有19个pad,这样会导致LSTM对它的表示通过了非常多无用的字符,这样得到的句子表示就会有误差。
解决办法就是用函数torch.nn.utils.rnn.pack_padded_sequence()以及torch.nn.utils.rnn.pad_packed_sequence()来进行处理避免padding对句子表示的影响。
Attention 机制
class Attn(nn.Module): ... def dot_score(self, hidden, encoder_output): return torch.sum(hidden * encoder_output, dim=2) def general_score(self, hidden, encoder_output): energy = self.attn(encoder_output) # [seq_len, batch, hid_dim] return torch.sum(hidden * energy, dim=2) # [seq_len, batch] def concat_score(self, hidden, encoder_output): energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output), 2)).tanh() return torch.sum(self.v * energy, dim=2) ...
在计算出注意力值后,Decoder将其与Encoder输出的隐藏状态进行加权平均,得到上下文向量context.
再将context与Decoder当前时间步的隐藏状态拼接,经过tanh。最后用softmax预测最终的输出概率。
def forward(self, token_inputs, last_hidden, encoder_outputs): ... attn_weights = self.attn(gru_output, encoder_outputs) # attn_weights = [batch, 1, sql_len] context = attn_weights.bmm(encoder_outputs.transpose(0, 1)) # [batch, 1, hid_dim * n directions] gru_output = gru_output.squeeze(0) # [batch, n_directions * hid_dim] context = context.squeeze(1) # [batch, n_directions * hid_dim] concat_input = torch.cat((gru_output, context), 1) # [batch, n_directions * hid_dim * 2] concat_output = torch.tanh(self.concat(concat_input)) # [batch, n_directions*hid_dim] output = self.out(concat_output) # [batch, output_dim] output = self.softmax(output) ...
训练时,根据use_teacher_forcing设置的阈值,决定下一时间步的输入是上一时间步的预测结果还是来自数据的真实值
if self.predict: ... else: max_target_length = max(target_lengths) all_decoder_outputs = torch.zeros((max_target_length, batch_size, self.decoder.output_dim), device=self.device) for t in range(max_target_length): use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False if use_teacher_forcing: # decoder_output = [batch, output_dim] # decoder_hidden = [n_layers*n_directions, batch, hid_dim] decoder_output, decoder_hidden, decoder_attn = self.decoder( decoder_input, decoder_hidden, encoder_outputs ) all_decoder_outputs[t] = decoder_output decoder_input = target_batches[t] # 下一个输入来自训练数据 else: decoder_output, decoder_hidden, decoder_attn = self.decoder( decoder_input, decoder_hidden, encoder_outputs ) # [batch, 1] topv, topi = decoder_output.topk(1) all_decoder_outputs[t] = decoder_output decoder_input = topi.squeeze(1).detach() # 下一个输入来自模型预测
损失函数通过使用设置ignore_index,忽略padding部分的损失
loss_fn = nn.NLLLoss(ignore_index=PAD_token) loss = loss_fn( all_decoder_outputs.reshape(-1, self.decoder.output_dim), target_batches.reshape(-1) )
Seq2Seq在预测阶段每次只输入一个样本,输出其翻译结果,对应forward()函数中的内容如下:
def forward(self, input_batches, input_lengths, target_batches=None, target_lengths=None, teacher_forcing_ratio=0.5): ... if self.predict: # 每次只输入一个样本 output_tokens = [] while True: decoder_output, decoder_hidden, decoder_attn = self.decoder(decoder_input, decoder_hidden, encoder_outputs) topv, topi = decoder_output.topk(1) decoder_input = topi.squeeze(1).detach() output_token = topi.squeeze().detach().item() # 当Decoder输出终止符或输出长度达到所设定的阈值时便停止 if output_token == EOS_token or len(output_tokens) == self.max_len: break output_tokens.append(output_token) return output_tokens else: ...
训练大概每个epoch 1m,这里设置了200epoch大概需要3个多小时。
最后来看看翻译结果吧,
大家觉得怎么样呢?欢迎积极留言。
- 点赞
- 收藏
- 关注作者
评论(0)