開發Bidirectional LSTM模型的簡單教程 | 博士帶你學LSTM

AMiner學術頭條發表於2019-02-27

LSTM是一種時間遞迴神經網路,適合於處理和預測時間序列中間隔和延遲相對較長的重要事件。在自然語言處理、語言識別等一系列的應用上都取得了很好的效果。

《Long Short Term Memory Networks with Python》是澳大利亞機器學習專家Jason Brownlee的著作,裡面詳細介紹了LSTM模型的原理和使用。

該書總共分為十四個章節,具體如下:

第一章:什麼是LSTMs?

第二章:怎麼樣訓練LSTMs?

第三章:怎麼樣準備LSTMs的資料?

第四章:怎麼樣在Keras中開發LSTMs?

第五章:序列預測建模

第六章:如何開發一個Vanilla LSTM模型?

第七章:怎麼樣開發Stacked LSTMs?

第八章:開發CNN LSTM模型(本期內容)

第九章:開發Encoder-Decoder LSTMs(本期內容)

第十章:開發Bidirectional LSTMs(下週一發布)

第十一章:開發生成LSTMs

第十二章:診斷和除錯LSTMs

第十三章:怎麼樣用LSTMs做預測?

第十四章:更新LSTMs模型

本文的作者對此書進行了翻譯整理之後,分享給大家,本文是第十期內容。

第一期內容為:一萬字純乾貨|機器學習博士手把手教你入門LSTM(附程式碼資料)

第二期內容為:乾貨推薦|如何基於時間的反向傳播演算法來訓練LSTMs?

第三期內容為:乾貨推薦|如何準備用於LSTM模型的資料並進行序列預測?(附程式碼)

第四期內容為:機器學習博士帶你入門|一文學會如何在Keras中開發LSTMs(附程式碼)

第五期內容為:初學者如何避免在序列預測問題中遇到的陷阱?

第六期內容為:如何開發和評估Vanilla LSTM模型?

第七期內容為:博士帶你學LSTM|怎麼樣開發Stacked LSTMs?(附程式碼)

第八期內容為:博士帶你學LSTM|手把手教你開發CNN LSTM模型,並應用在Keras中(附程式碼)

第九期內容為:博士帶你學LSTM|開發Encoder-Decoder LSTM模型的簡單教程(附程式碼)

我們還將繼續推出一系列的文章來介紹裡面的詳細內容,和大家一起來共同學習。

10.0 前言

10.0.1 課程目標

本課程的目標是學習怎麼樣開發Bidirectional LSTM模型。完成本課程之後,你將會學習到:

  • Bidirectional LSTM模型的結構和怎麼樣在Keras中實現它;

  • 積累和問題的;

  • 怎麼樣為積累和問題開發一個Bidirectional LSTM模型。

10.0.2 課程概覽

本課程分類為7個部分,它們是:

  1. Bidirectional LSTM;

  2. 積累和預測問題;

  3. 定義和編譯模型;

  4. 擬合模型;

  5. 評估模型;

  6. 用模型做預測;

  7. 完成例子。

讓我們開始吧!

10.1 Bidirectional LSTM

10.1.1 結構

我們已經看到了Encoder-Decoder LSTM的介紹中討論的LSTMs輸入序列的順序的好處。

我們對源句子中顛倒詞的改程式度感到驚訝。

— Sequence to Sequence Learning with Neural Networks, 2014.

Bidirectional LSTMs專注於透過輸入和輸出時間步長在向前和向後兩個方向上獲得最大的輸入序列的問題。在實踐中,該架構涉及複製網路中的第一個遞迴層,使得現在有兩個並排的層,然後提供輸入序列,作為輸入到第一層並且提供輸入序列到第二層的反向副本。這種方法是在不久前發展起來的一種用於改善迴圈神經網路(RNNs)效能的一般方法。

為了克服常規RNN的侷限性...我們提出了一個雙向遞迴神經網路(BRNN),可以使用所有可用的輸入資訊在過去和未來的特定時間幀進行訓練。...我們的方法是將一個規則的RNN狀態神經元分裂成兩個部分,一部分負責正時間方向(正向狀態),另外一個部分負責負時間方向(後向狀態)。

— Bidirectional Recurrent Neural Networks, 1997.

該方法以及被應用於LSTM迴圈神經網路。向前和向後提供整個序列是基於假設整個序列是可用的假設的。在使用向量化輸入時,在這個實踐中通常是一個要求。然而,它可能會引起哲學上的關注,其中理想的時間步長是按順序和及時(just-in-time)提供的。在語音識別領域中,雙向地提供輸入序列是合理的,因為有證據表明,在人類中,整個話語的上下文被用來解釋所說的話而不是一個線性解釋。

...依賴於乍一看是違反因果關係的未來知識。我們如何才能理解我們所聽到的關於海沒有說過的話呢?然而,人類的廳總就是這樣做的。在未來的語境中,聲音、詞語乃至於整個句子都是毫無意義的。我們必須記住的是,任務之間的區別是真的線上的——在每個輸入之後需要一個輸出,以及在某些輸入段的末尾只需要輸出。

— Framewise Phoneme Classification with Bidirectional LSTM and Other Neural Network Architectures, 2005.

雖然Bidirectional LSTMs被開發用於語音識別,但是使用雙向輸入序列是序列預測的主要因素,而LSTMs是提升模型效能的一種方法。

開發Bidirectional LSTM模型的簡單教程 | 博士帶你學LSTM 圖 10.1 Bidirectional LSTM結構

10.1.2 實現

Keras中的LSTM層使得你指定輸入序列的方向變得可能。可以透過設定go backwards引數為True(預設的為False)來完成。

model = Sequential() 

model.add(LSTM(..., input_shape=(...), go_backwards=True)) 

... 

表 10.1 具有反向輸入序列的Vanilla LSTM模型的例子

Bidirectional LSTMs是這個能力的一個小的步驟。具體來說,Keras所支援的透過Bidirectional層包裹的Bidirectional LSTM,該雙向層wrapper實質上合併來自兩個並行LSTMs的輸出,一個具有向前處理的輸入和一個向後處理的輸出。這個wrapper將一個遞迴層(例如,第一LSTM隱藏層)作為一個引數。

  1. model = Sequential()

    model.add(Bidirectional(LSTM(...), input_shape=(...))) 

  2. ... 

表 10.2 包裹在LSTM層中的Bidirectional層的例子

Bidirectional wrapper層還允許您指定合併模式;即在向前傳遞到下一層之前應該如何組合前向和向後輸出。選項是:

  • “sum”:將輸出加在一起;

  • “mul”:將輸出乘在一起;

  • “concat”:預設情況下,將輸出連線在一起,為下一層提供兩杯的輸出數。

  • “ave”:輸出的平均值。

預設模式是級聯,這在雙向LSTM研究t經常使用的方法。一般來說,測試您的問題中的每個合併模式可能是個好主意,看看是否可以改進級聯的預設選項。

10.2 計算和預測問題

我們將會定義一個簡單的序列分類問題來探索Bidirectional LSTMs叫做計算和預測問題。本節本分為下面的幾個部分:

  1. 計算和;

  2. 序列生成;

  3. 生成多序列.

10.2.1 計算和

該問題被定義為0和1之間的隨機值序列。這個序列作為輸入的問題,每個時間提供一個數字一次。二進位制標籤(0或者1)與每個輸入相關聯。輸出均值為0。一旦序列中的輸入值的累計和超過閾值,則輸出值從0翻轉到1.

使用閾值的四分之一}{4}$)的序列長度。例如,下面是10個輸入時間步長(X)的序列:

  1. 0.63144003 0.29414551 0.91587952 0.95189228 0.32195638 0.60742236 0.83895793 0.18023048 0.84762691 0.29165514

表 10.3 隨機真值的輸入序列的例子

相應的分類輸出(y)會是:

  1. 0 0 0 1 1 1 1 1 1 1

表 10.4 輸出計算和值的輸出的例子

我們將解決這個問題,以充分利用Bidirectional LSTM的結構。輸出序列將在整個輸入序列被喂進模型之後產生。

技術上,這意味著這是一個序列到序列的預測問題,需要一個多對多的預測模型。輸入和輸出序列具有相同的時間步長(長度)的情況下也是如此。

開發Bidirectional LSTM模型的簡單教程 | 博士帶你學LSTM 圖 10.2 用一個many-to-many的模型計算和預測問題

10.2.2 序列生成

我們可以用Python來實現。第一步是生成隨機數值的序列。我們可以使用隨機元件中的random()函式。

# create a sequence of random numbers in [0,1] 

X = array([random() for _ in range(10)])

表 10.5 生成隨機真值的輸入序列的例子

我們可以定義輸入序列長度的四分之一為閾值。

  1. # calculate cut-off value to change class values 

  2. limit = 10/4.0

表 10.6 計算累積和閾值的例子

輸入序列的累積和可以的計算可以使用NumPy的cumsum()函式。此函式返回一個累積和值序列,例如:

pos1, pos1+pos2, pos1+pos2+pos3, ...

表 10.7 計算累積和輸出序列的例子

然後,我們可以計算每個累積和值是否超過閾值的輸出序列。

# determine the class outcome for each item in cumulative sequence 

y = array([0 if x < limit else 1 for x in cumsum(X)])

表 10.8 實現累積和閾值計算的例子

下面的函式,叫做get_sequence(),將所有這些都結合在一起,將序列的長度作為輸入,並返回新問題案例的X和y

# create a sequence classification instance 

def get_sequence(n_timesteps): 

    # create a sequence of random numbers in [0,1] 

    X = array([random() for _ in range(n_timesteps)]

    # calculate cut-off value to change class values

    limit = n_timesteps/4.0

    # determine the class outcome for each item in cumulative sequence 

    y = array([0 if x < limit else 1 for x in cumsum(X)]) 

    return X, y

表 10.9 產生一個隨機輸入和輸出序列的函式

我們可以用一個新的10-step序列測試這個函式,如下所示:

from random import random 

from numpy import array 

from numpy import cumsum

# create a cumulative sum sequence

def get_sequence(n_timesteps):

    # create a sequence of random numbers in [0,1] 

    X = array([random() for _ in range(n_timesteps)]) 

    # calculate cut-off value to change class values 

    limit = n_timesteps/4.0 

    # determine the class outcome for each item in cumulative sequence 

    y = array([0 if x < limit else 1 for x in cumsum(X)]) 

    return X, y

X, y = get_sequence(10) 

print(X) 

print(y)

表 10.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]

表 10.11 生成一個隨機輸入和輸出序列的輸出的例子

10.2.3 生成多個序列

我們可以定義一個函式來建立多個序列。下面名為getsequences()的函式使用序列的數量來生成,同時每個序列的時間步長作為引數並使用getsequence()來生成序列。一旦特定數量的序列被生成,輸入和輸出序列的列表被變型為三維的,適合於LSTMs一起使用。

# create multiple samples of cumulative sum sequences 

def get_sequences(n_sequences, n_timesteps): 

    seqX, seqY = list(), list() 

    # create and store sequences 

    for _ in range(n_sequences): 

        X, y = get_sequence(n_timesteps) 

        seqX.append(X) 

        seqY.append(y) 

    # reshape input and output for lstm

    seqX = array(seqX).reshape(n_sequences, n_timesteps, 1)

    seqY = array(seqY).reshape(n_sequences, n_timesteps, 1) 

    return seqX, seqY

表 10.12 生成序列的函式和適應LSTM模型格式

我們現在準備好開始為整個問題開發一個Bidirectional LSTM模型。

10.3 定義和編譯模型

首先,我們定義了一個複雜的問題。我們會限制輸入時間步長的數目在一個合適的大小;在這種情況下,10,這意味著輸入形狀將是具有1個特徵的10個時間步長。

# define problem

n_timesteps = 10

表 10.13 配置問題的例子

下面,我們需要定義隱藏在一個Bidirectional層中的隱藏的LSTM層。我們將在LSTM隱藏層中使用50個儲存單元。Bidirectional wrapper將加倍,建立一個平行於第一層的第二層,也有50個儲存單元。

model.add(Bidirectional(LSTM(50, return_sequences=True), input_shape=(n_timesteps, 1)))

表 10.14 新增Bidirectional輸入層的例子

將從前向和後向LSTM隱藏層中的每一個輸出50個向量的向量(Bidirectional wrapper 層的預設合併方法)以建立100個元素的向量輸出。這是作為輸入到Dense層的輸入,該Dense層被包裹在TimeDistributed層中。這具有重用Dense層的權重以建立每個輸出時間步長的效果。

model.add(TimeDistributed(Dense(1, activation= sigmoid )))

表 10.15 新增TimeDistributed輸出層的例子

Bidirectional LSTM層返回給包裹著的Dense層序列。這具有向每個輸出時間步長提供Dense層的一個級聯的100個元素向量作為輸入的效果。如果不使用TimeDistributed wrapper,一個100個元素的向量將會被提供給Dense層,從該Dense層將需要輸出10個時間步長的分類。對於模型來說,這似乎是一個更具有挑戰性的問題。

把這些放在一起,模型定義如下。在Dense輸出層sigmoid被用作啟用函式,並且二項log損失被最佳化,因為每個輸出時間步長是一個累積和的閾值是否超過的二分類問題。在模型訓練和評價過程中,採用Adam梯度下降法來最佳化權值,並計算分類精度。

# define LSTM model = Sequential()

model.add(Bidirectional(LSTM(50, return_sequences=True), input_shape=(n_timesteps, 1))) 

model.add(TimeDistributed(Dense(1, activation= 'sigmoid' )))

model.compile(loss= 'binary_crossentropy' , optimizer= 'adam' , metrics=[ 'acc' ])

print(model.summary())

表 10.16 定義和編譯Bidirectional LSTM模型的例子

執行這個程式碼列印編譯模型的總結。我們可以確認Dense層由100個權重(加上偏置),一個用於從Bidirectional包裹的LSTM隱藏層提供的100個元素級聯向量中的每個單元。

_________________________________________________________________

Layer (type)                 Output Shape              Param #  

=================================================================

bidirectional_1 (Bidirection (None, 10, 100)           20800     

_________________________________________________________________

time_distributed_1 (TimeDist (None, 10, 1)             101       

=================================================================

Total params: 20,901

Trainable params: 20,901

Non-trainable params: 0

_________________________________________________________________

None

表 10.17 定義和編譯Bidirectional LSTM模型的輸出的例子

10.4 擬合模型

在這個模型中,我們使用get_sequences()函式來生成大量的隨機例子。我們可以透過使用速記生成序列的數目作為週期(epoch)的代理來簡化訓練。這使得我們能夠生成大量的例子,在這種情況下,5000,將它們儲存在記憶體中,並且在一個Keras週期(epoch)中儲存它們。

使用10個批次大小(batch size)來平衡學習速度和計算效率。在實驗和誤差的基礎上,發現了樣本的數量和皮批次的大小。用不同的值進行實驗,看看能否用較少的計算量來訓練一個精確的模型。

# train LSTM 

X, y = get_sequences(50000, n_timesteps) 

model.fit(X, y, epochs=1, batch_size=10)

表 10.18 擬合和編譯Bidirectional LSTM模型的例子

擬合模型並不需要很長的時間。進度條提供用於訓練,並且log損失和模型準確度將會在每個批次(batch)被更新。

50000/50000 [==============================] - 97s - loss: 0.0508 - acc: 0.9817

表 10.19 擬合一個編譯的Bidirectional LSTM模型的輸出的例子

10.5 模型評價

我們可以透過生成100個新的隨機序列來評估模型,並計算擬合模型的預測的準確性。

# evaluate LSTM 

X, y = get_sequences(100, n_timesteps)

loss, acc = model.evaluate(X, y, verbose=0) 

print( Loss: %f, Accuracy: %f % (loss, acc*100)) 

表 10.20 評價一個擬合Bidirectional LSTM模型輸出的例子

執行例子列印log損失和準確率。我們可以看到模型達到了100%的準確率。當給定演算法的隨機性質時,該示例的精度可能會有所不同。你可以看到模型的學習能力在很高的90s。嘗試執行例子幾次。

Loss: 0.016752, Accuracy: 100.000000

表 10.21 從評估一個擬合Bidirectional LSTM模型的輸出的例子

10.6 用模型進行預測

我們可以用類似於評估模型的方式進行預測。在這種情況下,我們將生成10個新的隨機序列,對每個進行預測,將預測的輸出序列與期望的輸出序列進行比較。

# make predictions 

for _ in range(10): 

    X, y = get_sequences(1, n_timesteps) 

    yhat = model.predict_classes(X, verbose=0) 

    exp, pred = y.reshape(n_timesteps), yhat.reshape(n_timesteps)

    print( y=%s, yhat=%s, correct=%s % (exp, pred, array_equal(exp,pred))) 

表 10.22 使用擬合Bidirectional LSTM模型做出預測的例子

執行示例列印預期的(y)和預測的(yhat)輸出序列以及預測序列是否正確。我們可以看到,至少在這種情況下,10個序列中的2個在一個時間步長中被預測錯誤。

你具體的結果會有所不同,但你應該看到類似的行為平均。這是一個具有挑戰性的問題,及時對於一個模型,它具有大量的例子,並顯示出良好的精度,它仍然可以在預測新序列中產生錯誤。

y=[0 0 0 0 0 0 1 1 1 1], yhat=[0 0 0 0 0 0 1 1 1 1], correct=True

y=[0 0 0 0 1 1 1 1 1 1], yhat=[0 0 0 0 1 1 1 1 1 1], correct=True

y=[0 0 0 1 1 1 1 1 1 1], yhat=[0 0 0 1 1 1 1 1 1 1], correct=True

y=[0 0 0 0 0 0 0 1 1 1], yhat=[0 0 0 0 0 0 0 0 1 1], correct=False

y=[0 0 0 0 0 1 1 1 1 1], yhat=[0 0 0 0 0 1 1 1 1 1], correct=True

y=[0 0 0 1 1 1 1 1 1 1], yhat=[0 0 0 1 1 1 1 1 1 1], correct=True

y=[0 0 0 0 0 1 1 1 1 1], yhat=[0 0 0 0 0 0 1 1 1 1], correct=False

y=[0 0 0 0 1 1 1 1 1 1], yhat=[0 0 0 0 1 1 1 1 1 1], correct=True

y=[0 0 0 0 0 0 0 0 1 1], yhat=[0 0 0 0 0 0 0 0 1 1], correct=True

y=[0 0 0 1 1 1 1 1 1 1], yhat=[0 0 0 1 1 1 1 1 1 1], correct=True

表 10.23 用一個擬合的Bidirectional LSTM模型做預測的輸出的例子

10.7 完整例子

為了完整性全部的例子列出如下供您參考。

from random import random

from numpy import array

from numpy import cumsum

from numpy import array_equal

from keras.models import Sequential

from keras.layers import LSTM

from keras.layers import Dense

from keras.layers import TimeDistributed

from keras.layers import Bidirectional

# create a cumulative sum sequence

def get_sequence(n_timesteps):

    # create a sequence of random numbers in [0,1]

    X = array([random() for _ in range(n_timesteps)])

    # calculate cut-off value to change class values

    limit = n_timesteps/4.0

    # determine the class outcome for each item in cumulative sequence

    y = array([0 if x < limit else 1 for x in cumsum(X)])

   return X, y

# create multiple samples of cumulative sum sequences

def get_sequences(n_sequences, n_timesteps):

    seqX, seqY = list(), list()

    # create and store sequences

    for _ in range(n_sequences):

        X, y = get_sequence(n_timesteps)

        seqX.append(X)

        seqY.append(y)

    # reshape input and output for lstm

    seqX = array(seqX).reshape(n_sequences, n_timesteps, 1)

    seqY = array(seqY).reshape(n_sequences, n_timesteps, 1)

   return seqX, seqY

# define problem

n_timesteps = 10

# define LSTM

model = Sequential()

model.add(Bidirectional(LSTM(50, return_sequences=True), input_shape=(n_timesteps, 1)))

model.add(TimeDistributed(Dense(1, activation= 'sigmoid' )))

model.compile(loss= 'binary_crossentropy' , optimizer= 'adam' , metrics=[ 'acc' ])

print(model.summary())

# train LSTM

X, y = get_sequences(50000, n_timesteps)

model.fit(X, y, epochs=1, batch_size=10)

# evaluate LSTM

X, y = get_sequences(100, n_timesteps)

loss, acc = model.evaluate(X, y, verbose=0)

print( 'Loss: %f, Accuracy: %f' % (loss, acc*100))

# make predictions

for _ in range(10):

    X, y = get_sequences(1, n_timesteps)

    yhat = model.predict_classes(X, verbose=0)

    exp, pred = y.reshape(n_timesteps), yhat.reshape(n_timesteps)

    print( 'y=%s, yhat=%s, correct=%s' % (exp, pred, array_equal(exp,pred)))

表 10.24 Bidirectional LSTM在積累和問題的完整的例子

10.8 擴充套件閱讀

本章節提供了一些擴充套件閱讀的資源。

10.8.1 研究論文

  • Bidirectional Recurrent Neural Networks, 1997.

  • Framewise Phoneme Classification with Bidirectional LSTM and Other Neural Network Architectures, 2005.

  • Bidirectional LSTM Networks for Improved Phoneme Classification and Recognition, 2005.

  • Speech Recognition with Deep Recurrent Neural Networks, 2013.

10.8.2 APIs

  • random() Python API.

  • cumsum() NumPy API.

  • Bidirectional Keras API.

10.9 擴充套件

你想更深入的瞭解Bidirectional LSTM嗎?本章節將會列出本課程中具有挑戰性的擴充套件:

  • 列出5個可能從Bidirectional LSTM中獲益的序列預測問題的例子;

  • 調整記憶單元的數目、訓練樣本和批次大小(batch size)來開發一個更小的或者訓練更快的具有100%準確度的模型;

  • 設計並執行一個實驗來比較模型大小和問題的複雜度(例如,序列長度);

  • 定義和執行一個實驗來比較前向、後向和Bidirectional LSTM輸入方向;

  • 定義和執行一個實驗來比較將Bidirectional LSTM wrapper層結合的方法。

10.10 總結

在本課中,你學習到了怎麼樣開發一個Bidirectional LSTM模型。特別地,你學習到了:

  • Bidirectional LSTM模型的結構和怎麼樣在Keras中實現它;

  • 積累和問題的;

  • 怎麼樣為積累和問題開發一個Bidirectional LSTM模型。

相關文章