【2020华为云AI实战营】ModelArts上seq2seq+attention模型实现中英文翻译

举报
pr0d1gy 发表于 2020/08/31 15:52:31 2020/08/31
【摘要】 Seq2seq是一个Encoder-Decoder结构的网络,Encoder和Decoder均由RNN组成,Encoder将输入编码变为向量,decoder根据向量和上一个时间步的预测结果作为输入,预测完整的结果。Attention机制整个输入序列的信息被Encoder编码为固定长度的向量,这个向量无法完全表达整个输入序列的信息。另外,随着输入长度的增加,这个固定长度的向量,会逐渐丢失更多信...


Seq2seq是一个Encoder-Decoder结构的网络,Encoder和Decoder均由RNN组成,Encoder将输入编码变为向量,decoder根据向量和上一个时间步的预测结果作为输入,预测完整的结果。

2_seq2seq.png


Attention机制

2_seq2seq_attention.png


整个输入序列的信息被Encoder编码为固定长度的向量,这个向量无法完全表达整个输入序列的信息。另外,随着输入长度的增加,这个固定长度的向量,会逐渐丢失更多信息。

以中英文翻译任务为例,我们翻译的时候,虽然要考虑上下文,但每个时间步的输出,不同单词的贡献是不同的。考虑下面这个句子对:

Screenshot 2020-08-31 at 15.29.34.png


当我们翻译“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个多小时。

最后来看看翻译结果吧,

Screenshot 2020-08-31 at 15.49.51.png

大家觉得怎么样呢?欢迎积极留言。


    附件下载

  • data.zip 8.95MB 下载次数:21
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。