当前位置:   article > 正文

基于Transformer实现机器翻译_transformer机器翻译mask

transformer机器翻译mask

目录

一、前言

1.1什么是Transformer?

1.2Transfomer的基本结构

1.2Transformer的重要组成部分

1.2.1位置编码(Positional Encode)

1.2.2 自注意力机制(Self-Attention)

1.2.3多头注意力(Multi-Head Attention)

1.2.4位置感知前馈层(Position-wise FFN)

1.2.5残差连接与层归一化

二、AutoDL云服务器的使用

三、具体实现

3.1 实验准备

3.1.1导入本次实验需要用到的包

3.1.2数据集准备

3.2数据处理

 3.2.1分词器的准备

 3.2.2词汇表的构建

 3.2.3预处理训练数据

3.2.4DataLoader 对象的创建

3.3Transformer模型的构建

3.3.1Transformer基本结构的构建

3.3.2词嵌入和位置编码

3.3.3创建mask(掩码)

3.3.4初始化Transformer模型

 3.4模型训练

3.5模型使用

3.6词汇表、模型保存


一、前言

1.1什么是Transformer?

Transformer 模型是由谷歌在2017 年提出并首先应用于机器翻译的神经网络模型结构。机器翻译的目标是从源语言(Source Language)转换到目标语言(Target Language)。Transformer 结构完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。

1.2Transfomer的基本结构

大多数竞争性神经序列转导模型具有编码器-解码器结构。Transformer遵循这个整体架构,使用堆叠(如图Nx表示进行了N次堆叠)的self-attention和position-wise,完全连接编码器和解码器层。

如图所示,可以看到基于Transfomer结构的编码器和解码器,左侧对应着编码器(Encode),右侧对应解码器(Decoder)。它们均由若干个基本的Transformer块组成(对应图中的方框)。

1.2Transformer的重要组成部分

1.2.1位置编码(Positional Encode)

由于Transformer模型不再使用基于循环的方式建模文本输入,没有额外的位置编码来表达输入序列中的单词的位置顺序,必须注入一些关于序列中标记的相对或绝对位置的信息,一个重要的操作就是加入位置编码(Positional Encode)这个特征。

1.2.2 自注意力机制(Self-Attention)

自注意力机制的基本思想是,对于序列中的每个元素,自注意力机制计算其与其他元素之间的相似度,并将这些相似度归一化为注意力权重。然后,通过将每个元素与对应的注意力权重进行加权求和,可以得到自注意力机制的输出。左下图为Self-Attention的架构。

其计算过程为:首先,计算矩阵Q和K每一行向量的内积,为了防止内积过大,除以d_k的平方根;其次,使用Softmax对上述内积的结果进行归一化最后得到Softmax矩阵之后和V相乘,得到最终的输出。

              

1.2.3多头注意力(Multi-Head Attention)

Transformer中的自注意力机制被扩展为多个注意力头。使用多头注意力(Multi-Head Attention)机制整合上下文语义,它使得序列中任意两个单词之间的依赖关系可以直接被建模而不基于传统的循环结构,从而更好地解决文本的长程依赖。

可以用独立学习得到的h组不同的线性投影来变换查询、键和值。 然后,这h组变换后的查询、键和值将并行地送到注意力汇聚中。 最后,将这h个注意力汇聚的输出拼接在一起, 并且通过另一个可以学习的线性投影进行变换, 以产生最终输出。基本架构如右上图所示。

1.2.4位置感知前馈层(Position-wise FFN)

除了注意子层之外,编码器和解码器中的每一层都包含一个完全连接的前馈网络,位置感知前馈层过全连接层对输入文本序列中的每个单词表示进行更复杂的变换。

1.2.5残差连接与层归一化

残差连接对应图一中的Add 部分。它是一条分别作用在上述两个子层当中的直连通路,被用于连接它们的输入与输出。从而使得信息流动更加高效,有利于模型的优化。
层归一化对应图一中的Norm 部分。作用于上述两个子层的输出表示序列中,对表示序列进行层归一化操作,同样起到稳定优化的作用。

 

二、AutoDL云服务器的使用

由于该模型对算力的要求较大,对电脑的配置要求较高。因此我借助AutoDL平台实现模型的训练。我根据自己的需求租用了我所需要的服务器。

然后跟Pyhcarm进行连接,输入自己的SSH信息进行配置连接。连接完成后也可以在Pycharm中查看远程主机信息。

 

三、具体实现

实现环境:

python3.10

torch                          2.1.2+cu118
torchdata                      0.7.1
torchtext                      0.6.0
torchvision                    0.16.2+cu118
tqdm                           4.64.1
numpy                          1.26.4
pandas                         2.2.2
sentencepiece                  0.1.96


GPU:NVIDIA GeForce RTX 4090D

3.1 实验准备

3.1.1导入本次实验需要用到的包

根据本次实验的需要导入实验所需用到的包并检测GPU

  1. import math
  2. import torchtext
  3. import torch
  4. import torch.nn as nn
  5. from torch import Tensor
  6. from torch.nn.utils.rnn import pad_sequence
  7. from torch.utils.data import DataLoader
  8. from collections import Counter
  9. from torchtext.vocab import Vocab
  10. from torch.nn import TransformerEncoder, TransformerDecoder, TransformerEncoderLayer, TransformerDecoderLayer
  11. import io
  12. import time
  13. import pandas as pd
  14. import numpy as np
  15. import pickle
  16. import tqdm
  17. import sentencepiece as spm
  18. torch.manual_seed(0) # 设置PyTorch随机种子,保证结果可复现
  19. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 检测是否有GPU,如果有GPU则使用cuda,否则使用cpu
  20. print(torch.cuda.get_device_name(0))

可以检测到GPU情况如下:

 

3.1.2数据集准备

使用 JParaCrawl 提供的日英平行数据集进行训练,可以看到本段代码删除了数据集中的最后一个数据,因为它包含缺失值。并且将df中的第三列赋值给trainen变量,第四列赋值给trainja变量。然后打印指定索引位置的数据。

  1. 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]
  4. trainen.pop(5972)
  5. trainja.pop(5972)
  6. print(trainen[500])
  7. print(trainja[500])

 打印出的数据如下:

 

3.2数据处理

 3.2.1分词器的准备

与英语或其他字母语言不同,日语句子不包含空格来分隔单词。我们可以使用JParaCrawl提供的分词器,该分词器是使用SentencePiece创建的日语和英语,SentencePiece 是一种基于子词的分词模型,能够处理多语言的文本,通过学习语料库中的频繁子词来进行分词,这种方法在处理语言的多样性和灵活性方面有优势。

  1. en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')
  2. ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.ja.nopretok.model')
  3. print(en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str'))
  4. print(ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type='str'))

加载完分词器后使用例子测试,可得到以下结果:

 可以观察到在 SentencePiece 中,以  开头的部分表示一个单词或者独立的标记。

 3.2.2词汇表的构建

通过使用 tokenizer 将训练数据的句子编码为词或子词序列,然后使用统计的词频couter 构建词汇表对象 。参数specials指定了词汇表中的特殊标记,如<unk>(未知词)、<pad>(填充标记)、<pos>(开始标记)、<eos>(结束标记)。

  1. def build_vocab(sentences, tokenizer):
  2. counter = Counter() # 创建一个空的计数器对象,用于统计词频
  3. for sentence in sentences: # 遍历每个句子
  4. counter.update(tokenizer.encode(sentence, out_type=str)) # 使用指定的tokenizer对句子进行编码,并更新计数器
  5. return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>']) # 根据统计的词频构建词汇表,并指定特殊标记

根据上面的函数构建日语词汇表和英语词汇表,并查看日语词汇表的一些属性。

  1. ja_vocab = build_vocab(trainja, ja_tokenizer) # 构建日语的词汇表,trainja为日语训练数据,ja_tokenizer为日语的tokenizer
  2. en_vocab = build_vocab(trainen, en_tokenizer) # 构建英语的词汇表,trainen为英语训练数据,en_tokenizer为英语的tokenizer
  3. print("日语词汇表大小:", len(ja_vocab)) # 查看日语词汇表的大小
  4. print("日语词汇表内容(部分):", ja_vocab.itos[:20]) # 打印前20个单词
  5. print("日语词汇表中 <unk> 的索引:", ja_vocab.stoi['<unk>']) # 访问日语词汇表中 <unk> 的索引

如图为日语词汇表的一些属性:

 3.2.3预处理训练数据

使用上述得到的词汇表和分词器对象来构建训练数据的张量,并以元组形式存储在列表中,为后续的机器翻译模型提供训练数据。

  1. def data_process(ja, en):
  2. data = []
  3. for (raw_ja, raw_en) in zip(ja, en):
  4. # 对日语句子进行编码
  5. ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
  6. dtype=torch.long)
  7. # 对英语句子进行编码
  8. en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
  9. dtype=torch.long)
  10. # 将处理后的结果作为一个元组加入数据列表中
  11. data.append((ja_tensor_, en_tensor_))
  12. return data
  13. # 调用data_process函数处理训练数据trainja和trainen,得到训练数据集train_data
  14. train_data = data_process(trainja, trainen)

3.2.4DataLoader 对象的创建

  • 常量定义:定义每个批次中样本的数量以及索引
  1. BATCH_SIZE = 8 # 定义每个批次的大小
  2. PAD_IDX = ja_vocab['<pad>'] # 获取日语词汇表中'<pad>'标记的索引
  3. BOS_IDX = ja_vocab['<bos>'] # 获取日语词汇表中'<bos>'标记的索引(句子开头)
  4. EOS_IDX = ja_vocab['<eos>'] # 获取日语词汇表中'<eos>'标记的索引(句子结尾)
  • generate_batch():用于对每个批次的数据进行处理和填充
  1. def generate_batch(data_batch):
  2. ja_batch, en_batch = [], []
  3. # 遍历每个数据批次中的每个数据项
  4. for (ja_item, en_item) in data_batch:
  5. # 在日语句子的开头和结尾分别添加'<bos>'和'<eos>'标记,并合并成一个张量
  6. ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0))
  7. # 在英语句子的开头和结尾分别添加'<bos>'和'<eos>'标记,并合并成一个张量
  8. en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))
  9. # 对日语批次进行填充,使其长度一致,并且用'<pad>'标记填充
  10. ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
  11. # 对英语批次进行填充,使其长度一致,并且用'<pad>'标记填充
  12. en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)
  13. return ja_batch, en_batch
  • 数据加载器创建
  1. # 创建数据加载器,每次返回一个批次的日语和英语句子对
  2. train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
  3. shuffle=True, collate_fn=generate_batch)

以上通过 generate_batch()将原始数据转换为模型可以处理的格式,并使用DataLoader创建了一个数据加载器 ,用于迭代训练过程中的每个批次的日语和英语句子对。这种批处理和填充操作有助于确保每个批次中的数据格式一致。

 

3.3Transformer模型的构建

3.3.1Transformer基本结构的构建

Seq2Seq模型:处理序列到序列问题的利器。传统的循环神经网络中是将一个序列转化成定长输出。而 Seq2Seq 可以将一个序列转化成一个不定长的序列输出。

针对基于CNN和RNN的Seq2Seq模型存在的不足,Transformer模型抛弃了CNN和RNN,基于Attention来构造Encoder和Decoder,搭建完全基于Attention的Seq2Seq模型。

构建了一个Seq2SeqTransformer 类,包括了 Transformer 编码器和解码器部分、生成器层、词嵌入层以及位置编码层。同时,forward()定义了整个 Seq2Seq 模型的前向计算过程,encode()方法用于对源语言序列进行编码,decode()用于对目标语言序列进行解码。

  1. class Seq2SeqTransformer(nn.Module):
  2. def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
  3. emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
  4. dim_feedforward:int = 512, dropout:float = 0.1):
  5. super(Seq2SeqTransformer, self).__init__()
  6. # Transformer编码器层
  7. encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
  8. dim_feedforward=dim_feedforward)
  9. self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
  10. # Transformer解码器层
  11. decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
  12. dim_feedforward=dim_feedforward)
  13. self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
  14. # 生成器层,用于将解码器输出映射到目标词汇空间
  15. self.generator = nn.Linear(emb_size, tgt_vocab_size)
  16. # 源语言和目标语言的词嵌入层
  17. self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
  18. self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size)
  19. # 位置编码层,用于增加序列中词嵌入的位置信息
  20. self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout)
  21. def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
  22. tgt_mask: Tensor, src_padding_mask: Tensor,
  23. tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
  24. # 对源语言和目标语言序列的词嵌入进行位置编码
  25. src_emb = self.positional_encoding(self.src_tok_emb(src))
  26. tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg))
  27. # 使用Transformer编码器将源语言序列编码成内部表示memory
  28. memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask)
  29. # 使用Transformer解码器将目标语言序列解码成输出序列
  30. outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
  31. tgt_padding_mask, memory_key_padding_mask)
  32. # 经过生成器层映射到目标词汇空间,返回最终输出
  33. return self.generator(outs)
  34. def encode(self, src: Tensor, src_mask: Tensor):
  35. return self.transformer_encoder(self.positional_encoding(
  36. self.src_tok_emb(src)), src_mask)
  37. def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
  38. return self.transformer_decoder(self.positional_encoding(
  39. self.tgt_tok_emb(tgt)), memory,
  40. tgt_mask)

这个 Seq2SeqTransformer 类实现了一个完整的基于 Transformer 架构的序列到序列模型。

3.3.2词嵌入和位置编码

在前文提到由于Transformer模型没有额外的位置编码来表达输入序列中的单词的位置顺序,需要加入位置编码(Positional Encode)这个特征。下面为位置编码的实现方法:

  1. class PositionalEncoding(nn.Module):
  2. def __init__(self, emb_size: int, dropout, maxlen: int = 5000):
  3. super(PositionalEncoding, self).__init__()
  4. # 计算位置编码,使用数学函数计算
  5. den = torch.exp(- torch.arange(0, emb_size, 2) * math.log(10000) / emb_size)
  6. # 生成位置索引
  7. pos = torch.arange(0, maxlen).reshape(maxlen, 1)
  8. # 初始化位置编码矩阵
  9. pos_embedding = torch.zeros((maxlen, emb_size))
  10. # 根据位置索引和计算得到的den值填充位置编码矩阵
  11. pos_embedding[:, 0::2] = torch.sin(pos * den)
  12. pos_embedding[:, 1::2] = torch.cos(pos * den)
  13. # 将位置编码矩阵添加一个维度,用于广播加法
  14. pos_embedding = pos_embedding.unsqueeze(-2)
  15. # 初始化Dropout层
  16. self.dropout = nn.Dropout(dropout)
  17. self.register_buffer('pos_embedding', pos_embedding)
  18. def forward(self, token_embedding: Tensor):
  19. # 在输入的词嵌入上加上位置编码,然后应用Dropout
  20. return self.dropout(token_embedding +
  21. self.pos_embedding[:token_embedding.size(0), :])
  22. class TokenEmbedding(nn.Module):
  23. def __init__(self, vocab_size: int, emb_size):
  24. super(TokenEmbedding, self).__init__()
  25. # 初始化词嵌入层
  26. self.embedding = nn.Embedding(vocab_size, emb_size)
  27. # 记录词嵌入的维度
  28. self.emb_size = emb_size
  29. def forward(self, tokens: Tensor):
  30. # 获取输入tokens的词嵌入并乘以sqrt(词嵌入维度)
  31. return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

TokenEmbedding类负责将输入的标记转换为词嵌入向量,而PositionalEncoding类则为这些词嵌入向量添加了位置信息,以便模型能够处理序列的顺序信息。

3.3.3创建mask(掩码)

Transformer模型在处理序列数据时,需要使用不同类型的mask来确保在自注意力机制和位置编码中,模型不会看到未来的信息或填充的无效信息。

  • generate_square_subsequent_mask():生成一个用于目标序列的mask,确保在解码器自注意力中,每个位置只能依赖于其之前的位置。
  1. def generate_square_subsequent_mask(sz):
  2. # 创建一个上三角矩阵,对角线及以下为1,其余为0
  3. mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
  4. # 将上三角矩阵转换为float类型,并用-infinity填充0位置
  5. mask = mask.float().masked_fill(mask == 0, float('-inf'))
  6. # 将对角线部分用0填充
  7. mask = mask.masked_fill(mask == 1, float(0.0))
  8. return mask
  • create_mask():生成用于Transformer模型的四种mask。其中src_mask用于源序列的mask,全零矩阵,表明源序列所有位置都是有效的。tgt_mask用于目标序列的mask,由generate_square_subsequent_mask()生成,确保在解码器中,每个位置只能看到之前的位置信息。src_padding_mask用于源序列的填充mask,标记源序列中的填充位置。tgt_padding_mask用于目标序列的填充mask,标记目标序列中的填充位置。
  1. def create_mask(src, tgt):
  2. # 获取源序列和目标序列的长度
  3. src_seq_len = src.shape[0]
  4. tgt_seq_len = tgt.shape[0]
  5. # 生成目标序列的mask
  6. tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
  7. # 创建源序列的mask,全零矩阵
  8. src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
  9. # 创建源序列和目标序列的填充mask
  10. src_padding_mask = (src == PAD_IDX).transpose(0, 1)
  11. tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
  12. return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

3.3.4初始化Transformer模型

  • 定义模型参数
  1. SRC_VOCAB_SIZE = len(ja_vocab) # 源语言词汇表大小
  2. TGT_VOCAB_SIZE = len(en_vocab) # 目标语言词汇表大小
  3. EMB_SIZE = 512 # 词嵌入维度
  4. NHEAD = 8 # 注意力头数
  5. FFN_HID_DIM = 512 # Feedforward层隐藏单元数
  6. BATCH_SIZE = 16 # 批量大小
  7. NUM_ENCODER_LAYERS = 3 # 编码器层数
  8. NUM_DECODER_LAYERS = 3 # 解码器层数
  9. NUM_EPOCHS = 16 # 训练轮数
  •  初始化Transformer模型
  1. # 初始化Transformer模型
  2. transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
  3. EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
  4. FFN_HID_DIM)
  5. # 对模型参数进行Xavier初始化
  6. for p in transformer.parameters():
  7. if p.dim() > 1:
  8. nn.init.xavier_uniform_(p)
  9. transformer = transformer.to(device)# 将模型移动到指定设备

 对Transformer模型的参数进行Xavier初始化,有助于加速模型的收敛和提高训练效果。

  •  定义损失函数和优化器
  1. loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)# 定义交叉熵损失函数,忽略填充标记PAD_IDX
  2. # 定义Adam优化器
  3. optimizer = torch.optim.Adam(
  4. transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
  5. )
  • train_epoch():将模型设置为训练模式,用于启用具有批处理规范化或dropout等训练特定操作的模型行为。
  1. def train_epoch(model, train_iter, optimizer):
  2. model.train() # 设置模型为训练模式
  3. losses = 0
  4. for idx, (src, tgt) in enumerate(train_iter):
  5. src = src.to(device) # 将源语言序列移动到设备
  6. tgt = tgt.to(device) # 将目标语言序列移动到设备
  7. tgt_input = tgt[:-1, :] # 获取目标序列的输入部分(不包括最后一个词)
  8. # 创建源语言mask、目标语言输入mask、源语言填充mask和目标语言填充mask
  9. src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)
  10. # 前向传播得到logits
  11. logits = model(src, tgt_input, src_mask, tgt_mask,
  12. src_padding_mask, tgt_padding_mask, src_padding_mask)
  13. optimizer.zero_grad() # 梯度清零
  14. tgt_out = tgt[1:, :] # 获取目标序列的输出部分(不包括第一个词)
  15. # 计算损失,注意将logits和目标序列输出展平以匹配交叉熵损失函数的要求
  16. loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
  17. loss.backward() # 反向传播计算梯度
  18. optimizer.step() # 更新模型参数
  19. losses += loss.item() # 累加损失值
  20. return losses / len(train_iter) # 返回平均损失
  • evaluate():将模型设置为评估模式,用于关闭具有随机性行为(如dropout)的训练特定操作,以便得到稳定的预测结果。 
  1. def evaluate(model, val_iter):
  2. model.eval() # 设置模型为评估模式
  3. losses = 0
  4. for idx, (src, tgt) in enumerate(val_iter):
  5. src = src.to(device) # 将源语言序列移动到设备
  6. tgt = tgt.to(device) # 将目标语言序列移动到设备
  7. tgt_input = tgt[:-1, :] # 获取目标序列的输入部分(不包括最后一个词)
  8. # 创建源语言mask、目标语言输入mask、源语言填充mask和目标语言填充mask
  9. src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)
  10. # 前向传播得到logits
  11. logits = model(src, tgt_input, src_mask, tgt_mask,
  12. src_padding_mask, tgt_padding_mask, src_padding_mask)
  13. tgt_out = tgt[1:, :] # 获取目标序列的输出部分(不包括第一个词)
  14. # 计算损失,注意将logits和目标序列输出展平以匹配交叉熵损失函数的要求
  15. loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
  16. losses += loss.item() # 累加损失值
  17. return losses / len(val_iter) # 返回平均损失

 这两个函数共同组成了模型的训练和评估过程,其中train_epoch 用于训练模型并更新参数,evaluate用于在验证集上评估模型的性能。

 3.4模型训练

 准备了必要的类和函数之后,准备训练模型。

  1. for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
  2. start_time = time.time()
  3. train_loss = train_epoch(transformer, train_iter, optimizer)
  4. end_time = time.time()
  5. print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
  6. f"Epoch time = {(end_time - start_time):.3f}s"))

 循环训练Transformer模型多个epoch,并在每个epoch结束后打印训练损失和训练时间。进度条可以帮助用户了解训练的进展情况,同时记录每个epoch的训练耗时和损失值,以便于监控和调试模型训练过程。

训练过程如下:

可以观察到使用单个 NVIDIA GeForce RTX 4090D GPU 每个 epoch 大约需要 6分钟,可以看到loss不断降低。

3.5模型使用

 在这里,尝试使用经过训练的模型翻译日语句子"HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)"。

  1. def greedy_decode(model, src, src_mask, max_len, start_symbol):
  2. src = src.to(device) # 将输入src移动到指定的设备上
  3. src_mask = src_mask.to(device) # 将输入src_mask移动到指定的设备上
  4. memory = model.encode(src, src_mask) # 使用编码器对输入进行编码,得到内部记忆
  5. ys = torch.ones(1, 1).fill_(start_symbol).type(torch.long).to(device) # 准备初始的输出序列,以起始符号填充
  6. for i in range(max_len-1):
  7. memory = memory.to(device) # 将内部记忆移动到指定的设备上
  8. memory_mask = torch.zeros(ys.shape[0], memory.shape[0]).to(device).type(torch.bool) # 创建内存掩码,用于注意力机制
  9. tgt_mask = (generate_square_subsequent_mask(ys.size(0))
  10. .type(torch.bool)).to(device) # 生成目标掩码,用于解码器的自注意力机制
  11. out = model.decode(ys, memory, tgt_mask) # 解码当前输出序列
  12. out = out.transpose(0, 1) # 调整输出的形状,以便获取每个时间步长的输出
  13. prob = model.generator(out[:, -1]) # 使用生成器获取当前时间步长的概率分布
  14. _, next_word = torch.max(prob, dim=1) # 获取概率最高的词作为下一个词
  15. next_word = next_word.item() # 将下一个词转换为Python整数
  16. ys = torch.cat([ys,
  17. torch.ones(1, 1).type_as(src.data).fill_(next_word)], dim=0) # 将预测的下一个词添加到输出序列中
  18. if next_word == EOS_IDX: # 如果预测到终止符号,则停止解码
  19. break
  20. return ys
  21. def translate(model, src, src_vocab, tgt_vocab, src_tokenizer):
  22. model.eval() # 设置模型为评估模式,关闭dropout等影响推理结果的操作
  23. tokens = [BOS_IDX] + [src_vocab.stoi[tok] for tok in src_tokenizer.encode(src, out_type=str)] + [EOS_IDX] # 将源语言句子编码为标记序列
  24. num_tokens = len(tokens) # 获取标记序列的长度
  25. src = torch.LongTensor(tokens).reshape(num_tokens, 1) # 将标记序列转换为PyTorch张量,并reshape为(num_tokens, 1)
  26. src_mask = torch.zeros(num_tokens, num_tokens).type(torch.bool) # 创建源语言掩码
  27. tgt_tokens = greedy_decode(model, src, src_mask, max_len=num_tokens + 5, start_symbol=BOS_IDX).flatten()# 使用贪婪解码生成目标语言标记序列
  28. return " ".join([tgt_vocab.itos[tok] for tok in tgt_tokens]).replace("<bos>", "").replace("<eos>", "")# 将目标语言标记序列转换为文本,并移除特殊标记符号

这里实现了一个简单的基于贪婪解码的机器翻译模型的推理过程。它通过编码器-解码器架构将输入的源语言句子编码为内部记忆,并通过贪婪解码方法逐步生成目标语言的翻译结果。在推理时,需要确保模型处于评估模式以避免训练中的随机性操作影响结果。

 调用translate函数:

translate(transformer, "HSコード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)", ja_vocab, en_vocab, ja_tokenizer)

运行结果:

3.6词汇表、模型保存

 在训练完成后,将使用 Pickle 保存 Vocab 对象(en_vocab 和 ja_vocab),将en_vocab保存到文件en_vocab.pkl中,将 ja_vocab保存到文件ja_vocab.pkl中

  1. import pickle
  2. # open a file, where you want to store the data
  3. file = open('en_vocab.pkl', 'wb')
  4. # dump information to that file
  5. pickle.dump(en_vocab, file)
  6. file.close()
  7. file = open('ja_vocab.pkl', 'wb')
  8. pickle.dump(ja_vocab, file)
  9. file.close()

 使用 PyTorch save 和 load 函数保存模型以供以后使用。有两种方法可以保存模型。第一种方法仅用于推理,可以稍后加载模型并使用它从日语翻译成英语。

  1. # save model for inference
  2. torch.save(transformer.state_dict(), 'inference_model')

 第二种方法不仅用于推理,还用于以后想要加载模型并想要恢复训练时。

  1. # save model + checkpoint to resume training later
  2. torch.save({
  3. 'epoch': NUM_EPOCHS,
  4. 'model_state_dict': transformer.state_dict(),
  5. 'optimizer_state_dict': optimizer.state_dict(),
  6. 'loss': train_loss,
  7. }, 'model_checkpoint.tar')

以下为保存的词汇表和模型:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/972708?site
推荐阅读
相关标签
  

闽ICP备14008679号