Pytorch学习记录- 训练GRU Seq2Seq(论文再读)

对Pytorch的Seq2Seq这6篇论文进行精读,第二篇,Cho, K., et al., Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. 2014.
发表于2014年,全文链接

摘要

很牛逼的一个神经网络,基于RNN的Seq2Seq,用于处理符号。使用这个encoder-decoder计算的短语对条件概率作为现有对数线性模型中的附加特征,通过实验发现使用这样特征的SMT成绩得到提升。

1. 介绍

SMT越来越用到神经网络,用作传统的基于短语的SMT系统的一部分。
基于RNN的encdoer-decoder,由两个递归神经网络(RNN)组成,其充当编码器和解码器对。编码器将可变长度源序列映射到固定长度矢量,并且解码器将矢量表示映射回可变长度目标序列。联合训练两个网络以最大化给定源序列的目标序列的条件概率。此外,我们建议使用复杂的隐藏单元,以提高内存容量和训练的便利性。
处理英文-法文,专门训练用来翻译短语,发现这个模型能够很好地捕捉短语表中的语言规律。基于RNN的encoder-decoder能够学习短语中的连续空间表示,而这个短语保留了语义和语法结构,也就是说,RNN能够较好地学习文本中的语义和语法结构。(也就是现在普遍的认知,RNN能够处理较长的文本(虽然在这篇论文里面仍只是处理短语结构))

2. RNN encoder-decoder

2.1 初步:RNN

在这里说了什么是RNN,略过

2.2 RNN encoder-decoder

同样也是encoder处理短语,生成向量,使用隐藏状态保存整个输入的sequence。decoder使用encoder生成的隐藏状态h_{<t>}训练生成输出结果y_t。但是与传统的RNN不同,h_{<t>}y_t也以输入序列y_{t-1}的汇总c作为条件。
decoder在t时间的隐藏状态计算公式
h_{<t>}=f(h_{<t>},y_{t-1},c)
而下一个短语的条件分布,使用softmax计算
P(y_t|y_{t-1},...,y_1,c)=g(h_{<t>},y_{t-1},c)
最终encoder-decoder两个部分整合,训练得到最大的条件log-likelihood,其中θ是模型参数,每一个(x_n,y_n)就是一个输入输出对
max_θ \frac{1}{N}\Sigma_{n=1}^Nlogp_{θ}{y_n|x_n}

2.3 能够自适应记忆和遗忘的隐藏状态

新的模型就有新的隐藏状态,之前LSTM的隐藏状态太过简单,这里使用GRU,帮助RNN记住长期信息。
由于每个隐藏单元具有单独的重置和更新门,每个隐藏单元将学习捕获不同时间尺度上的依赖性。学习捕获短期依赖关系的那些单元将倾向于重置经常活动的门,但那些捕获长期依赖关系的那些将具有最活跃的更新门。
总之,换成GRU。

3. 基于统计的机器翻译

在实践中,大多数SMT系统将log p(f|e)建模为具有附加特征和相应权重的对数线性模型
log p(f|e)=\Sigma_{n=1}^N w_nf_n(f,e)+logZ(e)

3.1 使用RNN encoder-decoder评分短语对

3.2 相关方法,神经网络机器翻译

列举了一系列相关类似研究,都是使用神经网络构建机器翻译系统。

4. 实验

4.1 数据和基线

在WMT'14翻译任务的框架内,可以利用大量资源建立英语/法语SMT系统。双语语料库包括Europarl(61M字),新闻评论(5.5M),UN(421M)和两个分别为90M和780M字的爬虫语料库。最后两个语料库很嘈杂。
实验里使用的语料库很大,是一个新闻语料库含有712M字。

应该关注给定任务的最相关数据子集。论文从一个超过2G字中选取了一个418M字子集中用于语言建模,并从850M字中选择348M的子集用于训练RNN encoder-decoder。我们使用测试集newstest2012和2013进行数据选择和使用MERT进行重量调整,并使用newstest2014作为我们的测试集。每组有超过7万个单词和一个参考翻译。
为了训练神经网络,将源和目标词汇限制为英语和法语最常见的15,000个单词。这涵盖了大约93%的数据集。所有词汇表外的单词都被映射到一个特殊的令牌([UNK])。

可以发现在教程中的复现很粗糙,2014年的论文中使用了超过2G的数据集,现在的运算量应该更大……

下面让我们来实现一下。

5. 模型实现

GRU Seq2Seq结构图.png

前一个模型的一个缺点是Decoder试图将大量信息塞入隐藏状态。在解码时,隐藏状态将需要包含关于整个源序列的信息,以及到目前为止已经解码的所有token。通过减轻一些信息压缩,我们可以创建一个更好的模型!
同时这里将使用LSTM的进化版GRU。
另外从原论文来看,似乎这个模型复现是一个很粗糙的,使用的数据依旧是Multi30k。

Multi30K共有30K个图片,每个图片对应的描述有两大类,(i)每个图片的英语描述和英语描述的德语翻译,(ii)五个独立的英语描述和德语描述(不是翻译)。正因为它独立的收集不同语言对图片的描述,因此可以更好地适用于有噪声的多模态内容。

这里我想试一下能不能使用WMT'14的数据集训练模型。不过还是先用Multi30k来实现。

5.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
SEED=1234
random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic=True

spacy_de=spacy.load('de')
spacy_en=spacy.load('en')

def tokenize_de(text):
    return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
    return [tok.text for tok in spacy_en.tokenizer(text)]

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)

train_data,valid_data,test_data=Multi30k.splits(exts=('.de','.en'),fields=(SRC,TRG))
print(vars(train_data.examples[11]))
{'src': ['vier', 'typen', ',', 'von', 'denen', 'drei', 'hüte', 'tragen', 'und', 'einer', 'nicht', ',', 'springen', 'oben', 'in', 'einem', 'treppenhaus', '.'], 'trg': ['four', 'guys', 'three', 'wearing', 'hats', 'one', 'not', 'are', 'jumping', 'at', 'the', 'top', 'of', 'a', 'staircase', '.']}
SRC.build_vocab(train_data, min_freq=2)
TRG.build_vocab(train_data, min_freq=2)
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
)

5.2 构建模型

数据集和之前是一样的,接下来构建模型,这次使用的是GRU作为神经网络单元,结构上有一定区别,但是在论文中没有提及,论文对模型的描述更加概括。

5.2.1 参数设定

这里的GRU是一个单层,所以不需要n_layers参数。

INPUT_DIM=len(SRC.vocab)
OUTPUT_DIM=len(TRG.vocab)
ENC_EMB_DIM=256
DEC_EMB_DIM=256
HID_DIM=512
ENC_DROPOUT=0.5
DEC_DROPOUT=0.5

和上一个模型类似,三个部分,encoder、decoder和seq2seq

5.2.2 Seq2Seq

我把里面预生成的张量outputs打印了出来。

for i ,batch in enumerate(train_iterator):
    if i <1:
        print(i)
        src=batch.src
        trg=batch.trg
        print(type(src))
        print(src.shape)
        print(src)
        print(src.shape[0])
        print(src.shape[1])
        max_len=trg.shape[0]
        batch_size=trg.shape[1]
        trg_vocab_size=len(TRG.vocab)
        print(max_len)
        print(batch_size)
        print(trg_vocab_size)
        
        outputs=torch.zeros(max_len,batch_size,trg_vocab_size)
        print(outputs.shape)
        print(outputs)
    else: break
0
<class 'torch.Tensor'>
torch.Size([26, 128])
tensor([[  2,   2,   2,  ...,   2,   2,   2],
        [  8, 241,   5,  ...,   5,   5,   5],
        [168, 163,   0,  ...,  26, 550,  66],
        ...,
        [  1,   1,   1,  ...,   1,   1,   1],
        [  1,   1,   1,  ...,   1,   1,   1],
        [  1,   1,   1,  ...,   1,   1,   1]], device='cuda:0')
26
128
23
128
5893
torch.Size([23, 128, 5893])
tensor([[[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]],

        [[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]],

        [[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]],

        ...,

        [[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]],

        [[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]],

        [[0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         ...,
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.],
         [0., 0., 0.,  ..., 0., 0., 0.]]])
class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super(Seq2Seq,self).__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!"
    def forward(self, src, trg, teacher_forcing_ratio=0.5):
        batch_size=trg.shape[1]
        max_len=trg.shape[0]
        trg_vocab_size=self.decoder.output_dim
        outputs=torch.zeros(max_len,batch_size,trg_vocab_size).to(self.device)
        context=self.encoder(src)
        hidden=context
        input=trg[0,:]
        
        for t in range(1,max_len):
            output, hidden=self.decoder(input,hidden,context)
            outputs[t]=output
            teacher_force=random.random()<teacher_forcing_ratio
            top1=output.max(1)[1]
            input=(trg[t] if teacher_forcing_ratio else top1)
        return outputs

5.2.3 Encoder

encoder和之前的很类似,因为使用GRU不会输出每个单元的状态,因此在返回中只是输出了隐藏层的状态。

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, hid_dim,dropout):
        super(Encoder,self).__init__()
        self.input_dim=input_dim
        self.emb_dim=emb_dim
        self.hid_dim=hid_dim
        self.dropout=dropout
        
        self.embedding=nn.Embedding(input_dim,emb_dim)
        self.rnn=nn.GRU(emb_dim, hid_dim)
        self.dropout=nn.Dropout(dropout)
        
    def forward(self, src):
        embedded=self.dropout(self.embedding(src))
        outputs, hidden=self.rnn(embedded)
        return hidden

5.2.4 Decoder

decoder因为也使用GRU,减少了信息压缩,同时GRU获取了目标token y_t,上一个时间的隐藏状态s_{t-1},上下文向量z。但是在这里要注意的是输入的初始隐藏状态s_0其实就是上下文向量,就是说,实际输入的是两个相同的上下文向量。

  • 在实现的时候,通过将y_tz串联传入GRU,所以输入的维度应该是emb_dim+ hid_dim
  • linear层输入的是 y_t, s_tz串联,而隐藏状态和上下文向量都是h维度相同,所以输入的维度是emb_dim+hid_dim*2
  • forward现在需要一个上下文参数。在forward过程中,我们将y_tz连接成emb_con,然后输入GRU,我们将y_ts_tz连接在一起作为输出,然后通过线性层提供它以接收我们的预测, \hat{Y} _ {T + 1}
class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, hid_dim, dropout):
        super(Decoder, self).__init__()
        self.output_dim=output_dim
        self.emb_dim=emb_dim
        self.hid_dim=hid_dim
        self.dropout=dropout
        
        self.embedding=nn.Embedding(output_dim,emb_dim)
        self.rnn=nn.GRU(emb_dim+hid_dim, hid_dim)
        self.out=nn.Linear(emb_dim+hid_dim*2, output_dim)
        self.dropout=nn.Dropout(dropout)
    def forward(self, input, hidden, context):
        input=input.unsqueeze(0)
        embedded=self.dropout(self.embedding(input))
        emb_con=torch.cat((embedded,context),dim=2)
        output,hidden=self.rnn(emb_con,hidden)
        output=torch.cat((embedded.squeeze(0),hidden.squeeze(0),context.squeeze(0)),dim=1)
        
        prediction=self.out(output)
        return prediction, hidden
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, DEC_DROPOUT)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# device = torch.device('cpu')
model=Seq2Seq(enc,dec,device).to(device)
def init_weights(m):
    for name,param in m.named_parameters():
        nn.init.normal_(param.data,mean=0,std=0.01)
        
model.apply(init_weights)
Seq2Seq(
  (encoder): Encoder(
    (embedding): Embedding(7855, 256)
    (rnn): GRU(256, 512)
    (dropout): Dropout(p=0.5)
  )
  (decoder): Decoder(
    (embedding): Embedding(5893, 256)
    (rnn): GRU(768, 512)
    (out): Linear(in_features=1280, out_features=5893, bias=True)
    (dropout): Dropout(p=0.5)
  )
)
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')
The model has 14,220,293 trainable parameters
optimizer=optim.Adam(model.parameters())
PAD_IDX=TRG.vocab.stoi['<pad>']
criterion=nn.CrossEntropyLoss(ignore_index=PAD_IDX)
# 构建训练循环和验证循环
def train(model, iterator, optimizer, criterion, clip):
    
    model.train()
    
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):
        
        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)
        print(loss.item())
        loss.backward()
        
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        
        optimizer.step()
        
        epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)


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(), 'tut2-model.pt')
    
    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}')
Epoch: 01 | Time: 1m 14s
    Train Loss: 4.430 | Train PPL:  83.960
     Val. Loss: 7.065 |  Val. PPL: 1169.895
Epoch: 02 | Time: 1m 14s
    Train Loss: 3.577 | Train PPL:  35.783
     Val. Loss: 6.738 |  Val. PPL: 843.836
Epoch: 03 | Time: 1m 14s
    Train Loss: 3.146 | Train PPL:  23.237
     Val. Loss: 6.359 |  Val. PPL: 577.889
Epoch: 04 | Time: 1m 14s
    Train Loss: 2.742 | Train PPL:  15.519
     Val. Loss: 5.904 |  Val. PPL: 366.446
Epoch: 05 | Time: 1m 14s
    Train Loss: 2.393 | Train PPL:  10.949
     Val. Loss: 5.749 |  Val. PPL: 313.895
Epoch: 06 | Time: 1m 14s
    Train Loss: 2.096 | Train PPL:   8.136
     Val. Loss: 5.654 |  Val. PPL: 285.460
Epoch: 07 | Time: 1m 14s
    Train Loss: 1.836 | Train PPL:   6.272
     Val. Loss: 5.626 |  Val. PPL: 277.580
Epoch: 08 | Time: 1m 14s
    Train Loss: 1.609 | Train PPL:   4.996
     Val. Loss: 5.626 |  Val. PPL: 277.538
Epoch: 09 | Time: 1m 14s
    Train Loss: 1.414 | Train PPL:   4.113
     Val. Loss: 5.707 |  Val. PPL: 301.039
Epoch: 10 | Time: 1m 14s
    Train Loss: 1.246 | Train PPL:   3.475
     Val. Loss: 5.727 |  Val. PPL: 307.084
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,373评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,732评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,163评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,700评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,036评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,425评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,737评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,421评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,141评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,398评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,908评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,276评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,907评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,018评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,772评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,448评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,325评论 2 261

推荐阅读更多精彩内容