對(duì)Pytorch的Seq2Seq這6篇論文進(jìn)行精讀,第二篇,Cho, K., et al., Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation. 2014.
發(fā)表于2014年,全文鏈接
摘要
很牛逼的一個(gè)神經(jīng)網(wǎng)絡(luò),基于RNN的Seq2Seq,用于處理符號(hào)。使用這個(gè)encoder-decoder計(jì)算的短語對(duì)條件概率作為現(xiàn)有對(duì)數(shù)線性模型中的附加特征,通過實(shí)驗(yàn)發(fā)現(xiàn)使用這樣特征的SMT成績得到提升。
1. 介紹
SMT越來越用到神經(jīng)網(wǎng)絡(luò),用作傳統(tǒng)的基于短語的SMT系統(tǒng)的一部分。
基于RNN的encdoer-decoder,由兩個(gè)遞歸神經(jīng)網(wǎng)絡(luò)(RNN)組成,其充當(dāng)編碼器和解碼器對(duì)。編碼器將可變長度源序列映射到固定長度矢量,并且解碼器將矢量表示映射回可變長度目標(biāo)序列。聯(lián)合訓(xùn)練兩個(gè)網(wǎng)絡(luò)以最大化給定源序列的目標(biāo)序列的條件概率。此外,我們建議使用復(fù)雜的隱藏單元,以提高內(nèi)存容量和訓(xùn)練的便利性。
處理英文-法文,專門訓(xùn)練用來翻譯短語,發(fā)現(xiàn)這個(gè)模型能夠很好地捕捉短語表中的語言規(guī)律。基于RNN的encoder-decoder能夠?qū)W習(xí)短語中的連續(xù)空間表示,而這個(gè)短語保留了語義和語法結(jié)構(gòu),也就是說,RNN能夠較好地學(xué)習(xí)文本中的語義和語法結(jié)構(gòu)。(也就是現(xiàn)在普遍的認(rèn)知,RNN能夠處理較長的文本(雖然在這篇論文里面仍只是處理短語結(jié)構(gòu)))
2. RNN encoder-decoder
2.1 初步:RNN
在這里說了什么是RNN,略過
2.2 RNN encoder-decoder
同樣也是encoder處理短語,生成向量,使用隱藏狀態(tài)保存整個(gè)輸入的sequence。decoder使用encoder生成的隱藏狀態(tài)訓(xùn)練生成輸出結(jié)果
。但是與傳統(tǒng)的RNN不同,
和
也以輸入序列
的匯總c作為條件。
decoder在t時(shí)間的隱藏狀態(tài)計(jì)算公式
而下一個(gè)短語的條件分布,使用softmax計(jì)算
最終encoder-decoder兩個(gè)部分整合,訓(xùn)練得到最大的條件log-likelihood,其中是模型參數(shù),每一個(gè)
就是一個(gè)輸入輸出對(duì)
2.3 能夠自適應(yīng)記憶和遺忘的隱藏狀態(tài)
新的模型就有新的隱藏狀態(tài),之前LSTM的隱藏狀態(tài)太過簡單,這里使用GRU,幫助RNN記住長期信息。
由于每個(gè)隱藏單元具有單獨(dú)的重置和更新門,每個(gè)隱藏單元將學(xué)習(xí)捕獲不同時(shí)間尺度上的依賴性。學(xué)習(xí)捕獲短期依賴關(guān)系的那些單元將傾向于重置經(jīng)?;顒?dòng)的門,但那些捕獲長期依賴關(guān)系的那些將具有最活躍的更新門。
總之,換成GRU。
3. 基于統(tǒng)計(jì)的機(jī)器翻譯
在實(shí)踐中,大多數(shù)SMT系統(tǒng)將建模為具有附加特征和相應(yīng)權(quán)重的對(duì)數(shù)線性模型
3.1 使用RNN encoder-decoder評(píng)分短語對(duì)
3.2 相關(guān)方法,神經(jīng)網(wǎng)絡(luò)機(jī)器翻譯
列舉了一系列相關(guān)類似研究,都是使用神經(jīng)網(wǎng)絡(luò)構(gòu)建機(jī)器翻譯系統(tǒng)。
4. 實(shí)驗(yàn)
4.1 數(shù)據(jù)和基線
在WMT'14翻譯任務(wù)的框架內(nèi),可以利用大量資源建立英語/法語SMT系統(tǒng)。雙語語料庫包括Europarl(61M字),新聞評(píng)論(5.5M),UN(421M)和兩個(gè)分別為90M和780M字的爬蟲語料庫。最后兩個(gè)語料庫很嘈雜。
實(shí)驗(yàn)里使用的語料庫很大,是一個(gè)新聞?wù)Z料庫含有712M字。
應(yīng)該關(guān)注給定任務(wù)的最相關(guān)數(shù)據(jù)子集。論文從一個(gè)超過2G字中選取了一個(gè)418M字子集中用于語言建模,并從850M字中選擇348M的子集用于訓(xùn)練RNN encoder-decoder。我們使用測試集newstest2012和2013進(jìn)行數(shù)據(jù)選擇和使用MERT進(jìn)行重量調(diào)整,并使用newstest2014作為我們的測試集。每組有超過7萬個(gè)單詞和一個(gè)參考翻譯。
為了訓(xùn)練神經(jīng)網(wǎng)絡(luò),將源和目標(biāo)詞匯限制為英語和法語最常見的15,000個(gè)單詞。這涵蓋了大約93%的數(shù)據(jù)集。所有詞匯表外的單詞都被映射到一個(gè)特殊的令牌([UNK])。
可以發(fā)現(xiàn)在教程中的復(fù)現(xiàn)很粗糙,2014年的論文中使用了超過2G的數(shù)據(jù)集,現(xiàn)在的運(yùn)算量應(yīng)該更大……
下面讓我們來實(shí)現(xiàn)一下。
5. 模型實(shí)現(xiàn)

前一個(gè)模型的一個(gè)缺點(diǎn)是Decoder試圖將大量信息塞入隱藏狀態(tài)。在解碼時(shí),隱藏狀態(tài)將需要包含關(guān)于整個(gè)源序列的信息,以及到目前為止已經(jīng)解碼的所有token。通過減輕一些信息壓縮,我們可以創(chuàng)建一個(gè)更好的模型!
同時(shí)這里將使用LSTM的進(jìn)化版GRU。
另外從原論文來看,似乎這個(gè)模型復(fù)現(xiàn)是一個(gè)很粗糙的,使用的數(shù)據(jù)依舊是Multi30k。
Multi30K共有30K個(gè)圖片,每個(gè)圖片對(duì)應(yīng)的描述有兩大類,(i)每個(gè)圖片的英語描述和英語描述的德語翻譯,(ii)五個(gè)獨(dú)立的英語描述和德語描述(不是翻譯)。正因?yàn)樗?dú)立的收集不同語言對(duì)圖片的描述,因此可以更好地適用于有噪聲的多模態(tài)內(nèi)容。
這里我想試一下能不能使用WMT'14的數(shù)據(jù)集訓(xùn)練模型。不過還是先用Multi30k來實(shí)現(xiàn)。
5.1 導(dǎo)入庫和數(shù)據(jù)預(yù)處理
和之前的操作一致,導(dǎo)入必須庫,并對(duì)數(shù)據(jù)集進(jìn)行預(yù)處理
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 構(gòu)建模型
數(shù)據(jù)集和之前是一樣的,接下來構(gòu)建模型,這次使用的是GRU作為神經(jīng)網(wǎng)絡(luò)單元,結(jié)構(gòu)上有一定區(qū)別,但是在論文中沒有提及,論文對(duì)模型的描述更加概括。
5.2.1 參數(shù)設(shè)定
這里的GRU是一個(gè)單層,所以不需要n_layers參數(shù)。
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
和上一個(gè)模型類似,三個(gè)部分,encoder、decoder和seq2seq
5.2.2 Seq2Seq
我把里面預(yù)生成的張量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和之前的很類似,因?yàn)槭褂肎RU不會(huì)輸出每個(gè)單元的狀態(tài),因此在返回中只是輸出了隱藏層的狀態(tài)。
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因?yàn)橐彩褂肎RU,減少了信息壓縮,同時(shí)GRU獲取了目標(biāo)token ,上一個(gè)時(shí)間的隱藏狀態(tài)
,上下文向量
。但是在這里要注意的是輸入的初始隱藏狀態(tài)
其實(shí)就是上下文向量,就是說,實(shí)際輸入的是兩個(gè)相同的上下文向量。
- 在實(shí)現(xiàn)的時(shí)候,通過將
和
串聯(lián)傳入GRU,所以輸入的維度應(yīng)該是emb_dim+ hid_dim
- linear層輸入的是
和
串聯(lián),而隱藏狀態(tài)和上下文向量都是
維度相同,所以輸入的維度是emb_dim+hid_dim*2
- forward現(xiàn)在需要一個(gè)上下文參數(shù)。在forward過程中,我們將
和
連接成emb_con,然后輸入GRU,我們將
,
和
連接在一起作為輸出,然后通過線性層提供它以接收我們的預(yù)測,
。
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)
# 構(gòu)建訓(xùn)練循環(huán)和驗(yàn)證循環(huán)
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