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

一、Transformer简介

       Transformer 是一种革命性的深度学习模型架构,最初由Vaswani等人在2017年提出,专门设计用于处理序列到序列(sequence-to-sequence)任务,如机器翻译、文本摘要等。它摒弃了传统的循环神经网络(RNNs)和卷积神经网络(CNNs),采用全新的注意力机制来实现更高效的并行化计算,从而显著提升了处理长距离依赖和全局信息的能力。

1. 主要组成部分

1.1 注意力机制(Attention Mechanism): Transformer 中的核心是自注意力机制(self-attention),它允许模型在处理输入序列时动态地将注意力集中在不同位置的信息上,而不是像传统的循环结构那样逐步处理序列。这使得模型能够同时处理输入序列中的所有位置,从而加速并行计算,提高效率。

1.2 编码器-解码器结构(Encoder-Decoder Architecture): Transformer 包含编码器和解码器两个核心部分。编码器负责将输入序列转换为一个上下文感知的表示,而解码器则使用编码器输出的表示来生成目标语言序列。每个编码器和解码器模块都包含多层注意力机制和前馈神经网络层。

1.3 多头注意力机制(Multi-head Attention): 为了进一步提高模型的表达能力,Transformer 使用多个注意力头,即同时学习多个不同的注意力表示。每个头都可以捕捉数据的不同方面和关系,然后通过合并来增强模型对输入和输出序列的表示能力。

1.4 前馈神经网络(Feedforward Neural Networks): 每个注意力子层后面跟随着一个全连接的前馈神经网络,这个网络在每个位置独立地作用,并且都是相同的。这个前馈神经网络包含两层线性变换,中间经过一个非线性激活函数(通常是ReLU)。

1.5 位置编码(Positional Encoding): 由于Transformer不具备显式的序列顺序信息,为了使模型能够理解序列中每个元素的位置,它引入了位置编码,这些编码向量被加到输入嵌入中,提供关于单词在序列中位置的相对或绝对信息。

2. 优势与应用

    Transformer 的出现极大地推动了自然语言处理领域的发展,其优势包括:

  • 并行计算能力强: 自注意力机制允许所有位置同时计算,大大提升了计算效率。
  • 能够处理长距离依赖: 相比传统的循环结构,Transformer 更能有效地捕捉长距离依赖关系。
  • 适用于多种序列到序列任务: 包括机器翻译、文本摘要、语言建模等。

       Transformer 作为一种创新的模型架构,通过引入自注意力机制和多头注意力机制,极大地提升了序列到序列任务的性能和效率,成为当前自然语言处理领域的主流模型之一。

二、实验环境

       本次实验所要训练的数据量较大,因此采用的是第三方平台算力互联的GPU来进行模型训练的,其网址为:算力互联-算力网与人工智能基础设施 (casdao.com)

       本次实验所使用的GPU为:NVIDIA Geforce 3090,具体配置如下:

三、数据准备

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

2. 获取并行数据集

       在本次实验中,我们将使用从 JParaCrawl 下载的日英并行数据集JParaCrawl, 它被描述为“NTT创建的最大的公开可用的英日平行语料库。它是通过大量抓取网络并自动对齐平行句子而创建的。

df = pd.read_csv('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)

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

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

print(trainen[500])
print(trainja[500])
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 ...

       我们还可以使用不同的并行数据集来遵循本文,只需确保我们可以将数据处理成两个字符串列表,如上所示,包含日语和英语句子。

四、准备分词器

       与英语或其他字母语言不同,日语句子不包含空格来分隔单词。我们可以使用JParaCrawl提供的分词器,该分词器是使用SentencePiece创建的日语和英语,您可以访问JParaCrawl网站下载它们。

en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')

   加载分词器后,我们便通过执行以下代码来测试它们。

en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.")
[227,
 2980,
 8863,
 373,
 8,
 9381,
 126,
 91,
 649,
 11,
 93,
 240,
 19228,
 11,
 419,
 14926,
 102,
 5]
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。")
[4,
 31,
 346,
 912,
 10050,
 222,
 1337,
 372,
 820,
 4559,
 858,
 750,
 3,
 13118,
 31,
 346,
 2000,
 10,
 8978,
 5461,
 5]

       然后,使用分词器和原始句子,我们构建从 TorchText 导入的 Vocab 对象。此过程可能需要几秒钟或几分钟,具体取决于我们的数据集大小和计算能力。不同的分词器也会影响构建词汇所需的时间。

def build_vocab(sentences, tokenizer):
    """
    构建词汇表(vocabulary)的函数。

    参数:
    sentences (list): 包含文本句子的列表。
    tokenizer (object): 用于将句子分词的分词器对象。

    返回值:
    Vocab: 词汇表对象,包含句子中所有的词及其频率。
    """
    
    # 创建一个Counter对象,用于统计词频
    counter = Counter()
    
    # 遍历每一个句子
    for sentence in sentences:
        # 使用tokenizer对句子进行分词,并更新Counter对象
        counter.update(tokenizer.encode(sentence, out_type=str))
    
    # 创建并返回一个Vocab对象,包含词频信息和特殊标记
    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):
    """
    将日语和英语句子对转换为张量列表的函数。

    参数:
    ja (list): 包含日语句子的列表。
    en (list): 包含英语句子的列表。

    返回值:
    list: 包含日语和英语张量对的列表。
    """
    
    # 初始化一个空列表,用于存储处理后的数据
    data = []
    
    # 使用zip函数并行遍历日语和英语句子的列表
    for (raw_ja, raw_en) in zip(ja, en):
        # 处理日语句子
        # 1. 使用分词器将日语句子分词并去掉结尾的换行符
        # 2. 将每个分词后的词转换为对应的词汇表索引
        # 3. 将索引列表转换为PyTorch的长整型张量
        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)

五、创建训练过程中要迭代的 DataLoader 对象

       在这里,我将BATCH_SIZE设置为 16 以防止“cuda 内存不足”,但这取决于各种因素,例如您的机器内存容量、数据大小等,因此请根据需要随意更改批处理大小(注意:PyTorch 的教程使用 Multi30k 德语-英语数据集将批处理大小设置为 128。

# 设置批处理的大小
BATCH_SIZE = 8

# 获取特殊标记在词汇表中的索引
PAD_IDX = ja_vocab['<pad>']  # 填充符的索引
BOS_IDX = ja_vocab['<bos>']  # 句子开始符的索引
EOS_IDX = ja_vocab['<eos>']  # 句子结束符的索引

def generate_batch(data_batch):
    """
    生成批处理数据的函数。

    参数:
    data_batch (list): 包含日语和英语张量对的批处理列表。

    返回值:
    tuple: 包含填充后的日语和英语批处理张量。
    """
    
    # 初始化空列表,用于存储批处理数据
    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))
    
    # 使用pad_sequence函数对批处理中的张量进行填充,使其长度一致
    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

# 使用DataLoader创建训练数据的迭代器
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)

六、序列到序列转换的Transformer

       Transformer 是 用于解决机器翻译任务。Transformer 模型由编码器和解码器块组成,每个块包含固定数量的层,编码器通过一系列多头注意力和前馈网络层传播输入序列来处理输入序列。编码器的输出称为内存,与目标张量一起馈送到解码器。编码器和解码器在端到端的 fash 中进行训练。

# 导入必要的库
import torch
import torch.nn as nn
from torch import Tensor
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):
        """
        初始化Seq2Seq Transformer模型。

        参数:
        num_encoder_layers (int): 编码器层数。
        num_decoder_layers (int): 解码器层数。
        emb_size (int): 嵌入维度大小。
        src_vocab_size (int): 源语言词汇表大小。
        tgt_vocab_size (int): 目标语言词汇表大小。
        dim_feedforward (int): 前馈神经网络的维度大小。
        dropout (float): Dropout的概率。
        """
        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)

        # 定义解码器层
        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):
        """
        前向传播函数。

        参数:
        src (Tensor): 源语言张量。
        trg (Tensor): 目标语言张量。
        src_mask (Tensor): 源语言掩码。
        tgt_mask (Tensor): 目标语言掩码。
        src_padding_mask (Tensor): 源语言填充掩码。
        tgt_padding_mask (Tensor): 目标语言填充掩码。
        memory_key_padding_mask (Tensor): 编码器输出的填充掩码。

        返回值:
        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):
        """
        编码函数。

        参数:
        src (Tensor): 源语言张量。
        src_mask (Tensor): 源语言掩码。

        返回值:
        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):
        """
        解码函数。

        参数:
        tgt (Tensor): 目标语言张量。
        memory (Tensor): 编码器的输出张量。
        tgt_mask (Tensor): 目标语言掩码。

        返回值:
        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: float, maxlen: int = 5000):
        """
        初始化位置编码层。

        参数:
        emb_size (int): 嵌入维度大小。
        dropout (float): Dropout的概率。
        maxlen (int): 位置编码的最大长度。
        """
        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)
        
        # 添加额外的维度以匹配token嵌入的形状
        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):
        """
        前向传播函数。

        参数:
        token_embedding (Tensor): Token嵌入张量。

        返回值:
        Tensor: 添加位置编码后的Token嵌入张量。
        """
        # 添加位置编码并应用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: int):
        """
        初始化Token嵌入层。

        参数:
        vocab_size (int): 词汇表大小。
        emb_size (int): 嵌入维度大小。
        """
        super(TokenEmbedding, self).__init__()
        
        # 定义嵌入层
        self.embedding = nn.Embedding(vocab_size, emb_size)
        
        # 保存嵌入维度大小
        self.emb_size = emb_size

    def forward(self, tokens: Tensor):
        """
        前向传播函数。

        参数:
        tokens (Tensor): Token张量。

        返回值:
        Tensor: Token嵌入张量。
        """
        # 获取Token嵌入并乘以嵌入维度的平方根
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

我们创建一个后续单词掩码来阻止目标单词关注其后续单词。

def generate_square_subsequent_mask(sz: int):
    """
    生成一个大小为 (sz, sz) 的上三角掩码矩阵,用于自注意力机制中的序列掩码。

    参数:
    sz (int): 掩码矩阵的大小。

    返回值:
    Tensor: 上三角掩码矩阵。
    """
    # 生成上三角矩阵,其中上三角部分为1,其余部分为0
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    
    # 将掩码矩阵转换为float类型,并将非上三角部分填充为负无穷大,将上三角部分填充为0
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

def create_mask(src: Tensor, tgt: Tensor):
    """
    为源序列和目标序列创建掩码。

    参数:
    src (Tensor): 源序列张量。
    tgt (Tensor): 目标序列张量。

    返回值:
    tuple: 包含源序列掩码、目标序列掩码、源序列填充掩码、目标序列填充掩码的元组。
    """
    # 获取源序列和目标序列的长度
    src_seq_len = src.shape[0]
    tgt_seq_len = tgt.shape[0]

    # 生成目标序列的上三角掩码
    tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
    
    # 生成源序列的掩码,这里源序列的掩码全为False,因为源序列没有后续依赖
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)

    # 生成源序列和目标序列的填充掩码
    # 填充掩码用于指示哪些位置是填充值,填充值的位置为True,其余位置为False
    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

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

# 定义一些超参数和模型参数
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                 # 训练轮数

# 初始化Seq2Seq 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上
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
)

def train_epoch(model, train_iter, optimizer):
    """
    训练一个epoch。

    参数:
    model (nn.Module): 要训练的模型。
    train_iter (DataLoader): 训练数据迭代器。
    optimizer (torch.optim.Optimizer): 优化器。

    返回值:
    float: 训练集的平均损失。
    """
    model.train()  # 设置模型为训练模式
    losses = 0
    for idx, (src, tgt) in enumerate(train_iter):
        src = src.to(device)  # 将源序列移动到GPU
        tgt = tgt.to(device)  # 将目标序列移动到GPU

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

        # 创建掩码
        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:, :]  # 目标输出序列,不包括第一个token
        # 计算损失
        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 (nn.Module): 要评估的模型。
    val_iter (DataLoader): 验证数据迭代器。

    返回值:
    float: 验证集的平均损失。
    """
    model.eval()  # 设置模型为评估模式
    losses = 0
    with torch.no_grad():  # 关闭梯度计算
        for idx, (src, tgt) in enumerate(val_iter):
            src = src.to(device)  # 将源序列移动到GPU
            tgt = tgt.to(device)  # 将目标序列移动到GPU

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

            # 创建掩码
            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:, :]  # 目标输出序列,不包括第一个token
            # 计算损失
            loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
            losses += loss.item()  # 累加损失
    return losses / len(val_iter)  # 返回平均损失

七、开始训练

        实际上,在准备了必要的类和函数之后,我们就可以训练我们的模型了。但事实上,完成训练所需的时间可能会有很大差异,具体取决于很多因素,例如计算能力、参数和数据集的大小。当我使用 JParaCrawl 的完整句子列表(每种语言大约有 590 万个句子)训练模型时,使用单个 NVIDIA GeForce RTX 3090 GPU 每个 epoch 大约需要4分钟。

代码如下: 

# 训练循环
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS + 1)):
    start_time = time.time()  # 记录开始时间
    train_loss = train_epoch(transformer, train_iter, optimizer)  # 训练一个epoch并获取训练损失
    end_time = time.time()  # 记录结束时间
    
    # 打印当前epoch的训练损失和所用时间
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
           f"Epoch time = {(end_time - start_time):.3f}s"))

训练的迭代过程为: 

0%|          | 0/16 [00:00<?, ?it/s]/opt/conda/lib/python3.11/site-packages/torch/nn/functional.py:5076: UserWarning: Support for mismatched key_padding_mask and attn_mask is deprecated. Use same type for both instead.
  warnings.warn(
  6%|▋         | 1/16 [03:48<57:01, 228.12s/it]
Epoch: 1, Train loss: 4.472, Epoch time = 228.120s
 12%|█▎        | 2/16 [07:32<52:46, 226.19s/it]
Epoch: 2, Train loss: 3.481, Epoch time = 224.837s
 19%|█▉        | 3/16 [11:16<48:45, 225.02s/it]
Epoch: 3, Train loss: 3.069, Epoch time = 223.631s
 25%|██▌       | 4/16 [15:01<44:59, 224.93s/it]
Epoch: 4, Train loss: 2.773, Epoch time = 224.781s
 31%|███▏      | 5/16 [18:46<41:14, 224.97s/it]
Epoch: 5, Train loss: 2.557, Epoch time = 225.050s
 38%|███▊      | 6/16 [22:30<37:27, 224.78s/it]
Epoch: 6, Train loss: 2.392, Epoch time = 224.418s
 44%|████▍     | 7/16 [26:12<33:34, 223.79s/it]
Epoch: 7, Train loss: 2.286, Epoch time = 221.730s
 50%|█████     | 8/16 [29:54<29:45, 223.22s/it]
Epoch: 8, Train loss: 2.192, Epoch time = 222.002s
 56%|█████▋    | 9/16 [33:38<26:03, 223.30s/it]
Epoch: 9, Train loss: 2.110, Epoch time = 223.489s
 62%|██████▎   | 10/16 [37:21<22:19, 223.30s/it]
Epoch: 10, Train loss: 2.040, Epoch time = 223.288s
 69%|██████▉   | 11/16 [41:03<18:34, 222.85s/it]
Epoch: 11, Train loss: 1.980, Epoch time = 221.845s
 75%|███████▌  | 12/16 [44:46<14:52, 223.09s/it]
Epoch: 12, Train loss: 1.927, Epoch time = 223.636s
 81%|████████▏ | 13/16 [48:30<11:09, 223.19s/it]
Epoch: 13, Train loss: 1.882, Epoch time = 223.409s
 88%|████████▊ | 14/16 [52:13<07:26, 223.33s/it]
Epoch: 14, Train loss: 1.841, Epoch time = 223.657s
 94%|█████████▍| 15/16 [55:57<03:43, 223.31s/it]
Epoch: 15, Train loss: 1.804, Epoch time = 223.253s
100%|██████████| 16/16 [59:39<00:00, 223.74s/it]
Epoch: 16, Train loss: 1.768, Epoch time = 222.630s

八、使用经过训练的模型翻译日语句子

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

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    """
    使用贪婪搜索法解码模型的输出。

    参数:
    model (nn.Module): 序列到序列模型。
    src (Tensor): 源序列张量。
    src_mask (Tensor): 源序列掩码。
    max_len (int): 解码的最大长度。
    start_symbol (int): 解码开始的起始符号。

    返回值:
    Tensor: 解码生成的目标序列张量。
    """
    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 (nn.Module): 序列到序列模型。
    src (str): 源语言序列。
    src_vocab (Vocab): 源语言词汇表。
    tgt_vocab (Vocab): 目标语言词汇表。
    src_tokenizer (Tokenizer): 源语言分词器。

    返回值:
    str: 翻译后的目标语言序列。
    """
    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)
' ▁H S 代 码 ▁85 15 ▁ 是 电 气 焊 接 用 于 焊 接 的 或 焊 接 设 备 ( 包 括 电 气 加 热 加 热 ) 。 '
trainen.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.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 ...'

九、小结

        实验结果表明,Transformer 模型在机器翻译任务中具有卓越的性能,能够生成高质量的译文。模型能够有效捕捉长距离的依赖关系,提高了翻译的准确性。相比传统的循环神经网络(RNN),Transformer 模型的并行计算能力显著加快了训练速度。

       通过本次实验,我们验证了 Transformer 模型在机器翻译中的强大性能,并为未来的研究方向提供了参考。我们相信,随着更多创新技术和训练策略的引入,机器翻译的质量和效率将继续提升。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值