对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生成的隐藏状态训练生成输出结果。但是与传统的RNN不同,和也以输入序列的汇总c作为条件。
decoder在t时间的隐藏状态计算公式
而下一个短语的条件分布,使用softmax计算
最终encoder-decoder两个部分整合,训练得到最大的条件log-likelihood,其中是模型参数,每一个就是一个输入输出对
2.3 能够自适应记忆和遗忘的隐藏状态
新的模型就有新的隐藏状态,之前LSTM的隐藏状态太过简单,这里使用GRU,帮助RNN记住长期信息。
由于每个隐藏单元具有单独的重置和更新门,每个隐藏单元将学习捕获不同时间尺度上的依赖性。学习捕获短期依赖关系的那些单元将倾向于重置经常活动的门,但那些捕获长期依赖关系的那些将具有最活跃的更新门。
总之,换成GRU。
3. 基于统计的机器翻译
在实践中,大多数SMT系统将建模为具有附加特征和相应权重的对数线性模型
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 ,上一个时间的隐藏状态,上下文向量。但是在这里要注意的是输入的初始隐藏状态其实就是上下文向量,就是说,实际输入的是两个相同的上下文向量。
- 在实现的时候,通过将和串联传入GRU,所以输入的维度应该是emb_dim+ hid_dim
- linear层输入的是 和 串联,而隐藏状态和上下文向量都是维度相同,所以输入的维度是emb_dim+hid_dim*2
- forward现在需要一个上下文参数。在forward过程中,我们将和连接成emb_con,然后输入GRU,我们将,和连接在一起作为输出,然后通过线性层提供它以接收我们的预测, 。
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
网友评论