如何開發和評估Vanilla LSTM模型?(附程式碼)

AMiner學術頭條發表於2019-01-14

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(附程式碼)

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

我們還將繼續推出一系列的文章來介紹裡面的詳細內容,和大家一起來共同學習,支援答疑支援答疑!支援答疑!重要的事情說三遍!您在學習過程中遇到相關問題,都可在文章底部給我們留言!一定會回覆你的!

6.0 前言

6.0.1 課程目標

本課程的目標是學習如何開發和評估Vanilla LSTM模型。完成這一課之後,你將會知道:

  • 用於序列預測的Vanilla LSTM模型的結構及其一般能力;

  • 如何實現echo序列預測問題;

  • 怎麼樣開發Vanilla LSTM模型模型來學習和準確地預測echo序列預測問題。

6.0.2 課程概覽

本課程被劃分為了7個部分,它們是:

  1. Vanilla LSTM模型;

  2. echo序列預測問題;

  3. 定義和編譯模型;

  4. 擬合模型;

  5. 評價模型;

  6. 用模型進行預測;

  7. 完成例子。

讓我們開始吧!

6.1 Vanilla LSTM模型

6.1.1 結構

LSTM(Long Short Term Memory)是在標準RNN基礎上改進而來的一種網路結構,其出現的主要作用是為了解決標準RNN訓練過程中的梯度消失問題,LSTM的結構如下圖所示。 

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.1 LSTM模型的結構

相比於標準RNN模型,LSTM主要是增加了三個控制門單元:遺忘門,輸入門和輸出門。如果去掉三個門控制單元(或者將三個門控制函式都設定為1),LSTM就會退化成標準的RNN模型。

  • 細胞狀態:LSTM的關鍵就是細胞狀態(如下圖所示的水平線),在圖上方貫穿執行,它類似於一條傳送帶,只有少量的資訊互動,很容易儲存資訊不變。

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.2 LSTM模型的細胞狀態

  • 遺忘門:結構如下圖所示,決定細胞狀態的資訊需要丟棄多少。其讀取和的資訊,輸出一個0到1之間的數值個細胞狀態,1表示細胞狀態完全保留,0表示細胞狀態完全丟棄。

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.3 LSTM模型的遺忘門

  • 輸入門:結構如下圖所示,確定什麼新的資訊需要被新增到細胞狀態中。這裡包含兩方面的內容:1. sigmod層決定了什麼值需要被更新;2. tanh層用來生成新的候選資訊向量會被更新到細胞狀態中。

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.3 LSTM模型的輸入門

對細胞狀態的更新為:將舊狀態與相乘,可以丟棄到確定的需要丟棄的狀態,然後加上新的需要加入的資訊即可完成細胞狀態的更新。對細胞狀態的更新表示如下圖:

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.4 LSTM模型的細胞狀態的更新

  • 輸出門:確定細胞狀態的什麼資訊需要被輸出,結構圖如下所示。首先是需要一個 sigmod函式用來確定上一隱層和新輸入資訊有多少需要被保留,然後將更新後的細胞狀態經過 tanh變到 [-1,1]的區間後再進行相乘,這樣就確定了最終的輸出資訊。

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.5 LSTM模型的輸出門

Vanilla LSTM模型是LSTM模型的一種簡單變種,其主要變化是在三個控制門的輸入分別加入了細胞狀態資訊(下圖中的黑線),這個連線被稱為 peephole connection

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.6 Vanilla LSTM模型結構

本書中名字叫Vanilla,以區別於更新的LSTM和一套更復雜的配置。它是原來1997年LSTM論文中所定義的LSTM結構,它將在大多數小序列預測問題上給出良好的結果。Vanilla LSTM被定義為:

  1. 輸入層。

  2. 全連線LSTM隱藏層。

  3. 全連線輸出層。

  1. graph TB

  2.    Input-->LSTM

  3.    LSTM-->Dense

  4.    Dense-->Output

圖 6.7 Vanilla LSTM模型整體表示

6.1.2 實現

在Keras中,一個Vanilla LSTM模型被定義如下,這裡為了方便大家看清楚結構,對每個神經元的數目做了省略:

  1. model = Sequential()

  2. model.add(LSTM(..., input_shape(...)))

  3. model.add(Dense(...))

表 6.1 定義一個Vanilla LSTM模型的例子

這種預設或者標準的LSTM在深入學習LSTM的許多工作和討論中被引用。Vanilla LSTM具有以下5個有吸引力的特性,具體可以在原論文中檢視:

  • 基於多分佈輸入時間步長的序列分類;

  • 數千個時間步長的精確輸入觀察的記憶;

  • 作為先前時間步長的函式的序列預測;

  • 對輸入序列上隨機時間步長的插入具有魯棒性;

  • 對輸入序列上好好資料的放置具有魯棒性;

接下里,我們將提出一個簡單的序列預測問題,我們可以用它來證明Vanilla LSTM。

6.2 Echo序列預測問題

echo序列預測問題是設計來證明LSTM能力的一個問題。該任務是:給定一個隨機的整數序列作為輸入,在特定時間輸入步長(time input step)輸出一個隨機整數值(該時間輸入步長(time input step)是由模型指定的)。

例如,給定隨機整數[5, 3, 2]輸入序列,而且被選擇的時間步長是第二個值,然後期待的輸出時3。技術上來說,這是一個序列分類的問題;當在序列的結尾有多個輸入時間步長和一個輸出時間步長時,它被定義為一個many-to-one預測問題。

如何開發和評估Vanilla LSTM模型?(附程式碼)

圖 6.2 echo序列預測問題構建成many-to-one預測模型

我們很小心地選擇了這個問題用來闡釋Vanilla LSTM儲存能力。此外,我們將手動執行模型生命週期中的一些元素,例如,對模型進行擬合和評估,以便對其中所含的原理有更深刻的理解。接下來,從程式碼中來解讀其執行的原理及過程。具體包括以下步驟:

  1. 生成隨機序列;

  2. one hot編碼序列;

  3. 例項;

  4. 序列變型。

6.2.1 生成隨機序列

在Python中使用 randint()函式可以生成隨機整數,該函式需要指定兩個整數以便明確從什麼返回內生成隨機數。在本課中,我們將會定義從0到99之間生成唯一的值。

  1. ranint(0, 99)

表 6.2 生成隨機整數

我們可以將這個函式放到一個叫做 generate_sequence()的函式中,該函式將會生成一個所期望長度序列的隨機整數。這個函式被列出來如下:

  1. # generate a sequence of random numbers in [0, n_features)

  2. def generate_sequence(length, n_features):

  3.    return [randint(0, n_features-1) for _ in range(length)]

表 6.3 生成隨機整數序列的函式

6.2.2 one hot編碼序列

我們生成了隨機整數序列之後,我們需要將其轉換成一個合適LSTM網路訓練的資料格式。其中一個選擇是將其縮放到[0,1]的範圍內。實踐證明這是可行的,而且我們可以把這個問題歸結為迴歸問題。

因為我們感興趣的是預測正確的數字,而不是接近預期值的數字。這意味著我們更喜歡將問題化為分類而不是迴歸,其中預期的輸出是一個類,並且有100個可能的類值。在這種情況下,我們可以使用整數值的熱編碼,其中每個值由100個元素的二進位制向量表示。

下面的函式稱為one_hot_encode()定義了怎麼樣在一個序列的整數值上迭代,併為每個建立一個二進位制向量表達,並將結果作為2維陣列返回。

  1. # one hot encode sequence

  2. def one_hot_encode(sequence, n_features):

  3.    encoding = list()

  4.    for value in sequence:

  5.        vector = [0 for _ in range(n_features)]

  6.        vector[value] = 1

  7.        encoding.append(vector)

  8. return array(encoding)

表 6.4 隨機整數序列one hot編碼函式

我們也需要解碼編碼的值,以便我們可以利用預測;在這種情況下,僅僅瀏覽下它們。用NumPy中的 argmax函式可以轉換one hot編碼,用最大的值返回向量中的值的索引。下面的函式,叫做 one_hot_decode(),將會解碼一個編碼了的序列,它後面可以被用作解碼你網路的預測。

  1. # decode a one hot encoded string

  2. def one_hot_decode(encoded_seq):

  3.    return [argmax(vector) for vector in encoded_seq]

表 6.5 解碼編碼序列的函式

6.2.3 例項

我們將其整合到一起。下面的是一個完整的用於生成25個序列的隨機整數並將每個整數編碼為二進位制向量的例子:

  1. from random import randint

  2. from numpy import array

  3. from numpy import argmax

  4. # generate a sequence of random numbers in [0, n_features)

  5. def generate_sequence(length, n_features):

  6.    return [randint(0, n_features-1) for _ in range(length)]

  7. # one hot encode sequence

  8. def one_hot_encode(sequence, n_features):

  9.    encoding = list()

  10.    for value in sequence:

  11.        vector = [0 for _ in range(n_features)]

  12.        vector[value] = 1

  13.        encoding.append(vector)

  14.    return array(encoding)

  15. # decode a one hot encoded string

  16. def one_hot_decode(encoded_seq):

  17.    return [argmax(vector) for vector in encoded_seq]

  18. # generate random sequence

  19. sequence = generate_sequence(25, 100)

  20. print(sequence)

  21. # one hot encode

  22. encoded = one_hot_encode(sequence, 100)

  23. print(encoded)

  24. # one hot decode

  25. decoded = one_hot_decode(encoded)

  26. print(decoded)

表 6.6 生成序列並進行編碼的例子

執行例子,首先列印25個隨機整數,然後是一個所有整數序列的截斷的二進位制編碼,每個向量一行,然後解碼序列。由於每次執行的時候所生成的隨機整數不一樣,你可能有不同的結果。

  1. [50, 65, 99, 36, 5, 60, 74, 77, 64, 22, 79, 8, 56, 70, 95, 52, 59, 32, 45, 49, 85, 67, 98, 82, 6]

  2. [[0 0 0 ..., 0 0 0]

  3. [0 0 0 ..., 0 0 0]

  4. [0 0 0 ..., 0 0 1]

  5. ...,

  6. [0 0 0 ..., 0 1 0]

  7. [0 0 0 ..., 0 0 0]

  8. [0 0 0 ..., 0 0 0]]

  9. [50, 65, 99, 36, 5, 60, 74, 77, 64, 22, 79, 8, 56, 70, 95, 52, 59, 32, 45, 49, 85, 67, 98, 82, 6]

表 6.7 生成序列並編碼它們的輸出的例子

這裡 ...表示的是省略了的,該部分顯示了25個長度為100的向量。

6.2.4 序列變型

最後一步是將one hot編碼序列變型到一個可以用於LSTM輸入的格式。這包括將編碼的序列變型為n個時間步長和k個特徵,這裡n是生成序列的整數數目,而k是每個時間步長可能整數的集合(例如100)。

一個序列然後可以被變型為一個樣本、時間步長和特徵的3維的矩陣,或者對於一個具有25個整數的單一的序列[1, 25, 100]。例如:

  1. X = encoded.reshape(1, 25, 100)

表 6.8 變換一個編碼序列的例子

序列的輸出是在特定預定義位置的單純的編碼整數。對於一個模型生成的所有示例,該位置必須保持一致,以便模型可以學習。例如,我們可以通過直接從編碼序列取編碼值,使用第二個時間步長作為具有25個時間步長的序列的輸出。

  1. y = encoded[1, :]

表 6.9 獲取解碼序列的值的例子

我們可以將這個以及上述所生產的以及編碼步長一起放到一個新的叫做generate_example()的函式中來生成一個序列,編碼它並返回輸入(X)和輸出(y)元件來訓練LSTM。

  1. # generate one example for an lstm

  2. def generate_example(length, n_features, out_index):

  3.    # generate sequence

  4.    sequence = generate_sequence(length, n_features)

  5.    # one hot encode

  6.    encoded = one_hot_encode(sequence, n_features)

  7.    # reshape sequence to be 3D

  8.    X = encoded.reshape((1, length, n_features))

  9.    # select output

  10.    y = encoded[out_index].reshape(1, n_features)

  11.    return X, y

表 6.10 生成序列、編碼它們並對其變型的例子

我們可以將這些放在一起,測試一個例子的生成,以擬合或者評估LSTM如下:

  1. from random import randint

  2. from numpy import array

  3. from numpy import argmax

  4. # generate a sequence of random numbers in [0, n_features)

  5. def generate_sequence(length, n_features):

  6.    return [randint(0, n_features-1) for _ in range(length)]

  7. # one hot encode sequence

  8. def one_hot_encode(sequence, n_features):

  9.    encoding = list()

  10.    for value in sequence:

  11.        vector = [0 for _ in range(n_features)]

  12.        vector[value] = 1

  13.        encoding.append(vector)

  14.    return array(encoding)

  15. # decode a one hot encoded string

  16. def one_hot_decode(encoded_seq):

  17.    return [argmax(vector) for vector in encoded_seq]

  18. # generate one example for an lstm

  19. def generate_example(length, n_features, out_index):

  20.    # generate sequence

  21.    sequence = generate_sequence(length, n_features)

  22.    # one hot encode

  23.    encoded = one_hot_encode(sequence, n_features)

  24.    # reshape sequence to be 3D

  25.    X = encoded.reshape((1, length, n_features))

  26.    # select output

  27.    y = encoded[out_index].reshape(1, n_features)

  28.    return X, y

  29. X, y = generate_example(25, 100, 2)

  30. print(X.shape)

  31. print(y.shape)

表 6.11 測試函式來生成編碼序列並變型的例子

執行程式碼生成編碼的序列並列印LSTM序列輸入和輸出元件的形狀。

  1. (1, 25, 100)

  2. (1, 100)

表 6.112 輸出從生成編碼序列並變型的例子

現在我們知道怎麼樣準備和表達隨機整數序列了,我們可以試著使用LSTMs來學習它們。

6.3 定義並編譯模型

我們將從定義和編譯模型開始。為了保證模型在一個合理的時間內擬合,我們將會通過將序列長度減少到5個整數和特徵數為10(例如0~9)來大大簡化問題。模型必須制定輸入資料的預期維數。在這種情況下,根據時間步長(5)和特徵(10)。我們將使用一個單一的隱藏層LSTM與25個記憶體單元,選擇一個小的嘗試和錯誤。

輸出層時一個全連線層(Dense),具有10個神經元,用於10個可能輸出的整數。在輸出層上使用softmax()啟用函式,以允許網路在可能的輸出值上學習和輸出分佈。

網路將在訓練是使用log損失函式,適用於多分類問題,以及效率Adam優化演算法。精度度量將會在每個訓練週期內給出,以便根據損失函式檢視模型的學習能力。

  1. from keras.models import Sequential

  2. from keras.layers import LSTM

  3. from keras.layers import Dense

  4. # define model

  5. length = 5

  6. n_features = 10

  7. out_index = 2

  8. model = Sequential()

  9. model.add(LSTM(25, input_shape=(length, n_features)))

  10. model.add(Dense(n_features, activation='softmax'))

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

  12. print(model.summary())

表 6.13 為echo問題定義一個Vanilla LSTM的例子

執行例子定義和編譯模型,然後輸出模型的總的結構。列印一個模型結構是一個很好的實踐,一般來說,確認模型是根據你的意圖定義和編譯的。

  1. _________________________________________________________________

  2. Layer (type)                 Output Shape              Param #  

  3. =================================================================

  4. lstm_1 (LSTM)                (None, 25)                3600      

  5. _________________________________________________________________

  6. dense_1 (Dense)              (None, 10)                260      

  7. =================================================================

  8. Total params: 3,860

  9. Trainable params: 3,860

  10. Non-trainable params: 0

  11. _________________________________________________________________

  12. None

表 6.14 從定義模型中輸出的例子

6.4 擬合模型

我們現在可以在樣本序列上擬合模型了。我們編寫的用於echo序列預測問題的程式碼可以生成隨機序列。我們可以生成一個很大數量的例子序列並將其傳遞給模型的fit()函式。資料集將會被載入到記憶體,訓練會很快,並且我們可以通過變換週期(epoch)的數量、資料集的大小以及批次(batch)的數量來實驗。

一個更簡單的方法是來手工管理訓練過程,其中生成一個訓練樣本用於更新模型,並且清楚任何的內部狀態。週期(epoch)的數目是生成樣本的迭代次數,並且基本上批次(batch)處理的大小是1個樣本。下面是一個例子,說明模型的1000個週期(epoch)發現的一個小的嘗試和錯誤。

  1. # fit model

  2. for i in range(10000):

  3.    X, y = generate_example(length, n_features, out_index)

  4.    model.fit(X, y, epochs=1,verbose = 2)

表 6.15 擬合定義的LSTM模型的例子

擬合模型將會報告log損失函式以及每個模式的準確率。這裡,準確率既不是0或1(0%或者100%),因為我們對每一個樣本進行序列分類預測並報告結果。

如何開發和評估Vanilla LSTM模型?(附程式碼)

表 6.16 輸出擬合定義模型的例子

6.5 評估模型

一旦模型擬合了,我們就可以估計模型在分類新的隨機序列的能力。我們可以通過簡單地對100個隨機生成的序列進行預測並計算正確預測的數量來做到這一點。

當我們擬合了模型,我們將會生成一個很大數量的樣本,將其連線在一起,然後使用evaluate()函式來評估模型。在這種情況下,我們將會手動預測並計算正確的輸出結果。我們可以在一個迴圈中完成生成樣本、做出預測、並在結果是正確的時候計數器加1。

  1. # evaluate model

  2. correct = 0

  3. for i in range(100):

  4.    X, y = generate_example(length, n_features, out_index)

  5.    yhat = model.predict(X)

  6.    if one_hot_decode(yhat) == one_hot_decode(y):

  7.        correct += 1

  8.    print('Accuracy: %f' % ((correct/100)*100.0))

表 6.17 評價擬合LSTM模型的例子

評價模型報告模型的學習能力是100%。

  1. Accuracy: 100.000000

表 6.18 輸出評估擬合模型的例子

6.6 用模型做出預測

最後,我們使用擬合模型在一個新的隨機生成的序列上來做出預測。對於這個問題,這與評估模型的情況大致相同。因為這是一個面向使用者的活動,我們可以解碼整個序列、預期輸出和預測,並將它們列印在螢幕上。

  1. # prediction on new data

  2. X, y = generate_example(length, n_features, out_index)

  3. yhat = model.predict(X)

  4. print('Sequence: %s' % [one_hot_decode(x) for x in X])

  5. print('Expected: %s' % one_hot_decode(y))

  6. print('Predicted: %s' % one_hot_decode(yhat))

表 6.19 用擬合LSTM模型做出預測的例子

執行該示例將列印解碼的隨機生成的序列、預期的結果以及(希望)滿足預期值的預測。你的具體結果會有所不同。

  1. Sequence: [[7, 0, 2, 6, 7]] Expected: [2]

  2. Predicted: [2]

表 6.20 輸出從擬合模型預測結果的例子

如果模型顯示錯誤請不要慌張。LSTMs是隨機的,有可能單一模型的執行可能收斂在一個不完全瞭解問題的解決方案上。如果這發生在你的身上,試試多執行這個例子幾次。

6.7 完整例子

本節列出了列出了完整的工作來供你進行參考。

  1. from random import randint

  2. from numpy import array

  3. from numpy import argmax

  4. from keras.models import Sequential

  5. from keras.layers import LSTM

  6. from keras.layers import Dense

  7. # generate a sequence of random numbers in [0, n_features)

  8. def generate_sequence(length, n_features):

  9.    return [randint(0, n_features-1) for _ in range(length)]

  10. # one hot encode sequence

  11. def one_hot_encode(sequence, n_features):

  12.    encoding = list()

  13.    for value in sequence:

  14.        vector = [0 for _ in range(n_features)]

  15.        vector[value] = 1

  16.        encoding.append(vector)

  17.    return array(encoding)

  18. # decode a one hot encoded string

  19. def one_hot_decode(encoded_seq):

  20.    return [argmax(vector) for vector in encoded_seq]

  21. # generate one example for an lstm

  22. def generate_example(length, n_features, out_index):

  23.    # generate sequence

  24.    sequence = generate_sequence(length, n_features)

  25.    # one hot encode

  26.    encoded = one_hot_encode(sequence, n_features)

  27.    # reshape sequence to be 3D

  28.    X = encoded.reshape((1, length, n_features))

  29.    # select output

  30.    y = encoded[out_index].reshape(1, n_features)

  31.    return X, y

  32. # define model

  33. length = 5

  34. n_features = 10

  35. out_index = 2

  36. model = Sequential()

  37. model.add(LSTM(25, input_shape=(length, n_features)))

  38. model.add(Dense(n_features, activation='softmax'))

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

  40. print(model.summary())

  41. # fit model

  42. for i in range(10000):

  43.    X, y = generate_example(length, n_features, out_index)

  44.    model.fit(X, y, epochs=1, verbose=2)

  45. # evaluate model

  46. correct = 0

  47. for i in range(100):

  48.    X, y = generate_example(length, n_features, out_index)

  49.    yhat = model.predict(X)

  50.    if one_hot_decode(yhat) == one_hot_decode(y):

  51.        correct += 1

  52.    print('Accuracy: %f' % ((correct/100)*100.0))

  53. # prediction on new data

  54. X, y = generate_example(length, n_features, out_index)

  55. yhat = model.predict(X)

  56. print('Sequence: %s' % [one_hot_decode(x) for x in X])

  57. print('Expected: %s' % one_hot_decode(y))

  58. print('Predicted: %s' % one_hot_decode(yhat))

表 6.21 將Vanilla LSTM模型應用到Echo問題的例子

相關文章