基于seq2seq的机器翻译系统

目录

前言

1.模型结构

1.1 encoder

1.2 decoder

2.数据处理

2.1 数据集

2.2 字典构建

2.3 特殊符号

3.参数加载

3.1 库的导入

3.2 词典导入

4.准备训练集

4.1 导入原始训练数据

4.2词典修改

4.3 加载数据集

5.Encoder

6.Decoder

7.模型训练

7.1输入格式

7.2 线性映射层

7.3超参数设置

7.4具体训练

8.模型预测

9.图形交互界面


前言

        前段时间老师推荐过进行LSTM方向的研究,且笔者考虑到transformer在NLP领域的火热现状,于是选择了seq2seq作为切入点。seq2seq具有和transformer一样的encoder和decoder结构,但缺少attention部分,同时seq2seq模型中的encoder和decoder一般采用LSTM进行实现,因此笔者认为学习seq2seq模型能同时有利于对LSTM和Transformer的理解和学习。本文对基于seq2seq的机器翻译系统从原理和代码上进行了分析和实现,并在代码部分进行了详尽的注释和解析,方便读者理解。


1.模型结构

seq2seq模型由encoder和decoder两部分构成,以下分别对两部分进行模型原理的说明。

1.1 encoder

        如下图所示,对于encoder层,假设想要翻译的英文句子为“Hi.”,其对应的汉语意思为"你好。"。

        encoder的输入中,将英文句子中的每个符号作为一次输入,利用LSTM模型提取英文句子的语义,即图中最后一个hidden层。

1.2 decoder

训练时:

        将encoder层提取的语义作为decoder层的初始hidden层输入,此时在训练中会在汉语译文中增加BOS和EOS用以训练。decoder层的训练输入为BOS+汉语译文各个字,对应的标签数据为汉语译文各个字+EOS。

         也就是用红框中的内容作为decoder层的输入,绿框部分作为Label进行训练。注意在测试集上的预测中,decoder层的输入只有BOS,因为实际使用中预测的就是汉语译文,自然不会有汉语译文供参考了。

2.数据处理

2.1 数据集

        本系统采用的数据集是含有两万多条的中英句子,具体形式如下图所示:

2.2 字典构建

        依据数据集,分别提取汉字,英文符号去重后分别构造词典(字符到数字索引的键值对)en_word_2_index,ch_word_2_index,以及en_index_2_word,ch_index_2_word(数字索引到字符的列表,因为列表排序序号可以替代数字,因此用列表表示即可)。

2.3 特殊符号

        系统中增加了PAD,BOS,EOS分别进行数据填充和译文修饰。

3.参数加载

3.1 库的导入

2.2中提到过对原始数据进行去重并构造字典,这里提供处理完成后的pickle类型文件直接供使用。对于pickle文件的提取也需要在之后 import pickle。

import torch
import torch.nn as nn
import pandas as pd
from torch.utils.data import Dataset,DataLoader
import pickle

3.2 词典导入

#读取提前构建好的双语字符与数字索引的字典
with open("datas\\ch.vec","rb") as f1:
    _, ch_word_2_index,ch_index_2_word = pickle.load(f1)

with open("datas\\en.vec","rb") as f2:
    _, en_word_2_index, en_index_2_word = pickle.load(f2)

        word_2_index,index_2_word的类型分别是字典和列表,后者是列表因为其默认序号可以替代数字,因而省略了数字部分。提取出的四个序列如下所示,此处并不是构造one-hot向量,而是作为数字索引,在之后直接构造对应的embbding。两个pickle文件中各自有三列数据,第一列是字向量,因此只需要提取后两列数据即可。

4.准备训练集

4.1 导入原始训练数据

        从csv文件中分别取出训练集数据,并将该过程封装为一个函数,同时增加了nums参数用来选择读取的数据样本数量。考虑到读者的机器性能,本系统对nums进行了较小的取值,因此预测准确率会受到相应的影响,读者可根据自己机器的性能对nums进行扩值或者忽略(默认是导入所有数据样本进行训练)。

def get_datas(file = "datas\\translate.csv",nums = None):
    all_datas = pd.read_csv(file)
    # 从csv文件中分别读取英语和汉语译文,并分别列表化
    en_datas = list(all_datas["english"])
    ch_datas = list(all_datas["chinese"])
    # 因为总数据比较多,因此在初期做测试的时候设置nums参数,限制取出的数据数量,取少了会影响训练的效果
    if nums == None:
        return en_datas,ch_datas
    else:
        return en_datas[:nums],ch_datas[:nums]

提取出的ch_datas,en_datas如下所示:

4.2词典修改

        在中英文词典中增加PAD,BOS,EOS三个符号,因为最开始的导入文件中没有这三种符号,也可以直接修改文件,省略这一部分。

# 在字典中添加PAD,BOS,EOS三个新字符,也可以对字典进行处理,就不用再添加了
ch_corpus_len = len(ch_word_2_index)
en_corpus_len = len(en_word_2_index)

ch_word_2_index.update({"<PAD>":ch_corpus_len,"<BOS>":ch_corpus_len + 1 , "<EOS>":ch_corpus_len+2})
en_word_2_index.update({"<PAD>":en_corpus_len})

ch_index_2_word += ["<PAD>","<BOS>","<EOS>"]
en_index_2_word += ["<PAD>"]

#直接增加长度或者自增len的长度都可以
ch_corpus_len += 3
en_corpus_len = len(en_word_2_index)

4.3 加载数据集

        ①使用pytorch提供的Dataloader加载数据集。在代码中的“return en_index,ch_index”部分,pytorch会自动将同一batch的结果进行自动组合,因此要通过自定义 vatch_data_process函数来完成对各结果的padding填充,BOS,EOS的增加等自定义操作。

        通过修改Dataloader里对应的collate_fn参数,实现自动组合前对各结果的自定义修改功能。

        ②batch_data_process函数有两个作用。第一,实现对encoder层输入的padding,否则一条一条输入太慢,将一个batch的数据作为矩阵输入更高效。这里的padding对象也是同一batch各样本的输入,因为考虑所有样本时,句子长度差异较大,效果不好。参数中的batch_datas也就是上文en_index,ch_index的batch-size个组合成的迭代器。

        ③第二,实现在训练集上decoder层输入的修改,即输入层的句子前加上BOS标志开始,标签最后加上EOS标志结束,参考1.2中的图示。加上BOS是因为预测时不知道中文,如果不加上BOS,则输入为空;加上EOS则是标志着预测过程的停止。

        ④结果:下两图分别代表经处理后返回的encoder,decoder层输入。

        假设batch为3,则英语句子对应索引经padding后,长度相等,且padding体现为77。      

  

        汉语译文经处理后,最开始都是表示BOS的3590,末尾是padding3589和表示EOS的3591。

总代码如下:

#对数据进行处理,①将每个英语和汉语句子中的字符对应通过字典对应为数字索引,②对各组数字索引进行pad处理,③对汉字句子进行BOS和EOS处理
class MyDataset(Dataset):
    def __init__(self,en_data,ch_data,en_word_2_index,ch_word_2_index):
        # 导入双语原始句子
        self.en_data = en_data
        self.ch_data = ch_data
        # 导入双语字符与数字索引的字典
        self.en_word_2_index = en_word_2_index
        self.ch_word_2_index = ch_word_2_index

    # 将一对双语句子从字符状态转换为数字索引状态,形式是将一个句子中的各个字符转换为数字索引的列表
    def __getitem__(self,index):
        en = self.en_data[index]
        ch = self.ch_data[index]

        en_index = [self.en_word_2_index[i] for i in en]
        ch_index = [self.ch_word_2_index[i] for i in ch]

        return en_index,ch_index

    # pytorch会自动地把batch-size个双语句子组合在一起,参数中的batch_datas也就是上文en_index,ch_index的batch-size个组合成的迭代器
    # 此处添加的函数作为Dataloader的一个参数,意在对数字索引格式进行再处理,也就是pad,EOS,BOS这些
    def batch_data_process(self,batch_datas):
        global device
        en_index , ch_index = [],[]
        en_len , ch_len = [],[]

        for en,ch in batch_datas:
            en_index.append(en)
            ch_index.append(ch)
            en_len.append(len(en))
            ch_len.append(len(ch))

        max_en_len = max(en_len)
        max_ch_len = max(ch_len)

        # 在每一句英文句子的数字索引列表后添加PAD统一大小,在每一句汉语句子的数字索引列表前加BOS,后加EOS,再添加PAD统一大小
        en_index = [ i + [self.en_word_2_index["<PAD>"]] * (max_en_len - len(i))   for i in en_index]
        ch_index = [[self.ch_word_2_index["<BOS>"]]+ i + [self.ch_word_2_index["<EOS>"]] + [self.ch_word_2_index["<PAD>"]] * (max_ch_len - len(i))   for i in ch_index]
        
        #tensor化并且布置到GPU上训练
        en_index = torch.tensor(en_index,device = device)
        ch_index = torch.tensor(ch_index,device = device)

        #返回的是一个batch的经过padding,BOS,EOS修改的句子中各字符对应的数字索引构成的列表
        return en_index,ch_index

    # 判断提取的双语句子的数量是否相等,不相等肯定是提取出问题了
    def __len__(self):
        assert len(self.en_data) == len(self.ch_data)
        return len(self.ch_data)

5.Encoder

        利用pytorch构造英文字符对应的词向量embbding,然后将输入句子的数字索引形式转化为对应的词向量,以词向量的形式输入,词向量的维度由超参数指定。

#对数字索引形式的句子进行Embedding处理和基于LSTM的语义提取
class Encoder(nn.Module):
    def __init__(self,encoder_embedding_num,encoder_hidden_num,en_corpus_len):
        super().__init__()
        # 基于英文语料库建立Embedding
        self.embedding = nn.Embedding(en_corpus_len,encoder_embedding_num)

        # batch-first设置为True是因为torch中dataset处理后的batch批次的样本,其向量维度是batch,input-size,hidden-size,因此不符合默认的input-size,batch,hidden-size
        # 顺序,通过该参数的设置实现参数的匹配
        self.lstm = nn.LSTM(encoder_embedding_num,encoder_hidden_num,batch_first=True)

    def forward(self,en_index):
        # 将输入的句子进行Embbdeing处理,同时得到Encoder层的输出:encoder_hidden
        en_embedding = self.embedding(en_index)
        _,encoder_hidden =self.lstm(en_embedding)

        return encoder_hidden

6.Decoder

        这部分和Encoder的思路基本相同,主要区别在于decoder部分的输入依据当前是训练还是测试有所不同,上文已经提及,此处不再赘述。

class Decoder(nn.Module):
    def __init__(self,decoder_embedding_num,decoder_hidden_num,ch_corpus_len):
        super().__init__()
        self.embedding = nn.Embedding(ch_corpus_len,decoder_embedding_num)
        self.lstm = nn.LSTM(decoder_embedding_num,decoder_hidden_num,batch_first=True)

    # decoder_input和encoder部分那个en_index是一个意思,都是数字索引格式的句子,
    # 但因为汉语句子进行了BOS和EOS处理,我们要对Decoder的输入额外地进行去除末尾EOS的处理
    def forward(self,decoder_input,hidden):
        embedding = self.embedding(decoder_input)
        decoder_output,decoder_hidden = self.lstm(embedding,hidden)

        return decoder_output,decoder_hidden
       

7.模型训练

        将超参数传入Encoder和Decoder,构造损失函数,优化器,调用两层得到输出,并对输出进行线性变换得到预测结果,与实际标签比较。

7.1输入格式

        注意到在decoder层的输入前,将汉语译文作了两次处理,分别是保留BOS和保留EOS,因为之前增加这两个符号时没有区别输入和标签,在此处进行分类。

7.2 线性映射层

        增加classifier,实现将decoder层输出的维度变换。

7.3超参数设置

        encoder_embedding_num和decoder_embedding_num依据英汉词典的大小设置,encoder_hidden_num = 100和decoder_hidden_num = 100则是对LSTM隐层维度的设置,应保持两者一致,否则应另增加一层线性变换用以修改维度。

# 超参数的设置
en_datas,ch_datas = get_datas(nums=200)
encoder_embedding_num = 50
encoder_hidden_num = 100
decoder_embedding_num = 107
decoder_hidden_num = 100

batch_size = 2
epoch = 40
lr = 0.001

7.4具体训练

        在loss函数求值过程中,en_index,ch_index的维度分别为3和2,因此将en_index的前两维拉成一维,ch_index同理,从而求loss,否则对一个batch的数据单独求loss会相对更加麻烦。

for e in range(epoch):
    for en_index,ch_index  in dataloader:
        loss = model(en_index,ch_index)
        loss.backward()
        opt.step()
        opt.zero_grad()

    print(f"loss:{loss:.3f}")

总代码如下:

class Seq2Seq(nn.Module):
    def __init__(self,encoder_embedding_num,encoder_hidden_num,en_corpus_len,decoder_embedding_num,decoder_hidden_num,ch_corpus_len):
        super().__init__()
        self.encoder = Encoder(encoder_embedding_num,encoder_hidden_num,en_corpus_len)
        self.decoder = Decoder(decoder_embedding_num,decoder_hidden_num,ch_corpus_len)

        # 将Decoder的输出从Embedding维度映射到汉字词典的维度,匹配对应的汉字
        self.classifier = nn.Linear(decoder_hidden_num,ch_corpus_len)

        self.cross_loss = nn.CrossEntropyLoss()

    def forward(self,en_index,ch_index):
        # 将汉语句子分别去首尾的BOS和EOS作为Decoder层的输入和标签
        decoder_input = ch_index[:,:-1]
        label = ch_index[:,1:]

        encoder_hidden = self.encoder(en_index)

        # 预测模型只需要各cell的hidden,不需要最后cell的hidden
        decoder_output,_ = self.decoder(decoder_input,encoder_hidden)

        pre = self.classifier(decoder_output)

        # label是[batch,len],pre是[batch,len,Embedding],因此要进行维度处理,此处是把一个batch里的句子都放在一个维度
        loss = self.cross_loss(pre.reshape(-1,pre.shape[-1]),label.reshape(-1))

        return loss

8.模型预测

应注意预测阶段中decoder的输入只有BOS符号

#供用户使用的API接口
def translate(sentence):
    global en_word_2_index,model,device,ch_word_2_index,ch_index_2_word

    # 将英文句子中的字母转换为数字索引,此处进行维度增加的处理并tensor化,方便传参
    en_index = torch.tensor([[en_word_2_index[i] for i in sentence]],device=device)

    result = []
    # 与训练时不同,预测时decoder的输入是“空”,能利用的只有encoder传入的语义hidden,因此之前会设置汉语句子首位为BOS,此处直接将BOS作为初始输入,并将cell的预测结果作为下一个cell
    # 的输入,实现汉语语义的预测
    encoder_hidden = model.encoder(en_index)
    decoder_input = torch.tensor([[ch_word_2_index["<BOS>"]]],device=device)

    # 将encoder层的输入作为decoder层的初始hidden
    decoder_hidden = encoder_hidden
    while True:
        decoder_output,decoder_hidden = model.decoder(decoder_input,decoder_hidden)
        pre = model.classifier(decoder_output)

        w_index = int(torch.argmax(pre,dim=-1))
        word = ch_index_2_word[w_index]

        if word == "<EOS>" or len(result) > 50:
            break

        result.append(word)

        # 将cell的预测结果作为下一个cell的输入,实现汉语语义的预测
        decoder_input = torch.tensor([[w_index]],device=device)


    hanyu="".join(result)
    return hanyu
    # print(hanyu)
    # print("*****")
    # print("译文: ","".join(result))

9.图形交互界面

        本系统的图形交互界面采用PySimpleGUI实现,该库的具体使用参考另一篇博客即可,最终的实现效果如下:

总代码如下:

from seq2seq import translate
import PySimpleGUI as sg

layout = [[sg.Text('输入你想翻译的英文:'), sg.Text(size=(15,1), key='-OUTPUT-')],
          [sg.Input(key='-IN-')],
          [sg.Button('Show'), sg.Button('Exit')],
          [sg.Text('注:预测的准确率与训练样本数量有关,本实验为了方便展示')],
          [sg.Text('注:只选择了少量样本进行训练,如要提高预测率,可增加训练样本数量')]
          ]
#第一行留有一个用以更新的变量区域,键名自定义设置,此处设置为-OUTPUT-

window = sg.Window('基于seq2seq的机器翻译系统', layout)

while True:  # Event Loop
    event, values = window.read()
    print(event, values)
    if event in  (None, 'Exit'):
        break
    if event == 'Show':
        text_input = values['-IN-']
        hanju = translate(text_input)
        # Update the "output" text element to be the value of "input" element
        window['-OUTPUT-'].update(hanju)
        #将输入的值更新在OUTPUT区域

window.close()

猜你喜欢

转载自blog.csdn.net/cuguanren/article/details/127143718