深層学習でトレイルランニング・山登りのコースタイムを予測する

#モチベーション
山や丘陵地帯を軽装備を駆け抜ける、トレイルランニングやスピードハイクと呼ばれるようなアウトドア活動において、個々人のパフォーマンスに基づいたコースタイム予測を深層学習でやってみよう、というのが今回のお題。

<img width="768" alt="IMG_20150524_111413.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/167856/3016f983-cbce-52a0-e5d8-459bbe4acf85.jpeg">

登りや下りを含む不整地での移動速度は斜度による影響が支配的ですが、例えば山地図に書いてあるコースタイムは重たい装備を背負った登山者向けのものなので、軽装＋ランニングシューズを履いた活動ではあまり参考にならず、新しいルートを開拓する際に一定レベル以上の精度でコースタイムの予測をする方法を確立したい...という個人プロジェクト([Trail Route Analytics](http://trail-route-analytics.blogspot.jp/))を進めていったところ、機械学習/深層学習的なアプローチにたどり着きました。

#結果
深層学習的なアプローチに依らない方法として、あれこれ数式を考えてパフォーマンスをパラメーター化する手法をあれこれトライしていましたが([例](http://trail-route-analytics.blogspot.jp/2015/12/calculating-threshold-grade-from.html))、そこから得られたコースタイムの予測精度は同一コースであってもせいぜい誤差が7,8%というもので、異なるコースデータとなると誤差が10%を超えてしまうような頼りないものでした。

シンプルなニューラル・ネットワークを構築してデータを流し込むことで、学習させていないコースであっても誤差が2,3%のレベルでコースタイム予測ができることが確認できました。

同一人物の同種のアクティビティー(トレラン対トレラン)であれば、異なるルートであっても一定レベルの精度で予測できることも確認できていますが、これについてはもう少しサンプル数を増やす必要がありそうです。

#アプローチ
大前提として、山での活動を記録したGPSログデータが必要になります。個人的に使っているのはGarminのGPSデバイス(FenixやEdgeシリーズなど)で、Garmin ConnectのアクティビティーページからダウンロードできるGPXファイルを利用してデータの解析を行っています。

GPSのログデータから、各データポイントをつなぐセグメントのリストを構築すると、ポイント間の水平距離、垂直距離、所要時間といった情報が連続したデータが取得できます。セグメントの距離はGPSログの記録インターバル等(一般的には毎秒)に依存しますが、概ね数メートルから20-30メートル程度となります。解析対象としたいのは移動中のデータのみなので、一定速度以下のデータ(0.2km/h未満)についてはレストタイムとして解析対象から外し、行動時間に対して事前に設定したレストタイムの比率を掛け合わせることで最終的な実所要時間を計算することにします([データ解析](http://yamakei.hatenadiary.jp/entry/2015/05/12/221844)によると、いわゆるスピードハイクだとレストタイムの比率は約5%程度になるようです)。

さて、ここで得られた移動時のみのセグメントデータですが、各セグメントの速度は前後のセグメントの影響を受けます。なぜかというと、例えばフラット区間が連続する箇所で、短いながらも急な登りがでてきて速度をあまり殺さずに突破する場合と、急な登りが延々と続く区間を一定のペースで進むのとでは、同じ斜度だったとしても全く異なる速度になる可能性が高いからです。

そんなわけで、連続したセグメントデータN個分がNNの学習データの入力(X)となり、この区間の所要時間がNNの出力(Y)となります。

#実装

## GPSログデータの読み込み

自作の[trail_analyzer](https://github.com/yama-kei/trail_analyzer/blob/master/trail_analyzer.py)(下回りに`gpxpy`を使った登山活動のログ分析ライブラリ)を利用して、GPXデータを読み込んで連続したセグメントのデータをCSVファイルに書き出します。

```python
import csv
import json
from trail_analyzer import *

SEGMENT_FEATURE_NUM = 2
TRACK_LEN = 20
SEGMENT_LEN = TRACK_LEN * 2
TRAIL_LOG_DATA = 'trail_log_data_nn.csv'

def segment_loader(file_list):
    track_rec = []
    segment_count = 0
    for filepath in file_list:
        f = open(filepath)
        rd = RecordData(f)
        moving_track = [track for track in rd.track_data if not track.is_stop()]
        segment_num = len(moving_track)
        for i, track in enumerate(moving_track):
            segment = [t.grade, t.dist, t.time]
            track_rec.append(segment)
            segment_count += 1
    with open(TRAIL_LOG_DATA, 'w') as f:
        writer = csv.writer(f, lineterminator='\n')
        writer.writerows(track_rec)
    return segment_count

segment_count = segment_loader(f_list)
```

## NNによる学習

深層学習にはKerasを利用します。hidden layerが一層のみの単純なNNを構築して、データを流し込んで学習を行います。

* hidden layerの幅はinput layerの2倍とし、activation functionには`relu`(Rectified Linear Unit)を利用
* output layerは所要時間(秒)なので1層
* Loss functionには`mean_squared_error`、Optimizerには`adam`を利用
* `validation_split=0.33`として入力データの1/3をテスト用データとして利用
* batch sizeは`200`を指定し、100 epoch分の学習を行う

```python
import numpy

from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout

dataset = numpy.loadtxt(TRAIL_LOG_DATA, delimiter=",")
# split into input (X) and output (Y) variables
X = dataset[:,0:SEGMENT_LEN]
Y = dataset[:,SEGMENT_LEN]

model = Sequential()
model.add(Dense(SEGMENT_LEN*2, input_dim=SEGMENT_LEN, init='uniform', activation='relu'))
model.add(Dense(1, init='uniform'))
model.compile(loss='mean_squared_error', optimizer='adam', metrics=['accuracy'])

# Fit the model
history = model.fit(X, Y, validation_split=0.33, nb_epoch=100, batch_size=200, shuffle=True, verbose=0)
```

## 学習曲線を描く

学習の進捗をチェックするには、学習曲線(Learning Curve)を描くと便利です。

```python
%matplotlib inline
import matplotlib.pyplot as plt

# summarize history for accuracy
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
# summarize history for loss
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')

plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
```

###予想精度の変動

![tra1.png](https://qiita-image-store.s3.amazonaws.com/0/167856/79715bb8-9ee2-63c2-b3e6-cec080ae9137.png)

概ね2%前後に収束していることがわかります。

###lossの変動

![tra2.png](https://qiita-image-store.s3.amazonaws.com/0/167856/e98df770-6cc4-6567-2705-ed53892bab67.png)

けっこう早い段階に収束し始めてます。

##NNのモデルを可視化

`keras.utils.visualize_util`を使うとNNのモデルを可視化できます。

```
from IPython.display import SVG
from keras.utils.visualize_util import model_to_dot, plot
SVG(model_to_dot(model, show_shapes=True).create(prog='dot', format='svg'))
```

![tra3.png](https://qiita-image-store.s3.amazonaws.com/0/167856/4d1237c6-dc20-2255-7c09-145b2f8ecc34.png)

#感想

NNを用いた学習の手続きはたかだか10行程度のコードですが、たったこれだけでかなり高いレベルの精度で山でのアクティビティーの移動速度の予想ができる可能性がある…という意味で、ものすごいポテンシャルがあるなーということを感じます。

ログデータのポイント数の削減とスムーズ化(閾値に満たないセグメントのデータを丸める)する処理の有無や、学習を行うセグメント数を変動させてみると、入力データのプロセッシングで予想精度が変わってくるので、そのあたりについては[プロジェクトのブログ](http://trail-route-analytics.blogspot.jp/2017/03/how-parameters-affects-prediction.html)に書いてあります。手持ちのログデータと、シンプルなNNのモデルに関して言えば、2mの閾値と20か30くらいのセグメント数でよい結果が求められるようです。

入力データの距離を平準化して、連続する同一距離のセグメントの斜度情報のみを入力して学習させたほうがよい結果が得られるのかなーという気がしていますが、データの処理の関係で難しいのかなと思っています。

連続したデータなので、1D CNNとかRNNとかもう少し複雑なモデルを用いて学習させてみるとどうなるかも試してみたいところです。