雑記 in hibernation

頭の整理と備忘録

Kerasで最短(?)LSTM実装

RNNのチュートリアルとして、LSTMによる時系列予測モデルをKerasにて実装しました。

多分これが必要最低限の実装だと思います。

備忘録として記録しておきます。

 

1. LSTMとは

LSTMは再帰ニューラルネットワークであるRNNのバリエーションの一つで、主に時系列予測などの連続的なデータの処理に利用されます。原理の詳しい解説はここではしません。というかできません。

原理の解説記事はググるといっぱい出てきますが、特に以下のリンク先が参考になりそうでした。

LSTMネットワークの概要 - Qiita

LSTM (Long short-term memory) 概要

LSTMのネットワークそのものはKerasを使えば割とあっさり実現できてしまいます。初めてLSTMを実装するにあたっては、モデルそれ自体よりも時系列処理のためのデータ分割や前処理がポイントになるかと思います。その辺りについて簡単な説明とともに実装した内容を記録しておこう、というのがこの記事の趣旨です。

 

2. 時系列処理の入出力

実装の説明をする前に、時系列処理におけるモデルの入出力について触れておきます。

RNNにおける時系列予測では、基本的に任意のステップ数の時系列データをモデルへの入力とし、入力に続く1ステップの値を予測値として出力します。一連の時系列データを入力として次の1ステップの予測を行い、予測値を含む時系列からさらに次のステップの予測を行い、、、と繰り返すことで連鎖的に時系列を予測していきます。

例えば、過去3ステップの時系列データから予測値を出力するモデルでは、入出力は以下の図のようなイメージになります。

f:id:toeming:20200531193748p:plain


したがって、学習のフェーズでは任意のステップの連続した入力に対して、これに続く次の1ステップの値が教師信号となります。例えば、過去3ステップの時系列データから予測値を出力するモデルでは、学習用のデータセットは以下の図のようなイメージになります。

f:id:toeming:20200531193806p:plain


上記のような入出力を行うため、学習・予測のためデータセットを、任意のステップ数の連続した時系列データと、それに続く1ステップの教師信号に分割する必要があります。

 

3. LSTM最短実装

ということで、機能的に最小限(と個人的には思っている)のLSTM時系列予測モデルのPythonコードを要点だけかいつまんで簡単な解説付きで記載していきます。

ソースコード全文はこちらにおいてあります。

なお、実装にあたってはこちらの記事を参考にさせていただきました。 qiita.com

実装したモデルの精度自体はかなり微妙な感じですが、結果の解釈やパラメタ探索、モデルのブラッシュアップについては深追いしません。この記事ではあくまで実装の流れを整理することを目的とします。  


3.1. ライブラリの読み込み

ライブラリを読み込みます。

おなじみのやつです。  

# 基本のライブラリを読み込む
import numpy as np
import pandas as pd

# グラフ描画
from matplotlib import pylab as plt
%matplotlib inline

# グラフを横長にする
from matplotlib.pylab import rcParams
rcParams['figure.figsize'] = 15, 6


3.2. データ読み込みと概観確認

今回扱うデータは"AirPassengers.csv" です。これは1ヶ月毎の飛行機の乗客数データで、時系列処理の世界ではおなじみのデータセットらしいです。ファイル名でググるとあちこちのサイトから入手可能です。

# データの読み込み
filepath = 'AirPassengers.csv'
data = pd.read_csv(filepath)
data.head()


データセットの中身はこんな感じです。

   Month   #Passengers
0  1949-01   112
1  1949-02   118
2  1949-03   132
3  1949-04   129
4  1949-05   12


データを時系列でプロットしたのが以下のグラフです。12ヶ月スパンの周期性や、長期間の増加傾向が確認できます。年にもよりますが、概ね年の頭に小さなピークを、年の中ごろに大きなピークを持つようです。 f:id:toeming:20200601003716p:plain

このデータの推移を学習し、これに続く時系列を予測することを目的にモデルを構築していきます。

なお、データ処理の定石として基礎統計量や欠損値の確認も行なっていますが、ソースコードは省略します。


3.3. 前処理

前処理として、スケールの正規化と入力データの分割を行います。

3.3.1. 正規化

データのスケールを0~1に正規化します。RNNモデルにおいては、入力を0~1とすると学習が安定するためです(なぜかはわかりません、すみません)。

# 型変換
input_data = data['Passengers'].values.astype(float)
print("input_data : " , input_data.shape ,type(input_data))

# スケールの正規化
norm_scale = input_data.max()
input_data /= norm_scale
print(input_data[0:5])


ためしに先頭の5行を出力したものがこちらです。正規化できていそうです。

input_data :  (144,) <class 'numpy.ndarray'>
[0.18006431 0.18971061 0.21221865 0.2073955  0.19453376]


3.3.2. 入力データと教師信号の分割

「2. 時系列処理の特徴」で触れた教師信号を分割する処理をここで行います。今回は1ヶ月のデータを予測するにあたり、直近の過去12ヶ月のデータを入力とすることとします。過去12ヶ月のデータをX, 教師信号となる13ヶ月目のデータをyとしてデータセットを分割しています。 

# 入力データと教師データの作成
def make_dataset(low_data, maxlen):

    data, target = [], []

    for i in range(len(low_data)-maxlen):
        data.append(low_data[i:i + maxlen])
        target.append(low_data[i + maxlen])

    re_data = np.array(data).reshape(len(data), maxlen, 1)
    re_target = np.array(target).reshape(len(data), 1)

    return re_data, re_target


# RNNへの入力データ数
window_size = 12

# 入力データと教師データへの分割
X, y = make_dataset(input_data, window_size)
print("shape X : " , X.shape)
print("shape y : " , y.shape)

 
出力結果は以下の通りです。

shape X :  (132, 12, 1)
shape y :  (132, 1)


3.4. 訓練データ/検証データ 分割

おなじみの処理です。ソースコードは省略します。

データセットの3割を検証に用いるように分割したところ、学習セットと検証セットの形状はこんな感じになりました。

train_size / test_size =  92 / 40
X_train: (92, 12, 1)
X_test : (40, 12, 1)
y_train: (92, 1)
y_test : (40, 1)


3.5. LSTM構築

KerasでLSTMを用いたネットワークを構築し、学習させます。

3.5.1. ネットワーク定義

今回はシンプルイズベストということで、Keras を用いて50層のLSTMと出力層のみによるモデルを構築します。  

# ライブラリのインポート
import keras
from keras.models import Sequential
from keras.layers import Dense, Activation, LSTM
from keras.optimizers import Adam
import tensorflow as tf

# ネットワークの構築
lstm_model = Sequential() # Sequentialモデル
lstm_model.add(LSTM(50, batch_input_shape=(None, window_size, 1))) # LSTM 50層
lstm_model.add(Dense(1)) # 出力次元数は1

#コンパイル
lstm_model.compile(loss='mean_squared_error', optimizer=Adam() , metrics = ['accuracy'])
lstm_model.summary()


コンパイルされたモデルの構成はこんな感じです。

Model: "sequential_3"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_3 (LSTM)                (None, 50)                10400     
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 51        
=================================================================
Total params: 10,451
Trainable params: 10,451
Non-trainable params: 0
_________________________________________________________________

 


3.5.2. 学習

コンパイルしたモデルを学習させます。今回は150エポックで学習させました。

学習が収束していることを確認するため、エポックごとの損失の推移もプロットします。 

# 学習用パラメータ
batch_size = 20
n_epoch = 150

# 学習
hist = lstm_model.fit(X_train, y_train,
                 epochs=n_epoch,
                 validation_data=(X_test, y_test),
                 verbose=0,
                 batch_size=batch_size)

# 損失値(Loss)の遷移のプロット
plt.plot(hist.history['loss'],label="train set")
plt.plot(hist.history['val_loss'],label="test set")
plt.title('model loss')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend()
plt.show()


損失の推移は以下のようになりました。ちゃんと収束してそうな感じです。 f:id:toeming:20200601005402p:plain


3.5.3. 学習結果を可視化

学習データに対して予測を行い、RSMEと推定結果のプロットにより学習結果を可視化します(今回は明確な精度目標や比較対象がないので良し悪しの判断はできませんが、参考までに出力しています)。

# 予測
y_pred_train = lstm_model.predict(X_train) 
y_pred_test = lstm_model.predict(X_test) 

# RMSEで評価
# 参考:https://deepage.net/deep_learning/2016/09/17/tflearn_rnn.html
def rmse(y_pred, y_true):
    return np.sqrt(((y_true - y_pred) ** 2).mean())
print("RMSE Score")
print("  train : " , rmse(y_pred_train, y_train))
print("  test : " , rmse(y_pred_test, y_test))

# 推定結果のプロット
plt.plot(X[:, 0, 0], color='blue',  label="observed")  # 元データ
plt.plot(y_pred_train, color='red',  label="train")   # 予測値(学習)
plt.plot(range(len(X_train),len(X_test)+len(X_train)),y_pred_test, color='green',  label="test")   # 予測値(検証)
plt.legend()
plt.xticks(np.arange(0, 145, 12)) # 12ヶ月ごとにグリッド線を表示
plt.grid()
plt.show()


出力されるRMSEスコアはこんな感じになりました。

RMSE Score
  train :  0.030219804426221682
  test :  0.04880182804815519


予測結果のプロットを見ると、1年単位の周期性や長期的な増加傾向は学習できていそうな雰囲気です。一方、学習データに対して高めの値が予測されている点や、小さいピークが潰れている点など、表現の至らない部分も見受けられます。

f:id:toeming:20200601005625p:plain


3.6. 未来予測

学習したモデルを用いて未来の時系列を予測してみます。

今回は1ヶ月の予測するにあたり過去12ヶ月のデータを入力とするように決めていたので、予測においても直近12ヶ月分のデータを入力として次の1ヶ月の予測を行います。予測した値を時系列データの最後尾に加え、さらに最新12ヶ月を用いた予測を行い、、、と繰り返して行きます。

今回は36ヶ月(=3年分)の時点まで予測します。

# 予測結果を保存する行列
future_pred = X[:,0,0].copy()

# 予測期間は観測値の終端から3年間を設定
pred_time_length = 12*3

for tmp in range(pred_time_length):
  # 観測結果の最後尾から予測に使うデータをピックアップ
  X_future_pred = future_pred[-1*window_size:]
  # 予測
  y_future_pred = lstm_model.predict( X_future_pred.reshape(1,window_size,1) )
  # 予測値をfuture_predの最後尾に追加
  future_pred = np.append(future_pred, y_future_pred)
  #print(y_future_pred ,  future_pred[-5:])

# プロット
plt.plot(X[:,0,0] * norm_scale, color='blue',  label="observed")  # 実測値
plt.plot(range(len(X),len(X)+pred_time_length), future_pred[-1*pred_time_length:] * norm_scale,  color='red',  label="feature pred")   # 予測値
plt.legend()
plt.xticks(np.arange(0, 145+pred_time_length, 12)) # 12ヶ月ごとにグリッド線を表示
plt.grid()
plt.show()


予測結果をプロットするとこんな感じになります。 高周波の情報は表現できていませんが、全体的な傾向はつかめている感じです。 f:id:toeming:20200601005737p:plain


以上、LSTMモデルの学習から未来予測までの一連の流れをさらうことができました。


4. おわりに

ということでLSTM実装の備忘録でした。

そもそも時系列処理についての知見が浅く、基礎的な知識すらなかったので結構苦労しました。統計的手法から地道に勉強する必要性を感じます。

ちなみに、勢いでLSTMを使った文章生成モデルも作ってみたので、それについてもまた気が向いたらまとめます。