seq2seq模型实现英译法

58 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情

一、 选题背景

机器翻译的发展主要分为三个阶段:基于规则的机器翻译;基于统计的机器翻译;基于人工神经网络的机器翻译。

现在神经机器翻译的主流模型是编码器-解码器模型结构。该模型由编码器与解码器两部分组成,编码器是一个循环神经网络(RNN),输入是一个词语的序列,每次向RNN单元输入一个词语的嵌入表示向量,并根据输入更新循环神经网络中的隐状态向量。输入完成后得到一个最终的定长的状态向量。理论上最后的这个状态向量包换了前面按序列输入的所有语意信息。解码器是一个生成模型,同样通过RNN单元,将状态向量中的信息一步步还原出来,直到输出特殊标记时结束输出。在编码器-解码器模型结构的基础上,提出了seq2seq模型结构,该模型适长短关系不确定的场景,例如机器翻译或问答系统。

本实验内容是基于seq2seq模型的英法翻译。在现有seq2seq模型基础上,在解码器端加入注意力机制。在解码器端的注意力机制能够根据模型目标有效的聚焦编码器的输出结果,当其作为解码器的输入时提升效果,改善以往编码器输出是单一定长张量,无法存储过多信息的情况。

二、 开发环境

本实验在Windows 10操作系统上进行,使用python版本为3.6.13,pytorch版本为1.3.1,模型在CPU 11th Gen Intel(R) Core(TM) i7-11370H @ 3.30GHz上训练和评估。导入的工具包有:

# 从io工具包导入open方法 

from io import open

# 用于字符规范化

import unicodedata

# 用于正则表达式

import re

# 用于随机生成数据

import random

# 用于构建网络结构和函数的torch工具包

import torch import torch.nn as nn import torch.nn.functional as F

# torch中预定义的优化方法工具包

from torch import optim

三、 数据处理

1. 翻译数据集

本实验使用的是来自pytorch官网的翻译数据集

2. 数据特点

数据中左边是英文,中间使用\t做为分隔,右边是对应的法文。

3. 数据处理

将给定的持久化文件中数据进行处理,以满足模型训练要求。将指定语言中的词汇映射成数值。生成词汇对应自然数值的字典和自然数值对应词汇的字典,其中0,1对应的SOS(开始标志)和EOS(结束标志)。

四、 模型设计

模型的构建需要依次实现其嵌入层(embedding)、编码器(encoder)、解码器(decoder)等部分。下面以python 3.6 + pytorch 1.3.1为例,给出了各部分的核心代码。

代码4-1 基于GRU的编码器
class EncoderRNN(nn.Module):def init(self, input_size, hidden_size):super(EncoderRNN, self).init()self.hidden_size = hidden_sizeself.embedding = nn.Embedding(input_size, hidden_size)self.gru = nn.GRU(hidden_size, hidden_size)  #编码器前向逻辑函数def forward(self, input, hidden):output = self.embedding(input).view(1, 1, -1)output, hidden = self.gru(output, hidden)return output, hidden#初始化隐层张量函数def initHidden(self):return torch.zeros(1, 1, self.hidden_size)

 

代码4-1中,input_size代表编码器的输入尺寸,即源语言英文的词表大小;hidden_size代表GRU的隐藏层神经单元数, 也是词嵌入维度。首先,我们需要将参数hidden_size传入类中,然后实例化Embedding层,输入参数分别是词表单词总数和词嵌入的维度,再实例化GRU,输入参数是hidden_size。

在编码器前向逻辑函数中,参数input代表源语言的输入张量,参数hidden代表初始化的隐藏层张量。经过Embedding处理后,张量是二维的,由于gru要求输入三维张量,所以要对结果拓展维度,同时让任意单词映射后的尺寸是[1,embedding],将经过维度拓展的结果与hidden传入GRU单元中得到返回结果。除此之外,还需要对隐藏层张量大小进行初始化。

 

代码4-2 基于GRU和Attention的解码器
class AttnDecoderRNN(nn.Module):def init(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):super(AttnDecoderRNN, self).init()self.hidden_size = hidden_sizeself.output_size = output_sizeself.dropout_p = dropout_pself.max_length = max_lengthself.embedding = nn.Embedding(self.output_size, self.hidden_size)self.attn = nn.Linear(self.hidden_size * 2, self.max_length)self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)self.dropout = nn.Dropout(self.dropout_p)self.gru = nn.GRU(self.hidden_size, self.hidden_size)self.out = nn.Linear(self.hidden_size, self.output_size)  #解码器前向逻辑函数def forward(self, input, hidden, encoder_outputs):embedded = self.embedding(input).view(1, 1, -1)embedded = self.dropout(embedded)attn_weights = F.softmax(self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))output = torch.cat((embedded[0], attn_applied[0]), 1)output = self.attn_combine(output).unsqueeze(0)output = F.relu(output)output, hidden = self.gru(output, hidden)output = F.log_softmax(self.out(output[0]), dim=1)return output, hidden, attn_weights  #初始化隐层张量函数def initHidden(self):return torch.zeros(1, 1, self.hidden_size)

 

代码4-2中,hidden_size代表解码器GRU的隐藏层神经单元数, 也是解码器输入尺寸。output_size代表解码器的输出尺寸,即目标语言法文的词表大小;output_size代表解码器的输出尺寸,即目标语言法文的词表大小;dropout_p代表使用dropout层时的置零比率,max_length代表句子的最大长度。

首先,我们需要将参数传入类中,接下来实例化Embedding层,输入参数分别是目标语言单词总数和词嵌入的维度,再实例化两个attention层,第二个attention层的输出要进入GRU中,再实例化dropout层,然后实例化GRU,输入参数是hidden_size。再实例化线性层对象,对GRU的输出做线性变换,得到期待的输出尺寸output_size;最后使用softmax进行处理,以便于分类。

在解码器前向逻辑函数中,参数input代表目标语言的输入张量,参数hidden代表初始化的隐藏层张量,参数encoder_output代表编码器的输出张量。与编码器相同,将经过Embedding处理后的张量维度拓展为三维,使用dropout进行随机丢弃,防止过拟合。再进行attention层处理。attention结构的结果使用relu激活,将激活后的结果作为GRU的输入和hidden一起传入解码器GRU。最后将结果降维并使用softmax处理得到最终的结果。除此之外,还需要对隐藏层张量大小进行初始化。

五、 模型训练

代码5-1 损失计算与优化训练
if use_teacher_forcing:for di in range(target_length):loss += criterion(decoder_output, target_tensor[di])decoder_input = target_tensor[di]else:for di in range(target_length):topv, topi = decoder_output.topk(1)loss += criterion(decoder_output, target_tensor[di])if topi.squeeze().item() == EOS_token:Breakdecoder_input = topi.squeeze().detach() encoder_optimizer.step()decoder_optimizer.step() # 使用预定义的SGD作为优化器,将参数和学习率传入其中 encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate) decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate) # 选择损失函数 criterion = nn.NLLLoss()

 

代码5-1中,target_length是目标文本张量获得对应的长度,target_tensor为目标语言输入张量,encoder_optimizer,decoder_optimizer为编码器和解码器优化方法,criterion指损失函数计算方法,通过总损失除以间隔得到平均损失。

训练过程中使用了teacher_forcing,它是一种用于序列生成任务的训练技巧,在seq2seq架构中,根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分,但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果,因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出,这种方式就叫做teacher_forcing。

训练时teacher_forcing比率设置为0.5,也就是有50%的概率使用teacher_forcing。如果使用了teacher_forcing,无论解码器输出的decoder_output是什么,我们都只使用‘正确的答案’,即target_tensor[di]来计算损失,并强制将下一次的解码器输入设置为‘正确的答案’。如果不使用teacher_forcing,损失计算仍然使用decoder_output和target_tensor[di],下一次的解码器输入即当前步最大概率值的那个。

训练参数设置如代码5-2所示:

代码5-2 参数设置
#设置隐层大小为256 ,也是词嵌入维度 hidden_size = 256 # input_lang.n_words输入词汇总数2803,与hidden_size一同传入EncoderRNN类中#得到编码器对象encoder1 encoder1 = EncoderRNN(input_lang.n_words, hidden_size) # output_lang.n_words获取目标词汇总数4345,与hidden_size和dropout_p一同传入AttnDecoderRNN类中#得到解码器对象attn_decoder1 attn_decoder1 = AttnDecoderRNN(hidden_size, output_lang.n_words, dropout_p=0.1)#学习率learning_rate=0.01# 设置迭代步数 n_iters = 75000 # 设置日志打印间隔 print_every = 5000