在恒源云GPU上实现基于transformer实现机器翻译(日译中)

一、GPU配置

打开恒源云的网页,点击右上角中的“我的实例”

点击创建实例以后,选择你想要的GPU性能,以下仅供参考。

购买成功后,点击jupyterlab即可开始自由操作。

二、实验过程

1.环境的配置

注意:运行程序前,云GPU可能缺少torchtext等python软件包,需要我们自己安装,安装过程如下:

点击终端

以torchtext安装包为例,在终端中输入以下安装代码:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple torchtext

 其他软件包安装类似,个人建议使用清华源安装速度快。

运行完成以后,开始正式进入代码阶段。这段代码的目的是准备机器学习和深度学习所需的基本环境和库。它包括导入必要的库和设置随机种子,以及检测和选择运行设备(GPU 或 CPU)。

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,请在你自己的电脑上尝试运行这一套代码
device

 以上两行代码的运行结果为:

2.数据集的处理

1.获取数据

df = pd.read_csv('./zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainen = df[2].values.tolist()#[:10000]
trainja = df[3].values.tolist()#[:10000]
# trainen.pop(5972)
# trainja.pop(5972)

2.打印数据集结果 

print(trainen[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 ...

3.准备分词器 

en_tokenizer = spm.SentencePieceProcessor(model_file='spm.en.nopretok.model')
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.")
ja_tokenizer.encode("年金 日本に住んでいる20歳~60歳の全ての人は、公的年金制度に加入しなければなりません。")

假设代码运行后,代码结果如图所示:

[227,
 2980,
 8863,
 373,
 8,
 9381,
 126,
 91,
 649,
 11,
 93,
 240,
 19228,
 11,
 419,
 14926,
 102,
 5] 

[2388,
 4,
 5539,
 1250,
 1478,
 43,
 4,
 2412,
 234,
 2022,
 2119,
 4,
 3329,
 4,
 10520,
 14651,
 5696,
 3304,
 4,
 17375,
 1803,
 1790,
 2175,
 4981,
 4,
 442,
 20322,
 1803,
 4,
 5455,
 2668,
 23468,
 4,
 19479,
 15]                   

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

构建日语和英语的词汇表,并将训练数据处理成模型可用的数据格式。

build_vocab(sentences, tokenizer) 函数

def build_vocab(sentences, tokenizer):
  counter = Counter()
  for sentence in sentences:
    counter.update(tokenizer.encode(sentence, out_type=str))
  return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
  • build_vocab 函数接收两个参数:sentences 是一个列表,包含了要构建词汇表的句子;tokenizer 是用来对句子进行编码的工具。
  • counter 是一个计数器对象,用于统计词频。
  • 对于每个句子 sentence,使用 tokenizer.encode() 方法将其编码为字符串形式,并更新计数器 counter
  • 最后,使用 Counter 对象创建一个 Vocab 对象,指定特殊标记 specials=['<unk>', '<pad>', '<bos>', '<eos>'],表示词汇表中的特殊标记。
  • 返回构建好的 Vocab 对象,即词汇表。

构建日语和英语词汇表

ja_vocab = build_vocab(trainja, ja_tokenizer)
en_vocab = build_vocab(trainen, en_tokenizer)
  • trainja 和 trainen 是之前从文件中读取并处理的日语和英语句子列表。
  • ja_tokenizer 和 en_tokenizer 是用来对日语和英语句子进行编码的工具。

data_process(ja, en) 函数

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
  • data_process 函数接收两个参数 ja 和 en,分别表示日语和英语的句子列表。
  • 创建一个空列表 data,用于存储处理后的数据。
  • 使用 zip(ja, en) 将日语列表 ja 和英语列表 en 进行逐对匹配处理。
  • 对于每一对 (raw_ja, raw_en)
    • ja_tensor_ 将日语句子 raw_ja 编码为字符串后,然后根据 ja_vocab 将每个词汇转换为对应的索引,最后转换为 torch.tensor 类型。
    • en_tensor_ 同样处理英语句子 raw_en

生成训练数据

train_data = data_process(trainja, trainen)

通过使用给定的句子分词工具和PyTorch张量,将日语和英语句子处理成适合神经网络训练的数据格式。它首先构建了两个语言的词汇表,然后将训练数据转换为张量表示,方便后续在深度学习模型中使用。 

5.创建DataLoader对象 

将预处理好的日语和英语数据转换成适合神经网络训练的批次数据格式,并使用 DataLoader 实现数据的批次加载和填充处理

常量定义

BATCH_SIZE = 8
PAD_IDX = ja_vocab['<pad>']
BOS_IDX = ja_vocab['<bos>']
EOS_IDX = ja_vocab['<eos>']

  1. BATCH_SIZE 定义了每个批次的大小,这里设定为 8。
  2. PAD_IDX 是日语词汇表中 <pad> 标记的索引。
  3. BOS_IDX 是日语词汇表中 <bos>(句子起始)标记的索引。
  4. EOS_IDX 是日语词汇表中 <eos>(句子结束)标记的索引。

generate_batch(data_batch) 函数

 
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)
  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)

6.Sequence-to-sequence Transformer

定义了一个完整的Transformer序列到序列模型,包括编码器、解码器、词嵌入、位置编码和生成器。它支持训练时的正向传播和单独的编码、解码操作,适用于需要处理序列到序列映射的任务,如机器翻译或语言生成。 

  1. 初始化方法 __init__:

    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__()
    

  2. 定义编码器和解码器层:

        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)
    

  3. 生成器和嵌入层:

     
        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)
  4. 前向传播方法 forward:

     

        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)
    
  5. 编码和解码方法:

     
    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)

接着定义一些辅助函数和模型组件,用于Transformer模型中的位置编码、词嵌入以及生成掩码的操作。

 5.类 PositionalEncoding

 
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)
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        return self.dropout(token_embedding +
                            self.pos_embedding[:token_embedding.size(0),:])

  • 功能:

    • PositionalEncoding 类用于为输入的词嵌入添加位置编码。
    • 在初始化方法中,通过计算得到一组固定的位置编码向量 pos_embedding,其形状为 (maxlen, emb_size),其中 maxlen 是最大序列长度,emb_size 是词嵌入的维度。
    • 位置编码采用了正弦和余弦函数的组合,以便模型能够学习序列中单词的位置信息。
    • dropout 是一个Dropout层,用于在训练过程中随机丢弃部分输入数据,防止过拟合。
  • 方法:

    • forward 方法接受一个张量 token_embedding,表示输入的词嵌入。
    • 将 token_embedding 与预先计算好的位置编码 pos_embedding 相加,并应用 dropout
    • 返回添加了位置编码和dropout的结果。

6.类 TokenEmbedding

 
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):
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)

  • 功能:
    • TokenEmbedding 类用于将输入的词索引转换为对应的词嵌入。
    • 在初始化方法中,创建了一个 nn.Embedding 层,用于存储词嵌入矩阵,大小为 (vocab_size, emb_size)
    • 在 forward 方法中,接受一个张量 tokens,表示输入的词索引序列。
    • 使用 self.embedding 将词索引转换为词嵌入,并乘以 math.sqrt(self.emb_size),这是为了缩放词嵌入,使其与位置编码的幅度相匹配。

7.函数 generate_square_subsequent_mask

 
def generate_square_subsequent_mask(sz):
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask

  • 功能:
    • generate_square_subsequent_mask 函数用于生成Transformer模型中解码器自注意力机制的遮罩(mask)。
    • 创建一个大小为 (sz, sz) 的上三角矩阵 mask,并使用 .transpose(0, 1) 将其转置,以便对角线以下的元素为 True,其余为 False
    • 将 mask 转换为浮点型张量,并用 .masked_fill 方法将 False 元素(即不应考虑的位置)分别填充为 -inf 和 0.0
    • 返回生成的遮罩 mask

8. 函数 create_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)
    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

  • 功能:

    • create_mask 函数用于创建Transformer模型中的输入序列和目标序列的遮罩。
    • 计算输入序列和目标序列的长度 src_seq_len 和 tgt_seq_len
    • 使用 generate_square_subsequent_mask 函数生成目标序列的自注意力遮罩 tgt_mask
    • 创建输入序列的填充遮罩 src_mask,其大小为 (src_seq_len, src_seq_len),所有元素为 False
    • 创建输入序列和目标序列的填充遮罩 src_padding_mask 和 tgt_padding_mask,这些遮罩将输入序列中的填充符号(如 PAD_IDX)标记为 True

9.模型定义和初始化

 
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 = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

  • 功能:
    • 定义了模型的超参数和架构。
    • Seq2SeqTransformer 是一个自定义的序列到序列Transformer模型,具有指定的编码器和解码器层数、词嵌入维度、隐藏层维度等。

10.参数初始化

 
for p in transformer.parameters():
    if p.dim() > 1:
        nn.init.xavier_uniform_(p)

  • 功能:
    • 使用Xavier初始化方法初始化模型的参数。这种方法有助于在训练初始阶段保持参数的梯度大致相等,从而加快模型的收敛速度。

11.损失函数和优化器定义

 
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
)

  • 功能:
    • CrossEntropyLoss 用作损失函数,对模型预测的logits和目标输出进行交叉熵计算,ignore_index=PAD_IDX指定忽略填充符号的损失计算。
    • Adam 是一种优化器,用于更新模型的参数,通过传入 transformer.parameters() 获取所有需要更新的参数。

12.训练过程 train_epoch

 
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)

  • 功能:
    • train_epoch 函数用于训练一个epoch的数据。
    • 将模型置为训练模式 (model.train())。
    • 遍历 train_iter 中的每个批次数据,将输入和目标数据移到设备上,并生成对应的掩码。
    • 调用模型 model 进行前向传播,计算logits。
    • 计算损失并进行反向传播和优化器更新。
    • 返回该epoch的平均损失。

13.评估过程 evaluate

 
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)

  • 功能:
    • evaluate 函数用于在验证集上评估模型的性能。
    • 将模型置为评估模式 (model.eval())。
    • 类似于训练过程,但不进行梯度计算和优化器更新。
    • 返回验证集上的平均损失,用于评估模型的泛化能力。

 

7.开始训练

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"))

假设代码运行结束,训练结果如下所示:

8.用模型翻译一句日语句子 

 

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)
    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 = [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>", "")

假设我们想让模型翻译下面这句日语:

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

则翻译结果为:

 ▁H S ▁ 代 码 ▁85 15 ▁ 用 于 焊 接 或 焊 接 设 备 的 电 气 ( 包 括 电 热 气 热 气 体 ) 。 '

实现了日译中的功能。

9.模型的保存

在训练完成后,我们将首先使用Pickle保存Vocab对象(en_vocab和ja_vocab)。我们还可以使用PyTorch保存和加载函数保存模型以供以后使用。 

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()

 

# 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')

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值