从零实现基于sequence2sequence的中英翻译模型(torch版)

本文介绍了一个基于PyTorch的中英翻译聊天机器人项目,使用Sequence-to-Sequence架构和GRU进行编码解码,实现了从数据预处理到模型训练、保存和预测的全过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

初学torch,复现了一波官网的tutorial的聊天机器人,只不过把任务场景换成了中英翻译并且简化了一些步骤,力求做到对初学者友好,如果是初学nlp,那这个案例将是一个很好的入门案例。官网链接在此

这篇博客仅对代码做一个记录和简单的说明,不涉及算法的数学原理。阅读此博客需要的知识储备有:
1、nlp中的基本概念,如word embedding
2、sequence2sequence架构的原理,可以通过原论文来学习。
3、GRU原理

数据来源

数据来源:http://www.manythings.org/anki/
包含包含了20133条中英文翻译
在这里插入图片描述
数据如图所示,第一列为英文,第二列为对应的中文,第三列不知道是什么属性,反正没用。

代码总结

import

需要以下的包

from torch import nn
import torch
import torch.nn.functional as F
import pandas as pd
import numpy as np
from sklearn.model_selection import KFold
import random
import itertools
import jieba

词库建立

一般nlp任务都需要有个这,负责词到index编号的映射,以方便后续通过index再次映射到embeding,也方便softmax输出后通过index找到对应词。

USE_CUDA = torch.cuda.is_available()
device = torch.device("cuda:0" if USE_CUDA else "cpu")
PAD_token = 0
SOS_token=1
EOS_token=2

#定义词库
class Voc:
    def __init__(self, name):
        self.name = name
        self.word2index = {}
        self.index2word = {PAD_token: "PAD", SOS_token: "/t", EOS_token: "/n"}   #/t开头,/n结尾
        self.n_words = 3

    def addSentence(self, sentence):   #setence为一个句子分词后的list
        for word in sentence:
            self.addWord(word)

    def addWord(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.n_words
            self.index2word[self.n_words] = word
            self.n_words += 1

    def setence2index(self,setence): #setence为一个句子分词后的list
        index=[]
        for word in setence:
            index.append(self.word2index[word])
        return index

其中SOS_token和EOS_token为起始符和休止符,PAD_token为解决一个batch中句子长短不齐的padding符。

Voc类的功能在于建立word2index和index2word两个字典,并将sentence转换成index。

搭建编/解码器

#定义编码器
class EncoderGRU(nn.Module):
    def __init__(self,hidden_size,embedding):
        super(EncoderGRU, self).__init__()
        self.hidden_size = hidden_size
        self.embedding = embedding
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input,input_len,hidden):
        embedded = self.embedding(input)
        packed = nn.utils.rnn.pack_padded_sequence(embedded, input_len)
        output, hidden = self.gru(packed, hidden)
        output, _ = nn.utils.rnn.pad_packed_sequence(output)
        return output, hidden
        
#定义解码器
class DecoderGRU(nn.Module):
    def __init__(self, hidden_size,output_size,embedding):
        super(DecoderGRU, self).__init__()
        self.hidden_size = hidden_size
        self.embedding = embedding
        self.gru = nn.GRU(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)
        self.softmax = nn.Softmax(dim=1)

    def forward(self, input, hidden):
        output = self.embedding(input)
        output = F.relu(output)
        output, decoder_hidden = self.gru(output, hidden)
        output = self.softmax(self.out(output[0]))
        return output, decoder_hidden

其中nn.utils.rnn.pack_padded_sequence和nn.utils.rnn.pad_packed_sequence分别是对一个含有不等长句子的batch的压缩和解压缩。

数据预处理与loss遮盖处理

一个batch中的句子长短不齐,编码器如上一节所示一样padding后进行压缩和解压即可。但解码器涉及到loss的计算,需要利用mask矩阵对无效的地方进行遮盖,仅计算有效的loss。

#padding函数,用于对不等长的句子进行填充,返回填充后的转置
def zeroPadding(l, fillvalue=PAD_token):
    return list(itertools.zip_longest(*l, fillvalue=fillvalue))

#制作mask矩阵
def binaryMatrix(l, value=PAD_token):
    m = []
    for i, seq in enumerate(l):
        m.append([])
        for token in seq:
            if token == value:
                m[i].append(0)
            else:
                m[i].append(1)
    return m

#对loss进行mask操作
def maskNLLLoss(inp, target, mask):
    # 收集目标词的概率,并取负对数
    crossEntropy = -torch.log(torch.gather(inp, 1, target.view(-1, 1)).squeeze(1))
    # 只保留mask中值为1的部分,并求均值
    loss = crossEntropy.masked_select(mask).mean()
    loss = loss.to(device)
    return loss

#将sentence转换成index
def sentence2index_eng(voc,sentence):
    return [voc.word2index[word] for word in sentence.split()] + [EOS_token]
def sentence2index_chi(voc,sentence):
    return [voc.word2index[word] for word in jieba.lcut(sentence)] + [EOS_token]

#输入处理函数
def input_preprocecing(l,voc):  #接受一个batch的输入
    index_batch=[sentence2index_eng(voc,sentence) for sentence in l]
    lengths=torch.tensor([len(index) for index in index_batch])
    padList=zeroPadding(index_batch)
    padVar=torch.LongTensor(padList)
    return padVar,lengths

#输出处理函数
def output_preprocecing(l,voc):
    index_batch=[sentence2index_chi(voc,sentence) for sentence in l]
    max_label_len=max([len(index) for index in index_batch])
    padList=zeroPadding(index_batch)
    mask=binaryMatrix(padList)
    mask=torch.BoolTensor(mask)
    padVar=torch.LongTensor(padList)
    return padVar,mask,max_label_len

#数据预处理总函数
#接受一个batch的成对儿的数据[['...','...'],[],[],[]]
def data_preprocecing(voc_e,voc_c,pair_batch):
    pair_batch.sort(key=lambda x: len(x[0].split()),reverse=True) #按长度排序
    input_batch,output_batch=[],[]
    for pair in pair_batch:
        input_batch.append(pair[0])
        output_batch.append(pair[1])
    inp,lengths=input_preprocecing(input_batch,voc_e)
    output,mask,max_label_len=output_preprocecing(output_batch,voc_c)
    return inp,lengths,output,mask,max_label_len

训练函数

def train(input_var,input_len,label_var,max_label_len,mask,encoder,decoder,encoder_optimizer,decoder_optimizer,batch_size):
    #梯度归零
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()
    #set device
    input_var=input_var.to(device)
    label_var=label_var.to(device)
    mask=mask.to(device)
    #初始化一些变量
    loss=0
    encoder_hidden=torch.zeros(1, batch_size, encoder.hidden_size, device=device) #初始化编码器的隐层
    #编码器前向传播
    _,encoder_hidden=encoder(input_var,input_len,encoder_hidden)
    #解码器前向传播
    decoder_input=torch.LongTensor([[SOS_token for _ in range(batch_size)]]).to(device)
    decoder_hidden=encoder_hidden
    for i in range(max_label_len):
        decoder_output,decoder_hidden=decoder(decoder_input,decoder_hidden)
        topv,topi=decoder_output.topk(1)
        decoder_input=torch.LongTensor([[topi[j][0] for j in range(batch_size)]]).to(device)   #每个都取出了最大的,shape=[[tensor1,tensor2,tensor3...]]
        mask_loss=maskNLLLoss(decoder_output,label_var[i],mask[i])
        loss+=mask_loss
    loss.backward()
    encoder_optimizer.step()
    decoder_optimizer.step()
    return loss.item()

其中batch的实现是利用random随机抽取,但严格来说这样就无法实现epoch,官网也是这种做法。我找了其他实现方法,对于非等长的数据来说,貌似还没什么好的办法。整齐的数据,利用dataloader函数可轻松实现。

模型建立、训练、保存

读入数据并建立词库

english=Voc('英文')
chinese=Voc('中文')

df=pd.read_table('/Users/zhaoduidemac/python wordspace/nlp学习/sequence2sequence/cmn-eng/cmn.txt',header=None).iloc[:,:-1]
df.reset_index(drop=True, inplace=True)
df.columns=['inputs','targets']
df_pair=[]

max_chi_len=0

#往两个VOC中加句子,并形成pair
for i in range(len(df['inputs'])):
    eng=df['inputs'][i].split()
    chi=jieba.lcut(df['targets'][i])
    max_chi_len=max(0,len(chi))
    english.addSentence(eng)
    chinese.addSentence(chi)
    df_pair.append([df['inputs'][i],df['targets'][i]])

划分训练集和测试集,利用10折交叉运算进行一折然后break实现hold out

df_pair_array=np.array(df_pair)
kfold=KFold(n_splits=10,shuffle=False,random_state=random.seed(2020))
for train_index,test_index in kfold.split([i for i in range(len(df_pair_array))]):
    train_pair=df_pair_array[train_index].tolist()
    test_pair=df_pair_array[test_index].tolist()
    break

初始化一些参数和模块

#设置参数
hidden_size=256
batch_size=64
learning_rate=0.001
n_iteration=60000
embedding_eng=torch.nn.Embedding(len(english.index2word),hidden_size)
embedding_chi=torch.nn.Embedding(len(chinese.index2word),hidden_size)

#网络模块及损失函数
encoder=EncoderGRU(hidden_size,embedding_eng).to(device)
decoder=DecoderGRU(hidden_size,len(chinese.index2word),embedding_chi).to(device)
encoder_optimizer=torch.optim.Adam(encoder.parameters(),lr=learning_rate)
decoder_optimizer=torch.optim.Adam(decoder.parameters(),lr=learning_rate)

模型训练

for iteration in range(n_iteration):
    input_var,lengths,label_var,mask,max_label_len=data_preprocecing(english,chinese,[random.choice(train_pair) for _ in range(batch_size)])
    loss=train(input_var,lengths,label_var,max_label_len,mask,encoder,decoder,encoder_optimizer,decoder_optimizer,batch_size)
    print('Iteration:',iteration,'loss:',loss)

保存模型,利用torch最简单的保存方法,一块打包成“泡菜”

torch.save(encoder,'se2se_encoder.pt')
torch.save(decoder,'se2se_decoder.pt')

模型读取、预测

预测函数

#预测函数,这里只接收一句,不支持一个batch的预测
def predict(input_var,encoder,decoder,max_label_len,voc_chi):
    res=''
    encoder_hidden = torch.zeros(1, 1, encoder.hidden_size, device=device)  # 初始化编码器的隐层
    input_len=torch.tensor([len(input_var)])
    _, encoder_hidden = encoder(input_var, input_len, encoder_hidden)
    decoder_input=torch.LongTensor([[SOS_token]]).to(device)
    decoder_hidden=encoder_hidden
    for i in range(max_label_len):
        decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
        topv, topi = decoder_output.topk(1)
        decoder_input=torch.LongTensor([[topi.item()]])
        word=voc_chi.index2word[topi.item()]
        if word=='/n':
            break
        res+=word
    return res

模型的load与预测

encoder_load=torch.load('se2se_encoder.pt')
decoder_load=torch.load('se2se_decoder.pt')

for sentence in test_pair:
    input_var=torch.LongTensor([sentence2index_eng(english,sentence[0])]).view(-1,1)
    output=predict(input_var,encoder_load,decoder_load,max_chi_len,chinese)
    print('原始句子:',sentence,'预测翻译:',output)

最终结果

训练集翻译的可以(那是当然)。
测试集基本狗屁不通。据说翻译任务是nlp中对模型、数据综合质量要求最高的任务,可能还是数据太少吧。

但这套代码相对完整地实现了一个简单的nlp任务,仍然值得学习。

### 使用 Seq2Seq 模型实现中英翻译 #### 数据准备 为了构建一个有效的中英翻译模型,首先需要准备好合适的数据集。数据集中每条记录应包含一对平行语句:一句英语及其对应的汉语译文,两者之间通过制表符分隔[^1]。 #### 构建 Encoder 和 Decoder 在设计 Seq2Seq 结构时,通常会采用两个主要组件——Encoder 和 Decoder。其中,Encoder 负责接收源语言(即待翻译的语言,在此案例里是英语)作为输入,并将其转化为内部表示形式;Decoder 则依据这个内部状态来生成目标语言(这里是汉语)。对于 Encoder 来说,可以选择 LSTM 或者 GRU 这样的循环神经单元来进行特征抽取工作,从而得到能够反映句子含义的信息向量 C[^2]。 #### 添加注意力机制 引入注意力机制可以使解码过程更加灵活高效。具体而言,就是在每次预测下一个单词之前,都重新评估哪些部分的原文最为重要,进而调整权重分配给不同的位置上的隐藏层激活值。这种做法有助于提高长距离依赖关系的学习效果,使得最终产生的翻译质量更高。 #### 实现 Iterator 类 为了让训练流程顺利运行,还需要创建专门用来处理批量数据的对象。这里提到的一个名为 `Iterator` 的类就承担着这样的职责。该类不仅支持按需读取文件中的内容并划分成多个批次供后续操作使用,而且还能完成诸如按照序列长度排序、补足较短序列直至达到统一标准长度等一系列预处理任务,最后再把这些整理好的信息转交给框架内的张量对象以便于参与计算活动[^3]。 #### 训练阶段细节说明 在整个学习过程中,由于采用了完整的标签作为指导信号,因此可以在每一个时间步上直接比较预期输出与实际结果之间的差异,据此更新参数直到收敛为止。值得注意的是,这里的监督信息是以 one-hot 编码的形式给出的一系列类别标记,这实际上构成了一个多分类问题设置下的优化场景[^4]。 ```python import torch.nn as nn from torch.utils.data import DataLoader, Dataset class TranslationDataset(Dataset): def __init__(self, data_path): self.pairs = [] with open(data_path, 'r', encoding='utf8') as f: lines = f.readlines() for line in lines: en_text, zh_text = line.strip().split('\t') self.pairs.append((en_text, zh_text)) def __getitem__(self, index): return self.pairs[index] def __len__(self): return len(self.pairs) def collate_fn(batch_data): english_sentences = [item[0] for item in batch_data] chinese_sentences = [item[1] for item in batch_data] # 假设已经实现了tokenize函数和pad_sequence方法 src_tensor = tokenize(english_sentences).transpose(0, 1) tgt_tensor = pad_sequence(chinese_sentences) return src_tensor, tgt_tensor train_loader = DataLoader( dataset=TranslationDataset('path/to/your/dataset'), batch_size=64, shuffle=True, collate_fn=collate_fn ) ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值