Github-jcjohnson/torch-rnn代碼詳解
zoerywzhou@gmail.com
http://www.cnblogs.com/swje/
作者:Zhouwan
2016-3-18
聲明
1)本文僅供學術交流,非商用。所以每一部分具體的參考資料並沒有詳細對應。如果某部分不小心侵犯了大家的利益,還望海涵,並聯系博主刪除。
2)本人才疏學淺,整理總結的時候難免出錯,還望各位前輩不吝指正,謝謝。
請聯系:zoerywzhou@gmail.com 或13813017783@163.com
本研究課題系本人本科畢業論文,具體學習計划見http://www.cnblogs.com/swje/p/5068069.html,
源文件及參考文獻如下
torch-rnn代碼@Github:https://github.com/jcjohnson/torch-rnn
學習體會
1、torch-rnn 提供了一個高性能、可再用的RNN和LSTM模塊,使用這些模塊對字符級別的語言建模和char-rnn是類似的。RNN和LSTM模塊僅僅依賴於torch和nn,所以可以很容易地整合到現有的項目中。相比於char-rnn,torch-rnn的速度快了1.9倍,並且節約了七倍的內存。
- 數據預處理:在訓練前,需要用腳本scripts/preprocess.py對數據進行預處理,這將會生成一個包含數據的預處理版本的HDF5 文件和 JSON 文件。 比如生成了
my_data.h5
和my_data.json
兩個文件。
- 訓練模型:預處理之后,需要用腳本train.lua 來訓練模型。這一步是最慢的。
可以運行下面的代碼來訓練:
th train.lua -input_h5 my_data.h5 -input_json my_data.json
以上代碼將會讀取存儲在my_data.h5
和 my_data.json兩個文件中的數據,
運行一段時間后,將會生成檢查點文件checkpoint,文件命名類似cv/checkpoint_1000.t7。
你可以通過參數設置改變RNN模型類型、隱藏層大小和 RNN層數,選擇使用CUDA在GPU模式下運行或在CPU模式下運行,也可以選擇是否使用OpenCL,還有其他參數設置,參考這里。
- 從模型中抽樣:訓練完一個模型之后,你可以通過使用腳本sample.lua從文本中抽樣來生成新的文本。運行以下代碼:th sample.lua -checkpoint cv/checkpoint_10000.t7 -length 2000,將會從前一步載入訓練好的檢查點集 ,從中抽取2000個字符,並將結果打印到控制台上。
你可以通過參數設置,選擇使用CUDA在GPU模式下運行或者在CPU模式下運行,也可以選擇是否使用OpenCL,還有其他參數設置,參考這里。
4、為了用基准問題測試torch-rnn和char-rnn,我們對莎士比亞散文集訓練LSTM語言模型,RNN層數和RNN大小分別設置為1、2、3層和64、128、256和512,並將minibatch大小設為50,序列長度設為50,dropout設為0。對於同一大小RNN模型的兩次實驗中,在前100次訓練迭代過程中,我們記錄下向前和向后傳播的時間和GPU的內存使用情況,並使用這些測量值來計算平均時間和內存使用情況。所有的基准測試程序數值都是在一個配置有 Intel i7-4790k CPU, 32 GB 主存和帶有一個 Titan X GPU的機器上運行的。
從實驗結果出可以看出,torch-rnn在任何模型大小下都比char-rnn運行速度快,小模型的加速比更大一些。對於有128個隱藏單元的單層LSMT來說,加速了1.9倍;對於較大的模型,我們達到大約1.4倍的加速。
從節約GPU內存方面來看,torch-rnn在所有的模型大小上都勝過char-rnn,但是,對於較大一些的模型節約內存更多一些,例如:對於有512個隱藏單元的模型來說,torch-rnn比char-rnn少用了七倍內存。
一句話總結:torch-rnn相比於char-rnn,更節約內存,模型越大越節約;且訓練時間(前向傳播&反向傳播的時間)更短,模型越小加速越快!
模塊分析
1、VanillaRNN:
rnn = nn.VanillaRNN(D, H)
VanillaRNN 是 torch nn.Module的一個子類,用雙曲正切函數實施一個vanilla 遞歸神經網絡。它將一個D維輸入向量的序列轉換為一個H維隱藏狀態向量的序列;在長度為T的序列、大小為N的minibatch的模型上運行,在每一次前向傳播中,序列長度和minibatch的大小可以改變。
暫且忽略minibatch,vanilla RNN使用下面的遞歸關系式 從前一個隱藏狀態 h[t - 1](of shape (
H,)
) 和當前的輸入向量 x[t](of shape (
D,)
) 來計算下一個隱藏層的狀態向量 h[t]:
h[t] = tanh(Wh h[t- 1] + Wx x[t] + b)
其中, Wx是一個連接輸入層和隱藏層的矩陣,
Wh是一個連接隱藏層和隱藏層的矩陣,b 是一個偏項。其中權重Wx 和Wh存儲在大小為
(D + H, H)
的張量rnn.weight 中,而偏差項 b 存儲在為H維的張量 rnn.bias中。
你可以用兩種不同的方式來使用 VanillaRNN
實例:
h = rnn:forward({h0, x}) grad_h0, grad_x = unpack(rnn:backward({h0, x}, grad_h)) h = rnn:forward(x) grad_x = rnn:backward(x, grad_h)
h0
是隱藏層初始狀態,大小為(N,H), x
是輸入向量序列,大小為(N, T, D)
。在每個時間步長,輸出 h 是隱藏狀態的序列,
大小是 (N, T, H)
。在一些應用中,比如圖像字幕,隱藏層的初始狀態可能是一些其他網絡計算出來的輸出結果。
默認情況下,如果前向傳播中沒有提供 h0 ,那么隱藏層的初始狀態就設為0。這種方法可能對情感分析等應用有幫助,這類應用往往需要用一個RNN處理很多獨立的序列。
如果沒有提供h0,且實例變量rnn.remember_states的值被設置為真,那么首次調用rnn:forward時將會將隱藏層的初始狀態設為0;在隨后調用rnn:forward時,從先前的調用得到的最后一個隱藏狀態將被作為下一個隱藏層的初始狀態。這種方法常用在語言模型中,我們想用很長的序列(可能無限長)訓練網絡,並且使用沿時間截斷反向傳播的方法(truncated back-propagation through time)計算梯度。通過調用rnn:resetStates()使模型忘記它的隱藏狀態,然后下次調用rnn:forward時會將h0的值初始化為0。
這種方法在unit test for VanillaRNN.lua中有所運用。
作為一個實現,我們直接執行 :backward 以同時計算關於輸入的梯度並積累關於權重的梯度,因為這兩個操作涉及很多相同的計算。我們將 :updateGradInput
和:accGradparameters重寫進調用 :backward中,然后直接調用:backward而不是先調用:updateGradInput
再調用:accGradparameters,
這樣就避免了計算兩次同樣的事情。
文件VanillaRNN.lua是獨立的,除了torch 和 nn,對其他模塊沒有依賴性。
2、LSTM:
lstm = nn.LSTM(D, H)
LSTM( Long Short-Term Memory的簡稱)是一個別致的遞歸神經網絡類型,比vanilla RNNs常用的多。類似於上面提到的vanilla RNNs的性質,LSTM是一個執行LSTM的torch nn.Module 的子類。它將一個D維輸入向量的序列轉換為一個H維隱藏狀態向量的序列;在長度為T的序列、大小為N的minibatch的模型上運行,在每一次前向傳播中,序列長度和minibatch的大小可以改變。
LSTM和vanilla RNN的不同之處在於,在每一個時間步長上它都跟蹤隱藏狀態和cell 狀態。暫且忽略minibatch,vanilla RNN使用下面的遞歸關系式 從前一個隱藏狀態 h[t - 1](of shape (
H,)
) 、前一個cell狀態 c[t-1] 和當前的輸入向量 x[t](of shape (
D,)
) 來計算下一個隱藏狀態向量 h[t]和cell狀態向量 c[t]:
ai[t] = Wxi x[t] + Whi h[t - 1] + bi # Matrix / vector multiplication af[t] = Wxf x[t] + Whf h[t - 1] + bf # Matrix / vector multiplication ao[t] = Wxo x[t] + Who h[t - 1] + bo # Matrix / vector multiplication ag[t] = Wxg x[t] + Whg h[t - 1] + bg # Matrix / vector multiplication i[t] = sigmoid(ai[t]) # Input gate f[t] = sigmoid(af[t]) # Forget gate o[t] = sigmoid(ao[t]) # Output gate g[t] = tanh(ag[t]) # Proposed update c[t] = f[t] * c[t - 1] + i[t] * g[t] # Elementwise multiplication of vectors h[t] = o[t] * tanh(c[t]) # Elementwise multiplication of vectors
輸入層到隱藏層的矩陣 Wxi
, Wxf
, Wxo和
Wxg
以及隱藏層到隱藏層的矩陣 Whi
, Whf
, Who和
Whg
都被存儲在大小為(D + H, 4 * H)的單個張量lstm.weight
中,偏差向量 bi
, bf
, bo
和 bg
被存儲在大小為(4 * H,)的單個張量 lstm.bias
中。
你可以用三種不同的方式來使用 LSTM
實例:
h = lstm:forward({c0, h0, x}) grad_c0, grad_h0, grad_x = unpack(lstm:backward({c0, h0, x}, grad_h)) h = lstm:forward({h0, x}) grad_h0, grad_x = unpack(lstm:backward({h0, x}, grad_h)) h = lstm:forward(x) grad_x = lstm:backward(x, grad_h)
在所有情況下,c0
是初始cell 狀態,大小為(N,H), h0
是初始隱藏狀態,大小為(N,H),x 是輸入向量序列,大小為(N, T, D)
, h 是輸出隱藏狀態的序列,
大小是 (N, T, H)
。
如果沒有提供初始cell狀態或者初始隱藏狀態,那么它們的初始狀態就默認設為0。
如果沒有提供初始 cell 狀態或者初始隱藏狀態,且實例變量 lstm.remember_states 的值被設置為真,那么首次調用 lstm:forward 時將會將隱藏層的初始狀態和cell 狀態設為0;在隨后調用lstm:forward 中,將會把隱藏狀態和cell狀態的初始值設為前一次調用時得到的最終的隱藏層和cell的狀態,這和 VanillaRNN 很類似
。你可以通過調用lstm:resetStates() 重置它們的cell 狀態和隱藏狀態,然后下次調用 lstm:forward時會將初始隱藏狀態和cell 狀態設為 0。
這種方法在unit test for LSTM.lua中有所運用。
作為一個實現,我們直接執行 :backward 以同時計算關於輸入的梯度並積累關於權重的梯度,因為這兩個操作涉及很多相同的計算。我們將 :updateGradInput
和 :accGradparameters 重寫進調用 :backward
中,然后直接調用:backward而不是先調用:updateGradInput
再調用:accGradparameters,
這樣就避免了計算兩次同樣的事情。
文件LSTM.lua 是獨立的,除了torch 和 nn,對其他模塊沒有依賴性。
3、LanguageModel module:
torch-rnn提供了一個LanguageModel module,用於對字符級的語言建模。
model = nn.LanguageModel(kwargs)
LanguageModel 使用以上模塊 通過dropout 正則化來實現多層遞歸神經網絡語言模型。因為 LSTM
和 VanillaRNN
是 nn.Module
子類,我們可以通過在容器 nn.Sequential
中簡單地堆積多個實例來實現多層遞歸神經網絡。
kwargs
是一張表,包含以下關鍵詞:
-
idx_to_token
: 一張給定語言模型詞匯的表,將整型ids 映射為 字符串tokensmodel_type
: "lstm" 或 "rnn"wordvec_size
: 單詞向量嵌入的維度rnn_size
: RNN的隱藏狀態大小num_layers
: 使用的RNN層數dropout
: 介於0和1之間的數字,指定經過每個RNN層之后的dropout長度