2019-CS224n-Assignment4

我的原文:2019-CS224n-Assignment4

這一次的內容甚至可以作爲一個項目了,我最終得到BLEU是22.66。

點擊 這裏 下載實驗指導文檔,這裏 下載實驗的預備代碼

RNN和神經機器翻譯

機器翻譯是指,給定一個源句子(比如西班牙語),輸出一個目標句子(比如英語)。本次作業中要實現的是一個帶注意力機制的Seq2Seq神經模型,用於構建神經機器翻譯(NMT)系統。首先我們來看NMT系統的訓練過程,它用到了雙向LSTM作爲編碼器(encoder)和單向LSTM作爲解碼器(decoder)。

mark

給定長度爲m的源語言句子(source),經過嵌入層,得到輸入序列 x1,x2,...,xmRe×1x_1, x_2, ..., x_m \in R^{e \times 1},e是詞向量大小。經過雙向Encoder後,得到前向(→)和反向(←)LSTM的隱藏層和神經元狀態,將兩個方向的狀態連接起來得到時間步 ii 的隱藏狀態 hiench_i^{enc}ciencc_i^{enc}

mark

接着我們使用一個線性層來初始化Decoder的初始隱藏、神經元的狀態:

mark

Decoder的時間步 tt 的輸入爲 yˉt\bar{y}_t ,它由目標語言句子 yty_t 和上一神經元的輸出 ot1o_{t-1} 經過連接得到,o0o_0 是0向量,所以 $ \bar{y}_t \in R^{(e + h) \times 1}$

mark

接着我們使用 htdech^{dec}_t 來計算在 h0enc,h1enc,...,hmench^{enc}_0, h^{enc}_1, ..., h^{enc}_m 的乘積注意力(multiplicative attention):

mark

然後將注意力 αt\alpha_t 和解碼器的隱藏狀態 htdech^{dec}_t 連接,送入線性層,得到 combined-output 向量 oto_t

mark

這樣以來,目標詞的概率分佈則爲:

mark

使用交叉熵做目標函數即可。

關鍵在於每個向量的維度,把維度搞清楚,整個過程就清晰了。

實現

在寫完代碼後,需要訓練很久才能得到結果,這是因爲翻譯系統比較複雜,所以建議在GPU上跑,可以試試CoLab,而我是在實驗室的TeslaP100上跑的,所以比較快。

問題(b)

model_embeddings.py的__init__函數:

self.source = nn.Embedding(len(vocab.src), embed_size, padding_idx=src_pad_token_idx)
self.target = nn.Embedding(len(vocab.tgt), embed_size, padding_idx=tgt_pad_token_idx)

注意一定要把padding_idx參數填進去

問題©

nmt_model.py的__init__函數:

self.encoder = nn.LSTM(embed_size, hidden_size, bias=True, bidirectional=True)
self.decoder = nn.LSTMCell(embed_size + hidden_size, hidden_size, bias=True)
self.h_projection = nn.Linear(2*hidden_size, hidden_size , bias=False)
self.c_projection = nn.Linear(2*hidden_size, hidden_size, bias=False)
self.att_projection = nn.Linear(2*hidden_size, hidden_size, bias=False)
self.combined_output_projection = nn.Linear(3*hidden_size, hidden_size, bias=False)
self.target_vocab_projection = nn.Linear(hidden_size, len(vocab.tgt), bias=False)
self.dropout = nn.Dropout(p=dropout_rate)

特別要注意各個維度的變化

原始的數據是詞索引,經過embedding,每個詞變成了大小爲 embed_size 的向量,所以encoder的輸入大小爲 embed_size ,隱藏層大小爲 hidden_size

decoder的輸入是神經元輸出和目標語言句子的嵌入向量,所以輸入大小爲 embed_size + hidden_size

h_projection、c_projection的作用是將encoder的隱藏層狀態降維,所以輸入大小是 2*hidden_size,輸出大小是hidden_size

att_projection的作用也是降維,以便後續與decoder的隱藏層狀態做矩陣乘法

combined_output_projection的作用也將解碼輸出降維,輸入是注意力向量和隱藏層狀態連接得到的向量,大小爲3*hidden_size,並保持輸出大小爲 hidden_size

target_vocab_projection是將輸出投影到詞庫的詞中去

問題(d)

nmt_model.py 的encode函數:

X = self.model_embeddings.source(source_padded) # (src_len, b, e)
X = pack_padded_sequence(X, source_lengths)
enc_hiddens, (last_hidden, last_cell) = self.encoder(X) # hidden/cell = (2, b, h)
enc_hiddens = pad_packed_sequence(enc_hiddens)[0] # (src_len, b, 2*h)
enc_hiddens = enc_hiddens.permute(1, 0, 2) # (b, src_len, 2*h)

init_decoder_hidden = self.h_projection(torch.cat((last_hidden[0], last_hidden[1]), dim=1))
init_decoder_cell = self.c_projection(torch.cat((last_cell[0], last_cell[1]), dim=1))
dec_init_state = (init_decoder_hidden, init_decoder_cell) # (b, h)

再次說明,一定要注意各個向量的維度。維度搞清楚了,過程就明瞭了。

這裏用到了 padpack 兩個概念。

pad :填充。將幾個大小不一樣的Tensor以最長的tensor長度爲標準進行填充,一般是填充 0

pack:打包。將幾個 tensor打包成一個,返回一個PackedSequence 對象。

經過pack後,RNN可以對不同長度的樣本數據進行小批量訓練,否則就只能一個一個樣本進行訓練了。

torch.cat 可以將兩個tensor拼接成一個

torch.Tensor.permute 可以變換矩陣的維度比如 shape=(1,2,3) -> shape=(3,2,1)

問題(e)

nmt_model.py的decode函數:

enc_hiddens_proj = self.att_projection(enc_hiddens) # (b, src_len, h)
Y = self.model_embeddings.target(target_padded) # (tgt_len, b, e)
for Y_t in torch.split(Y, 1):
    Y_t = torch.squeeze(Y_t, dim=0) # (b, e)
    Ybar_t = torch.cat((Y_t, o_prev), dim=1) # (b, e + h)
    dec_state, o_t, e_t = self.step(Ybar_t, dec_state, enc_hiddens, enc_hiddens_proj, enc_masks)
    o_prev = o_t
    combined_outputs.append(o_t)
combined_outputs = torch.stack(combined_outputs, dim=0) # (tgt_len, b, h)

還是強調一句,一定要注意向量的維度。

torch.stack 可以將一個list裏的長度一樣的tensor堆疊成一個tensor

torch.squeeze 可以將tensor裏大小爲1的維度給刪掉,比如shape=(1,2,3) -> shape=(2,3)

問題(f)

nmt_model.py的step函數:

首先計算注意力係數

dec_state = self.decoder(Ybar_t, dec_state)
dec_hidden, dec_cell = dec_state # (b, h)
e_t = torch.bmm(enc_hiddens_proj, torch.unsqueeze(dec_hidden, dim=2)) # (b, src_len, 1)
e_t = torch.squeeze(e_t, dim=2) # (b, src_len)

接着計算注意力和combined output,並輸出

alpha_t = torch.unsqueeze(F.softmax(e_t, dim=1), dim=1)
a_t = torch.squeeze(torch.bmm(alpha_t, enc_hiddens), dim=1) # (b, 2*h)
u_t = torch.cat((a_t, dec_hidden), dim=1) # (b, 3*h)
v_t = self.combined_output_projection(u_t) # (b, h)
O_t = self.dropout(torch.tanh(v_t))

一定要使用 torch.bmm小批量 數據進行矩陣乘法運算,不能用通常意義上的矩陣乘法

至此所有代碼都寫完了,運行

python sanity_check.py 1d
python sanity_check.py 1e
python sanity_check.py 1f

進行初步測試

完成後運行下述代碼進行冒煙測試:

sh run.sh train_local

經過10或20次迭代後程序沒崩,那就可以正式進行測試了:

sh run.sh train

參考資料

[1] CS224n: Natural Language Processing with Deep Learning, 2019-03-21.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章