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

一、实验原理(Transformer)

1.1 概述

Transformer是一种深度学习模型,最初由谷歌的研究人员在2017年提出,尤其在论文《Attention is All You Need》中被详细介绍。它革新了自然语言处理(NLP)领域,并逐渐扩展到计算机视觉、语音识别等多个领域

1.2 特点
  1. 核心创新:自注意力机制(Self-Attention)
    Transformer的核心贡献在于其自注意力机制,它允许模型在处理序列数据(如文本中的单词序列)时,能够考虑序列中所有位置的上下文信息。这意味着模型可以动态地为序列中的每个元素分配注意力权重,从而有效捕捉长距离依赖关系,解决了循环神经网络(RNN)和卷积神经网络(CNN)在处理此类任务时的一些局限性。

  2. 并行处理能力
    由于其结构设计,Transformer模型支持并行计算,这大大加速了训练过程,相比于需要顺序处理的RNN模型,这是一个显著优势。

  3. Encoder-Decoder架构
    Transformer采用经典的编码器-解码器架构,其中编码器负责将输入序列编码为高级语义表示,而解码器则利用这些表示来生成输出序列。两部分都由多层相同的块(或称层栈)构成,每一层包括多头自注意力(Multi-Head Attention)、层归一化(Layer Normalization)、全连接前馈网络(Position-wise Feed-Forward Networks)等组件。

  4. 位置编码(Positional Encoding)
    由于Transformer放弃了循环结构,为了使模型能够区分输入序列中元素的位置信息,它通过位置编码为每个输入添加一个位置相关的向量。这样,模型在处理序列数据时仍然能保留位置信息。

  5. 广泛的应用
    Transformer不仅限于机器翻译,还广泛应用于文本生成、摘要、情感分析、问答系统、图像识别等多种任务。随着预训练模型如BERT、GPT系列的出现,Transformer成为了现代NLP的基石。

  6. 发展与变体
    随着时间推移,Transformer模型不断发展,出现了许多变体和改进版本,如更高效的模型结构、更大的预训练模型(如T5、BERT等),以及针对特定任务的优化版本,这些都不断推动着自然语言处理乃至整个AI领域的进步。

二、实验步骤

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,请在你自己的电脑上尝试运行这一套代码
device
2.2 获取并行数据集

在本教程中,我们将使用从 JParaCrawl 下载的日英并行数据集![JParaCrawl],它被描述为“NTT创建的最大的公开可用的英日平行语料库。它是通过大量抓取网络并自动对齐平行句子而创建的。你也可以在这里看到这篇论文。

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

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

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 ...
2.3 准备分词器

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

en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/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')
2.4 构建 TorchText Vocab 对象并将句子转换为 Torch 张量

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

def build_vocab(sentences, tokenizer):
    # 初始化一个计数器(Counter),用于统计每个词项出现的频次
    counter = Counter()
    
    # 遍历训练语句列表
    for sentence in sentences:
        # 使用tokenizer对句子进行编码,转换成token(单词或子词)序列
        # 参数out_type=str表明输出为字符串形式的token
        tokens = tokenizer.encode(sentence, out_type=str)
        
        # 更新计数器,对当前句子中每个token的频次进行累加
        counter.update(tokens)
    
    # 根据计数器创建词汇表实例,同时包含特殊符号:未知词(<unk>)、填充词(<pad>)、开始符(<bos>)、结束符(<eos>)
    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 = []
    
    # 使用zip函数同时遍历日语文本和英语文本的对应句子
    for (raw_ja, raw_en) in zip(ja, en):
        # 对每一对句子进行如下处理:
        
        # 使用ja_tokenizer对日语句子进行编码,去除末尾换行符,然后根据词汇表ja_vocab查找每个token的索引
        # 将索引列表转换为Long类型的张量
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                dtype=torch.long)
        
        # 同样的过程应用于英语句子,使用en_tokenizer和en_vocab
        en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
                                dtype=torch.long)
        
        # 将处理后的日语和英语张量作为元组添加到data列表中
        data.append((ja_tensor_, en_tensor_))
    
    # 处理完成后返回包含所有数据对的列表
    return data

# 调用data_process函数处理训练数据集
train_data = data_process(trainja, trainen)
2.5 创建要在训练期间迭代的 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):
    # 初始化空列表以存放处理后的批次数据
    ja_batch, en_batch = [], []
    
    # 遍历当前批次中的每个数据对
    for (ja_item, en_item) in data_batch:
        # 对于每个句子,添加开始符号(BOS)在句首,结束符号(EOS)在句尾
        # 使用torch.cat拼接张量,并保持在第0维度(dim=0)
        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函数对句子进行填充,确保每个批次中的序列长度相同,未填充的位置使用PAD_IDX
    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创建迭代器,用于在训练时提供数据
# 指定了批次大小、是否随机打乱数据、以及自定义的collate_fn来生成批次
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)
2.6 序列到序列转换器

接下来的几个代码和文本说明(用斜体编写)取自原始的 PyTorch 教程 [Language Translation with nn.Transformer and torchtext — PyTorch Tutorials 2.3.0+cu121 documentation]。除了BATCH_SIZE之外,我没有做任何更改,de_vocabwhich 这个词被改成了ja_vocab。

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

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

# 导入必要的Transformer组件
from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)

# 定义Seq2SeqTransformer类,继承自nn.Module
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,  # 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_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):
        # 给定目标序列、编码器的输出记忆以及目标序列的mask,进行解码
        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__()
        # 计算位置编码中的衰减因子,确保不同频率的sin/cos函数覆盖不同的位置信息尺度
        den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)
        # 生成一个从0到maxlen的positions矩阵,用于计算sin和cos的位置值
        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)
        # 添加一个额外的维度,以便与Transformer中的其他嵌入操作对齐
        pos_embedding = pos_embedding.unsqueeze(-2)
        
        # 定义一个dropout层,用于正则化和防止过拟合
        self.dropout = nn.Dropout(dropout)
        # 使用register_buffer注册位置嵌入张量,这样它不会被视为模型参数,但会保存在GPU上(如果可用)
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: 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):
        super(TokenEmbedding, self).__init__()
        # 初始化一个基于vocab_size大小的词嵌入层,每个词映射到emb_size维的向量空间
        self.embedding = nn.Embedding(vocab_size, emb_size)
        # 记录嵌入维度大小,用于后续计算
        self.emb_size = emb_size
    
    def forward(self, tokens: Tensor):
        # 将输入的token索引转换为词嵌入表示,并乘以sqrt(emb_size)进行缩放,
        # 这一操作来源于Transformer论文,用于保持梯度的尺度一致,有助于训练
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

我们创建一个后续单词掩码来阻止目标单词关注其后续单词。我们还创建掩码,用于屏蔽源和目标填充令牌

def generate_square_subsequent_mask(sz):
    # 创建一个上三角矩阵,对角线及上方为1,下方为0;sz为序列长度
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    # 转置矩阵,使得未来信息(下三角)被屏蔽
    # 将布尔矩阵转换为浮点型,并将0位置替换为负无穷,1位置替换为0.0,用于后续的softmax操作中有效屏蔽
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return 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)
    
    # 源序列的自注意力掩码通常初始化为全1(即不需要屏蔽,除非有特定的双向注意力需求)
    src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
    
    # 创建源序列和目标序列的填充掩码,标记PAD_IDX位置(通常是0),用于在注意力计算时忽略这些位置
    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

定义模型参数并实例化模型。这里我们服务器实在是计算能力有限,按照以下配置可以训练但是效果应该是不行的。如果想要看到训练的效果,请使用你自己的带GPU的电脑运行这一套代码。

当你使用自己的GPU的时候,NUM_ENCODER_LAYERS 和 NUM_DECODER_LAYERS 设置为3或者更高,NHEAD设置8,EMB_SIZE设置为512。

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
)
def train_epoch(model, train_iter, optimizer):
    # 将模型设置为训练模式,启用如dropout等训练时的行为
    model.train()
    
    # 初始化损失总和变量
    losses = 0
    
    # 遍历训练数据迭代器,获取每个批次的数据
    for idx, (src, tgt) in enumerate(train_iter):
        # 将源序列和目标序列数据移动到计算设备(如GPU)上
        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:,:]
        
        # 计算损失,需要将logits和tgt_out调整形状以匹配交叉熵损失函数要求
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        
        # 反向传播计算梯度
        loss.backward()
        
        # 根据计算出的梯度更新模型参数
        optimizer.step()
        
        # 累加批次损失
        losses += loss.item()
    
    # 计算整个epoch的平均损失
    return losses / len(train_iter)


def evaluate(model, val_iter):
    # 将模型设置为评估模式,这会影响某些层的行为,如dropout和batch normalization
    model.eval()
    
    # 初始化损失总和变量
    losses = 0
    
    # 使用enumerate遍历验证数据迭代器,idx是批次索引,(src, tgt)是源语言和目标语言的批次数据
    for idx, (src, tgt) in enumerate(val_iter):
        # 将源序列和目标序列移动到计算设备(如GPU)上
        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:,:]
        
        # 计算损失,先将logits和tgt_out调整形状以便交叉熵损失函数计算
        loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
        
        # 累加批次损失
        losses += loss.item()
    
    # 计算平均损失:总损失除以验证迭代器的长度(即批次数量)
    return losses / len(val_iter)
2.7 开始训练

最后,在准备了必要的类和函数之后,我们准备训练我们的模型。这是不言而喻的,但完成训练所需的时间可能会有很大差异,具体取决于很多因素,例如计算能力、参数和数据集的大小。

当我使用 JParaCrawl 的完整句子列表(每种语言大约有 590 万个句子)训练模型时,使用单个 NVIDIA GeForce RTX 3070 GPU 每个 epoch 大约需要 5 小时。

代码如下:

for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):  # tqdm是一个进度条库,使循环过程可视化
    start_time = time.time()  # 记录当前时间,用于计算每个epoch的持续时间

    # 训练单个epoch,调用前面定义的train_epoch函数
    # transformer是待训练的模型,train_iter是训练数据的迭代器,optimizer是优化器
    train_loss = train_epoch(transformer, train_iter, optimizer)
    
    end_time = time.time()  # 记录当前时间,计算epoch结束时间

    # 打印每个epoch的相关信息
    print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "  # f-string格式化输出
           f"Epoch time = {(end_time - start_time):.3f}s"))  # 显示训练该epoch所花费的时间,精确到小数点后三位
2.8 尝试使用经过训练的模型翻译日语句子
def greedy_decode(model, src, src_mask, max_len, start_symbol):
    # 将源序列(src)和源序列的mask(src_mask)移动到计算设备上(如GPU)
    src = src.to(device)
    src_mask = src_mask.to(device)
    
    # 使用模型的编码器部分对源序列进行编码,得到记忆向量(memory)
    memory = model.encode(src, src_mask)
    
    # 初始化解码器的输入序列ys,开始符号通常为<s> 或者对应的索引值
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
    
    # 遍历最大解码长度(max_len-1),因为在初始化时已经添加了一个token
    for i in range(max_len-1):
        # 确保memory在目标设备上
        memory = memory.to(device)
        
        # 创建一个与ys形状相同的mask,用于后续操作,这里初始化为全False,可能未直接使用
        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)
        
        # 使用解码器解码当前序列ys,结合记忆向量memory和tgt_mask
        out = model.decode(ys, memory, tgt_mask)
        
        # 转置解码器的输出,以便于后续处理
        out = out.transpose(0, 1)
        
        # 将解码器输出的最后一位置入生成器,得到下一个词的概率分布
        prob = model.generator(out[:, -1])
        
        # 选择概率最大的词作为下一个词
        _, next_word = torch.max(prob, dim = 1)
        
        # 获取下一个词的索引值并转换为Python标量
        next_word = next_word.item()
        
        # 将选中的词添加到序列ys中
        ys = torch.cat([ys, torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)
        
        # 如果生成的词是结束符号(EOS_IDX),则停止生成
        if next_word == EOS_IDX:
            break
    
    # 返回生成的序列
    return ys
def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    # 将模型设置为评估模式,这会关闭诸如dropout等训练时使用的特性
    model.eval()
    
    # 对源文本进行预处理:
    # 1. 在句子开头添加开始标志(BOS_IDX)
    # 2. 使用源文本的分词器将其转换为token,并查找每个token在词汇表中的索引
    # 3. 在句子末尾添加结束标志(EOS_IDX)
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]
    
    # 计算tokens的总数
    num_tokens = len(tokens)
    
    # 将tokens转换为PyTorch的LongTensor,并调整形状为(num_tokens, 1),以便输入模型
    src = torch.LongTensor(tokens).reshape(num_tokens, 1)
    
    # 创建源序列的mask,此处因为是单向解码,故mask全为False
    src_mask = (torch.zeros(num_tokens, num_tokens)).type(torch.bool)
    
    # 使用之前定义的贪心解码方法进行解码
    # max_len设置为源序列长度加5,以允许生成比输入略长的序列
    tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()
    
    # 将解码得到的token索引转换回目标语言的词汇项,并用空格连接成字符串
    # 同时移除开始和结束标记
    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, en_vocab, ja_tokenizer)
trainen.pop(5)
trainja.pop(5)
2.9 保存 Vocab 对象和训练的模型
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()

最后,我们还可以使用 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')

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基于Transformer实现机器翻译是一种先进的方法。在Pytorch,可以使用nn.Transformer实现英文到文的机器翻译任务\[1\]。如果想要深入了解nn.Transformer的使用,可以参考一篇博文《Pytorch nn.Transformer的使用详解与Transformer的黑盒讲解》\[1\]。在这篇博文,作者建议先学习CopyTask任务,然后再学习机器翻译任务,这样会更容易理解。 此外,谷歌翻译也在逐步将转换器编码器引入其翻译算法\[2\]。他们提供了一个即用型翻译界面,可以在谷歌翻译网站上使用\[2\]。另外,瓦斯瓦尼等人在2017年的研究发现,Transformer在WMT 2014英德翻译任务和WMT 2014英法翻译任务上取得了最先进的BLEU分数\[3\]。BLEU是一种用于评估机器翻译质量的指标,具体的评估方法可以在《Evaluating machine translation with BLEU》部分找到\[3\]。 综上所述,基于Transformer机器翻译方法在实践取得了很好的效果,并且在Pytorch有相应的实现。同时,谷歌翻译也在逐步引入转换器编码器,并且Transformer机器翻译任务取得了最先进的结果。 #### 引用[.reference_title] - *1* [Pytorch入门实战(5):基于nn.Transformer实现机器翻译(英译汉)](https://blog.csdn.net/zhaohongfei_358/article/details/126175328)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [【NLP】第6章 使用 Transformer 进行机器翻译](https://blog.csdn.net/sikh_0529/article/details/127037111)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值