当前位置:   article > 正文

基于Transformer实现机器翻译_transformer机器翻译代码

transformer机器翻译代码

一、前言

1.1 Transformer概述

        Transformer模型是一种深度学习模型,由Vaswani等人在2017年提出,主要用于自然语言处理(NLP)任务。它的核心思想是通过自注意力(Self-Attention)机制来捕捉输入数据之间的全局依赖关系,从而能够处理序列数据。

1.2 Transformer整体模型架构

二、使用Transformer和PyTorch的机器翻译模型(日译中)

2.1 配置环境以及导入软件包

确保我们的系统中安装了以下软件包,如果您发现缺少某些软件包,请务必安装它们。

由于我的电脑没有GPU,租用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)
  19. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  20. # print(torch.cuda.get_device_name(0)) ## 如果你有GPU,请在你自己的电脑上尝试运行这一套代码

2.2 获取平行数据集

        我们将使用从JParaCrawl[http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl]下载的日英平行数据集,该数据集被描述为由NTT创建的最大的可公开获取的英日平行语料库。它是通过大量抓取网络并自动对齐平行句子而创建的。

  1. df = pd.read_csv('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)

        在导入所有日语和英语对应数据后,我删除了数据集中的最后一个数据,因为它缺少值。总的来说,trainen 和 trainja 中的句子数为 5,973,071,但是,出于学习目的,通常建议在一次性使用所有数据之前对数据进行采样并确保一切按预期工作,以节省时间。

下面是数据集中包含的句子示例:

  1. print(trainen[500])
  2. print(trainja[500])

2.3 准备分词器

        与英语或其他字母语言不同,日语句子不包含空格来分隔单词。我们可以使用JParaCrawl提供的分词器,该分词器是使用SentencePiece创建的日语和英语,您可以访问JParaCrawl网站下载它们,或单击此处。

  1. en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
  2. 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)

结果:

ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。", out_type=str)

 结果:

2.4 构建 TorchText Vocab 对象并将句子转换为 Torch 张量

使用分词器和原始句子,我们构建从 TorchText 导入的 Vocab 对象。

  1. def build_vocab(sentences, tokenizer):#定义了一个名为 build_vocab 的函数,它接受两个参数 sentences 和 tokenizer
  2. counter = Counter()
  3. for sentence in sentences:
  4. counter.update(tokenizer.encode(sentence, out_type=str))#对于每个句子 sentence,它使用 tokenizer 对象的 encode 方法将句子编码成字符串类型的标记序列,并更新计数器 counter
  5. return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
  6. ja_vocab = build_vocab(trainja, ja_tokenizer)
  7. en_vocab = build_vocab(trainen, en_tokenizer)#使用 build_vocab 函数来构建日语 trainja 和英语 trainen 的词汇表,使用了对应的 ja_tokenizer 和 en_tokenizer 来对句子进行编码

获得词汇表对象后,我们可以使用词汇表和分词器对象来构建训练数据的张量。

  1. def data_process(ja, en):
  2. data = []#初始化一个空列表 data,用于存储处理后的数据
  3. for (raw_ja, raw_en) in zip(ja, en):#使用 zip 函数迭代 ja 和 en 中对应的元素
  4. ja_tensor_ = torch.tensor([ja_vocab[token] for token in ja_tokenizer.encode(raw_ja.rstrip("\n"), out_type=str)],
  5. dtype=torch.long)#使用 ja_tokenizer 对当前的日语句子 raw_ja 进行编码,将其转换为一个字符串标记列表
  6. en_tensor_ = torch.tensor([en_vocab[token] for token in en_tokenizer.encode(raw_en.rstrip("\n"), out_type=str)],
  7. dtype=torch.long)
  8. data.append((ja_tensor_, en_tensor_))#将元组 (ja_tensor_, en_tensor_) 添加到 data 列表中
  9. return data
  10. train_data = data_process(trainja, trainen)#使用 data_process 函数将训练数据 (trainja 和 trainen) 处理为张量,并将结果存储在 train_data 中,用于后续的机器翻译模型训练

2.5 创建要在训练期间迭代的 DataLoader 对象

  1. BATCH_SIZE = 8 #定义了批量处理的大小为 8,即每次从数据中取出 8 个样本进行处理
  2. PAD_IDX = ja_vocab['<pad>'] #获取了日语词汇表中 <pad> 标记的索引,用于数据填充
  3. BOS_IDX = ja_vocab['<bos>'] #获取了日语词汇表中 <bos>(句子开始)标记的索引
  4. EOS_IDX = ja_vocab['<eos>'] #获取了日语词汇表中 <eos>(句子结束)标记的索引
  5. def generate_batch(data_batch):
  6. ja_batch, en_batch = [], [] #初始化两个空列表 ja_batch 和 en_batch,用于存储处理后的批量数据
  7. for (ja_item, en_item) in data_batch:
  8. ja_batch.append(torch.cat([torch.tensor([BOS_IDX]), ja_item, torch.tensor([EOS_IDX])], dim=0)) #在日语句子张量的开头和结尾分别添加 <bos> 和 <eos> 标记,并进行张量拼接
  9. en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0)) #在英语句子张量的开头和结尾分别添加 <bos> 和 <eos> 标记,并进行张量拼接
  10. ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX) #使用 pad_sequence 函数对日语批量 ja_batch 进行填充,使得每个批量的日语句子张量具有相同的长度,用 PAD_IDX 值进行填充
  11. en_batch = pad_sequence(en_batch, padding_value=PAD_IDX) #使用 pad_sequence 函数对英语批量 en_batch 进行填充,使得每个批量的英语句子张量具有相同的长度,用 PAD_IDX 值进行填充
  12. return ja_batch, en_batch
  13. train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
  14. shuffle=True, collate_fn=generate_batch)

 2.6 序列到序列转换器

Transformer 是 “Attention is all you need” 论文中介绍的 Seq2Seq 模型,用于解决机器翻译任务。Transformer 模型由编码器和解码器块组成,每个块包含固定数量的层。

编码器通过一系列多头注意力和前馈网络层传播输入序列来处理输入序列。编码器的输出称为内存,与目标张量一起馈送到解码器。编码器和解码器使用教师强制技术以端到端的方式进行训练。

  1. from torch.nn import (TransformerEncoder, TransformerDecoder,
  2. TransformerEncoderLayer, TransformerDecoderLayer)
  3. class Seq2SeqTransformer(nn.Module):
  4. def __init__(self, num_encoder_layers: int, num_decoder_layers: int,
  5. emb_size: int, src_vocab_size: int, tgt_vocab_size: int,
  6. dim_feedforward:int = 512, dropout:float = 0.1):
  7. super(Seq2SeqTransformer, self).__init__() # 调用父类 nn.Module 的初始化方法
  8. # encoder_layer 和 decoder_layer 是 Transformer 模型的编码器层和解码器层,通过 TransformerEncoderLayer 和 TransformerDecoderLayer 初始化
  9. encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
  10. dim_feedforward=dim_feedforward)
  11. self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
  12. decoder_layer = TransformerDecoderLayer(d_model=emb_size, nhead=NHEAD,
  13. dim_feedforward=dim_feedforward)
  14. self.transformer_decoder = TransformerDecoder(decoder_layer, num_layers=num_decoder_layers)
  15. self.generator = nn.Linear(emb_size, tgt_vocab_size) #一个线性层,用于将解码器的输出转换为目标词汇的分布
  16. self.src_tok_emb = TokenEmbedding(src_vocab_size, emb_size)
  17. self.tgt_tok_emb = TokenEmbedding(tgt_vocab_size, emb_size) #输入和输出词汇表的词嵌入层
  18. self.positional_encoding = PositionalEncoding(emb_size, dropout=dropout) #位置编码层,用于给输入的词嵌入增加位置信息
  19. def forward(self, src: Tensor, trg: Tensor, src_mask: Tensor,
  20. tgt_mask: Tensor, src_padding_mask: Tensor,
  21. tgt_padding_mask: Tensor, memory_key_padding_mask: Tensor):
  22. src_emb = self.positional_encoding(self.src_tok_emb(src))
  23. tgt_emb = self.positional_encoding(self.tgt_tok_emb(trg)) #经过位置编码后的输入和输出的词嵌入
  24. memory = self.transformer_encoder(src_emb, src_mask, src_padding_mask) #通过编码器得到的内部记忆
  25. outs = self.transformer_decoder(tgt_emb, memory, tgt_mask, None,
  26. tgt_padding_mask, memory_key_padding_mask) #解码器的输出
  27. return self.generator(outs)
  28. #将输入序列的词嵌入经过位置编码后,通过编码器进行处理,返回编码器的输出
  29. def encode(self, src: Tensor, src_mask: Tensor):
  30. return self.transformer_encoder(self.positional_encoding(
  31. self.src_tok_emb(src)), src_mask)
  32. #将目标序列的词嵌入经过位置编码后,通过解码器进行处理,返回解码器的输出
  33. def decode(self, tgt: Tensor, memory: Tensor, tgt_mask: Tensor):
  34. return self.transformer_decoder(self.positional_encoding(
  35. self.tgt_tok_emb(tgt)), memory,
  36. tgt_mask)

文本标记通过使用标记嵌入来表示。位置编码被添加到标记嵌入中,以引入词序的概念。 

  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. # 根据位置索引和分母计算 sin 和 cos 形式的位置编码
  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. self.emb_size = emb_size
  28. def forward(self, tokens: Tensor):
  29. # 返回经缩放后的嵌入向量
  30. return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

 我们创建一个后续单词掩码来阻止目标单词关注其后续单词,同时还创建了用于屏蔽源语言和目标语言填充标记的屏蔽(mask)。

  1. def generate_square_subsequent_mask(sz):
  2. # 创建一个上三角矩阵,并将其转置为下三角
  3. mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
  4. # 将 mask 转换为浮点型,并将值为 0 的位置填充为负无穷,将值为 1 的位置填充为 0
  5. mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
  6. return mask
  7. def create_mask(src, tgt):
  8. src_seq_len = src.shape[0]
  9. tgt_seq_len = tgt.shape[0]
  10. # 生成目标序列的自注意力掩码
  11. tgt_mask = generate_square_subsequent_mask(tgt_seq_len)
  12. # 创建源序列的填充掩码
  13. src_mask = torch.zeros((src_seq_len, src_seq_len), device=device).type(torch.bool)
  14. # 创建源序列的填充掩码,标记 PAD_IDX 的位置为 True
  15. src_padding_mask = (src == PAD_IDX).transpose(0, 1)
  16. # 创建目标序列的填充掩码,标记 PAD_IDX 的位置为 True
  17. tgt_padding_mask = (tgt == PAD_IDX).transpose(0, 1)
  18. 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。 

  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
  6. BATCH_SIZE = 16
  7. NUM_ENCODER_LAYERS = 3
  8. NUM_DECODER_LAYERS = 3
  9. NUM_EPOCHS = 16
  10. # 初始化 Seq2SeqTransformer 模型
  11. transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
  12. EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
  13. FFN_HID_DIM)
  14. # 使用 Xavier 初始化方法初始化模型参数
  15. for p in transformer.parameters():
  16. if p.dim() > 1:
  17. nn.init.xavier_uniform_(p)
  18. # 将模型移动到指定的设备(如 GPU)
  19. transformer = transformer.to(device)
  20. # 定义损失函数为交叉熵损失函数,忽略 PAD_IDX 的预测
  21. loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)
  22. # 定义优化器为 Adam 优化器
  23. optimizer = torch.optim.Adam(
  24. transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9
  25. )
  26. def train_epoch(model, train_iter, optimizer):
  27. model.train()
  28. losses = 0
  29. for idx, (src, tgt) in enumerate(train_iter):
  30. src = src.to(device)
  31. tgt = tgt.to(device)
  32. # 生成源序列和目标输入序列的掩码
  33. tgt_input = tgt[:-1, :]
  34. src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)
  35. # 前向传播
  36. logits = model(src, tgt_input, src_mask, tgt_mask,
  37. src_padding_mask, tgt_padding_mask, src_padding_mask)
  38. # 梯度清零
  39. optimizer.zero_grad()
  40. # 生成目标输出序列
  41. tgt_out = tgt[1:, :]
  42. # 计算损失
  43. loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
  44. # 反向传播和优化
  45. loss.backward()
  46. optimizer.step()
  47. # 累计损失
  48. losses += loss.item()
  49. # 返回平均损失
  50. return losses / len(train_iter)
  51. def evaluate(model, val_iter):
  52. model.eval()
  53. losses = 0
  54. for idx, (src, tgt) in enumerate(val_iter):
  55. src = src.to(device)
  56. tgt = tgt.to(device)
  57. # 生成源序列和目标输入序列的掩码
  58. tgt_input = tgt[:-1, :]
  59. src_mask, tgt_mask, src_padding_mask, tgt_padding_mask = create_mask(src, tgt_input)
  60. # 前向传播
  61. logits = model(src, tgt_input, src_mask, tgt_mask,
  62. src_padding_mask, tgt_padding_mask, src_padding_mask)
  63. # 生成目标输出序列
  64. tgt_out = tgt[1:, :]
  65. # 计算损失
  66. loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
  67. # 累计损失
  68. losses += loss.item()
  69. # 返回平均损失
  70. return losses / len(val_iter)

2.7 开始训练

        最后,在准备了必要的类和函数之后,我们准备训练我们的模型。 使用A800-80GB GPU进行训练,时间预计为55分钟。

  1. for epoch in tqdm.tqdm(range(1, NUM_EPOCHS+1)):
  2. start_time = time.time()
  3. # 训练一个 epoch
  4. train_loss = train_epoch(transformer, train_iter, optimizer)
  5. end_time = time.time()
  6. # 打印当前 epoch 的训练损失和训练时间
  7. print((f"Epoch: {epoch}, Train loss: {train_loss:.3f}, "
  8. f"Epoch time = {(end_time - start_time):.3f}s"))

 训练过程如下:

  

2.8 保存词汇表对象和训练好的模型

在训练完成后,我们使用 Pickle 来保存词汇表对象(en_vocab 和 ja_vocab)。

  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博客】
推荐阅读
相关标签
  

闽ICP备14008679号