基于Transformer实现机器翻译——NLP实验

一.机器翻译

机器翻译(Machine Translation, MT)是一种利用计算机程序自动将一种自然语言(源语言)的文本翻译成另一种自然语言(目标语言)的过程。它的作用在于帮助人们跨越语言障碍,快速和高效地进行文本翻译。以下是机器翻译的一些关键特点和作用:

  1. 自动化翻译: 机器翻译系统能够自动处理大量文本,无需人工逐句翻译,节省了大量时间和人力成本。
  2. 跨语言沟通: 它使得不同语言使用者可以更容易地交流和理解彼此的信息,有助于国际间的商务、学术、文化交流。
  3. 实时性: 在某些情况下,机器翻译能够提供即时翻译服务,例如在线聊天、视频会议等,大大提高了信息传递的效率。
  4. 大数据处理: 对于大量重复性高的文本,例如新闻报道、科技文档等,机器翻译可以快速且一致地完成翻译任务。
  5. 辅助翻译工具: 对于人工翻译人员来说,机器翻译系统可以作为辅助工具,提供翻译建议和参考,加快翻译的速度和准确性。


二.Transformer的简介

Transformer是一种深度学习模型架构,被广泛用于处理序列数据,尤其是在自然语言处理领域取得了显著的成就。Transformer模型最初由Vaswani等人在2017年提出,作为一种完全基于注意力机制(self-attention)的序列到序列模型。传统的循环神经网络(RNNs)和卷积神经网络(CNNs)在处理长距离依赖关系和并行计算方面存在一些限制,而Transformer通过引入注意力机制来解决这些问题,使得它能够更有效地处理长序列和并行化计算。

关键组成部分

     1. 注意力机制(Self-Attention)

Transformer的核心是自注意力机制,它允许模型在处理序列时将不同位置的信息关联起来。通过计算每个位置对其他位置的注意力权重,模型可以根据输入的上下文动态地调整每个词的表示。

      2.位置编码(Positional Encoding)

因为Transformer没有显式的序列顺序信息(如RNN中的时间步或CNN中的位置信息),所以需要引入位置编码来帮助模型理解序列中词的相对位置。常见的位置编码包括正弦和余弦函数的组合。

      3.编码器(Encoder)

Transformer由多个编码器堆叠而成。每个编码器由两个子层组成:多头自注意力机制层(Multi-Head Self-Attention)和前馈神经网络层(Feedforward Neural Network)。多头注意力允许模型在不同的表示空间中并行地关注不同的位置信息,前馈神经网络则在每个位置上独立地应用。

      4.解码器(Decoder)

在序列到序列任务中(如机器翻译),Transformer还包括解码器部分。解码器在编码器的基础上添加了一个额外的多头自注意力机制层,用来关注输入序列的不同部分,以及一个编码器-解码器注意力机制层,用来关注输入和输出序列之间的对应关系。

       5.残差连接和层归一化

Transformer中的每个子层(注意力层和前馈网络层)都包含了残差连接(Residual Connection),即将输入直接添加到子层的输出上,以帮助信息流动和减轻梯度消失问题。此外,每个子层后面还跟随一个层归一化(Layer Normalization)步骤,用来稳定训练过程。

主要应用于自然语言处理:Transformer在机器翻译、文本生成、问答系统等任务中表现出色,如Google的BERT和OpenAI的GPT系列就是基于Transformer架构的。计算机视觉: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')
# print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码

 设备信息:

2.获取并行数据集

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

# 从 'zh-ja.bicleaner05.txt' 文件中读取数据集,使用制表符 '\t' 作为分隔符
df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)

# 将英文数据存储到列表 trainen 中
trainen = df[2].values.tolist()  # 取第3列数据,并转换为列表
# trainen = trainen[:10000]  # 如果需要只取前10000条数据,可以取消注释此行

# 将日文数据存储到列表 trainja 中
trainja = df[3].values.tolist()  # 取第4列数据,并转换为列表
# trainja = trainja[:10000]  # 如果需要只取前10000条数据,可以取消注释此行

# 如果需要移除特定索引的数据,可以使用 pop 方法
# trainen.pop(5972)
# trainja.pop(5972)

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

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

#打印输出
print(trainen[500])
print(trainja[500])

 结果: 

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

3.准备标记器

与英语或其他按字母顺序排列的语言不同,日语句子不包含空格来分隔单词。我们可以使用JParaCrawl提供的标记器,该标记器是使用日语和英语的句子片段创建的,您可以访问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.", out_type='str')

 结果:

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

  结果:

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

使用标记器和原始句子,我们构建从TorchText导入的Vocab对象。这个过程可能需要几秒钟或几分钟,这取决于我们数据集的大小和计算能力。不同的标记器也会影响构建vocab所需的时间,我尝试了其他几种日语标记器,但PensionePiece似乎对我来说足够好、足够快。

def build_vocab(sentences, tokenizer):
    """
    根据句子列表构建词汇表。

    Args:
    - sentences (list): 句子列表。
    - tokenizer: 分词器,用于对句子进行分词。

    Returns:
    - Vocab: 构建的词汇表对象,包含特殊标记如 '<unk>', '<pad>', '<bos>', '<eos>'。
    """
    counter = Counter()
    # 遍历每个句子并更新词频
    for sentence in sentences:
        counter.update(tokenizer.encode(sentence, out_type=str))
    # 使用 Counter 构建词汇表,同时指定特殊标记
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 分别为日语和英语构建词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
en_vocab = build_vocab(trainen, en_tokenizer)

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

def data_process(ja, en):
    """
    将日语和英语句子列表转换为张量数据集。

    Args:
    ja (list): 日语句子列表。
    en (list): 英语句子列表。

    Returns:
    list: 包含转换后的张量对的数据集,每个元素是一个元组 (ja_tensor, en_tensor)。
    """
    data = []
    for (raw_ja, raw_en) in zip(ja, en):
        # 使用 ja_vocab 和 ja_tokenizer 将日语句子转换为张量
        ja_tensor = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
                                 dtype=torch.long)
        # 使用 en_vocab 和 en_tokenizer 将英语句子转换为张量
        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设置为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):
    """
    生成一个批量的数据,包括日语和英语句子。

    Args:
    data_batch (list): 包含日语和英语句子张量对的批量数据。

    Returns:
    tuple: 包含日语和英语句子的张量批量 (ja_batch, en_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

# 使用 DataLoader 将训练数据集 train_data 转换为可以迭代的批量数据集 train_iter
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, collate_fn=generate_batch)

6.Sequence-to-sequence Transformer

接下来的几个代码和文本解释(用斜体书写)取自PyTorch的原始教程[https://pytorch.org/tutorials/beginner/translation_transformer.html]. 我没有做任何更改,除了BATCH_SIZE和单词de_vocab被改为ja_vocab。

Transformer是“注意力就是你所需要的”论文中介绍的一个Seq2Seq模型,用于解决机器翻译任务。转换器模型由编码器和解码器块组成,每个块包含固定数量的层。

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

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__()
        
        # 定义Transformer编码器层
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        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)
        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))
        
        # 编码阶段:通过Transformer编码器处理源语言序列
        memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
        
        # 解码阶段:通过Transformer解码器处理目标语言序列
        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):
        # 编码阶段:返回Transformer编码器的输出,不经过解码过程
        return self.transformer_encoder(self.positional_encoding(
                            self.src_tok_emb(src)), src_mask)

    def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
        # 解码阶段:返回Transformer解码器的输出,需要提供编码器的记忆和目标语言的掩码
        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):
        """
        初始化位置编码器。
        
        Args:
        - emb_size (int): 词嵌入的维度大小。
        - dropout (float): Dropout 概率,用于防止过拟合。
        - maxlen (int, optional): 位置编码的最大长度,默认为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)
        
        # 在最后添加一个维度,以便与token_embedding相加
        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):
        """
        执行前向传播。
        
        Args:
        - token_embedding (Tensor): 经过词嵌入后的张量,形状为 (seq_length, batch_size, emb_size)。
        
        Returns:
        - Tensor: 添加了位置编码后的张量,形状与token_embedding相同。
        """
        # 添加位置编码并应用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):
        """
        初始化标记嵌入器。
        
        Args:
        - 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):
        """
        执行前向传播。
        
        Args:
        - tokens (Tensor): 输入的标记张量,形状为 (seq_length, batch_size).
        
        Returns:
        - Tensor: 经过词嵌入后的张量,形状为 (seq_length, batch_size, emb_size)。
        """
        # 通过词嵌入层计算词嵌入并乘以sqrt(emb_size)进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

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

def generate_square_subsequent_mask(sz):
    """
    生成一个针对 Transformer 解码器的下三角矩阵掩码。

    Args:
    - sz (int): 序列的长度。

    Returns:
    - torch.Tensor: 下三角矩阵掩码,形状为 (sz, sz),数据类型为 torch.float32。
      对角线及其以上的元素为 -inf,其余元素为 0.0。
    """
    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

def create_mask(src, tgt):
    """
    创建用于 Transformer 模型的掩码。

    Args:
    - src (torch.Tensor): 输入源序列,形状为 (src_seq_len, batch_size).
    - tgt (torch.Tensor): 输出目标序列,形状为 (tgt_seq_len, batch_size).

    Returns:
    - torch.Tensor: 源序列掩码,形状为 (src_seq_len, src_seq_len),数据类型为 torch.bool。
    - torch.Tensor: 目标序列的下三角矩阵掩码,形状为 (tgt_seq_len, tgt_seq_len),数据类型为 torch.float32。
    - torch.Tensor: 源序列的填充掩码,形状为 (batch_size, src_seq_len),数据类型为 torch.bool。
    - torch.Tensor: 目标序列的填充掩码,形状为 (batch_size, tgt_seq_len),数据类型为 torch.bool。
    """
    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)

    # 创建源序列和目标序列的填充掩码
    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的时候,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  # FeedForward 层隐藏单元数
BATCH_SIZE = 16  # 批量大小
NUM_ENCODER_LAYERS = 3  # 编码器层数
NUM_DECODER_LAYERS = 3  # 解码器层数
NUM_EPOCHS = 16  # 训练轮数

# 创建 Seq2SeqTransformer 模型实例
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)

# 将模型移动到设备上(如 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。

    Args:
    - model (Seq2SeqTransformer): Transformer 模型实例。
    - train_iter (torch.utils.data.DataLoader): 训练数据迭代器。
    - optimizer (torch.optim.Optimizer): 优化器实例。

    Returns:
    - float: 平均损失值。
    """
    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):
    """
    在验证集上评估模型。

    Args:
    - model (Seq2SeqTransformer): Transformer 模型实例。
    - val_iter (torch.utils.data.DataLoader): 验证数据迭代器。

    Returns:
    - float: 平均损失值。
    """
    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 = 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)

7.开始训练

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

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

这是代码:

# 训练模型多个 epoch
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
    start_time = time.time()
    # 训练一个 epoch,并计算训练损失
    train_loss = train_epoch(transformer, train_iter, optimizer)
    end_time = time.time()
    # 打印每个 epoch 的训练损失和训练时间
    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_mask传到计算设备(如GPU)
    src = src.to(device)
    src_mask = src_mask.to(device)
    
    # 编码输入序列,得到编码后的memory
    memory = model.encode(src, src_mask)
    
    # 初始化生成序列ys,起始符号为start_symbol
    ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
    
    for i in range(max_len-1):  # 逐步生成每一个词
        # 将memory传到计算设备
        memory = memory.to(device)
        
        # 创建memory mask,全零表示不屏蔽
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)
        
        # 生成目标序列的mask,用于防止模型看到未来的信息
        tgt_mask = (generate_square_subsequent_mask(ys.size(0))
                                    .type(torch.bool)).to(device)
        
        # 解码阶段:基于已生成的序列ys和memory预测下一个词
        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中
        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()
    
    # 对输入句子进行分词,并转化为ID序列
    tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX]
    num_tokens = len(tokens)
    
    # 将tokens转化为张量,并调整形状为(num_tokens, 1)
    src = torch.LongTensor(tokens).reshape(num_tokens, 1)
    
    # 创建src mask,全零表示不屏蔽
    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()
    
    # 将生成的ID序列转化为词语,并去掉特殊符号
    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)

 结果: 

 

9.保存Vocab对象和训练的模型 

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

import pickle

# 打开一个文件,用于存储数据
file = open('en_vocab.pkl', 'wb')

# 将英语词汇表(en_vocab)的信息写入文件
pickle.dump(en_vocab, file)

# 关闭文件
file.close()

# 打开一个文件,用于存储数据
file = open('ja_vocab.pkl', 'wb')

# 将日语词汇表(ja_vocab)的信息写入文件
pickle.dump(ja_vocab, file)

# 关闭文件
file.close()

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

# 保存模型以供推理使用

# 将transformer模型的状态字典保存到名为'inference_model'的文件中
torch.save(transformer.state_dict(), 'inference_model')

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

# 导入PyTorch库
import torch

# 将模型和训练相关信息保存到checkpoint文件以便稍后恢复训练

# 定义保存路径和文件名为'model_checkpoint.tar'
torch.save({
    'epoch': NUM_EPOCHS,                    # 保存当前训练的轮次数
    'model_state_dict': transformer.state_dict(),  # 保存模型的状态字典
    'optimizer_state_dict': optimizer.state_dict(),  # 保存优化器的状态字典
    'loss': train_loss,                     # 保存当前训练的损失值
    }, 'model_checkpoint.tar')

  结果: 

 

四.实验总结

在本次实验中基于Transformer实现机器翻译,了解了Transformer的原理和实现过程,实现了从日语到中文的翻译过程。完成了从导入相关库,获取数据集,准备标记器,构建TorchText Vocab对象并将句子转换为Torch张量,创建要在训练期间迭代的DataLoader对象,Sequence-to-sequence Transformer,训练模型,保存Vocab对象和训练的模型等过程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值