B站視頻講解php
文本主要介紹一下如何使用PyTorch復現Seq2Seq(with Attention),實現簡單的機器翻譯任務,請先閱讀論文Neural Machine Translation by Jointly Learning to Align and Translate,以後花上15分鐘閱讀個人這兩篇文章Seq2Seq 與注意力機制,圖解Attention,最後再來看文本,方能達到醍醐灌頂,事半功倍的效果html
數據預處理的代碼其實就是調用各類API,我不但願讀者被這些不過重要的部分分散了注意力,所以這裏我不貼代碼,僅口述一下帶過便可python
以下圖所示,本文使用的是德語→英語數據集,輸入是德語,而且輸入的每一個句子開頭和結尾都帶有特殊的標識符。輸出是英語,而且輸出的每一個句子開頭和結尾也都帶有特殊標識符git
不論是英語仍是德語,每句話長度都是不固定的,因此我對於每一個batch內的句子,將它們的長度經過加<PAD>變得同樣,也就說,一個batch內的句子,長度都是相同的,不一樣batch內的句子長度不必定相同。下圖維度表示分別是[seq_len, batch_size]
github
隨便打印一條數據,看一下數據封裝的形式markdown
在數據預處理的時候,須要將源句子和目標句子分開構建字典,也就是單獨對德語構建一個詞庫,對英語構建一個詞庫網絡
Encoder我是用的單層雙向GRUapp
雙向GRU的隱藏狀態輸出由兩個向量拼接而成,例如 , ......全部時刻的最後一層隱藏狀態就構成了GRU的outputdom
假設這是個m層GRU,那麼最後一個時刻全部層中的隱藏狀態就構成了GRU的final hidden stateside
其中
因此
根據論文,或者你看了個人圖解Attention這篇文章就會知道,咱們須要的是hidden的最後一層輸出(包括正向和反向),所以咱們能夠經過hidden[-2,:,:]
和hidden[-1,:,:]
取出最後一層的hidden states,將它們拼接起來記做
最後一個細節之處在於,
的維度是[batch_size, en_hid_dim*2]
,即使是沒有Attention機制,將
做爲Decoder的初始隱藏狀態也不對,由於維度不匹配,Decoder的初始隱藏狀態是三維的,而如今咱們的
是二維的,所以須要將
的維度轉爲三維,而且還要調整各個維度上的值。首先我經過一個全鏈接神經網絡,將
的維度變爲[batch_size, dec_hid_dim]
Encoder的細節就這麼多,下面直接上代碼,個人代碼風格是,註釋在上,代碼在下
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim)
self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)
self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
''' src = [src_len, batch_size] '''
src = src.transpose(0, 1) # src = [batch_size, src_len]
embedded = self.dropout(self.embedding(src)).transpose(0, 1) # embedded = [src_len, batch_size, emb_dim]
# enc_output = [src_len, batch_size, hid_dim * num_directions]
# enc_hidden = [n_layers * num_directions, batch_size, hid_dim]
enc_output, enc_hidden = self.rnn(embedded) # if h_0 is not give, it will be set 0 acquiescently
# enc_hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
# enc_output are always from the last layer
# enc_hidden [-2, :, : ] is the last of the forwards RNN
# enc_hidden [-1, :, : ] is the last of the backwards RNN
# initial decoder hidden is final hidden state of the forwards and backwards
# encoder RNNs fed through a linear layer
# s = [batch_size, dec_hid_dim]
s = torch.tanh(self.fc(torch.cat((enc_hidden[-2,:,:], enc_hidden[-1,:,:]), dim = 1)))
return enc_output, s
複製代碼
attention無非就是三個公式
其中
指的就是Encoder中的變量s
,
指的就是Encoder中的變量enc_output
,
其實就是一個簡單的全鏈接神經網絡
咱們能夠從最後一個公式反推各個變量的維度是什麼,或者維度有什麼要求
首先
的維度應該是[batch_size, src_len]
,這是毋庸置疑的,那麼
的維度也應該是[batch_size, src_len]
,或者
是個三維的,可是某個維度值爲1,能夠經過squeeze()
變成兩維的。這裏咱們先假設
的維度是[batch_size, src_len, 1]
,等會兒我再解釋爲何要這樣假設
繼續往上推,變量
的維度就應該是[?, 1]
,?
表示我暫時不知道它的值應該是多少。
的維度應該是[batch_size, src_len, ?]
如今已知
的維度是[batch_size, src_len, enc_hid_dim*2]
,
目前的維度是[batch_size, dec_hid_dim]
,這兩個變量須要作拼接,送入全鏈接神經網絡,所以咱們首先須要將
的維度變成[batch_size, src_len, dec_hid_dim]
,拼接以後的維度就變成[batch_size, src_len, enc_hid_dim*2+dec_hid_dim]
,因而
這個函數的輸入輸出值也就有了
attn = nn.Linear(enc_hid_dim*2+dec_hid_dim, ?)
複製代碼
到此爲止,除了?
部分的值不清楚,其它全部維度都推導出來了。如今咱們回過頭思考一下?
設置成多少,好像其實並無任何限制,因此咱們能夠設置?
爲任何值(在代碼中我設置?
爲dec_hid_dim
)
Attention細節就這麼多,下面給出代碼
class Attention(nn.Module):
def __init__(self, enc_hid_dim, dec_hid_dim):
super().__init__()
self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim, bias=False)
self.v = nn.Linear(dec_hid_dim, 1, bias = False)
def forward(self, s, enc_output):
# s = [batch_size, dec_hid_dim]
# enc_output = [src_len, batch_size, enc_hid_dim * 2]
batch_size = enc_output.shape[1]
src_len = enc_output.shape[0]
# repeat decoder hidden state src_len times
# s = [batch_size, src_len, dec_hid_dim]
# enc_output = [batch_size, src_len, enc_hid_dim * 2]
s = s.unsqueeze(1).repeat(1, src_len, 1)
enc_output = enc_output.transpose(0, 1)
# energy = [batch_size, src_len, dec_hid_dim]
energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim = 2)))
# attention = [batch_size, src_len]
attention = self.v(energy).squeeze(2)
return F.softmax(attention, dim=1)
複製代碼
我調換一下順序,先講Seq2Seq,再講Decoder的部分
傳統Seq2Seq是直接將句子中每一個詞接二連三輸入Decoder進行訓練,而引入Attention機制以後,我須要可以人爲控制一個詞一個詞進行輸入(由於輸入每一個詞到Decoder,須要再作一些運算),因此在代碼中會看到我使用了for循環,循環trg_len-1次(開頭的<SOS>我手動輸入,因此循環少一次)
而且訓練過程當中我使用了一種叫作Teacher Forcing的機制,保證訓練速度的同時增長魯棒性,若是不瞭解Teacher Forcing能夠看個人這篇文章
思考一下for循環中應該要作哪些事?首先要將變量傳入Decoder,因爲Attention的計算是在Decoder的內部進行的,因此我須要將dec_input
、s
、enc_output
這三個變量傳入Decoder,Decoder會返回dec_output
以及新的s
。以後根據機率對dec_output
作Teacher Forcing便可
Seq2Seq細節就這麼多,下面給出代碼
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio = 0.5):
# src = [src_len, batch_size]
# trg = [trg_len, batch_size]
# teacher_forcing_ratio is probability to use teacher forcing
batch_size = src.shape[1]
trg_len = trg.shape[0]
trg_vocab_size = self.decoder.output_dim
# tensor to store decoder outputs
outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
# enc_output is all hidden states of the input sequence, back and forwards
# s is the final forward and backward hidden states, passed through a linear layer
enc_output, s = self.encoder(src)
# first input to the decoder is the <sos> tokens
dec_input = trg[0,:]
for t in range(1, trg_len):
# insert dec_input token embedding, previous hidden state and all encoder hidden states
# receive output tensor (predictions) and new hidden state
dec_output, s = self.decoder(dec_input, s, enc_output)
# place predictions in a tensor holding predictions for each token
outputs[t] = dec_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 = dec_output.argmax(1)
# if teacher forcing, use actual next token as next input
# if not, use predicted token
dec_input = trg[t] if teacher_force else top1
return outputs
複製代碼
Decoder我用的是單向單層GRU
Decoder部分實際上也就是三個公式
指的是Encoder中的變量enc_output
,
指的是將dec_input
通過WordEmbedding後獲得的結果,
函數實際上就是爲了轉換維度,由於須要的輸出是TRG_VOCAB_SIZE
大小。其中有個細節,GRU的參數只有兩個,一個輸入,一個隱藏層輸入,可是上面的公式有三個變量,因此咱們應該選一個做爲隱藏層輸入,另外兩個"整合"一下,做爲輸入
咱們從第一個公式正推各個變量的維度是什麼
首先在Encoder中最開始先調用一次Attention,獲得權重
,它的維度是[batch_size, src_len]
,而
的維度是[src_len, batch_size, enc_hid_dim*2]
,它倆要相乘,同時應該保留batch_size
這個維度,因此應該先將
擴展一維,而後調換一下
維度的順序,以後再按照batch相乘(即同一個batch內的矩陣相乘)
a = a.unsqueeze(1) # [batch_size, 1, src_len]
H = H.transpose(0, 1) # [batch_size, src_len, enc_hid_dim*2]
c = torch.bmm(a, h) # [batch_size, 1, enc_hid_dim*2]
複製代碼
前面也說了,因爲GRU不須要三個變量,因此須要將
和
整合一下,
實際上就是Seq2Seq類中的dec_input
變量,它的維度是[batch_size]
,所以先將
擴展一個維度,再經過WordEmbedding,這樣他就變成[batch_size, 1, emb_dim]
。最後對
和
進行concat
y = y.unsqueeze(1) # [batch_size, 1]
emb_y = self.emb(y) # [batch_size, 1, emb_dim]
rnn_input = torch.cat((emb_y, c), dim=2) # [batch_size, 1, emb_dim+enc_hid_dim*2]
複製代碼
的維度是[batch_size, dec_hid_dim]
,因此應該先將其拓展一個維度
rnn_input = rnn_input.transpose(0, 1) # [1, batch_size, emb_dim+enc_hid_dim*2]
s = s.unsqueeze(1) # [batch_size, 1, dec_hid_dim]
# dec_output = [1, batch_size, dec_hid_dim]
# dec_hidden = [1, batch_size, dec_hid_dim] = s (new, is not s previously)
dec_output, dec_hidden = self.rnn(rnn_input, s)
複製代碼
最後一個公式,須要將三個變量所有拼接在一塊兒,而後經過一個全鏈接神經網絡,獲得最終的預測。咱們先分析下這個三個變量的維度,
的維度是[batch_size, 1, emb_dim]
,
的維度是[batch_size, 1, enc_hid_dim]
,
的維度是[1, batch_size, dec_hid_dim]
,所以咱們能夠像下面這樣把他們所有拼接起來
emd_y = emb_y.squeeze(1) # [batch_size, emb_dim]
c = w.squeeze(1) # [batch_size, enc_hid_dim*2]
s = s.squeeze(0) # [batch_size, dec_hid_dim]
fc_input = torch.cat((emb_y, c, s), dim=1) # [batch_size, enc_hid_dim*2+dec_hid_dim+emb_hid]
複製代碼
以上就是Decoder部分的細節,下面給出代碼(上面的那些只是示例代碼,和下面代碼變量名可能不同)
class Decoder(nn.Module):
def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
super().__init__()
self.output_dim = output_dim
self.attention = attention
self.embedding = nn.Embedding(output_dim, emb_dim)
self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)
self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, dec_input, s, enc_output):
# dec_input = [batch_size]
# s = [batch_size, dec_hid_dim]
# enc_output = [src_len, batch_size, enc_hid_dim * 2]
dec_input = dec_input.unsqueeze(1) # dec_input = [batch_size, 1]
embedded = self.dropout(self.embedding(dec_input)).transpose(0, 1) # embedded = [1, batch_size, emb_dim]
# a = [batch_size, 1, src_len]
a = self.attention(s, enc_output).unsqueeze(1)
# enc_output = [batch_size, src_len, enc_hid_dim * 2]
enc_output = enc_output.transpose(0, 1)
# c = [1, batch_size, enc_hid_dim * 2]
c = torch.bmm(a, enc_output).transpose(0, 1)
# rnn_input = [1, batch_size, (enc_hid_dim * 2) + emb_dim]
rnn_input = torch.cat((embedded, c), dim = 2)
# dec_output = [src_len(=1), batch_size, dec_hid_dim]
# dec_hidden = [n_layers * num_directions, batch_size, dec_hid_dim]
dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0))
# embedded = [batch_size, emb_dim]
# dec_output = [batch_size, dec_hid_dim]
# c = [batch_size, enc_hid_dim * 2]
embedded = embedded.squeeze(0)
dec_output = dec_output.squeeze(0)
c = c.squeeze(0)
# pred = [batch_size, output_dim]
pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1))
return pred, dec_hidden.squeeze(0)
複製代碼
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
ENC_HID_DIM = 512
DEC_HID_DIM = 512
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
attn = Attention(ENC_HID_DIM, DEC_HID_DIM)
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, DEC_DROPOUT, attn)
model = Seq2Seq(enc, dec, device).to(device)
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]
criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
複製代碼
倒數第二行CrossEntropyLoss()
中的參數不多見,ignore_index=TRG_PAD_IDX
,這個參數的做用是忽略某一類別,不計算其loss,可是要注意,忽略的是真實值中的類別,例以下面的代碼,真實值的類別都是1,而預測值所有預測認爲是2(下標從0開始),同時loss function設置忽略第一類的loss,此時會打印出0
label = torch.tensor([1, 1, 1])
pred = torch.tensor([[0.1, 0.2, 0.6], [0.2, 0.1, 0.8], [0.1, 0.1, 0.9]])
loss_fn = nn.CrossEntropyLoss(ignore_index=1)
print(loss_fn(pred, label).item()) # 0
複製代碼
若是設置loss function忽略第二類,此時loss並不會爲0
label = torch.tensor([1, 1, 1])
pred = torch.tensor([[0.1, 0.2, 0.6], [0.2, 0.1, 0.8], [0.1, 0.1, 0.9]])
loss_fn = nn.CrossEntropyLoss(ignore_index=2)
print(loss_fn(pred, label).item()) # 1.359844
複製代碼
最後給出完整代碼連接(須要科學的力量) Github項目地址:nlp-tutorial