開發Encoder-Decoder LSTM模型的簡單教程(附程式碼)

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

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

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

9.0 前言

9.0.1 課程目標

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

  • Encoder-Decoder LSTM的結構以及怎麼樣在Keras中實現它;

  • 加法序列到序列的預測問題;

  • 怎麼樣開發一個Encoder-Decoder LSTM模型用來解決加法seq2seq預測問題。

9.1 課程概覽

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

  1. Encoder-Decoder LSTM;

  2. 加法預測問題;

  3. 定義並編譯模型;

  4. 擬合模型;

  5. 評估模型;

  6. 用模型做預測;

  7. 完成例子

讓我們開始吧!

9.2 Encoder-Decoder LSTM模型

9.2.1 序列到序列預測問題

序列預測問題通常涉及預測真實序列中的下一個值或者輸出輸入序列的類標籤。這通常被構造為一個輸入時間步長序列到一個輸出時間步長(例如,one-to-one)或者多個輸入時間步長到一個輸出時間步長(many-to-many)型別的序列預測問題。

有一種更具挑戰性的序列預測問題,它以序列作為輸入,需要序列預測作為輸出。這些被稱為序列到序列預測問題,或者簡稱為seq2seq問題。使這些問題具有挑戰性的一個建模問題是輸入和輸出序列的長度可能變化。由於存在多個輸入時間步長和多個輸出時間步長,這種形式的問題被稱為many-to-many序列預測問題。

9.2.2 結構

seq2seq預測問題的一種被證明是非常有效的方法被稱為Encoder-Decoder LSTM。該體系結構包括兩個模型:一個用於讀取輸入序列並將其編碼成一個固定長度的向量,另一個用於解碼固定長度的向量並輸出預測序列。模型的使用相應地給出了該體系結構的名字——Encoder-Decoder LSTM,專門針對seq2seq問題而設計。

... RNN Encoder-Decoder由兩個迴圈神經元網路(RNN)所組成,它們作為編碼和解碼對存在。編碼器可將可變長度的源序列對映到一個固定長度的向量,同時解碼器將向量使用回可變長度的目標序列。

— Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014

Encoder-Decoder LSTM是為處理自然語言處理問題而開發的,它顯示了-of-the-art的效能,特別是在文字翻譯領域稱為統計機器翻譯。這種體系結構的創新是在模型的最核心的部分使用了固定大小的內部表示,這裡輸入序列被讀取並且輸出序列從中被讀取。由於這個原因,該方法被稱為序列嵌入。

在英語與法語翻譯的體系結構的一個應用中,編碼的英語短語的內部表示被視覺化。輸出的影像解釋了翻譯任務中短語管理的一個定性的有意義的學習結構。

提出的RNN Encoder-Decoder自然地生成一個短語的連續空間表示。[...]從視覺化角度,很明顯地RNN Encoder-Decoder捕獲語義和句法結構的短語。

— Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014.

在翻譯任務上,該模型在輸入順序顛倒時更有效。此外,即使在很長的輸入序列上,該模型也被證明是有效的。

我們能夠很好地完成長句,因為我們顛倒了原來句子的詞序,而不是訓練和測試集中的目標句子。透過這樣做,我們引入了許多短期的依賴關係,是的最佳化問題變得簡單多了。...源句倒換的簡單技巧是這項工作的主要技術貢獻之一。

— Sequence to Sequence Learning with Neural Networks, 2014.

這種方法也被用於影像輸入,其中卷積神經網路被用作輸入影像上的特徵提取器,然後由解碼器LSTM讀取。

...我們建議遵循這個優雅的配方,有一個深度卷積神經網路(CNN)來取代encoder RNN。[...]使用CNN作為影像編碼器是很自然的,首先對影像分類任務進行預訓練,最後使用隱藏層作為RNN解碼器輸出句子的輸入。

— Show and Tell: A Neural Image Caption Generator, 2014.

開發Encoder-Decoder LSTM模型的簡單教程(附程式碼)

圖 9.1 Encoder-decoder LSTM結構

9.2.3 應用

下面的列表突出了Encoder-Decoder LSTM結構的一些有趣的應用。

  • 機器翻譯,如短語的英譯法語。

  • 學習執行,例如小程式的計算結果;

  • 影像標題,例如用於生成影像;

  • 對話建模,例如對語篇產生的答案的問題;

  • 運動序列的分類,例如對一系列的手勢生成一系列的命令;

9.2.4 實現

Encoder-Decoder LSTM可以直接在Keras中實現。我們可以認為模型由兩個關鍵部分組成:編碼器和解碼器。首先,輸入序列一次向網路顯示一個編碼字元。我們需要一個編碼水平來學習輸入序列中的步驟之間的關係,並開發這些關係的內部表示。

一個或多個LSTM層可用於顯示編碼器模型。這個模型的輸出時一個固定大小的向量,表示輸入許的內部表示。這個層中的儲存單元的數目與這個大小的向量長度無關。

  1. model = Sequential()

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

表 9.1 Vanilla LSTM模型的例子

解碼器必須將所學習的輸入序列的內部表示轉換成正確的輸出序列。還可以使用一個或多個LSTM層來實現編解碼模型。這個模型是從編碼器模型的大小輸出中讀取的。與Vanilla LSTM一樣,一個Dense層可以被用做網路的輸出。透過將Dense層包裹在TimeDistributed層中,同樣的權重可以被用來在每個輸出序列中輸出每個時間步長。

  1. model.add(LSTM(..., return_sequences=True))

  2. model.add(TimeDistributed(Dense(...)))

表 9.2 用TimeDistributed包裹Dense層的LSTM模型的例子

但是有一個問題。我們必須把編碼器和解碼器連線起來,但是它們不適合。也就是說,編碼器將產生輸出的二維矩陣,其中長度由層中的儲存單元的數目決定。解碼器是一個LSTM層,它期望3D輸入(樣本、時間步長、特徵),以產生由該問題產生的不同長度的解碼序列。

如果你試圖強迫這些碎片在一起,你會得到一個錯誤,表明解碼器的輸出是2D,需要3D解碼器。我們可以用重複向量層來解決這個問題。該層簡單地多次重複所提出的2D輸入以建立3D輸出。

RepeatVector層可以像介面卡一樣使用,以將網路的編碼器和解碼器部分適配在一起。我們可以配置重複向量以在輸出序列中的每個時間步長中重複一個固定長度向量。

  1. model.add(RepeatVector(...))

表 9.3 一個RepeatVector層的例子

把它們放在一起,我們得到:

  1. model = Sequential()

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

  3. model.add(RepeatVector(...))

  4. model.add(LSTM(..., return_sequences=True))

  5. model.add(TimeDistributed(Dense(...)))

表 9.4 Encoder-Decoder模型的例子

總的來說,使用RepeatVector作為編碼器的固定大小的2D輸出,以適應解碼器期望的不同長度和3D輸入。TimeDistributed wrapper允許相同的輸出層用於輸出序列中的每個元素。

9.3 加法預測問題

加法問題是一個序列到序列,或者seq2seq的預測問題。它被用於 Wojciech Zaremba和Ilya Sutskever2014年名為《Learning to Execute》的論文來探索Encoder-Decoder LSTM的能力,其中的體系結構被證明學習計算小程式的輸出。

該問題被定義為計算兩個輸入數的和的輸出。這是具有挑戰性的,因為每個數字和數學符號被提供為字元型別的,並且預期輸出也被預期為字元。例如,輸入10+6與輸出16將由序列表示:

  1. Input: [ 1 , 0 , + , 6 ]

  2. Output: [ 1 , 6 ]

表 9.5 加法問題中輸入和輸出序列的例子

該模型不僅要學習字元的整數性質,還要學習要執行的數學運算的性質。注意序列是如何重要的,並且隨機地拖動輸入將建立與輸出序列無關的無意義序列。還是要注意序列在輸入和輸出序列中如何變化。在技術上,這使得加法預測問題是一個序列到序列的問題,需要many-to-many的模型來求解。

開發Encoder-Decoder LSTM模型的簡單教程(附程式碼) 圖 9.2 用many-to-many預測模型構造加法預測問題

我們可以透過新增兩個數字來保持事物的簡單性,但是我們可以看到如何將其縮放成可變數量的術語和數學運算,這些數學運算可以作為模型的輸入來學習和推廣。這個問題可以用Python來實現。我們可以把它們分成以下步驟:

  1. 生成加法對;

  2. 填充字串的整數;

  3. 整數編碼序列;

  4. one hot編碼序列;

  5. 序列生成流水線;

  6. 解碼序列。

9.3.1 生成加法對

第一步是生成隨機整數序列及其總和。我們可以把它放在一個名為randomsumpairs()的函式中,如下所示:

  1. from random import seed

  2. from random import randint

  3. # generate lists of random integers and their sum

  4. def random_sum_pairs(n_examples, n_numbers, largest):

  5.    X, y = list(), list()

  6.    for i in range(n_examples):

  7.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  8.        out_pattern = sum(in_pattern)

  9.        X.append(in_pattern)

  10.        y.append(out_pattern)

  11.    return X, y

  12. seed(1)

  13. n_samples = 1

  14. n_numbers = 2

  15. largest = 10

  16. # generate pairs

  17. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  18. print(X, y)

表 9.6 生成隨機序列對的例子

執行這個函式只列印一個在1到10之間新增兩個隨機整數的例子。

  1. [[3, 10]] [13]

表 9.7 輸出生成一個隨機序列對的例子

9.3.2 填充字串的整數

下一步是將整數轉換為字串。輸入字串將是“10+10”格式,輸出字串將是“20”格式。這個函式的關鍵是填充數字,以確保每個輸出和輸出序列具有相同的字元數。填充字元應該與資料無關,因此模型可以學會忽略它們。在這種情況下,我們使用空格字串(“ ”)填充,並在左側填充字串,保持最右邊的資訊。

還有其他的方法來填充,比如單個填充每個術語。試試看它是否會帶來更好的效能。填充需要我們知道最長序列的長度。我們可以透過計算我們可以生成的最大整數的 $log_{10}()$和這個數字的上線來計算每個數字需要多少字元。我們增加了1個最大的數字,以確保我們期望3個字元而不是2個字元,對於一個圓最大的數字,比如200個取結果的上限(例如 $ceil(log10(largest+1)$)。然後,我們需要新增正確數目的加符號(例如, n numbers-1)。

  1. max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

表 9.8 計算輸入序列最大長度的例子

我們可以用一個實際的例子來做這個具體的例子,其中總的數量(n個數)是3,最大的值(最大)是10。

  1. max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  2. max_length = 3 * ceil(log10(10+1)) + 3 - 1

  3. max_length = 3 * ceil(1.0413926851582251) + 3 - 1

  4. max_length = 3 * 2 + 3 - 1

  5. max_length = 6 + 3 - 1

  6. max_length = 8

表 9.9 最大輸入序列長度工作例項

直觀來說,我們期望每個詞兩個空間(例如['1', '0'])乘以3個詞,或者最大長度為6個空間的輸入序列,如果有加法符號的話就再加兩位(例如:[‘1’,‘0’,‘+’,‘1’,‘0’,‘+’,‘1’,‘0’])使得最大的可能序列長度為8個字元。這就是我們在實際例子中看到的。

在輸出序列上重複一個類似的過程,當然沒有加號。

  1. max_length = ceil(log10(n_numbers * (largest+1)))

表 9.10 計算輸出序列長度的例子

再次,我們可以透過計算期望的最大輸出序列長度來具體實現,上面的例子的總數量(n個數)是3,最大值(最大)是10。

  1. max_length = ceil(log10(n_numbers * (largest+1)))

  2. max_length = ceil(log10(3 * (10+1)))

  3. max_length = ceil(log10(33))

  4. max_length = ceil(1.5185139398778875)

  5. max_length = 2

表 9.11 最大輸出序列長度的工作例項

同樣的,直觀的,我們期望最大可能的加法是10+10+10或者30的 值。這將需要最大長度為2,這就是我們在工作示例中所看到的。下面的示例新增了string()函式,並用一個輸入/輸出對來演示它的用法。

  1. from random import seed

  2. from random import randint

  3. from math import ceil

  4. from math import log10

  5. # generate lists of random integers and their sum

  6. def random_sum_pairs(n_examples, n_numbers, largest):

  7.    X, y = list(), list()

  8.    for i in range(n_examples):

  9.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  10.        out_pattern = sum(in_pattern)

  11.        X.append(in_pattern)

  12.        y.append(out_pattern)

  13.    return X, y

  14. # convert data to strings

  15. def to_string(X, y, n_numbers, largest):

  16.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  17.    Xstr = list()

  18.    for pattern in X:

  19.        strp = '+'.join([str(n) for n in pattern])

  20.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  21.        Xstr.append(strp)

  22.    max_length = ceil(log10(n_numbers * (largest+1)))

  23.    ystr = list()

  24.    for pattern in y:

  25.        strp = str(pattern)

  26.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  27.        ystr.append(strp)

  28.    return Xstr, ystr

  29. seed(1)

  30. n_samples = 1

  31. n_numbers = 2

  32. largest = 10

  33. # generate pairs

  34. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  35. print(X, y)

  36. # convert to strings

  37. X, y = to_string(X, y, n_numbers, largest)

  38. print(X, y)

表 9.12 將一個序列對轉換成插補字元的例子

執行例子首先輸出整數序列,並插補同樣序列的字串表達。

  1. [[3, 10]] [13]

  2. ['3+10'] ['13']

表 9.13 將一個序列對轉換為插補字元的輸出的例子

9.3.3 整數編碼序列

接下來,我們需要將字串中的每個字元編碼為整數值。在神經網路中我們必須用數字進行工作,而不是字元。整數編碼將問題轉化為一個分類問題,其中輸出序列可以被認為是具有11個可能值的類輸出。這恰好是具有一些序數關係的整數(前10類值)。為了執行此編碼,我們必須確定字串編碼中可能出現的符號的完整字母表,如下:

  1. alphabet = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , + , ]

表 9.14 定義一個字元表的例子

然後,整數編碼成為一個簡單的過程,構建一個字串到整數偏移的查詢表,並逐個轉換每個字串的每個字元。下面的示例提供整數編碼的integer_encode()函式,並演示它如何使用。

  1. from random import seed

  2. from random import randint

  3. from math import ceil

  4. from math import log10

  5. # generate lists of random integers and their sum

  6. def random_sum_pairs(n_examples, n_numbers, largest):

  7.    X, y = list(), list()

  8.    for i in range(n_examples):

  9.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  10.        out_pattern = sum(in_pattern)

  11.        X.append(in_pattern)

  12.        y.append(out_pattern)

  13.    return X, y

  14. # convert data to strings

  15. def to_string(X, y, n_numbers, largest):

  16.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  17.    Xstr = list()

  18.    for pattern in X:

  19.        strp = '+ '.join([str(n) for n in pattern])

  20.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  21.        Xstr.append(strp)

  22.    max_length = ceil(log10(n_numbers * (largest+1)))

  23.    ystr = list()

  24.    for pattern in y:

  25.        strp = str(pattern)

  26.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  27.        ystr.append(strp)

  28.    return Xstr, ystr

  29. # integer encode strings

  30. def integer_encode(X, y, alphabet):

  31.    char_to_int = dict((c, i) for i, c in enumerate(alphabet))

  32.    Xenc = list()

  33.    for pattern in X:

  34.        integer_encoded = [char_to_int[char] for char in pattern]

  35.        Xenc.append(integer_encoded)

  36.    yenc = list()

  37.    for pattern in y:

  38.        integer_encoded = [char_to_int[char] for char in pattern]

  39.        yenc.append(integer_encoded)

  40.    return Xenc, yenc

  41. seed(1)

  42. n_samples = 1

  43. n_numbers = 2

  44. largest = 10

  45. # generate pairs

  46. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  47. print(X, y)

  48. # convert to strings

  49. X, y = to_string(X, y, n_numbers, largest)

  50. print(X, y)

  51. # integer encode

  52. alphabet = [ '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , '+' , ' ']

  53. X, y = integer_encode(X, y, alphabet)

  54. print(X, y)

表 9.15 整數編碼插補序列的例子

執行例子列印每個字串編碼模式的整數編碼版本。我們可以看到空字串(“ ”)被編碼成了11,字元三(“3”)被編碼成了3,等等。

  1. [[3, 10]] [13]

  2. ['3+ 10'] ['13']

  3. [[3, 10, 11, 1, 0]] [[1, 3]]

表 9.16 從整數編碼輸入和輸出序列輸出的例子

9.3.4 one hot編碼序列

下一步是對整數編碼序列進行二進位制編碼。這涉及到將每個整數轉換成與字母相同長度的二進位制向量,並用1標記特定的整數。例如,0個整數表示“0”字元,並將其編碼為11個向量元素的第0位位1的二進位制向量: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]。下面的例子為二進位制編碼定義了onehotencode() 函式,並演示瞭如何使用它。

  1. from random import seed

  2. from random import randint

  3. from math import ceil

  4. from math import log10

  5. # generate lists of random integers and their sum

  6. def random_sum_pairs(n_examples, n_numbers, largest):

  7.    X, y = list(), list()

  8.    for i in range(n_examples):

  9.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  10.        out_pattern = sum(in_pattern)

  11.        X.append(in_pattern)

  12.        y.append(out_pattern)

  13.    return X, y

  14. # convert data to strings

  15. def to_string(X, y, n_numbers, largest):

  16.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  17.    Xstr = list()

  18.    for pattern in X:

  19.        strp = '+'.join([str(n) for n in pattern])

  20.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  21.        Xstr.append(strp)

  22.    max_length = ceil(log10(n_numbers * (largest+1)))

  23.    ystr = list()

  24.    for pattern in y:

  25.        strp = str(pattern)

  26.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  27.        ystr.append(strp)

  28.    return Xstr, ystr

  29. # integer encode strings

  30. def integer_encode(X, y, alphabet):

  31.    char_to_int = dict((c, i) for i, c in enumerate(alphabet))

  32.    Xenc = list()

  33.    for pattern in X:

  34.        integer_encoded = [char_to_int[char] for char in pattern]

  35.        Xenc.append(integer_encoded)

  36.    yenc = list()

  37.    for pattern in y:

  38.        integer_encoded = [char_to_int[char] for char in pattern]

  39.        yenc.append(integer_encoded)

  40.    return Xenc, yenc

  41. # one hot encode

  42. def one_hot_encode(X, y, max_int):

  43.    Xenc = list()

  44.    for seq in X:

  45.        pattern = list()

  46.        for index in seq:

  47.            vector = [0 for _ in range(max_int)]

  48.            vector[index] = 1

  49.            pattern.append(vector)

  50.        Xenc.append(pattern)

  51.    yenc = list()

  52.    for seq in y:

  53.        pattern = list()

  54.        for index in seq:

  55.            vector = [0 for _ in range(max_int)]

  56.            vector[index] = 1

  57.            pattern.append(vector)

  58.        yenc.append(pattern)

  59.    return Xenc, yenc

  60. seed(1)

  61. n_samples = 1

  62. n_numbers = 2

  63. largest = 10

  64. # generate pairs

  65. X, y = random_sum_pairs(n_samples, n_numbers, largest)

  66. print(X, y)

  67. # convert to strings

  68. X, y = to_string(X, y, n_numbers, largest)

  69. print(X, y)

  70. # integer encode

  71. alphabet = [ '0' , '1' , '2' , '3', '4' , '5' , '6' , '7' , '8' , '9' , '+' ,' ']

  72. X, y = integer_encode(X, y, alphabet)

  73. print(X, y)

  74. # one hot encode

  75. X, y = one_hot_encode(X, y, len(alphabet))

  76. print(X, y)

表 9.17 one hot編碼一個整數編碼序列的例子

執行示例為每個整數編碼列印二進位制編碼序列。我新增了一些新行,使輸入和輸出的二進位制編碼更加清晰。可以看到,一個和模式變成5個二進位制編碼向量的序列,每一個都有11個元素。輸出或者和成為2個二進位制編碼向量的序列,每一個都具有11個元素。

  1. [[3, 10]] [13]

  2. ['3+10'] ['13']

  3. [[3, 10, 1, 0]] [[1, 3]]

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

表 9.18 one hot編碼一個整數編碼序列的輸出的例子

9.3.5 序列生成流水線

我們可以將所有這些步驟結合到一個名為generate_data()的函式中,如下所示。給定設計的樣本數量、術語數量、每個術語的最大值和可能字元的字母表,函式將生成一組輸入和輸出序列。

  1. # generate an encoded dataset

  2. def generate_data(n_samples, n_numbers, largest, alphabet):

  3.    # generate pairs

  4.    X, y = random_sum_pairs(n_samples, n_numbers, largest)

  5.    # convert to strings

  6.    X, y = to_string(X, y, n_numbers, largest)

  7.    # integer encode

  8.    X, y = integer_encode(X, y, alphabet)

  9.    # one hot encode

  10.    X, y = one_hot_encode(X, y, len(alphabet))

  11.    # return as NumPy arrays

  12.    X, y = array(X), array(y)

  13.    return X, y

表 9.19 生成一個序列、編碼和對其進行變型以適應LSTM模型的例子

9.3.6 解碼序列

最後,我們需要反轉編碼來將輸出向量轉換成數字,這樣我們就可以將預期輸出整數有預測整數進行比較。下面的invert()函式執行此操作。關鍵是使用argmax()函式將二進位制編碼轉回到整數,然後將整數轉換成字元,使用整數的反向對映到字母表中的字元。

  1. # invert encoding

  2. def invert(seq, alphabet):

  3.    int_to_char = dict((i, c) for i, c in enumerate(alphabet))

  4.    strings = list()

  5.    for pattern in seq:

  6.        string = int_to_char[argmax(pattern)]

  7.        strings.append(string)

  8.    return ''.join(strings)

表 9.20 決定一個編碼輸入或者輸出序列的例子

現在我們為這個例子準備好了所有的事情了。

9.4 定義並編譯模型

第一步是定義一個特定的序列預測問題。我們必須指定3個引數作為generate_data()函式(如上)的輸入來生成輸入-輸出序列的樣本:

  • n_term:等式中單詞的數目(例如,2則為10+10)。

  • largest:每個單詞的最大數(例如,10則為值在0-10之間)。

  • alphabet:用於編碼輸入和輸出序列的字母表(例如,0-9,+和“ ”)。

我們將使用具有適度複雜性的問題的配置。每個例項由3個術語組成,每個術語的最大值為10.不管值為0-9,+,還是“0”,字母表保持不變。

  1. # number of math terms

  2. n_terms = 3

  3. # largest value for any single input digit

  4. largest = 10

  5. # scope of possible symbols for each input or output time step

  6. alphabet = [str(x) for x in range(10)] + [ '+' , ' ']

表 9.21 配置問題例項的例子

由於加法問題的特殊性,網路需要三個配置值。

  • n_chars:一個時間步長的字母表的大小(例如,12對應0,9,'+'和’ ‘)。

  • ninseq_length:編碼輸入序列的時間步長(例如,8的時候對應'10+10+10')

  • noutseq_length:編碼輸出序列的時間步長(例如,2時候對應'30')。

nchars變數用於對輸入層中的特徵數目和輸出層中的每個輸入和輸出時間步長的特徵數進行分解。使用ninseqlength變數來定義時間步長的數量以在RepeatVector中重複編碼輸入,這反過來定義了序列喂入用於產生輸出序列的解碼器中的長度。ninseqlength和noutseqlength的定義使用了來自tostring()函式相同的程式碼,tostring()函式是用作將整數序列對映為字串的。

  1. # size of alphabet: (12 for 0-9, + and )

  2. n_chars = len(alphabet)

  3. # length of encoded input sequence (8 for 10+10+10)

  4. n_in_seq_length = n_terms * ceil(log10(largest+1)) + n_terms - 1

  5. # length of encoded output sequence (2 for 30 )

  6. n_out_seq_length = ceil(log10(n_terms * (largest+1)))

表 9.22 在問題例項的基礎上定義網路配置的例子

現在我們準備好定義Encoder-Decoder LSTM了。我們將使用一個單一的LSTM層的編碼器和另一個單一層的解碼器。編碼器具有75個儲存單元和50個儲存單元的解碼器。記憶細胞的數量是透過一次次的實驗和錯誤確定的。由於輸入序列相對輸出序列較長,所以編碼器和解碼器中的層的大小不對稱似乎是一種自然的組織。

輸出層使用可預測的12個可能類別的分類log損失。使用了有效的Adam演算法實現梯度下降法,並且在訓練和模型評估期間計算精度。

  1. # define LSTM

  2. model = Sequential()

  3. model.add(LSTM(75, input_shape=(n_in_seq_length, n_chars)))

  4. model.add(RepeatVector(n_out_seq_length))

  5. model.add(LSTM(50, return_sequences=True))

  6. model.add(TimeDistributed(Dense(n_chars, activation= 'softmax' )))

  7. model.compile(loss= categorical_crossentropy , optimizer= 'adam' , metrics=[ 'accuracy' ])

  8. print(model.summary())

表 9.23 定義並編譯Encoder-Decoder LSTM的例子

執行示例列印網路結構的摘要。我們可以看到,編碼器將輸出一個固定大小的向量,對於給定的輸入序列長度為75。該序列被重複2次,以提供75個特徵的2個時間步長序列到解碼器。解碼器將50個特徵的兩個時間步長輸入到Dense輸出層,透過一個TimeDistributed wrapper一次處理這些輸出,以每次輸出一個編碼字元。

  1. _________________________________________________________________

  2. Layer (type)                 Output Shape              Param #  

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

  4. lstm_1 (LSTM)                (None, 75)                26400    

  5. _________________________________________________________________

  6. repeat_vector_1 (RepeatVecto (None, 2, 75)             0        

  7. _________________________________________________________________

  8. lstm_2 (LSTM)                (None, 2, 50)             25200    

  9. _________________________________________________________________

  10. time_distributed_1 (TimeDist (None, 2, 12)             612      

  11. =================================================================

  12. Total params: 52,212

  13. Trainable params: 52,212

  14. Non-trainable params: 0

  15. _________________________________________________________________

  16. None

表 9.24 定義和編譯Encoder-Decoder LSTM輸出的例子

9.5 擬合模型

該模型適合於75000個隨機生成的輸入輸出對例項的單個週期(epoch)。序列的數目是訓練週期(epoch)的代理。總共有75000個數,選定批次大小(batch size)為32個是透過一次次嘗試和錯誤得來的,並不是一個最佳的配置。

  1. # fit LSTM

  2. X, y = generate_data(75000, n_terms, largest, alphabet)

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

表 9.25 擬合定義的Encoder-Decoder LSTM的例子

擬合提供進度條,顯示模型在每個批次結束時的損失和準確性。該模型不需要很長的時間就可以安裝在CPU上。如果進度條干擾您的開發環境,您可以透過在fit()函式中設定verbose=0來關閉它。

  1. 75000/75000 [==============================] - 37s - loss: 0.6982 - acc: 0.7943

表 9.26 擬合定義的Encoder-Decoder LSTM的輸出的例子

9.6 評價模型

我們可以透過在100個不同的隨機產生的輸入-輸出對上生成預測來評估模型。結果將給出一般隨機生成示例的模型學習能力的估計。

  1. # evaluate LSTM

  2. X, y = generate_data(100, n_terms, largest, alphabet)

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

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

表 9.27 評價擬合Encoder-Decoder LSTM擬合的例子

執行該示例同時列印模型的log損失和準確性。由於神經網路的隨機性,您的特定值可能有所不同,但是模型的精度應該是在90%以內的。

  1. Loss: 0.128379, Accuracy: 100.000000

表 9.28 評估擬合Encoder-Decoder LSTM輸出的例子

9.7 用模型做預測

我們可以使用擬合模型進行預測。我們將演示一次做出一個預測,並提供解碼輸入、預期輸出和預測輸出的摘要。列印解碼輸出使我們對問題和模型能力有了更具體的聯絡。在這裡,我們生成10個新的隨機輸入-輸出序列對,使用每一個擬合模型進行預測,解碼所涉及的所有序列,並將它們列印到螢幕上。

  1. # predict

  2. for _ in range(10):

  3.    # generate an input-output pair

  4.    X, y = generate_data(1, n_terms, largest, alphabet)

  5.    # make prediction yhat = model.predict(X, verbose=0)

  6.    # decode input, expected and predicted

  7.    in_seq = invert(X[0], alphabet)

  8.    out_seq = invert(y[0], alphabet)

  9.    predicted = invert(yhat[0], alphabet)

  10.    print( '%s = %s (expect %s)' % (in_seq, predicted, out_seq))

表 9.29 使用Encoder-Decoder LSTM做預測的例子

執行該示例表明,模型使大部分序列正確。你生成的葉鼎序列和模型的學習能力在10個例子中會有所不同。嘗試執行預測幾次,以獲得良好的模型行為的感覺。

  1. 9+10+9 = 27 (expect 28)

  2. 9+6+9 = 24 (expect 24)

  3. 8+9+10 = 27 (expect 27)

  4. 9+9+10 = 28 (expect 28)

  5. 2+4+5 = 11 (expect 11)

  6. 2+9+7 = 18 (expect 18)

  7. 7+3+2 = 12 (expect 12)

  8. 4+1+4 = 9 (expect 9)

  9. 8+6+7 = 21 (expect 21)

  10. 5+2+7 = 14 (expect 14)

表 9.30 用擬合Encoder-Decoder LSTM做預測輸出的例子

9.8 完整例子

為了完整性,我們將全部的程式碼列表提供如下供你參考。

  1. from random import seed

  2. from random import randint

  3. from numpy import array

  4. from math import ceil

  5. from math import log10

  6. from math import sqrt

  7. from numpy import argmax

  8. from keras.models import Sequential

  9. from keras.layers import Dense

  10. from keras.layers import LSTM

  11. from keras.layers import TimeDistributed

  12. from keras.layers import RepeatVector

  13. # generate lists of random integers and their sum

  14. def random_sum_pairs(n_examples, n_numbers, largest):

  15.    X, y = list(), list()

  16.    for i in range(n_examples):

  17.        in_pattern = [randint(1,largest) for _ in range(n_numbers)]

  18.        out_pattern = sum(in_pattern)

  19.        X.append(in_pattern)

  20.        y.append(out_pattern)

  21.    return X, y

  22. # convert data to strings

  23. def to_string(X, y, n_numbers, largest):

  24.    max_length = n_numbers * ceil(log10(largest+1)) + n_numbers - 1

  25.    Xstr = list()

  26.    for pattern in X:

  27.        strp = '+' .join([str(n) for n in pattern])

  28.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  29.        Xstr.append(strp)

  30.    max_length = ceil(log10(n_numbers * (largest+1)))

  31.    ystr = list()

  32.    for pattern in y:

  33.        strp = str(pattern)

  34.        strp = ''.join(['' for _ in range(max_length-len(strp))]) + strp

  35.        ystr.append(strp)

  36.    return Xstr, ystr

  37. # integer encode strings

  38. def integer_encode(X, y, alphabet):

  39.    char_to_int = dict((c, i) for i, c in enumerate(alphabet))

  40.    Xenc = list()

  41.    for pattern in X:

  42.        integer_encoded = [char_to_int[char] for char in pattern]

  43.        Xenc.append(integer_encoded)

  44.    yenc = list()

  45.    for pattern in y:

  46.        integer_encoded = [char_to_int[char] for char in pattern]

  47.        yenc.append(integer_encoded)

  48.    return Xenc, yenc

  49. # one hot encode

  50. def one_hot_encode(X, y, max_int):

  51.    Xenc = list()

  52.    for seq in X: pattern = list()

  53.    for index in seq:

  54.        vector = [0 for _ in range(max_int)]

  55.        vector[index] = 1

  56.        pattern.append(vector)

  57.        Xenc.append(pattern)

  58.    yenc = list()

  59.    for seq in y:

  60.        pattern = list()

  61.        for index in seq:

  62.            vector = [0 for _ in range(max_int)]

  63.            vector[index] = 1

  64.            pattern.append(vector)

  65.        yenc.append(pattern)

  66.    return Xenc, yenc

  67. # generate an encoded dataset

  68. def generate_data(n_samples, n_numbers, largest, alphabet):

  69.    # generate pairs

  70.    X, y = random_sum_pairs(n_samples, n_numbers, largest)

  71.    # convert to strings

  72.    X, y = to_string(X, y, n_numbers, largest)

  73.    # integer encode

  74.    X, y = integer_encode(X, y, alphabet)

  75.    # one hot encode

  76.    X, y = one_hot_encode(X, y, len(alphabet))

  77.    # return as numpy arrays

  78.    X, y = array(X), array(y)

  79.    return X, y

  80. # invert encoding

  81. def invert(seq, alphabet):

  82.    int_to_char = dict((i, c) for i, c in enumerate(alphabet))

  83.    strings = list()

  84.    for pattern in seq:

  85.        string = int_to_char[argmax(pattern)]

  86.        strings.append(string)

  87.    return ''.join(strings)

  88. # configure problem

  89. # number of math terms

  90. n_terms = 3

  91. # largest value for any single input digit

  92. largest = 10

  93. # scope of possible symbols for each input or output time step

  94. alphabet = [str(x) for x in range(10)] + [ '+' , ' ']

  95. # size of alphabet: (12 for 0-9, + and )

  96. n_chars = len(alphabet)

  97. # length of encoded input sequence (8 for 10+10+10)

  98. n_in_seq_length = n_terms * ceil(log10(largest+1)) + n_terms - 1

  99. # length of encoded output sequence (2 for 30 )

  100. n_out_seq_length = ceil(log10(n_terms * (largest+1)))

  101. # define LSTM

  102. model = Sequential()

  103. model.add(LSTM(75, input_shape=(n_in_seq_length, n_chars)))

  104. model.add(RepeatVector(n_out_seq_length))

  105. model.add(LSTM(50, return_sequences=True))

  106. model.add(TimeDistributed(Dense(n_chars, activation= 'softmax' )))

  107. model.compile(loss= 'categorical_crossentropy' , optimizer= 'adam' , metrics=[ 'accuracy' ])

  108. print(model.summary())

  109. # fit LSTM

  110. X, y = generate_data(75000, n_terms, largest, alphabet)

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

  112. # evaluate LSTM

  113. X, y = generate_data(100, n_terms, largest, alphabet)

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

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

  116. # predict

  117. for _ in range(10):

  118.    # generate an input-output pair

  119.    X, y = generate_data(1, n_terms, largest, alphabet)

  120.    # make prediction

  121.    yhat = model.predict(X, verbose=0)

  122.    # decode input, expected and predicted

  123.    in_seq = invert(X[0], alphabet)

  124.    out_seq = invert(y[0], alphabet)

  125.    predicted = invert(yhat[0], alphabet)

  126.    print('%s = %s (expect %s)' % (in_seq, predicted, out_seq))

表 9.31 Encoder-Decoder LSTM在加法預測問題上的完整例子

9.9 擴充套件閱讀

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

9.9.1 Encoder-Decoder LSTM論文

  • Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014.

  • [Sequence to Sequence Learning with Neural Networks, 2014. https://arxiv.org/abs/1409.3215

  • Show and Tell: A Neural Image Caption Generator, 2014.

  • Learning to Execute, 2015.

  • {A Neural Conversational Model, 2015.](https://arxiv.org/abs/1506.05869)

9.9.2 Keras API

  • RepeatVector Keras API.

  • TimeDistributed Keras API.

9.10 擴充套件

你想更深度地瞭解Encoder-Decoder LSTMs嗎?本章節列出了本課程的一些具有挑戰性的擴充套件。

  • 列出10個可以從Encoder-Decoder LSTM結構中獲益的序列到序列預測問題;

  • 增加terms的數量或者數字的數量,並調整模型以獲得100%的準確度;

  • 設計一個比較模型大小與問題序列問題複雜度(term和/或數字)的研究;

  • 更新示例以支援給定例項中的可變數量的術語,並調整模型以獲得100%的準確度。

  • 增加對其他數學運算的支援,例如減法、除法和乘法。

9.11 總結

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

  • Encoder-Decoder LSTM的結構以及怎麼樣在Keras中實現它;

  • 加法序列到序列的預測問題;

  • 怎麼樣開發一個Encoder-Decoder LSTM模型用來解決加法seq2seq預測問題。

在下一個章節中,我們將會學習到怎麼樣開發並評估一個Bidirectional LSTM模型。

相關文章