基于Transformer实现机器翻译(日译中)
函数首先遍历给定的日语和英语句子列表,对每一对句子进行以下处理步骤:去除行尾换行符,使用对应的分词器进行编码,然后将每个token(单词或子词)转换为词汇表中的索引,并将这些索引构建成Long类型的PyTorch张量。这两个类共同作用于NLP模型的输入层,其中TokenEmbedding负责将单词转换为词嵌入,而PositionalEncoding则在此基础上添加位置信息,两者结合使模型能够理解文
文章目录
- 基于Transformer & PyTorch 的中日机器翻译模型
-
- Import required packages
- Get the parallel dataset
- Prepare the tokenizers
- Build the TorchText Vocab objects and convert the sentences into Torch tensors
- Create the DataLoader object to be iterated during training
- Sequence-to-sequence Transformer
- Start training
- Try translating a Japanese sentence using the trained model
- Save the Vocab objects and trained model
- Conclusion
基于Transformer & PyTorch 的中日机器翻译模型
机器翻译是一项重要的自然语言处理任务,而Transformer模型是一种广泛应用于机器翻译任务的强大模型。它在2017年被提出,通过引入自注意力机制(self-attention)来解决了传统循环神经网络在长距离依赖建模上的限制。在本文中,我们将学习如何使用Transformer模型进行机器翻译。
以下是一个使用Jupiter 笔记本、 PyTorch、 Torchtext 和 SentencePiece 的教程
Import required packages
首先,让我们确保在我们的系统中安装了以下包,如果您发现有些包丢失,请务必安装它们。
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
device(type='cpu')
Get the parallel dataset
在本教程中,我们将使用从 JParaCrawl 下载的日语-英语并行数据集![ http://www.kecl.NTT.co.jp/icl/lirg/jparacrawl ]被描述为“ NTT 创建的最大的公开可用的英日平行语料库。它主要是通过网络爬行和自动对齐并行句创建的。”你也可以在这里看到那篇论文。
# 使用pandas库读取中日双语对照数据集文件
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 将数据集中第三列(索引为2)的中文句子转换为列表存储
trainen = df[2].values.tolist()# 示例中去除了前10000条数据的注释,实际使用时可取消注释部分
# 将数据集中第四列(索引为3)的日文句子转换为列表存储
trainja = df[3].values.tolist()# 同样,示例中去除了前10000条数据的注释
# 若有需要处理特定错误或不符合条件的样本,可以取消注释并调整索引
# trainen.pop(5972)# 从中文句子列表中移除索引为5972的元素
# trainja.pop(5972)# 对应地,从日文句子列表中也移除相同索引位置的元素
在导入所有的日语和英语对应项之后,我删除了数据集中的最后一个数据,因为它缺少一个值。两个网站的句子总数为5,973,071,但是,为了学习的目的,经常建议在一次性使用所有数据之前抽样数据并确保一切正常,以节省时间。
下面是数据集中包含的句子示例:
# 打印列表中第500个位置的中文句子
print(trainen[500])
# 打印列表中第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 ...
我们也可以使用不同的并行数据集来跟随本文,只是要确保我们可以将数据处理成上面所示的两个字符串列表,其中包含日语和英语句子。
这两行代码分别打印出了trainen列表(存储中文句子的列表)和trainja列表(存储日文句子的列表)中索引为500的元素,也就是第501个句子(因为Python列表索引是从0开始的)。这样做通常是为了检查数据加载是否正确,以及直观地了解数据集中句子的内容。
Prepare the tokenizers
与英语或其他按字母顺序排列的语言不同,日语句子不包含用于分隔单词的空格。我们可以使用 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')
['▁All',
'▁residents',
'▁aged',
'▁20',
'▁to',
'▁59',
'▁years',
'▁who',
'▁live',
'▁in',
'▁Japan',
'▁must',
'▁enroll',
'▁in',
'▁public',
'▁pension',
'▁system',
'.']
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str')
['▁',
'年',
'金',
'▁日本',
'に住んでいる',
'20',
'歳',
'~',
'60',
'歳の',
'全ての',
'人は',
'、',
'公的',
'年',
'金',
'制度',
'に',
'加入',
'しなければなりません',
'。']
Build the TorchText Vocab objects and convert the sentences into Torch tensors
然后,使用标记器和原始句子,构建从 TorchText 导入的词汇对象。这个过程可能需要几秒钟或几分钟,这取决于我们的数据集的大小和计算能力。不同的标记器也会影响构建单词所需的时间,我尝试了其他几个日语标记器,但 SentencePiece 似乎对我来说工作得很好,速度也足够快。
def build_vocab(sentences, tokenizer):
"""
构建词汇表。
参数:
- sentences: 句子列表,每个元素是一个字符串。
- tokenizer: 分词器,用于将句子拆分为单词或token。
返回:
- Vocab对象,包含了句子中所有不重复单词的词汇表,以及特定的特殊标记('<unk>', '<pad>', '<bos>', '<eos>')。
"""
# 初始化计数器,用于统计单词出现的频次
counter = Counter()
# 遍历句子列表
for sentence in sentences:
# 使用tokenizer对句子进行编码,获得单词列表,并更新计数器
# 注意:此处假设tokenizer.encode的输出被转换为了字符串类型以适配Counter
counter.update(tokenizer.encode(sentence, out_type=str))
# 根据单词频次创建词汇表,包含特殊标记
return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用自定义的ja_tokenizer处理日语文本数据,构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 使用自定义的en_tokenizer处理英语文本数据,构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)
这段代码定义了一个build_vocab函数,其作用是根据给定的句子列表和一个分词器(tokenizer),统计句子中所有单词的出现频次,并创建一个词汇表(Vocab)对象。
这个词汇表不仅包括了正常的单词,还特别包含了四个特殊标记:未知词标记、填充标记、句子开始标记和句子结束标记。
之后,该函数被调用来分别构建了日语和英语的词汇表。
在获得词汇表对象之后,我们可以使用词汇表和 tokenizer 对象为我们的训练数据构建张量。
def data_process(ja, en):
"""
数据处理函数,将原始的中日句子转换为张量格式,以便模型训练。
参数:
- ja: 日语文本列表。
- en: 英语文本列表。
返回:
- data: 包含日语和英语句子张量对的列表,每个对表示为(ja_tensor, en_tensor)。
"""
# 初始化一个空列表,用于存储处理后的数据对
data = []
# 使用zip函数同步遍历日语和英语句子列表
for (raw_ja, raw_en) in zip(ja, en):
# 对日语文本进行处理
# 使用ja_tokenizer对日语句子进行编码,然后将每个token转换为词汇表中的索引,并构建为张量
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对英语句子进行编码,然后将每个token转换为词汇表中的索引,并构建为张量
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)
这段代码定义了一个data_process函数,其目的是将原始的日语和英语句子转换为模型可以直接处理的张量格式。函数首先遍历给定的日语和英语句子列表,对每一对句子进行以下处理步骤:去除行尾换行符,使用对应的分词器进行编码,然后将每个token(单词或子词)转换为词汇表中的索引,并将这些索引构建成Long类型的PyTorch张量。处理完成后,将每一对日语和英语的张量作为一个元素添加到结果列表中。最后,调用这个函数处理训练数据,生成train_data,为模型训练做好数据准备。
Create the DataLoader object to be iterated during training
在这里,我将 BATCH _ SIZE 设置为16,以防止“内存不足”,但这取决于各种事情,如您的机器内存容量,数据大小等,所以随时根据您的需要更改批量大小(注意: PyTorch 的教程设置批量大小为128使用 Multi30k 德语-英语数据集)
# 定义批次大小
BATCH_SIZE = 8
# 获取词汇表中'<pad>', '<bos>', '<eos>'的索引
PAD_IDX = ja_vocab['<pad>'] # 填充符号的索引
BOS_IDX = ja_vocab['<bos>'] # 句子开始符号的索引
EOS_IDX = ja_vocab['<eos>'] # 句子结束符号的索引
def generate_batch(data_batch):
"""
生成批次数据的函数,为每个批次的数据添加开始与结束标志,并进行填充处理以确保批内各序列等长。
参数:
- data_batch: 一个批次的原始数据对列表,每个对包含两个张量(ja_item, en_item)。
返回:
- ja_batch: 处理后的日语批次数据,已添加<BOS>和<EOS>标记并进行了填充。
- en_batch: 处理后的英语批次数据,已添加<BOS>和<EOS>标记并进行了填充。
"""
# 初始化空列表用于存放处理后的日语和英语批次数据
ja_batch, en_batch = [], []
# 遍历批次中的每一对数据
for (ja_item, en_item) in data_batch:
# 对于每个句子,添加开始标志<BOS>在句首,结束标志<EOS>在句尾,并使用torch.cat拼接
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创建训练数据迭代器,指定批次大小、数据混洗以及自定义的生成批次函数
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创建了一个训练数据迭代器,它会在每次迭代时返回由generate_batch处理过的、大小为BATCH_SIZE的训练批次,并且数据会在每个epoch开始时被随机打乱,以增加模型训练时的泛化能力。
Sequence-to-sequence Transformer
接下来的几段代码和文本解释(用斜体写成)摘自最初的 PyTorch 教程[ https://PyTorch.org/tutorials/beginner/translation_transformer.html ]。除了 BATCH _ SIZE 和单词 de _ ocabwhich 被更改为 ja _ ocabb 之外,我没有做任何更改。
Transformer是一个 Seq2Seq 模型介绍了“注意力是你所需要的一切”文件,以解决机器翻译任务。Transformer模型由一个编码器和解码器块组成,每个编码器和解码器块包含固定数量的层。
编码器处理输入序列的传播,通过一系列的多头注意和前馈网络层。编码器的输出称为存储器,与目标张量一起被馈送到解码器。编码器和解码器是在一个端到端的方式使用强制教学培训。
[
](javascript:😉
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):
"""
初始化序列到序列的Transformer模型。
参数:
- num_encoder_layers: 编码器的层数
- num_decoder_layers: 解码器的层数
- emb_size: 词嵌入维度
- src_vocab_size: 源语言词汇表大小
- tgt_vocab_size: 目标语言词汇表大小
- dim_feedforward: 前馈网络的维度,默认为512
- dropout: Dropout比例,默认为0.1
"""
super(Seq2SeqTransformer, self).__init__()
# 初始化编码器层
encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
dim_feedforward=dim_feedforward)
# 创建编码器,由多个编码器层堆叠而成
self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
# 初始化解码器层
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: 源语言输入的张量
- trg: 目标语言输入的张量
- src_mask: 源语言序列的自注意力掩码
- tgt_mask: 目标语言序列的自注意力掩码,包括前瞻遮蔽
- src_padding_mask: 源语言序列的填充掩码
- tgt_padding_mask: 目标语言序列的填充掩码
- memory_key_padding_mask: 编码器输出的记忆掩码,用于处理padding
返回:
- 生成的目标语言序列的预测分布
"""
# 对源语言和目标语言输入添加位置编码
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):
"""
单独执行编码过程。
参数:
- src: 源语言输入的张量
- src_mask: 源语言序列的自注意力掩码
返回:
- 编码器的输出(记忆向量)
"""
return self.transformer_encoder(self.positional_encoding(
self.src_tok_emb(src)), src_mask)
def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
"""
给定编码器输出和目标语言的部分输入,执行解码过程。
参数:
- tgt: 目标语言部分输入的张量
- memory: 编码器的输出记忆
- tgt_mask: 解码器自注意力所需的掩码
返回:
- 解码器的输出
"""
return self.transformer_decoder(self.positional_encoding(
self.tgt_tok_emb(tgt)), memory,
tgt_mask)
定义了一个基于PyTorch的序列到序列(seq2seq)Transformer模型。它包括编码器(TransformerEncoder)和解码器(TransformerDecoder)两大部分,分别由多个编码器层和解码器层构成。模型通过词嵌入将输入序列转换为连续向量表示,应用位置编码以捕捉序列中单词的位置信息,然后通过Transformer架构进行编码和解码,最后通过一个线性层生成目标语言词汇的概率分布。此外,还提供了单独的encode和decode方法,允许分步执行编码和解码过程。
文本标记使用标记嵌入来表示。将位置编码添加到标记嵌入中,以引入词序的概念。
class PositionalEncoding(nn.Module):
"""
位置编码类,为模型输入添加位置信息。
参数:
- emb_size: 词嵌入维度
- dropout: Dropout比例
- maxlen: 最大序列长度,默认为5000
"""
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)
# 生成位置索引张量
pos = torch.arange(0, maxlen).reshape(maxlen, 1)
# 初始化位置嵌入矩阵,并分别计算偶数位置(sin)和奇数位置(cos)的值
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)
# 将位置嵌入注册为模型的缓冲区,不会被优化器更新
self.register_buffer('pos_embedding', pos_embedding)
def forward(self, token_embedding: Tensor):
"""
前向传播,将位置编码添加到词嵌入上。
参数:
- token_embedding: 输入的词嵌入张量
返回:
- 结合了位置信息的词嵌入张量
"""
# 将位置编码与词嵌入相加,并应用Dropout
return self.dropout(token_embedding +
self.pos_embedding[:token_embedding.size(0),:])
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
def forward(self, tokens: Tensor):
"""
前向传播,将词汇索引转换为词嵌入向量,并调整其幅度。
参数:
- tokens: 输入的词汇索引张量
返回:
- 调整幅度后的词嵌入张量
"""
# 获取词嵌入并乘以sqrt(emb_size)来缩放词嵌入的初始化范围
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
这段代码定义了两个类,PositionalEncoding 和 TokenEmbedding,它们是用于自然语言处理任务中Transformer模型的核心组件,旨在增强模型对序列中单词顺序的理解能力。
这两个类共同作用于NLP模型的输入层,其中TokenEmbedding负责将单词转换为词嵌入,而PositionalEncoding则在此基础上添加位置信息,两者结合使模型能够理解文本中单词的语义以及它们在句子中的相对位置,这是Transformer模型理解序列数据的基础。
我们创建一个后续的单词掩码来阻止目标单词注意到它的后续单词。我们还为屏蔽源和目标填充标记创建了掩码。
def generate_square_subsequent_mask(sz):
# 生成一个上三角矩阵,对角线及上方为1,下方为0
mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
# 将mask转换为float类型,并将原为0的位置替换为负无穷,1的位置替换为0
# 这样在后续softmax操作中,位置i之后的位置j的得分将被抑制(因为softmax后接近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)
# 创建源序列的自注意力掩码,这里全为False,意味着源序列中没有需要屏蔽的位置(除非特定情况下手动修改)
src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
# 创建源序列和目标序列的填充掩码,标记PAD索引所在的位置,用于在自注意力和编码器-解码器注意力中屏蔽PAD符号
# PAD_IDX通常代表句子结束或填充的特殊标记,这里假设PAD_IDX是已知的常量
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
这段代码主要实现了两种类型的掩码生成函数,用于Transformer模型中的注意力机制:
generate_square_subsequent_mask 生成一个上三角矩阵,用于在解码器的自注意力层屏蔽未来时刻的信息,确保预测第i个词时只能看到i时刻之前的词。
create_mask 函数综合生成了源序列自注意力掩码、目标序列的后续子序列掩码以及源序列和目标序列的填充掩码。填充掩码用于忽略输入中的PAD符号,避免它们对注意力分数产生影响。
Define model parameters and instantiate model. 这里我们服务器实在是计算能力有限,按照以下配置可以训练但是效果应该是不行的。如果想要看到训练的效果请使用你自己的带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 # 训练的轮数
# 初始化Seq2SeqTransformer模型
transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
FFN_HID_DIM)
# 使用Xavier初始化方法初始化模型参数
for p in transformer.parameters():
if p.dim() > 1:# 对多维参数(通常是权重矩阵)应用初始化
nn.init.xavier_uniform_(p)
# 将模型移动到预定义的设备上(如GPU)
transformer = transformer.to(device)
# 定义损失函数,忽略PAD_IDX位置的损失
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
)
# 定义训练一个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
logits = model(src, tgt_input, src_mask, tgt_mask,
src_padding_mask, tgt_padding_mask, src_padding_mask)
# 清零梯度
optimizer.zero_grad()
# 计算损失,只针对除了PAD之外的token
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)# 返回平均损失
以上代码段首先设置了模型的基本参数,然后初始化了一个Seq2SeqTransformer模型,并对其参数进行了Xavier初始化。接着定义了优化器、损失函数,并实现了训练一个epoch和评估模型性能的函数。在训练过程中,通过计算损失并反向传播来更新模型参数;在评估阶段,则不进行反向传播,仅用来评估模型在验证集上的性能。
Start training
最后,在准备好必要的类和函数之后,我们就可以开始训练模型了。这是不言而喻的,但是完成训练所需的时间可能会因计算能力、参数和数据集大小等许多因素而有很大的不同。
当我使用来自 JParaCrawl 的完整句子列表来训练这个模型时,每种语言大约有590万个句子,使用一个单独的 NVIDIA GeForce RTX 3070图形处理器每个时代大约需要5个小时。
代码如下:
# 使用tqdm库显示进度条,遍历每个训练轮次(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()
# 打印本轮次的信息,包括轮次号、训练损失以及该轮次所花费的时间
print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "# 显示当前epoch和训练损失(保留三位小数)
f"Epoch time = {(end_time - start_time):.3f}s"))# 显示该轮次训练耗时(秒,保留三位小数)
0%| | 0/16 [00:00<?, ?it/s]
此段代码执行模型的训练循环,利用tqdm.tqdm库动态地展示训练进度。对于每一个epoch(训练轮次),它记录开始时间,调用train_epoch函数进行训练并计算训练损失,然后记录结束时间以计算该轮次的持续时间。最后,打印出当前轮次的编号、训练损失值以及完成该轮次所需的总时间,帮助监控训练过程。
注:由于条件的不足,只有CPU,并不能支持训练成功,读者重点在于理解思想和过程。
Try translating a Japanese sentence using the trained model
首先,我们创建翻译新句子的函数,包括获取日语句子、标记、转换为张量、推理,然后将结果解码回一个句子,但这次是用英语。
def greedy_decode(model, src, src_mask, max_len, start_symbol):
src = src.to(device) # 源序列数据移到GPU
src_mask = src_mask.to(device) # 源序列遮罩移到GPU
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) # 确保记忆向量在GPU上
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 # 返回最终生成的词序列
#翻译函数Translate
def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
model.eval() # 确保模型处于评估模式
# 预处理源文本:添加开始和结束符号,然后转为索引表示
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).to(device) # 转为张量并移到GPU
src_mask = torch.zeros(num_tokens, num_tokens).type(torch.bool).to(device) # 创建源序列遮罩
# 使用贪心解码策略生成目标序列
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>", "")
这两段代码定义了贪婪解码(greedy_decode)和翻译(translate)的过程。
greedy_decode 函数实现了贪心解码策略,从给定的源序列逐步生成目标序列。它首先对源序列进行编码,然后基于开始符号逐步生成下一个最可能的词,直到达到最大长度或生成结束符号。
translate 函数则是将整个翻译流程封装起来,包括准备输入数据、设置模型为评估模式、调用贪心解码函数生成目标序列,最后将目标序列的索引转换回实际的单词序列,并去除特殊标记符号,返回翻译结果。
然后,我们可以调用平移函数并传递所需的参数。
#翻译调用实例
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
' ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ '
trainen.pop(5) # 从训练的英文数据集中移除第5个元素
'Chinese HS Code Harmonized Code System < HS编码 8515 : 电气(包括电热气体)、激光、其他光、光子束、超声波、电子束、磁脉冲或等离子弧焊接机器及装置,不论是否 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'
trainja.pop(5) # 从训练的日文数据集中移除第5个元素
'Japanese HS Code Harmonized Code System < HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)、レーザーその他の光子ビーム式、超音波式、電子ビーム式、 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'
Save the Vocab objects and trained model
最后,在培训结束后,我们将首先使用 Pickle 保存词汇表对象(en _ voab 和 ja _ voab)。
import pickle
# 打开一个文件,用于存储数据
# 'wb' 模式表示写入二进制数据
file = open('en_vocab.pkl', 'wb')
# 使用pickle库的dump方法将en_vocab字典的数据存储到打开的文件中
pickle.dump(en_vocab, file)
# 关闭文件,释放资源
file.close()
# 同样的操作,对ja_vocab字典进行存储
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()
这段代码使用Python的pickle模块将两个词汇表(en_vocab 和 ja_vocab)分别保存到两个文件中 (en_vocab.pkl 和 ja_vocab.pkl)。pickle模块允许将Python对象转化为字节流,从而可以保存到文件或者通过网络传输。这里采用二进制写入模式(‘wb’)打开文件,完成序列化过程后关闭文件,确保数据安全写入磁盘。
最后,我们还可以使用 PyTorch 保存和加载函数保存模型以供以后使用。一般来说,有两种方法可以保存模型,这取决于我们以后要使用它们的内容。第一个模型只是为了推理,我们可以稍后加载模型,然后用它来从日语翻译成英语。
# 保存模型状态字典以供推理
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')
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-44-f3085f2f03f6> in <module>
4 'model_state_dict': transformer.state_dict(),
5 'optimizer_state_dict': optimizer.state_dict(),
----> 6 'loss': train_loss,
7 }, 'model_checkpoint.tar')
NameError: name 'train_loss' is not defined
注:由于并未成功训练模型,此处出现运行错误,读者无须纠结。
Conclusion
再次强调,读者重点理解思想和过程即可。
以上便是全部,感谢阅读。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)