DeepLearningを使った時系列分類

## 前書き
昨年度までは諸事情によりCaffeを利用させていただいていましたが、
今年度はなるべくTensorFlowで開発を実施したいなぁと考えております。

kerasを使ったことはありますが、ここ最近のTensorFlowのアップデートを見ると
どうもEstimatorというAPIがキテいる模様・・・

ということもありますので、以前紹介させていただきました、時系列データを使用して
Estimatorを使ったDeepLearningを行ないます。

## 下準備
私は以下環境で開発しました。

+ python (3.5.2)
+ tensorflow (1.7.0)
+ pandas (0.22.0)

+ 評価データ ([UCR Time Series Classification Archive](http://www.cs.ucr.edu/~eamonn/time_series_data/))
    + リンク先上部のarchiveからダウンロードできます。 

足りないものあれば適宜pip等でインストールしていただければ幸いです。

TensorFlowのバージョンが低いとEstimatorなどが"contrib"の方にいるかもしれないので、
なるべく最新の安定板が良いんじゃないかなぁと思います。

また時系列データが対象なので、GPUを積んでないようなマシンでも十分動作します。
（私もノートPCで開発しておりますし・・・）

## ソースコード
本当はGitHubを利用すれば良いのでしょうが、ほとんど使ったことなく
導入に時間がかかりそうなため、今回はベタ張りでディレクトリ構成およびソースコードを張ります。

一応ソースにはコメントをつけておりますが、公式ドキュメントが充実しておりますので、
そちらを参照してもらった方が良いかもしれません。

### ディレクトリ構成
評価データをdataフォルダに格納します。
サンプルだと”Adiac”のみを使用しているので、自分にあった使いたい
データセットを格納してください。

```
estimator_test
├── data
│   └── UCR_TS_Archive_2015
│       ├── Adiac
│       ...
│       └── yoga
│
└── src
    └── main.py
```

### main.pyの中身
下記の論文およびコードを参考にしています。
[Time Series Classification from Scratch with Deep Neural Networks: A Strong Baseline](https://arxiv.org/abs/1611.06455)
↑論文で提案されているMLPを再現しています。
[TensorFlow/models/samples/core/get_started/](https://github.com/tensorflow/models/tree/master/samples/core/get_started)


```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import pandas as pd
import numpy as np
import tensorflow as tf

label_name = "label"

def data_norm_label(data_y):
    # 分類クラスの番号を正規化します
    # ラベルを0スタートにしないとDatasetの作成部分で上手くいきませんでした
    nb_classes = data_y.nunique()
    data_y = (data_y - data_y.min()) / (data_y.max() - data_y.min()) * (nb_classes - 1)
    data_y = data_y.astype("int64")
    return data_y

def data_norm_feature(data_x, train_mean=None, train_std=None):
    # 学習データの正規化（平均０分散１）
    # 正規化しないとうまく学習が進みません
    # また、テストデータは学習データと同様の範囲で正規化します
    if train_mean == None:
        train_mean = np.mean(data_x.values.flatten())
        train_std = np.std(data_x.values.flatten())
        # 以下だと列ごとに正規化する
        # train_mean = data_x.mean()
        # train_std = data_x.std()
    data_x = (data_x - train_mean) / (train_std)
    return data_x, train_mean, train_std

def column_rename(df_data):
    # ヘッダーの指定なしに読み込んでいるので、
    # 列名には行数がそのまま割り振られています
    # 列名が数値だとDatasetでエラーが出たため、ラベル名を作成してます
    rename_list = [label_name]
    feature_list = []
    for index in range(len(df_data.columns)-1):
        feature_list.append("feature" + str(index))
    rename_list = rename_list + feature_list
    df_data.columns = rename_list
    return df_data

def load_data(dir_name):
    # ファイルより学習データ、テストデータを読み込む
    # 読み込み時に正規化等の前処理を行う
    train_path = ".." + "/" + "data" + "/" + "UCR_TS_Archive_2015" + "/" + dir_name + "/" + dir_name + "_TRAIN"
    test_path = ".." + "/" + "data" + "/" + "UCR_TS_Archive_2015" + "/" + dir_name + "/" + dir_name + "_TEST"

    train = pd.read_csv(train_path, header=None)
    train = column_rename(train)
    train_x, train_y = train, train.pop(label_name)
    train_x, train_mean, train_std = data_norm_feature(train_x)
    train_y = data_norm_label(train_y)

    print(train_mean)
    print(train_std)

    test = pd.read_csv(test_path, header=None)
    test = column_rename(test)
    test_x, test_y = test, test.pop(label_name)
    test_x, train_mean, train_std = data_norm_feature(test_x, train_mean=train_mean, train_std=train_std)
    test_y = data_norm_label(test_y)

    return (train_x, train_y), (test_x, test_y)

def my_model(features, labels, mode, params):
    # 学習モデルの構成
    net = tf.feature_column.input_layer(features, params['feature_columns'])

    net = tf.layers.dropout(net, rate=0.1)
    net = tf.layers.dense(net, units=500, activation=tf.nn.relu)
    net = tf.layers.dropout(net, rate=0.2)
    net = tf.layers.dense(net, units=500, activation=tf.nn.relu)
    net = tf.layers.dropout(net, rate=0.2)
    net = tf.layers.dense(net, units=500, activation=tf.nn.relu)
    net = tf.layers.dropout(net, rate=0.3)

    logits = tf.layers.dense(net, params['n_classes'], activation=None)

    predicted_classes = tf.argmax(logits, 1)
    if mode == tf.estimator.ModeKeys.PREDICT:
        predictions = {
            'class_ids': predicted_classes[:, tf.newaxis],
            'probabilities': tf.nn.softmax(logits),
            'logits': logits,
        }
        return tf.estimator.EstimatorSpec(mode, predictions=predictions)

    loss = tf.losses.sparse_softmax_cross_entropy(labels=labels, logits=logits)

    accuracy = tf.metrics.accuracy(labels=labels,
                                   predictions=predicted_classes,
                                   name='acc_op')
    metrics = {'accuracy': accuracy}
    tf.summary.scalar('accuracy', accuracy[1])

    if mode == tf.estimator.ModeKeys.EVAL:
        return tf.estimator.EstimatorSpec(
            mode, loss=loss, eval_metric_ops=metrics)

    assert mode == tf.estimator.ModeKeys.TRAIN

    optimizer = tf.train.AdamOptimizer(learning_rate=0.001)
    train_op = optimizer.minimize(loss, global_step=tf.train.get_global_step())
    return tf.estimator.EstimatorSpec(mode, loss=loss, train_op=train_op)

def train_input_fn(features, labels, batch_size):
    dataset = tf.data.Dataset.from_tensor_slices((dict(features), labels))
    dataset = dataset.shuffle(1000).repeat().batch(batch_size)
    return dataset

def eval_input_fn(features, labels, batch_size):
    features=dict(features)
    if labels is None:
        inputs = features
    else:
        inputs = (features, labels)
    dataset = tf.data.Dataset.from_tensor_slices(inputs)
    assert batch_size is not None, "batch_size must not be None"
    dataset = dataset.batch(batch_size)
    return dataset

parser = argparse.ArgumentParser()
parser.add_argument('--batch_size', default=100, type=int, help='batch size')
parser.add_argument('--train_steps', default=1000, type=int,
                    help='number of training steps')

#flist = ['Adiac', 'Beef', 'CBF', 'ChlorineConcentration', 'CinC_ECG_torso', 'Coffee', 'Cricket_X', 'Cricket_Y', 'Cricket_Z', 
#'DiatomSizeReduction', 'ECGFiveDays', 'FaceAll', 'FaceFour', 'FacesUCR', '50words', 'FISH', 'Gun_Point', 'Haptics', 
#'InlineSkate', 'ItalyPowerDemand', 'Lighting2', 'Lighting7', 'MALLAT', 'MedicalImages', 'MoteStrain', 'NonInvasiveFatalECG_Thorax1', 
#'NonInvasiveFatalECG_Thorax2', 'OliveOil', 'OSULeaf', 'SonyAIBORobotSurface', 'SonyAIBORobotSurfaceII', 'StarLightCurves', 'SwedishLeaf', 'Symbols', 
#'synthetic_control', 'Trace', 'TwoLeadECG', 'Two_Patterns', 'uWaveGestureLibrary_X', 'uWaveGestureLibrary_Y', 'uWaveGestureLibrary_Z', 'wafer', 'WordsSynonyms', 'yoga']

flist  = ['Adiac']

def main(argv):
    args = parser.parse_args(argv[1:])

    for file_name in flist:
        (train_x, train_y), (test_x, test_y) = load_data(file_name)
        nb_classes = train_y.nunique()

        my_feature_columns = []
        for key in train_x.keys():
            my_feature_columns.append(tf.feature_column.numeric_column(key=key))
        
        classifier = tf.estimator.Estimator(
            model_fn=my_model,
            params={
                'feature_columns': my_feature_columns,
                'n_classes': nb_classes,
            })

        classifier.train(
            input_fn=lambda:train_input_fn(train_x, train_y,
                                        args.batch_size),
            steps=args.train_steps)

        eval_result = classifier.evaluate(
            input_fn=lambda:eval_input_fn(test_x, test_y,
                                        args.batch_size))

        print('\nTest set accuracy: {accuracy:0.3f}\n'.format(**eval_result))


if __name__ == '__main__':
    tf.logging.set_verbosity(tf.logging.INFO)
    tf.app.run(main)
```

##実行結果
学習ステップに応じて、lossが低下していることがわかります。
最終的なテストデータに対する正解率は63.4%、論文のErrRateは0.248(正解率:75.2%) 
...(ﾟ∀ﾟ)？？

論文のGitHubを見ると、fitのValidationにテストデータを指定しているので、
テストデータ寄りに学習が進んでるのかもしれません。
（kerasの中身を知らないので私のカンではありますが・・・）

また、batchsizeや、epochsizeも異なるので、その辺りも影響しているのだと思います。

サンプル等々のツギハギソースで、とりあえず上手く動いて、
それなりに分類できていることがわかりました。

```
NFO:tensorflow:global_step/sec: 55.9142
INFO:tensorflow:loss = 1.8498216, step = 101 (1.790 sec)
INFO:tensorflow:global_step/sec: 69.5335
INFO:tensorflow:loss = 0.93660194, step = 201 (1.438 sec)
INFO:tensorflow:global_step/sec: 69.4014
INFO:tensorflow:loss = 1.1435629, step = 301 (1.441 sec)
INFO:tensorflow:global_step/sec: 70.3544
INFO:tensorflow:loss = 0.49161476, step = 401 (1.421 sec)
INFO:tensorflow:global_step/sec: 69.8491
INFO:tensorflow:loss = 0.4364499, step = 501 (1.432 sec)
INFO:tensorflow:global_step/sec: 70.5117
INFO:tensorflow:loss = 0.48663452, step = 601 (1.418 sec)
INFO:tensorflow:global_step/sec: 70.9824
INFO:tensorflow:loss = 0.3646434, step = 701 (1.409 sec)
INFO:tensorflow:global_step/sec: 70.5241
INFO:tensorflow:loss = 0.5106917, step = 801 (1.418 sec)
INFO:tensorflow:global_step/sec: 70.6443
INFO:tensorflow:loss = 0.25705165, step = 901 (1.416 sec)
INFO:tensorflow:Saving checkpoints for 1000 into /tmp/tmpl8zt72wq/model.ckpt.
INFO:tensorflow:Loss for final step: 0.25710252.
INFO:tensorflow:Calling model_fn.
INFO:tensorflow:Done calling model_fn.
INFO:tensorflow:Starting evaluation at 2018-04-29-09:15:37
INFO:tensorflow:Graph was finalized.
INFO:tensorflow:Restoring parameters from /tmp/tmpl8zt72wq/model.ckpt-1000
INFO:tensorflow:Running local_init_op.
INFO:tensorflow:Done running local_init_op.
INFO:tensorflow:Finished evaluation at 2018-04-29-09:15:37
INFO:tensorflow:Saving dict for global step 1000: accuracy = 0.6342711, global_step = 1000, loss = 2.2607203

Test set accuracy: 0.634
```

## 所感
以下作ってみて思ったことを・・・
+ 本題であるEstimatorの記述は直感的で使いやすいと思った
+ ただし、他の高位APIや他OSSと同様に入力データの編集部分で手間
+ ↑に関連してDataSetの作りがキモな気がする
+ 入力データの生成とモデルの生成が切り離されているので、一度DataSetを作ればモデルの変更を行ないながらの検討は捗るか・・？
+ MLPベース->CNNベースに変更したらDataSetの形式を変更になるのでは？↑の旨味は薄くなる？
+ シャッフルやバッチ作成など、取り回しはすごい楽そう
+ 今の所kerasとの違い（旨味）が体感できていない。kerasより高自由度な感触はあるが・・・
+ 使い慣れてないだけで、プログラム自体は書きやすい印象
+ OSSやAPIがいっぱいあって追いつけない(ﾟ∀ﾟ)

DataSetも含めてまだまだ勉強だなぁと思いました。