一. 介绍Transformer
机器翻译是利用计算机将一种自然语言(源语言)转换为另一种自然语言(目标语言)的过程。
本项目是在PyTorch被用来构建和训练一个序列到序列的Transformer模型,用于日语到英语的机器翻译任务。该教程涵盖了模型的各个组成部分,包括数据的准备、模型的定义、训练过程以及如何使用训练好的模型进行翻译。
Transformer 是论文 Attention Is All You Need 中提出的用以完成机器翻译(Machine Translation)等序列到序列(Seq2Seq)学习任务的一种全新网络结构,其完全使用注意力(Attention)机制来实现序列到序列的建模。
相较于此前 Seq2Seq 模型中广泛使用的循环神经网络(Recurrent Neural Network, RNN),使用Self Attention进行输入序列到输出序列的变换主要具有以下优势:
- 计算复杂度小
- 特征维度为 d 、长度为 n 的序列,在 RNN 中计算复杂度为 O(n * d * d) (n 个时间步,每个时间步计算 d 维的矩阵向量乘法),在 Self-Attention 中计算复杂度为 O(n * n * d) (n 个时间步两两计算 d 维的向量点积或其他相关度函数),n 通常要小于 d 。
- 计算并行度高
- RNN 中当前时间步的计算要依赖前一个时间步的计算结果;Self-Attention 中各时间步的计算只依赖输入不依赖之前时间步输出,各时间步可以完全并行。
- 容易学习长距离依赖(long-range dependencies)
- RNN 中相距为 n 的两个位置间的关联需要 n 步才能建立;Self-Attention 中任何两个位置都直接相连;路径越短信号传播越容易。 Transformer 中引入使用的基于 Self-Attention 的序列建模模块结构,已被广泛应用在 Bert 等语义表示模型中,取得了显著效果。
二. 准备环境
2.1 安装所需的Python包和环境配置
https://blog.51cto.com/u_16099358/8789563https://blog.51cto.com/u_16099358/8789563采用kaggle软件,具体使用流程参考上方教程。
环境为GPU T4 X2
确保PyTorch、Torchtext、SentencePiece等库已经安装在系统中。这些库是进行机器翻译任务的基础,其中PyTorch提供了强大的深度学习框架,Torchtext提供了文本数据的处理工具,而SentencePiece则是用于文本分词的库。
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))
!pip install torchtext==0.6.0
安装了torchtext==0.6.0,安装之后可能需要重启内核,不然内核里面的torchtext还是之前的版本。你可以通过打印torchtext.__version__查看当前加载到内核中的版本
python:3.8
torch: 1.11.0+cu113
torchvision: 0.12.0+cu113
torchaudio: 0.11.0
numpy: >=1.20.0
matplotlib: >=3.4.0
scikit-learn: >=0.24.0
pandas: >=1.3.0
jupyterlab: >=3.2.0
tqdm: >=4.62.0
sentencepiece:0.2.0
torchtext:0.6.0
2.2 获取并加载日语-英语平行语料库
从JParaCrawl下载并加载数据(http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl),用于后续的模型训练和翻译。平行语料库包含日语和对应的英语翻译。
# 读取CSV文件,指定文件路径、分隔符、引擎和是否有头部
df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 获取DataFrame的第三列(索引从0开始,所以第三列是[2]),并将其转换为列表
# 第三列是训练数据的英文部分,且只保留前10000个元素
trainen = df[2].values.tolist()[:10000]
# 获取DataFrame的第四列(索引从0开始,所以第四列是[3]),并将其转换为列表
# 第四列是训练数据的日文部分,且只保留前10000个元素
trainja = df[3].values.tolist()[:10000]
# 删除列表中的第5972个元素,即移除英文和日文对应的第5972个训练样本
# 注意:由于列表是从0开始索引的,所以要删除的是第5973个元素
# trainen.pop(5972)
# trainja.pop(5972)
展示:
print(trainen[500])
print(trainja[500])
三. 数据预处理
3.1 准备分词器
使用JParaCrawl提供的SentencePiece分词器对日语和英语句子进行分词。SentencePiece是一种高效的文本分词方法,可以处理不规则的文本,如日语。
# 创建两个SentencePieceProcessor对象,分别用于英文和日文的分词
# 这些对象需要加载对应的预训练SentencePiece模型
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对象对英文文本进行分词
# en_tokenizer是一个SentencePieceProcessor对象,它加载了用于英文分词的SentencePiece模型
# out_type参数指定输出类型为'str',这意味着返回的将是分词后的子词单元的字符串形式
# 则返回的字符串将是这个分词结果的字符串形式
encoded_text = en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type=str)
# 打印分词后的字符串结果
print(encoded_text)
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type=str)
3.2 构建TorchText词汇表
使用分词器和原始句子构建TorchText的Vocab对象。Vocab对象用于存储词汇表和索引映射,以便将句子转换为模型可以处理的数字表示。
def build_vocab(sentences, tokenizer):
# 创建一个Counter对象来计数词汇出现的频率
counter = Counter()
# 遍历句子列表
for sentence in sentences:
# 使用分词器对每个句子进行分词
# 参数out_type='str'表示将分词结果作为字符串返回
encoded_sentence = tokenizer.encode(sentence, out_type=str)
# 更新Counter对象,统计每个子词单元的出现次数
counter.update(encoded_sentence)
# 创建一个Vocab对象,使用Counter对象中的计数来构建词汇表
# specials参数用于指定特殊词汇,例如unknown词、填充词、开始词和结束词
return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用build_vocab函数构建日语文本和英文文本的词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
en_vocab = build_vocab(trainen, en_tokenizer)
3.3 将句子转换为Torch张量
使用Vocab和分词器将句子转换为适合模型训练的张量。这一步将原始文本转换为模型可以处理的数字格式,为后续的训练做好准备。
def data_process(ja, en):
# 创建一个空列表data,用于存储处理后的数据
data = []
# 使用zip函数将训练数据中的日文和英文文本对齐
# trainja和trainen是两个列表,每个列表包含一个与另一个列表长度相同的文本序列
for (raw_ja, raw_en) in zip(ja, en):
# 使用ja_tokenizer对日文文本进行分词
# 参数out_type='str'表示将分词结果作为字符串返回
ja_encoded = ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)
# 使用en_tokenizer对英文文本进行分词
en_encoded = en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)
# 创建ja_tensor_和en_tensor_,分别用于存储分词后的日语文本和英文文本的索引
ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_encoded], dtype=torch.long)
en_tensor_ = torch.tensor([en_vocab[token] for token in en_encoded], dtype=torch.long)
# 将ja_tensor_和en_tensor_作为元组添加到data列表中
data.append((ja_tensor_, en_tensor_))
# 返回处理后的数据列表
return data
# 使用data_process函数处理训练数据
train_data = data_process(trainja, trainen)
四. 构建数据加载器
根据计算资源和数据大小设置合适的批次大小。本项目选取的批次为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:
# 将BOS和EOS添加到日语文本和英文文本的开头和结尾
ja_item_with_bos_eos = torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0)
en_item_with_bos_eos = torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0)
# 将处理后的日语文本和英文文本添加到相应的列表中
ja_batch.append(ja_item_with_bos_eos)
en_batch.append(en_item_with_bos_eos)
# 使用pad_sequence函数将列表转换为批量
# 参数padding_value=PAD_IDX表示使用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
# 定义一个数据加载器,用于迭代训练数据并生成批量
# 参数batch_size=BATCH_SIZE表示每个批量包含的数据数量
# 参数shuffle=True表示在迭代数据时进行随机打乱
# 参数collate_fn=generate_batch表示使用generate_batch函数来生成批量
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, collate_fn=generate_batch)
五. 序列到序列的Transformer模型
5.1 定义Transformer模型的参数
以下代码展示了如何使用PyTorch实现一个序列到序列的Transformer模型,用于解决机器翻译任务。
1. Transformer模型:它包含一个编码器(Encoder)和一个解码器(Decoder),每个部分都包含固定数量的层。
2. 编码器(Encoder):
- 编码器通过多头注意力(Multi-head Attention)和前馈网络(Feed forward network)层处理输入序列。
- 编码器的输出被称为“记忆”(memory),它被用来与目标语言的单词一起输入到解码器中。
- 编码器和解码器使用端到端的方式,并通过教师强制(Teacher Forcing)技术进行训练。
3. Seq2SeqTransformer类:
- 这是一个继承自`nn.Module`的自定义神经网络模块,用于构建Transformer模型。
- 它定义了编码器和解码器的层数、词嵌入维度、位置编码和生成器等参数。
4. 前向传播(Forward):
- 在前向传播过程中,源语言句子首先通过位置编码和词嵌入层进行处理。
- 目标语言句子也经过同样的处理。
- 编码器对源语言句子进行处理,生成记忆。
- 解码器使用记忆和目标语言句子进行处理,生成翻译。
- 最后,使用生成器将解码器的输出映射到目标词汇表的大小。
5. 其他方法:
- encode方法:仅对源语言句子进行编码。
- decode方法:仅对目标语言句子进行解码。
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__()
# 定义编码器层
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_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)
5.2 位置编码与词嵌入
在这个上下文中,PositionalEncoding
类用于为每个单词添加位置编码。位置编码有助于模型理解单词在句子中的相对位置。TokenEmbedding
类用于将单词转换为词嵌入向量。这两个类都是Transformer模型的重要组成部分,用于将原始文本转换为模型可以处理的数字表示。
# 定义一个位置编码类
class PositionalEncoding(nn.Module):
def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
# 调用父类的初始化方法
super(PositionalEncoding, self).__init__()
# 定义位置编码的参数
self.dropout = nn.Dropout(dropout) # 定义dropout层,用于防止过拟合
self.register_buffer('pos_embedding', None) # 注册缓冲区,用于存储位置嵌入
# 计算位置编码矩阵
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) # 扩展维度,使其与token_embedding匹配
# 将位置编码矩阵存储在注册缓冲区中
self.register_buffer('pos_embedding', pos_embedding)
def forward(self, token_embedding: Tensor):
# 将位置编码矩阵与token_embedding进行拼接
return self.dropout(token_embedding + self.pos_embedding[:token_embedding.size(0), :])
# 定义一个词嵌入类
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):
# 计算词嵌入,并乘以sqrt(emb_size)
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
5.3 生成掩码
用于生成Transformer模型中的掩码。这些掩码用于控制模型在训练过程中对不同部分的关注。
-
生成随后的掩码(generate_square_subsequent_mask):
该函数用于生成一个随后的掩码,即阻止目标语言句子中的一个词关注其后续的词。它首先创建一个上三角矩阵,其中1表示位置关系,0表示不相关位置。然后将上三角矩阵转换为float类型,并填充负无穷和0。负无穷表示位置关系,0表示不相关位置。最后返回生成的掩码。 -
根据源语言和目标语言的序列长度生成所需掩码(create_mask):
该函数根据源语言和目标语言的序列长度生成所需的掩码。它首先获取源语言和目标语言的序列长度。然后生成目标语言的随后的掩码。接着创建源语言的掩码,默认为0,表示位置关系。创建源语言和目标语言的填充掩码,用于忽略填充项。最后返回生成的源语言和目标语言的掩码。
# 定义一个生成随后的掩码的函数
def generate_square_subsequent_mask(sz):
# 创建一个上三角矩阵,其中1表示位置关系,0表示不相关位置
mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
# 将上三角矩阵转换为float类型,并填充负无穷和0
# 负无穷表示位置关系,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)
# 创建源语言的掩码,默认为0,表示位置关系
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
六. 开始训练
6.1 准备训练所需的类和函数
定义训练过程中所需的类和函数,如损失函数、优化器等。这些类和函数用于指导模型的训练过程。
# 定义Transformer模型的参数
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 # 训练周期数
# 创建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)
# 将模型转移到指定设备(如GPU)
transformer = transformer.to(device)
# 定义损失函数,忽略索引为PAD的损失
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)
6.2 训练模型
使用数据加载器进行模型训练,调整模型参数以最小化损失函数。训练过程中,模型会不断优化其参数,以提高翻译质量。
# 开始训练模型
for epoch in tqdm.tqdm(range(1, NUM_EPOCHS + 1)): # 遍历每个训练周期
start_time = time.time() # 记录开始时间
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")) # 打印损失和时间
七. 翻译日语句子
7.1 创建翻译新句子的函数
以下代码定义了两个函数,用于执行序列到序列模型的贪婪解码和翻译。这两个函数是序列到序列模型的重要组成部分,用于在实际应用中进行翻译。贪婪解码函数用于生成翻译的词索引序列,而翻译函数则用于将源语言句子翻译为目标语言句子。
# 定义贪婪解码函数
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)
# 初始化输出序列,包含一个开始符号BOS和一个结束符号EOS
ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device)
# 循环遍历最大长度减一的时间步
for i in range(max_len-1):
# 更新内存的掩码,确保只关注当前序列
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)
# 如果遇到结束符号EOS,则停止解码
if next_word == EOS_IDX:
break
# 返回解码得到的输出序列
return ys
# 定义翻译函数
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))
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>", "")
7.2 调用翻译函数并传递所需参数
使用该函数和参数翻译日语句子。翻译结果是模型对输入句子的翻译输出。
# 调用translate函数进行翻译
# 参数transformer是预训练的Transformer模型
# 参数src是源语言句子
# 参数src_vocab是源语言词汇表
# 参数tgt_vocab是目标语言词汇表
# 参数src_tokenizer是源语言句子的分词器
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
trainen.pop(5)
八. 保存词汇表和训练好的模型
8.1 保存词汇表
使用Pickle将词汇表保存为文件,以便后续使用。保存词汇表可以确保模型的可移植性和可复现性。
import pickle
# 导入pickle库,用于序列化Python对象
# 打开一个文件,用于存储数据
# 文件名为'en_vocab.pkl',以二进制写模式('wb')打开
file = open('en_vocab.pkl', 'wb')
# 使用pickle.dump方法将en_vocab对象序列化并写入文件
# 注意:pickle.dump的第一个参数是要序列化的对象,第二个参数是文件对象
pickle.dump(en_vocab, file)
# 关闭文件
file.close()
# 重复上述过程,但这次用于ja_vocab对象
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()
8.2 使用PyTorch保存和加载函数保存模型
使用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',使用.tar扩展名表示压缩文件
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模型,并将其应用于日语到英语的机器翻译任务。通过这个实验,可以得到以下思考和知识延申:
1. Transformer模型的优势:与传统的循环神经网络相比,Transformer模型在计算复杂度、计算并行度和学习长距离依赖方面具有优势。这使得Transformer模型在处理长序列时更加高效和准确。
2. 注意力机制的重要性:注意力机制是Transformer模型的核心组成部分,它有助于模型关注输入序列中与当前输出最相关的部分。这使得模型能够更好地理解句子中的上下文信息,从而提高翻译质量。
3. 数据预处理的重要性:在进行模型训练之前,对数据进行预处理是非常重要的。这包括使用分词器对文本进行分词,构建词汇表,并将句子转换为模型可以处理的数字格式。这些预处理步骤对于模型的训练和翻译质量至关重要。
4. 模型的训练和评估:在模型训练过程中,我们需要定义损失函数和优化器,并调整模型参数以最小化损失函数。评估函数用于计算模型在验证数据上的性能,帮助我们了解模型的训练效果。
5. 模型的保存和加载:在训练完成后,我们需要保存模型的参数和词汇表,以便后续使用。这包括使用Pickle保存词汇表和使用PyTorch保存和加载功能保存模型。这些步骤对于模型的可移植性和可复现性至关重要。
6. 实际应用中的挑战:尽管Transformer模型在机器翻译任务中取得了显著效果,但在实际应用中仍面临一些挑战。例如,如何处理未登录词、如何提高模型在低资源语言对上的性能等。这些问题需要进一步的研究和探索。
7. 其他应用场景:除了机器翻译,Transformer模型还可以应用于其他序列到序列的学习任务,如文本摘要、问答系统等。这些应用场景对于Transformer模型的研究和应用具有重要意义。
通过这个实验,我们可以更好地理解Transformer模型的工作原理和应用场景,并为后续的研究和应用提供参考。同时,我们也需要不断探索和优化模型,以解决实际应用中的挑战。
参考资源
https://blog.csdn.net/weixin_49376454/article/details/139222589