基于transformer实现机器翻译(日译中)

一、实验介绍

1.实验目的

本次实验的目的是利用Transformer模型来实现机器翻译任务,探索Transformer在序列到序列(Seq2Seq)任务中的性能,并深入理解其工作原理和优化方法。

2.实验方法

在本次实验中,我们采用了基于自注意力机制(Self-Attention)的Transformer模型来实现机器翻译。模型主要包括编码器(Encoder)和解码器(Decoder)两部分,每部分都包含多个相同的层,每层包含自注意力子层和前馈神经网络子层。数据集选用了标准的机器翻译数据集,如WMT(Workshop on Machine Translation)数据集。

在模型训练过程中,我们使用了掩码(Masking)来确保解码器在生成每个位置的输出时只能依赖于该位置之前的输入。同时,我们采用了Adam优化器来更新网络参数,并使用了如BLEU等自动评价指标来衡量模型的翻译质量。

二、用Transformer和PyTorch实现日汉机器翻译模型

1.什么是transformer

在计算机科学和深度学习领域,Transformer特指一种基于注意力机制的神经网络模型。这种模型最初由Google在2017年提出,用于自然语言处理任务中的序列到序列学习,如机器翻译等。

在深度学习领域,Transformer模型的核心组件包括编码器和解码器。编码器负责处理输入序列,将其转换为一种内部表示;而解码器则基于这种内部表示生成输出序列。Transformer模型的关键特点在于其自注意力机制,这种机制允许模型在处理序列中的每个元素时,都能考虑到序列中的其他所有元素,从而捕获元素之间的依赖关系。

Transformer模型已被广泛应用于各种深度学习任务中,包括但不限于自然语言处理、图像识别、语音识别等。例如,GPT和BERT等著名的深度学习模型都是基于Transformer架构的变体。

2.transformer的结构组成

  1. Embedding部分

    • Embedding部分是整个网络最开始的部分,主要作用是将输入的文本转化成向量进行后续的运算。通常做法是给每一个词创建一个初始的向量表示,然后在后续训练中进行不断的调整优化。
    • Transformer没有采用类似RNN中循环计算的方式来引入距离和位置,而是采用了引入位置向量来表示距离和位置。位置向量有两种实现方式:一种是基于三角函数的位置向量,这种向量在训练中保持不变为固定值;另一种是使用可训练的位置向量在训练中会不断调整。
  2. Self-Attention部分

    • Transformer模型架构使用Self-Attention结构取代了在NLP任务中常用的RNN网络结构。Self-Attention层允许模型在处理序列中的每个元素时,都能考虑到序列中的其他所有元素,从而捕获元素之间的依赖关系。
  3. FeedForward部分(前馈网络,缩写为FFN):

    • 每个编码器和解码器都包含前馈网络层。前馈网络层在Self-Attention层的输出上进行操作,通常是一个全连接网络。
  4. 残差连接和Layer Normalization

    • 在Encoder和Decoder的每一层中,通常都包含残差连接和Layer Normalization,这有助于模型训练时的稳定性和收敛速度。
  5. Encoder和Decoder

    • Transformer本质上是一个Encoder-Decoder架构。Encoder负责处理输入序列,将其转换为一种内部表示;而Decoder则基于这种内部表示生成输出序列。
    • Encoder由多个相同的编码器层堆叠而成,每个编码器层都包含Self-Attention层和前馈网络层。Decoder的结构与Encoder类似,但在两层之间还有一个Encoder-Decoder Attention层,帮助Decoder关注输入序列的相关部分。
  6. Multi-Head Attention

    • 在Self-Attention和Encoder-Decoder Attention中,通常使用Multi-Head Attention来并行地关注输入序列的不同部分,从而提高模型的表示能力。

三、代码实现

1.添加所需要的库

In [5]:

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

In [6]:

device

Out[6]:

device(type='cpu')

2.并行数据采集

In this tutorial, we will use the Japanese-English parallel dataset downloaded from JParaCrawl![JParaCrawl] which is described as the “largest publicly available English-Japanese parallel corpus created by NTT. It was created by largely crawling the web and automatically aligning parallel sentences.” You can also see the paper here.

In [7]:

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

Out[7]:

'2014年と2017年のサンデータイムズ紙によってイギリス国内で生活に最も適した街と名付けられ、またヨーロッパグリーンキャピタルの賞も受賞しています。'

After importing all the Japanese and their English counterparts, I deleted the last data in the dataset because it has a missing value. In total, the number of sentences in both trainen and trainja is 5,973,071, however, for learning purposes, it is often recommended to sample the data and make sure everything is working as intended, before using all the data at once, to save time.

Here is an example of sentence contained in the dataset.

In [8]:

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

We can also use different parallel datasets to follow along with this article, just make sure that we can process the data into the two lists of strings as shown above, containing the Japanese and English sentences.

3.准备分词器

Unlike English or other alphabetical languages, a Japanese sentence does not contain whitespaces to separate the words. We can use the tokenizers provided by JParaCrawl which was created using SentencePiece for both Japanese and English, you can visit the JParaCrawl website to download them, or click here.

In [9]:

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

After the tokenizers are loaded, you can test them, for example, by executing the below code.

In [10]:

en_tokenizer.encode("All residents aged 20 to 59 years who live in Japan must enroll in public pension system.", out_type='str')

Out[10]:

['▁All',
 '▁residents',
 '▁aged',
 '▁20',
 '▁to',
 '▁59',
 '▁years',
 '▁who',
 '▁live',
 '▁in',
 '▁Japan',
 '▁must',
 '▁enroll',
 '▁in',
 '▁public',
 '▁pension',
 '▁system',
 '.']

In [11]:

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

Out[11]:

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

4.使用TorchText库构建词汇表(Vocab对象)并将句子转换为PyTorch张量(tensors)

使用分词器和原始句子,我们随后会构建从TorchText导入的Vocab对象。这个过程所需的时间取决于数据集的大小和计算能力,可能需要几秒钟到几分钟不等。不同的分词器也会影响构建词汇表所需的时间。对于日语,我尝试了几种不同的分词器,但SentencePiece似乎对我而言工作得很好且足够快。

In [39]:

# 定义一个构建词汇表的函数
def build_vocab(sentences, tokenizer):
    """
    根据给定的句子列表和分词器构建词汇表。

    参数:
    sentences -- 列表,包含多个句子字符串。
    tokenizer -- 一个分词器,能够将句子编码为单词或子词的标识符。

    返回:
    Vocab -- 一个词汇表对象,它包含句子中所有单词的频率信息,以及特殊标记。

    这个函数首先创建一个Counter对象来计算每个单词或子词的出现次数。
    然后,它遍历提供的句子列表,使用分词器对每个句子进行编码,并更新Counter。
    最后,它使用Counter创建一个Vocab对象,这个对象还会添加几个特殊标记,
    例如 '<unk>' 表示未知单词,'<pad>' 表示填充,'<bos>' 表示句子的开始,'<eos>' 表示句子的结束。
    """
    counter = Counter()
    for sentence in sentences:
        # 使用分词器编码句子,并更新计数器
        counter.update(tokenizer.encode(sentence, out_type=str))
    # 创建词汇表对象,并添加特殊标记
    return Vocab(counter, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

# 使用日语训练数据和分词器构建日语词汇表
ja_vocab = build_vocab(trainja, ja_tokenizer)

# 使用英语训练数据和分词器构建英语词汇表
en_vocab = build_vocab(trainen, en_tokenizer)

当我们有了词汇表(Vocab)对象后,我们可以使用词汇表和分词器(tokenizer)对象来为我们的训练数据构建张量(tensors)。

In [40]:

# 定义一个数据处理函数,将文本数据转换为张量形式
def data_process(ja, en):
    """
    将给定的日语和英语句子对转换为张量形式的数据。

    参数:
    ja -- 列表,包含多个日语句子字符串。
    en -- 列表,包含多个英语句子字符串。

    返回:
    data -- 列表,包含多个日语和英语句子对的张量表示。

    这个函数首先创建一个空列表data来存储处理后的数据。
    然后,它遍历输入的日语和英语句子对,使用对应的词汇表和分词器将每个句子转换为张量。
    具体来说,它使用分词器对每个句子进行编码,然后使用词汇表将编码后的单词或子词转换为整数索引。
    最后,它将这些索引转换为PyTorch张量,并将每个语言对的张量元组添加到data列表中。
    """
    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)

5.使用DataLoader来遍历训练集

在这里,我将BATCH_SIZE设置为16来防止“cuda out of memory”(即CUDA内存不足)错误,但这取决于各种因素,如你的机器内存容量、数据集的大小等,因此你可以根据自己的需要自由调整批处理大小(注意:PyTorch的教程在使用Multi30k德语-英语数据集时,将批处理大小设置为128)。

In [41]:

# 定义批处理大小
BATCH_SIZE = 8
# 填充索引,通常用于在序列填充时使用
PAD_IDX = ja_vocab['<pad>']
# 句子开始的索引,通常用于在序列开始处添加特殊标记
BOS_IDX = ja_vocab['<bos>']
# 句子结束的索引,通常用于在序列结束处添加特殊标记
EOS_IDX = ja_vocab['<eos>']

def generate_batch(data_batch):
    """
    根据给定的数据批次,生成带有BOS和EOS标记的日语文本和英文文本批次,
    并对它们进行填充以确保每个序列具有相同的长度。

    Args:
        data_batch (List[Tuple[Tensor, Tensor]]): 包含日语文本和英文文本的数据批次。

    Returns:
        Tuple[Tensor, Tensor]: 包含填充后的日语文本和英文文本的批次。
    """
    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))
        # 在英文文本前添加BOS标记,后添加EOS标记
        en_batch.append(torch.cat([torch.tensor([BOS_IDX]), en_item, torch.tensor([EOS_IDX])], dim=0))

    # 使用pad_sequence函数对日语文本进行填充,使所有序列具有相同长度
    ja_batch = pad_sequence(ja_batch, padding_value=PAD_IDX)
    # 使用pad_sequence函数对英文文本进行填充,使所有序列具有相同长度
    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)

# train_iter现在是一个迭代器,它将在每次迭代时返回一个批次的填充后的日语文本和英文文本

6.序列到序列(Sequence-to-Sequence)Transformer模型 

Transformer是一个Seq2Seq(序列到序列)模型,该模型在“Attention is all you need”论文中被提出,用于解决机器翻译任务。Transformer模型由编码器和解码器两部分组成,每一部分都包含固定数量的层。

编码器通过一系列的多头注意力(Multi-head Attention)和前馈网络(Feed forward network)层来处理输入序列。编码器的输出,被称为“记忆”(memory),与目标张量一起被送入解码器。编码器和解码器使用教师强制(teacher forcing)技术以端到端的方式进行训练。

In [42]:

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

# 定义一个序列到序列的Transformer模型
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):
        """
        初始化Seq2SeqTransformer模型。

        参数:
        num_encoder_layers -- int, 编码器层的数量。
        num_decoder_layers -- int, 解码器层的数量。
        emb_size -- int, 词嵌入的维度。
        src_vocab_size -- int, 源语言的词汇表大小。
        tgt_vocab_size -- int, 目标语言的词汇表大小。
        dim_feedforward -- int, feedforward层的维度,默认为512。
        dropout -- float, dropout概率,默认为0.1。

        这个构造函数创建了一个序列到序列的Transformer模型。
        它包括一个编码器和一个解码器,每个都由指定数量的层组成。
        模型还包含词嵌入层、位置编码和输出线性层。
        """
        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 -- Tensor, 源语言输入序列。
        trg -- Tensor, 目标语言输入序列。
        src_mask -- Tensor, 源语言注意力掩码。
        tgt_mask -- Tensor, 目标语言注意力掩码。
        src_padding_mask -- Tensor, 源语言填充掩码。
        tgt_padding_mask -- Tensor, 目标语言填充掩码。
        memory_key_padding_mask -- Tensor, 编码器输出填充掩码。

        返回:
        generator(outs) -- 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):
        """
        实现编码器的前向传播。

        参数:
        src -- Tensor, 源语言输入序列。
        src_mask -- Tensor, 源语言注意力掩码。

        返回:
        transformer_encoder -- 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):
        """
        实现解码器的前向传播。

        参数:
        tgt -- Tensor, 目标语言输入序列。
        memory -- Tensor, 编码器的输出序列。
        tgt_mask -- Tensor, 目标语言注意力掩码。

        返回:
        transformer_decoder -- Tensor, 解码器的输出序列。
        
        这个函数对目标语言序列进行词嵌入和位置编码,然后使用解码器处理。
        """
        return self.transformer_decoder(self.positional_encoding(
                          self.tgt_tok_emb(tgt)), memory,
                          tgt_mask)

在自然语言处理(NLP)任务中,特别是当使用Transformer模型(如BERT、GPT等)时,文本标记(tokens)通常是通过标记嵌入(token embeddings)来表示的

In [43]:

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),:])

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)

在机器翻译任务中,尤其是在使用Transformer模型时,我们经常需要创建掩码(masks)来指导模型的注意力机制。掩码是一种用于控制模型在哪些位置进行注意力计算的技术。

In [44]:

# 生成一个方形后续掩码矩阵
def generate_square_subsequent_mask(sz):
    """
    生成一个方形后续掩码矩阵,用于Transformer模型中的自注意力机制。

    参数:
    sz -- int, 掩码矩阵的大小。

    返回:
    mask -- Tensor, 大小为(sz, sz)的方形后续掩码矩阵。
    
    这个函数首先创建一个上三角矩阵,所有的元素都是1。
    然后,它将上三角矩阵转置,并将上三角部分的元素设置为0,其余部分设置为-无穷大。
    这样,掩码矩阵确保了在自注意力机制中,一个位置的输出只依赖于该位置之前的输入。
    """
    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

# 创建源语言和目标语言的掩码
def create_mask(src, tgt):
    """
    创建源语言和目标语言的掩码,包括注意力掩码和填充掩码。

    参数:
    src -- Tensor, 源语言序列,形状为(src_seq_len, batch_size)。
    tgt -- Tensor, 目标语言序列,形状为(tgt_seq_len, batch_size)。

    返回:
    src_mask -- Tensor, 源语言注意力掩码,形状为(src_seq_len, src_seq_len)。
    tgt_mask -- Tensor, 目标语言注意力掩码,形状为(tgt_seq_len, tgt_seq_len)。
    src_padding_mask -- Tensor, 源语言填充掩码,形状为(batch_size, src_seq_len)。
    tgt_padding_mask -- Tensor, 目标语言填充掩码,形状为(batch_size, tgt_seq_len)。
    
    这个函数首先获取源语言和目标语言的序列长度。
    然后,它使用generate_square_subsequent_mask函数为目标语言生成方形后续掩码矩阵。
    对于源语言,由于Transformer编码器的自注意力机制允许位置之间的相互关注,因此创建一个全为False的掩码矩阵。
    最后,它还创建了源语言和目标语言的填充掩码,用于在注意力机制中忽略填充标记。
    """
    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

Define model parameters and instantiate model. 这里我们服务器实在是计算能力有限,按照以下配置可以训练但是效果应该是不行的。如果想要看到训练的效果请使用你自己的带GPU的电脑运行这一套代码。

当你使用自己的GPU的时候,NUM_ENCODER_LAYERS 和 NUM_DECODER_LAYERS 设置为3或者更高,NHEAD设置8,EMB_SIZE设置为512。

In [45]:

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 = 4
NUM_DECODER_LAYERS = 4
# 增加迭代轮数
NUM_EPOCHS = 20
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
)
# 定义一个训练函数,用于训练模型一个epoch
def train_epoch(model, train_iter, optimizer):
    """
    在一个epoch内训练模型。

    参数:
    model -- Seq2SeqTransformer, 要训练的模型。
    train_iter -- DataLoader, 训练数据迭代器。
    optimizer -- Optimizer, 用于优化模型参数的优化器。

    返回:
    losses / len(train_iter) -- float, 平均损失。

    这个函数首先将模型设置为训练模式。
    然后,它初始化损失累加器,并遍历训练数据迭代器中的每个批次。
    在每个批次中,它将数据移动到指定设备,创建注意力掩码和填充掩码,然后通过模型前向传播。
    接着,它计算损失,执行反向传播,并更新模型参数。
    最后,它返回整个epoch的平均损失。
    """
    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 -- Seq2SeqTransformer, 要评估的模型。
    val_iter -- DataLoader, 验证数据迭代器。

    返回:
    losses / len(val_iter) -- float, 平均损失。

    这个函数首先将模型设置为评估模式。
    然后,它初始化损失累加器,并遍历验证数据迭代器中的每个批次。
    在每个批次中,它将数据移动到指定设备,创建注意力掩码和填充掩码,然后通过模型前向传播。
    接着,它计算损失。
    最后,它返回整个验证集的平均损失。
    """
    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)

7.开始训练

最后,在准备好必要的类和函数之后,我们就可以开始训练我们的模型了。这不用说,但完成训练所需的时间可能会根据很多因素有很大的不同,比如计算能力、参数设置和数据集的大小等。

当我使用JParaCrawl的完整句子列表进行模型训练时,该列表包含每种语言大约590万个句子,使用单个NVIDIA GeForce RTX 3070 GPU进行训练,每个epoch需要大约5个小时。

Here is the code:

In [46]:

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"))
  0%|          | 0/20 [00:43<?, ?it/s]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-46-4bd9116cfebb> in <module>
      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}, "

<ipython-input-45-d239aec8eeba> in train_epoch(model, train_iter, optimizer)
     61         tgt_out = tgt[1:,:]
     62         loss = loss_fn(logits.reshape(-1, logits.shape[-1]), tgt_out.reshape(-1))
---> 63         loss.backward()
     64 
     65         optimizer.step()

/opt/conda/lib/python3.6/site-packages/torch/tensor.py in backward(self, gradient, retain_graph, create_graph)
    196                 products. Defaults to ``False``.
    197         """
--> 198         torch.autograd.backward(self, gradient, retain_graph, create_graph)
    199 
    200     def register_hook(self, hook):

/opt/conda/lib/python3.6/site-packages/torch/autograd/__init__.py in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables)
     98     Variable._execution_engine.run_backward(
     99         tensors, grad_tensors, retain_graph, create_graph,
--> 100         allow_unreachable=True)  # allow_unreachable flag
    101 
    102 

KeyboardInterrupt: 

8.使用训练好的模型来翻译一个日语句子

首先,我们创建用于翻译新句子的函数,包括以下步骤:获取日语句子、分词、转换为张量、进行推理,然后将结果解码回句子,但这次是以英文的形式。

In [47]:

# 定义一个贪婪解码函数,用于生成翻译结果
def greedy_decode(model, src, src_mask, max_len, start_symbol):
    """
    使用贪婪解码策略生成翻译结果。

    参数:
    model -- Seq2SeqTransformer, 训练好的模型。
    src -- Tensor, 源语言序列。
    src_mask -- Tensor, 源语言注意力掩码。
    max_len -- int, 生成序列的最大长度。
    start_symbol -- int, 开始标记的索引。

    返回:
    ys -- 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 -- Seq2SeqTransformer, 训练好的模型。
    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>", "")

Then, we can just call the translate function and pass the required parameters.

In [66]:

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

Out[66]:

' ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁ ▁'

In [49]:

trainen.pop(5)

Out[49]:

'美国 设施: 停车场, 24小时前台, 健身中心, 报纸, 露台, 禁烟客房, 干洗, 无障碍设施, 免费停车, 上网服务, 电梯, 快速办理入住/退房手续, 保险箱, 暖气, 传真/复印, 行李寄存, 无线网络, 免费无线网络连接, 酒店各处禁烟, 空调, 阳光露台, 自动售货机(饮品), 自动售货机(零食), 每日清洁服务, 内部停车场, 私人停车场, WiFi(覆盖酒店各处), 停车库, 无障碍停车场, 简短描述Gateway Hotel Santa Monica酒店距离海滩2英里(3.2公里),提供24小时健身房。每间客房均提供免费WiFi,客人可以使用酒店的免费地下停车场。'

In [50]:

trainja.pop(5)

Out[50]:

'アメリカ合衆国 施設・設備: 駐車場, 24時間対応フロント, フィットネスセンター, 新聞, テラス, 禁煙ルーム, ドライクリーニング, バリアフリー, 無料駐車場, インターネット, エレベーター, エクスプレス・チェックイン / チェックアウト, セーフティボックス, 暖房, FAX / コピー, 荷物預かり, Wi-Fi, 無料Wi-Fi, 全館禁煙, エアコン, サンテラス, 自販機(ドリンク類), 自販機(スナック類), 客室清掃サービス(毎日), 敷地内駐車場, 専用駐車場, Wi-Fi(館内全域), 立体駐車場, 障害者用駐車場, 短い説明Gateway Hotel Santa Monicaはビーチから3.2kmの場所に位置し、24時間利用可能なジム、無料Wi-Fi付きのお部屋、無料の地下駐車場を提供しています。'

9.保存Vocab对象和训练好的模型

最终,在训练完成后,我们首先需要使用Pickle来保存词汇表对象(en_vocab和ja_vocab)

In [59]:

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

最后,我们还可以使用PyTorch的保存和加载函数来保存模型以便后续使用。通常,根据我们后续想要如何使用模型,有两种方式来保存模型。第一种是只用于推理,我们可以在后续加载模型并使用它来进行从日语到英语的翻译。

In [64]:

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

第二种方式也是用于推理的,但除此之外,当我们想要在之后加载模型并继续训练时,也会使用这种方式。

In [67]:

# 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')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-67-2e0ecdf61e01> in <module>
      4   'model_state_dict': transformer.state_dict(),
      5   'optimizer_state_dict': optimizer.state_dict(),
----> 6   'loss': train_loss,
      7   }, 'model_checkpoint.tar')

NameError: name 'train_loss' is not defined

四、实验小结

1.总结

  1. Transformer模型通过自注意力机制能够捕获序列中的长距离依赖关系,这对于机器翻译等Seq2Seq任务至关重要。相比于传统的RNN模型,Transformer能够并行处理整个序列,从而提高了训练效率。

  2. 在解码过程中,Transformer采用了掩码技术来确保生成每个位置的输出时只能依赖于该位置之前的输入。这种机制使得模型在生成翻译结果时能够保持正确的时序关系。

  3. 通过调整模型的参数和采用适当的正则化技术,我们可以有效地控制模型的复杂度,从而防止过拟合现象的发生。同时,适当的优化算法(如Adam)也能够帮助模型更快地收敛到最优解。

2.实验意义与展望

本次实验通过基于Transformer的机器翻译模型,验证了其在自然语言处理任务中的优越性能。实验结果证明了Transformer在处理长距离依赖关系和并行计算方面的优势,为未来的自然语言处理研究提供了新的思路和方法。

未来,我们可以进一步探索Transformer在其他NLP任务中的应用,如文本生成、情感分析等。同时,我们也可以尝试将Transformer与其他技术相结合,如知识图谱、多模态学习等,以进一步提升模型的性能和应用范围。此外,随着计算能力的提升和数据的不断增长,我们可以进一步增加模型的深度和宽度,以挖掘更多的信息并提高模型的泛化能力。

  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值