导入所需的软件包
import math # 导入数学库,用于数学计算
import torchtext # 导入torchtext库,用于文本处理
import torch # 导入PyTorch库,用于深度学习
import torch.nn as nn # 导入PyTorch的神经网络模块
from torch import Tensor # 从torch中导入Tensor类,用于创建张量
from torch.nn.utils.rnn import pad_sequence # 从torch.nn.utils.rnn中导入pad_sequence函数,用于对序列进行填充
from torch.utils.data import DataLoader # 从torch.utils.data中导入DataLoader类,用于加载数据
from collections import Counter # 从collections中导入Counter类,用于统计元素出现次数
from torchtext.vocab import Vocab # 从torchtext.vocab中导入Vocab类,用于创建词汇表
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer # 从torch.nn中导入Transformer相关的类和层
import io # 导入io库,用于文件读写
import time # 导入time库,用于时间操作
import pandas as pd # 导入pandas库,用于数据处理
import numpy as np # 导入numpy库,用于数值计算
import pickle # 导入pickle库,用于序列化和反序列化
import tqdm # 导入tqdm库,用于显示进度条
import sentencepiece as spm # 导入sentencepiece库,用于文本分词
torch.manual_seed(0) # 设置随机数种子,保证实验结果可重复
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 根据GPU是否可用,设置设备为GPU或CPU
# print(torch.cuda.get_device_name(0)) ##GPU # 打印GPU设备名称(注释掉的代码)
device
获取并行数据集
我们将使用从JParaCrawl![http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl]]下载的日语-英语并行数据集,该数据集被描述为“NTT创建的最大的公开可用的英语-日语并行语料库”。它主要是通过抓取网络并自动对齐平行句子创建的。”
# 读取txt文件,使用'\t'作为分隔符,使用Python引擎,没有表头
df = pd.read_csv('home/zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 将第3列的数据转换为列表,赋值给trainen
trainen = df[2].values.tolist()#[:10000]
# 将第4列的数据转换为列表,赋值给trainja
trainja = df[3].values.tolist()#[:10000]
# 从trainen中删除索引为5972的元素
# trainen.pop(5972)
# 从trainja中删除索引为5972的元素
# trainja.pop(5972)
pd.read_csv
:读取并行语料库文件。sep='\\t'
:指定文件的分隔符为制表符。engine='python'
:使用Python引擎进行解析。header=None
:文件没有标题行。values.tolist()
:将数据转换为列表形式。
在导入所有日语和英语对应项之后,可删除数据集中的最后一个数据,因为它有一个缺失的值。总的来说,trainen和trainja中的句子数量都是5,973,071,然而,出于学习目的,通常建议在一次使用所有数据之前对数据进行采样并确保一切正常工作,以节省时间。
print(trainen[500])
print(trainja[500])
准备标记器
与英语或其他按字母顺序排列的语言不同,日语句子不包含空格来分隔单词。我们可以使用JParaCrawl提供的标记器,它是使用sentencepece为日语和英语创建的,可以访问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')
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
构建TorchText Vocab对象并将句子转换为Torch张量
使用标记器和原始句子,然后构建从TorchText导入的Vocab对象。根据数据集的大小和计算能力,这个过程可能需要几秒钟或几分钟。不同的标记器也会影响构建词汇所需的时间,尝试了其他几个日语标记器,但sensenepece似乎工作得很好。
# 定义一个函数 build_vocab,用于构建词汇表
def build_vocab(sentences, tokenizer):
# 创建一个计数器对象
counter = Counter()
# 遍历输入的句子列表
for sentence in sentences:
# 使用分词器对句子进行编码,并更新计数器
counter.update(tokenizer.encode(sentence, out_type=str))
# 返回一个词汇表对象,包含特殊符号 '<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)
Counter
:统计每个标记出现的频次。Vocab
:创建包含特殊符号的词汇表对象。
在我们有了词汇表对象之后,我们可以使用词汇表和标记器对象来为我们的训练数据构建张量。
# 定义一个名为data_process的函数,接收两个参数:ja和en
def data_process(ja, en):
# 初始化一个空列表data
data = []
# 使用zip函数将ja和en中的元素一一对应地组合在一起,然后遍历这些组合
for (raw_ja, raw_en) in zip(ja, en):
# 对原始的日语文本进行分词,并将分词结果转换为对应的词汇表索引,然后将这些索引转换为LongTensor类型的张量
ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("
"), out_type=str)],
dtype=torch.long)
# 对原始的英语文本进行分词,并将分词结果转换为对应的词汇表索引,然后将这些索引转换为LongTensor类型的张量
en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("
"), out_type=str)],
dtype=torch.long)
# 将处理好的日语和英语张量作为元组添加到data列表中
data.append((ja_tensor_, en_tensor_))
# 返回处理后的数据列表
return data
# 调用data_process函数处理训练数据,并将结果赋值给train_data变量
train_data = data_process(trainja, trainen)
创建要在训练期间迭代的DataLoader对象
在这里,将BATCH_SIZE设置为16以防止“cuda内存不足”,但这取决于各种事情,例如机器内存容量,数据大小等,因此可以根据需要随意更改批大小(注意:PyTorch的教程使用Multi30k德语-英语数据集将批大小设置为128)。
# 设置批量大小为8
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:
# 将开始索引、日语项目和结束索引连接起来,并添加到日语批次列表中
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
# 创建训练迭代器,输入为训练数据,批量大小为BATCH_SIZE,打乱顺序,并使用generate_batch函数作为collate_fn参数
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
:生成批次数据并进行填充。DataLoader
:创建训练迭代器。
Sequence-to-sequence Transformer
接下来的代码和文本解释(以斜体书写)来自原始的PyTorch教程[https://pytorch.org/tutorials/beginner/translation_transformer.html]]。除了BATCH_SIZE和单词de_vocab被更改为ja_vocab之外,没有做任何更改。
Transformer是在“Attention is all you need”论文中提出的用于解决机器翻译任务的Seq2Seq模型。变压器模型由编码器和解码器块组成,每个块包含固定数量的层。
编码器通过一系列多头注意和前馈网络层对输入序列进行传播处理。编码器的输出称为存储器,与目标张量一起馈送到解码器。编码器和解码器以端到端方式使用教师强迫技术进行培训。
# 导入PyTorch中的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__()
# 创建Transformer编码器层
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)
# 创建Transformer解码器层
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):
return self.transformer_decoder(self.positional_encoding(
self.tgt_tok_emb(tgt)), memory,
tgt_mask)
__init__
:初始化模型,包括编码器、解码器和生成器。forward
:前向传播函数。encode
:编码函数。decode
:解码函数。
文本标记通过使用标记嵌入表示。位置编码被添加到标记嵌入中以引入词序的概念。
emb_size
: 词嵌入的维度。dropout
: 丢弃率,用于防止过拟合。maxlen
: 位置编码的最大长度,默认为5000。den
:计算位置编码中正弦和余弦函数的分母部分。pos
:生成位置序列。pos_embedding
:初始化位置嵌入矩阵,并根据位置和嵌入维度计算正弦和余弦函数的值。正弦函数应用于偶数列,余弦函数应用于奇数列。self.dropout
:初始化dropout层。self.register_buffer('pos_embedding', pos_embedding)
:将位置嵌入矩阵注册为buffer,这样它在模型保存和加载时不会被认为是模型参数。token_embedding
:输入的词嵌入张量。vocab_size
: 词汇表的大小,即不同词的数量。emb_size
: 词嵌入的维度。self.embedding
: 初始化词嵌入层,其维度为(vocab_size, emb_size)
。self.emb_size
: 记录词嵌入的维度。
# 定义位置编码类,继承自nn.Module
class PositionalEncoding(nn.Module):
# 初始化函数,输入参数为emb_size(嵌入维度)、dropout(丢弃率)和maxlen(最大长度,默认为5000)
def __init__(self, emb_size: int, dropout, 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)
# 初始化dropout层
self.dropout = nn.Dropout(dropout)
# 注册位置嵌入矩阵为buffer
self.register_buffer('pos_embedding', pos_embedding)
# 前向传播函数,输入参数为token_embedding(词嵌入张量)
def forward(self, token_embedding: Tensor):
# 返回经过dropout处理的位置嵌入与词嵌入相加后的结果
return self.dropout(token_embedding +
self.pos_embedding[:token_embedding.size(0),:])
# 定义词嵌入类,继承自nn.Module
class TokenEmbedding(nn.Module):
# 初始化函数,输入参数为vocab_size(词汇表大小)和emb_size(嵌入维度)
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
# 前向传播函数,输入参数为tokens(词索引张量)
def forward(self, tokens: Tensor):
# 返回经过词嵌入层处理后的词嵌入张量乘以嵌入维度的平方根
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
- 位置编码类
PositionalEncoding
:用于生成位置编码,以保留序列中每个位置的信息。它在初始化时计算正弦和余弦函数的值,并在前向传播时将位置编码添加到词嵌入上。 - 词嵌入类
TokenEmbedding
:用于生成词嵌入向量。它将输入的词索引转换为词嵌入向量,并对嵌入进行缩放。
我们创建一个后续单词掩码来阻止目标单词关注它的后续单词。我们还创建遮罩,用于屏蔽源和目标填充令牌
generate_square_subsequent_mask
:生成一个用于目标序列的上三角掩码矩阵,确保在序列处理时每个位置只关注当前及之前的位置。create_mask
:生成源序列和目标序列的掩码,包括序列的填充掩码和目标序列的后续位置掩码。这些掩码用于确保模型在处理序列时不会看到未来的位置,并正确处理填充符号。
# 定义一个函数,用于生成平方矩阵的掩码
def generate_square_subsequent_mask(sz):
# 创建一个上三角矩阵,元素为1,其余为0
mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
# 将掩码中的0替换为负无穷,1替换为0
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)
# 创建一个全零矩阵,用于表示源序列的掩码
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
sz
:矩阵的大小,即序列的长度。mask
:创建一个上三角矩阵,元素为1,其余为0。上三角矩阵是通过torch.triu
函数生成的,它返回一个上三角矩阵。transpose(0, 1)
:将矩阵进行转置,以便生成的上三角矩阵满足序列掩码的要求。mask.float().masked_fill(mask == 0, float('-inf'))
:将掩码矩阵中的0替换为负无穷。负无穷表示在计算注意力分数时这些位置会被忽略。.masked_fill(mask == 1, float(0.0))
:将掩码矩阵中的1替换为0。0表示这些位置是有效的。- 返回处理后的掩码矩阵。
src
:源序列。tgt
:目标序列。src_seq_len
:源序列的长度。tgt_seq_len
:目标序列的长度。tgt_mask
:调用generate_square_subsequent_mask
函数生成目标序列的掩码,以确保每个位置的预测只依赖于当前位置及之前的位置。src_mask
:创建一个全零矩阵,表示源序列的掩码。对于源序列,不需要后续位置的掩码,因此使用全零矩阵。src_padding_mask
:生成源序列的填充掩码。通过检查源序列中是否存在填充符号(假设填充符号索引为PAD_IDX
),并进行转置以适应模型输入的形状要求。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
# 设置批量大小
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或CPU)
transformer = transformer.to(device)
# 定义损失函数,忽略PAD_IDX位置的损失
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):
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):
model.eval()
losses = 0
for idx, (src, tgt) in (enumerate(valid_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)
model.train()
:将模型设置为训练模式。for idx, (src, tgt) in enumerate(train_iter)
:遍历训练数据集。src
和tgt
:将源序列和目标序列移动到设备上。tgt_input
:目标序列去掉最后一个元素,作为输入。create_mask
:生成源序列和目标序列的掩码。logits
:模型的输出。optimizer.zero_grad()
:清空梯度。tgt_out
:目标序列去掉第一个元素,作为输出。loss
:计算损失。loss.backward()
:反向传播计算梯度。optimizer.step()
:更新参数。
model.eval()
:将模型设置为评估模式。- 与
train_epoch
类似,但不进行反向传播和参数更新,只计算损失。
开始训练
最后,在准备好必要的类和函数之后,我们准备训练我们的模型。但是完成训练所需的时间可能会有很大的不同,这取决于很多事情,比如计算能力、参数和数据集的大小。
当我使用JParaCrawl(每种语言大约有590万个句子)的完整句子列表来训练模型时,使用单个NVIDIA GeForce RTX 3070 GPU,每个epoch大约需要5个小时。
通过tqdm
库创建一个进度条,显示训练过程的进度,并在每轮训练结束后打印当前轮数、训练损失值以及本轮训练所需的时间。这可以帮助我们更好地监控训练过程,了解模型的收敛情况和每轮训练所需的时间。
代码如下:
# 使用tqdm库创建一个进度条,用于显示训练轮数的进度
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
# 记录当前时间,用于计算每轮训练所需的时间
start_time = time.time()
# 调用train_epoch函数进行一轮训练,并返回训练损失值
train_loss = train_epoch(transformer, train_iter, optimizer)
# 记录当前时间,用于计算每轮训练所需的时间
end_time = time.time()
# 打印当前轮数、训练损失值以及本轮训练所需时间
print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
f"Epoch time = {(end_time - start_time):.3f}s"))
- 打印当前轮数(
epoch
)、训练损失值(train_loss
)以及本轮训练所需时间(end_time - start_time
)。 train_loss:.3f
表示将训练损失值保留三位小数。(end_time - start_time):.3f
表示将时间差保留三位小数。
试着用训练好的模型翻译一个日语句子
首先,我们创建翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理,然后将结果解码回句子,但这次是英语。
# 定义贪婪解码函数
def greedy_decode(model, src, src_mask, max_len, start_symbol):
# 将输入数据放到设备上
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):
# 对源序列进行编码
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>", "")
-
贪婪解码函数参数解释:
model
: 经过训练的Seq2SeqTransformer模型。src
: 源序列的张量表示。src_mask
: 源序列的掩码。max_len
: 生成目标序列的最大长度。start_symbol
: 目标序列的起始符号。
-
功能:
- 将源序列
src
通过编码器编码得到memory
。 - 初始化目标序列
ys
,起始为起始符号start_symbol
。 - 使用循环生成目标序列:
- 创建目标序列的掩码,确保只能生成当前位置之前的单词。
- 解码生成输出,计算概率分布并选择概率最大的单词作为下一个单词。
- 更新目标序列,并在遇到结束符号
EOS_IDX
时停止生成。
- 将源序列
-
翻译函数参数解释:
model
: 经过训练的Seq2SeqTransformer模型。src
: 源语言文本字符串。src_vocab
: 源语言词汇表。tgt_vocab
: 目标语言词汇表。src_tokenizer
: 源语言文本的分词器。
-
功能:
- 使用分词器
src_tokenizer
对源语言文本src
进行分词并映射到词汇表src_vocab
中的索引,同时添加起始符号BOS_IDX
和结束符号EOS_IDX
。 - 将处理后的序列转换为PyTorch张量,并创建相应的掩码
src_mask
。 - 使用
greedy_decode
函数生成目标语言序列的张量表示tgt_tokens
。 - 将生成的目标语言序列转换为字符串并返回,去除特殊标记
<bos>
和<eos>
。
- 使用分词器
然后,我们可以调用翻译函数并传递所需的参数。
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
trainen.pop(5)
trainja.pop(5)
保存Vocab对象和训练好的模型
最后,在训练完成后,我们将首先使用Pickle保存Vocab对象(en_vocab和ja_vocab)。
import pickle
# 导入pickle模块,用于序列化和反序列化Python对象
file = open('en_vocab.pkl', 'wb')
# 以二进制写入模式('wb')打开名为'en_vocab.pkl'的文件
pickle.dump(en_vocab, file)
# 使用pickle.dump()函数将en_vocab对象序列化并写入到文件中
file.close()
# 关闭文件,释放资源
file = open('ja_vocab.pkl', 'wb')
# 以二进制写入模式('wb')打开名为'ja_vocab.pkl'的文件
pickle.dump(ja_vocab, file)
# 使用pickle.dump()函数将ja_vocab对象序列化并写入到文件中
file.close()
# 关闭文件,释放资源
最后,我们还可以使用PyTorch保存和加载函数保存模型以供以后使用。通常,有两种保存模型的方法,这取决于我们以后想要使用它们的目的。第一个仅用于推理,我们可以稍后加载模型并使用它从日语翻译成英语。
# save model for inference
torch.save(transformer.state_dict(), 'inference_model')
第二个也是用于推理的,但当我们稍后想要加载模型并想要恢复训练时也是如此。
# 保存模型和检查点以便稍后恢复训练
torch.save({
'epoch': NUM_EPOCHS, # 当前训练的轮数
'model_state_dict': transformer.state_dict(), # 模型的状态字典,包含模型的参数
'optimizer_state_dict': optimizer.state_dict(), # 优化器的状态字典,包含优化器的参数
'loss': train_loss, # 当前训练的损失值
}, 'model_checkpoint.tar') # 保存的文件名为'model_checkpoint'
总结
总的来说这次实现了一个基于Transformer架构的中文和日文机器翻译模型。使用了PyTorch框架,包括位置编码、词嵌入、掩码生成、模型定义、训练和评估等步骤。
位置编码(PositionalEncoding)用于为输入序列中的每个位置添加唯一的信息,使模型能够识别序列中的位置信息。通过使用正弦和余弦函数生成位置编码矩阵,并在前向传播过程中将其与词嵌入相加,实现这一功能。词嵌入(TokenEmbedding)层则将词汇表中的每个单词映射到一个高维向量空间中,创建词嵌入层,并将输入的词索引转换为词嵌入向量。
掩码生成是Transformer模型中不可或缺的一部分,用于防止模型在解码时看到未来的单词。generate_square_subsequent_mask函数生成一个上三角矩阵的掩码,确保模型仅能看到当前和之前的单词。create_mask函数则为源序列和目标序列创建填充掩码和目标序列的未来掩码。
模型定义和初始化部分,通过设置词汇表大小、嵌入层大小、多头注意力机制头数、前馈神经网络隐藏层维度、批量大小、编码器和解码器层数以及训练轮数等超参数,创建Seq2SeqTransformer模型实例,并初始化模型参数。模型参数初始化使用了Xavier均匀分布,以确保参数在训练初期的适当分布。
在训练过程中,定义了train_epoch函数,每个训练周期都会进行前向传播、计算损失、反向传播和参数更新。使用Adam优化器对模型参数进行优化,并使用交叉熵损失函数来计算损失。每个训练轮结束时,打印当前轮数、训练损失值以及训练所需时间。
评估模型性能时,定义了evaluate函数,对验证数据进行前向传播,计算损失值以评估模型性能。
在整个训练过程中,使用了tqdm库创建进度条,显示训练轮数的进度。每轮训练前后记录时间,以计算每轮训练所需的时间,并打印当前轮数、训练损失值以及训练所需时间。
最后,定义了贪婪解码函数greedy_decode,用于生成目标序列。该函数先对源序列进行编码,然后循环生成目标序列中的每个单词,直至生成结束符或达到最大长度。translate函数使用贪婪解码函数对源序列进行翻译,并将生成的目标序列转换为字符串输出。
通过上述步骤,实现了一个完整的中文和日文机器翻译模型,能够对源序列进行编码、解码,并生成翻译结果。