通過keras例子理解LSTM 循環神經網絡(RNN)

博文的翻譯和實踐:

Understanding Stateful LSTM Recurrent Neural Networks in Python with Keras

正文

一個強大而流行的循環神經網絡(RNN)的變種是長短期模型網絡(LSTM)。

它使用廣泛,因爲它的架構克服了困擾着所有週期性的神經網絡梯度消失梯度爆炸的問題,允許創建非常大的、非常深的網絡。

與其他週期性的神經網絡一樣,LSTM網絡保持狀態,在keras框架中實現這一點的細節可能會令人困惑。

在這篇文章中,您將會確切地瞭解到在LSTM網絡中,如何在LSTM深度學習庫中維護狀態。

 本文目標:

  1. 怎麼在keras上實習一個普通的lstm循環神經網絡
  2. 在lstm中怎樣小心的利用好時間狀態特徵
  3. 怎樣在lstm上實現狀態的預測

本文在一個很簡單的例子上說明lstm的使用和lstm的特點,通過對這個簡化例子的理解,可以幫助我們對一般的序列預測問題和序列預測問題有更高的理解和使用。
用到的庫:Keras 2.0.2,TensorFlow 1.0.1Theano 0.9.0.

問題描述:學習字母

在本教程中,我們將開發和對比許多不同的LSTM循環神經網絡模型。

這些比較的背景是學習字母表的一個簡單的序列預測問題。也就是說,根據字母表的字母,可以預測字母表的下一個字母。

這是一個簡單的序列預測問題,一旦被理解,就可以被推廣到其他的序列預測問題,如時間序列預測和序列分類。

讓我們用一些python代碼來準備這個問題,我們可以從示例中重用這些代碼。

首先,讓我們導入本教程中計劃使用的所有類和函數。

import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils

接下來,我們可以對隨機數生成器選定隨機數種子,以確保每次執行代碼時結果都是相同的。

# fix random seed for reproducibility
numpy.random.seed(7)

我們現在可以定義我們的數據集,字母表。爲了便於閱讀,我們用大寫字母來定義字母表。

神經網絡是對數字建模,因此我們需要將字母表中的字母映射到整數值(把字母映射爲數字)。我們可以很容易地通過創建字母索引的字典(map)到字符。我們還可以創建一個反向查找,以便將預測轉換回字符,以便稍後使用。

# define the raw dataset
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# create mapping of characters to integers (0-25) and the reverse
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))

現在我們需要創建我們的輸入和輸出鍵值對來訓練我們的神經網絡。我們可以通過定義輸入序列長度,然後從輸入字母序列中讀取序列來實現這一點。

例如,我們使用的輸入長度是1。從原始輸入數據的開始,我們可以讀出第一個字母A和下一個字母“B”。我們沿着一個字符移動,直到我們到達一個“Z”的預測。

我們先創造這樣一個數據集,用一個字母,來預測下一個字母是什麼。

# prepare the dataset of input to output pairs encoded as integers
seq_length = 1
dataX = []
dataY = []
for i in range(0, len(alphabet) - seq_length, 1):
    seq_in = alphabet[i:i + seq_length]
    seq_out = alphabet[i + seq_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])
    print seq_in, '->', seq_out

我們運行上面的代碼,來觀察現在我們的input和output數據集是這樣一種情況

A -> B
B -> C
C -> D
D -> E
E -> F
F -> G
G -> H
H -> I
I -> J
J -> K
K -> L
L -> M
M -> N
N -> O
O -> P
P -> Q
Q -> R
R -> S
S -> T
T -> U
U -> V
V -> W
W -> X
X -> Y
Y -> Z

input是一個一個字母的序列,output是一個一個的序列。
ok,就在這樣的數據集上來應用我們的lstm。看看會有什麼結果?

這時候dataX是一個一個用字母組成的序列,但是還要轉換一下格式,才能用到keras上。我們需要將NumPy數組重新構造爲LSTM網絡所期望的格式,即[samples示例, time steps時間步數, features特徵]。

# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (len(dataX), seq_length, 1))

然後我們需要把我們的整數值歸一化到0~1的區間上,這是LSTM網絡使用的s形激活函數(sigmoid)的範圍。

# normalize
X = X / float(len(alphabet))

最後,我們可以把這個問題看作是一個序列分類任務,其中26個字母代表一個不同的類。因此,我們用keras的內置的 to_categorical()函數把輸出output(y)進行 one-hot編碼(one-hot指n維單位向量a=(0,…,0,1,0,…,0))作爲輸出層的結果。

# one hot encode the output variable
y = np_utils.to_categorical(dataY)

現在我們已經準備好去訓練不同的LSTM模型了。

 單字符——單字符的映射的簡單LSTM

讓我們從設計一個簡單的LSTM開始,學習如何根據一個字符的上下文來預測字母表中的下一個字符。

我們將定義這個問題爲:一些單字母的隨機集合作爲輸入,另一些單字母作爲輸出,由輸入輸出對組成。正如我們所看到的,這對於LSTM來說是一個很難用來學習的結構。

讓我們定義一個LSTM網絡,它有32個單元(the LSTM units are the “memory units” or you can just call them the neurons.),一個輸出層,其中有一個softmax的激活函數來進行預測。由於這是一個多類分類問題,所以我們可以使用在Keras中使用對數損失函數(稱爲“分類交叉熵”(categorical_crossentropy)),並使用ADAM優化函數對網絡進行優化。

該模型以500批次(epochs),每批次數據輸入大小(batch)爲1的形式訓練

我們通過lstm在這個問題上的預測,會發現這對lstm循環網絡來說是很難解決的問題。

keras上LSTM用於上述問題的代碼如下:

# create and fit the model
model = Sequential()
model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, nb_epoch=500, batch_size=1, verbose=2)


在我們訓練模型之後,我們可以對整個訓練集的性能進行評估和總結。

# summarize performance of the model
scores = model.evaluate(X, y, verbose=0)
print("Model Accuracy: %.2f%%" % (scores[1]*100))

然後,我們可以通過網絡重新運行訓練數據,並生成預測,將輸入和輸出對轉換回原來的字符格式,以獲得關於網絡如何瞭解問題的視覺效果。

# demonstrate some model predictions
for pattern in dataX:
    x = numpy.reshape(pattern, (1, len(pattern), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = int_to_char[index]
    seq_in = [int_to_char[value] for value in pattern]
    print seq_in, "->", result



我們可以看到,這個問題對於網絡來說確實是很困難的。
原因是可憐的lstm單元根本沒有可以利用的上下文章信息。
每個輸入輸出模式都以隨機的順序顯示在網絡中,並且網絡的狀態在每個模式之後被重置(每個批處理的每個批次包含一個模式)。

這是對LSTM網絡架構的濫用,因爲我們把它當作了一個標準的多層感知器。

接下來,讓我們嘗試一個不同的問題框架,以便爲網絡提供更多的序列來學習。

 

三字符特徵——單字符的映射的簡單LSTM

在多層感知器中添加更多上下文最流行的方法是特徵窗口方法(Feature Window method)。

即序列中的前面步驟的輸出被作爲附加的輸入特性提供給網絡。我們可以用相同的技巧,爲LSTM網絡提供更多的上下文。

在這裏,我們將序列長度從1增加到3,例如:
我們把輸入從一個字符升到三個字符。

# prepare the dataset of input to output pairs encoded as integers
seq_length = 3

就像這樣:

ABC -> D
BCD -> E
CDE -> F

然後將序列中的每個元素作爲網絡的一個新輸入特性提供給它。這需要修改輸入序列在數據準備步驟中的reshape:

# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (len(dataX), 1, seq_length))

還需要對示例模式的reshape進行修改,以展示模型的預測結果。

x = numpy.reshape(pattern, (1, 1, len(pattern)))

全部的代碼如下:

# Naive LSTM to learn three-char window to one-char mapping
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils
# fix random seed for reproducibility
numpy.random.seed(7)
# define the raw dataset
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# create mapping of characters to integers (0-25) and the reverse
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))
# prepare the dataset of input to output pairs encoded as integers
seq_length = 3
dataX = []
dataY = []
for i in range(0, len(alphabet) - seq_length, 1):
    seq_in = alphabet[i:i + seq_length]
    seq_out = alphabet[i + seq_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])
    print seq_in, '->', seq_out
# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (len(dataX), 1, seq_length))
# normalize
X = X / float(len(alphabet))
# one hot encode the output variable
y = np_utils.to_categorical(dataY)
# create and fit the model
model = Sequential()
model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, epochs=500, batch_size=1, verbose=2)
# summarize performance of the model
scores = model.evaluate(X, y, verbose=0)
print("Model Accuracy: %.2f%%" % (scores[1]*100))
# demonstrate some model predictions
for pattern in dataX:
    x = numpy.reshape(pattern, (1, 1, len(pattern)))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = int_to_char[index]
    seq_in = [int_to_char[value] for value in pattern]
    print seq_in, "->", result

運行結果如下:

Model Accuracy: 86.96%
['A', 'B', 'C'] -> D
['B', 'C', 'D'] -> E
['C', 'D', 'E'] -> F
['D', 'E', 'F'] -> G
['E', 'F', 'G'] -> H
['F', 'G', 'H'] -> I
['G', 'H', 'I'] -> J
['H', 'I', 'J'] -> K
['I', 'J', 'K'] -> L
['J', 'K', 'L'] -> M
['K', 'L', 'M'] -> N
['L', 'M', 'N'] -> O
['M', 'N', 'O'] -> P
['N', 'O', 'P'] -> Q
['O', 'P', 'Q'] -> R
['P', 'Q', 'R'] -> S
['Q', 'R', 'S'] -> T
['R', 'S', 'T'] -> U
['S', 'T', 'U'] -> V
['T', 'U', 'V'] -> Y
['U', 'V', 'W'] -> Z
['V', 'W', 'X'] -> Z
['W', 'X', 'Y'] -> Z

我們發現有了一點點的提升,但是這一點點的提升未必是真的,梯度下降算法本來就是具有隨機性的。

也就是說我們再一次的錯誤使用了lstm循環神經網絡。
我們確實給了上下文,但是並不是合適的方式,
實際上,字母序列A-B-C纔是一個特徵的timesteps,而不是單獨ABC一個特徵的timestep
我們已經給網絡提供了更多的上下文,但並沒有像預期的那樣有更多的順序。

在下一節中,我們將以timesteps的形式爲網絡提供更多的上下文。

keras實踐循環的正確打開方式!

在keras中,利用lstm的關鍵是以時間序列(time steps)的方法來提供上下文,而不是像其他網絡結構(CNN)一樣,通過windowed features的方式。

這次我們還是採用這樣的訓練方式

seq_length = 3

輸入輸出對(input-output pairs)

ABC -> D
BCD -> E
CDE -> F
DEF -> G

我們這次唯一改變的地方是下面這裏:

# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (len(dataX), seq_length, 1))

timesteps這個參數,我們設置了3,而不是前面的1。

不同之處是,對輸入數據的reshape是將輸入序列作爲一個特性的time step序列,而不是多個特性的單一time step。
也就是說我們把ABC 看成獨立的一個特徵組成的多個時間序列,而不是把ABC看成一個多個特徵組成一個時間序列。

這就是keras中LSTM循環神經網絡的正確打開的方式。
我的理解是,這樣在訓練 ABC——D的時候,BCD,CDE,都可以發揮作用。而最開始那種使用方法,只是利用了ABC——D這樣一個訓練樣本。

完整代碼如下:

# Naive LSTM to learn three-char time steps to one-char mapping
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils
# fix random seed for reproducibility
numpy.random.seed(7)
# define the raw dataset
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# create mapping of characters to integers (0-25) and the reverse
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))
# prepare the dataset of input to output pairs encoded as integers
seq_length = 3
dataX = []
dataY = []
for i in range(0, len(alphabet) - seq_length, 1):
    seq_in = alphabet[i:i + seq_length]
    seq_out = alphabet[i + seq_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])
    print seq_in, '->', seq_out
# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
# normalize
X = X / float(len(alphabet))
# one hot encode the output variable
y = np_utils.to_categorical(dataY)
# create and fit the model
model = Sequential()
model.add(LSTM(32, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, nb_epoch=500, batch_size=1, verbose=2)
# summarize performance of the model
scores = model.evaluate(X, y, verbose=0)
print("Model Accuracy: %.2f%%" % (scores[1]*100))
# demonstrate some model predictions
for pattern in dataX:
    x = numpy.reshape(pattern, (1, len(pattern), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = int_to_char[index]
    seq_in = [int_to_char[value] for value in pattern]
    print seq_in, "->", result

最終的訓練結果是

Model Accuracy: 100.00%
['A', 'B', 'C'] -> D
['B', 'C', 'D'] -> E
['C', 'D', 'E'] -> F
['D', 'E', 'F'] -> G
['E', 'F', 'G'] -> H
['F', 'G', 'H'] -> I
['G', 'H', 'I'] -> J
['H', 'I', 'J'] -> K
['I', 'J', 'K'] -> L
['J', 'K', 'L'] -> M
['K', 'L', 'M'] -> N
['L', 'M', 'N'] -> O
['M', 'N', 'O'] -> P
['N', 'O', 'P'] -> Q
['O', 'P', 'Q'] -> R
['P', 'Q', 'R'] -> S
['Q', 'R', 'S'] -> T
['R', 'S', 'T'] -> U
['S', 'T', 'U'] -> V
['T', 'U', 'V'] -> W
['U', 'V', 'W'] -> X
['V', 'W', 'X'] -> Y
['W', 'X', 'Y'] -> Z

它已經學會了用字母表中的三個字母來預測下一個字母的順序。它可以顯示字母表中的任意三個字母的隨機序列,並預測下一個字母。

我們還沒有展示出循環神經網絡的強大之處,因爲上面這個問題我們用多層感知器,足夠多的神經元,足夠多的迭代次數也可以很好的解決。(三層神經網絡擬合任意可以表示的函數)

LSTM網絡是有狀態的。它們應該能夠學習整個字母表序列,但是在默認情況下,keras在每次訓練之後重新設置網絡狀態。


那麼接下來就是展示循環神經網絡的獨到之處!!

一個批處理中的LSTM狀態

keras實現的LSTM在每一個batch以後,都重置了LSTM的狀態。

這表明,如果我們的批處理大小足夠容納所有輸入模式,如果所有輸入模式都按順序排序,LSTM就可以使用序列中的序列上下文來更好地學習序列。

通過修改第一個示例來學習一對一映射,並將批處理大小從1增加到訓練數據集的大小,我們可以很容易地演示這一點。

此外,在每個epoch前,keras都重置了訓練數據集。爲了確保訓練數據模式保持順序,我們可以禁用這種洗牌。

model.fit(X, y, epochs=5000, batch_size=len(dataX), verbose=2, shuffle=False)

該網絡將使用 within-batch批序列來學習字符的映射,但在進行預測時,這個上下文將無法用於網絡。我們可以對網絡進行評估,以確定網絡在隨機序列和順序序列的預測能力。

完整代碼如下:

 Naive LSTM to learn one-char to one-char mapping with all data in each batch
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils
from keras.preprocessing.sequence import pad_sequences
# fix random seed for reproducibility
numpy.random.seed(7)
# define the raw dataset
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# create mapping of characters to integers (0-25) and the reverse
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))
# prepare the dataset of input to output pairs encoded as integers
seq_length = 1
dataX = []
dataY = []
for i in range(0, len(alphabet) - seq_length, 1):
    seq_in = alphabet[i:i + seq_length]
    seq_out = alphabet[i + seq_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])
    print seq_in, '->', seq_out
# convert list of lists to array and pad sequences if needed
X = pad_sequences(dataX, maxlen=seq_length, dtype='float32')
# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (X.shape[0], seq_length, 1))
# normalize
X = X / float(len(alphabet))
# one hot encode the output variable
y = np_utils.to_categorical(dataY)
# create and fit the model
model = Sequential()
model.add(LSTM(16, input_shape=(X.shape[1], X.shape[2])))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, epochs=5000, batch_size=len(dataX), verbose=2, shuffle=False)
# summarize performance of the model
scores = model.evaluate(X, y, verbose=0)
print("Model Accuracy: %.2f%%" % (scores[1]*100))
# demonstrate some model predictions
for pattern in dataX:
    x = numpy.reshape(pattern, (1, len(pattern), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = int_to_char[index]
    seq_in = [int_to_char[value] for value in pattern]
    print seq_in, "->", result
# demonstrate predicting random patterns
print "Test a Random Pattern:"
for i in range(0,20):
    pattern_index = numpy.random.randint(len(dataX))
    pattern = dataX[pattern_index]
    x = numpy.reshape(pattern, (1, len(pattern), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = int_to_char[index]
    seq_in = [int_to_char[value] for value in pattern]
    print seq_in, "->", result

結果:

Model Accuracy: 100.00%
['A'] -> B
['B'] -> C
['C'] -> D
['D'] -> E
['E'] -> F
['F'] -> G
['G'] -> H
['H'] -> I
['I'] -> J
['J'] -> K
['K'] -> L
['L'] -> M
['M'] -> N
['N'] -> O
['O'] -> P
['P'] -> Q
['Q'] -> R
['R'] -> S
['S'] -> T
['T'] -> U
['U'] -> V
['V'] -> W
['W'] -> X
['X'] -> Y
['Y'] -> Z
Test a Random Pattern:
['T'] -> U
['V'] -> W
['M'] -> N
['Q'] -> R
['D'] -> E
['V'] -> W
['T'] -> U
['U'] -> V
['J'] -> K
['F'] -> G
['N'] -> O
['B'] -> C
['M'] -> N
['F'] -> G
['F'] -> G
['P'] -> Q
['A'] -> B
['K'] -> L
['W'] -> X
['E'] -> F

正如我們所期望的那樣,網絡能夠使用 within-sequence的上下文來學習字母表,在訓練數據上達到100%的準確率。

重要的是,該網絡可以對隨機選擇的字符的下一個字母進行準確的預測。非常令人印象深刻。

單字符——單字符的映射的有狀態LSTM

我們已經看到,我們可以將原始數據拆分爲固定大小的序列,並且這種表示可以由LSTM來學習,且只需要學習3個字符到1個字符的隨機映射。

我們也看到,我們可以對批量的大小進行限制,爲網絡提供更多的序列,但只有在訓練期間才行。

理想情況下,我們希望將網絡公開給整個序列,並讓它學習相互依賴關係,而不是在問題的框架中明確地定義這些依賴關係。

我們可以在keras中做到這一點,通過使LSTM層擁有狀態,並在epoch結束時手動重新設置網絡的狀態,這時也結束了訓練整個序列的過程。

這纔是LSTM網絡的真正用途。我們發現,如果允許網絡本身學習字符之間的依賴關係,我們只需要一個更小的網絡(一半的單位數量)和更少的訓練期(幾乎是一半)。

首先我們需要將LSTM層定義爲有狀態的。這樣做的話,我們必須顯式地指定批大小作爲輸入形狀的一個維度。這也意味着當我們評估網絡或用網絡進行預測時,我們也必須指定並遵守相同的批大小。現在這不是問題,因爲我們使用的是批大小的1。這可能會在預測的時候帶來困難,因爲當批大小不是1時,預測需要按批進行和按順序進行。

batch_size = 1
model.add(LSTM(16, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))

訓練有狀態的LSTM的一個重要區別是,我們每次都手動地訓練它,並且在每個時代之後重新設置狀態。我們可以在for循環中這樣做。同樣,我們不會對輸入進行洗牌,保留輸入訓練數據創建的順序。

for i in range(300):
    model.fit(X, y, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
    model.reset_states()

如前所述,在評估整個培訓數據集的網絡性能時,我們指定批處理大小。

# summarize performance of the model
scores = model.evaluate(X, y, batch_size=batch_size, verbose=0)
model.reset_states()
print("Model Accuracy: %.2f%%" % (scores[1]*100))

最後,我們可以證明網絡確實學會了整個字母表。我們可以用第一個字母A“A”來做輸入,獲得一個預測,把預測作爲輸入反饋給它,然後把這個過程一直重複到“Z”。

# demonstrate some model predictions
seed = [char_to_int[alphabet[0]]]
for i in range(0, len(alphabet)-1):
    x = numpy.reshape(seed, (1, len(seed), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    print int_to_char[seed[0]], "->", int_to_char[index]
    seed = [index]
model.reset_states()

我們也可以看看這個網絡是否可以從任意的字母開始預測

# demonstrate a random starting point
letter = "K"
seed = [char_to_int[letter]]
print "New start: ", letter
for i in range(0, 5):
    x = numpy.reshape(seed, (1, len(seed), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    print int_to_char[seed[0]], "->", int_to_char[index]
    seed = [index]
model.reset_states()

完整代碼如下:

# Stateful LSTM to learn one-char to one-char mapping
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils
# fix random seed for reproducibility
numpy.random.seed(7)
# define the raw dataset
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# create mapping of characters to integers (0-25) and the reverse
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))
# prepare the dataset of input to output pairs encoded as integers
seq_length = 1
dataX = []
dataY = []
for i in range(0, len(alphabet) - seq_length, 1):
    seq_in = alphabet[i:i + seq_length]
    seq_out = alphabet[i + seq_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])
    print seq_in, '->', seq_out
# reshape X to be [samples, time steps, features]
X = numpy.reshape(dataX, (len(dataX), seq_length, 1))
# normalize
X = X / float(len(alphabet))
# one hot encode the output variable
y = np_utils.to_categorical(dataY)
# create and fit the model
batch_size = 1
model = Sequential()
model.add(LSTM(16, batch_input_shape=(batch_size, X.shape[1], X.shape[2]), stateful=True))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
for i in range(300):
    model.fit(X, y, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
    model.reset_states()
# summarize performance of the model
scores = model.evaluate(X, y, batch_size=batch_size, verbose=0)
model.reset_states()
print("Model Accuracy: %.2f%%" % (scores[1]*100))
# demonstrate some model predictions
seed = [char_to_int[alphabet[0]]]
for i in range(0, len(alphabet)-1):
    x = numpy.reshape(seed, (1, len(seed), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    print int_to_char[seed[0]], "->", int_to_char[index]
    seed = [index]
model.reset_states()
# demonstrate a random starting point
letter = "K"
seed = [char_to_int[letter]]
print "New start: ", letter
for i in range(0, 5):
    x = numpy.reshape(seed, (1, len(seed), 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    print int_to_char[seed[0]], "->", int_to_char[index]
    seed = [index]
model.reset_states()

output

Model Accuracy: 100.00%
A -> B
B -> C
C -> D
D -> E
E -> F
F -> G
G -> H
H -> I
I -> J
J -> K
K -> L
L -> M
M -> N
N -> O
O -> P
P -> Q
Q -> R
R -> S
S -> T
T -> U
U -> V
V -> W
W -> X
X -> Y
Y -> Z
New start:  K
K -> B
B -> C
C -> D
D -> E
E -> F

我們可以看到,網絡已經完美地記住了整個字母表。它使用了樣本的上下文,並學習了預測序列中下一個字符所需要的依賴關係。

我們還可以看到,如果我們用第一個字母輸入網絡,它就能正確地對字母表的其他部分進行正確的理解。

我們還可以看到,它只是從一個冷啓動開始,就學會了完整的字母表順序。當要求預測“K”的下一個字母時,它會預測“B”,然後返回到整個字母表中。

爲了真正地預測“K”,網絡的狀態需要被反覆地從“A”到“J”的字母“加熱”。這告訴我們,我們也可以達到“無狀態”LSTM的效果,如果我們通過準備形如下面的訓練數據:

---a -> b
--ab -> c
-abc -> d
abcd -> e

輸入序列固定在25(a-y,以預測z)的位置,並且模式以 zero-padding爲前綴。

最後,這提出了另一個問題,即是否可以使用可變長度的輸入序列來訓練LSTM網絡,以預測下一個字符。

可變長度輸入——單字符輸出的LSTM

在上一節中,我們發現keras的“有狀態的”LSTM實際上只是重新播放第一個n序列的一個快捷方式,並沒有真正學習一個通用的字母表模型。

在這一節中,我們將探索一個“無狀態”LSTM的變體,它學習了字母表中的隨機子序列,並可以根據任意字母或字母序列去預測字母表中的下一個字母。

首先,我們改變問題的框架。爲了簡化,我們定義一個最大的輸入序列長度(maximum input sequence length),並將其設置爲5這樣的小值來加速訓練。這就定義了(用於訓練的字母表的)子序列的最大長度。在擴展中,如果我們允許循環回到序列的開始,這就可以設置爲完整的字母表(26)或更長。

我們還需要定義要創建的隨機序列的數量,在本例中爲1000。這也可能是更多或更少。我希望實際需要的模式更少。

# prepare the dataset of input to output pairs encoded as integers
num_inputs = 1000
max_len = 5
dataX = []
dataY = []
for i in range(num_inputs):
    start = numpy.random.randint(len(alphabet)-2)
    end = numpy.random.randint(start, min(start+max_len,len(alphabet)-1))
    sequence_in = alphabet[start:end+1]
    sequence_out = alphabet[end + 1]
    dataX.append([char_to_int[char] for char in sequence_in])
    dataY.append(char_to_int[sequence_out])
    print sequence_in, '->', sequence_out

輸入大概像這樣

PQRST -> U
W -> X
O -> P
OPQ -> R
IJKLM -> N
QRSTU -> V
ABCD -> E
X -> Y
GHIJ -> K

輸入序列的長度在1和maxlen之間變化,因此需要zero padding(零填充)。在這裏,我們使用了left-hand-side (prefix) padding和 keras自帶的pad_sequences()函數。

X = pad_sequences(dataX, maxlen=max_len, dtype='float32')

訓練模型在隨機選擇的輸入模式下進行評估。這可以很容易地成爲新的隨機生成的字符序列。我認爲,這也可以是一個線性序列,用“A”作爲單個字符輸入的輸出。

# LSTM with Variable Length Input Sequences to One Character Output
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.utils import np_utils
from keras.preprocessing.sequence import pad_sequences
from theano.tensor.shared_randomstreams import RandomStreams
# fix random seed for reproducibility
numpy.random.seed(7)
# define the raw dataset
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# create mapping of characters to integers (0-25) and the reverse
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))
# prepare the dataset of input to output pairs encoded as integers
num_inputs = 1000
max_len = 5
dataX = []
dataY = []
for i in range(num_inputs):
    start = numpy.random.randint(len(alphabet)-2)
    end = numpy.random.randint(start, min(start+max_len,len(alphabet)-1))
    sequence_in = alphabet[start:end+1]
    sequence_out = alphabet[end + 1]
    dataX.append([char_to_int[char] for char in sequence_in])
    dataY.append(char_to_int[sequence_out])
    print sequence_in, '->', sequence_out
# convert list of lists to array and pad sequences if needed
X = pad_sequences(dataX, maxlen=max_len, dtype='float32')
# reshape X to be [samples, time steps, features]
X = numpy.reshape(X, (X.shape[0], max_len, 1))
# normalize
X = X / float(len(alphabet))
# one hot encode the output variable
y = np_utils.to_categorical(dataY)
# create and fit the model
batch_size = 1
model = Sequential()
model.add(LSTM(32, input_shape=(X.shape[1], 1)))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, epochs=500, batch_size=batch_size, verbose=2)
# summarize performance of the model
scores = model.evaluate(X, y, verbose=0)
print("Model Accuracy: %.2f%%" % (scores[1]*100))
# demonstrate some model predictions
for i in range(20):
    pattern_index = numpy.random.randint(len(dataX))
    pattern = dataX[pattern_index]
    x = pad_sequences([pattern], maxlen=max_len, dtype='float32')
    x = numpy.reshape(x, (1, max_len, 1))
    x = x / float(len(alphabet))
    prediction = model.predict(x, verbose=0)
    index = numpy.argmax(prediction)
    result = int_to_char[index]
    seq_in = [int_to_char[value] for value in pattern]
    print seq_in, "->", result

output

Model Accuracy: 98.90%
['Q', 'R'] -> S
['W', 'X'] -> Y
['W', 'X'] -> Y
['C', 'D'] -> E
['E'] -> F
['S', 'T', 'U'] -> V
['G', 'H', 'I', 'J', 'K'] -> L
['O', 'P', 'Q', 'R', 'S'] -> T
['C', 'D'] -> E
['O'] -> P
['N', 'O', 'P'] -> Q
['D', 'E', 'F', 'G', 'H'] -> I
['X'] -> Y
['K'] -> L
['M'] -> N
['R'] -> T
['K'] -> L
['E', 'F', 'G'] -> H
['Q'] -> R
['Q', 'R', 'S'] -> T

我們可以看到,儘管這個模型沒有從隨機生成的子序列中完美地學習字母表,但它做得很好。該模型沒有進行調整,可能需要更多的訓練或更大的網絡,或者兩者都需要(爲讀者提供一個練習)。

這是一個很好的自然擴展,對於“每個批處理中的所有順序輸入示例”,都可以在上面學到,它可以處理特殊的查詢,但是這一次是任意的序列長度(最多的是最大長度)。

總結

這篇文章你應該學會了:

  • 如何開發一個簡單的LSTM網絡,一個字符到一個字符的預測。
  • 如何配置一個簡單的LSTM,以在一個示例中跨時間步驟學習一個序列。
  • 如何配置LSTM來通過手動管理狀態來學習跨示例的序列。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章