使用 Transformer 和 PyTorch 的日文机器翻译模型
——使用 Transformer 和 PyTorch 的日文机器翻译模型
1 导入需要的包
首先,让我们确保我们的系统中安装了以下软件包,如果您发现某些软件包丢失,请务必安装它们。
# 导入所需的库
import math # 数学库
import torchtext # 处理文本数据的库
import torch # PyTorch深度学习框架
import torch.nn as nn # PyTorch神经网络模块
from torch import Tensor # PyTorch张量
from torch.nn.utils.rnn import pad_sequence # 序列填充工具
from torch.utils.data import DataLoader # PyTorch数据加载工具
from collections import Counter # Python内置库,用于计数
from torchtext.vocab import Vocab # 词汇表工具
from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer # Transformer模型相关模块
import io # 输入输出库
import time # 时间库
import pandas as pd # 数据处理库
import numpy as np # 数值计算库
import pickle # 数据序列化库
import tqdm # 进度条库
import sentencepiece as spm # 文本处理工具
# 设置随机种子以便复现结果
torch.manual_seed(0)
# 检查可用设备(CPU或GPU)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码
device(type='cpu')希冀平台仅支持CPU。。。无法支持模型训练,因此可以选择在其他支持GPU服务器平台训练模型(可供参考选择的有,Free: Kaggle,aliyun;Pay: AutoDL,UCloud)。
以下我选择 Kaggle平台的 GPU P100 训练模型:
![](https://img-blog.csdnimg.cn/direct/d426ca484ea743fe9cbce95c716ec3fe.png)
device
![](https://img-blog.csdnimg.cn/direct/7c380ef0027146b89581c201d7476af7.png)
注意Kaggle平台上传数据集后,需要确保文件path正确,通过以下代码查询输入 input 文件夹下的文件路径:
import os
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
![](https://img-blog.csdnimg.cn/direct/06b1148fddb647429a779f27587604f1.png)
2 获取并行数据集
在本教程中,我们将使用从 JParaCrawl 下载的日英并行数据集!
该数据集被描述为“最大的公开可用的英日并行数据集”。 NTT 创建的平行语料库。它主要是通过抓取网络并自动对齐平行句子而创建的。”您还可以在此处查看该论文[http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl] 。
# 从CSV文件中读取数据并存储到DataFrame中
df = pd.read_csv('/kaggle/input/zh-ja-data/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 提取训练数据并转换为列表
trainen = df[2].values.tolist() # 英文训练数据
# trainen = df[2].values.tolist()[:10000] # 提取前10000条数据(训练效果较差)
trainja = df[3].values.tolist() # 日文训练数据
# trainja = df[3].values.tolist()[:10000] # 提取前10000条数据(训练效果较差)
导入所有日语及其英语对应项后,我删除了数据集中的最后一个数据,因为它缺少值。总的来说,trainen 和 trainja 中的句子数量均为 5,973,071,但是,出于学习目的,通常建议在一次使用所有数据之前对数据进行采样并确保一切按预期工作,以节省时间。
# 从训练数据列表中删除索引为5972的条目
trainen.pop(5972)
'2014年和2017年,它被《星期日泰晤士报》(Sunday Times)评为英国最宜居的城市,并获得了欧洲绿色之都(European Green Capital)的美名。'
# 从训练数据列表中删除索引为5972的条目
trainja.pop(5972)
'2014年と2017年のサンデータイムズ紙によってイギリス国内で生活に最も適した街と名付けられ、またヨーロッパグリーンキャピタルの賞も受賞しています。'
Here is an example of sentence contained in the dataset.
# 打印训练数据列表中索引为500的英文数据
print(trainen[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 ...
# 打印训练数据列表中索引为500的日语数据
print(trainja[500])
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 ...
我们还可以使用不同的并行数据集来完成本文,只需确保我们可以将数据处理为如上所示的两个字符串列表,其中包含日语和英语句子。
这里提供语料库提升中文翻译性能 [GitHub - DezhiKong00/Sentencepiece-chinese-bbpe: 使用Sentencepiece对中文语料进行分词]。
3 准备tokenizers
与英语或其他字母语言不同,日语句子不包含空格来分隔单词。我们可以使用 JParaCrawl 提供的分词器,该分词器是使用 SentencePiece 创建的日文和英文分词器,您可以访问 JParaCrawl 网站下载它们,或者单击此处导入所必须的tokenizer。
en_tokenizer = spm.SentencePieceProcessor(model_file='/kaggle/input/enja-spm-models/spm.en.nopretok.model')
ja_tokenizer = spm.SentencePieceProcessor(model_file='/kaggle/input/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', '歳の', '全ての', '人は', '、', '公的', '年', '金', '制度', 'に', '加入', 'しなければなりません', '。']
4 构建 TorchText Vocab 对象并将句子转换为 Torch 张量
使用分词器和原始句子构建从 TorchText 导入的 Vocab 对象。这个过程可能需要几秒钟或几分钟,具体取决于我们的数据集的大小和计算能力。不同的分词器也会影响构建词汇所需的时间,我尝试了其他几种日语分词器,但 SentencePiece 似乎对我来说运行良好且足够快。
from torchtext.vocab import build_vocab_from_iterator
from collections import Counter
import torch
# 定义构建词汇表的函数
def build_vocab(sentences, tokenizer):
# 初始化一个计数器
counter = Counter()
# 遍历句子列表,使用指定的分词器编码句子并更新词频计数
for sentence in sentences:
counter.update(tokenizer.encode_as_pieces(sentence))
# 添加特殊标记(未知词、填充词、句子起始标记、句子终止标记)
specials = ['<unk>', '<pad>', '<bos>', '<eos>']
for special in specials:
counter[special] += 1
# 使用计数器构建词汇表并返回
return Vocab(counter)
# 构建日语和英语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer) # 日语词汇表
en_vocab = build_vocab(trainen, en_tokenizer) # 英语词汇表
获得词汇对象后,我们可以使用词汇和分词器对象为训练数据构建张量。
# 定义数据处理函数,将日语和英语句子转换为张量形式
def data_process(ja, en):
data = []
for (raw_ja, raw_en) in zip(ja, en):
# 使用日语词汇表和分词器将日语句子转换为张量形式
ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
dtype=torch.long)
# 使用英语词汇表和分词器将英语句子转换为张量形式
en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
dtype=torch.long)
# 将处理后的日语和英语张量添加到数据列表中
data.append((ja_tensor_, en_tensor_))
return data
# 对训练数据进行处理,生成训练数据集
train_data = data_process(trainja, trainen)
5 创建要在训练期间迭代的 DataLoader 对象
这里,我将 BATCH_SIZE 设置为 16 以防止“cuda 内存不足”,但这取决于多种因素,例如您的机器内存容量、数据大小等,因此请根据您的需要随意更改批处理大小(注意:在PyTorch 的教程使用 Multi30k 德语-英语数据集将批量大小设置为 128。)
BATCH_SIZE = 16 # 定义批处理大小为16
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, batch_first=True)
en_batch = pad_sequence(en_batch, padding_value=PAD_IDX, batch_first=True)
return ja_batch, en_batch
# 使用DataLoader加载训练数据集,设置批处理大小、打乱数据以及使用自定义的数据处理函数
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, collate_fn=generate_batch)
6 序列到序列转换器(seq2seq)
seq2seq transformer 关于接下来的几个代码和文本解释取自原始 PyTorch 教程 [https://pytorch.org/tutorials/beginner/translation_transformer.html ] 除了BATCH_SIZE 和单词 de_vocab 更改为 ja_vocab 之外,我没有做任何更改。
Transformer 是 <<Attention is all you need>> 论文中介绍的用于解决机器翻译任务的 Seq2Seq 模型。 Transformer 模型由编码器和解码器块组成,每个块包含固定数量的层。
编码器通过一系列多头注意力和前馈网络层传播输入序列来处理输入序列。编码器的输出(称为存储器)与目标张量一起馈送到解码器。编码器和解码器使用教师强制技术以端到端方式进行训练。
from torch.nn import (TransformerEncoder, TransformerDecoder,
TransformerEncoderLayer, TransformerDecoderLayer)
# 定义Seq2SeqTransformer类,用于实现序列到序列的Transformer模型
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)
文本标记使用标记嵌入来表示。将位置编码添加到标记嵌入中以引入词序概念。
# 定义位置编码类PositionalEncoding,用于为输入的token embedding添加位置编码
class PositionalEncoding(nn.Module):
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)
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), :])
# 定义词嵌入类TokenEmbedding,用于将词汇索引转换为词嵌入向量
class TokenEmbedding(nn.Module):
def __init__(self, vocab_size: int, emb_size):
super(TokenEmbedding, self).__init__()
# 使用nn.Embedding定义词嵌入层
self.embedding = nn.Embedding(vocab_size, emb_size)
self.emb_size = emb_size
def forward(self, tokens: Tensor):
# 根据词汇索引将输入转换为词嵌入向量并乘以emb_size的平方根
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
我们创建一个后续单词掩码来阻止目标单词关注其后续单词。我们还创建掩码,用于掩蔽源和目标填充标记。
# 定义生成方形的后续mask函数,用于生成Transformer模型中的mask
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
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask
# 创建mask函数,用于生成源语句子和目标语句子的mask
def create_mask(src, tgt):
src_seq_len = src.shape[1]
tgt_seq_len = tgt.shape[1]
# 生成目标语句子的mask
tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
# 创建源语句子的mask,全零矩阵
src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
# 创建源语句子和目标语句子的填充mask
src_padding_mask = (src == PAD_IDX)
tgt_padding_mask = (tgt == PAD_IDX)
return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask
7 定义模型参数并实例化模型
这里我们服务器实在是计算能力有限,按照以下配置可以训练但是效果应该是不行的。如果想要看到训练的效果请使用你自己的带GPU的电脑运行这一套代码。
当你使用自己的GPU的时候,NUM_ENCODER_LAYERS 和 NUM_DECODER_LAYERS 设置为3或者更高,NHEAD设置8,EMB_SIZE设置为512。
# 定义SRC_VOCAB_SIZE为日语词汇表大小,TGT_VOCAB_SIZE为英语词汇表大小
SRC_VOCAB_SIZE = len(ja_vocab)
TGT_VOCAB_SIZE = len(en_vocab)
EMB_SIZE = 512 # 定义词嵌入的维度
NHEAD = 8 # 定义注意力头的数量
FFN_HID_DIM = 128 # 定义前馈神经网络隐藏层的维度
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)
# 将模型移动到设备(device)
transformer = transformer.to(device)
# 定义交叉熵损失函数和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
)
# 定义训练一个epoch的函数,用于模型训练过程
def train_epoch(model, train_iter, optimizer):
# 设置模型为训练模式
model.train()
losses = 0
# 遍历训练数据迭代器
for idx, (src, tgt) in enumerate(train_iter):
# 将数据移动到设备(device)
src = src.to(device)
tgt = tgt.to(device)
# 获取目标语句子的输入部分
tgt_input = tgt[:-1, :]
# 创建源语句子和目标语句子的mask
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):
# 将数据移动到设备(device)
src = src.to(device)
tgt = tgt.to(device)
# 获取目标语句子的输入部分
tgt_input = tgt[:, :-1]
# 创建源语句子和目标语句子的mask
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)
8 开始训练
最后,在准备好必要的类和函数之后,我们就准备好训练我们的模型了。这是不言而喻的,但完成训练所需的时间可能会有很大差异,具体取决于许多因素,例如计算能力、参数和数据集大小。
当我使用 JParaCrawl 的完整句子列表(每种语言大约有 590 万个句子)训练模型时,使用单个 NVIDIA GeForce RTX 3070 GPU 每个周期大约需要 5 个小时。
import tqdm # 导入tqdm模块用于显示进度条
import time # 导入time模块用于获取时间信息
# 遍历每个epoch进行训练,并显示进度条
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"))
可以看到希冀平台实在是无法支持模型训练。
6%|▋ | 1/16 [10:47<2:41:53, 647.57s/it] Epoch: 1, Train loss: 4.448, Epoch time = 647.571s12%|█▎ | 2/16 [21:35<2:31:09, 647.79s/it] Epoch: 2, Train loss: 3.553, Epoch time = 647.942s19%|█▉ | 3/16 [32:20<2:20:05, 646.61s/it] Epoch: 3, Train loss: 3.242, Epoch time = 645.206s25%|██▌ | 4/16 [43:05<2:09:09, 645.76s/it] Epoch: 4, Train loss: 3.029, Epoch time = 644.444s31%|███▏ | 5/16 [53:50<1:58:21, 645.60s/it] Epoch: 5, Train loss: 2.870, Epoch time = 645.316s38%|███▊ | 6/16 [1:04:36<1:47:38, 645.86s/it] Epoch: 6, Train loss: 2.744, Epoch time = 646.362s44%|████▍ | 7/16 [1:15:22<1:36:53, 645.92s/it] Epoch: 7, Train loss: 2.648, Epoch time = 646.038s50%|█████ | 8/16 [1:26:12<1:26:16, 647.02s/it] Epoch: 8, Train loss: 2.586, Epoch time = 649.386s56%|█████▋ | 9/16 [1:37:03<1:15:39, 648.49s/it] Epoch: 9, Train loss: 2.517, Epoch time = 651.719s62%|██████▎ | 10/16 [1:47:55<1:04:56, 649.45s/it] Epoch: 10, Train loss: 2.453, Epoch time = 651.581s69%|██████▉ | 11/16 [1:58:45<54:08, 649.61s/it] Epoch: 11, Train loss: 2.394, Epoch time = 649.996s75%|███████▌ | 12/16 [2:09:31<43:14, 648.62s/it] Epoch: 12, Train loss: 2.341, Epoch time = 646.331s81%|████████▏ | 13/16 [2:20:18<32:23, 647.96s/it] Epoch: 13, Train loss: 2.296, Epoch time = 646.436s88%|████████▊ | 14/16 [2:31:04<21:34, 647.35s/it] Epoch: 14, Train loss: 2.250, Epoch time = 645.944s94%|█████████▍| 15/16 [2:41:50<10:46, 646.99s/it] Epoch: 15, Train loss: 2.213, Epoch time = 646.142s100%|██████████| 16/16 [2:52:37<00:00, 647.34s/it] Epoch: 16, Train loss: 2.177, Epoch time = 646.933s
9 尝试使用经过训练的模型翻译日语句子
首先,我们创建翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理、然后将结果解码回原句等步骤,但这次是英语。
def greedy_decode(model, src, src_mask, max_len, start_symbol):
# 将源语句子和mask移动到设备(device)
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):
model.eval()
# 将源语句子转换为tokens
tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)]+ [EOS_IDX]
num_tokens = len(tokens)
# 构建源语句子的Tensor表示和mask
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>", "")
然后,我们只需调用翻译函数并传递所需的参数即可。
translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)
未调整前运行结果很差 :
trainen.pop(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 ...'
'美国 设施: 停车场, 24小时前台, 健身中心, 报纸, 露台, 禁烟客房, 干洗, 无障碍设施, 免费停车, 上网服务, 电梯, 快速办理入住/退房手续, 保险箱, 暖气, 传真/复印, 行李寄存, 无线网络, 免费无线网络连接, 酒店各处禁烟, 空调, 阳光露台, 自动售货机(饮品), 自动售货机(零食), 每日清洁服务, 内部停车场, 私人停车场, WiFi(覆盖酒店各处), 停车库, 无障碍停车场, 简短描述Gateway Hotel Santa Monica酒店距离海滩2英里(3.2公里),提供24小时健身房。每间客房均提供免费WiFi,客人可以使用酒店的免费地下停车场。 '
trainja.pop(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 ...'
'アメリカ合衆国 施設・設備: 駐車場, 24時間対応フロント, フィットネスセンター, 新聞, テラス, 禁煙ルーム, ドライクリーニング, バリアフリー, 無料駐車場, インターネット, エレベーター, エクスプレス・チェックイン / チェックアウト, セーフティボックス, 暖房, FAX / コピー, 荷物預かり, Wi-Fi, 無料Wi-Fi, 全館禁煙, エアコン, サンテラス, 自販機(ドリンク類), 自販機(スナック類), 客室清掃サービス(毎日), 敷地内駐車場, 専用駐車場, Wi-Fi(館内全域), 立体駐車場, 障害者用駐車場, 短い説明Gateway Hotel Santa Monicaはビーチから3.2kmの場所に位置し、24時間利用可能なジム、無料Wi-Fi付きのお部屋、無料の地下駐車場を提供しています。'
10 保存 Vocab 对象和训练好的模型
最后,训练结束后,我们将首先使用 Pickle 保存 Vocab 对象(en_vocab 和 ja_vocab)。
import pickle
# open a file, where you want to store the data
file = open('en_vocab.pkl', 'wb')
# dump information to that file
pickle.dump(en_vocab, file)
file.close()
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
file.close()
我们还可以使用 PyTorch 保存和加载函数保存模型以供以后使用。通常,有两种方法可以保存模型,具体取决于我们以后想用它们做什么。第一种方法仅用于推理,我们可以稍后加载模型并使用它将日语翻译成英语。
# save model for inference
torch.save(transformer.state_dict(), 'inference_model')
第二个也用于推理,但也用于我们稍后想要加载模型并想要恢复训练时。
# save model + checkpoint to resume training later
torch.save({
'epoch': NUM_EPOCHS,
'model_state_dict': transformer.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': train_loss,
}, 'model_checkpoint.tar')
在 kaggle/output 文件夹中可以找到保存好的en_vocab, ja_vocab, inference_model, model_check.tar文件。
11 结论
通过该项目,我们实现了一个基于Transformer的序列到序列(Seq2Seq)模型,用于中文和日语之间的翻译。
在实验中出现报错:
RuntimeError: The shape of the 2D attn_mask is torch.Size([32, 32]), but should be (1, 1).
经过多次排查后,开始怀疑是参数传递出现问题,实则只是版本不匹配问题,使用 pip list 输出服务器环境情况,可以对照如下(版本不匹配会爆各种奇奇怪怪的错误。。。)
python:3.8
torch: 1.11.0+cu113
torchvision: 0.12.0+cu113
pandas: >=1.3.0
sentencepiece:0.2.0
torchtext:0.6.0
安装完成后,你可以通过以下命令验证安装的版本:
import torchtext
print(torchtext.__version__)
- 数据预处理:
我们从CSV文件中读取了中文和日语的平行语料,并删除了一条异常数据。 我们使用了SentencePiece进行分词,并构建了词汇表。
- 数据加载:
我们编写了一个数据处理函数,将分词后的中文和日语句子转换为张量形式。 使用PyTorch的DataLoader加载数据并进行批处理。
- 模型构建:
我们定义了一个Seq2SeqTransformer类,实现了基于Transformer的编码器和解码器。 我们还定义了位置编码和词嵌入类,增强了输入的表示。
- 模型训练:
使用Adam优化器和交叉熵损失函数进行模型训练。 编写了训练函数,训练了5个epoch。
- 模型评估:
在每个epoch结束时,计算训练损失并打印结果。
通过以上步骤,我们成功构建并训练了一个基于Transformer的中文到日语翻译模型。 最后的翻译结果对比其他较为完善的翻译结果下,可以通过以下方法进一步提升模型的性能:
超参数调优:调整学习率、批次大小、模型层数等超参数。
数据扩展:增加训练数据的量和多样性,提高模型的泛化能力。
模型改进:引入预训练模型、使用更高级的Transformer变体等。
总的来说,该项目展示了从数据预处理、模型构建到训练和评估的完整流程,为进一步的研究和应用奠定了基础。