基于Transformer实现机器翻译(日译中)

一、引言

在自然语言处理(NLP)领域,机器翻译是一项重要且具有挑战性的任务。近年来,基于Transformer的模型在机器翻译任务中表现出色。本文将详细介绍如何使用PyTorch、Torchtext、SentencePiece以及Jupyter Notebook构建一个日语到中文的机器翻译模型。

二、原理解释

1.Transformer模型的基础概念

Transformer模型是由Vaswani等人在2017年提出的,是一种基于注意力机制的序列到序列(Seq2Seq)模型。它主要由编码器和解码器组成,每个部分包含多个层。

编码器和解码器的结构

  • 编码器(Encoder):编码器由多个相同的层组成,每一层包含两个子层:

    1. 多头自注意力机制(Multi-Head Self-Attention Mechanism):该机制允许模型在不同的子空间中关注输入序列的不同部分,从而捕捉到更丰富的上下文信息。
    2. 前馈神经网络(Feed-Forward Neural Network):每个位置的表示通过两个线性变换和一个ReLU激活函数进行处理。
  • 解码器(Decoder):解码器也由多个相同的层组成,每一层包含三个子层:

    1. 多头自注意力机制:与编码器类似,但只关注目标序列中已经生成的部分。
    2. 编码器-解码器注意力机制(Encoder-Decoder Attention Mechanism):该机制允许解码器在生成每个单词时关注编码器的输出。
    3. 前馈神经网络:与编码器中的前馈神经网络相同。

多头自注意力机制

自注意力机制的核心思想是通过计算输入序列中每个位置与其他位置之间的相似度,来捕捉全局上下文信息。多头自注意力机制通过并行地计算多个自注意力,从不同的子空间中提取信息。

计算过程包括三个步骤:

  1. 计算查询(Query)、键(Key)和值(Value)矩阵。
  2. 计算查询和键的点积,并进行缩放和归一化,得到注意力权重。
  3. 使用注意力权重对值进行加权求和,得到最终的输出。

位置编码

由于Transformer模型没有卷积和循环结构,无法直接捕捉序列的位置信息。为了解决这个问题,Transformer通过位置编码(Positional Encoding)将位置信息加入到输入嵌入中。位置编码使用正弦和余弦函数生成,具有周期性,能够捕捉到不同粒度的位置信息。

2.Transformer在机器翻译中的应用

在机器翻译任务中,Transformer模型的工作流程如下:

  1. 编码器处理输入序列:编码器接收源语言的输入序列,通过多个编码器层进行处理,生成上下文表示(记忆)。
  2. 解码器生成翻译结果:解码器接收目标语言的输入序列(通常是已经生成的部分),通过多个解码器层进行处理,同时利用编码器的上下文表示,生成最终的翻译结果。

通过这种方式,Transformer能够高效地捕捉源语言和目标语言之间的复杂关系,实现高质量的翻译。

三、实现过程

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')

2.获取并处理平行语料库

使用JParaCrawl提供的日语-中文平行语料库。该语料库是目前公开的最大日中平行语料库之一。我们可以通过以下代码读取并处理数据:

df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)  # 使用pandas读取制表符分隔的文件,不带标题行
trainen = df[2].values.tolist()  # 提取第3列的值并转换为列表
trainja = df[3].values.tolist()  # 提取第4列的值并转换为列表

3.准备分词器

由于日语句子中没有空格,直接使用SentencePiece进行分词。我们可以从JParaCrawl网站下载分词模型:

en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')  # 加载英语的SentencePiece模型
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')  # 加载日语的SentencePiece模型

4.构建词汇表并转换为张量

使用Torchtext的Vocab对象构建词汇表:

def build_vocab(sentences, tokenizer):
  counter = Counter()  # 初始化一个计数器
  for sentence in sentences:  # 遍历所有句子
    counter.update(tokenizer.encode(sentence, out_type=str))  # 使用分词器对句子进行编码,并更新计数器
  return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])  # 创建词汇表,包含特殊符号

ja_vocab = build_vocab(trainja, ja_tokenizer)  # 使用日语训练数据和日语分词器构建词汇表
en_vocab = build_vocab(trainen, en_tokenizer)  # 使用英语训练数据和英语分词器构建词汇表

将句子转换为张量:

def data_process(ja, en):
  data = []  # 初始化数据列表
  for (raw_ja, raw_en) in zip(ja, en):  # 遍历日语和英语句子的对
    ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                            dtype=torch.long)  # 对每个日语句子进行编码并转换为张量
    en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                            dtype=torch.long)  # 对每个英语句子进行编码并转换为张量
    data.append((ja_tensor_, en_tensor_))  # 将日语张量和英语张量作为元组添加到数据列表中
  return data

train_data = data_process(trainja, trainen)  # 处理训练数据,生成训练数据集

5.创建DataLoader对象

BATCH_SIZE = 8  # 设置批次大小为8
PAD_IDX = ja_vocab['<pad>']  # 获取填充符号的索引
BOS_IDX = ja_vocab['<bos>']  # 获取序列开始符号的索引
EOS_IDX = ja_vocab['<eos>']  # 获取序列结束符号的索引

def generate_batch(data_batch):
  ja_batch, en_batch = [], []  # 初始化日语和英语批次列表
  for (ja_item, en_item) in data_batch:  # 遍历数据批次中的每个元组
    ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))  # 为每个日语句子添加BOS和EOS
    en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))  # 为每个英语句子添加BOS和EOS
  ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)  # 对日语批次进行填充
  en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)  # 对英语批次进行填充
  return ja_batch, en_batch  # 返回填充后的日语和英语批次

train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,  # 创建数据加载器
                        shuffle=True, collate_fn=generate_batch)  # 打乱数据,并使用generate_batch作为整理函数

6.构建Seq2Seq Transformer模型

class Seq2SeqTransformer(nn.Module):
    def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
                 emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
                 dim_feedforward:int = 512, dropout:float = 0.1):
        super(Seq2SeqTransformer, self).__init__()
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)  # 定义Transformer编码器层
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)  # 定义Transformer编码器
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)  # 定义Transformer解码器层
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)  # 定义Transformer解码器

        self.generator = nn.Linear(emb_size, tgt_vocab_size)  # 定义线性层,用于生成最终的词汇表分布
        self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)  # 定义源语言的嵌入层
        self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)  # 定义目标语言的嵌入层
        self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)  # 定义位置编码层

    def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
                tgt_mask: Tensor, src_padding_mask: Tensor,
                tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
        src_emb = self.positional_encoding(self.src_tok_emb(src))  # 对源语言进行嵌入和位置编码
        tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))  # 对目标语言进行嵌入和位置编码
        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)  # 编码器处理源语言嵌入
        outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
                                        tgt_padding_mask, memory_key_padding_mask)  # 解码器处理目标语言嵌入和编码器记忆
        return self.generator(outs)  # 生成最终的词汇表分布

    def encode(self, src: Tensor, src_mask: Tensor):
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)  # 编码器处理源语言嵌入和位置编码

    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)  # 解码器处理目标语言嵌入、编码器记忆和位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
        super(PositionalEncoding, self).__init__()
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)  # 计算位置编码中的除数
        pos = torch.arange(0, maxlen).reshape(maxlen, 1)  # 生成位置序列
        pos_embedding = torch.zeros((maxlen, emb_size))  # 初始化位置嵌入矩阵
        pos_embedding[:, 0::2] = torch.sin(pos * den)  # 在偶数维度应用sin函数
        pos_embedding[:, 1::2] = torch.cos(pos * den)  # 在奇数维度应用cos函数
        pos_embedding = pos_embedding.unsqueeze(-2)  # 在最后一维添加一个新的维度

        self.dropout = nn.Dropout(dropout)  # 定义dropout层
        self.register_buffer('pos_embedding', pos_embedding)  # 注册位置嵌入为持久缓冲区

    def forward(self, token_embedding: Tensor):
        return self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0),:])  # 将位置嵌入添加到输入的token嵌入上,并应用dropout

class TokenEmbedding(nn.Module):
    def __init__(self, vocab_size: int, emb_size):
        super(TokenEmbedding, self).__init__()
        self.embedding = nn.Embedding(vocab_size, emb_size)  # 定义嵌入层
        self.emb_size = emb_size
    def forward(self, tokens: Tensor):
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)  # 返回嵌入向量,并乘以嵌入维度的平方根进行缩放

7.训练模型

SRC_VOCAB_SIZE = len(ja_vocab)  # 设置源语言词汇表的大小
TGT_VOCAB_SIZE = len(en_vocab)  # 设置目标语言词汇表的大小
EMB_SIZE = 512  # 嵌入维度
NHEAD = 8  # 多头注意力机制中头的数量
FFN_HID_DIM = 512  # 前馈神经网络的隐藏层维度
BATCH_SIZE = 16  # 批次大小
NUM_ENCODER_LAYERS = 3  # 编码器层数
NUM_DECODER_LAYERS = 3  # 解码器层数
NUM_EPOCHS = 16  # 训练轮数
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)  # 初始化Seq2Seq Transformer模型

for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)  # 对参数进行Xavier均匀初始化

transformer = transformer.to(device)  # 将模型加载到设备上

loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)  # 定义交叉熵损失函数,忽略填充索引

optimizer = torch.optim.Adam(
    transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
)  # 定义Adam优化器

def train_epoch(model, train_iter, optimizer):
  model.train()  # 将模型设置为训练模式
  losses = 0
  for idx, (src, tgt) in  enumerate(train_iter):
      src = src.to(device)  # 将源序列加载到设备上
      tgt = tgt.to(device)  # 将目标序列加载到设备上

      tgt_input = tgt[:-1, :]  # 目标输入序列,不包括最后一个时间步

      src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)  # 创建掩码

      logits = model(src, tgt_input, src_mask, tgt_mask,
                                src_padding_mask, tgt_padding_mask, src_padding_mask)  # 前向传播

      optimizer.zero_grad()  # 清零梯度

      tgt_out = tgt[1:,:]  # 目标输出序列,不包括第一个时间步
      loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  # 计算损失
      loss.backward()  # 反向传播

      optimizer.step()  # 更新参数
      losses += loss.item()  # 累计损失
  return losses / len(train_iter)  # 返回平均损失

def evaluate(model, val_iter):
  model.eval()  # 将模型设置为评估模式
  losses = 0
  for idx, (src, tgt) in (enumerate(valid_iter)):
    src = src.to(device)  # 将源序列加载到设备上
    tgt = tgt.to(device)  # 将目标序列加载到设备上

    tgt_input = tgt[:-1, :]  # 目标输入序列,不包括最后一个时间步

    src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)  # 创建掩码

    logits = model(src, tgt_input, src_mask, tgt_mask,
                              src_padding_mask, tgt_padding_mask, src_padding_mask)  # 前向传播
    tgt_out = tgt[1:,:]  # 目标输出序列,不包括第一个时间步
    loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))  # 计算损失
    losses += loss.item()  # 累计损失
  return losses / len(val_iter)  # 返回平均损失

开始训练:

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):  # 使用tqdm显示训练进度条,遍历每个训练轮次
  start_time = time.time()  # 记录当前时间,作为训练开始时间
  train_loss = train_epoch(transformer, train_iter, optimizer)  # 调用train_epoch函数进行训练,返回训练损失
  end_time = time.time()  # 记录当前时间,作为训练结束时间
  print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "  # 打印当前轮次,训练损失和训练时间
          f"Epoch time = {(end_time - start_time):.3f}s"))  # 计算并打印每轮次训练所需时间

 

 

8.进行翻译

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    src = src.to(device)  # 将源序列加载到设备上
    src_mask = src_mask.to(device)  # 将源序列掩码加载到设备上
    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):  # 遍历最大长度减一的序列
        memory = memory.to(device)  # 将记忆加载到设备上
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)  # 创建记忆掩码
        tgt_mask = (generate_square_subsequent_mask(ys.size(0))
                                    .type(torch.bool)).to(device)  # 生成目标序列的掩码
        out = model.decode(ys, memory, tgt_mask)  # 解码器处理当前目标序列和记忆
        out = out.transpose(0, 1)  # 转置输出
        prob = model.generator(out[:, -1])  # 生成最后一个时间步的概率分布
        _, 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]  # 对源序列进行编码并添加起始和结束符号
    num_tokens = len(tokens)  # 获取令牌数量
    src = (torch.LongTensor(tokens).reshape(num_tokens, 1))  # 将令牌转换为张量并调整形状
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)  # 创建源序列掩码
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()  # 使用贪婪解码生成目标序列
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")  # 将目标序列转换为文本并去掉起始和结束符号
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)  
# 使用transformer模型将日语句子翻译为英语,传入日语词汇表、英语词汇表和日语分词器

(这里使用的是英语-日语数据集,所以效果不佳,可以更改成中文数据集,使结果准确率更高) 

 

9.保存词汇表和模型

import pickle
# open a file, where you want to store the data
file = open('en_vocab.pkl', 'wb')  # 打开一个文件,用于存储数据(英语词汇表)
# dump information to that file
pickle.dump(en_vocab, file)  # 将英语词汇表序列化并写入文件
file.close()  # 关闭文件
file = open('ja_vocab.pkl', 'wb')  # 打开一个文件,用于存储数据(日语词汇表)
pickle.dump(ja_vocab, file)  # 将日语词汇表序列化并写入文件
file.close()  # 关闭文件
torch.save(transformer.state_dict(), 'inference_model')
torch.save({
  'epoch': NUM_EPOCHS,  # 保存训练轮数
  'model_state_dict': transformer.state_dict(),  # 保存模型的状态字典
  'optimizer_state_dict': optimizer.state_dict(),  # 保存优化器的状态字典
  'loss': train_loss,  # 保存训练损失
  }, 'model_checkpoint.tar')  # 将以上信息保存到文件 'model_checkpoint.tar'

四、小结

通过本文的详细步骤,我们成功地构建了一个基于Transformer的日语-中文机器翻译模型。这个模型利用了Transformer的强大能力,能够高效地捕捉源语言和目标语言之间的复杂关系。虽然我们使用的硬件资源有限,但通过合理的参数设置和优化,我们仍然能够取得不错的翻译效果。

未来的改进方向包括:

  • 使用更大的数据集和更长的训练时间以提高模型的翻译质量。
  • 尝试不同的优化算法和超参数调优。
  • 引入更多的上下文信息,如句法和语义信息,以进一步提升模型性能。

通过不断的优化和改进,我们相信基于Transformer的机器翻译模型将会在实际应用中发挥越来越重要的作用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值