一、简介
-----------------------------------------------------------------------------------------------------------------------
机器翻译(MT)是自然语言处理(NLP)领域中的一个重要任务,其主要目的是将文本从一种语言自动翻译成另一种语言。随着全球化的发展,跨语言交流需求日益增加,机器翻译技术的应用也变得愈加广泛。无论是在商业、教育、还是旅游等领域,准确且高效的翻译系统都能大大提升跨文化交流的效率和质量。
自从2017年Google发表了“Attention is All You Need”论文以来,Transformer架构在机器翻译和其他NLP任务中迅速成为标准。这篇论文提出了一种全新的神经网络架构——Transformer,不仅在性能上超越了传统的循环神经网络(RNN)和长短时记忆网络(LSTM),还显著提高了训练速度。
-----------------------------------------------------------------------------------------------------------------------
Transformer的优势
Transformer之所以能够在机器翻译中取得如此显著的成就,主要得益于其独特的架构设计。传统的RNN和LSTM模型在处理长序列数据时存在诸多问题,例如长距离依赖问题和训练时间过长。而Transformer通过引入自注意力机制(Self-Attention),有效解决了这些问题。
-
自注意力机制(Self-Attention):自注意力机制允许模型在编码每个词时,能够关注输入序列中的所有其他词。这种机制不仅能捕捉到远距离词汇之间的关系,还能并行计算,提高了训练效率。
-
并行计算:Transformer架构摒弃了RNN中串行处理的方式,改用并行计算,大大加快了训练速度。模型的每一层都可以同时处理输入序列中的所有词,这使得Transformer在处理大规模数据时表现尤为出色。
-
多头注意力机制(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_IDX
、BOS_IDX
、EOS_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.TransformerEncoderLayer
和nn.TransformerDecoderLayer
分别定义了 Transformer 模型中的基本单元。这些单元包含多头自注意力机制和前馈神经网络。 -
编码器和解码器:通过堆叠多个编码器层和解码器层,我们构建了完整的 Transformer 编码器和解码器。
-
输出生成器:
self.generator
是一个线性层,将 Transformer 的输出映射到目标词汇表的概率分布。这个层的输出是每个时间步的词汇表中每个词的概率。 -
词嵌入和位置编码:
self.src_tok_emb
和self.tgt_tok_emb
分别为源语言和目标语言的词嵌入层。位置编码用于为每个词添加位置信息,以保留序列的顺序信息。
前向传播过程
- 词嵌入和位置编码:将输入的源句子和目标句子通过词嵌入层和位置编码层,得到带有位置信息的词嵌入表示。
- 编码源句子:将源句子的词嵌入表示输入到编码器中,得到编码表示(memory)。
- 解码目标句子:将目标句子的词嵌入表示和编码表示输入到解码器中,得到解码后的输出。
- 生成输出:将解码器的输出通过生成器映射到目标词汇表的概率分布。
通过这种架构设计,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_vocab
和 ja_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 的 save
和 load
函数保存和加载模型。一般来说,有两种保存模型的方法,具体取决于我们希望后续如何使用这些模型。
用于推理的模型
我们可以保存模型的状态字典,以便后续进行推理使用。
# 保存用于推理的模型
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 模型能够有效地捕捉序列中的全局依赖关系,从而在机器翻译任务中表现出色。
我们详细讨论了模型的各个组件,包括编码器和解码器的实现、位置编码和词嵌入的作用,以及掩码技术在防止信息泄露和处理填充标记中的重要性。
训练模型
我们通过定义损失函数和优化器,并编写训练和评估函数,对模型进行了多轮训练。在训练过程中,我们可以通过观察损失值的变化来评估模型的收敛情况。
模型推理和保存
在模型训练完成后,我们实现了贪心解码算法,用于生成翻译结果。我们还讨论了如何保存词汇表和模型,以便后续使用。通过保存模型的状态字典和优化器状态字典,我们可以方便地进行推理和继续训练。
希望本教程能为您构建和训练机器翻译模型提供有价值的指导。如果您在实现过程中遇到任何问题或有任何疑问,欢迎与我交流。