Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/08/02
このエントリーをはてなブックマークに追加

はじめに

カブクで機械学習エンジニアをしている大串正矢です。今回は複数時系列データを1つの深層学習モデルで学習させる方法について書きます。

背景

複数時系列データは複数企業の株価の変動、各地域における気温変動、複数マシーンのログなど多岐に渡って観測できます。この時系列ごとにモデルを用意して管理するとなると学習、運用において多大なるコストがかかってしまいます。1つのモデルで複数の時系列を管理できれば運用が楽になるだけでなく、学習も1度で済むのでトライアンドエラーの工数も大幅に削減できます。
本記事ではそのために実現可能な2つの手法を紹介します。

複数時系列データを1つの深層学習モデルで学習させる方法

Kerasで複数の時系列データを深層学習モデルで学習させる手法には2つあります。

  1. 複数入力型の深層学習モデル
  2. 個別入力型の深層学習モデルの組み合わせ

1の手法の利点はモデルがシンプルなので学習と予測が2の手法に比べ高速になります。

2の手法の利点は時系列ごとにカスタマイズ可能なので1よりも精度を高めることが容易になります。

複数入力型の深層学習モデル

データ

今回は下記のレポジトリのデータをいくつかピックアップして使用します。

https://github.com/jamesrobertlloyd/gpss-research/tree/master/data/tsdlr_9010

下記の2つのデータをピックアップしてモデルを作成します。

  • 気温変化のデータ
  • ガスの生産量のデータ

前処理

データを前処理をして学習が容易な形にします。まずはデータを取得し対数化します。これはガスの生産量はデータのスケールが1646から約6000近くまであるのに対し、気温変化は-0.8から26.3とデータ間の差が大きいためです。

  • データをリンク先から読み込みます。
  • データのスケールが異なるので両データを対数にスケーリングします。
    • 0の部分は計算できないので + 1の処理が入ります。
    • それでも発生する計算できない値はnanになるため0に置き換えます。

pythonコードでは下記のようになります。

wave_data = read_csv('https://raw.githubusercontent.com/jamesrobertlloyd/gpss-research/master/data/tsdlr_5050/daily-minimum-temperatures-in-me-train.csv', header=None, names=["Date", "Temp"])
wave_data = wave_data.sort_values(by=['Date'])
production_of_gas_data = read_csv('https://raw.githubusercontent.com/jamesrobertlloyd/gpss-research/master/data/tsdlr_5050/monthly-production-of-gas-in-aus-train.csv', header=None, names=["Date", "production-of-gas"])
production_of_gas_data = production_of_gas_data.sort_values(by=['Date'])

X_orig = np.nan_to_num(np.log(wave_data["Temp"].values + 1))
X_day = wave_data["Date"].values

X_orig_second = np.nan_to_num(np.log(production_of_gas_data["production-of-gas"].values + 1))
X_day_second = production_of_gas_data["Date"].values

対数スケーリング済みデータ

図1. 気温の変化

図2. ガスの生産量

データの分割

学習データとテストデータに分割し、複数入力に使用可能なように学習データを合わせます。

X_train_joint = np.vstack((X_train, X_train_second))
X_test_joint = np.vstack((X_test, X_test_second))

学習データの正規化及びwindow幅に合わせたデータ設定

学習データを正規化し、window幅に合わせたデータセットに変更します。正規化することで学習が容易になり、window幅は3にしています。

def get_data(data, time_steps: int=3):
    dataX = []
    print(data.shape)
    dataX = np.zeros((data.shape[0], data.shape[1], time_steps))

    for i in range(data.shape[0]):
        for j in range(data.shape[1] - time_steps - 1):
            dataX[i][j] = data[i, j:(j + time_steps)].T
    return np.array(dataX)

def transform_data(original_data: np.array, 
                   inverse_option: bool, 
                   scaler: object,
                   variable_number: int,
                  ):
    data_shape = original_data.shape
    print(original_data.shape)
    data = original_data.reshape(-1, variable_number)
    print(data.shape)
    if inverse_option is True:
        print('before max {}'.format(max(data[0])))
        print('Inverse')
        data = scaler.inverse_transform(data)
        print('after max {}'.format(max(data[0])))
    else:
        print('before max {}'.format(max(data[0])))
        print('Normalize')
        data = scaler.fit_transform(data)
        print('after max {}'.format(max(data[0])))
    data = data.reshape(data_shape)
    return data, scaler

def prepare_data(original_data, time_steps, variable_number):
    copy_data = original_data.copy()
    scaler = MinMaxScaler(feature_range=(0, 1), copy=False)
    data, scaler = transform_data(original_data=copy_data, 
                              inverse_option=False, scaler=scaler, variable_number=variable_number)
    data = np.asarray(data)
    x = get_data(data, time_steps=time_steps)
    print(x.shape)
    x = np.swapaxes(x, 0, 1)
    x = np.swapaxes(x, 1, 2)
    return x, scaler

time_steps = 3
variable_number = 2

x, scaler = prepare_data(X_train_joint, time_steps, variable_number)


input_dim = x.shape[-1]
timesteps = x.shape[1]

モデルの定義部分

下記でモデルの定義をします。input_dimがデータの入力種類を表しています。今回は2種類のデータなので2が入ります。

def create_model(input_dim,
                 time_steps,
                 latent_dim,
                 # データが一つしかないので1しか選べない
                 batch_size=1,
                 model_option='lstm',
                 optimizer='adam',
                ):
    with tf.name_scope('Model'):
        x = Input(shape=(time_steps, input_dim,))

        if model_option == 'lstm':
            with tf.name_scope('LSTM'):
                h = LSTM(latent_dim, stateful=False, return_sequences=True)(x)
        elif model_option == 'gru':
            with tf.name_scope('GRU'):
                h = GRU(latent_dim, stateful=False, return_sequences=True)(x)

        print('input_dim:', input_dim)
        with tf.name_scope('Dense'):
            out = Dense(input_dim)(h)

        model = Model(x, out)
        model.summary()

        with tf.name_scope('ModelCompile'):
            model.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mse'])

    return model

定義されたモデルは下記のようになります。1つのモデルで複数の時系列データを扱っているのでLSTMモデルは1つだけになります。

モデルの予測部分

モデルが学習した結果を用いて適切に予測できるかを確認します。正規化したデータを元のスケールに戻す処理を入れています。

def predict_model_show_graph(day, x, scaler, model, variable_number):
    prepare_value = x.copy()
    preds = model.predict(prepare_value)

    print('prepare_value: ', prepare_value.shape)
    print('preds: ', preds.shape)

    x_scale, scaler = transform_data(original_data=prepare_value, inverse_option=True, 
                                     scaler=scaler, variable_number=variable_number)

    predict_scale, scaler = transform_data(original_data=preds, inverse_option=True, 
                                           scaler=scaler, variable_number=variable_number)

    for i in range(prepare_value.shape[-1]):
        plt.figure(figsize=(14, 8))
        print('x_scale: ', prepare_value.shape)
        plt.plot(prepare_value[:, 0, i], color='r', label='data')
        plt.plot(preds[:, 0, i], color='b', label='predict')
        plt.legend()
        plt.show()

    print('model evaluate mse:', model.evaluate(preds, prepare_value))
    return preds, prepare_value

モデルの学習

下記のコードでモデルの学習、予測結果の確認を行います。

predict_list = []
var_list = []

print(max_weight)
print(x.shape)
model = create_model(input_dim, 
                     time_steps=time_steps,
                     latent_dim=120,
                     model_option='lstm',
                    )
log_dir = 'simple_multi_model'
p = Path(log_dir)
p.mkdir(parents=True, exist_ok=True)
tensorboard = TensorBoard(log_dir=log_dir,
                          write_graph=True,
                          embeddings_freq=0,
                          )

with timer('train model simple'):
    model.fit(x, x, epochs=400, callbacks=[tensorboard])
window = time_steps
x_test, scaler = prepare_data(X_test_joint, time_steps, variable_number)
predict_test, x_scale_test = predict_model_show_graph(X_test_day[window + 1:], 
                                                       x_test, scaler, model,
                                                       variable_number)

予測結果

テストデータに対する予測結果は下記のようになります。

気温に関する実データ(赤)と予測データ(青)

ガスの生産量に関する実データ(赤)と予測データ(青)

実データと予測データの差の指標としてRMSEを用いました。この値が小さいほど精度が高いモデルになります。

値は下記になります。

データRMSE
気温0.0341
ガスの生産量0.0910

個別入力型の深層学習モデルの組み合わせ

モデル定義

コードは下記のようになります。
下記のコードの場合はlist型のデータセットに時系列ごとに設定するLSTMモデルごとのノード数を入れておけば個別に設定が可能になります。時系列ごとにモデルを変更しても構いませんが今回はシンプルにするため、モデルのパラメータのみ変更しています。

def create_model_individual(
     input_dim,
     time_steps,
     latent_dim_list,
     # データが一つしかないので1しか選べない
     batch_size=1,
     model_option='lstm',
     optimizer='adam',
):
    input_list = []
    output_list = []

    with tf.name_scope('Model'):
        for i in range(input_dim):
            x = Input(shape=(time_steps, 1,))

            if model_option == 'lstm':
                with tf.name_scope('LSTM' + str(i)):
                    h = LSTM(latent_dim_list[i], stateful=False, return_sequences=True)(x)
            elif model_option == 'gru':
                with tf.name_scope('GRU' + str(i)):
                    h = GRU(latent_dim_list[i], stateful=False, return_sequences=True)(x)

            with tf.name_scope('Dense' + str(i)):
                out = Dense(1)(h)
            input_list.append(x)
            output_list.append(out)

        model = Model(inputs=input_list, outputs=output_list)
        model.summary()
        with tf.name_scope('ModelCompile'):
            model.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mse'])

    return model

定義されたモデルは下記のようになります。時系列ごとにLSTMが用意されていますが全体のモデルとしては1つになります。

データの前処理

モデルが異なるので入力するデータのフォーマットが異なります。(2, 150, 3, 1)(データの種類、バッチサイズ、window幅、lstmに入力するデータの種類)のサイズのデータをlist型にして返しています。

def prepare_data_individual(original_data, time_steps, variable_number):
    copy_data = original_data.copy()
    scaler = MinMaxScaler(feature_range=(0, 1), copy=False)
    data, scaler = transform_data(original_data=copy_data, 
                              inverse_option=False, scaler=scaler, variable_number=variable_number)
    data = np.asarray(data)
    x = get_data(data, time_steps=time_steps)
    x_reshape = []
    for i in range(x.shape[0]):
        x_reshape.append([x[i]])
    x_reshape = np.array(x_reshape)
    x_reshape = np.swapaxes(x_reshape, 1, 2)
    x_reshape = np.swapaxes(x_reshape, 2, 3)
    return list(x_reshape), scaler


x, scaler = prepare_data_individual(X_train_joint, time_steps, variable_number)

keras2.1.5のバージョンではデータの長さを下記でチェックしており、numpyのarray式でデータを与えるとデータのサイズチェックエラーが発生してしまいます。下記のコードを避けるためにデータの本質は同じですがlist形式にしています。

https://github.com/keras-team/keras/blob/2fae46169239287796d44523deb5e2ac38712ba3/keras/engine/training.py#L78

    if len(data) != len(names):
        if data and hasattr(data[0], 'shape'):
            raise ValueError(
                'Error when checking model ' + exception_prefix +
                ': the list of Numpy arrays that you are passing to '
                'your model is not the size the model expected. '
                'Expected to see ' + str(len(names)) + ' array(s), '
                'but instead got the following list of ' +
                str(len(data)) + ' arrays: ' + str(data)[:200] + '...')

モデルの学習

モデルの学習に使用したコードは下記になります。時系列ごとのlstmの出力空間の次元を変更しています。

predict_list = []
var_list = []

model = create_model_individual(input_dim, 
                     time_steps=time_steps,
                     latent_dim_list=[120, 150],
                     model_option='lstm',
                    )
log_dir = 'model_individual'
p = Path(log_dir)
p.mkdir(parents=True, exist_ok=True)
tensorboard = TensorBoard(log_dir=log_dir,
                          write_graph=True,
                          embeddings_freq=0,
                          )

with timer('train model individual'):
    model.fit(x, x, epochs=400, callbacks=[tensorboard])

window = time_steps
x_test, scaler = prepare_data_individual(X_test_joint, time_steps, variable_number)
predict_test, x_scale_test = predict_model_show_graph_individual(X_test_day[window + 1:], 
                                                       x_test, scaler, model,
                                                       variable_number)

予測結果

手法1と同様にRMSEで評価しました。

データRMSE
気温0.0174
ガスの生産量0.0732

比較結果

精度と学習速度、予測速度を比較します。

  • 実行環境
    • OS: macOS Sierra
    • CPU: 2.9 GHz Intel Core i7
    • メモリー: 16 GB 2133 MHz LPDDR3
  • pythonバージョン
    • 3.6.0
  • ライブラリ
 numpy==1.14.2
ipython==6.2.1
notebook==5.4.1
pandas==0.22.0
statsmodels==0.8.0
matplotlib==2.2.2
lxml==4.2.0
bs4==0.0.1
scikit-learn==0.19.1
scipy==1.0.0
keras==2.1.5
tensorflow==1.7.0
  • モデル
    • LSTM
    • ノード数:120, 個別入力は気温変化が120, ガスの生産量は150
    • 他のパラメータはkearasで提供されているデフォルト値
  • Optimiser
  • epoch
    • 400
モデル平均RMSE学習時間(400 epoch)予測時間
複数入力型の深層学習モデル0.062515.620秒0.52秒
個別入力型の深層学習モデルの組み合わせ0.045323.065秒0.693秒

平均RMSEは’個別入力型の深層学習モデルの組み合わせ’の方が約37%向上しています。(低い方が良い値です。)
学習時間は約33%遅くなり、予測時間は約33%遅くなっています。

このように各手法は精度と速度のトレードオフになるので用途に合わせて使用を使い分けて下さい。

今回使用したコードは下記になります。

https://github.com/SnowMasaya/time_series_anomaly_detect_hands_on/blob/master/advanced/time_series_anomaly_detect_keras_multivariate.ipynb

最後に

Kerasを使いこなせればこのような実装も楽にできます。Kerasの実装や時系列データに興味があるエンジニアがいらっしゃれば絶賛採用中なので是非、弊社へ応募してください。

参考

https://machinelearningmastery.com/multivariate-time-series-forecasting-lstms-keras/

その他の記事

Other Articles

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏


Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

バックエンドエンジニア(Python・Go)

業務内容

当ポジションは弊社Webサービスのバックエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

フロントエンドエンジニア(TypeScript)

業務内容

当ポジションは弊社Webサービスのフロントエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

機械学習エンジニア

業務内容

センサーデータの分析モデルの調査・研究・開発。 Kabuku Connectの製造データ(3D、2D)から情報を抽出するモデルの構築。 データの前処理や学習、ハイパーパラメータチューニング、獲得モデルの評価、プロダクションのデータパイプラインとの連携をお願いします。

インターン(Webエンジニア)

業務内容

業務から独立した、調査・研究系のタスクをおまかせしています。コードレビュー、 社内での報告会、 ブログ記事執筆を通して着実にスキルアップしていただくことを目指しています。 (希望があれば、プロダクトの開発業務もおまかせします。)

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く