Keras : Ex-Tutorials : LSTM リカレント・ネットワークで時系列予測

Keras : Ex-Tutorials : LSTM リカレント・ネットワークで時系列予測 (翻訳/解説)

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

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

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

 

本文

時系列予測問題は予測モデル問題の中でも困難なもので、それは回帰予測モデルと違い、時系列は入力変数の中のシークエンス依存関係の複雑さが追加されます。

シークエンス依存関係を処理するために設計されたニューラルネットのパワフルな型はリカレント・ニューラルネットと呼ばれますが、特に Long Short-Term Memory あるいは LSTM ネットワークは深層学習で使用されるリカレント・ニューラルネットの一種で、非常に巨大なアーキテクチャが成功的に訓練可能です。

この記事では、時系列予測問題への対応を示すために Keras を使用して LSTM ネットワークをどのように開発するかを見ていきます。貴方自身の時系列予測問題や他の更に一般的なシークエンス問題に対してLSTM ネットワークをどのように実装して開発するかを知ることができます :

  • 国際線乗客の時系列予測問題。
  • 回帰、ウィンドウとタイムステップ・ベースの時系列予測問題の構成のためのLSTM ネットワークをどのように開発するか。
  • 非常に長いシークエンスに渡り状態 (メモリ) を保持する LSTM ネットワークを使用してどのように開発して予測するか。

標準的な時系列予測問題のための多くの LSTM を開発していきます。これらの例は時系列予測モデル問題に対して貴方自身の異なる構造の LSTM ネットワークをどのように開発可能かを正確に示すでしょう。

 

課題

課題は国際線の乗客数予測問題です。これは年月が与えられた場合に、千人単位の国際線乗客の数を予測することが課題です。データは 144 観測結果を持ち、1949 年 1 月から 1960 年 12 月 (12 年間) に渡ります。

データセットはファイル名 “international-airline-passengers.csv“ を持ち “DataMarket webpage as a CSV download” から無料で利用可能です。

以下はファイルの最初の数行のサンプルです :

"Month","International airline passengers: monthly totals in thousands. Jan 49 ? Dec 60"
"1949-01",112
"1949-02",118
"1949-03",132
"1949-04",129
"1949-05",121

Pandas ライブラリでこのデータセットを簡単にロードできます。各観測結果が 1 ヶ月の同じ間隔で分けられており、(ここでは日付には興味ありませんので) データセットをロードするときに最初のカラムは除外できます。ダウンロードしたデータセットはまたフッタ情報も持ちますが、これも pandas.read_csv() への skipfooter 引数を (3 フッタ行のための) 3 に設定することで除外可能です。

ロードされればデータセット全体を簡単にプロットできます。そのためのコードは以下のようなものです :

import pandas
import matplotlib.pyplot as plt
dataset = pandas.read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
plt.plot(dataset)
plt.show()

データセットが時間とともに上昇傾向にあることを見て取れます。北半球の休暇期間に多分対応する、データセットの何某かの周期性を見ることもできます :

 

Long Short-Term Memory ネットワーク

Long Short-Term Memory あるいは LSTM ネットワークは BPTT (Backpropagation Through Time) を使用して訓練されるリカレント・ニューラルネットで勾配消失問題への対処が考慮されています。これは巨大なリカレント・ネットワークを作成して機械学習における困難なシークエンス問題に対処するために利用可能で、最先端の結果を獲得することが期待できます。

ニューロンの代わりに LSTM ネットワークは層を通して接続されるメモリブロックを持ちます。ブロックは古典的なニューロンよりもスマートなコンポーネントと直近のシークエンスのためにメモリを持ちます。ブロックはブロックの状態と出力を管理するゲートを含みます。ブロックは入力シークエンス上で作用してブロック内の各ゲートはそれらがトリガーされるか否かを制御する sigmoid 活性化ユニットを使用し、ブロックの条件節を通して状態の変更と情報の追加を行ないます。

ユニット内部には 3 つのタイプのゲートがあります :

  • 忘却ゲート: 条件付きでブロックから何の情報を捨てるかを決定します。
  • 入力ゲート: 条件付きでメモリ状態を更新するために入力からのどの値かを決定します。
  • 出力ゲート: 条件付きで入力とブロックのメモリを基に何を出力するかを決定します。

各ユニットはミニ状態マシンのようなもので、そこではユニットのゲートは訓練手続きの間に学習される重みを持ちます。

 

回帰のための LSTM ネットワーク

この問題を回帰問題として表現することができます。つまり、今月の乗客数が与えられた場合、来月の乗客数はどのくらいでしょう?

データの単一カラムを 2-カラムのデータセットに変換する単純な関数を書くことができます : 最初のカラムは今月 (t) の乗客カウントで 2 番目のカラムは予測されるべき、次の月 (t+1) の乗客カウントです。

取り敢えず :

import numpy
import matplotlib.pyplot as plt
import pandas
import math
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
# fix random seed for reproducibility
numpy.random.seed(7)

データセットは Pandas dataframe としてロードします。そして dataframe から NumPy 配列を取り出して整数値を浮動小数点値に変換します、これはニューラルネットでモデリングする際に多くの場合、より適切です :

# load the dataset
dataframe = pandas.read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
dataset = dataframe.values
dataset = dataset.astype('float32')

LSTM は、特に sigmoid (デフォルト) や tanh 活性化関数が使用されるとき、入力データのスケールに敏感です。データを 0-to-1 の範囲にスケールすることは良い実践です、これは正規化とも呼ばれます。scikit-learn ライブラリからの MinMaxScaler 前処理クラスを使用してデータセットを簡単に正規化することができます :

# normalize the dataset
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)

データをモデル化して訓練データセット上でモデルの技量を見積もった後、新しいまだ見ていないデータ上でのモデルの技量の知見を得ることが必要です。通常の分類問題や回帰問題のためには、これを交差検証で行ないます。

時系列データでは、値のシークエンスが重要です。使用可能な単純な方法は順序付けられたデータセットを訓練とテスト・データセットに分けることです。下のコードは分割点のインデックスを計算して、データをモデルを訓練するために使用可能な観測結果の 67% の訓練データセットを取り分け、残りの 33% をモデルのテストのために残します :

# split into train and test sets
train_size = int(len(dataset) * 0.67)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]
print(len(train), len(test))
96 48

これで上で説明したように、新しいデータセットを作成する関数を定義できます。

その関数は 2 つの引数を取ります : dataset、これはデータセットに変換することを望む NumPy 配列 ; look_back、これは次の時間区間を予測するために入力変数として使用する前の時間ステップの数です – この場合 1 にデフォルト設定されています。

このデフォルトは、X が与えられた時間 (t) における乗客数、Y が次の時間 (t + 1) における乗客数であるようなデータセットを作成します :

# convert an array of values into a dataset matrix
def create_dataset(dataset, look_back=1):
	dataX, dataY = [], []
	for i in range(len(dataset)-look_back-1):
		a = dataset[i:(i+look_back), 0]
		dataX.append(a)
		dataY.append(dataset[i + look_back, 0])
	return numpy.array(dataX), numpy.array(dataY)

データ・セットの最初の数行上でこの関数の効果を見てみましょう (分かりやすさのために非正規化された形式で示します) :

X		Y
112		118
118		132
132		129
129		121
121		135

これらの最初の 5 行を前のセクションの元のデータセット・サンプルと比較すれば、数字の X=t と Y=t+1 パターンを見ることができます。

モデリングのために訓練とテスト・データセットを準備するためにこの関数を使用します :

# reshape into X=t and Y=t+1
look_back = 1
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)

データの冒頭を確認しておきます :

print (trainX[:10,:])
print (trainY[:10])
[[ 0.01544401]
 [ 0.02702703]
 [ 0.05405405]
 [ 0.04826255]
 [ 0.03281853]
 [ 0.05984557]
 [ 0.08494207]
 [ 0.08494207]
 [ 0.06177607]
 [ 0.02895753]]
[ 0.02702703  0.05405405  0.04826255  0.03281853  0.05984557  0.08494207
  0.08494207  0.06177607  0.02895753  0.        ]

ついでにデータのボトムも確認しましょう :

print(trainX[-10:,:])
print(trainY[-10:])
[[ 0.34749034]
 [ 0.33397684]
 [ 0.41119692]
 [ 0.4034749 ]
 [ 0.41312739]
 [ 0.52123547]
 [ 0.59652507]
 [ 0.58108103]
 [ 0.48455599]
 [ 0.38996139]]
[ 0.33397684  0.41119692  0.4034749   0.41312739  0.52123547  0.59652507
  0.58108103  0.48455599  0.38996139  0.3223938 ]

LSTM ネットワークは入力データ (X) が [samples, time steps, features] の形式の特定の配列構造で提供されることを想定しています。現在、私達のデータは [samples, features] の形式で各サンプルのための 1 タイムステップとして問題を構成しています。次のように numpy.reshape() を使用して準備された訓練とテスト入力データを想定される構造に変換することができます :

# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = numpy.reshape(testX, (testX.shape[0], 1, testX.shape[1]))

ここで変換前の trainX.shape は (94, 1) で reshape 後は (94, 1, 1) になります。
trainX の冒頭を確認しておきます :

print(trainX[:10,:])
[[[ 0.01544401]]
 [[ 0.02702703]]
 [[ 0.05405405]]
 [[ 0.04826255]]
 [[ 0.03281853]]
 [[ 0.05984557]]
 [[ 0.08494207]]
 [[ 0.08494207]]
 [[ 0.06177607]]
 [[ 0.02895753]]]

今、この問題に対する LSTM ネットワークを設計して fit する準備ができました。ネットワークは 1 入力を持つ可視層、4 LSTM ブロックあるいはニューロンを持つ隠れ層、そして単一の値予測を行なう出力層を持ちます。デフォルトの sigmoid 活性化関数が LSTM ブロックのために使用されます。ネットワークは 100 エポック訓練されてサイズ 1 のバッチが使用されます :

# create and fit the LSTM network
model = Sequential()
model.add(LSTM(4, input_shape=(1, look_back)))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(trainX, trainY, epochs=100, batch_size=1, verbose=2)

モデルが fit すれば、モデルのパフォーマンスを訓練とテスト・データセット上で見積もることができます。これは新しいモデルのための比較ポイントを与えてくれます。

元データと同じ単位 (月毎に乗客千人) でパフォーマンスをレポートすることを確かなものとするために、エラー・スコアを計算する前に予測を invert することに注意してください :

# 予想を遂行する
trainPredict = model.predict(trainX)
testPredict = model.predict(testX)
# 予測を invert する
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])
# 平方平均二乗誤差 (RMSE, root mean squared error) を計算する
trainScore = math.sqrt(mean_squared_error(trainY[0], trainPredict[:,0]))
print('Train Score: %.2f RMSE' % (trainScore))
testScore = math.sqrt(mean_squared_error(testY[0], testPredict[:,0]))
print('Test Score: %.2f RMSE' % (testScore))

最後に、モデルの技量の視覚表示を得るために訓練とテスト・データセットの両者のためにモデルを使用して予測を生成することができます。

データがどのように準備されたかにより、x-軸上で元データセットと共に整列されるように予測をシフトしなければなりません。準備ができればデータがプロットされて、元データセットが青色、訓練データセットのための予測が緑色、そしてまだ見ていないテスト・データセットのための予測は赤色で示されます :

# shift train predictions for plotting
trainPredictPlot = numpy.empty_like(dataset)
trainPredictPlot[:, :] = numpy.nan
trainPredictPlot[look_back:len(trainPredict)+look_back, :] = trainPredict
# shift test predictions for plotting
testPredictPlot = numpy.empty_like(dataset)
testPredictPlot[:, :] = numpy.nan
testPredictPlot[len(trainPredict)+(look_back*2)+1:len(dataset)-1, :] = testPredict
# plot baseline and predictions
plt.plot(scaler.inverse_transform(dataset))
plt.plot(trainPredictPlot)
plt.plot(testPredictPlot)
plt.show()

サンプルの実行は次のような出力を生成します。まずは 10 エポック :

Epoch 1/10
 - 0s - loss: 0.0413
Epoch 2/10
 - 0s - loss: 0.0202
Epoch 3/10
 - 0s - loss: 0.0145
Epoch 4/10
 - 0s - loss: 0.0131
Epoch 5/10
 - 0s - loss: 0.0121
Epoch 6/10
 - 0s - loss: 0.0111
Epoch 7/10
 - 0s - loss: 0.0102
Epoch 8/10
 - 0s - loss: 0.0093
Epoch 9/10
 - 0s - loss: 0.0081
Epoch 10/10
 - 0s - loss: 0.0071
Train Score: 41.80 RMSE
Test Score: 115.66 RMSE

続いて 100 エポック :

Epoch 1/100
 - 0s - loss: 0.0413
Epoch 2/100
 - 0s - loss: 0.0202
Epoch 3/100
 - 0s - loss: 0.0146
Epoch 4/100
 - 0s - loss: 0.0131
Epoch 5/100
 - 0s - loss: 0.0121
Epoch 6/100
 - 0s - loss: 0.0111
Epoch 7/100
 - 0s - loss: 0.0102
Epoch 8/100
 - 0s - loss: 0.0093
Epoch 9/100
 - 0s - loss: 0.0081
Epoch 10/100
 - 0s - loss: 0.0071
...
Epoch 95/100
 - 0s - loss: 0.0020
Epoch 96/100
 - 0s - loss: 0.0020
Epoch 97/100
 - 0s - loss: 0.0020
Epoch 98/100
 - 0s - loss: 0.0020
Epoch 99/100
 - 0s - loss: 0.0020
Epoch 100/100
 - 0s - loss: 0.0020
Train Score: 22.92 RMSE
Test Score: 47.53 RMSE

モデルは、訓練とテスト・データセットの両者にフィットし、ジョブを上手く遂行することが見て取れます。モデルは訓練データセット上でおよそ 23 乗客数 (千人単位) の平均エラー、テスト・データセット上でおよそ 48 乗客数 (千人単位) の平均エラーを持ちます。悪くはありません。

ちなみに、1,000 エポック訓練すると overfitting の傾向が見られます :

Train Score: 22.74 RMSE
Test Score: 53.89 RMSE

 

ウィンドウ・メソッドを使用した回帰のための LSTM

次のタイムステップの予測をするために複数の、直近のタイムステップを使用出来るように問題を構成することもできます。これはウィンドウと呼ばれ、そのサイズは各問題のために調整可能なパラメータです。

例えば、現在の時間 (t) が与えられてシークエンスの次の時間 (t+1) における値を予測することを望む場合、入力変数として現在時間 (t)、そして 2 つの前の時間 (t-1 と t-2) を使用することができます。

回帰問題として構成されるとき、入力変数は t-2, t-1, t で、出力変数は t+1 です。前のセクションで作成した create_dataset() 関数は look_back 引数を 1 から 3 に増やすことで時系列問題のこの形式化を作成できます。

この手形式化を持つデータセットのサンプルは以下のようなものです :

X1	X2	X3	Y
112	118	132	129
118	132	129	121
132	129	121	135
129	121	135	148
121	135	148	148

前のセクションのサンプルをより大きいウィンドウサイズで再実行できますが、念のため、そのウィンドウサイズを変更しただけのコード全体を再掲しておきます :

# LSTM for international airline passengers problem with window regression framing
import numpy
import matplotlib.pyplot as plt
from pandas import read_csv
import math
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
# convert an array of values into a dataset matrix
def create_dataset(dataset, look_back=1):
	dataX, dataY = [], []
	for i in range(len(dataset)-look_back-1):
		a = dataset[i:(i+look_back), 0]
		dataX.append(a)
		dataY.append(dataset[i + look_back, 0])
	return numpy.array(dataX), numpy.array(dataY)
# fix random seed for reproducibility
numpy.random.seed(7)
# load the dataset
dataframe = read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
dataset = dataframe.values
dataset = dataset.astype('float32')
# normalize the dataset
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)
# split into train and test sets
train_size = int(len(dataset) * 0.67)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]
# reshape into X=t and Y=t+1
look_back = 3
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)
# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = numpy.reshape(testX, (testX.shape[0], 1, testX.shape[1]))
# create and fit the LSTM network
model = Sequential()
model.add(LSTM(4, input_shape=(1, look_back)))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(trainX, trainY, epochs=100, batch_size=1, verbose=2)
# make predictions
trainPredict = model.predict(trainX)
testPredict = model.predict(testX)
# invert predictions
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])
# calculate root mean squared error
trainScore = math.sqrt(mean_squared_error(trainY[0], trainPredict[:,0]))
print('Train Score: %.2f RMSE' % (trainScore))
testScore = math.sqrt(mean_squared_error(testY[0], testPredict[:,0]))
print('Test Score: %.2f RMSE' % (testScore))
# shift train predictions for plotting
trainPredictPlot = numpy.empty_like(dataset)
trainPredictPlot[:, :] = numpy.nan
trainPredictPlot[look_back:len(trainPredict)+look_back, :] = trainPredict
# shift test predictions for plotting
testPredictPlot = numpy.empty_like(dataset)
testPredictPlot[:, :] = numpy.nan
testPredictPlot[len(trainPredict)+(look_back*2)+1:len(dataset)-1, :] = testPredict
# plot baseline and predictions
plt.plot(scaler.inverse_transform(dataset))
plt.plot(trainPredictPlot)
plt.plot(testPredictPlot)
plt.show()

reshape 前の trainX, trainY の冒頭です。shape はそれぞれ (87, 8) (87,) です :

[[ 0.01544401  0.02702703  0.05405405]
 [ 0.02702703  0.05405405  0.04826255]
 [ 0.05405405  0.04826255  0.03281853]
 [ 0.04826255  0.03281853  0.05984557]
 [ 0.03281853  0.05984557  0.08494207]
 [ 0.05984557  0.08494207  0.08494207]
 [ 0.08494207  0.08494207  0.06177607]
 [ 0.08494207  0.06177607  0.02895753]
 [ 0.06177607  0.02895753  0.        ]
 [ 0.02895753  0.          0.02702703]]
[ 0.04826255  0.03281853  0.05984557  0.08494207  0.08494207  0.06177607
  0.02895753  0.          0.02702703  0.02123553]

reshape 後の trainX の冒頭です。shape は (87, 1, 8) :

[[[ 0.01544401  0.02702703  0.05405405]]
 [[ 0.02702703  0.05405405  0.04826255]]
 [[ 0.05405405  0.04826255  0.03281853]]
 [[ 0.04826255  0.03281853  0.05984557]]
 [[ 0.03281853  0.05984557  0.08494207]]
 [[ 0.05984557  0.08494207  0.08494207]]
 [[ 0.08494207  0.08494207  0.06177607]]
 [[ 0.08494207  0.06177607  0.02895753]]
 [[ 0.06177607  0.02895753  0.        ]]
 [[ 0.02895753  0.          0.02702703]]]

サンプルの実行は次のような出力を生成します :

...
Epoch 91/100
 - 0s - loss: 0.0021
Epoch 92/100
 - 0s - loss: 0.0021
Epoch 93/100
 - 0s - loss: 0.0021
Epoch 94/100
 - 0s - loss: 0.0021
Epoch 95/100
 - 0s - loss: 0.0021
Epoch 96/100
 - 0s - loss: 0.0021
Epoch 97/100
 - 0s - loss: 0.0021
Epoch 98/100
 - 0s - loss: 0.0021
Epoch 99/100
 - 0s - loss: 0.0022
Epoch 100/100
 - 0s - loss: 0.0020
Train Score: 24.19 RMSE
Test Score: 58.03 RMSE

エラーが前のセクションのそれ (Train Score: 22.74 RMSE; Test Score: 53.89 RMSE) と比較して僅かばかり増えたことが分かりますが、これはウィンドウ・サイズとネットワーク・アーキテクチャが調整されていないためです。

試しに look_back = 8 として 200 エポック訓練すれば改良されます :

Epoch 191/200
 - 0s - loss: 0.0017
Epoch 192/200
 - 0s - loss: 0.0017
Epoch 193/200
 - 0s - loss: 0.0017
Epoch 194/200
 - 0s - loss: 0.0017
Epoch 195/200
 - 0s - loss: 0.0017
Epoch 196/200
 - 0s - loss: 0.0019
Epoch 197/200
 - 0s - loss: 0.0018
Epoch 198/200
 - 0s - loss: 0.0017
Epoch 199/200
 - 0s - loss: 0.0017
Epoch 200/200
 - 0s - loss: 0.0016
Train Score: 20.56 RMSE
Test Score: 49.93 RMSE

 

タイムステップを伴う回帰のための LSTM

タイムステップは時系列問題を構成するためのもう一つの方法を提供します。上のウィンドウ・サンプルのように、次のタイムステップの出力を予測するために時系列の先行するタイプステップを入力として取ることができます。

過去の観測を個別の入力特徴として構成する代わりに、それらを一つの入力特徴の (複数の) タイムステップとして使用することができます、これは実際には問題のより正確な構成になります。

前のウィンドウ・ベースのサンプルと同じデータ表現を使用してこれを行なうことができますが、データを reshape するとき、カラムをタイムステップ次元として設定して特徴次元は 1 に変更し戻す必要があります。例えば :

# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
testX = numpy.reshape(testX, (testX.shape[0], testX.shape[1], 1))

(look_back = 8 の場合、) trainX の reshape 前の shape は (87, 8)で、reshape 後は (87, 8, 1) になります。

このサンプルを実行すると次のような出力を生成します。look_back = 8、200 エポックで訓練しました :

Epoch 191/200
 - 0s - loss: 0.0017
Epoch 192/200
 - 0s - loss: 0.0017
Epoch 193/200
 - 0s - loss: 0.0017
Epoch 194/200
 - 0s - loss: 0.0017
Epoch 195/200
 - 0s - loss: 0.0018
Epoch 196/200
 - 0s - loss: 0.0018
Epoch 197/200
 - 0s - loss: 0.0019
Epoch 198/200
 - 0s - loss: 0.0017
Epoch 199/200
 - 0s - loss: 0.0017
Epoch 200/200
 - 0s - loss: 0.0017
Train Score: 21.13 RMSE
Test Score: 46.41 RMSE

 

バッチ間のメモリを持つ LSTM

LSTM ネットワークは、長いシークエンスに渡り記憶することができるメモリを持ちます。

通常は、ネットワーク内の状態は model.predict() や model.evaluate() の各呼び出しに時に加えて、モデルを fit するとき各訓練バッチ後にリセットされますが、LSTM 層を “ステートフル” にすることで、LSTM ネットワークの内部状態が Keras でクリアされるときにより良い制御を得ることができます。これは、それが訓練シークエンス全体に渡り状態を構築できて、予測をするために必要であればその状態を維持することさえ可能であることを意味します。

この場合、ネットワークを fit しているとき訓練データがシャッフルされていないことを必要とします。またエポック後に model.reset_states() の呼び出しによるネットワーク状態の明示的なりセットを要求します。これはエポックの外側のループを作成して各エポック内で model.fit() そして model.reset_states() を呼びださなければならないことを意味します。例えば :

for i in range(100):
	model.fit(trainX, trainY, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
	model.reset_states()

最後に、LSTM 層を構築するとき、stateful パラメータは True に設定されなければなりません、そして input 次元を指定する代わりに、batch_input_shape パラメータを設定することにより、バッチのサンプル数、サンプルのタイムステップ数、そしてタイムステップの特徴数をハードコードしなければなりません。例えば :

model.add(LSTM(4, batch_input_shape=(batch_size, time_steps, features), stateful=True))

それから後でモデルを評価したり予測するときには同じバッチサイズが使用されなければなりません。例えば :

model.predict(trainX, batch_size=batch_size)

ステートフル LSTM を使用するためにタイムステップの例を適応させることができます。

完全なコードは以下のようなものです :

# LSTM for international airline passengers problem with memory
import numpy
import matplotlib.pyplot as plt
from pandas import read_csv
import math
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
# convert an array of values into a dataset matrix
def create_dataset(dataset, look_back=1):
	dataX, dataY = [], []
	for i in range(len(dataset)-look_back-1):
		a = dataset[i:(i+look_back), 0]
		dataX.append(a)
		dataY.append(dataset[i + look_back, 0])
	return numpy.array(dataX), numpy.array(dataY)
# fix random seed for reproducibility
numpy.random.seed(7)
# load the dataset
dataframe = read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
dataset = dataframe.values
dataset = dataset.astype('float32')
# normalize the dataset
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)
# split into train and test sets
train_size = int(len(dataset) * 0.67)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]
# reshape into X=t and Y=t+1
look_back = 3
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)
# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
testX = numpy.reshape(testX, (testX.shape[0], testX.shape[1], 1))
# create and fit the LSTM network
batch_size = 1
model = Sequential()
model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
for i in range(100):
	model.fit(trainX, trainY, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
	model.reset_states()
# make predictions
trainPredict = model.predict(trainX, batch_size=batch_size)
model.reset_states()
testPredict = model.predict(testX, batch_size=batch_size)
# invert predictions
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])
# calculate root mean squared error
trainScore = math.sqrt(mean_squared_error(trainY[0], trainPredict[:,0]))
print('Train Score: %.2f RMSE' % (trainScore))
testScore = math.sqrt(mean_squared_error(testY[0], testPredict[:,0]))
print('Test Score: %.2f RMSE' % (testScore))
# shift train predictions for plotting
trainPredictPlot = numpy.empty_like(dataset)
trainPredictPlot[:, :] = numpy.nan
trainPredictPlot[look_back:len(trainPredict)+look_back, :] = trainPredict
# shift test predictions for plotting
testPredictPlot = numpy.empty_like(dataset)
testPredictPlot[:, :] = numpy.nan
testPredictPlot[len(trainPredict)+(look_back*2)+1:len(dataset)-1, :] = testPredict
# plot baseline and predictions
plt.plot(scaler.inverse_transform(dataset))
plt.plot(trainPredictPlot)
plt.plot(testPredictPlot)
plt.show()

サンプルの実行は次のような出力を生成します :

...
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0016
Train Score: 20.34 RMSE
Test Score: 64.11 RMSE

結果が悪くなっていることを見て取れます。このモデルは、問題の構造を内在化するために更なるモジュールを必要としより多くのエポックの間訓練されることが必要なのかもしれません。

 

バッチ間のメモリを持つスタック LSTM

最後に、LSTM の大きな恩恵を見てみます : それらは深層ネットワーク・アーキテクチャにスタックされたとき成功的に訓練されるという事実です。

LSTM ネットワークは他の層タイプがスタックできるのと同じ方法で Keras でスタック可能です。構成に一つ付け足す必要があるのは、先行する LSTM 層は続く LSTM 層それぞれにシークエンスを返さなければならないことです。これは層の return_sequences パラメータを True に設定することにより成されます。

次のようにして、前のセクションのステートフル LSTM を 5 の層を持つように拡張できます :

model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True, return_sequences=True))
model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))

完全なコードリストは以下です :

# Stacked LSTM for international airline passengers problem with memory
import numpy
import matplotlib.pyplot as plt
from pandas import read_csv
import math
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
# convert an array of values into a dataset matrix
def create_dataset(dataset, look_back=1):
	dataX, dataY = [], []
	for i in range(len(dataset)-look_back-1):
		a = dataset[i:(i+look_back), 0]
		dataX.append(a)
		dataY.append(dataset[i + look_back, 0])
	return numpy.array(dataX), numpy.array(dataY)
# fix random seed for reproducibility
numpy.random.seed(7)
# load the dataset
dataframe = read_csv('international-airline-passengers.csv', usecols=[1], engine='python', skipfooter=3)
dataset = dataframe.values
dataset = dataset.astype('float32')
# normalize the dataset
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)
# split into train and test sets
train_size = int(len(dataset) * 0.67)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]
# reshape into X=t and Y=t+1
look_back = 3
trainX, trainY = create_dataset(train, look_back)
testX, testY = create_dataset(test, look_back)
# reshape input to be [samples, time steps, features]
trainX = numpy.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
testX = numpy.reshape(testX, (testX.shape[0], testX.shape[1], 1))
# create and fit the LSTM network
batch_size = 1
model = Sequential()
model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True, return_sequences=True))
model.add(LSTM(4, batch_input_shape=(batch_size, look_back, 1), stateful=True))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
for i in range(100):
	model.fit(trainX, trainY, epochs=1, batch_size=batch_size, verbose=2, shuffle=False)
	model.reset_states()
# make predictions
trainPredict = model.predict(trainX, batch_size=batch_size)
model.reset_states()
testPredict = model.predict(testX, batch_size=batch_size)
# invert predictions
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])
# calculate root mean squared error
trainScore = math.sqrt(mean_squared_error(trainY[0], trainPredict[:,0]))
print('Train Score: %.2f RMSE' % (trainScore))
testScore = math.sqrt(mean_squared_error(testY[0], testPredict[:,0]))
print('Test Score: %.2f RMSE' % (testScore))
# shift train predictions for plotting
trainPredictPlot = numpy.empty_like(dataset)
trainPredictPlot[:, :] = numpy.nan
trainPredictPlot[look_back:len(trainPredict)+look_back, :] = trainPredict
# shift test predictions for plotting
testPredictPlot = numpy.empty_like(dataset)
testPredictPlot[:, :] = numpy.nan
testPredictPlot[len(trainPredict)+(look_back*2)+1:len(dataset)-1, :] = testPredict
# plot baseline and predictions
plt.plot(scaler.inverse_transform(dataset))
plt.plot(trainPredictPlot)
plt.plot(testPredictPlot)
plt.show()

サンプルの実行は次の出力を生成します。look_back = 8 で 200 エポックです :

Epoch 1/1
 - 0s - loss: 0.0016
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Epoch 1/1
 - 0s - loss: 0.0015
Train Score: 20.03 RMSE
Test Score: 68.57 RMSE

テスト・データセット上の予測は再度、悪化しています。
これは追加の訓練エポックの必要性を提示している更なる証拠と言えます。

 

以上