Keras : Ex-Tutorials : ステートフル LSTM リカレント・ニューラルネットの理解

Keras : Ex-Tutorials : ステートフル LSTM リカレント・ニューラルネットの理解 (翻訳/解説)

翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 06/26/2018 (2.2.0)

* 本ページは、Keras 開発チーム推奨の外部チュートリアル・リソースの一つ : “Understanding Stateful LSTM Recurrent Neural Networks in Python with Keras” を題材にしてまとめ直したものです:

* サンプルコードの動作確認はしておりますが、適宜、追加改変している場合もあります。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

本文

パワフルなリカレント・ニューラルネット LSTM は広く使用されています。その理由は端的に言えば、そのアーキテクチャは総てのリカレント・ニューラルネットを悩ませる勾配消失問題を乗り越えて非常に巨大で深いネットワークの作成を可能にするからです。

LSTM ネットワークも他のリカレント・ニューラルネットのように状態を維持しますが、この記事ではその LSTM ネットワークの状態が Keras でどのように実装されて維持されるかを示します :

  • シークエンス予測問題に対して naive LSTM ネットワークをどのように開発するか。
  • LSTM ネットワークでバッチと特徴を通して状態をどのように注意深く維持するか。
  • ステートフル予測に対して LSTM ネットワークの状態をどのように手動で維持するか。

 

課題 : アルファベットの学習

この記事では多くの異なる LSTM リカレントニューラルネット・モデルを開発して対比していきます。
比較内容はアルファベットを学習する単純なシークエンス予測問題です i.e. アルファベット文字 (= letter) が与えられたときアルファベットの次の文字を予測します。これは単純なシークエンス予測問題ですが一度理解されれば、時系列予測やシークエンス分類のような他のシークエンス予測問題に一般化できます。

取り敢えず :

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)

データセット – アルファベットを定義しますが、可読性のために大文字のアルファベットで定義します。

最初にアルファベット文字を整数値にマップする必要がありますが、文字インデックスから文字への辞書を作成することで簡単に対応します。後で予測を文字に変換するために逆引きも作成しておきます :

# 生データセットを定義します
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# 文字の数字 (0-25) へのマッピングとその逆を作成します。
char_to_int = dict((c, i) for i, c in enumerate(alphabet))
int_to_char = dict((i, c) for i, c in enumerate(alphabet))

辞書の中身を確認しておくと :

print(char_to_int)
print(int_to_char)
{'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'J': 9, 'K': 10, 'L': 11, 'M': 12, 'N': 13, 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20, 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25}
{0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'F', 6: 'G', 7: 'H', 8: 'I', 9: 'J', 10: 'K', 11: 'L', 12: 'M', 13: 'N', 14: 'O', 15: 'P', 16: 'Q', 17: 'R', 18: 'S', 19: 'T', 20: 'U', 21: 'V', 22: 'W', 23: 'X', 24: 'Y', 25: 'Z'}

 
ニューラルネットを訓練するために、入力と出力ペアを作成する必要があります。入力シークエンス長を定義して入力アルファベット・シークエンスからシークエンスを読むことで対処します。例えば入力長として 1 を使用する場合、生入力データの最初から始めて最初の文字 “A” と (予測としての) 次の文字 “B” を読み取ることができ、そして 1 文字ずつ移動して “Z” の予測に到達するまでこれを繰り返します :

# 整数としてエンコードされる入力から出力ペアのデータセットを準備します。
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)

検証のために入力ペアを出力させます。コードを実行すると、長さ 1 の入力シークエンスと単一の出力キャラクタが要約出力されます :

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

ここで dataX, dataY はそれぞれ :

print(dataX)
print(dataY)
[[0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23], [24]]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]

問題がなければ次に、NumPy 配列を LSTM ネットワークが想定するフォーマット [samples, time steps, features] に reshape する必要があります :

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

reshape されれば入力整数を範囲 0-1 に正規化できます。これは LSTM ネットワークにより使用される sigmoid 活性化関数の範囲です :

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

最後に、この問題はシークエンス分類タスクとして考えることができて、その場合には 26 文字各々が異なるクラスを表しています。Keras 組み込み関数 to_categorical() を使用すれば出力 (y) を one-hot エンコーディングに変換できます :

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

これで異なる LSTM モデルを fit する準備ができました。X, y からサンプリングしてみましょう :

print(X[0], X[1], "...", X[23], X[24])

print(y[0])
print(y[1])
print("...")
print(y[24])
[[ 0.]] [[ 0.03846154]] ... [[ 0.88461538]] [[ 0.92307692]]

[ 0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  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.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
...
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  1.]

 

「1 文字 => 1 文字」マッピングのための Naive LSTM

1 文字だけのコンテキストが与えられたときアルファベットの次のキャラクタをどのように予測するかを学習する単純な LSTM の設計から始めます。問題を 1-文字入力から 1-文字出力へのペアのランダム・コレクションとして構成しますが、実はこれは (後で見るように) LSTM が学習するためには困難な問題の構成です。

32 ユニットを持つ LSTM ネットワークと予測を行なうための softmax 活性化関数を持つ出力層を定義します。これは多クラス分類問題ですから、log 損失関数 (Keras では “categorical_crossentropy”) を使用して Adam 最適化関数を使用してネットワークを最適化できます。

モデルはバッチサイズ 1 で 500 エポックに渡り fit します :

# モデルを作成して fit します
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)
Epoch 1/500
 - 0s - loss: 3.2661 - acc: 0.0000e+00
Epoch 2/500
 - 0s - loss: 3.2582 - acc: 0.0000e+00
Epoch 3/500
 - 0s - loss: 3.2551 - acc: 0.0400
Epoch 4/500
 - 0s - loss: 3.2524 - acc: 0.0400
Epoch 5/500
 - 0s - loss: 3.2495 - acc: 0.0400
Epoch 6/500
 - 0s - loss: 3.2471 - acc: 0.0400
Epoch 7/500
 - 0s - loss: 3.2440 - acc: 0.0400
Epoch 8/500
 - 0s - loss: 3.2412 - acc: 0.0400
Epoch 9/500
 - 0s - loss: 3.2378 - acc: 0.0400
Epoch 10/500
 - 0s - loss: 3.2348 - acc: 0.0400
...
Epoch 491/500
 - 0s - loss: 1.7039 - acc: 0.7600
Epoch 492/500
 - 0s - loss: 1.7027 - acc: 0.8800
Epoch 493/500
 - 0s - loss: 1.7019 - acc: 0.7600
Epoch 494/500
 - 0s - loss: 1.7022 - acc: 0.8800
Epoch 495/500
 - 0s - loss: 1.6990 - acc: 0.7600
Epoch 496/500
 - 0s - loss: 1.7000 - acc: 0.8000
Epoch 497/500
 - 0s - loss: 1.6993 - acc: 0.6800
Epoch 498/500
 - 0s - loss: 1.6992 - acc: 0.7600
Epoch 499/500
 - 0s - loss: 1.6999 - acc: 0.8000
Epoch 500/500
 - 0s - loss: 1.6961 - acc: 0.8400

モデルを fit した後で、訓練データセット全体の上でパフォーマンスを評価して要約することができます :

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

それから訓練データをネットワークを通して再実行して予測を生成し、(ネットワークがどれくらい上手く問題を学習したかを可視化するために) 入力と出力ペアを元の文字フォーマットに変換し戻すことができます :

# 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)
['A'] -> B
['B'] -> B
['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'] -> Y
['X'] -> Z
['Y'] -> Z

この問題はネットワークが学習するためには実際には難しいことが見て取れます。その理由は LSTM ユニットが共同作業するためのどのようなコンテキストも持たないからです。各入力-出力パターンがランダム順序でネットワークに示されますが、各パターン (これは各バッチに相当し、各バッチは 1 つのパターンを含んでいます) の後にはネットワーク状態はリセットされます 。これは LSTM ネットワーク・アーキテクチャの明らかな誤用で標準的な多層パーセプトロンのように扱っています。

次には、更なるシークエンスをネットワークに供給するために、問題を違う構成にしてみます。

 

「3 文字特徴ウィンドウ => 1 文字マッピング」のための Naive LSTM

多層パーセプトロンのために、データに更なるコンテキストを追加する通常のアプローチはウィンドウ・メソッドです。LSTM ネットワークでも同じテクニックを試すことができます。

具体的には例えばここで、シークエンス長を 1 から 3 に増やしてやります :

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

これは次のような訓練パターンを生成するでしょう :

ABC -> D
BCD -> E
CDE -> F
DEF -> G
EFG -> H
FGH -> I
GHI -> J
HIJ -> K
IJK -> L
JKL -> M
KLM -> N
LMN -> O
MNO -> P
NOP -> Q
OPQ -> R
PQR -> S
QRS -> T
RST -> U
STU -> V
TUV -> W
UVW -> X
VWX -> Y
WXY -> Z

dataX, dataY を確認しておくと :

[[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [6, 7, 8], [7, 8, 9], [8, 9, 10], [9, 10, 11], [10, 11, 12], [11, 12, 13], [12, 13, 14], [13, 14, 15], [14, 15, 16], [15, 16, 17], [16, 17, 18], [17, 18, 19], [18, 19, 20], [19, 20, 21], [20, 21, 22], [21, 22, 23], [22, 23, 24]]
[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]

 
そしてシークエンスの各要素がネットワークへの新しい入力特徴として提供されるわけですが、これはデータを準備するステップで入力シークエンスをどのように reshape するかについて変更を必要とします :

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

X, y からサンプリングしてみます :

print(X[0], X[1], "...", X[23], X[24])

print(y[0])
print(y[1])
print("...")
print(y[24])
[[ 0.          0.03846154  0.07692308]] [[ 0.03846154  0.07692308  0.11538462]] ... [[ 0.84615385  0.88461538  0.92307692]]
[ 0.  0.  0.  1.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  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.
  0.  0.  0.  0.  0.  0.  0.  0.]
...
[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.
  0.  0.  0.  0.  0.  0.  0.  1.]

 
それはまたモデルから予測を示すときにも、サンプルパターンがどのように reshape されるかについて変更を必要とします :

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

先ほどと同じ 500 エポック訓練してみます :

Epoch 1/500
 - 1s - loss: 3.2651 - acc: 0.0000e+00
Epoch 2/500
 - 0s - loss: 3.2527 - acc: 0.0435
Epoch 3/500
 - 0s - loss: 3.2462 - acc: 0.0435
Epoch 4/500
 - 0s - loss: 3.2402 - acc: 0.0000e+00
Epoch 5/500
 - 0s - loss: 3.2339 - acc: 0.0435
...
Epoch 491/500
 - 0s - loss: 1.6574 - acc: 0.7826
Epoch 492/500
 - 0s - loss: 1.6556 - acc: 0.8261
Epoch 493/500
 - 0s - loss: 1.6567 - acc: 0.7391
Epoch 494/500
 - 0s - loss: 1.6550 - acc: 0.7391
Epoch 495/500
 - 0s - loss: 1.6499 - acc: 0.8261
Epoch 496/500
 - 0s - loss: 1.6520 - acc: 0.7391
Epoch 497/500
 - 0s - loss: 1.6502 - acc: 0.7391
Epoch 498/500
 - 0s - loss: 1.6516 - acc: 0.8261
Epoch 499/500
 - 0s - loss: 1.6490 - acc: 0.7826
Epoch 500/500
 - 0s - loss: 1.6452 - acc: 0.7826

次のような出力が得られます :

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'] -> X
['U', 'V', 'W'] -> Z
['V', 'W', 'X'] -> Z
['W', 'X', 'Y'] -> Z

本質的であるかはどうかは分かりませんがパフォーマンスの小さな向上を見ることができます。これはウィンドウ・メソッドでさえも依然として LSTM で学習できていないという単純な問題です。再度、これは問題の貧弱な構成による LSTM ネットワークの誤用です。実際には、文字のシークエンスは分離した特徴の一つのタイプステップではなくて一つの特徴のタイムステップ (群) です。

次のセクションでは、タイムステップの形式で更なるコンテキストをネットワークに与えます。

 

「3 文字タイムステップ・ウィンドウ => 1 文字」マッピングのための Naive LSTM

Keras における LSTM の意図される使用方法は、ウィンドウ化された特徴ではなくタイムステップ形式でコンテキストを提供することです。

そのため、最初の例において単にシークエンス長を 1 から 3 に変更します :

seq_length = 3

再度、これは次のような入力- 出力ペアを生成します :

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

違いは、入力データの reshape の際、シークエンスを複数の特徴の単一タイムステップではなく、一つの特徴のタイムステップ・シークエンスとして捉えることにあります :

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

これが Keras の LSTM にシークエンス・コンテキストを提供する正しい意図された使用方法です。

500 エポック訓練してみましょう :

Epoch 1/500
 - 0s - loss: 3.2700 - acc: 0.0000e+00
Epoch 2/500
 - 0s - loss: 3.2547 - acc: 0.0000e+00
Epoch 3/500
 - 0s - loss: 3.2466 - acc: 0.0000e+00
Epoch 4/500
 - 0s - loss: 3.2393 - acc: 0.0435
Epoch 5/500
 - 0s - loss: 3.2313 - acc: 0.0435
...
Epoch 491/500
 - 0s - loss: 0.2057 - acc: 1.0000
Epoch 492/500
 - 0s - loss: 0.1994 - acc: 1.0000
Epoch 493/500
 - 0s - loss: 0.2020 - acc: 1.0000
Epoch 494/500
 - 0s - loss: 0.2005 - acc: 1.0000
Epoch 495/500
 - 0s - loss: 0.2012 - acc: 1.0000
Epoch 496/500
 - 0s - loss: 0.2039 - acc: 1.0000
Epoch 497/500
 - 0s - loss: 0.1984 - acc: 1.0000
Epoch 498/500
 - 0s - loss: 0.1968 - acc: 1.0000
Epoch 499/500
 - 0s - loss: 0.1963 - acc: 1.0000
Epoch 500/500
 - 0s - loss: 0.1929 - acc: 1.0000

次のような完全な結果が得られます :

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

モデル評価とサンプル予測により実証されるようにモデルは問題を完全に学習することを見て取れます。

しかしこれはより単純な問題、つまりアルファベットの 3 文字のシークエンスから次の文字を予測することを学習しています。アルファベットからの 3 文字の任意のランダム・シークエンスを見て次の文字を予測することができます。

実際にはアルファベットを列挙できません。十分に大きい多層パーセプトロン・ネットワークはウィンドウ・メソッドを使用して同じマッピングを学習することが多分できるでしょう。

LSTM ネットワークはステートフルです。それらはアルファベット・シークエンス全体を学習することができるはずですが、Keras 実装のデフォルトでは各訓練バッチの後にネットワーク状態をリセットします。

 

バッチ内の LSTM 状態

LSTM の Keras 実装は各バッチ後にネットワークの状態をリセットします。これは次のことを提示しています: 総ての入力パターンを保持するに十分に大きなバッチサイズを持ちそして総ての入力パターンがシーケンシャルに順序付けられたならば、LSTM はシークエンスをより良く学習するためにバッチ内のシークエンスのコンテキストを使用できるでしょう。

one-to-one マッピングを学習するための最初の例を修正してバッチサイズを 1 から訓練データセットのサイズに増やすことによりこれを簡単に示すことができます。

更に、Keras は各訓練エポックの前に訓練データセットをシャッフルします。訓練データ・パターンがシーケンシャルで在り続けることを確実にするために、このシャッフリングを無効にできます :

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

ネットワークはこのバッチ内シークエンスを使用して文字のマッピングを学習しますが、このコンテキストは予測をするときにはネットワークで利用可能ではありません。

完全なコード・サンプルは以下のようなものです :

# 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:
['K'] -> L
['A'] -> B
['N'] -> O
['S'] -> T
['G'] -> H
['K'] -> L
['T'] -> U
['X'] -> Y
['G'] -> H
['Q'] -> R
['X'] -> Y
['I'] -> J
['N'] -> O
['U'] -> V
['H'] -> I
['S'] -> T
['I'] -> J
['R'] -> S
['C'] -> D
['A'] -> B

期待通りに、ネットワークはシークエンス内コンテキストを使用してアルファベットを学習することができて、訓練データで 100% 精度を獲得しています。そして重要なことに、ネットワークはランダムに選択された文字に対してアルファベットの次の文字を正確に予測できることです。非常に印象深いです。

 

「1 文字 => 1 文字」マッピングのためのステートフル LSTM

ここまで、生データを固定サイズのシークエンスに分解して得た表現は LSTM により学習可能で、しかし 3 文字の 1 文字へのランダム・マッピングを学習できるだけであることを見ました。またネットワークに更なるシークエンスを供給するためにバッチサイズを (トリックとして) 利用可能であること、しかし訓練時だけであることも見てきました。

理想的には、シークエンス全体をネットワークにさらして、そしてそれに (問題の構成内で明示的にそれらの依存を定義するよりも) 相互依存性を学習させることを望むでしょう。LSTM 層をステートフルにしてエポックの最後に (これはまた訓練シークエンスの最後でもあります) ネットワークの状態を手動でリセットすることによりこれを Keras で行なうことが可能です

これはまさに LSTM ネットワークがどのように使用されるかを意図していたかです。ネットワーク自身に文字間の依存性を学習させることを可能にすることにより、より小さいネットワーク (半数のユニット) とより少ない訓練エポック (およそ半分) だけを必要とすることを見出しました。

最初に LSTM 層をステートフルとして定義する必要があります。それを行なうには、バッチサイズを入力 shape 上の次元として明示的に指定しなければなりません。ネットワークを評価するか予測をするとき、この同じバッチサイズを指定して固着しなければならないことも意味します。バッチサイズ 1 を使用している今これは問題ではありませんが、バッチサイズが 1 でないときに予測をする際に困難を導入するかもしれません。というのは予測はバッチとシークエンス内で行われる必要があるからです。

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

ステートフル LSTM の訓練の重要な違いはそれを一度に 1 エポック手動で訓練して各エポックの後状態をリセットすることです。これを 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” をシードとしてそれに与え、予測を要求し、予測を入力として供給し戻してそして “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(1000):
	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()

次のような出力が生成されます :

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 (z を予測するために a-to-y) に固定されてパターンはゼロ・パディングでプレフィックスされます。

最後に、この事は次の文字を予測するために可変長の入力シークエンスを使用して LSTM ネットワークを訓練する問題を呼び起こします。

 

「可変長入力 => 1 文字出力」を伴う LSTM

前のセクションでは、Keras “ステートフル” LSTM が実際には最初の n-シークエンスのリプレイへのショートカットに過ぎないことを発見しましたが、アルファベットの一般的なモデルを学習するためには実際には役立ちませんでした。

このセクションでは、アルファベットのランダム・シークエンスを学習する “ステートレス” LSTM の変種を探検し、任意の文字か文字の部分シークエンスを与えられるとアルファベットの次の文字を予測できるモデルを構築する努力をします。

最初に、問題の構成を変更します。単純化のために最大入力シークエンス長を定義してそれに (訓練のスピードアップのために) 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 から max_len の間で長さは様々ですのでゼロ・パディングが必要です。ここでは、Keras 組み込みの pad_sequences() 関数で 左辺 (prefix) パディングを使用します。

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

訓練されたモデルはランダムに選択された入力パターン上で評価されます。

完全なコードは以下のようになります :

# 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

# 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)

このコードの実行は次の出力を生成します :

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

モデルはランダムに生成された部分シークエンスからアルファベットを完全には学習していませんが、非常に上手くやっていることが分かります。モデルは調整されておらず、更なる訓練かより大きなネットワーク、あるいは両者が必要でしょう。

 

以上