使用 Transformer 与 PyTorch 构建中日机器翻译模型

一、简介

-----------------------------------------------------------------------------------------------------------------------

机器翻译(MT)是自然语言处理(NLP)领域中的一个重要任务,其主要目的是将文本从一种语言自动翻译成另一种语言。随着全球化的发展,跨语言交流需求日益增加,机器翻译技术的应用也变得愈加广泛。无论是在商业、教育、还是旅游等领域,准确且高效的翻译系统都能大大提升跨文化交流的效率和质量。

自从2017年Google发表了“Attention is All You Need”论文以来,Transformer架构在机器翻译和其他NLP任务中迅速成为标准。这篇论文提出了一种全新的神经网络架构——Transformer,不仅在性能上超越了传统的循环神经网络(RNN)和长短时记忆网络(LSTM),还显著提高了训练速度。

-----------------------------------------------------------------------------------------------------------------------

Transformer的优势

Transformer之所以能够在机器翻译中取得如此显著的成就,主要得益于其独特的架构设计。传统的RNN和LSTM模型在处理长序列数据时存在诸多问题,例如长距离依赖问题和训练时间过长。而Transformer通过引入自注意力机制(Self-Attention),有效解决了这些问题。

  1. 自注意力机制(Self-Attention):自注意力机制允许模型在编码每个词时,能够关注输入序列中的所有其他词。这种机制不仅能捕捉到远距离词汇之间的关系,还能并行计算,提高了训练效率。

  2. 并行计算:Transformer架构摒弃了RNN中串行处理的方式,改用并行计算,大大加快了训练速度。模型的每一层都可以同时处理输入序列中的所有词,这使得Transformer在处理大规模数据时表现尤为出色。

  3. 多头注意力机制(Multi-Head Attention):通过多个注意力头,模型可以从不同的子空间中学习表示。这种多视角的关注方式使得模型能够捕捉到更丰富的语义信息,提高了翻译的准确性。

-----------------------------------------------------------------------------------------------------------------------

机器翻译的应用场景

机器翻译技术已经在诸多领域得到了广泛应用:

  • 商务和贸易:跨国公司在处理国际业务时,需要将各种合同、邮件和报告从一种语言翻译成另一种语言。高效的机器翻译系统可以大大提高工作效率,降低沟通成本。

  • 教育:随着国际交流的增加,教育领域对翻译的需求也在不断增长。学生和研究人员需要访问大量的外文资料,机器翻译可以帮助他们更快地获取所需信息。

  • 旅游:旅游者在异国他乡旅行时,常常需要与当地人进行交流。机器翻译应用程序能够实时翻译对话,帮助旅游者克服语言障碍。

-----------------------------------------------------------------------------------------------------------------------

本教程的目标

本教程旨在通过实际操作,详细介绍如何使用PyTorch和Transformer架构来构建一个日语-汉语机器翻译模型。我们将从数据预处理开始,逐步讲解模型的构建、训练和评估过程。希望通过本教程,读者不仅能掌握机器翻译的基本原理,还能学会如何使用最新的深度学习技术来实现高效的翻译系统。

接下来,我们将开始准备所需的工具和数据,为构建翻译模型做准备

-----------------------------------------------------------------------------------------------------------------------

前提条件

在深入了解模型实现之前,请确保系统中已安装以下软件包:

  • PyTorch
  • TorchText
  • SentencePiece
  • Pandas

如果没有,请使用以下命令安装它们:

pip install torch torchtext sentencepiece pandas

二. 数据准备

在本节中,我们将使用一个中日双语平行数据集来构建我们的机器翻译模型。该数据集包含日语句子及其对应的中文翻译。

部分展示如下:

-----------------------------------------------------------------------------------------------------------------------

www.maximintegrated.com    0.825    为了在3.4V电压下保持35%的PAE,还需要高达530mA的PA集电极电流: 28dBm RF功率: 102.8 mW = 631mW 所需PA功率(VCC x ICC): 631mW/(PAE/100) = 1803mW 3.4V VCC时所需PA ICC : ICC = 1803mW/3.4V = 530mA 要保证3.4V VCC和530mA ICC, DC-DC转换器要求输入和输出电压之间有一定的裕量,如果转换器内部的p沟道MOSFET (P-FET)的导通电阻为0.4Ω,电感电阻为0.1Ω元件串联后将产生:(0.4Ω+0.1Ω) x530mA = 265mV的压差,当电池电压降至3.665V以下时,DC-DC转换器将无法支持3.4V的输出。    3.4Vで35%のPAEを維持するには、530mAという大きなPA-コレクタ電流も必要となります: 28dBmのRF電力:102.8mW = 631mW 必要なPAの電力(VCC × ICC):631mW/(PAE/100) = 1803mW 3.4VのVCCで必要なPAのICC:ICC = 1803mW/3.4V = 530mA 3.4VのVCCと530mAのICCに対応するためには、PA電力用のDC-DCコンバータに、一定量の入力-出力ヘッドルームが必要となります。たとえば、コンバータの内部PチャネルMOSFET (P-FET)のオン抵抗が0.4Ωで、インダクタの抵抗が0.1Ωの場合、これら2個の直列部品の両端における電圧降下は(0.4Ω + 0.1Ω) × 530mA = 265mVになります。
www.hscode.org    0.823    Chinese HS Code Harmonized Code System < HS编码 9001 : 光导纤维及光导纤维束;光缆,但品目8544的货品除外;偏振材料制的片及板;未装配的各种材料制透镜(包括隐 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コード 9001 光ファイバー、光ファイバーケーブル、偏光材料製のシート及び板並びにレンズ HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...900110 光ファイバー(束にしたものを含む。

...........

-----------------------------------------------------------------------------------------------------------------------

1.1 读取和处理数据

首先,我们需要读取数据并将其转换为适合训练的格式。我们将使用 pandas 库来加载数据,并将其拆分为单独的日语和英语句子列表。

import pandas as pd

# 读取制表符分隔的文件
df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\t', engine='python', header=None)

# 提取中文和日文句子
train_zh = df[2].values.tolist()  # 中文训练数据
train_ja = df[3].values.tolist()  # 日文训练数据

print("中文句子样本:", train_zh[:5])
print("日文句子样本:", train_ja[:5])

输出结果:

1.2 分词

与字母语言不同,中文和日文句子中没有空格来分隔单词。为了解决这一问题,我们将使用 SentencePiece 库进行分词。SentencePiece 是一个适用于多种语言的分词工具,它能够很好地处理中文和日文文本,通过子词单元(subword units)将文本拆分为更小的单元,从而提高模型的处理效果。

import sentencepiece as spm

# 加载预训练的 SentencePiece 模型
zh_tokenizer = spm.SentencePieceProcessor(model_file='spm.zh.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')

# 测试分词
sample_zh = "所有20至59岁居住在日本的居民必须参加公共养老金制度。"
sample_ja = "年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。"

print("中文分词结果:", zh_tokenizer.encode(sample_zh, out_type=str))
print("日文分词结果:", ja_tokenizer.encode(sample_ja, out_type=str))

分词的目的是将句子拆分为单词或子词,从而更好地构建词汇表并进行后续的张量转换。在分词之后,我们需要构建词汇表以便将单词映射到唯一的索引。

1.3 构建词汇表

我们将使用 torchtext 库来构建词汇表,并将分词后的句子转换为张量格式。词汇表是将单词映射到索引的关键组件,能够有效地处理大量的文本数据。

import torch
import torchtext
from collections import Counter
from torchtext.vocab import 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>'])

# 根据中文训练数据构建中文词汇表
zh_vocab = build_vocab(train_zh, zh_tokenizer)

# 根据日文训练数据构建日文词汇表
ja_vocab = build_vocab(train_ja, ja_tokenizer)

print("中文词汇表大小:", len(zh_vocab))
print("日文词汇表大小:", len(ja_vocab))

1.4 转换为张量

我们将使用词汇表和分词器将句子转换为张量格式,以便输入到模型中进行训练。张量是深度学习框架中数据表示的主要形式,它能够高效地进行矩阵运算。

# 数据处理函数
def data_process(zh, ja):
    data = []
    for (raw_zh, raw_ja) in zip(zh, ja):
        zh_tensor_ = torch.tensor([zh_vocab[token] for token in zh_tokenizer.encode(raw_zh, out_type=str)], dtype=torch.long)
        ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja, out_type=str)], dtype=torch.long)
        data.append((zh_tensor_, ja_tensor_))
    return data

# 处理训练数据
train_data = data_process(train_zh, train_ja)

print("处理后的训练数据样本(前2个):", train_data[:2])

通过以上步骤,我们完成了数据的预处理,包括读取、分词、构建词汇表以及转换为张量。在接下来的部分中,我们将介绍如何构建和训练我们的 Transformer 模型。

三、构建和训练 Transformer 模型

3.1 定义批次大小和特殊标记的索引

为了有效地处理和训练序列数据,我们需要定义批次大小和一些特殊标记(如 <pad><bos><eos>)的索引。这些特殊标记在序列处理和填充时非常重要。通过设置批次大小,我们可以控制每次输入模型的数据量,从而提高训练效率并避免内存溢出等问题

from torch.nn.utils.rnn import pad_sequence
from torch.utils.data import 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:
        # 为每个日文句子添加BOS和EOS标记,并转换为张量
        ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
        # 为每个中文句子添加BOS和EOS标记,并转换为张量
        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)

在自然语言处理中,批量处理数据是提高训练效率的重要手段。通过批次大小的设置,我们可以在每个训练步骤中处理多个样本,从而更有效地利用计算资源。此外,适当的批次大小还能帮助稳定模型的训练过程,减少梯度波动,提高模型的收敛速度。

在上述代码中,我们首先定义了批次大小 BATCH_SIZE 和特殊标记的索引 PAD_IDXBOS_IDXEOS_IDX。然后,我们定义了一个 generate_batch 函数,用于生成批次数据。在这个函数中,我们为每个句子添加起始标记和结束标记,并将句子转换为张量。接着,我们使用 pad_sequence 函数将序列填充到相同长度。

最后,我们使用 DataLoader 创建数据加载器 train_iter,用于批量加载训练数据。通过设置 shuffle=True,我们可以在每个 epoch 随机打乱数据,从而提高模型的泛化能力。

-----------------------------------------------------------------------------------------------------------------------

3.2 构建 Seq2Seq Transformer 模型

Transformer 模型是由编码器和解码器组成的序列到序列(Seq2Seq)模型。它们各自包含多层的自注意力机制和前馈神经网络层。通过这种架构,Transformer 能够有效地捕捉序列中的全局依赖关系,并在机器翻译等任务中表现出色。

在此部分,我们将定义一个基于 Transformer 的 Seq2Seq 模型。

import torch.nn as nn
from torch import Tensor

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 = nn.TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD, dim_feedforward=dim_feedforward)
        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        
        # 定义 Transformer 解码器层
        decoder_layer = nn.TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD, dim_feedforward=dim_feedforward)
        self.transformer_decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
        
        # 定义输出生成器,将 Transformer 的输出映射到目标词汇表的概率分布
        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)
详细解释
  • 编码器层和解码器层nn.TransformerEncoderLayernn.TransformerDecoderLayer 分别定义了 Transformer 模型中的基本单元。这些单元包含多头自注意力机制和前馈神经网络。

  • 编码器和解码器:通过堆叠多个编码器层和解码器层,我们构建了完整的 Transformer 编码器和解码器。

  • 输出生成器self.generator 是一个线性层,将 Transformer 的输出映射到目标词汇表的概率分布。这个层的输出是每个时间步的词汇表中每个词的概率。

  • 词嵌入和位置编码self.src_tok_embself.tgt_tok_emb 分别为源语言和目标语言的词嵌入层。位置编码用于为每个词添加位置信息,以保留序列的顺序信息。

前向传播过程
  1. 词嵌入和位置编码:将输入的源句子和目标句子通过词嵌入层和位置编码层,得到带有位置信息的词嵌入表示。
  2. 编码源句子:将源句子的词嵌入表示输入到编码器中,得到编码表示(memory)。
  3. 解码目标句子:将目标句子的词嵌入表示和编码表示输入到解码器中,得到解码后的输出。
  4. 生成输出:将解码器的输出通过生成器映射到目标词汇表的概率分布。

通过这种架构设计,Transformer 模型能够高效地处理长距离依赖,并在机器翻译任务中实现高质量的翻译效果。在实际应用中,可以根据具体任务需求调整模型参数,如编码器层数、解码器层数、嵌入维度和注意力头数等。

-----------------------------------------------------------------------------------------------------------------------

3.3 定义位置编码和词嵌入

在 Transformer 模型中,位置编码和词嵌入是两个关键组件。词嵌入将每个词转换为一个固定维度的向量,而位置编码则为词嵌入添加位置信息,使模型能够捕捉序列中词语的顺序。

1、位置编码

位置编码是为了弥补 Transformer 模型中缺少序列顺序信息的不足。传统的循环神经网络(RNN)和卷积神经网络(CNN)在处理序列数据时天然包含顺序信息,而 Transformer 依赖于位置编码来保留顺序信息。

import math

class PositionalEncoding(nn.Module):
    def __init__(self, emb_size: int, dropout: float, 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)
        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), :])
详细解释
  • 初始化位置编码矩阵pos_embedding 矩阵的每一行对应序列中每个位置的编码,编码方式是固定的三角函数(sin 和 cos)组合。
  • 位置编码矩阵的注册:使用 register_buffer 将位置编码矩阵注册为模型的一个缓冲区(buffer),这意味着该矩阵不会作为模型参数进行更新。
  • 前向传播:在前向传播过程中,将位置编码添加到词嵌入上,并通过 Dropout 防止过拟合。

2、词嵌入

词嵌入是将离散的词转换为连续的向量表示,这些向量表示包含了词语的语义信息。

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)
详细解释
  • 嵌入层nn.Embedding 层将词汇表中的每个词映射到一个固定维度的向量。
  • 缩放嵌入向量:为了更稳定的训练,将嵌入向量乘以嵌入维度的平方根。

通过位置编码和词嵌入,Transformer 模型能够有效地捕捉序列中的顺序和语义信息。

综合说明

在自然语言处理中,词嵌入和位置编码是理解和生成文本的基础。词嵌入将每个词转换为高维向量,使模型能够处理文本数据。位置编码则提供了词语在序列中的位置信息,确保模型能够理解词语之间的顺序关系。

传统的 RNN 和 CNN 在处理序列数据时,会逐步处理每个时间步,天然地保留了序列顺序信息。而 Transformer 则通过全局自注意力机制一次性处理整个序列,这虽然提高了效率,但也使得模型需要显式地引入位置信息,这就是位置编码的作用。

在实现过程中,我们使用固定的三角函数来生成位置编码,这样的编码方式不仅简单高效,还能够为模型提供足够的位置信息。通过将位置编码添加到词嵌入上,模型在处理序列数据时就能同时考虑词语的语义和位置信息,从而更好地理解和生成自然语言。

-----------------------------------------------------------------------------------------------------------------------

3.4 生成掩码

在训练 Transformer 模型时,掩码(masking)是一个重要的步骤。掩码用于防止模型在训练时看到未来的单词信息(这在语言模型训练中是至关重要的),以及处理填充标记(padding tokens)以避免影响模型性能。

1、生成方形的下三角掩码

下三角掩码用于掩盖序列中未来的标记,确保每个时间步只能看到当前及之前的标记,从而防止信息泄露。

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
详细解释
  • 创建上三角矩阵:使用 torch.triu 创建一个上三角矩阵,其中上三角部分的元素为 1,其余部分为 0。
  • 转置矩阵:将上三角矩阵转置为下三角矩阵。
  • 掩码填充:将矩阵中值为 0 的位置填充为负无穷,值为 1 的位置填充为 0.0。负无穷确保这些位置的注意力得分在 softmax 操作中变得极小,从而被忽略。

2、创建源和目标序列的掩码

源序列和目标序列的掩码用于处理填充标记,以避免它们对模型性能产生负面影响。

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
详细解释
  • 目标序列的下三角掩码:确保每个位置只能看到当前及之前的标记,防止未来信息泄露。
  • 源序列的全零矩阵:因为源序列在编码阶段不需要进行未来标记的掩盖,所以使用全零矩阵。
  • 填充掩码:用于掩盖填充标记(padding tokens),避免它们在计算注意力时被考虑,从而提高模型性能。

综合说明

在自然语言处理任务中,掩码(masking)技术是确保模型训练效果的关键。通过掩盖未来标记和填充标记,掩码技术能够防止模型在训练时获取不合理的信息,从而提高模型的泛化能力和性能。

具体来说,下三角掩码用于防止模型在解码时看到未来的标记,这对于自回归模型(如语言模型)尤为重要。填充掩码则用于处理序列中的填充标记,避免它们对模型的注意力机制和损失计算产生负面影响。

通过上述掩码技术,我们可以确保 Transformer 模型在处理序列数据时既能捕捉全局依赖关系,又能避免不合理的信息泄露,从而在实际应用中取得更好的效果。

-----------------------------------------------------------------------------------------------------------------------

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

在构建 Seq2Seq Transformer 模型时,我们需要定义一些超参数(如嵌入维度、多头注意力机制中的头数、前馈神经网络的隐藏层维度等),并实例化模型。这些超参数直接影响模型的性能和训练效果。

1、定义超参数

首先,我们需要定义一些常量和超参数。

# 定义一些常量和超参数
SRC_VOCAB_SIZE = len(zh_vocab)  # 源语言词汇表大小
TGT_VOCAB_SIZE = len(ja_vocab)  # 目标语言词汇表大小
EMB_SIZE = 512  # 嵌入层大小
NHEAD = 8  # 多头注意力机制中的头数
FFN_HID_DIM = 512  # 前馈神经网络的隐藏层大小
BATCH_SIZE = 16  # 批次大小
NUM_ENCODER_LAYERS = 3  # 编码器层数
NUM_DECODER_LAYERS = 3  # 解码器层数
NUM_EPOCHS = 16  # 训练轮数

这些超参数的设置需要根据具体的任务、数据集和硬件资源来调整。例如,较大的嵌入维度和更多的编码器/解码器层数通常能提高模型性能,但也会增加计算资源的消耗。

2、实例化 Seq2Seq Transformer 模型

接下来,我们实例化 Seq2Seq Transformer 模型,并对模型参数进行初始化。

# 初始化 Seq2Seq Transformer 模型
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)

在上述代码中,我们使用了 nn.init.xavier_uniform_ 对模型参数进行初始化。这种初始化方法能使模型在训练初期更快地收敛,并提高训练效果。

3、将模型移动到设备

为了加速训练,我们通常会将模型移动到 GPU 上。如果没有 GPU,则使用 CPU。

# 将模型移动到 GPU 或 CPU 上
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
transformer = transformer.to(device)

4、定义损失函数和优化器

我们使用交叉熵损失函数来计算模型输出与目标之间的误差。为了避免填充标记对损失计算的影响,我们在定义损失函数时忽略填充标记。此外,我们使用 Adam 优化器来优化模型参数。

# 定义损失函数,忽略填充标记的损失
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
)

5、训练一个 epoch 的函数

我们定义一个函数,用于训练模型的一个 epoch。该函数将模型设置为训练模式,然后遍历训练数据,对每个批次进行前向传播、计算损失、反向传播和参数更新。

# 训练一个 epoch 的函数
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)

6、评估模型的函数

我们还定义一个函数,用于评估模型在验证集上的性能。该函数与训练函数类似,但不进行参数更新。

# 评估模型的函数
def evaluate(model, val_iter):
    model.eval()
    losses = 0
    with torch.no_grad():
        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)

四、训练

在准备好必要的类和函数后,我们就可以开始训练我们的模型了。需要注意的是,训练所需的时间因计算能力、参数设置和数据集大小等因素而异。

例如,当我使用完整的 JParaCrawl 数据集(每种语言大约有 590 万个句子)进行训练时,每个 epoch 需要大约 5 小时(使用 NVIDIA GeForce RTX 3070 GPU)。下面是训练模型的代码:

import time
import tqdm

# 使用 tqdm 库显示训练进度条,训练模型 NUM_EPOCHS 次
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"))

在训练过程中,我们可以通过观察每个 epoch 的损失值和耗时,来评估模型的收敛情况。如果损失值持续下降,说明模型在不断学习和优化。

使用训练好的模型进行翻译

在模型训练完成后,我们可以使用它进行翻译任务。我们首先定义一些函数,用于处理输入句子的分词、转换为张量、进行推断,并将结果解码为目标语言的句子。

贪心解码函数

贪心解码是一种简单的解码策略,它在每一步选择概率最大的词,直到生成结束标记。

# 贪心解码函数,用于生成翻译
def greedy_decode(model, src, src_mask, max_len, start_symbol):
    src = src.to(device)  # 将源序列移动到设备(GPU 或 CPU)
    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)  # 初始化目标序列,起始标记为 start_symbol
    for i in range(max_len - 1):  # 循环生成每个词
        memory = memory.to(device)  # 将 memory 移动到设备
        memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool)  # 创建 memory 掩码
        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:  # 如果生成的词是 EOS,则停止生成
            break
    return ys  # 返回生成的目标序列
翻译函数

该函数用于翻译输入的源句子,并返回翻译结果。

# 翻译函数
def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
    model.eval()  # 设置模型为评估模式
    # 将源句子分词并转换为词汇索引,同时添加 BOS 和 EOS 标记
    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()  # 贪心解码生成目标序列
    # 将目标序列中的索引转换回词,并去除 BOS 和 EOS 标记
    return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")

然后,我们可以调用 translate 函数并传递必要的参数进行翻译。

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

# 打印翻译结果
print(result)

保存词汇表和训练好的模型

在训练完成后,我们需要保存词汇表和模型,以便后续使用。

保存词汇表

我们使用 pickle 库保存词汇表对象(en_vocabja_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 的 saveload 函数保存和加载模型。一般来说,有两种保存模型的方法,具体取决于我们希望后续如何使用这些模型。

用于推理的模型

我们可以保存模型的状态字典,以便后续进行推理使用。

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

用于继续训练的模型

如果我们希望后续能够继续训练模型,我们还需要保存优化器的状态字典和当前 epoch 等信息。

# 保存模型和检查点以便后续继续训练
torch.save({
  'epoch': NUM_EPOCHS,
  'model_state_dict': transformer.state_dict(),
  'optimizer_state_dict': optimizer.state_dict(),
  'loss': train_loss,
  }, 'model_checkpoint.tar')

通过以上步骤,我们已经完成了模型的训练、翻译功能的实现以及模型的保存。

五、总结

在本教程中,我们详细介绍了如何使用 PyTorch 和 Transformer 架构构建一个中日机器翻译模型。整个过程包括数据的读取和预处理、构建词汇表、定义和训练 Transformer 模型,以及使用训练好的模型进行翻译和保存模型。

数据准备

我们首先从双语平行数据集中读取数据,进行分词处理,并构建词汇表。通过分词和构建词汇表,我们能够将文本数据转换为适合模型处理的格式。

构建和训练 Transformer 模型

接着,我们定义了一个基于 Transformer 的 Seq2Seq 模型,包括位置编码和词嵌入的实现。通过自注意力机制,Transformer 模型能够有效地捕捉序列中的全局依赖关系,从而在机器翻译任务中表现出色。

我们详细讨论了模型的各个组件,包括编码器和解码器的实现、位置编码和词嵌入的作用,以及掩码技术在防止信息泄露和处理填充标记中的重要性。

训练模型

我们通过定义损失函数和优化器,并编写训练和评估函数,对模型进行了多轮训练。在训练过程中,我们可以通过观察损失值的变化来评估模型的收敛情况。

模型推理和保存

在模型训练完成后,我们实现了贪心解码算法,用于生成翻译结果。我们还讨论了如何保存词汇表和模型,以便后续使用。通过保存模型的状态字典和优化器状态字典,我们可以方便地进行推理和继续训练。

希望本教程能为您构建和训练机器翻译模型提供有价值的指导。如果您在实现过程中遇到任何问题或有任何疑问,欢迎与我交流。

  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值