雙向LSTM是傳統LSTM的擴展,可以提高序列分類問題的模型性能。
在輸入序列的所有時間步長可用的問題中,雙向LSTM在輸入序列上訓練兩個而不是一個LSTM。輸入序列中的第一個是原樣的,第二個是輸入序列的反轉副本。這可以爲網絡提供額外的上下文,並導致更快,甚至更充分的學習問題。
在本教程中,您將發現如何使用Keras深度學習庫在Python中開發用於序列分類的雙向LSTM。
完成本教程後,您將知道:
如何開發一個小的、可設計的、可配置的序列分類問題。
如何爲序列分類開發LSTM和雙向LSTM。
如何在雙向LSTM中應用中比較合併模式的性能。
讓我們開始吧
![](http://static.javashuo.com/static/loading.gif)
如何開發帶有Keras的Python序列分類的雙向LSTM(照片版權所有Cristiano Medeiros Dalbem)
概觀
本教程分爲6部分; 他們是:
- 雙向LSTM
- 序列分類問題
- LSTM序列分類
- 雙向LSTM序列分類
- 將LSTM和雙向LSTM作比較
- 比較雙向LSTM不同的合併模式
環境
本教程假定您已安裝Python SciPy環境。您可以使用Python 2或3與此示例。
本教程假定您使用了TensorFlow(v1.1.0 +)或Theano(v0.9 +)後臺,安裝了Keras(v2.0.4 +)。
本教程還假定您已經安裝了scikit-learn,Pandas,NumPy和Matplotlib。
如果您需要幫助設置您的Python環境,請參閱這篇文章:
雙向LSTM
雙向循環神經網絡(RNNs)的概念很簡單。
它涉及複製網絡中的第一個循環層,使得現在有兩層並排,然後將輸入序列原樣提供給第一層的輸入,並將輸入序列的反向副本提供給第二層。
爲了克服常規RNN的侷限[…],我們提出了可以使用特定時間框架的過去和未來的所有可用輸入信息進行訓練的雙向循環神經網絡(BRNN)。
…
這個想法是將負責正時間方向(正向狀態)的部分和負時間方向的一部分(後向狀態)分割成正常RNN的狀態神經元,
– Mike Schuster和Kuldip K. Paliwal,Bidirectional Recurrent Neural Networks,1997
這種方法對於LSTM的遞歸神經網絡有重大影響。
雙向提供序列的使用最初在語音識別領域是合理的,因爲有證據表明整個話語的上下文用於解釋所說的內容而不是線性解釋。
依靠未來的知識視乎違背了因果關係。我們怎麼能把我們對所聽到的事情的理解建立在還沒有聽到的事情上呢?然而,人類就是這樣做的。從未來的角度看,我們聽到的聲音、單詞甚至是句子一開始都毫無意義。我們必須記住的是,真正在線的任務之間的區別即:每次輸入後都需要一個輸出和某些輸入段結束時才需要輸出。
– Alex Graves和Jurgen Schmidhuber,Framangular Phoneme Classification with Bidirectional LSTM and Other Neural Network Architectures,2005
使用雙向LSTM可能不會對所有序列預測問題都有意義,但是可以對適合的領域提供更好的結果。
我們發現雙向網絡比單向網絡顯着更有效
– Alex Graves和Jurgen Schmidhuber,Framangular Phoneme Classification with Bidirectional LSTM and Other Neural Network Architectures,2005
要知道,輸入序列中的時間步仍然一次處理一個,只是同時在網絡的從兩個方向通過輸入序列而已。
Keras中的雙向LSTM
Keras通過雙向層包裝器支持雙向LSTM 。
這個包裝器需要一個循環層(例如第一個LSTM層)作爲參數。
它還允許你指定合併模式,也就是在傳遞到下一個層之前,如何組合前向和後退輸出。選項有:
- ‘ sum’:輸出相加。
- ‘ mul’:輸出相乘。
- ‘ concat’:輸出連接在一起(默認),將輸出數量提高到下一層的兩倍。
- ‘ ave’:輸出的平均值。
默認模式是連接,這是雙向LSTM研究中經常使用的方法。
序列分類問題
我們將定義一個簡單的序列分類問題來探索雙向LSTM。
該問題被定義爲0和1之間的隨機值序列。該序列作爲每個時間步長每個數字的問題的輸入。
二進制標籤(0或1)與每個輸入相關聯。輸出值全爲0.一旦序列中的輸入值的累積和超過閾值,則輸出值從0翻轉爲1。
使用序列長度的1/4。
例如,下面是10個輸入時間步長(X)的序列:
|
0.63144003 0.29414551 0.91587952 0.95189228 0.32195638 0.60742236 0.83895793 0.18023048 0.84762691 0.29165514 |
相應的分類輸出(y)爲:
我們可以在Python中實現。
第一步是生成一系列隨機值。我們可以使用隨機模塊中的random()函數。
1 |
# create a sequence of random numbers in [0,1] |
3 |
X = array([random() for _ in range ( 10 )]) |
我們可以將閾值定義爲輸入序列長度的四分之一。
1 |
# calculate cut-off value to change class values |
可以使用cumsum()NumPy函數計算輸入序列的累積和。該函數返回累積和值的序列,例如:
1 |
pos1, pos1+pos2, pos1+pos2+pos3, … |
然後,我們可以計算輸出序列,以確定每個累積和值是否超過閾值。
1 |
# determine the class outcome for each item in cumulative sequence |
3 |
y = array([ 0 if x < limit else 1 for x in cumsum(X)]) |
下面的名爲get_sequence()的函數將所有這些函數一起繪製,作爲輸入序列的長度,並返回一個新的問題情況的X和Y組件。
01 |
from random import random |
03 |
from numpy import array |
05 |
from numpy import cumsum |
09 |
# create a sequence classification instance |
11 |
def get_sequence(n_timesteps): |
13 |
# create a sequence of random numbers in [0,1] |
15 |
X = array([random() for _ in range (n_timesteps)]) |
17 |
# calculate cut-off value to change class values |
19 |
limit = n_timesteps / 4.0 |
21 |
# determine the class outcome for each item in cumulative sequence |
23 |
y = array([ 0 if x < limit else 1 for x in cumsum(X)]) |
我們可以用新的10個時間步序來測試這個函數,如下所示:
1 |
X, y = get_sequence( 10 ) |
運行示例首先打印生成的輸入序列,然後輸出匹配的輸出序列。
|
[ 0.22228819 0.26882207 0.069623 0.91477783 0.02095862 0.71322527 0.90159654 0.65000306 0.88845226 0.4037031 ] [0 0 0 0 0 0 1 1 1 1] |
LSTM用於序列分類
我們可以通過開發用於序列分類問題的傳統LSTM來開始。
首先,我們必須更新get_sequence()函數,以將輸入和輸出序列重新形成3維以滿足LSTM的期望。預期的結構具有尺寸[樣本,時間步長,特徵]。分類問題具有1個樣本(例如一個序列),可配置的時間步長數量和每個時間步長的一個特徵。
分類問題具有1個樣本(例如一個序列),可配置的時間步長數量和每個時間步長的一個特徵。
因此,我們可以按如下順序重新整理。
1 |
# reshape input and output data to be suitable for LSTMs |
3 |
X = X.reshape( 1 , n_timesteps, 1 ) |
5 |
y = y.reshape( 1 , n_timesteps, 1 ) |
更新的get_sequence()函數如下所示。
01 |
# create a sequence classification instance |
02 |
def get_sequence(n_timesteps): |
03 |
# create a sequence of random numbers in [0,1] |
04 |
X = array([random() for _ in range (n_timesteps)]) |
05 |
# calculate cut-off value to change class values |
06 |
limit = n_timesteps / 4.0 |
07 |
# determine the class outcome for each item in cumulative sequence |
08 |
y = array([ 0 if x < limit else 1 for x in cumsum(X)]) |
09 |
# reshape input and output data to be suitable for LSTMs |
10 |
X = X.reshape( 1 , n_timesteps, 1 ) |
11 |
y = y.reshape( 1 , n_timesteps, 1 ) |
我們將把序列定義爲具有10個時間步長。
接下來,我們可以爲問題定義一個LSTM。輸入層將具有10個時間步長,其中1個特徵爲一組,input_shape =(10,1)。我們將把序列定義爲具有10個時間步長。
第一個隱藏層將具有20個存儲單元,輸出層將是完全連接的層,每個時間步長輸出一個值。在輸出端使用S形激活函數來預測二進制值。
在輸出層周圍使用TimeDistributed包裝層,以便在提供的完整序列作爲輸入時,可以預測每個時間步長的一個值。這要求LSTM隱藏層返回一個值序列(每個時間步長一個),而不是整個輸入序列的單個值。
最後,因爲這是二進制分類問題,所以使用二進制日誌丟失(Keras中的binary_crossentropy)。使用有效的ADAM優化算法找到權重,並計算每個時期報告的精度度量。
5 |
model.add(LSTM( 20 , input_shape = ( 10 , 1 ), return_sequences = True )) |
7 |
model.add(TimeDistributed(Dense( 1 , activation = 'sigmoid' ))) |
9 |
model. compile (loss = 'binary_crossentropy' , optimizer = 'adam' , metrics = [ 'acc' ]) |
LSTM將接受1000次訓練。將爲每次訓練生成一個新的隨機輸入序列,以使網絡適合。這樣可以確保模型不會記住單個序列,也可以通過推廣解決方案來解決所有可能的隨機輸入序列。
03 |
for epoch in range ( 1000 ): |
05 |
# generate new random sequence |
07 |
X,y = get_sequence(n_timesteps) |
09 |
# fit model for one epoch on this sequence |
11 |
model.fit(X, y, epochs = 1 , batch_size = 1 , verbose = 2 ) |
一旦被訓練,網絡將被評估另一個隨機序列。然後將預測與預期輸出序列進行比較,以提供系統技能的具體示例。
3 |
X,y = get_sequence(n_timesteps) |
5 |
yhat = model.predict_classes(X, verbose = 0 ) |
7 |
for i in range (n_timesteps): |
9 |
print ( 'Expected:' , y[ 0 , i], 'Predicted' , yhat[ 0 , i]) |
完整的示例如下所示。
01 |
from random import random |
03 |
from numpy import array |
05 |
from numpy import cumsum |
07 |
from keras.models import Sequential |
09 |
from keras.layers import LSTM |
11 |
from keras.layers import Dense |
13 |
from keras.layers import TimeDistributed |
17 |
# create a sequence classification instance |
19 |
def get_sequence(n_timesteps): |
21 |
# create a sequence of random numbers in [0,1] |
23 |
X = array([random() for _ in range (n_timesteps)]) |
25 |
# calculate cut-off value to change class values |
27 |
limit = n_timesteps / 4.0 |
29 |
# determine the class outcome for each item in cumulative sequence |
31 |
y = array([ 0 if x < limit else 1 for x in cumsum(X)]) |
33 |
# reshape input and output data to be suitable for LSTMs |
35 |
X = X.reshape( 1 , n_timesteps, 1 ) |
37 |
y = y.reshape( 1 , n_timesteps, 1 ) |
43 |
# define problem properties |
51 |
model.add(LSTM( 20 , input_shape = (n_timesteps, 1 ), return_sequences = True )) |
53 |
model.add(TimeDistributed(Dense( 1 , activation = 'sigmoid' ))) |
55 |
model. compile (loss = 'binary_crossentropy' , optimizer = 'adam' , metrics = [ 'acc' ]) |
59 |
for epoch in range ( 1000 ): |
61 |
# generate new random sequence |
63 |
X,y = get_sequence(n_timesteps) |
65 |
# fit model for one epoch on this sequence |
67 |
model.fit(X, y, epochs = 1 , batch_size = 1 , verbose = 2 ) |
71 |
X,y = get_sequence(n_timesteps) |
73 |
yhat = model.predict_classes(X, verbose = 0 ) |
75 |
for i in range (n_timesteps): |
77 |
print ( 'Expected:' , y[ 0 , i], 'Predicted' , yhat[ 0 , i]) |
運行示例打印每個時期隨機序列的日誌丟失和分類精度。
這提供了一個清晰的想法,模型如何推廣到序列分類問題的解決方案。運行示例打印每個時期隨機序列的日誌丟失和分類精度。
我們可以看到,該模型表現良好,達到90%和100%準確度的最終精度。不完美,但對我們的目的有好處。
將新隨機序列的預測與預期值進行比較,顯示出具有單個錯誤的大多數正確的結果。
05 |
0s - loss: 0.2039 - acc: 0.9000 |
09 |
0s - loss: 0.2985 - acc: 0.9000 |
13 |
0s - loss: 0.1219 - acc: 1.0000 |
17 |
0s - loss: 0.2031 - acc: 0.9000 |
21 |
0s - loss: 0.1698 - acc: 0.9000 |
23 |
Expected: [ 0 ] Predicted [ 0 ] |
25 |
Expected: [ 0 ] Predicted [ 0 ] |
27 |
Expected: [ 0 ] Predicted [ 0 ] |
29 |
Expected: [ 0 ] Predicted [ 0 ] |
31 |
Expected: [ 0 ] Predicted [ 0 ] |
33 |
Expected: [ 0 ] Predicted [ 1 ] |
35 |
Expected: [ 1 ] Predicted [ 1 ] |
37 |
Expected: [ 1 ] Predicted [ 1 ] |
39 |
Expected: [ 1 ] Predicted [ 1 ] |
41 |
Expected: [ 1 ] Predicted [ 1 ] |
雙向LSTM用於序列分類
現在我們知道如何爲序列分類問題開發LSTM,我們可以擴展示例來演示雙向LSTM。
我們可以通過用雙向層包裝LSTM隱藏層來實現,如下所示:
1 |
model.add(Bidirectional(LSTM( 20 , return_sequences = True ), input_shape = (n_timesteps, 1 ))) |
這將創建隱藏層的兩個副本,一個適合輸入序列,一個在輸入序列的反轉副本上。默認情況下,這些LSTM的輸出值將被連接。
這意味着,代替時分佈層接收到20個輸出的10個時間步長,它現在將接收10個20(20個單位+20個單位)輸出的10個時間步長。
完整的示例如下所示。
01 |
from random import random |
02 |
from numpy import array |
03 |
from numpy import cumsum |
04 |
from keras.models import Sequential |
05 |
from keras.layers import LSTM |
06 |
from keras.layers import Dense |
07 |
from keras.layers import TimeDistributed |
08 |
from keras.layers import Bidirectional |
10 |
# create a sequence classification instance |
11 |
def get_sequence(n_timesteps): |
12 |
# create a sequence of random numbers in [0,1] |
13 |
X = array([random() for _ in range (n_timesteps)]) |
14 |
# calculate cut-off value to change class values |
15 |
limit = n_timesteps / 4.0 |
16 |
# determine the class outcome for each item in cumulative sequence |
17 |
y = array([ 0 if x < limit else 1 for x in cumsum(X)]) |
18 |
# reshape input and output data to be suitable for LSTMs |
19 |
X = X.reshape( 1 , n_timesteps, 1 ) |
20 |
y = y.reshape( 1 , n_timesteps, 1 ) |
23 |
# define problem properties |
27 |
model.add(Bidirectional(LSTM( 20 , return_sequences = True ), input_shape = (n_timesteps, 1 ))) |
28 |
model.add(TimeDistributed(Dense( 1 , activation = 'sigmoid' ))) |
29 |
model. compile (loss = 'binary_crossentropy' , optimizer = 'adam' , metrics = [ 'acc' ]) |
31 |
for epoch in range ( 1000 ): |
32 |
# generate new random sequence |
33 |
X,y = get_sequence(n_timesteps) |
34 |
# fit model for one epoch on this sequence |
35 |
model.fit(X, y, epochs = 1 , batch_size = 1 , verbose = 2 ) |
37 |
X,y = get_sequence(n_timesteps) |
38 |
yhat = model.predict_classes(X, verbose = 0 ) |
39 |
for i in range (n_timesteps): |
40 |
print ( 'Expected:' , y[ 0 , i], 'Predicted' , yhat[ 0 , i]) |
運行示例,我們看到與上一個示例類似的輸出。
使用雙向LSTM具有允許LSTM更快地學習問題的效果。
從運行結束時來看,這並不太明顯,但隨着時間的推移,也越來越明顯。
05 |
0s - loss: 0.0967 - acc: 0.9000 |
09 |
0s - loss: 0.0865 - acc: 1.0000 |
13 |
0s - loss: 0.0905 - acc: 0.9000 |
17 |
0s - loss: 0.2460 - acc: 0.9000 |
21 |
0s - loss: 0.1458 - acc: 0.9000 |
23 |
Expected: [ 0 ] Predicted [ 0 ] |
25 |
Expected: [ 0 ] Predicted [ 0 ] |
27 |
Expected: [ 0 ] Predicted [ 0 ] |
29 |
Expected: [ 0 ] Predicted [ 0 ] |
31 |
Expected: [ 0 ] Predicted [ 0 ] |
33 |
Expected: [ 1 ] Predicted [ 1 ] |
35 |
Expected: [ 1 ] Predicted [ 1 ] |
37 |
Expected: [ 1 ] Predicted [ 1 ] |
39 |
Expected: [ 1 ] Predicted [ 1 ] |
41 |
Expected: [ 1 ] Predicted [ 1 ] |
將LSTM與雙向LSTM進行比較
在這個例子中,我們將比較訓練中的傳統的LSTM與雙向LSTM的模型的性能。
我們將調整實驗,使模型只能訓練250個時期。這樣,我們可以清楚地瞭解每個模型的學習方式如何展開,以及雙向LSTM的學習行爲如何不同。
我們將比較三種不同的型號; 具體如下:
- LSTM(按原樣)
- 帶有輸入序列LSTM(例如,您可以通過將LSTM圖層的「go_backwards」參數設置爲「True」來實現)
- 雙向LSTM
這種比較將有助於表明雙向LSTM實際上可以增加一些東西,而不僅僅是帶有逆轉輸入序列。
我們將定義一個函數來創建和返回具有前向或後向輸入序列的LSTM,如下所示:
01 |
def get_lstm_model(n_timesteps, backwards): |
05 |
model.add(LSTM( 20 , input_shape = (n_timesteps, 1 ), return_sequences = True , go_backwards = backwards)) |
07 |
model.add(TimeDistributed(Dense( 1 , activation = 'sigmoid' ))) |
09 |
model. compile (loss = 'binary_crossentropy' , optimizer = 'adam' ) |
我們可以爲雙向LSTM開發類似的功能,其中可以將合併模式指定爲參數。可以通過將合併模式設置爲值’concat’來指定連接的默認值。
01 |
def get_bi_lstm_model(n_timesteps, mode): |
05 |
model.add(Bidirectional(LSTM( 20 , return_sequences = True ), input_shape = (n_timesteps, 1 ), merge_mode = mode)) |
07 |
model.add(TimeDistributed(Dense( 1 , activation = 'sigmoid' ))) |
09 |
model. compile (loss = 'binary_crossentropy' , optimizer = 'adam' ) |
最後,我們定義一個函數來擬合模型,並且檢索和存儲每個訓練時期的損失,然後在模型擬合之後返回收集的損失值的列表。這樣我們可以從每個模型配置中繪製日誌丟失並進行比較。
01 |
def train_model(model, n_timesteps): |
07 |
# generate new random sequence |
09 |
X,y = get_sequence(n_timesteps) |
11 |
# fit model for one epoch on this sequence |
13 |
hist = model.fit(X, y, epochs = 1 , batch_size = 1 , verbose = 0 ) |
15 |
loss.append(hist.history[ 'loss' ][ 0 ]) |
將這一切放在一起,下面列出了完整的例子。
首先創建一個傳統的LSTM,並對日誌損失值進行匹配。這與使用反向輸入序列的LSTM重複,最後是具有級聯合並的LSTM。
001 |
from random import random |
003 |
from numpy import array |
005 |
from numpy import cumsum |
007 |
from matplotlib import pyplot |
009 |
from pandas import DataFrame |
011 |
from keras.models import Sequential |
013 |
from keras.layers import LSTM |
015 |
from keras.layers import Dense |
017 |
from keras.layers import TimeDistributed |
019 |
from keras.layers import Bidirectional |
023 |
# create a sequence classification instance |
025 |
def get_sequence(n_timesteps): |
027 |
# create a sequence of random numbers in [0,1] |
029 |
X = array([random() for _ in range (n_timesteps)]) |
031 |
# calculate cut-off value to change class values |
033 |
limit = n_timesteps / 4.0 |
035 |
# determine the class outcome for each item in cumulative sequence |
037 |
y = array([ 0 if x < limit else 1 for x in cumsum(X)]) |
039 |
# reshape input and output data to be suitable for LSTMs |
041 |
X = X.reshape( 1 , n_timesteps, 1 ) |
043 |
y = y.reshape( 1 , n_timesteps, 1 ) |
049 |
def get_lstm_model(n_timesteps, backwards): |
053 |
model.add(LSTM( 20 , input_shape = (n_timesteps, 1 ), return_sequences = True , go_backwards = backwards)) |
055 |
model.add(TimeDistributed(Dense( 1 , activation = 'sigmoid' ))) |
057 |
model. compile (loss = 'binary_crossentropy' , optimizer = 'adam' ) |