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

1. 引言

1.1 实验简介

在现代自然语言处理(Natural Language Process, NLP)领域,机器翻译(Machine Translation, MT)是一个极具挑战性和重要性的任务。随着深度学习的进步,特别是Transformer模型的引入,机器翻译的效果得到了显著提升。本实验旨在尝试如何使用Transformer模型实现日文到中文的机器翻译。本实验报告会从数据准备、模型构建、训练和推理几个方面来进行介绍。

1.2 实验环境

  • Windows 10
  • AutoDL NVIDIA RTX 3090
  • jupyter notebook
  • Python 3.8
  • PyTorch 1.11
  • sentencepiece 0.2.0
  • torchtext 0.6.0

1.3 数据集和分词器下载链接

数据集和分词器下载链接, 提取码:qgj3

2. Transformer模型的主要架构

Transformer模型由Vaswani等人在2017年提出,是一种基于自注意力机制的深度学习模型。与传统的循环神经网络(RNN)和卷积神经网络(CNN)不同,Transformer不依赖序列的顺序处理输入数据,而是通过自注意力机制同时处理整个序列。这一特性使得Transformer在处理长距离依赖关系和并行计算方面具有显著优势。自提出以来,Transformer已经在机器翻译、文本生成、语音识别等多个NLP任务中得到了广泛应用和验证。

图2.1 Transformer模型架构

  1. 输入嵌入(Input Embedding):将源语言和目标语言的单词转化为向量表示
  2. 位置编码(Positional Encoding):为每个输入向量添加位置信息
  3. 编码器(Encoder ×N):由多个编码器层组成,每层包括多头自注意力机制前馈神经网络
  4. 解码器(Decoder ×N):由多个解码器层组成,每层包括掩蔽多头自注意力机制编码器-解码器注意力机制(交叉注意力机制)前馈神经网络
  5. 输出生成(Output Generation):解码器的输出通过线性变换Softmax层生成目标语言的翻译结果。

位置编码(Positional Encoding)

由于Transformer模型不依赖于RNN的顺序计算,因此需要通过位置编码(Positional Encoding)为每个输入标记提供位置信息。位置编码通过正弦和余弦函数生成,并添加到输入嵌入中,使模型能够识别序列中各个位置的信息。

通过上述结构,Transformer模型能够高效地捕捉序列中的全局依赖关系,并生成高质量的翻译结果。

编码器(Encoder)和解码器(Decoder)

Transformer模型主要由编码器(Encoder)和解码器(Decoder)两部分组成,每个部分都包含多个相同的层(Layer)。每一层又包括以下两个子层:

  1. 多头自注意力机制(Multi-Head Self-Attention Mechanism):通过多个注意力头来捕捉序列中不同位置之间的关系,使模型能够从不同子空间中提取信息
  2. 前馈神经网络(Feed Forward Neural Network, FFN):对每个位置上的表示进行进一步的非线性变换

此外,每一层还包含两个重要的组件:

  • 残差连接和层归一化(Residual Connection and Layer Normalization):通过残差连接使信息在层间流动更为顺畅,并通过层归一化加速模型收敛,提高训练稳定性。

在后续的实验中,我们会使用代码详细设计Transformer模型。

3. 实验步骤

3.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
from tqdm import tqdm
import sentencepiece as spm
torch.manual_seed(0)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

device

运行结果:

device(type='cuda')

3.2 获取并行数据集

使用数据集:ch-ja.bicleaner05.txt

图3.2.1 数据集截图

使用如下代码导入数据集:

df = pd.read_csv('./ch-ja/ch-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainch = df[2].values.tolist()  # 提取第三列作为中文训练数据
trainja = df[3].values.tolist()  # 提取第四列作为日文训练数据
# 如果需要限制数据量,可以取消以下两行代码的注释并调整索引范围
# trainch = trainch[:10000]
# trainja = trainja[:10000]

在导入所有日文和中文对应数据后,我们删除了数据集中的最后一个数据,因为它缺少值。总的来说,trainch 和 trainja 中的句子数为 5,973,071,但是,出于学习目的,通常建议在一次性使用所有数据之前对数据进行采样并确保一切按预期工作,以节省时间。

下面是数据集中包含的句子示例:

print(trainch[500])  # 打印第 501 个中文训练数据点
print(trainja[500])  # 打印第 501 个日文训练数据点

运行结果:

Chinese HS Code Harmonized Code System < HS编码 2905 无环醇及其卤化、磺化、硝化或亚硝化衍生物 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...
Japanese HS Code Harmonized Code System < HSコード 2905 非環式アルコール並びにそのハロゲン化誘導体、スルホン化誘導体、ニトロ化誘導体及びニトロソ化誘導体 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...

3.3 准备分词器

ch_tokenizer = spm.SentencePieceProcessor(model_file='chja_spm_models/spm.ch.nopretok.model')  # 中文句子分词器,加载模型文件
ja_tokenizer = spm.SentencePieceProcessor(model_file='chja_spm_models/spm.ja.nopretok.model')  # 日文句子分词器,加载模型文件

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

# 使用中文句子分词器编码输入句子为字符串形式
ch_tokenizer.encode("所有居住在日本的20至59岁的居民都必须参加公共养老金制度。", out_type=str)
print()
# 使用日文句子分词器编码输入句子为字符串形式
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type=str)

运行结果:

['▁',
 '所',
 '有',
 '居',
 '住',
 '在',
 '日',
 '本',
 '的',
 '20',
 '至',
 '59',
 '岁',
 '的',
 '居',
 '民',
 '都',
 '必',
 '须',
 '参',
 '加',
 '公',
 '共',
 '养',
 '老',
 '金',
 '制',
 '度',
 '。']

 ['▁',
 '年',
 '金',
 '▁日本',
 'に住んでいる',
 '20',
 '歳',
 '~',
 '60',
 '歳の',
 '全ての',
 '人は',
 '、',
 '公的',
 '年',
 '金',
 '制度',
 'に',
 '加入',
 'しなければなりません',
 '。']

3.4 构建TorchText Vocab对象并将句子转换为Torch张量

然后,使用分词器和原始句子,我们构建从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>'])
  # 使用计数器创建词汇表对象,特殊标记包括未知词(<unk>)、填充标记(<pad>)、开始标记(<bos>)、结束标记(<eos>)

ja_vocab = build_vocab(trainja, ja_tokenizer)  # 构建日语词汇表
ch_vocab = build_vocab(trainch, ch_tokenizer)  # 构建汉语词汇表
print(f'the length of Japanese vocabulary is {len(ja_vocab)}')
print(f'the length of Chinese vocabulary is {len(ch_vocab)}')

运行结果:

the length of Japanese vocabulary is 24058
the length of Chinese vocabulary is 18747

在有了词汇表对象之后,我们可以使用词汇表和分词器对象来构建训练数据的张量:

def data_process(ja, en):
  data = []
  for (raw_ja, raw_en) in zip(ja, en):
    # 对日语句子进行编码,并将结果转换为PyTorch张量
    ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                              dtype=torch.long)
    # 对汉语句子进行编码,并将结果转换为PyTorch张量
    ch_tensor_ = torch.tensor([ch_vocab[token] for token in ch_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                              dtype=torch.long)
    data.append((ja_tensor_, ch_tensor_))  # 将编码后的句子对添加到数据列表中
  return data

train_data = data_process(trainja, trainch)  # 处理训练数据,得到编码后的数据集

3.5 创建要在训练期间迭代的DataLoader对象

在这里,我将BATCH_SIZE设置为 16 以防止“cuda 内存不足”,但这取决于各种因素,例如您的机器内存容量、数据大小等,因此请根据需要随意更改批处理大小。

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, ch_batch = [], []
  for (ja_item, ch_item) in data_batch:
    # 在日语句子的开头和结尾添加开始和结束标记,并连接成一个张量序列
    ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
    # 在英语句子的开头和结尾添加开始和结束标记,并连接成一个张量序列
    ch_batch.append(torch.cat([torch.tensor([BOS_IDX]), ch_item, torch.tensor([EOS_IDX])], dim=0))
  
  # 使用pad_sequence函数对句子序列进行填充,使用PAD_IDX作为填充值
  ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
  ch_batch = pad_sequence(ch_batch, padding_value=PAD_IDX)
  return ja_batch, ch_batch

# 创建数据加载器,每次返回一个批量的数据,通过generate_batch函数处理
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

3.6 Transformer模型的实现

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

3.6.1 词嵌入和位置编码

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

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))
        # 使用sin和cos函数计算位置编码矩阵的值
        pos_embedding[:, 0::2] = torch.sin(pos * den)      # 下标为偶数的用正弦函数进行位置编码
        pos_embedding[:, 1::2] = torch.cos(pos * den)      # 下标为奇数的用余弦函数进行位置编码
        # 增加一个维度以匹配张量形状
        pos_embedding = pos_embedding.unsqueeze(-2)

        # 初始化Dropout层和位置编码张量
        self.dropout = nn.Dropout(dropout)
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        # 将位置编码张量加到输入的词嵌入张量上,并应用Dropout
        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):
        # 返回词嵌入张量,并乘以sqrt(emb_size)以缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

3.6.2 编码器和解码器

编码器通过一系列多头注意力和前馈网络层传播输入序列来处理输入序列。编码器的输出称为memory,与目标张量一起馈送到解码器。编码器和解码器使用教师强制(Teacher Forcing)技术以端到端的方式进行训练。

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


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__()
        # 定义编码器层,使用TransformerEncoderLayer
        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)
        # 定义解码器层,使用TransformerDecoderLayer
        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)

        # 线性层用于生成最终的输出词汇表大小的张量
        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):
        """
        Transformer模型的前向传播过程

        Args:
        - src (Tensor): 输入源序列张量,形状为 (batch_size, src_seq_len)
        - trg (Tensor): 输入目标序列张量,形状为 (batch_size, tgt_seq_len)
        - src_mask (Tensor): 输入源序列的掩码张量,形状为 (src_seq_len, src_seq_len)
        - tgt_mask (Tensor): 输入目标序列的掩码张量,形状为 (tgt_seq_len, tgt_seq_len)
        - src_padding_mask (Tensor): 输入源序列的填充掩码张量,形状为 (batch_size, src_seq_len)
        - tgt_padding_mask (Tensor): 输入目标序列的填充掩码张量,形状为 (batch_size, tgt_seq_len)
        - memory_key_padding_mask (Tensor): 内存(源序列)填充掩码张量,形状为 (batch_size, src_seq_len)

        Returns:
        - Tensor: 输出的线性层生成的张量,形状为 (batch_size, tgt_seq_len, tgt_vocab_size)
        """
        # 对输入的源语言和目标语言进行位置编码后,通过编码器和解码器进行处理得到最终输出
        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):
        """
        编码器的前向传播过程

        Args:
        - src (Tensor): 输入源序列张量,形状为 (batch_size, src_seq_len)
        - src_mask (Tensor): 输入源序列的掩码张量,形状为 (src_seq_len, src_seq_len)

        Returns:
        - Tensor: 编码器的输出张量,形状为 (src_seq_len, batch_size, emb_size)
        """
        # 对输入的源语言进行位置编码后,通过编码器处理得到编码器的输出
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)

    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        """
        解码器的前向传播过程

        Args:
        - tgt (Tensor): 输入目标序列张量,形状为 (batch_size, tgt_seq_len)
        - memory (Tensor): 编码器的输出张量,形状为 (src_seq_len, batch_size, emb_size)
        - tgt_mask (Tensor): 输入目标序列的掩码张量,形状为 (tgt_seq_len, tgt_seq_len)

        Returns:
        - Tensor: 解码器的输出张量,形状为 (tgt_seq_len, batch_size, emb_size)
        """
        # 对输入的目标语言进行位置编码后,通过解码器处理得到解码器的输出
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)

3.6.3 生成掩码

我们创建一个subsequent word mask来阻止目标单词关注其后续单词。我们还创建了create_mask类,用于屏蔽source and target padding tokens

def generate_square_subsequent_mask(sz):
    """
    生成一个方形的下三角掩码矩阵,用于遮盖未来位置的信息。
    """
    # 生成一个单位矩阵并将其转置,然后将其转换为布尔类型的掩码矩阵
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    # 把掩码矩阵转换为浮点类型
    mask = mask.float()
    # 把矩阵中为0的位置填充为负无穷,用于遮盖无效位置
    mask = mask.masked_fill(mask == 0, float('-inf'))
    # 把矩阵中为1的位置填充为0,用于保留有效位置
    mask = mask.masked_fill(mask == 1, float(0.0))
    return mask


def create_mask(src, tgt):
    """
    创建用于Transformer模型的掩码矩阵。
    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

3.6.4 实例化Transformer模型

定义模型参数并实例化模型:

from tqdm import tqdm
import matplotlib.pyplot as plt

SRC_VOCAB_SIZE = len(ja_vocab)
TGT_VOCAB_SIZE = len(ch_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模型实例
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

# 初始化模型参数,使用Xavier初始化
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

# 将模型移动到指定设备(GPU或CPU)
transformer = transformer.to(device)

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

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

3.7 训练

训练需要用到的函数如下:

# 定义训练一个epoch的函数
def train_epoch(model, train_iter, optimizer):
    model.train()
    losses = 0
    with tqdm(train_iter, desc=f"Epoch{epoch}/{NUM_EPOCHS}", ncols=100, unit="batch") as bar:
        for idx, (src, tgt) in enumerate(bar):
            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
            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(val_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
        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)


# 定义绘图函数
def plot_metrics(metrics, num_epochs):
    epochs = range(1, num_epochs + 1)
    train_losses = metrics

    plt.figure(figsize=(8, 6))

    # Plot training loss
    plt.plot(epochs, train_losses, 'r', label='Training loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title('Training Loss')
    plt.legend()   # 添加图例
    plt.show()

执行训练:

metrics = []

for epoch in range(1, NUM_EPOCHS+1):
  start_time = time.time()
  train_loss = train_epoch(transformer, train_iter, optimizer)
  end_time = time.time()
  metrics.append(train_loss)
  print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
          f"Epoch time = {(end_time - start_time):.3f}s"))

plot_metrics(metrics, NUM_EPOCHS)

运行结果:

Epoch1/16: 100%|███████████████████████████████████████████| 10487/10487 [05:21<00:00, 32.59batch/s]
Epoch: 1, Train loss: 4.475, Epoch time = 321.742s
Epoch2/16: 100%|███████████████████████████████████████████| 10487/10487 [05:17<00:00, 33.07batch/s]
Epoch: 2, Train loss: 3.483, Epoch time = 317.108s
Epoch3/16: 100%|███████████████████████████████████████████| 10487/10487 [05:10<00:00, 33.76batch/s]
Epoch: 3, Train loss: 3.074, Epoch time = 310.676s
Epoch4/16: 100%|███████████████████████████████████████████| 10487/10487 [05:28<00:00, 31.97batch/s]
Epoch: 4, Train loss: 2.779, Epoch time = 328.075s
Epoch5/16: 100%|███████████████████████████████████████████| 10487/10487 [05:23<00:00, 32.44batch/s]
Epoch: 5, Train loss: 2.558, Epoch time = 323.248s
Epoch6/16: 100%|███████████████████████████████████████████| 10487/10487 [05:25<00:00, 32.23batch/s]
Epoch: 6, Train loss: 2.392, Epoch time = 325.357s
Epoch7/16: 100%|███████████████████████████████████████████| 10487/10487 [05:15<00:00, 33.23batch/s]
Epoch: 7, Train loss: 2.280, Epoch time = 315.569s
Epoch8/16: 100%|███████████████████████████████████████████| 10487/10487 [05:30<00:00, 31.69batch/s]
Epoch: 8, Train loss: 2.185, Epoch time = 330.953s
Epoch9/16: 100%|███████████████████████████████████████████| 10487/10487 [05:14<00:00, 33.36batch/s]
Epoch: 9, Train loss: 2.098, Epoch time = 314.400s
Epoch10/16: 100%|██████████████████████████████████████████| 10487/10487 [05:19<00:00, 32.81batch/s]
Epoch: 10, Train loss: 2.028, Epoch time = 319.664s
Epoch11/16: 100%|██████████████████████████████████████████| 10487/10487 [05:23<00:00, 32.47batch/s]
Epoch: 11, Train loss: 1.967, Epoch time = 323.017s
Epoch12/16: 100%|██████████████████████████████████████████| 10487/10487 [05:23<00:00, 32.45batch/s]
Epoch: 12, Train loss: 1.914, Epoch time = 323.216s
Epoch13/16: 100%|██████████████████████████████████████████| 10487/10487 [05:12<00:00, 33.52batch/s]
Epoch: 13, Train loss: 1.869, Epoch time = 312.879s
Epoch14/16: 100%|██████████████████████████████████████████| 10487/10487 [05:11<00:00, 33.63batch/s]
Epoch: 14, Train loss: 1.826, Epoch time = 311.881s
Epoch15/16: 100%|██████████████████████████████████████████| 10487/10487 [05:18<00:00, 32.89batch/s]
Epoch: 15, Train loss: 1.789, Epoch time = 318.898s
Epoch16/16: 100%|██████████████████████████████████████████| 10487/10487 [05:19<00:00, 32.85batch/s]
Epoch: 16, Train loss: 1.756, Epoch time = 319.284s
图3.7.1 训练损失曲线

3.8 尝试使用训练后的模型翻译日文

首先,我们创建翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理等步骤,然后将结果解码回句子,但这次是汉语:

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    """
    使用贪婪解码策略生成目标序列。

    Args:
    - model: Transformer模型
    - src: 源语言序列张量
    - src_mask: 源语言序列掩码
    - max_len: 生成序列的最大长度
    - start_symbol: 目标序列起始符号索引

    Returns:
    - ys: 生成的目标语言序列张量
    """
    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):
    """
    使用训练好的Transformer模型进行翻译。

    Args:
    - model: 训练好的Transformer模型
    - src: 源语言文本字符串
    - src_vocab: 源语言词汇表
    - tgt_vocab: 目标语言词汇表
    - src_tokenizer: 源语言文本的分词器

    Returns:
    - translated_text: 翻译后的目标语言文本字符串
    """
    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()
    # 将生成的目标语言序列转换为文本字符串
    translated_text = " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")
    return translated_text

然后,我们可以调用 translate 函数并传递所需的参数:

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

翻译结果:

' ▁H S ▁ 代 码 ▁85 15 ▁ 是 用 于 焊 接 或 焊 接 设 备 的 ( 包 括 电 气 加 热 加 热 气 体 ) 。 '
# 移除trainch列表中索引为5的元素(删除第6个元素)
trainch.pop(5)
'Chinese HS Code Harmonized Code System < HS编码 8515 : 电气(包括电热气体)、激光、其他光、光子束、超声波、电子束、磁脉冲或等离子弧焊接机器及装置,不论是否 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'
# 移除trainja列表中索引为5的元素(删除第6个元素)
trainja.pop(5)
'Japanese HS Code Harmonized Code System < HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)、レーザーその他の光子ビーム式、超音波式、電子ビーム式、 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'

可以发现,初步评估显示翻译结果基本准确,但可能是训练次数较少,或者是其他原因,在完整性和流畅性方面可以进一步改进。总体而言,机器翻译模型在处理技术性文本时表现良好,但仍需要人为校对和改进。

3.9 保存Vocab对象和训练的模型

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

import pickle

# 打开一个文件,用于存储数据
file = open('ch_vocab.pkl', 'wb')
# 将信息存储到文件中
pickle.dump(ch_vocab, file)
file.close()

file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()

最后,我们还可以使用 PyTorch save 和 load 函数保存模型以供以后使用。通常,有两种方法可以保存模型,具体取决于我们以后要使用它们的内容。第一个仅用于推理,我们可以稍后加载模型并使用它从日语翻译为汉语。

# save model for inference
torch.save(transformer.state_dict(), 'inference_model')      # 保存模型用于推理

第二个也用于推理,但也用于我们稍后想要加载模型并想要恢复训练时。

# save model + checkpoint to resume training later
torch.save({
  'epoch': NUM_EPOCHS,
  'model_state_dict': transformer.state_dict(),
  'optimizer_state_dict': optimizer.state_dict(),
  'loss': train_loss,
  }, 'model_checkpoint.tar')      # 保存模型和检查点以便稍后恢复训练
图3.9.1 Vocab对象和模型的保存
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值