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实战——机器翻译 的相关文章

  • 如何从另一种语言单词创建英文字母字符串?

    我需要找到一种方法将某些语言的单词 翻译 重写为英语 例如 俄语 听起来像privet 用英语讲 含义和语法并不重要 但我希望它听起来更相似 一切都应该用Python编写 我在网上努力查找 但没有找到好的方法 例如 类似这样的事情 tran
  • 当我在本地运行 CoreNLP 时,为什么“corenlp.run”会产生不同的结果?

    网站corenlp run http corenlp run它应该是 CoreNLP 演示站点 显示的结果与我在本地计算机上运行 CoreNLP 管道时的结果截然不同 网站上确实显示了正确的结果 而本地机版本却没有 我想知道是否有任何接近
  • Keras 文本预处理 - 将 Tokenizer 对象保存到文件中以进行评分

    我按照以下步骤 大致 使用 Keras 库训练了一个情感分类器模型 使用 Tokenizer 对象 类将文本语料库转换为序列 使用 model fit 方法构建模型 评估这个模型 现在 为了使用此模型进行评分 我可以将模型保存到文件中并从文
  • 在哪里可以找到英语短语列表? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我的任务是搜索文本中陈词滥调和常见短语的用法 这些短语与您在财富之轮的短语谜题中可能看到的短语类似 这
  • doc2vec 获得良好性能所需的最小数据集大小是多少?

    在不同大小的数据集上进行训练时 doc2vec 的表现如何 原始语料库中没有提到数据集大小 所以我想知道从 doc2vec 中获得良好性能所需的最小大小是多少 有很多东西被称为 doc2vec 但它似乎最常指的是 Le 和 Mikolov
  • 如何使用 python 中的 spacy 库将句子转换为问题 [请参阅下面的我的代码进行更正]

    我需要使用 python 中的 spacy 将任何句子转换为问题 我下面的代码太长了 我需要做更多的工作才能将任何句子完成为问题格式 现在在这段代码中我根据以下条件制定条件是形式 需要形式 有形式 做形式通过检查过去时和现在时 输入 尼娜拉
  • NLTK CoreNLPDependencyParser:无法建立连接

    我正在尝试通过 NLTK 使用斯坦福解析器 按照示例here http www nltk org api nltk parse html nltk parse corenlp CoreNLPDependencyParser 20tutori
  • word2vec中单词的向量代表什么?

    word2vec https code google com p word2vec 是 Google 的开源工具 它为每个单词提供一个浮点值向量 它们到底代表什么 还有一篇论文关于段落向量 http cs stanford edu quoc
  • 如何有效计算文档流中文档之间的相似度

    我收集文本文档 在 Node js 中 其中一个文档i表示为单词列表 考虑到新文档以文档流的形式出现 计算这些文档之间相似性的有效方法是什么 我目前对每个文档中单词的归一化频率使用余弦相似度 我不使用 TF IDF 词频 逆文档频率 因为我
  • target_vocab_size 在方法 tfds.features.text.SubwordTextEncoder.build_from_corpus 中到底意味着什么?

    根据这个链接 https www tensorflow org datasets api docs python tfds features text SubwordTextEncoder build from corpus target
  • Blenderbot 微调

    我一直在尝试微调 HuggingFace 的对话模型 Blendebot 我已经尝试过官方拥抱脸网站上给出的传统方法 该方法要求我们使用 trainer train 方法来完成此操作 我使用 compile 方法尝试了它 我尝试过使用 Py
  • 生成易于记忆的随机标识符

    与所有开发人员一样 我们在日常工作中不断处理某种标识符 大多数时候 它与错误或支持票有关 我们的软件在检测到错误后 会创建一个包 该包的名称由时间戳和版本号格式化 这是创建合理唯一标识符以避免混淆包的一种廉价方法 例子 错误报告 20101
  • 将单引号替换为双引号并排除某些元素

    我想用双引号替换字符串中的所有单引号 但出现的情况除外 例如 n t ll m 等 input the stackoverflow don t said hey what output the stackoverflow don t sai
  • 否定句子的算法

    我想知道是否有人熟悉算法句子否定的任何尝试 例如 给定一个句子 这本书很好 请提供任意数量的意思相反的替代句子 例如 这本书不好 甚至 这本书不好 显然 以高精度实现这一点可能超出了当前 NLP 的范围 但我确信在这个主题上已经有了一些工作
  • 如何训练斯坦福 NLP 情感分析工具

    地狱大家 我正在使用斯坦福核心 NLP 包 我的目标是对推文直播进行情感分析 按原样使用情感分析工具对文本 态度 的分析非常差 许多积极因素被标记为中性 许多消极因素被评为积极 我已经在文本文件中获取了超过一百万条推文 但我不知道如何实际获
  • 快速 shell 命令删除文本文件中的停用词

    我有一个 2GB 的文本文件 我正在尝试从此文件中删除经常出现的英语停用词 我有 stopwords txt 包含这样的 a an the for and I 使用 shell 命令 例如 tr sed 或 awk 执行此操作的快速方法是什
  • 使用正则表达式标记化进行 NLP 词干提取和词形还原

    定义一个函数 名为performStemAndLemma 它需要一个参数 第一个参数 textcontent 是一个字符串 编辑器中给出了函数定义代码存根 执行以下指定任务 1 对给出的所有单词进行分词textcontent 该单词应包含字
  • 如何检测文本是否可读?

    我想知道是否有一种方法可以告诉给定的文本是人类可读的 我所说的人类可读的意思是 它有一些含义 格式就像某人写的文章 或者至少是由软件翻译器生成的供人类阅读的文章 这是背景故事 最近我正在制作一个应用程序 允许用户将短文本上传到数据库 在部署
  • 保存具有自定义前向功能的 Bert 模型并将其置于 Huggingface 上

    我创建了自己的 BertClassifier 模型 从预训练开始 然后添加由不同层组成的我自己的分类头 微调后 我想使用 model save pretrained 保存模型 但是当我打印它并从预训练上传时 我看不到我的分类器头 代码如下
  • 缩短文本并仅保留重要句子

    德国网站 nandoo net 提供了缩短新闻文章的可能性 如果使用滑块更改百分比值 文本会发生变化并且某些句子会被遗漏 您可以在这里看到它的实际效果 http www nandoo net read article 299925 http

随机推荐

  • 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