使用Transformer中日机器翻译模型教程

使用Transformer和PyTorch进行中日机器翻译模型教程

一、背景介绍

1.1数据介绍

在本实验中,我们将使用从JParaCrawl[http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl] 下载的日英平行数据集。它被描述为“由NTT创建的最大公开可用的英日平行语料库,主要通过爬取网络并自动对齐平行句子。”你也可以在这里查看相关论文。

1.2云服务器介绍

本实验对计算能力要求较高,在GPU上运行,对显卡的配置需求高于普通笔记本。如有需要,可以观看以下链接租用云服务器进行本实验https://blog.csdn.net/nonosaysyes/article/details/140074095?spm=1001.2014.3001.5502。在本实验中,尝试了在pycharm调用云服务进行实验,以及直接使用云服务器对应的jupyter进行实验两种方式,大致形势一致,但也有小小的差异,会在后文提及。

二、案例实现

2.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,请在你自己的电脑上尝试运行这一套代码
print(device)
print(device(type='cuda'))

"""
python==3.8
torch>=1.11.0+cu113
numpy>=1.22.4
torchaudio==0.11.0
numpy>=1.20.0
matplotlib>=3.4.0
scikit-learn>=0.24.0
pandas>=1.3.0
jupyterlab>=3.2.0
tqdm>=4.62.0
sentencepiece==0.2.0
torchtext==0.6.0
"""

2.2数据预处理

2.2.1获取平行数据集
df = pd.read_csv('/root/autodl-tmp/nlp/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainen = df[2].values.tolist()#[:10000] # 获取英语句子
trainja = df[3].values.tolist()#[:10000]# 获取日语句子
# 删除数据集中最后一条数据,因为它有缺失值
# trainen.pop(5972)
# trainja.pop(5972)

在导入所有日语和英语对应的句子后,我删除了数据集中的最后一条数据,因为它有缺失值。训练集中共有5,973,071条句子,不过为了学习目的,通常建议抽样数据并确保一切正常,然后再一次性使用所有数据,以节省时间。

以下是数据集中包含的句子的示例。

# 打印第500条句子作为示例
print(trainen[500])
print(trainja[500])

在这里插入图片描述

我们也可以使用不同的平行数据集来跟随本文,确保我们能将数据处理成上面显示的两个字符串列表,包含日语和英语句子。

2.2.2准备分词器

与英语或其他字母语言不同,日语句子中没有空格来分隔单词。我们可以使用JParaCrawl提供的分词器,这些分词器使用SentencePiece为日语和英语创建。你可以访问JParaCrawl网站下载它们。

# 加载分词器
en_tokenizer = spm.SentencePieceProcessor(model_file='/root/autodl-tmp/nlp/spm.en.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='/root/autodl-tmp/nlp/spm.ja.nopretok.model')

加载分词器后,可以测试它们,例如,通过执行以下代码。

# 测试分词器
en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type=str)

在这里插入图片描述

ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')

在这里插入图片描述

2.2.3构建Vocab对象

使用分词器和原始句子,然后我们构建从TorchText导入的Vocab对象。这个过程可能需要几秒钟或几分钟,具体取决于数据集的大小和计算能力。不同的分词器也会影响构建词汇所需的时间,我尝试了几种其他的日语分词器,但SentencePiece似乎效果不错且速度足够快。

# 构建词汇对象
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) # 处理训练数据
2.2.4创建DataLoader对象

这里,我将BATCH_SIZE设置为16以防止“cuda out of memory”,但这取决于各种因素,如你的机器内存容量、数据大小等,因此请根据需要随时更改批量大小。

# 创建DataLoader对象
BATCH_SIZE = 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))
    en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
  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)

2.3确定模型——Seq2SeqTransformer

2.3.1Seq2SeqTransformer

接下来的代码和文字解释(用斜体表示)取自原始PyTorch教程[https://pytorch.org/tutorials/beginner/translation_transformer.html]。我没有做任何修改,除了将BATCH_SIZE和de_vocab改为ja_vocab。

Transformer是“Attention is all you need”论文中提出的一种用于解决机器翻译任务的Seq2Seq模型。Transformer模型由一个编码器和解码器块组成,每个块包含固定数量的层。

编码器通过一系列多头注意力和前馈网络层处理输入序列。编码器的输出称为记忆,与目标张量一起传递到解码器。编码器和解码器通过教师强制技术进行端到端训练。

from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)


# 定义Seq2SeqTransformer模型
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)
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)

        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)# 仅解码目标语言
2.3.2嵌入位置编码

文本标记通过使用标记嵌入来表示。位置编码添加到标记嵌入中,以引入单词顺序的概念。

# 嵌入位置编码
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) # 计算偶数位置的正弦编码
        pos_embedding[:, 1::2] = torch.cos(pos * den)# 计算奇数位置的余弦编码
        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),:])
# 定义嵌入类
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)
2.3.3掩码

我们创建一个后续单词掩码,以阻止目标单词关注其后续单词。我们还创建了掩码,用于掩盖源和目标填充标记。

# 生成方形掩码
def generate_square_subsequent_mask(sz):
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask
    
# 创建掩码(mask)函数
def create_mask(src, tgt):
  src_seq_len = src.shape[0] # 源语言序列长度
  tgt_seq_len = tgt.shape[0]# 目标语言序列长度

  tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
  src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

  src_padding_mask = (src == PAD_IDX).transpose(0, 1)# 生成源语言的填充掩码
  tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)# 生成目标语言的填充掩码
  return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask
2.3.4定义模型参数并实例化模型
# 定义模型参数
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)
# 初始化模型参数
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

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)

2.4开始训练

最后,在准备必要的类和函数之后,我们准备好训练我们的模型。虽然这无需多言,但完成训练所需的时间可能因计算能力、参数和数据集大小等诸多因素而异。

该图为使用pycharm远端连接云服务训练。

在这里插入图片描述

该图为直接使用云服务器中jupyter训练结果

在这里插入图片描述

2.5评估模型

尝试使用训练好的模型翻译一个日语句子。首先,我们创建翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理,然后将结果解码回一个句子,这次是英文的。

# 定义贪婪解码函数,用于生成目标句子
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函数并传递所需的参数。


# 测试翻译函数
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)

在这里插入图片描述

trainen.pop(5)

在这里插入图片描述

trainja.pop(5)

在这里插入图片描述

2.6保存词汇对象和训练好的模型

在训练完成后,我们将首先使用Pickle保存词汇对象(en_vocab和ja_vocab)。

import pickle
#保存英文词汇表
file = open('en_vocab.pkl', 'wb')
pickle.dump(en_vocab, file)
file.close()
# 保存日文词汇表
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()

最后,我们还可以使用PyTorch的保存和加载功能保存模型以供以后使用。通常,有两种方式保存模型,取决于我们以后要使用它们的目的。第一种是仅用于推理,我们可以稍后加载模型并使用它从日语翻译成英语。

# 保存模型用于推理
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')

在这里插入图片描述

  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值