目录
构建 TorchText Vocab 对象并将句子转换为 Torch 张量
实验内容
导入所需的包
首先,让我们确保我们的系统中安装了以下软件包,如果您发现缺少某些软件包,请务必安装它们。
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,请在你自己的电脑上尝试运行这一套代码
配置信息:
import torch
print('你对应的torch的版本信息',torch.__version__)
print('返回true即为可用',torch.cuda.is_available())
print(torchvision.__version__)
获取并行数据集
在本教程中,我们将使用从 JParaCrawl 下载的日英并行数据集!http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl,它被描述为“NTT创建的最大的公开可用的英日平行语料库。它是通过大量抓取网络并自动对齐平行句子而创建的。你也可以在这里看到这篇论文。
# 导入pandas库并读取CSV文件
df = pd.read_csv('zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 从DataFrame中提取第2列的数据,并转换为列表
trainen = df[2].values.tolist()
# 从DataFrame中提取第3列的数据,并转换为列表
#限制数据量为前10000个元素
trainja = df[3].values.tolist()#[:10000]
# 从trainen列表中移除索引为5972的元素
trainen.pop(5972)
# 从trainja列表中移除索引为5972的元素
trainja.pop(5972)
'2014年と2017年のサンデータイムズ紙によってイギリス国内で生活に最も適した街と名付けられ、またヨーロッパグリーンキャピタルの賞も受賞しています。'
在导入所有日语和英语对应数据后,我删除了数据集中的最后一个数据,因为它缺少值。总的来说,trainen 和 trainja 中的句子数为 5,973,071,但是,出于学习目的,通常建议在一次性使用所有数据之前对数据进行采样并确保一切按预期工作,以节省时间。
下面是数据集中包含的句子示例。
# 打印训练数据集trainen中的第500个元素
print(trainen[500])
# 打印训练数据集trainja中的第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 ...
我们还可以使用不同的并行数据集来遵循本文,只需确保我们可以将数据处理成两个字符串列表,如上所示,包含日语和英语句子。
准备分词器
与英语或其他字母语言不同,日语句子不包含空格来分隔单词。我们可以使用JParaCrawl提供的分词器,该分词器是使用SentencePiece创建的日语和英语,您可以访问JParaCrawl网站下载它们,或单击此处。
# 创建一个英文分词器,使用预训练的SentencePiece模型
en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
# 创建一个日文分词器,使用预训练的SentencePiece模型
ja_tokenizer = spm.SentencePieceProcessor(model_file='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', '歳の', '全ての', '人は', '、', '公的', '年', '金', '制度', 'に', '加入', 'しなければなりません', '。']
构建 TorchText Vocab 对象并将句子转换为 Torch 张量
然后,使用分词器和原始句子,我们构建从 TorchText 导入的 Vocab 对象。此过程可能需要几秒钟或几分钟,具体取决于我们的数据集大小和计算能力。不同的分词器也会影响构建词汇所需的时间,我尝试了其他几种日语分词器,但 SentencePiece 对我来说似乎运行良好且速度足够快。
# 定义一个函数,用于构建词汇表
def build_vocab(sentences, tokenizer):
# 初始化一个计数器,用于统计子词出现的频率
counter = Counter()
# 遍历给定的句子列表
for sentence in sentences:
# 使用分词器对句子进行编码
# 编码后的结果以字符串形式返回
encoded_sentence = tokenizer.encode(sentence, out_type=str)
# 更新计数器,统计每个子词的出现次数
counter.update(encoded_sentence)
# 创建一个Vocab对象,该对象包含计数器中的所有子词及其频率
# 同时添加一些特殊的子词,如未知词、填充词、句子开始和结束标记
return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用日文分词器构建日文词汇表
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)
创建要在训练期间迭代的 DataLoader 对象
在这里,我将BATCH_SIZE设置为 16 以防止“cuda 内存不足”,但这取决于各种因素,例如您的机器内存容量、数据大小等,因此请根据需要随意更改批处理大小(注意:PyTorch 的教程使用 Multi30k 德语-英语数据集将批处理大小设置为 128。
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))
# 使用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)
序列到序列转换器
接下来的几个代码和文本说明(用斜体书写)取自原始的 PyTorch 教程 [https://pytorch.org/tutorials/beginner/translation_transformer.html]。除了BATCH_SIZE之外,我没有做任何更改,de_vocabwhich 这个词被改成了ja_vocab。
Transformer 是 “Attention is all you need” 论文中介绍的 Seq2Seq 模型,用于解决机器翻译任务。Transformer 模型由编码器和解码器块组成,每个块包含固定数量的层。
编码器通过一系列多头注意力和前馈网络层传播输入序列来处理输入序列。编码器的输出称为内存,与目标张量一起馈送到解码器。编码器和解码器使用教师强制技术以端到端的方式进行训练。
# 导入Transformer相关模块
from torch.nn import (TransformerEncoder, TransformerDecoder,
TransformerEncoderLayer, TransformerDecoderLayer)
# 定义序列到序列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__()
# 创建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))
# 使用Transformer编码器处理源文本嵌入,生成编码记忆
memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
# 使用Transformer解码器处理目标文本嵌入和编码记忆,生成输出
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):
# 仅使用Transformer编码器部分进行编码
return self.transformer_encoder(self.positional_encoding(
self.src_tok_emb(src)), src_mask)
def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
# 仅使用Transformer解码器部分进行解码
return self.transformer_decoder(self.positional_encoding(
self.tgt_tok_emb(tgt)), memory,
tgt_mask)
文本标记通过使用标记嵌入来表示。位置编码被添加到标记嵌入中,以引入词序的概念。
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)
self.dropout = nn.Dropout(dropout)
# 将位置嵌入向量注册为缓冲区,以便在forward方法中使用
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):
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转换为长整型,并计算它们的嵌入
return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
我们创建一个后续单词掩码来阻止目标单词关注其后续单词。我们还创建掩码,用于屏蔽源和目标填充令牌
def generate_square_subsequent_mask(sz):
# 生成一个上三角矩阵,其中对角线上的元素为1,其余为0
mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
# 将上三角矩阵转换为浮点数,对角线上的元素变为0.0,其余变为-inf(无穷小)
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]
# 生成目标序列的后续掩码,确保对角线上的元素为1,其余为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)
# 生成源序列的填充掩码,检查是否为填充字符(PAD_IDX)
src_padding_mask = (src == PAD_IDX).transpose(0, 1)
# 生成目标序列的填充掩码,检查是否为填充字符(PAD_IDX)
tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
# 返回源掩码、目标掩码、源填充掩码和目标填充掩码
return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask
定义模型参数并实例化模型
这里我们服务器实在是计算能力有限,按照以下配置可以训练但是效果应该是不行的。如果想要看到训练的效果请使用你自己的带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
# 定义训练的epoch数量
NUM_EPOCHS = 100
# 创建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():
# 如果参数的维度大于1,则初始化参数
if p.dim() > 1:
nn.init.xavier_uniform_(p)
# 将模型移动到指定的设备上
transformer = transformer.to(device)
# 定义损失函数
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)
# 获取目标数据的输入(不包括最后一个 token)
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()
# 获取目标数据的输出(不包括第一个 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)
# 获取目标数据的输入(不包括最后一个 token)
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)
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 万个句子)训练模型时,使用单个 NVIDIA GeForce RTX 3070 GPU 每个 epoch 大约需要 5 小时。
代码如下:
# 遍历训练的epochs
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()
# 打印当前epoch的信息
print(f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
f"Epoch time = {(end_time - start_time):.3f}s")
单个epoch花费了将近6分钟,训练完100个epoch约需10小时,而4090的gpu平均只使用了不到40%(峰值:47%),可以试着提高单个批次的样本的数量来提高速度。
因为余额不足,训练了5个小时后我停止了训练
尝试使用经过训练的模型翻译日语句子
首先,我们创建翻译新句子的函数,包括获取日语句子、标记化、转换为张量、推理等步骤,然后将结果解码回句子,但这次是英语。
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)
# 初始化输出序列,第一个 token 是 start_symbol
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)
# 解码下一个 token
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()
# 将下一个 token 添加到输出序列中
ys = torch.cat([ys,
torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0)
# 如果生成了 EOS_IDX,则停止生成
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)
# 使用 greedy 解码进行翻译
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)
我又添加了两个句子:
“董さんはとても面白い方です” (译文:董老师是个很有趣的人)
“自然言語処理について多くのことを学びました”(译文:自然语言处理这门课我们学到了很多)
translate(transformer, "董さんはとても面白い方です", ja_vocab, en_vocab, ja_tokenizer)
translate(transformer, "自然言語処理について多くのことを学びました", ja_vocab, en_vocab, ja_tokenizer)
保存 Vocab 对象和训练的模型
最后,在训练完成后,我们将首先使用 Pickle 保存 Vocab 对象(en_vocab 和 ja_vocab)。
import pickle # 导入 pickle 模块,用于序列化和反序列化 Python 对象
# 打开一个文件,准备将数据存储进去
# 'en_vocab.pkl' 是文件名,'wb' 表示以二进制写模式打开文件
file = open('en_vocab.pkl', 'wb')
# 使用 pickle.dump() 函数将 en_vocab 对象序列化并写入到文件中
# 'file' 是打开的文件对象
pickle.dump(en_vocab, file)
# 关闭文件
file.close()
# 重复上述过程,这次是针对 ja_vocab 对象
file = open('ja_vocab.pkl', 'wb')
pickle.dump(ja_vocab, file)
# 再次关闭文件
file.close()
最后,我们还可以使用 PyTorch save 和 load 函数保存模型以供以后使用。通常,有两种方法可以保存模型,具体取决于我们以后要使用它们的内容。第一个仅用于推理,我们可以稍后加载模型并使用它从日语翻译成英语。
# save model for inference
torch.save(transformer.state_dict(), 'inference_model')
第二个也用于推理,但也用于我们稍后想要加载模型并想要恢复训练时。
# 导入 PyTorch 保存和加载模型的功能
import torch
# 定义要保存到磁盘的对象
checkpoint = {
'epoch': NUM_EPOCHS, # 保存当前的 epoch 数
'model_state_dict': transformer.state_dict(), # 保存模型的参数状态
'optimizer_state_dict': optimizer.state_dict(), # 保存优化器的参数状态
'loss': train_loss, # 保存训练损失
}
# 指定保存文件的路径和文件名
# 'model_checkpoint.tar' 是保存文件的名称
torch.save(checkpoint, 'model_checkpoint.tar')
实验总结
实验的过程一曲三折,配置环境挺不容易!
去官网下载了cudatoolkit驱动和cudnn后,配置虚拟环境,直接conda install pytorch gpu版会因为网速问题断开连接并报错,接着尝试了在自己电脑上去镜像网站上下载torch-2.3.1+cu118_py3.9和3.8包,配置虚拟环境,部署解释器,torch.cuda显示可用,高兴没多久,训练过程报错RuntimeError: "nll_loss_forward_reduce_cuda_kernel_2d_index" not implemented for 'Float'我仔细看了下问题出现在torch._C._nn.cross_entropy_loss交叉熵函数的计算上,开始以为是torch的版本太高,又下载了torch-1.11.0+cu113,问题仍没解决!
没办法,只能试着去租用云服务器,这其中又绕了不少弯路:pycharm只有专业版提供SSH远程解释器;AutoDL是linux系统,我下的windows包不适配;在系统的终端上新create了一个python3.8环境,但切换到此内核时一直说缺失torchtext模块,即使我已经pip install了!后来发现我调用的内核的路径还是之前默认的路径,名称变了路径没变?没办法,只能将就着用默认的内核,安装了必要的包后,又出现了Torch not compiled with CUDA enabled的问题。说实话要奔溃了,山穷水路的我最后试了更换租用主机的镜像,没想到这是成功的最后一步,代码终于跑起来了。
这些花费了我四五天的时间,每天寝室难安(dog),不管怎样,结果是好的,努力是值得的!我也因此受益颇丰,有时候停下来思考一下,会少走不少弯路:选择大于努力。