Seq2Seq实战——机器翻译

2023-10-29

基于seq2seq做一个机器翻译

我们将使用PyTorch和TorchText构建一个机器学习模型,从一个序列到另一个序列。 将德语到英语翻译成英语

该模型是《Sequence to Sequence Learning with Neural Networks》这篇论文的Pytorch实现

 

使用Encoder生成上下文向量

使用Decoder预测目标语言句子

 

步骤:

    1)准备数据

    2)创建Seq2Seq模型

    3)训练模型

    4)验证模型

 

 

准备数据

import torch
import torch.nn as nn
import torch.optim as optim

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator

import spacy
import random
import math
import time

#1、preparing data
#设置一个随机种子
SEED = 1234
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True  #使用cuda保证每次结果一样。将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的


#创建tokenizer
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')



#把tokenizer从一串字符转成一个list,同时做一个reverse取反
#在原论文中,作者发现颠倒源语言的输入的顺序可以取得不错的翻译效果,例如,一句话为“good morning!”,颠倒顺序分词后变为"!", “morning”, 和"good"。

#将德语进行分词并颠倒顺序
def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
#将英语进行分词,不颠倒顺序
def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]


#我们创建SRC和TRG两个Field对象,tokenize为我们刚才定义的分词器函数,在每句话的开头加入字符SOS,结尾加入字符EOS,将所有单词转换为小写。

#TorchText的Field定义数据应该如何被处理
#SRC即source,是德语
#TRG即target,是英语
#sos是start of sequence, eos是end of sequence
#lower=True是将所有单词转换为小写
SRC = Field(tokenize = tokenize_de,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)

TRG = Field(tokenize = tokenize_en,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)


#使用torchtext自带的Multi30k数据集,这是一个包含约30000个平行的英语、德语和法语句子的数据集,每个句子包含约12个单词。
train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'),
                                                    fields = (SRC, TRG))

#查看一下加载完的数据集
print(f"Number of training examples: {len(train_data.examples)}")
print(f"Number of validation examples: {len(valid_data.examples)}")
print(f"Number of testing examples: {len(test_data.examples)}")


#看一下生成的第一个训练样本,可以看到源语言的顺序已经颠倒了
print(vars(train_data.examples[0]))


#构建词表
#所谓构建词表,即需要给每个单词编码,也就是用数字表示每个单词,这样才能传入模型。
#可以使用dataset类中的build_vocab()方法传入用于构建词表的数据集。
#注意,源语言和目标语言的词表是不同的,而且词表应该只从训练集构建,而不是验证/测试集,这可以防止“信息泄漏”到模型中。
SRC.build_vocab(train_data, min_freq = 2) #设置最小词频为2,当一个单词在数据集中出现次数小于2时会被转换为<unk>字符。
TRG.build_vocab(train_data, min_freq = 2)


#查看一下生成的词表大小
print(f"Unique tokens in source (de) vocabulary: {len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary: {len(TRG.vocab)}")


#指定GPU还是CPU进行训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')




BATCH_SIZE = 128

#创建迭代器
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data),
    batch_size = BATCH_SIZE,
    device = device)

#查看一下生成的batch
batch = next(iter(train_iterator))
print(batch)
#输出:
# [torchtext.data.batch.Batch of size 128 from MULTI30K]
# 	[.src]:[torch.cuda.LongTensor of size 23x128 (GPU 0)]
# 	[.trg]:[torch.cuda.LongTensor of size 21x128 (GPU 0)]

 

建立Seq2Seq模型

我们将分别创建编码器(Encoder)、解码器(Eecoder)和seq2seq模型。

encoder

原论文使用了一个4层的单向LSTM,出于训练时间的考虑,我们将其缩减到了2层。结构如图所示

参数说明:

  • input_dim:输入编码器的one-hot向量的维度,等于源语言词汇表的大小。
  • emb_dim:embedding层的维度
  • hid_dim:隐藏层h和c的维度
  • n_layers:LSTM网络的层数
  • dropout:如果非零的话,将会在LSTM的输出上加个dropout,最后一层除外。

 

在forward函数中,我们传入源语言src,经过embedding层将其转换为密集向量,然后应用dropout,然后将这些词嵌入传递到LSTM。读者可能注意到,我们没有将初始隐藏状态h_0和单元格状态c_0传递给LSTM。这是因为如果没有向LSTM传递隐藏/单元格状态,它将自动创建一个全0的张量作为初始状态。

 

decoder

decoder网络同样是一个2层的LSTM(原论文中为4层),结构如图所示:

在这里插入图片描述

Decoder网络的参数和初始化类似于Encoder,不同的地方在于:

  • output_dim:输入解码器的one-hot向量维度,等于目标语言词汇表的大小。

  • 添加了Linear层,用于预测最终输出。

在forward函数中,我们接受目标语言trg作为输入数据,由于目标语言每次是输入一个词(源语言每次输入一句话),因此用unsqueeze()方法给为其添加一个句子长度为1的维度(即将一维变为二维,以便能够作为embedding层的输入)。

然后,与编码器类似,我们通过一个embedding层并应用dropout。然后,将这些
嵌入与Encoder层生成的隐藏状态h_n和单元格状态c_n一起传递到LSTM。注意:在Encoder中,我们使用了一个全0的张量作为初始隐藏状态h_0和单元格状态c_0,在Decoder中,我们使用的是Encoder生成的h_n和c_n作为初始的隐藏状态和单元格状态,这就相当于我们在翻译时使用了源语言的上下文信息。

 

Seq2Seq网络

Seq2Seq网络将Encoder和Decoder网络组合在,实现以下功能:

  • 使用源语言句子作为输入
  • 使用Encoder生成上下文向量
  • 使用Decoder预测目标语言句子

参数说明:

  • device :把张量放到GPU上。新版的Pytorch使用to方法可以容易地将对象移动到不同的设备(代替以前的cpu()或cuda()方法)。
  • outputs:存储Decoder所有输出的张量
  • teacher_forcing_ratio:该参数的作用是,当使用teacher force时,decoder网络的下一个input是目标语言的下一个字符,当不使用时,网络的下一个input是其预测出的那个字符。

在该网络中,编码器和解码器的层数(n_layers)和隐藏层维度(hid_dim)是相等。但是,在其他的Seq2seq模型中不一定总是需要相同的层数或相同的隐藏维度大小。例如,编码器有2层,解码器只有1层,这就需要进行相应的处理,如对编码器输出的两个上下文向量求平均值,或者只使用最后一层的上下文向量作为解码器的输入等。

import torch
import torch.nn as nn
import torch.optim as optim

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator

import spacy
import random
import math
import time

#1、preparing data
#设置一个随机种子
SEED = 1234
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True  #使用cuda保证每次结果一样。将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的


#创建tokenizer
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')



#把tokenizer从一串字符转成一个list,同时做一个reverse取反
#在原论文中,作者发现颠倒源语言的输入的顺序可以取得不错的翻译效果,例如,一句话为“good morning!”,颠倒顺序分词后变为"!", “morning”, 和"good"。

#将德语进行分词并颠倒顺序
def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
#将英语进行分词,不颠倒顺序
def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]


#我们创建SRC和TRG两个Field对象,tokenize为我们刚才定义的分词器函数,在每句话的开头加入字符SOS,结尾加入字符EOS,将所有单词转换为小写。

#TorchText的Field定义数据应该如何被处理
#SRC即source,是德语
#TRG即target,是英语
#sos是start of sequence, eos是end of sequence
#lower=True是将所有单词转换为小写
SRC = Field(tokenize = tokenize_de,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)

TRG = Field(tokenize = tokenize_en,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)


#使用torchtext自带的Multi30k数据集,这是一个包含约30000个平行的英语、德语和法语句子的数据集,每个句子包含约12个单词。
train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'),
                                                    fields = (SRC, TRG))

#查看一下加载完的数据集
print(f"Number of training examples: {len(train_data.examples)}")
print(f"Number of validation examples: {len(valid_data.examples)}")
print(f"Number of testing examples: {len(test_data.examples)}")


#看一下生成的第一个训练样本,可以看到源语言的顺序已经颠倒了
print(vars(train_data.examples[0]))


#构建词表
#所谓构建词表,即需要给每个单词编码,也就是用数字表示每个单词,这样才能传入模型。
#可以使用dataset类中的build_vocab()方法传入用于构建词表的数据集。
#注意,源语言和目标语言的词表是不同的,而且词表应该只从训练集构建,而不是验证/测试集,这可以防止“信息泄漏”到模型中。
SRC.build_vocab(train_data, min_freq = 2) #设置最小词频为2,当一个单词在数据集中出现次数小于2时会被转换为<unk>字符。
TRG.build_vocab(train_data, min_freq = 2)


#查看一下生成的词表大小
print(f"Unique tokens in source (de) vocabulary: {len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary: {len(TRG.vocab)}")


#指定GPU还是CPU进行训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')




BATCH_SIZE = 128

#创建迭代器
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data),
    batch_size = BATCH_SIZE,
    device = device)

#查看一下生成的batch
batch = next(iter(train_iterator))
print(batch)
#输出:
# [torchtext.data.batch.Batch of size 128 from MULTI30K]
# 	[.src]:[torch.cuda.LongTensor of size 23x128 (GPU 0)]
# 	[.trg]:[torch.cuda.LongTensor of size 21x128 (GPU 0)]



#2、创建Seq2Seq模型

#我们将分别创建编码器(Encoder)、解码器(Eecoder)和seq2seq模型。
#原论文使用了一个4层的单向LSTM,出于训练时间的考虑,我们将其缩减到了2层。结构如图所示

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.input_dim = input_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(input_dim, emb_dim)

        #encoder部分
        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.dropout = nn.Dropout(dropout)

    def forward(self, src):
        #src:(sent_len, batch_size)

        embedded = self.dropout(self.embedding(src))

        #embedded:(sent_len, batch_size, emb_dim)
        outputs, (hidden, cell) = self.rnn(embedded)
        #outputs:(sent_len, batch_size, hid_dim)
        #hidden:(n_layers, batch_size, hid_dim)
        #cell:(n_layers, batch_size, hid_dim)
        return hidden, cell


class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.output_dim = output_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(output_dim, emb_dim)

        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.out = nn.Linear(hid_dim, output_dim)

        self.dropout = nn.Dropout(dropout)

    def forward(self, input, hidden, cell):
        # input: (batch_size) -> input: (1, batch_size)
        input = input.unsqueeze(0)
        # embedded: (1, batch_size, emb_dim)
        embedded = self.dropout(self.embedding(input))
        # hidden: (n_layers, batch size, hid_dim)
        # cell: (n_layers, batch size, hid_dim)
        # output(1, batch_size, hid_dim)
        output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
        # prediction: (batch_size, output_dim)
        prediction = self.out(output.squeeze(0))

        return prediction, hidden, cell


class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()

        self.encoder = encoder
        self.decoder = decoder
        self.device = device

        assert encoder.hid_dim == decoder.hid_dim, \
            "Hidden dimensions of encoder and decoder must be equal!"
        assert encoder.n_layers == decoder.n_layers, \
            "Encoder and decoder must have equal number of layers!"

    def forward(self, src, trg, teacher_forcing_ratio=0.5):
        # src = [src sent len, batch size]
        # trg = [trg sent len, batch size]
        # teacher_forcing_ratio is probability to use teacher forcing
        # e.g. if teacher_forcing_ratio is 0.75 we use ground-truth inputs 75% of the time

        batch_size = trg.shape[1]
        max_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim

        # tensor to store decoder outputs
        #创建outputs张量存储Decoder的输出
        outputs = torch.zeros(max_len, batch_size, trg_vocab_size).to(self.device)

        # last hidden state of the encoder is used as the initial hidden state of the decoder
        hidden, cell = self.encoder(src)

        #输入到Decoder网络的第一个字符是<sos>(句子开始标记)
        input = trg[0, :]

        for t in range(1, max_len):
            # insert input token embedding, previous hidden and previous cell states
            # receive output tensor (predictions) and new hidden and cell states
            output, hidden, cell = self.decoder(input, hidden, cell)

            # place predictions in a tensor holding predictions for each token
            outputs[t] = output

            # decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio

            # get the highest predicted token from our predictions
            top1 = output.argmax(1)

            # if teacher forcing, use actual next token as next input
            # if not, use predicted token
            input = trg[t] if teacher_force else top1

        return outputs

 

训练模型

初始化模型的参数。在原论文中,作者将所有参数初始化为-0.08和+0.08之间的均匀分布。我们通过创建一个函数来初始化模型中的参数权重。当使用apply方法时,模型中的每个模块和子模块都会调用init_weights函数。

 

在定义训练函数中

  • model.train() : 让model变为训练模式,启用 batch normalization(本模型未使用)和 Dropout。
  • clip_grad_norm: 进行梯度裁剪,防止梯度爆炸。clip:梯度阈值
  • view函数: 减少output和trg的维度以便进行loss计算。由于trg每句话的开头都是标记符sos,为了提高准确度,output和trg的第一列将不参与计算损失。

 

在定义测试函数中

  • model.eval(): 开启测试模式,关闭batch normalization(本模型未使用)和 dropout。
  • torch.no_grad():关闭autograd 引擎(不会进行反向传播计算),这样的好处是减少内存的使用并且加速计算。
  • teacher_forcing_ratio = 0:在测试阶段须要关闭teacher forcing,保证模型使用预测的结果作为下一步的输入。
import torch
import torch.nn as nn
import torch.optim as optim

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator

import spacy
import random
import math
import time

#1、preparing data
#设置一个随机种子
SEED = 1234
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True  #使用cuda保证每次结果一样。将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的


#创建tokenizer
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')



#把tokenizer从一串字符转成一个list,同时做一个reverse取反
#在原论文中,作者发现颠倒源语言的输入的顺序可以取得不错的翻译效果,例如,一句话为“good morning!”,颠倒顺序分词后变为"!", “morning”, 和"good"。

#将德语进行分词并颠倒顺序
def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
#将英语进行分词,不颠倒顺序
def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]


#我们创建SRC和TRG两个Field对象,tokenize为我们刚才定义的分词器函数,在每句话的开头加入字符SOS,结尾加入字符EOS,将所有单词转换为小写。

#TorchText的Field定义数据应该如何被处理
#SRC即source,是德语
#TRG即target,是英语
#sos是start of sequence, eos是end of sequence
#lower=True是将所有单词转换为小写
SRC = Field(tokenize = tokenize_de,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)

TRG = Field(tokenize = tokenize_en,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)


#使用torchtext自带的Multi30k数据集,这是一个包含约30000个平行的英语、德语和法语句子的数据集,每个句子包含约12个单词。
train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'),
                                                    fields = (SRC, TRG))

#查看一下加载完的数据集
print(f"Number of training examples: {len(train_data.examples)}")
print(f"Number of validation examples: {len(valid_data.examples)}")
print(f"Number of testing examples: {len(test_data.examples)}")


#看一下生成的第一个训练样本,可以看到源语言的顺序已经颠倒了
print(vars(train_data.examples[0]))


#构建词表
#所谓构建词表,即需要给每个单词编码,也就是用数字表示每个单词,这样才能传入模型。
#可以使用dataset类中的build_vocab()方法传入用于构建词表的数据集。
#注意,源语言和目标语言的词表是不同的,而且词表应该只从训练集构建,而不是验证/测试集,这可以防止“信息泄漏”到模型中。
SRC.build_vocab(train_data, min_freq = 2) #设置最小词频为2,当一个单词在数据集中出现次数小于2时会被转换为<unk>字符。
TRG.build_vocab(train_data, min_freq = 2)


#查看一下生成的词表大小
print(f"Unique tokens in source (de) vocabulary: {len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary: {len(TRG.vocab)}")


#指定GPU还是CPU进行训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')




BATCH_SIZE = 128

#创建迭代器
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data),
    batch_size = BATCH_SIZE,
    device = device)

#查看一下生成的batch
batch = next(iter(train_iterator))
print(batch)
#输出:
# [torchtext.data.batch.Batch of size 128 from MULTI30K]
# 	[.src]:[torch.cuda.LongTensor of size 23x128 (GPU 0)]
# 	[.trg]:[torch.cuda.LongTensor of size 21x128 (GPU 0)]



#2、创建Seq2Seq模型

#我们将分别创建编码器(Encoder)、解码器(Eecoder)和seq2seq模型。
#原论文使用了一个4层的单向LSTM,出于训练时间的考虑,我们将其缩减到了2层。结构如图所示

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.input_dim = input_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(input_dim, emb_dim)

        #encoder部分
        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.dropout = nn.Dropout(dropout)

    def forward(self, src):
        #src:(sent_len, batch_size)

        embedded = self.dropout(self.embedding(src))

        #embedded:(sent_len, batch_size, emb_dim)
        outputs, (hidden, cell) = self.rnn(embedded)
        #outputs:(sent_len, batch_size, hid_dim)
        #hidden:(n_layers, batch_size, hid_dim)
        #cell:(n_layers, batch_size, hid_dim)
        return hidden, cell


class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.output_dim = output_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(output_dim, emb_dim)

        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.out = nn.Linear(hid_dim, output_dim)

        self.dropout = nn.Dropout(dropout)

    def forward(self, input, hidden, cell):
        # input: (batch_size) -> input: (1, batch_size)
        input = input.unsqueeze(0)
        # embedded: (1, batch_size, emb_dim)
        embedded = self.dropout(self.embedding(input))
        # hidden: (n_layers, batch size, hid_dim)
        # cell: (n_layers, batch size, hid_dim)
        # output(1, batch_size, hid_dim)
        output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
        # prediction: (batch_size, output_dim)
        prediction = self.out(output.squeeze(0))

        return prediction, hidden, cell


class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()

        self.encoder = encoder
        self.decoder = decoder
        self.device = device

        assert encoder.hid_dim == decoder.hid_dim, \
            "Hidden dimensions of encoder and decoder must be equal!"
        assert encoder.n_layers == decoder.n_layers, \
            "Encoder and decoder must have equal number of layers!"

    def forward(self, src, trg, teacher_forcing_ratio=0.5):
        # src = [src sent len, batch size]
        # trg = [trg sent len, batch size]
        # teacher_forcing_ratio is probability to use teacher forcing
        # e.g. if teacher_forcing_ratio is 0.75 we use ground-truth inputs 75% of the time

        batch_size = trg.shape[1]
        max_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim

        # tensor to store decoder outputs
        #创建outputs张量存储Decoder的输出
        outputs = torch.zeros(max_len, batch_size, trg_vocab_size).to(self.device)

        # last hidden state of the encoder is used as the initial hidden state of the decoder
        hidden, cell = self.encoder(src)

        #输入到Decoder网络的第一个字符是<sos>(句子开始标记)
        input = trg[0, :]

        for t in range(1, max_len):
            # insert input token embedding, previous hidden and previous cell states
            # receive output tensor (predictions) and new hidden and cell states
            output, hidden, cell = self.decoder(input, hidden, cell)

            # place predictions in a tensor holding predictions for each token
            outputs[t] = output

            # decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio

            # get the highest predicted token from our predictions
            top1 = output.argmax(1)

            # if teacher forcing, use actual next token as next input
            # if not, use predicted token
            input = trg[t] if teacher_force else top1

        return outputs


#3、训练模型

#定义模型参数
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5


#编码器和解码器的嵌入层维度(emb_dim)和dropout可以不同,但是层数和隐藏层维度必须相同。
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)  #7855 256 512 2 0.5
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) #5893 256 512 2 0.5

model = Seq2Seq(enc, dec, device).to(device)

#初始化模型参数
#在原论文中,作者将所有参数初始化为-0.08和+0.08之间的均匀分布。我们通过创建一个函数来初始化模型中的参数权重。当使用apply方法时,模型中的每个模块和子模块都会调用init_weights函数。
def init_weights(m):
    for name, param in m.named_parameters():
        nn.init.uniform_(param.data, -0.08, 0.08)

model.apply(init_weights)

#看一下模型中可训练参数的总数量
def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')



#使用Adam作为优化器
optimizer = optim.Adam(model.parameters())


#使用交叉熵损失作为损失函数
#使用交叉熵损失作为损失函数,由于Pytorch在计算交叉熵损失时在一个batch内求平均,因此需要忽略target为的值(在数据处理阶段,一个batch里的所有句子都padding到了相同的长度,不足的用补齐),否则将影响梯度的计算
PAD_IDX = TRG.vocab.stoi['<pad>']

criterion = nn.CrossEntropyLoss(ignore_index = PAD_IDX)


#定义训练函数
def train(model, iterator, optimizer, criterion, clip):   #criterion是损失函数
    model.train()

    epoch_loss = 0

    for i, batch in enumerate(iterator):
        #这里的src和trg都是tensor的形式了
        src = batch.src
        trg = batch.trg

        optimizer.zero_grad()

        output = model(src, trg)

        # trg = [trg sent len, batch size]
        # output = [trg sent len, batch size, output dim]

        output = output[1:].view(-1, output.shape[-1])
        trg = trg[1:].view(-1)

        # trg = [(trg sent len - 1) * batch size]
        # output = [(trg sent len - 1) * batch size, output dim]

        loss = criterion(output, trg)

        loss.backward()

        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)

        optimizer.step()

        epoch_loss += loss.item()

    return epoch_loss / len(iterator)

#定义验证函数,即val的
#评估阶段和训练阶段的区别是不需要更新任何参数
def evaluate(model, iterator, criterion):
    model.eval()

    epoch_loss = 0

    with torch.no_grad():
        for i, batch in enumerate(iterator):
            src = batch.src
            trg = batch.trg

            output = model(src, trg, 0)  # turn off teacher forcing

            # trg = [trg sent len, batch size]
            # output = [trg sent len, batch size, output dim]

            output = output[1:].view(-1, output.shape[-1])
            trg = trg[1:].view(-1)

            # trg = [(trg sent len - 1) * batch size]
            # output = [(trg sent len - 1) * batch size, output dim]

            loss = criterion(output, trg)

            epoch_loss += loss.item()

    return epoch_loss / len(iterator)


def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs


#训练模型
N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

    start_time = time.time()

    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)

    end_time = time.time()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut1-model.pt')   #保存最佳验证损失的epoch参数作为模型的最终参数

    print(f'Epoch: {epoch + 1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')
    #math.exp():使用一个batch内的平均损失计算困惑度

 

验证模型

import torch
import torch.nn as nn
import torch.optim as optim

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator

import spacy
import random
import math
import time

#1、preparing data
#设置一个随机种子
SEED = 1234
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True  #使用cuda保证每次结果一样。将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的


#创建tokenizer
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')



#把tokenizer从一串字符转成一个list,同时做一个reverse取反
#在原论文中,作者发现颠倒源语言的输入的顺序可以取得不错的翻译效果,例如,一句话为“good morning!”,颠倒顺序分词后变为"!", “morning”, 和"good"。

#将德语进行分词并颠倒顺序
def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
#将英语进行分词,不颠倒顺序
def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]


#我们创建SRC和TRG两个Field对象,tokenize为我们刚才定义的分词器函数,在每句话的开头加入字符SOS,结尾加入字符EOS,将所有单词转换为小写。

#TorchText的Field定义数据应该如何被处理
#SRC即source,是德语
#TRG即target,是英语
#sos是start of sequence, eos是end of sequence
#lower=True是将所有单词转换为小写
SRC = Field(tokenize = tokenize_de,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)

TRG = Field(tokenize = tokenize_en,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)


#使用torchtext自带的Multi30k数据集,这是一个包含约30000个平行的英语、德语和法语句子的数据集,每个句子包含约12个单词。
train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'),
                                                    fields = (SRC, TRG))

#查看一下加载完的数据集
print(f"Number of training examples: {len(train_data.examples)}")
print(f"Number of validation examples: {len(valid_data.examples)}")
print(f"Number of testing examples: {len(test_data.examples)}")


#看一下生成的第一个训练样本,可以看到源语言的顺序已经颠倒了
print(vars(train_data.examples[0]))


#构建词表
#所谓构建词表,即需要给每个单词编码,也就是用数字表示每个单词,这样才能传入模型。
#可以使用dataset类中的build_vocab()方法传入用于构建词表的数据集。
#注意,源语言和目标语言的词表是不同的,而且词表应该只从训练集构建,而不是验证/测试集,这可以防止“信息泄漏”到模型中。
SRC.build_vocab(train_data, min_freq = 2) #设置最小词频为2,当一个单词在数据集中出现次数小于2时会被转换为<unk>字符。
TRG.build_vocab(train_data, min_freq = 2)


#查看一下生成的词表大小
print(f"Unique tokens in source (de) vocabulary: {len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary: {len(TRG.vocab)}")


#指定GPU还是CPU进行训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')




BATCH_SIZE = 128

#创建迭代器
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data),
    batch_size = BATCH_SIZE,
    device = device)

#查看一下生成的batch
batch = next(iter(train_iterator))
print(batch)
#输出:
# [torchtext.data.batch.Batch of size 128 from MULTI30K]
# 	[.src]:[torch.cuda.LongTensor of size 23x128 (GPU 0)]
# 	[.trg]:[torch.cuda.LongTensor of size 21x128 (GPU 0)]



#2、创建Seq2Seq模型

#我们将分别创建编码器(Encoder)、解码器(Eecoder)和seq2seq模型。
#原论文使用了一个4层的单向LSTM,出于训练时间的考虑,我们将其缩减到了2层。结构如图所示

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.input_dim = input_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(input_dim, emb_dim)

        #encoder部分
        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.dropout = nn.Dropout(dropout)

    def forward(self, src):
        #src:(sent_len, batch_size)

        embedded = self.dropout(self.embedding(src))

        #embedded:(sent_len, batch_size, emb_dim)
        outputs, (hidden, cell) = self.rnn(embedded)
        #outputs:(sent_len, batch_size, hid_dim)
        #hidden:(n_layers, batch_size, hid_dim)
        #cell:(n_layers, batch_size, hid_dim)
        return hidden, cell


class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.output_dim = output_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(output_dim, emb_dim)

        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.out = nn.Linear(hid_dim, output_dim)

        self.dropout = nn.Dropout(dropout)

    def forward(self, input, hidden, cell):
        # input: (batch_size) -> input: (1, batch_size)
        input = input.unsqueeze(0)
        # embedded: (1, batch_size, emb_dim)
        embedded = self.dropout(self.embedding(input))
        # hidden: (n_layers, batch size, hid_dim)
        # cell: (n_layers, batch size, hid_dim)
        # output(1, batch_size, hid_dim)
        output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
        # prediction: (batch_size, output_dim)
        prediction = self.out(output.squeeze(0))

        return prediction, hidden, cell


class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()

        self.encoder = encoder
        self.decoder = decoder
        self.device = device

        assert encoder.hid_dim == decoder.hid_dim, \
            "Hidden dimensions of encoder and decoder must be equal!"
        assert encoder.n_layers == decoder.n_layers, \
            "Encoder and decoder must have equal number of layers!"

    def forward(self, src, trg, teacher_forcing_ratio=0.5):
        # src = [src sent len, batch size]
        # trg = [trg sent len, batch size]
        # teacher_forcing_ratio is probability to use teacher forcing
        # e.g. if teacher_forcing_ratio is 0.75 we use ground-truth inputs 75% of the time

        batch_size = trg.shape[1]
        max_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim

        # tensor to store decoder outputs
        #创建outputs张量存储Decoder的输出
        outputs = torch.zeros(max_len, batch_size, trg_vocab_size).to(self.device)

        # last hidden state of the encoder is used as the initial hidden state of the decoder
        hidden, cell = self.encoder(src)

        #输入到Decoder网络的第一个字符是<sos>(句子开始标记)
        input = trg[0, :]

        for t in range(1, max_len):
            # insert input token embedding, previous hidden and previous cell states
            # receive output tensor (predictions) and new hidden and cell states
            output, hidden, cell = self.decoder(input, hidden, cell)

            # place predictions in a tensor holding predictions for each token
            outputs[t] = output

            # decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio

            # get the highest predicted token from our predictions
            top1 = output.argmax(1)

            # if teacher forcing, use actual next token as next input
            # if not, use predicted token
            input = trg[t] if teacher_force else top1

        return outputs


#3、训练模型

#定义模型参数
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5


#编码器和解码器的嵌入层维度(emb_dim)和dropout可以不同,但是层数和隐藏层维度必须相同。
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)  #7855 256 512 2 0.5
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) #5893 256 512 2 0.5

model = Seq2Seq(enc, dec, device).to(device)

#初始化模型参数
#在原论文中,作者将所有参数初始化为-0.08和+0.08之间的均匀分布。我们通过创建一个函数来初始化模型中的参数权重。当使用apply方法时,模型中的每个模块和子模块都会调用init_weights函数。
def init_weights(m):
    for name, param in m.named_parameters():
        nn.init.uniform_(param.data, -0.08, 0.08)

model.apply(init_weights)

#看一下模型中可训练参数的总数量
def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')



#使用Adam作为优化器
optimizer = optim.Adam(model.parameters())


#使用交叉熵损失作为损失函数
#使用交叉熵损失作为损失函数,由于Pytorch在计算交叉熵损失时在一个batch内求平均,因此需要忽略target为的值(在数据处理阶段,一个batch里的所有句子都padding到了相同的长度,不足的用补齐),否则将影响梯度的计算
PAD_IDX = TRG.vocab.stoi['<pad>']

criterion = nn.CrossEntropyLoss(ignore_index = PAD_IDX)


#定义训练函数
def train(model, iterator, optimizer, criterion, clip):   #criterion是损失函数
    model.train()

    epoch_loss = 0

    for i, batch in enumerate(iterator):
        #这里的src和trg都是tensor的形式了
        src = batch.src
        trg = batch.trg

        optimizer.zero_grad()

        output = model(src, trg)

        # trg = [trg sent len, batch size]
        # output = [trg sent len, batch size, output dim]

        output = output[1:].view(-1, output.shape[-1])
        trg = trg[1:].view(-1)

        # trg = [(trg sent len - 1) * batch size]
        # output = [(trg sent len - 1) * batch size, output dim]

        loss = criterion(output, trg)

        loss.backward()

        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)

        optimizer.step()

        epoch_loss += loss.item()

    return epoch_loss / len(iterator)

#定义验证函数,即val的
#评估阶段和训练阶段的区别是不需要更新任何参数
def evaluate(model, iterator, criterion):
    model.eval()

    epoch_loss = 0

    with torch.no_grad():
        for i, batch in enumerate(iterator):
            src = batch.src
            trg = batch.trg

            output = model(src, trg, 0)  # turn off teacher forcing

            # trg = [trg sent len, batch size]
            # output = [trg sent len, batch size, output dim]

            output = output[1:].view(-1, output.shape[-1])
            trg = trg[1:].view(-1)

            # trg = [(trg sent len - 1) * batch size]
            # output = [(trg sent len - 1) * batch size, output dim]

            loss = criterion(output, trg)

            epoch_loss += loss.item()

    return epoch_loss / len(iterator)


def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs


#训练模型
N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

    start_time = time.time()

    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)

    end_time = time.time()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut1-model.pt')   #保存最佳验证损失的epoch参数作为模型的最终参数

    print(f'Epoch: {epoch + 1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')
    #math.exp():使用一个batch内的平均损失计算困惑度

#4、验证模型
model.load_state_dict(torch.load('tut1-model.pt'))

test_loss = evaluate(model, test_iterator, criterion)

print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')

 

 

 

最终

import torch
import torch.nn as nn
import torch.optim as optim

from torchtext.datasets import TranslationDataset, Multi30k
from torchtext.data import Field, BucketIterator

import spacy
import random
import math
import time

#1、preparing data
#设置一个随机种子
SEED = 1234
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True  #使用cuda保证每次结果一样。将这个 flag 置为True的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的


#创建tokenizer
spacy_de = spacy.load('de')
spacy_en = spacy.load('en')



#把tokenizer从一串字符转成一个list,同时做一个reverse取反
#在原论文中,作者发现颠倒源语言的输入的顺序可以取得不错的翻译效果,例如,一句话为“good morning!”,颠倒顺序分词后变为"!", “morning”, 和"good"。

#将德语进行分词并颠倒顺序
def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
#将英语进行分词,不颠倒顺序
def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]


#我们创建SRC和TRG两个Field对象,tokenize为我们刚才定义的分词器函数,在每句话的开头加入字符SOS,结尾加入字符EOS,将所有单词转换为小写。

#TorchText的Field定义数据应该如何被处理
#SRC即source,是德语
#TRG即target,是英语
#sos是start of sequence, eos是end of sequence
#lower=True是将所有单词转换为小写
SRC = Field(tokenize = tokenize_de,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)

TRG = Field(tokenize = tokenize_en,
            init_token = '<sos>',
            eos_token = '<eos>',
            lower = True)


#使用torchtext自带的Multi30k数据集,这是一个包含约30000个平行的英语、德语和法语句子的数据集,每个句子包含约12个单词。
train_data, valid_data, test_data = Multi30k.splits(exts = ('.de', '.en'),
                                                    fields = (SRC, TRG))

#查看一下加载完的数据集
print(f"Number of training examples: {len(train_data.examples)}")
print(f"Number of validation examples: {len(valid_data.examples)}")
print(f"Number of testing examples: {len(test_data.examples)}")


#看一下生成的第一个训练样本,可以看到源语言的顺序已经颠倒了
print(vars(train_data.examples[0]))


#构建词表
#所谓构建词表,即需要给每个单词编码,也就是用数字表示每个单词,这样才能传入模型。
#可以使用dataset类中的build_vocab()方法传入用于构建词表的数据集。
#注意,源语言和目标语言的词表是不同的,而且词表应该只从训练集构建,而不是验证/测试集,这可以防止“信息泄漏”到模型中。
SRC.build_vocab(train_data, min_freq = 2) #设置最小词频为2,当一个单词在数据集中出现次数小于2时会被转换为<unk>字符。
TRG.build_vocab(train_data, min_freq = 2)


#查看一下生成的词表大小
print(f"Unique tokens in source (de) vocabulary: {len(SRC.vocab)}")
print(f"Unique tokens in target (en) vocabulary: {len(TRG.vocab)}")


#指定GPU还是CPU进行训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')




BATCH_SIZE = 128

#创建迭代器
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
    (train_data, valid_data, test_data),
    batch_size = BATCH_SIZE,
    device = device)

#查看一下生成的batch
batch = next(iter(train_iterator))
print(batch)
#输出:
# [torchtext.data.batch.Batch of size 128 from MULTI30K]
# 	[.src]:[torch.cuda.LongTensor of size 23x128 (GPU 0)]
# 	[.trg]:[torch.cuda.LongTensor of size 21x128 (GPU 0)]



#2、创建Seq2Seq模型

#我们将分别创建编码器(Encoder)、解码器(Eecoder)和seq2seq模型。
#原论文使用了一个4层的单向LSTM,出于训练时间的考虑,我们将其缩减到了2层。结构如图所示

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.input_dim = input_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(input_dim, emb_dim)

        #encoder部分
        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.dropout = nn.Dropout(dropout)

    def forward(self, src):
        #src:(sent_len, batch_size)

        embedded = self.dropout(self.embedding(src))

        #embedded:(sent_len, batch_size, emb_dim)
        outputs, (hidden, cell) = self.rnn(embedded)
        #outputs:(sent_len, batch_size, hid_dim)
        #hidden:(n_layers, batch_size, hid_dim)
        #cell:(n_layers, batch_size, hid_dim)
        return hidden, cell


class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
        super().__init__()

        self.output_dim = output_dim
        self.emb_dim = emb_dim
        self.hid_dim = hid_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.embedding = nn.Embedding(output_dim, emb_dim)

        self.rnn = nn.LSTM(emb_dim, hid_dim, n_layers, dropout=dropout)

        self.out = nn.Linear(hid_dim, output_dim)

        self.dropout = nn.Dropout(dropout)

    def forward(self, input, hidden, cell):
        # input: (batch_size) -> input: (1, batch_size)
        input = input.unsqueeze(0)
        # embedded: (1, batch_size, emb_dim)
        embedded = self.dropout(self.embedding(input))
        # hidden: (n_layers, batch size, hid_dim)
        # cell: (n_layers, batch size, hid_dim)
        # output(1, batch_size, hid_dim)
        output, (hidden, cell) = self.rnn(embedded, (hidden, cell))
        # prediction: (batch_size, output_dim)
        prediction = self.out(output.squeeze(0))

        return prediction, hidden, cell


class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()

        self.encoder = encoder
        self.decoder = decoder
        self.device = device

        assert encoder.hid_dim == decoder.hid_dim, \
            "Hidden dimensions of encoder and decoder must be equal!"
        assert encoder.n_layers == decoder.n_layers, \
            "Encoder and decoder must have equal number of layers!"

    def forward(self, src, trg, teacher_forcing_ratio=0.5):
        # src = [src sent len, batch size]
        # trg = [trg sent len, batch size]
        # teacher_forcing_ratio is probability to use teacher forcing
        # e.g. if teacher_forcing_ratio is 0.75 we use ground-truth inputs 75% of the time

        batch_size = trg.shape[1]
        max_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim

        # tensor to store decoder outputs
        #创建outputs张量存储Decoder的输出
        outputs = torch.zeros(max_len, batch_size, trg_vocab_size).to(self.device)

        # last hidden state of the encoder is used as the initial hidden state of the decoder
        hidden, cell = self.encoder(src)

        #输入到Decoder网络的第一个字符是<sos>(句子开始标记)
        input = trg[0, :]

        for t in range(1, max_len):
            # insert input token embedding, previous hidden and previous cell states
            # receive output tensor (predictions) and new hidden and cell states
            output, hidden, cell = self.decoder(input, hidden, cell)

            # place predictions in a tensor holding predictions for each token
            outputs[t] = output

            # decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio

            # get the highest predicted token from our predictions
            top1 = output.argmax(1)

            # if teacher forcing, use actual next token as next input
            # if not, use predicted token
            input = trg[t] if teacher_force else top1

        return outputs


#3、训练模型

#定义模型参数
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5


#编码器和解码器的嵌入层维度(emb_dim)和dropout可以不同,但是层数和隐藏层维度必须相同。
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)  #7855 256 512 2 0.5
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT) #5893 256 512 2 0.5

model = Seq2Seq(enc, dec, device).to(device)

#初始化模型参数
#在原论文中,作者将所有参数初始化为-0.08和+0.08之间的均匀分布。我们通过创建一个函数来初始化模型中的参数权重。当使用apply方法时,模型中的每个模块和子模块都会调用init_weights函数。
def init_weights(m):
    for name, param in m.named_parameters():
        nn.init.uniform_(param.data, -0.08, 0.08)

model.apply(init_weights)

#看一下模型中可训练参数的总数量
def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')



#使用Adam作为优化器
optimizer = optim.Adam(model.parameters())


#使用交叉熵损失作为损失函数
#使用交叉熵损失作为损失函数,由于Pytorch在计算交叉熵损失时在一个batch内求平均,因此需要忽略target为的值(在数据处理阶段,一个batch里的所有句子都padding到了相同的长度,不足的用补齐),否则将影响梯度的计算
PAD_IDX = TRG.vocab.stoi['<pad>']

criterion = nn.CrossEntropyLoss(ignore_index = PAD_IDX)


#定义训练函数
def train(model, iterator, optimizer, criterion, clip):   #criterion是损失函数
    model.train()

    epoch_loss = 0

    for i, batch in enumerate(iterator):
        #这里的src和trg都是tensor的形式了
        src = batch.src
        trg = batch.trg

        optimizer.zero_grad()

        output = model(src, trg)

        # trg = [trg sent len, batch size]
        # output = [trg sent len, batch size, output dim]

        output = output[1:].view(-1, output.shape[-1])
        trg = trg[1:].view(-1)

        # trg = [(trg sent len - 1) * batch size]
        # output = [(trg sent len - 1) * batch size, output dim]

        loss = criterion(output, trg)

        loss.backward()

        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)

        optimizer.step()

        epoch_loss += loss.item()

    return epoch_loss / len(iterator)

#定义验证函数,即val的
#评估阶段和训练阶段的区别是不需要更新任何参数
def evaluate(model, iterator, criterion):
    model.eval()

    epoch_loss = 0

    with torch.no_grad():
        for i, batch in enumerate(iterator):
            src = batch.src
            trg = batch.trg

            output = model(src, trg, 0)  # turn off teacher forcing

            # trg = [trg sent len, batch size]
            # output = [trg sent len, batch size, output dim]

            output = output[1:].view(-1, output.shape[-1])
            trg = trg[1:].view(-1)

            # trg = [(trg sent len - 1) * batch size]
            # output = [(trg sent len - 1) * batch size, output dim]

            loss = criterion(output, trg)

            epoch_loss += loss.item()

    return epoch_loss / len(iterator)


def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs


#训练模型
N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

    start_time = time.time()

    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)

    end_time = time.time()

    epoch_mins, epoch_secs = epoch_time(start_time, end_time)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut1-model.pt')   #保存最佳验证损失的epoch参数作为模型的最终参数

    print(f'Epoch: {epoch + 1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')
    #math.exp():使用一个batch内的平均损失计算困惑度

#4、验证模型
model.load_state_dict(torch.load('tut1-model.pt'))

test_loss = evaluate(model, test_iterator, criterion)

print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')

 

 

 

参考:

https://blog.csdn.net/weixin_43632501/article/details/98731800

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Seq2Seq实战——机器翻译 的相关文章

  • scikit加权f1分数计算及使用

    我有一个关于weightedsklearn metrics f1 score 中的平均值 sklearn metrics f1 score y true y pred labels None pos label 1 average weig
  • 生成易于记忆的随机标识符

    与所有开发人员一样 我们在日常工作中不断处理某种标识符 大多数时候 它与错误或支持票有关 我们的软件在检测到错误后 会创建一个包 该包的名称由时间戳和版本号格式化 这是创建合理唯一标识符以避免混淆包的一种廉价方法 例子 错误报告 20101
  • 从文本文件中提取与输入单词最相似的前 N ​​个单词

    我有一个文本文件 其中包含我使用 BeautifulSoup 提取的网页内容 我需要根据给定的单词从文本文件中找到 N 个相似的单词 流程如下 从中提取文本的网站 https en wikipedia org wiki Football h
  • ANEW 字典可以用于 Quanteda 中的情感分析吗?

    我正在尝试找到一种方法来实施英语单词情感规范 荷兰语 以便使用 Quanteda 进行纵向情感分析 我最终想要的是每年的 平均情绪 以显示任何纵向趋势 在数据集中 所有单词均由 64 名编码员按照 7 分李克特量表在四个类别上进行评分 这提
  • BERT 输出不确定

    BERT 输出是不确定的 当我输入相同的输入时 我希望输出值是确定性的 但我的 bert 模型的值正在变化 听起来很尴尬 同一个值返回两次 一次 也就是说 一旦出现另一个值 就会出现相同的值并重复 如何使输出具有确定性 让我展示我的代码片段
  • 快速 shell 命令删除文本文件中的停用词

    我有一个 2GB 的文本文件 我正在尝试从此文件中删除经常出现的英语停用词 我有 stopwords txt 包含这样的 a an the for and I 使用 shell 命令 例如 tr sed 或 awk 执行此操作的快速方法是什
  • 阻止斯坦福核心 NLP 服务器输出它收到的文本

    我正在运行一个斯坦福核心自然语言处理 http stanfordnlp github io CoreNLP server java mx4g cp edu stanford nlp pipeline StanfordCoreNLPServe
  • 如何提取句子中的主语及其各自的从属短语?

    我正在尝试在句子中进行主题提取 以便我能够根据主题获得情感 我在用nltk在 python2 7 中用于此目的 以下面的句子为例 Donald Trump is the worst president of USA but Hillary
  • 旧版本的 spaCy 在尝试安装模型时抛出“KeyError: 'package'”错误

    我在 Ubuntu 14 04 4 LTS x64 上使用 spaCy 1 6 0 和 python3 5 为了安装 spaCy 的英文版本 我尝试运行 这给了我错误消息 ubun ner 3 NeuroNER master src pyt
  • 使用 NLP 进行地址分割

    我目前正在开发一个项目 该项目应识别地址的每个部分 例如来自 str Jack London 121 Corvallis ARAD ap 1603 973130 输出应如下所示 street name Jack London no 121
  • 使用大数据集在 Google Colab TPU 上训练 seq2seq 模型 - Keras

    我正在尝试使用 Google Colab TPU 上的 Keras 训练用于机器翻译的序列到序列模型 我有一个可以加载到内存中的数据集 但我必须对其进行预处理才能将其提供给模型 特别是 我需要将目标单词转换为一个热向量 并且在许多示例中 我
  • 用于词性标记的优秀 Java 库是什么? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 给定文档,选择相关片段

    当我在这里提出问题时 自动搜索返回的问题的工具提示给出了问题的前一点 但其中相当一部分没有给出任何比理解问题更有用的文本 标题 有谁知道如何制作一个过滤器来删除问题中无用的部分 我的第一个想法是修剪仅包含某个列表中的单词的任何前导句子 例如
  • nltk 标记化和缩写

    我用 nltk 对文本进行标记 只是将句子输入到 wordpunct tokenizer 中 这会拆分缩写 例如 don t 到 don t 但我想将它们保留为一个单词 我正在改进我的方法 以实现更精确的文本标记化 因此我需要更深入地研究
  • 对产品列表进行分类的算法? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我有一个代表或多或少相同的产品的列表 例如 在下面的列表中 它们都是希捷硬盘 希捷硬盘 500Go 适用于笔记本电脑的希捷硬盘 120
  • 如何将句子或文档转换为向量?

    我们有将单词转换为向量的模型 例如 word2vec 模型 是否存在类似的模型 可以使用为单个单词学习的向量将句子 文档转换为向量 1 跳克法 以及使用它的工具 谷歌 word2vec https code google com p wor
  • 如何使用Bert进行长文本分类?

    我们知道 BERT 有 token 的最大长度限制 512 因此如果一篇文章的长度远大于 512 例如文本中有 10000 个 token 如何使用 BERT 您基本上有三个选择 您可以剪掉较长的文本并仅使用前 512 个令牌 最初的 BE
  • 分词统计方法

    我想解决分词问题 从没有空格的长字符串中解析单词 例如我们想要从中提取单词somelongword to some long word 我们可以通过字典的动态方法来实现这一点 但我们遇到的另一个问题是解析歧义 IE orcore gt or
  • BERT 获取句子嵌入

    我正在复制代码这一页 https colab research google com drive 1yFphU6PW9Uo6lmDly ud9a6c4RCYlwdX 我已将 BERT 模型下载到本地系统并获取句子嵌入 我有大约 500 00
  • 如何在 bertopic 建模中获取每个主题的所有文档

    我有一个数据集并尝试使用 berTopic 建模将其转换为主题 但问题是 我无法获取主题的所有文档 berTopic 每个主题仅返回 3 个文档 topic model BERTopic verbose True embedding mod

随机推荐

  • Amazon Fargate 使用 Seekable OCI 实现更快的容器启动速度

    虽然在部署和扩展应用程序时 使用容器进行开发的方式已日趋流行 但仍有一些领域可以改进 扩展容器化应用程序的主要问题之一是启动时间长 尤其是在纵向扩展期间 需要添加较新的实例 此问题可能会对客户体验 例如 当网站需要横向扩展以提供额外流量时
  • cross-tissue 成纤维细胞比例.r

    getwd setwd G lung fibrosis mouse stable state mouse ssfibro readRDS Mouse SS Fibro RDS library Seurat head mouse ssfibr
  • Vue组件生命周期与钩子函数

    组件生命周期 组件 组件是可复用的 Vue 实例 从创建到销毁的过程就是组件的生命周期 是一个时间段 组件生命周期钩子函数 vue3与vue2生命周期钩子函数略有不同 本文以vue2为主 VUE 提供的生命周期钩子函数 会伴随组件的生命周期
  • 超级签具体实现

    签名原理 使用了苹果提供给开发者的Ad Hoc分发通道 把安装设备当做开发设备进行分发 优势 直接分发 安装即可运行 稳定 不会有证书吊销导致的风险 缺点 单开发者账号的iPhone设备数量只有100个 整体架构 设备安装描述文件后 会向服
  • 数据结构(C语言)——单链表

    整体结构如上 看似简单 但第一次用C语言实现还是感觉有点吃力 尤其是特别容易让链表断裂 下面是代码 有链表的增删改查 注 这里E类型是用define将int进行了宏定义 include
  • java 单一登录

    对于一个帐号在同一时间只能一个人登录 可以通过下面的方法实现 1 在用户登录时 把用户添加到一个ArrayList中 2 再次登录时查看ArrayList中有没有该用户 如果ArrayList中已经存在该用户 则阻止其登录 3 当用户退出时
  • 【笔记】三剑客之awk、sed后向引用

    sed后向引用 语法格式 sed r s 1 g file 1 表示获取第一个括号中的内容 sed支持扩展正则需要加r参数 案例1 调用括号中的内容 root ahui echo root sed r s root 1 g root 案例2
  • 计算机组成原理实验三-----系统总线和具有基本输入输出功能的总线接口实验

    总线是计算机中连接各个功能部件的纽带 是计算机各部件之间进行信息传输的公共通路 总线不只是一组简单的信号传输线 它还是一组协议 他有两大特征 分时 同一总线在同一时刻 只能有一个部件占领总线发送信息 其他部件要发送信息得在该 部件发送完释放
  • 【Echarts】echarts渐变色仪表盘

    echarts渐变色仪表盘 echarts随意一个示例代码 直接点击上方链接 将此段代码放到echarts的示例代码编辑框里 let dataList9 total 85 list name 待处置 value 1501 name 处置中
  • 目标检测发展与综述

    目标检测发展与综述 绪论 在github上的git主hoya012整理了关于目标检测的相关论文 点击此处可获取原文链接GitHub hoya012 deep learning object detection A paper list of
  • curl -u 背后的内容以及和 Django rest framework 的 BasicAuthentication 的呼应

    curl u 的基本介绍 curl 是常用的命令行工具 用来请求 Web 服务器 它的名字就是客户端 client 的 URL 工具的意思 它的功能非常强大 命令行参数多达几十种 如果熟练的话 完全可以取代 Postman 这一类的图形界面
  • libusb编译、测试、使用

    要用到才开始学 啥都不懂 感觉好难受 最近要在ARM Linux嵌入式端集成libusb 刚开始搞 慢慢写 首先是libusb的交叉编译和测试 交叉编译 下载libusb的源码 下载地址 https sourceforge net proj
  • upload-labs1-5

    用浏览器打开upload 第一关 js检查 上传文件会获得提示 把文件后缀改为 jpg png gif 我这里用的是jpg 打开burpsuite在upload上传修改后的文件进行爪包 在burpsuite中修改文件进行回显 第二关 仅判断
  • Java学习笔记(十九)

    Spring Cloud 什么是Spring Cloud Spring cloud 应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序 提供与外部系统的集成 更专注于服务治理 Spring cloud Task 一
  • eix安装_U盘安装原版Windows7

    教程简介 本教程为U盘安装原版Windows 7 我将带领大家学习如何用U盘安装原版的Windows7系统 希望对大家有帮助 在开始安装之前需要了解常见电脑的U盘启动按键都有哪些 请仔细阅读下表 安装步骤 一定要看步骤3 不然造成数据损坏概
  • 爬虫入门第2课:代理池的设计

    爬虫学习知识点及案例篇 汇总 爬虫入门第1课 代理池概述及开发环境 本阶段带大家从代理池的设计开始 学习Python爬虫及项目实战 详情关注上方专栏 1 代理池的工作流程 目标 理解代理池的工作流程 以及 各个模块的作用 内容介绍 代理池的
  • Java-API简析_java.net.InetAddress类(基于 Latest JDK)(浅析源码)

    版权声明 未经博主同意 谢绝转载 请尊重原创 博主保留追究权 https blog csdn net m0 69908381 article details 131590559 出自 进步 于辰的博客 因为我发现目前 我对Java API的
  • 模板引擎 template

    1 特性 性能卓越 执行速度通常是Mustache与tmpl的20多倍 性能测试 支持运行时调试 可精确定位异常模板所在的语句 对NodeJS Express友好支持 安全 默认对输出进行转义 在沙箱中运行编译后的代码 Node版本可以安全
  • SQL Server批处理运行时错误的影响

    前言 批处理是同时从应用程序发送到 SQL Server 2005 并得以执行的一组单条或多条 Transact SQL 语句 我们通常认为当一个批处理的多条语句中有一条发生运行时错误 将停止执行批处理中当前语句和它之后的语句 这使得在实际
  • Seq2Seq实战——机器翻译

    基于seq2seq做一个机器翻译 我们将使用PyTorch和TorchText构建一个机器学习模型 从一个序列到另一个序列 将德语到英语翻译成英语 该模型是 Sequence to Sequence Learning with Neural