基于Transformer和PyTorch的日中机器翻译模型研究

目录

摘要

一、编码器和解码器

二、训练模型

三、 预测不定长序列

四、评价翻译结果

BLEU分数的计算方法:

五、 中日翻译模型实现

5.1 数据准备

5.2 模型实现

5.3位置编码类(PositionalEncoding)

 5.4Token 嵌入类(TokenEmbedding)

5.5掩码生成函数(generate_square_subsequent_mask) 

5.6创建掩码(create_mask)

 5.7使用模型进行翻译尝试


摘要

本实验报告详细介绍了使用PyTorch框架和Transformer模型进行日中机器翻译的过程。实验使用了JParaCrawl数据集,并通过自定义的数据处理、模型构建、训练和翻译流程,实现了从日语到中文的机器翻译。实验结果表明,所构建的模型能够达到基本的翻译效果,但受限于计算资源,模型的翻译质量和效率有待进一步提升。

一、编码器和解码器

编码器使用GRU(门控循环单元)来处理输入序列并输出隐藏状态。解码器同样使用GRU,但它还包含一个全连接层来生成最终的输出。示例用法展示了如何初始化这些模型并进行前向传播。

import torch
import torch.nn as nn

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, dropout=0.5):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.rnn = nn.GRU(input_size, hidden_size, num_layers, dropout=dropout, batch_first=True)
        
    def forward(self, input_seq, hidden=None):
        # input_seq shape: (batch, seq_len, input_size)
        # hidden shape: (num_layers, batch, hidden_size)
        out, hidden = self.rnn(input_seq, hidden)
        # out shape: (batch, seq_len, hidden_size)
        return out, hidden

class DecoderRNN(nn.Module):
    def __init__(self, output_size, hidden_size, num_layers, dropout=0.5):
        super(DecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.rnn = nn.GRU(output_size, hidden_size, num_layers, dropout=dropout, batch_first=True)
        self.fc_out = nn.Linear(hidden_size, output_size)  # Fully connected layer to output size

    def forward(self, input_step, hidden, encoder_outputs):
        # input_step shape: (batch, output_size)
        # hidden shape: (num_layers, batch, hidden_size)
        # encoder_outputs shape: (batch, seq_len, hidden_size)
        out, hidden = self.rnn(input_step.unsqueeze(1), hidden)
        # out shape: (batch, 1, hidden_size)
        out = self.fc_out(out.squeeze(1))
        # out shape: (batch, output_size)
        return out, hidden

# Example usage:
# Define the model parameters
input_size = 10  # Size of the input layer (e.g., size of the vocabulary)
hidden_size = 20 # Size of the hidden layers
num_layers = 2   # Number of GRU layers
output_size = 10 # Size of the output layer (e.g., size of the vocabulary)

# Initialize the encoder and decoder
encoder = EncoderRNN(input_size, hidden_size, num_layers)
decoder = DecoderRNN(output_size, hidden_size, num_layers)

# Create a dummy input tensor of shape (batch, seq_len, input_size)
# For example, a batch of 3 sequences of length 5
dummy_input = torch.rand(3, 5, input_size)

# Forward pass through the encoder
encoder_out, encoder_hidden = encoder(dummy_input)

# Forward pass through the decoder (using zeros for the input and hidden states)
decoder_input = torch.zeros(3, output_size)  # Batch of 3, output size
decoder_hidden = torch.zeros(num_layers, 3, hidden_size)  # Num layers, batch, hidden size
output, decoder_hidden = decoder(decoder_input, decoder_hidden, encoder_out)

print("Encoder output shape:", encoder_out.shape)
print("Decoder output shape:", output.shape)

二、训练模型

在序列到序列模型的训练中,batch_loss函数用于计算一个批次序列的损失值。这个过程考虑了以下几个关键点:

  1. 起始标记(BOS):在解码序列的初始时间步,解码器的输入是一个特殊的起始标记(通常表示为BOS)。

  2. 强制教学(Teacher Forcing):在每个后续时间步,解码器的输入是目标序列在上一时间步的实际输出词。这种方法可以加速模型的收敛,但也可能导致模型对错误的容忍性降低。

  3. 掩码变量(Mask Variable):为了避免填充项(通常表示为PAD)对损失函数计算的影响,使用一个掩码变量来忽略这些填充项。掩码通常初始化为1,表示非填充项,对于填充项则为0。

import torch
import torch.nn as nn

# 定义批量损失函数
def batch_loss(encoder, decoder, X, Y, loss):
    batch_size = X.shape[0]  # 获取批次大小
    enc_state = encoder.begin_state()  # 初始化编码器的状态
    enc_outputs, enc_state = encoder(X, enc_state)  # 编码器前向传播
    
    # 初始化解码器的隐藏状态,使用编码器的最后一个状态
    dec_state = decoder.begin_state(enc_state)
    # 解码器在最初时间步的输入是BOS(开始标记)
    dec_input = torch.tensor([out_vocab.stoi[BOS]] * batch_size)
    
    # 初始化掩码,用于忽略填充项PAD的损失
    mask, num_not_pad_tokens = torch.ones(batch_size,), 0
    # 初始化损失
    l = torch.tensor([0.0])
    
    # 对于Y中的每个时间步
    for y in Y.permute(1, 0):  # Y的形状为(batch, seq_len)
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)  # 解码器前向传播
        # 计算损失,使用掩码避免计算填充项PAD的损失
        l = l + (mask * loss(dec_output, y)).sum()
        dec_input = y  # 使用强制教学,即下一个输入是当前的输出
        num_not_pad_tokens += mask.sum().item()  # 计算非填充项的数量
        # 一旦遇到EOS(结束标记),将mask置为0,忽略后续的损失计算
        mask = mask * (y != out_vocab.stoi[EOS]).float()
    
    # 返回平均损失
    return l / num_not_pad_tokens

# 定义训练函数
def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    # 初始化编码器和解码器的优化器
    enc_optimizer = torch.optim.Adam(encoder.parameters(), lr=lr)
    dec_optimizer = torch.optim.Adam(decoder.parameters(), lr=lr)
    
    # 定义损失函数,这里使用交叉熵损失,不进行自动求和
    loss = nn.CrossEntropyLoss(reduction='none')
    
    # 创建数据迭代器
    data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)
    
    # 训练循环
    for epoch in range(num_epochs):
        l_sum = 0.0  # 初始化损失总和
        for X, Y in data_iter:  # 遍历数据批次
            # 梯度清零
            enc_optimizer.zero_grad()
            dec_optimizer.zero_grad()
            
            # 计算损失并反向传播
            l = batch_loss(encoder, decoder, X, Y, loss)
            l.backward()
            # 更新模型参数
            enc_optimizer.step()
            dec_optimizer.step()
            
            # 累加损失
            l_sum += l.item()
        
        # 每10个epoch打印一次训练信息
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))

# 模型参数设置
embed_size, num_hiddens, num_layers = 64, 64, 2
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50

# 假设Encoder和Decoder类以及in_vocab和out_vocab已经定义
# encoder = Encoder(in_vocab_size, embed_size, num_hiddens, num_layers, drop_prob)
# decoder = Decoder(out_vocab_size, embed_size, num_hiddens, num_layers, attention_size, drop_prob)

# 假设dataset已经准备好
# train(encoder, decoder, dataset, lr, batch_size, num_epochs)

三、 预测不定长序列

贪婪搜索:

import torch

# 定义翻译函数
def translate(encoder, decoder, input_seq, max_seq_len):
    # 将输入序列拆分为单词列表,并添加EOS和PAD标记
    in_tokens = input_seq.split(' ')
    # 确保输入序列长度不超过max_seq_len
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    
    # 将输入序列转换为张量,并创建一个大小为1的批次
    enc_input = torch.tensor([[in_vocab.stoi[tk] for tk in in_tokens]])
    
    # 初始化编码器状态
    enc_state = encoder.begin_state()
    # 编码器前向传播
    enc_output, enc_state = encoder(enc_input, enc_state)
    
    # 解码器的初始输入是BOS标记
    dec_input = torch.tensor([out_vocab.stoi[BOS]])
    # 使用编码器的最终状态初始化解码器状态
    dec_state = decoder.begin_state(enc_state)
    
    # 初始化输出列表
    output_tokens = []
    
    # 循环生成翻译序列,直到生成EOS标记或达到最大序列长度
    for _ in range(max_seq_len):
        # 解码器前向传播
        dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
        # 选择概率最高的输出作为预测
        pred = dec_output.argmax(dim=1)
        # 将预测的索引转换为单词
        pred_token = out_vocab.itos[int(pred.item())]
        
        # 如果预测的标记是EOS,则翻译结束
        if pred_token == EOS:
            break
        else:
            # 将预测的单词添加到输出列表中
            output_tokens.append(pred_token)
            
            # 将预测的单词作为下一个时间步的输入
            dec_input = pred
            
    # 返回生成的翻译序列
    return output_tokens

# 假设in_vocab, out_vocab, EOS, PAD, Encoder, Decoder已经定义
# 假设encoder和decoder是已经训练好的模型实例
# input_seq = "输入的文本序列"
# max_seq_len = 你希望翻译的最大序列长度
# output_seq = translate(encoder, decoder, input_seq, max_seq_len)

四、评价翻译结果

BLEU(Bilingual Evaluation Understudy)是一种评估机器翻译质量的度量,特别是它如何接近人类翻译的程度。它主要关注两个方面:子序列的匹配精度和预测序列的长度。

BLEU分数的计算方法:

  1. 子序列精度(pn​):对于长度为 n 的子序列,pn​ 是预测序列中与标签序列(参考翻译)匹配的 n-gram数量与预测序列中 n-gram总数的比值。

  2. BLEU公式: BLEU=exp(∑n=1k​n1​logpn​)×BP 其中,k 是我们希望匹配的子序列的最大词数(通常是2或4),pn​ 是第 n 个长度子序列的精度,BP是长度惩罚项。

  3. 长度惩罚(BP,Brevity Penalty): BP=exp⁡(1−lenpredlenref)BP=exp(1−lenref​lenpred​​) 这里,lenpredlenpred​ 是预测序列的长度,而 lenreflenref​ 是标签序列的长度。这个惩罚项确保了BLEU分数不会偏向较短的输出。

BLEU分数范围从0到1,1表示完美的匹配,而0表示没有重叠的子序列。BLEU分数是一个有用的指标,但它也有局限性,比如它不能很好地评估语义的准确性和流畅性。因此,在实际应用中,通常需要结合其他指标和人工评估来全面评价机器翻译的质量。

import math
import collections

# 定义计算BLEU分数的函数
def bleu(pred_tokens, label_tokens, k):
    # 计算预测序列和标签序列的长度
    len_pred, len_label = len(pred_tokens), len(label_tokens)
    
    # 基础BLEU分数计算,考虑长度惩罚
    score = math.exp(min(0, 1 - len_label / len_pred))
    
    # 对于不同长度的n-gram进行计算
    for n in range(1, k + 1):
        num_matches, label_subs = 0, collections.defaultdict(int)  # 初始化匹配数和标签子序列计数器
        
        # 计算标签中的n-gram出现次数
        for i in range(len_label - n + 1):
            label_subs[''.join(label_tokens[i: i + n])] += 1
        
        # 计算预测序列和标签序列n-gram的匹配数
        for i in range(len_pred - n + 1):
            if label_subs[''.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[''.join(pred_tokens[i: i + n])] -= 1
        
        # 更新BLEU分数,使用BP(brevity penalty)避免长度惩罚
        score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
    
    return score

# 定义评分函数,使用翻译模型和BLEU分数计算
def score(input_seq, label_seq, k, encoder, decoder, max_seq_len):
    # 使用翻译模型进行翻译
    pred_tokens = translate(encoder, decoder, input_seq, max_seq_len)
    
    # 将标签序列拆分为单词列表
    label_tokens = label_seq.split(' ')
    
    # 计算BLEU分数
    bleu_score = bleu(pred_tokens, label_tokens, k)
    
    # 打印BLEU分数和预测的翻译结果
    print('bleu %.3f, predict: %s' % (bleu_score, ' '.join(pred_tokens)))

# 假设translate函数、encoder、decoder、max_seq_len已经定义
# 假设input_seq是输入的文本序列,label_seq是对应的标签序列,k是BLEU分数计算的n-gram的最大长度
# score('ils regardent .', 'they are watching .', k=2)
# score('ils sont canadienne .', 'they are canadian .', k=2)

五、 中日翻译模型实现

5.1 数据准备

import math
import torchtext
import torch
import torch.nn as nn
from torch import Tensor
from torch.nn.utils.rnn import pad_sequence
from torch.utils.data import DataLoader
from collections import Counter
from torchtext.vocab import Vocab
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer
import io
import time
import pandas as pd
import numpy as np
import pickle
import tqdm
import sentencepiece as spm
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码
import pandas as pd
# 读取以制表符分隔的文件
df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
 
# 将特定列的值提取到列表中
trainen = df[2].values.tolist()  # 提取第2列
trainja = df[3].values.tolist()  # 提取第3列
# trainen.pop(5972)
# trainja.pop(5972)

5.2 模型实现

import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.vocab import vocab
from collections import Counter
import sentencepiece as spm
import math
from tqdm import tqdm

# 设备配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# SentencePiece 分词
def load_tokenizer(model_path):
    tokenizer = spm.SentencePieceProcessor()
    tokenizer.load(model_path)
    return tokenizer

# 构建词汇表
def build_vocab(sentences, tokenizer):
    counter = Counter()
    for sentence in sentences:
        counter.update(tokenizer.encode(sentence))
    vocab_obj = vocab(counter, specials=['<pad>', '<bos>', '<eos>', '<unk>'])
    return vocab_obj

# 数据预处理
def data_process(ja_sentences, en_sentences, ja_tokenizer, en_tokenizer, ja_vocab, en_vocab):
    data = []
    for ja, en in zip(ja_sentences, en_sentences):
        ja_indices = [ja_vocab[token] for token in ja_tokenizer.encode(ja)]
        en_indices = [en_vocab[token] for token in en_tokenizer.encode(en)]
        data.append((torch.tensor(ja_indices), torch.tensor(en_indices)))
    return data

5.3位置编码类(PositionalEncoding)

class PositionalEncoding(nn.Module):
    def __init__(self, emb_size, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        # 为每个位置/时间步骤生成一个位置编码向量
        self.pos_encoding = torch.zeros(max_len, emb_size)
        
        # 位置编码的分母部分,用于生成正弦和余弦函数的频率
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, emb_size, 2).float() * (-math.log(10000.0) / emb_size))
        
        # 填充位置编码矩阵
        self.pos_encoding[:, 0::2] = torch.sin(position * div_term)
        self.pos_encoding[:, 1::2] = torch.cos(position * div_term)
        # 使位置编码的维度与嵌入向量维度匹配
        self.pos_encoding = self.pos_encoding.unsqueeze(0).transpose(0, 1)
        
        self.dropout = nn.Dropout(p=dropout)
        self.emb_size = emb_size

    def forward(self, token_embedding):
        """
        将位置编码添加到token_embedding中,并应用dropout。
        
        参数:
            token_embedding (Tensor): [seq_len, batch_size, emb_size]
        
        返回:
            Tensor: [seq_len, batch_size, emb_size] 包含位置编码的token_embedding
        """
        # 确保位置编码与token_embedding具有相同的batch大小和嵌入维度
        pos_encoding = self.pos_encoding[:, :token_embedding.size(1), :]
        
        # 将位置编码加到token_embedding上,并应用dropout
        x = token_embedding + pos_encoding
        x = self.dropout(x)
        return x

 5.4Token 嵌入类(TokenEmbedding)

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size, emb_size):
        super(TokenEmbedding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, emb_size)

    def forward(self, tokens):
        # 返回 token 的嵌入表示
        return self.embedding(tokens)

5.5掩码生成函数(generate_square_subsequent_mask) 

def generate_square_subsequent_mask(sz, device):
    # 创建一个上三角矩阵,上三角部分为 0,其余部分为 1
    mask = torch.triu(torch.ones(sz, sz), diagonal=1).to(device)
    # 将上三角矩阵置为负无穷,其余部分置为0
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

5.6创建掩码(create_mask)

def create_mask(src, tgt):
    src_seq_len = src.size(0)
    tgt_seq_len = tgt.size(0)
    
    # 为目标序列生成下三角掩码
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len, src.device)
    
    # 为源序列生成全零掩码
    src_mask = torch.zeros(src_seq_len, src_seq_len, device=src.device)
    
    # 为源序列和目标序列生成填充掩码,填充位置为 1,其余位置为 0
    src_padding_mask = (src == src_pad_idx).unsqueeze(1).unsqueeze(2)
    tgt_padding_mask = (tgt == tgt_pad_idx).unsqueeze(1).unsqueeze(2)
    
    return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

 5.7使用模型进行翻译尝试

import torch

# 设备配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Greedy解码函数
def greedy_decode(model, src, src_mask, max_len, start_symbol):
    # 编码器处理源序列
    memory = model.encode(src, src_mask)
    # 开始解码,初始化解码器输入为起始符号
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
    for i in range(max_len - 1):
        out = model.decode(ys, memory)
        # 从解码器的输出中获取概率最高的下一个词的概率分布
        prob = model.generator(out)
        # 选择概率最高的词作为下一个词
        _, next_word = torch.max(prob, dim=1)
        next_word = next_word.item()
        # 将选中的词添加到解码序列中
        ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)
        # 如果选中的是结束符号,则结束解码
        if next_word == EOS_IDX:
            break
    return ys

# 翻译函数
def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    model.eval()  # 设置模型为评估模式
    # 对源序列进行分词并转换为索引
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]
    # 将索引转换为张量
    src = torch.LongTensor(tokens).reshape(len(tokens), 1).to(device)
    # 创建源序列掩码
    src_mask = (torch.ones_like(src) == 1).to(device)
    # 进行解码
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=len(tokens) + 5, start_symbol=BOS_IDX).flatten()
    # 将解码的索引转换为目标语言的词汇
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens if tok not in {BOS_IDX, EOS_IDX}])

# 示例使用
# 假设 transformer, src_vocab, tgt_vocab, src_tokenizer 等已定义
# src = "Hello, world!"
# translated = translate(transformer, src, src_vocab, tgt_vocab, src_tokenizer)
# print(translated)
  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值