目录

Transformer介绍

为什么需要用transformer

Seq2Seq任务

Transformer属于Encoder-Decoder结构

Transformer基本架构

Transformer优缺点

Transformer模块介绍

多头注意力

基本概念

RNN与注意力机制

注意力机制的计算

位置编码

为什么需要位置编码

基本概念

计算流程

掩码注意力

掩码注意力背景

基本概念

计算流程

编解码注意力

基本思想

计算流程

残差连接

为什么需要残差连接

工作流程

​编辑基于Transformer框架的中日机器翻译

导入库

数据预处理

构建词汇表以及Torch张量

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

Sequence-to-sequence Transformer架构

编码器与解码器

位置编码

掩码注意力

 创建训练模型

模型训练 

测试模型 

保存模型

总结


Transformer介绍

为什么需要用transformer

之前我们使用的是RNN(或者是其的单向或者双向变种LSTM/GRU等) 来作为编解码器。RNN模块每次只能够吃进一个输入token和前一次的隐藏状态 ,然后得到输出。它的时序结构使得这个模型能够得到长距离的依赖关系,但是 这也使得它不能够并行计算,模型效率十分低。有时我们还使用了CNN,虽然CNN能够并行计算,但是处理变长序列时不够灵活。

为了克服以上问题,提出了Transform框架,该架构采用了自注意力机制,使得模型能够同时关注序列中的所有位置,从而捕捉长距离依赖关系。此外,Transformer 还采用了多头注意力和位置编码等技术,进一步提高了模型的性能。

Seq2Seq任务

基于Transform的机器翻译,是一个Seq2Seq任务。Seq2Seq 任务指的是输入和输出都是序列的任务,输出的长度不确定时采用的模型,这种情况一般是在机器翻译的任务中出现,将一句中文翻译 成英文,那么这句英文的长度有可能会比中文短,也有可能会比中文长, 所以输出的长度就不确定了。

Transformer属于Encoder-Decoder结构

如图。通常来说,Seq2Seq任务最常见的是使用Encoder+Decoder的模式,先将一 个序列编码成一个上下文矩阵,在使用Decoder来解码。当然,我们仅仅把 context vector作为编码器到解码器的输入。

Transformer基本架构

如图,Transform架构的核心思想是利用自注意力机制来建立输入序列中不同位置之间的关联。它通过对输入序列中的每个位置进行编码,使得模型能够同时考虑到所有位置的信息,而不仅仅依赖于局部上下文。

Transform架构主要由两个关键组件组成:编码器和解码器。

编码器负责将输入序列进行编码成一系列隐藏状态表示,其基本组件是多层的自注意力机制和前馈神经网络。自注意力机制允许编码器在计算隐藏状态时,根据输入序列中其他位置的信息自适应地调整注意力权重。具体而言,编码器通过将输入序列映射到一组查询(Query)、键(Key)和值(Value)向量,并利用这些向量计算注意力分数,进而得到加权和表示隐藏状态。随后,前馈神经网络对每个位置的隐藏状态进行非线性变换,从而得到最终的编码表示。

解码器则负责将编码器的输出解码为目标序列,它也由多层的自注意力机制和前馈神经网络组成。解码器在生成目标序列时,通过对已生成的部分序列进行自注意力机制,以便更好地捕捉上下文信息。此外,解码器还可以利用编码器的输出作为额外的输入,以进一步提高对输入序列的理解和翻译质量。

除了编码器和解码器,Transform架构还引入了一种叫做位置编码(Positional Encoding)的技术,用于为输入序列中的每个位置添加位置信息,以便模型能够区分不同位置之间的关系。

Transformer优缺点

Transformer虽然好,但它也不是万能地,还是存在着一些不足之处,接下来就来介绍一下它的优缺点:
优点:

  • 1.Transformer摆脱了人工标注数据集的缺陷,模型在质量上更优、更易于并行化,所需训练时间明显更少。
  • 2.Transformer通过成功地将其应用于具有大量和有限训练数据的分析,可以很好地推广到其他任务
  • 3.Transformer,它完全基于注意力机制,完全不用重复和卷积,因而这些模型在质量上更优,同时更易于并行化,并且需要的训练时间明显更少。
  • 4.Transformer出现以后,迅速取代了RNN系列变种,跻身主流模型架构基础。(RNN缺陷正在于流水线式的顺序计算)

缺点:

  • 1.完全基于self-attention,对于词语位置之间的信息有一定的丢失,虽然加入了positional encoding来解决这个问题,但也还存在着可以优化的地方

Transformer模块介绍

多头注意力

基本概念

多头注意力机制的核心思想是在计算注意力时,不是只用一个注意力机制,而是使用多个注意力机制(头)。每个头会在不同的子空间中独立地进行注意力计算,然后将这些头的输出结果拼接起来,再经过一次线性变换,得到最终的输出。

RNN与注意力机制

RNN会将它已经处理过的前面的所有单词/向量的表示与它正在处理的当前单词/向量结合起来。而自注意力机制会将所有相关单词的理解融入到我们正在处理的单词中。

注意力机制的计算

首先先算出Query,Key,Value矩阵,用输入的嵌入向量组成矩阵X,然后再把它乘以训练好的W^{Q},W^{K},W^{V}。得到Q,K,V矩阵

接着按下列公式计算

第二步可分为多步,如图以向量表示。

首先计算自注意力分数,然后将其除以8(为什么选8?是因为key向量的维度是64,取其平方根,这样让梯度计算的时候更稳定。默认是这么设置的,当然也可以用其他值),除8之后将结果扔进softmax计算,使结果归一化,softmax之后注意力分数相加等于1,并且都是正数。之后将每个value向量乘以注意力分数。这是为了留下我们想要关注的单词的value,并把其他不相关的单词丢掉,将上一步的结果相加,输出本位置的注意力结果。

位置编码

为什么需要位置编码

在传统的RNN或LSTM模型中,序列信息是通过递归结构天然地保留的;在卷积神经网络(CNN)中,位置关系则是通过卷积核的局部连接特性隐含的。然而,Transformer模型完全基于注意力机制,这种机制在处理输入序列时本质上是无序的。为了让Transformer能够理解序列中的位置信息,我们必须显式地引入位置信息。

基本概念

位置编码(Positional Encoding):将位置向量添加到Embedding中。Transformer在每个输入的嵌入向量中添加了位置向量。这些位置向量遵循某些特定的模式,这有助于模型确定每 个单词的位置或不同单词之间的距离。将这些值添加到嵌入矩阵中,一旦它们被投射到Q、K、V中,就可以在计 算点积注意力时提供有意义的距离信息。

计算流程

掩码注意力

掩码注意力背景

在自然语言处理中,生成任务(如文本生成、机器翻译等)需要逐词地产生输出。在这种场景下,模型在生成每个词时,只能依赖于已经生成的之前的词,而不能看到未来的词。因此,需要一种机制来屏蔽未来的信息,这就是掩码注意力的主要功能。

基本概念

掩码注意力:训练时模拟测试阶段 ,只使用上文信息。

计算流程

掩码注意力通过在计算注意力分数时引入一个掩码矩阵(masking matrix),将当前词之后的位置屏蔽掉,从而确保模型在生成每个词时只考虑前面的词。如前面所述,为了实现掩码注意力,我们在计算注意力分数时引入一个掩码矩阵 ( M )。这个掩码矩阵用于将未来位置的注意力分数设为负无穷大,确保这些位置在 softmax 操作后被忽略。

编解码注意力

基本思想

编解码注意力机制的基本思想是,在解码器中,每个时间步的解码过程不仅依赖于已经生成的部分,还可以参照整个输入序列的编码表示。这通过计算输入序列(编码器输出)和当前解码位置之间的注意力权重来实现。

计算流程

编码器首先处理输入序列,将最后一个编码器组件的输出转换为一组注意向量K和V。每个解码器组件将在“encoder decoder attention”层中使用编码器传过来的K和V,这有助于解码器将注意力集中在输入序列中的适当位置。

输出步骤会一直重复,直到遇到句子结束符表明transformer的解码器已完成输出。 每一步的输出都会在下一个时间步喂给给底部解码器,解码器会像编码器一样运算并输出结果(每次往外蹦一个词)。跟编码器一样,在解码器中我们也为其添加位置编码,以指示每个单词的位置。

“encoder-decoder attention”层的工作原理和前边的多头自注意力差不多,但是Q、K、V的来源不用,Q是从下层创 建的(比如解码器的输入和下层decoder组件的输出),但是其K和V是来自编码器最后一个组件的输出结果。

残差连接

为什么需要残差连接

深度神经网络的“两朵乌云”:梯度消失和梯度爆炸,网络退化。

梯度消失与梯度爆炸:参数不能有效更新。梯度消失:步长几乎为0 ,止步不前 ,无法达到最优点。梯度爆炸:步长过大 ,波动 ,错过最优点。

网络退化:网络层过深时 ,其准确率反而下降 , 因为神经网络不容易学习恒等映射。

而残差连接能很好的解决这些问题。

工作流程

基于Transform框架的中日机器翻译

导入库

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

数据预处理

首先,读取我们得到的中文-日文数据,并将其转换成对应tensor数据。并且可视化数据,观察数据特点。

df = pd.read_csv('./zh-ja/zh-ja.bicleaner05.txt', sep='\\t', engine='python', header=None)
trainen = df[2].values.tolist()#[:10000] #将数据框中的第三列(索引为2)的所有值转换为列表,并赋值给变量 trainen
trainja = df[3].values.tolist()#[:10000] #将数据框中的第四列(索引为3)的所有值转换为列表,并赋值给变量 trainja
# trainen.pop(5972)
# trainja.pop(5972)

测试代码: 

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

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

en_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/spm.en.nopretok.model')#加载英文模型
ja_tokenizer = spm.SentencePieceProcessor(model_file='enja_spm_models/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)
#使用英文 tokenizer对给定的文本进行编码
#encode()方法接受两个参数:要编码的文本和输出类型(out_type)

输出:

['▁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)
#使用日文 tokenizer对给定的文本进行编码
#encode()方法接受两个参数:要编码的文本和输出类型(out_type)

输出:

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

构建词汇表以及Torch张量

使用标记器和原始句子,然后我们构建从 TorchText 导入的词汇对象。

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>'])#返回vocab对象
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 = 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))#添加起始和结束标记后添加到ja_batch中
    en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))#添加起始和结束标记后添加到en_batch中
  ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)#用pad填充
  en_batch = pad_sequence(en_batch, padding_value=PAD_IDX)#填充
  return ja_batch, en_batch#返回的是日文和英文的张量序列
train_iter = DataLoader(train_data, batch_size=BATCH_SIZE,
                        shuffle=True, collate_fn=generate_batch)#使用 DataLoader 创建训练迭代器,指定批处理大小、是否打乱数据以及数据处理函数

Sequence-to-sequence Transformer架构

编码器与解码器

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

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__()#继承父类方法
        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)

        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)

位置编码

然后我们定义位置编码函数。文本标记通过使用标记嵌入来表示。位置编码被添加到标记嵌入中以引入单词顺序的概念。

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和cos函数应用在不同维度
        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)
        #注册位置编码为buffer,确保不更新
        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),:])

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)

本文创建一个后续单词掩码来阻止目标单词关注其后续单词。我们还创建掩码,用于掩盖源和目标填充标记。以下是掩码注意力的函数

掩码注意力

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))
    #将掩码中由上三角产生的 0填充为负无穷大
    #将掩码中由上三角产生的 1填充为 0
    return 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)#为pad则为1
  return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

 创建训练模型

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#训练轮次
#初始化transform
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)
#看是否有GPU,没有就CPU
transformer = transformer.to(device)
#定义损失函数,交叉熵,并忽略填充
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=PAD_IDX)
#定义Adam优化器
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)

      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):#评价器
  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)#返回平均损失

模型训练 

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

输出:

 

 

本人训练时显卡内存等参数如下:

测试模型 

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)
trainen.pop(5)
trainja.pop(5)

输出:

' ▁H S 编 码 ▁85 15 ▁ 是 用 于 焊 接 的 电 气 ( 包 括 电 热 加 热 加 热 器 ) 。 '
'Chinese HS Code Harmonized Code System < HS编码 8515 : 电气(包括电热气体)、激光、其他光、光子束、超声波、电子束、磁脉冲或等离子弧焊接机器及装置,不论是否 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コード 8515 はんだ付け用、ろう付け用又は溶接用の機器(電気式(電気加熱ガス式を含む。)、レーザーその他の光子ビーム式、超音波式、電子ビーム式、 HS Code List (Harmonized System Code) for US, UK, EU, China, India, France, Japan, Russia, Germany, Korea, Canada ...'

保存模型

保存词汇对象和训练好的模型,在训练完成后,我们首先使用 Pickle 来保存词汇对象(en_vocab 和 ja_vocab)。导入 pickle 模块(用于序列化和反序列化 Python 对象)。

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)#使用pickle模块将en_vocab对象序列化并写入文件
file.close()#关闭文件
file = open('ja_vocab.pkl', 'wb')#打开一个文件存储日文词汇表
pickle.dump(ja_vocab, file)#写入
file.close()#关闭

  最后,使用 PyTorch 的保存和加载函数来保存模型以供以后使用。通常,根据我们之后想要用它们来做什么,有两种保存模型的方式。第一种是仅用于推理,我们之后可以加载模型并使用它将日语翻译为英语。

# save model for inference
torch.save(transformer.state_dict(), 'inference_model')#保存模型

 同时也用于当我们之后想要加载模型,并且想要继续训练的时候。

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

总结

本次实验的目标是实现一个基于 Transformer 架构的中日机器翻译模型。实验过程包括对中日文本数据的预处理、模型的实现与训练以及对模型性能的评估。

在数据预处理阶段,我们首先对中日文本进行了分词,并构建了相应的词汇表,将文本转换为张量格式。

在模型训练阶段,我们使用 PyTorch 实现了 Transformer 模型,并进行了多次实验以调整超参数,从而优化模型性能。实验结果表明,通过增加编码器和解码器的层数、增加多头注意力机制的头数以及增加词嵌入维度等方式,可以显著提升模型的表现。

我们的实验取得了成功,基于 Transformer 架构的中日机器翻译模型达到了较好的性能。在模型评估过程中,我们发现模型在翻译长句子和复杂句子时表现较好,但在翻译简单句子时可能会出现一些错误。这可能是由于训练数据集中简单句子的数量较少,导致模型在这方面的学习不够充分。

Transformer 作为一种强大的模型架构,在自然语言处理(NLP)领域展现出了巨大的潜力。基于 Transformer 的衍生模型如 BERT 和 GPT 已经在多个 NLP 任务中取得了卓越的成绩。然而,NLP 领域仍然有许多待优化和探索的地方。尽管目前 NLP 在工业应用上的普及程度不如计算机视觉(CV),但自然语言是人类文明延续的重要载体。没有文字,我们无法追溯古人的发展历史;没有语言,人类社会也难以和谐运转。我们所看到的图片、听到的话语,最终都需要在大脑中转化为可以理解的文字和语言信息。因此,NLP 的前景依旧非常广阔。

这是一个充满机遇的时代,由于 NLP 仍处于不断探索的发展阶段,现在开始学习和研究仍然不晚。希望这些经验和见解能对同学们有所帮助,共同推动 NLP 领域的进步。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐