本文不會介紹LSTM的原理,具體可看如下兩篇文章
1、舉個栗子
在介紹LSTM各種參數含義之前我們還是需要先用一個例子(參考LSTM神經網絡輸入輸出究竟是怎樣的?Scofield的回答)來理解LSTM。
Recurrent NNs,一般看的最多的圖是這個:

rnn但是這個圖對初學者相當不太友好。個人認為,目前所有的關於描述RecurrentNNs的圖都畫得不好,不夠明確,里面的細節丟失了。(事實上里面一個"A"僅僅表示了一層的變換,具體如下圖所示。)

非常清楚,這是很多初學者不能理解RecurrentNNs的根本原因,即在於Recurrent NNs是在time_step上的拓展的這一特性。MLP好理解,CNN也好理解,但Recurrent NNs,就是無法搞清楚里面的拓撲結構,跟MLP聯系不上。
先看看MLP,很好理解,就是一張網絡清楚地顯示了張量流向。
general MLP是這樣的拓撲:

mlp然后CNN也好理解,跟MLP無差若干,只是權重運算由\(*\)變為\(\otimes\)。CNN是這樣的拓撲:

但RecurrentNNs的拓撲發生了一個很大的改動,即一個MLP會在time_step這個維度上進行延伸,每個時序都會有input。
所以RecurrentNNs的結構圖應該這樣畫,在理解上才會更清晰些,對比MLP,也一目了然。(為了簡約,只畫了4個time-steps )……

如上圖所示,
- 每個時序\(t\) 的輸入\(T_i^t\),也就是說一次time_step輸入一個input tensor。
- 隱狀態\(h_i^t\)也就代表了一張MLP的hidden layer的一個cell,可以看到中間黃色圈圈就表示隱藏層.
- 輸出\(O_i^t\)理解無異,可以看到每個時序的輸出節點數是等於隱藏節點數的。注意,紅色的箭頭指向僅僅表示數據流動方向,並不是表示隱藏層之間相連。
再結合一個操作實例說明。如果我們有一條長文本,我們給句子事先分割好句子,並且進行tokenize, dictionarize,接着再由look up table 查找到embedding,將token由embedding表示,再對應到上圖的輸入。流程如下:
- step1, raw text (語料庫如下):
接觸LSTM模型不久,簡單看了一些相關的論文,還沒有動手實現過。然而至今仍然想不通LSTM神經網絡究竟是怎么工作的。…… - step2, tokenize (中文得分詞):
- sentence1: 接觸 LSTM 模型 不久 ,簡單 看了 一些 相關的 論文 , 還 沒有 動手 實現過 。
- sentence2: 然而 至今 仍然 想不通 LSTM 神經網絡 究竟是 怎么 工作的。
- ……
- step3, dictionarize:
- sentence1: 1 34 21 98 10 23 9 23
- sentence2: 17 12 21 12 8 10 13 79 31 44 9 23
- ……
- step4, padding every sentence to fixed length:
- sentence1: 1 34 21 98 10 23 9 23 0 0 0 0 0
- sentence2: 17 12 21 12 8 10 13 79 31 44 9 23 0
- ……
- step5, mapping token to an embeddings:
- sentence1:\(\left[\begin{array}{cccc}{0.341} & {0.133} & {0.011} & {\cdots} \\ {0.435} & {0.081} & {0.501} & {\cdots} \\ {0.013} & {0.958} & {0.121} & {\ldots} \\ {\cdots} & {\cdots} & {\cdots} & {\cdots}\end{array}\right]\),每一列代表一個詞向量,詞向量維度自行確定(假設一個單詞由長度為100的向量表示);矩陣列數固定為time_step length。
- sentence2: ...
- ……
- step6, feed into RNNs as input:
假設 一個RNN的time_step 確定為\(l\),則padded sentence length(step5中矩陣列數)固定為 \(l\)。一次RNNs的run只處理一條sentence。每個sentence的每個token的embedding對應了每個時序 的輸入 。一次RNNs的run,連續地將整個sentence處理完。簡單理解就是每次傳入RNN的句子長度為\(l\),換句話就是RNN橫向長度為\(l\) - step7, get output:
看圖,每個time_step都是可以輸出當前時序\(t\)的隱狀態\(h_i^t\);但整體RNN的輸出\(O_i^t\)是在最后一個time_step\(t=l\)時獲取,才是完整的最終結果。 - step8, further processing with the output:
我們可以將output根據分類任務或回歸擬合任務的不同,分別進一步處理。比如,傳給cross_entropy&softmax進行分類……或者獲取每個time_step對應的隱狀態\(h_i^t\),做seq2seq 網絡……或者搞創新……
2、Pytorch源代碼參數理解
2.1 LSTM模型參數含義
通過源代碼中可以看到nn.LSTM繼承自nn.RNNBase,其初始化函數定義如下
class RNNBase(Module):
...
def __init__(self, mode, input_size, hidden_size,
num_layers=1, bias=True, batch_first=False,
dropout=0., bidirectional=False):
我們需要關注的參數以及其含義解釋如下:
- input_size – 輸入數據的大小,也就是前面例子中每個單詞向量的長度
- hidden_size – 隱藏層的大小(即隱藏層節點數量),輸出向量的維度等於隱藏節點數
- num_layers – recurrent layer的數量,默認等於1。
- bias – If False, then the layer does not use bias weights b_ih and b_hh. Default: True
- batch_first – 默認為False,也就是說官方不推薦我們把batch放在第一維,這個CNN有點不同,此時輸入輸出的各個維度含義為 (seq_length,batch,feature)。當然如果你想和CNN一樣把batch放在第一維,可將該參數設置為True。
- dropout – 如果非0,就在除了最后一層的其它層都插入Dropout層,默認為0。
- bidirectional – If True, becomes a bidirectional LSTM. Default: False
2.2 輸入數據
下面介紹一下輸入數據的維度要求(batch_first=False):
輸入數據需要按如下形式傳入 input, (h_0,c_0)
- input: 輸入數據,即上面例子中的一個句子(或者一個batch的句子),其維度形狀為 (seq_len, batch, input_size)
- seq_len: 句子長度,即單詞數量,這個是需要固定的。當然假如你的一個句子中只有2個單詞,但是要求輸入10個單詞,這個時候可以用
torch.nn.utils.rnn.pack_padded_sequence()或者torch.nn.utils.rnn.pack_sequence()來對句子進行填充或者截斷。 - batch:就是你一次傳入的句子的數量
- input_size: 每個單詞向量的長度,這個必須和你前面定義的網絡結構保持一致
- seq_len: 句子長度,即單詞數量,這個是需要固定的。當然假如你的一個句子中只有2個單詞,但是要求輸入10個單詞,這個時候可以用
- h_0:維度形狀為 (num_layers * num_directions, batch, hidden_size):
- 結合下圖應該比較好理解第一個參數的含義num_layers * num_directions, 即LSTM的層數乘以方向數量。這個方向數量是由前面介紹的
bidirectional決定,如果為False,則等於1;反之等於2。 - batch:同上
- hidden_size: 隱藏層節點數
- 結合下圖應該比較好理解第一個參數的含義num_layers * num_directions, 即LSTM的層數乘以方向數量。這個方向數量是由前面介紹的
- c_0: 維度形狀為 (num_layers * num_directions, batch, hidden_size),各參數含義和h_0類似。
當然,如果你沒有傳入(h_0, c_0),那么這兩個參數會默認設置為0。
2.3 輸出數據
- output: 維度和輸入數據類似,只不過最后的feature部分會有點不同,即 (seq_len, batch, num_directions * hidden_size)
- 這個輸出tensor包含了LSTM模型最后一層每個time step的輸出特征,比如說LSTM有兩層,那么最后輸出的是\([h^1_0,h^1_1,...,h^1_l]\),表示第二層LSTM每個time step對應的輸出。
- 另外如果前面你對輸入數據使用了
torch.nn.utils.rnn.PackedSequence,那么輸出也會做同樣的操作編程packed sequence。 - 對於unpacked情況,我們可以對輸出做如下處理來對方向作分離
output.view(seq_len, batch, num_directions, hidden_size), 其中前向和后向分別用0和1表示Similarly, the directions can be separated in the packed case.
- h_n:(num_layers * num_directions, batch, hidden_size),
- 只會輸出最后個time step的隱狀態結果(如下圖所示)。
- Like output, the layers can be separated using h_n.view(num_layers, num_directions, batch, hidden_size) and similarly for c_n.
- c_n :(num_layers * num_directions, batch, hidden_size),只會輸出最后個time step的cell狀態結果(如下圖所示)。

3、 代碼示例
rnn = nn.LSTM(10, 20, 2) # 一個單詞向量長度為10,隱藏層節點數為20,LSTM有2層
input = torch.randn(5, 3, 10) # 輸入數據由3個句子組成,每個句子由5個單詞組成,單詞向量長度為10
h0 = torch.randn(2, 3, 20) # 2:LSTM層數*方向 3:batch 20: 隱藏層節點數
c0 = torch.randn(2, 3, 20) # 同上
output, (hn, cn) = rnn(input, (h0, c0))
print(output.shape, hn.shape, cn.shape)
>>> torch.Size([5, 3, 20]) torch.Size([2, 3, 20]) torch.Size([2, 3, 20])
參考:
