【新手入门】NLP机器翻译(3)——基于Transformer的翻译模型

一、引言

背景

本翻译模型主要使用Jupyter Notebook、PyTorch、Torchtext和SentencePiece来实现日语-中文的机器翻译。

Transformer模型由Vaswani等人在2017年的论文《Attention Is All You Need》中首次提出。它是一种采用注意力机制的深度学习模型。该模型广泛应用于自然语言处理(NLP)和计算机视觉(CV)领域。

在Transformer模型之前,大多数最先进的NLP系统都依赖于诸如LSTM门控循环单元(GRU)等门控RNN模型,并在此基础上增加了注意力机制。与之前的循环神经网络(RNN)和长短期记忆网络(LSTM)不同的是,Transformer完全基于注意力机制来捕捉输入序列中的依赖关系,从而实现并行化处理,并且训练时间更少。

Transformer 的整体模型架构如下图所示:

                                

e98905db01e9482589fb10e9c46f5a47.png

使用Tranformer模型的基本步骤:

  1. 输入嵌入:首先将输入句子转换为称为嵌入的数字表示。它们捕获输入序列中标记的语义含义。对于单词序列,这些嵌入可以在训练期间学习,也可以从预训练的单词嵌入中获得。

  2. 位置编码:位置编码通常作为一组附加值或向量引入,这些值或向量在将它们输入转换器模型之前添加到令牌嵌入中。这些位置编码具有对位置信息进行编码的特定模式。

  3. 多头注意力:自注意力在多个“注意力头”中运行,以捕获令牌之间不同类型的关系。Softmax函数是一种激活函数,用于计算自注意力机制中的注意力权重。

  4. 层归一化和残差连接:该模型使用层归一化和残差连接来稳定和加速训练。

  5. 前馈神经网络:自注意力层的输出通过前馈层传递。这些网络将非线性变换应用于标记表示,使模型能够捕获数据中的复杂模式和关系。

  6. 堆叠层:转换器通常由堆叠在一起的多个层组成。每一层都处理前一层的输出,逐渐细化表示。堆叠多个图层使模型能够捕获数据中的分层和抽象特征。

  7. 输出层:在神经机器翻译等序列到序列任务中,可以在编码器顶部添加单独的解码器模块来生成输出序列。

  8. 训练:Transformer 模型使用监督学习进行训练,在监督学习中,它们学习最小化损失函数,该函数量化模型的预测与给定任务的基本事实之间的差异。训练通常涉及优化技术,如 Adam 或随机梯度下降 (SGD)。

  9. 推理:训练后,模型可用于对新数据进行推理。在推理过程中,输入序列通过预训练模型传递,模型为给定任务生成预测或表示。

二、Transformer模型原理

Transformer属于编码器-解码器(Encoder-Decoder)结构

编码器和解码器都分别有6层,Encoder的最后一层会连接Decoder的每一层

9d129e7aac8c4fb8bac71738731829a8.png

编码器和解码器的详细结构

编码器结构:1)多头自注意力机制

                      2)前馈网络

解码器结构:1)掩码多头自注意力机制

                      2)多头自注意力机制

                      3)前馈网络

cd44133cb7b245698d767c886e4ce48c.png

原理解释

1.自注意力(Self-Attention)

 1)公式:

4d011e7d724b4a4498ad1d67c9e8d0c8.png

2)具体过程:

计算自注意力的第一步是从编码器的每个输入向量(在本例中为每个单词的嵌入)创建三个向量。因此,对于每个单词,我们创建一个 Query 向量、一个 Key 向量和一个 Value 向量。这些向量是通过将嵌入乘以我们在训练过程中训练的三个矩阵而创建的。

请注意,这些新向量的维度小于嵌入向量。它们的维数为 64,而嵌入和编码器输入/输出向量的维数为 512。它们不必更小,这是一种架构选择,可以使多头注意力的计算(大部分)恒定。

d066facc59afa0efd245e8983f30107c.png

将 x1 乘以 WQ 权重矩阵得到 q1,即与该单词关联的“查询”向量。我们最终为输入句子中的每个单词创建了一个“查询”、“键”和一个“值”投影。

什么是“查询(Queries)”、“键(Keys)”和“值(Values)”向量?

它们是用于计算和思考注意力的抽象概念。一旦你继续阅读下面的注意力是如何计算的,你就会知道你需要知道的关于这些向量中的每一个所扮演的角色的所有信息。

第二步是计算分数。假设我们正在计算这个例子中第一个词“思考”的自我注意力。我们需要根据这个单词对输入句子的每个单词进行评分。分数决定了当我们在某个位置对单词进行编码时,对输入句子的其他部分的关注程度。

分数的计算方法是将查询向量的点积与我们评分的相应单词的关键向量相得益彰。因此,如果我们处理位置 #1 中单词的自我注意力,第一个分数将是 q1 和 k1 的点积。第二个分数是 q1 和 k2 的点积。

8a95c1b2844d3bd5ac7edf204ed8d9a1.png

第三步和第四步是将分数除以 8(论文中使用的关键向量维度的平方根 - 64。这导致梯度更稳定。这里可能还有其他可能的值,但这是默认值),然后通过 softmax 操作传递结果。Softmax 对分数进行归一化,因此它们都是正数,加起来为 1。

6d656a48f430f15f1bb5a87b3d47cbfa.png

这个 softmax 分数决定了每个单词在这个位置的表达量。显然,这个位置的单词将具有最高的 softmax 分数,但有时关注与当前单词相关的另一个单词很有用。

第五步是将每个值向量乘以 softmax 分数(准备将它们相加)。这里的直觉是保持我们想要关注的单词的值不变,并淹没不相关的单词(例如,通过将它们乘以像 0.001 这样的小数字)。

第六步是求和加权值向量。这将在此位置(对于第一个单词)产生自我注意力层的输出。

278b576de68ef9c4a891319561bd4514.png

2.多头注意力(Multi-Head Attention)

其实就是多个Self-Attention结构的结合,每个head学习到在不同表示空间中的特征,所谓“多头”(Multi-Head),就是做h次同样的事情(参数不共享),然后把结果拼接。

e9bff18180a246f99ffad96525f4500c.png

3.位置编码(Positional Encoding)

位置编码通过给输入的每个位置添加一个向量,从而引入位置信息。这些向量与输入的词嵌入向量相加,形成带有位置信息的输入表示。

位置编码有多种实现方式,最常见的一种是使用正弦和余弦函数。这种方法的优点是位置编码具有周期性,可以对任意长度的序列进行编码。

公式:

1aa1c1d112ae4c9d9e434b1d07c398b2.png

4.层归一化(Layer Normalization

层归一化是由Ba等人提出的一种归一化方法。与批归一化(Batch Normalization)不同,层归一化在每个样本的特征维度上进行归一化。它应用于每个变压器层内的每个子层(例如,多头自注意力层和前馈层)之后,也适用于不同的批次大小。它通过确保每层中的激活具有一致的均值和方差来帮助稳定训练过程。通过归一化加速收敛,可提高模型的训练速度和性能。

5.残差连接(Residual Connection)

残差连接是由He等人在ResNet(Residual Networks)中提出的。其核心思想是在每一层的输出中添加输入,使得网络能够更容易地训练更深的层次。

公式:Output=Layer(x)+x

优点:

1)梯度更稳定:残差连接可以缓解梯度消失和梯度爆炸问题,从而使深层网络更容易训练。

2)信息传递:它允许信息直接从浅层传递到深层,从而保留了浅层的信息。提高模型学习复杂和分层表示的能力。

三、代码实现

环境:

python:3.8
torch: 1.11.0+cu113
torchvision: 0.12.0+cu113
torchaudio: 0.11.0
numpy: =1.20.0
matplotlib: =3.4.0
scikit-learn: =0.24.0
pandas: =1.3.0
jupyterlab: =3.2.0
tqdm: =4.62.0
sentencepiece:0.1.91
torchtext:0.6.0

具体的环境配置问题在【NLP机器翻译(1)—基础介绍】http://t.csdnimg.cn/J4hmm有详细说明

1.数据处理

(1)导入必要的库

下好环境后才能使用下面import的库

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,请在你自己的电脑上尝试运行这一套代码
(2)下载数据集

我们将使用从JParaCrawl下载的日语-中文平行数据集,该数据集是目前最大公开可用的英日平行语料库之一,由NTT创建,通过大规模爬取网络并自动对齐平行句子生成。

下载网址:

http://www.kecl.ntt.co.jp/icl/lirg/jparacrawl]

需要提前下好这三个文件:

bbdd06b898a44683990d20607feb7b86.png

读取文件:

# 读取一个制表符分隔的 CSV 文件
df = pd.read_csv('./zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
# 从读取的数据帧(df)中提取第三列(索引为2)并将其转换为列表,用作训练数据(英语部分)。
trainen = df[2].values.tolist()#[:10000]
# 从读取的数据帧(df)中提取第四列(索引为3)并将其转换为列表,用作训练数据(日语部分)。
trainja = df[3].values.tolist()#[:10000]
trainen.pop(5972)
trainja.pop(5972)# 从训练数据的日语部分中移除索引为5972的元素。

在导入所有的日语和中文数据后,删除最后一个包含缺失值的数据。训练数据中的句子总数为5,973,071。

eg:例子

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)准备分词器

与英语或其他拼音语言不同,日语句子不包含用空格分隔的单词。

我们可以使用JParaCrawl提供的由SentencePiece创建的分词器对日语和中文进行分词。

# 加载中文SentencePiece模型,用于将中文文本进行分词或子词化处理
zh_tokenizer = spm.SentencePieceProcessor(model_file='spm.zh.nopretok.model')
# 加载日文SentencePiece模型,用于将日文文本进行分词或子词化处理
ja_tokenizer = spm.SentencePieceProcessor(model_file='spm.ja.nopretok.model')

eg1:测试英文分词器

#encode 方法将输入的句子拆分成子词单元,编码输出为字符串
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',
 '.']

eg2:测试日文分词器

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

运行结果:

['▁',
 '年',
 '金',
 '▁日本',
 'に住んでいる',
 '20',
 '歳',
 '~',
 '60',
 '歳の',
 '全ての',
 '人は',
 '、',
 '公的',
 '年',
 '金',
 '制度',
 'に',
 '加入',
 'しなければなりません',
 '。']

2.建立模型

(1)构建词汇表
#构建词汇表
def build_vocab(sentences, tokenizer):
    counter = Counter() # 创建一个 Counter 对象用于统计词频
    for sentence in sentences:
        # 对句子进行分词,并更新 counter 统计每个子词的频率
        counter.update(tokenizer.encode(sentence, out_type=str))
    # 返回构建的词汇表 Vocab 对象,包含特殊符号
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])
# 使用 build_vocab 函数构建日文词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)
# 使用 build_vocab 函数构建英文词汇表
en_vocab = build_vocab(trainen, en_tokenizer)
(2)构建张量(tensor)
# 定义函数 data_process,用于处理日文和英文数据,生成相应的张量数据
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 列表中
        data.append((ja_tensor_, en_tensor_))
    return data
# 使用 data_process 函数处理训练集中的日文和英文句子,生成训练数据
train_data = data_process(trainja, trainen)
(3)处理张量并创建DataLoader对象

        创建 DataLoader对象方便迭代,将BATCH_SIZE设置为8以防止“cuda内存不足”

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: 
        # 在每个张量的开始和结束添加 BOS 和 EOS 符号
        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_data = data_process(trainja, trainen)
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)
(4)Seq2SeqTransformer模型

一个基于Transformer的序列到序列(Seq2Seq)模型,由编码器(Encoder)和解码器(Decoder)组成,用于将源语言序列转换为目标语言序列。

from torch.nn import (TransformerEncoder, TransformerDecoder,
                      TransformerEncoderLayer, TransformerDecoderLayer)


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__()
        # 定义编码器层,使用TransformerEncoderLayer
        encoder_layer = TransformerEncoderLayer(d_model=emb_size, nhead=NHEAD,
                                                dim_feedforward=dim_feedforward)
        # 定义编码器,由多个编码器层组成
        self.transformer_encoder = TransformerEncoder(encoder_layer, num_layers=num_encoder_layers)
        
        # 定义解码器层,使用TransformerDecoderLayer
        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)
(5)位置编码和词嵌入

        PositionalEncoding为序列中的每个位置添加位置编码,使模型能够利用序列中单词的位置信息。位置编码有助于Transformer在没有循环神经网络(RNN)的情况下处理序列数据。

        TokenEmbedding类为输入序列中的每个token提供词嵌入表示,并对嵌入进行缩放。

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))
        # 计算位置编码的奇数列使用sin函数
        pos_embedding[:, 0::2] = torch.sin(pos * den)
        # 计算位置编码的偶数列使用cos函数
        pos_embedding[:, 1::2] = torch.cos(pos * den)
        # 增加一个维度以适应批处理
        pos_embedding = pos_embedding.unsqueeze(-2)

        # 定义dropout层
        self.dropout = nn.Dropout(dropout)
        # 注册位置编码矩阵为buffer,使其在模型保存和加载时保持不变
        self.register_buffer('pos_embedding', pos_embedding)

    def forward(self, token_embedding: Tensor):
        # 将位置编码和token嵌入相加并应用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):
        # 返回token的嵌入表示,并乘以嵌入维度的平方根进行缩放
        return self.embedding(tokens.long()) * math.sqrt(self.emb_size)
(6)创建掩码

生成掩码用于在seq2seq的任务中处理源序列和目标序列。掩码的作用是在训练模型时阻止某些位置的信息传播,以避免模型看到未来的词或无意义的填充值

# 生成一个方形的逐步掩码用于序列。
def generate_square_subsequent_mask(sz):#sz (int): 掩码的大小(序列长度)
    # 创建一个矩阵,上三角元素设置为1,其余设置为0
    mask = (torch.triu(torch.ones((sz, sz), device=device)) == 1).transpose(0, 1)
    # 将掩码转换为浮点型
    mask = mask.float()
    # 填充掩码:将0值填充为'-inf',将1值填充为'0.0'
    mask = mask.masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
    return mask
#创建源序列和目标序列的掩码
def create_mask(src, tgt):#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

3.训练模型

(1)给定参数并初始化模型

使用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  # 解码器层数
NUM_EPOCHS = 16  # 训练轮数

transformer = Seq2SeqTransformer(NUM_ENCODER_LAYERS, NUM_DECODER_LAYERS,
                                 EMB_SIZE, SRC_VOCAB_SIZE, TGT_VOCAB_SIZE,
                                 FFN_HID_DIM)

# 初始化参数
for p in transformer.parameters():
    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):
    """
    训练模型一个epoch。

    参数:
    - model (nn.Module): 要训练的模型。
    - train_iter (DataLoader): 训练数据的迭代器。
    - optimizer (torch.optim.Optimizer): 优化器。

    返回:
    - float: 训练过程中的平均损失。
    """
    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)  # 返回平均损失


def evaluate(model, val_iter): #val_iter: 验证数据的迭代器。
    model.eval()  # 设置模型为评估模式
    losses = 0
    for idx, (src, tgt) in enumerate(val_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)  # 返回平均损失
(2)开始训练

使用gpu训练大概需要1小时

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

运行结果:

 24af1f9fa0f74a48afc41e4ffd4188d1.png

4.翻译

(1)翻译函数

使用贪婪解码法生成翻译结果,再用translate函数翻译

def greedy_decode(model, src, src_mask, max_len, start_symbol):
    """
    使用贪婪解码法生成翻译结果。

    参数:
    - model (nn.Module): 训练好的模型。
    - src (Tensor): 源语言序列。
    - src_mask (Tensor): 源语言掩码。
    - max_len (int): 生成的最大长度。
    - start_symbol (int): 起始符号的索引。

    返回:
    - Tensor: 生成的目标语言序列。
    """
    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 (nn.Module): 训练好的模型。
    - src (str): 源语言句子。
    - src_vocab (Vocab): 源语言词汇表。
    - tgt_vocab (Vocab): 目标语言词汇表。
    - src_tokenizer (Tokenizer): 源语言的分词器。

    返回:
    - str: 生成的目标语言句子。
    """
    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>", "")  # 返回翻译结果

eg:翻译一句日语

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

运行结果:9d858bf951354fe38183f12205ff74a8.png

5.保存词汇表和翻译模型

(1)保存词汇表

         使用 Pickle 保存英文,日文词汇表(en_vocab 和 ja_vocab)

import pickle

# 保存英语词汇表
with open('en_vocab.pkl', 'wb') as file:
    pickle.dump(en_vocab, file)

# 保存日语词汇表
with open('ja_vocab.pkl', 'wb') as file:
    pickle.dump(ja_vocab, file)
(2)保存模型

        有两种保存模型的方式

        i:第一种仅仅保存当前训练好的模型,以后直接用来日语到英语的翻译。

torch.save(transformer.state_dict(), 'inference_model')

        ii:第二种还保存了模型的相关参数,日后使用模型时也可以继续训练

# 保存模型和检查点以便以后恢复训练
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、付费专栏及课程。

余额充值