ボット開発者のための機械学習入門

## はじめに

2016年はボットが注目を集めた1年でした。また、機械学習関連の技術進歩がめざましく、次々と新たな可能性が見えてきた年でもありました。

この記事を開いたあなたはきっと「機械学習を使ったボットを開発したい」と思っていることでしょう。
「でも何をどうしたらいいか分からない」
この記事はそんな読者を想定しています。

## 機械学習の使いどころ・使い方

機械学習の使いどころには大きく分けて２つあると思います。
一つは「ユーザとボットのやりとり」の部分、もう一つは「ユーザに提供するサービス」の部分です。

前者は、自然文によってユーザとやりとりすることを可能にします。ここはMicrosoft LUISなどの既存サービスを利用するのが現実的でしょう。
LUIS以外にどういったサービスがあるか、下記によくまとまっています。

[BOTで使える会話API・ライブラリ・サービスまとめ](http://qiita.com/kenzo0202/items/582e3a5e06b64ab24964)

次に「ユーザに提供するサービス」の部分について。
これはさらに以下の2つに分けられると思います。

### その１.一般提供されているサービス, APIを利用する

[(主に)ディープラーニングの成果を利用したAPI集(自分用)](http://qiita.com/shngt/items/966799cc3f7c77a6fa17)

上記記事で紹介されているAPI等を使うと、最新の機械学習の成果がいとも簡単に利用できるようになります。
注意したいのは、APIをそのまま使うだけだと似たようなボットばかりになってしまうことでしょうか？

### その２.機械学習フレームワークを使う

もう一つは、機械学習フレームワークを使ってコードを書く。さらに自前のデータ、リソース(GPUとかですね)を使って学習させるというアプローチです。
一気にハードルは上がりますが、オリジナリティの高いサービスを提供するのであれば避けて通れない部分かと思います。

さあ、それでは機械学習を使った独自のサービスを提供するボットを実際に開発してみましょう！

## フレームワークの選択

実装に入る前に、今回使う機械学習フレームワークを選択します。

[深層学習フレームワークヒッチハイクガイド](http://qiita.com/aokikenichi/items/c359a53e8307be3ebe02)

一番人気はTensorflowですが、低レベルAPI(性能が低いという意味ではないですよ？)のため初心者にとっては少し敷居が高いかもしれません。
今回は[Keras](https://keras.io/ja/)を選択したいと思います。こちらは高レベルAPIなので記述が簡潔で分かりやすいという利点があります。

さらに、ボットを実装するフレームワークも選択しましょう。

上記記事を見て分かる通り、使用言語はPythonが多いです。一方、ボットの実装に使われることが多いのはNode.jsでしょうか？
このミスマッチを解消する方法としては恐らく「それぞれを分けて実装し、API経由で利用する」が一般的なのでしょうが、今回は実装を簡単にするため、ボットもPythonで書きたいと思います。
使用するフレームワークは[python-rtmbot](https://github.com/slackhq/python-rtmbot)です。

## 何をすべきか？

機械学習を使った独自のサービスを提供するボットを開発しようとした場合、非常に大まかに言って以下の３つを行う必要があります。

1. 学習モデルを構築する
2. データを用意し、モデルを学習させる
3. 学習したモデルを使って処理する

機械学習フレームワークのチュートリアルで行われているのはほとんど上記1, 2のみです。
※参考：[TensorFlowチュートリアル - TensorFlowメカニクス101（翻訳）](http://qiita.com/KojiOhki/items/0640d01029371d6ae092)

モデルの構築、学習は基本的に1回行えば十分です。もちろん、それで十分に精度が高い予測ができることが前提です。
その後、モデル構造、学習結果(パラメータ)をファイルに書き出し、実行時(=3)に読み込むというのが大まかな流れとなります。

### モデル構築, 学習

モデルの構築と学習については前述の通りチュートリアル、サンプルが豊富ですので、今回は既存のものを流用します。

[Kerasのcallbackを試す（modelのsave,restore/TensorBoard書き出し/early stopping）](http://qiita.com/yukiB/items/f45f0f71bc9739830002)
これを利用し、「話しかけられるとsinの値を返す」ボットを作成します。実用性はありませんが、サンプルとしては十分でしょう。

また、今回のコード一式を[Github(fullkawa/ml-bot-sample)](https://github.com/fullkawa/ml-bot-sample)に上げました。
以後はこちらから必要な部分のみ抜粋して解説していきます。

まず、["train.py"](https://github.com/fullkawa/ml-bot-sample/blob/master/train.py)ですが、こちらは参考記事のソースをほぼまとめただけのものとなります。

ポイントは、73行目の

```python:train.py
cp_cb = ModelCheckpoint(filepath = fpath, monitor='val_loss', verbose=1, save_best_only=True, mode='auto')
```

と78〜80行目の

```python:train.py
json_string = model.to_json()
#open(os.path.join(f_model,'./tensorlog/rnn_model.json'), 'w').write(json_string)
open(os.path.join('./tensorlog/rnn_model.json'), 'w').write(json_string)
```

です。
前者で学習結果(パラメータ)、後者でモデル構造をファイルに書き出しています。
他の部分で何をしているかは参照元記事をご覧ください。

ボットを動かす前に`python train.py`を一度実行します。

```
112-233:ml-bot-sample y.furukawa$ python train.py
Using TensorFlow backend.
____________________________________________________________________________________________________
Layer (type) Output ShapeParam # Connected to
====================================================================================================
lstm_1 (LSTM)(None, 300) 362400lstm_input_1[0][0]
____________________________________________________________________________________________________

dense_1 (Dense)(None, 1) 301 lstm_1[0][0]
____________________________________________________________________________________________________
activation_1 (Activation)(None, 1) 0 dense_1[0][0]
====================================================================================================
Total params: 362701
____________________________________________________________________________________________________
Train on 3325 samples, validate on 176 samples
Epoch 1/10
3000/3325 [==========================>...] - ETA: 4s - loss: 0.2627 - acc: 0.0000e+00 Epoch 00000: val_loss improved from inf to 0.19250, saving model to ./tensorlog/weights.00-0.24-0.19.hdf5
3325/3325 [==============================] - 49s - loss: 0.2408 - acc: 3.0075e-04 - val_loss: 0.1925 - val_acc: 0.0000e+00
Epoch 2/10
3000/3325 [==========================>...] - ETA: 4s - loss: 0.0456 - acc: 3.3333e-04 Epoch 00001: val_loss improved from 0.19250 to 0.00085, saving model to ./tensorlog/weights.01-0.04-0.00.hdf5
3325/3325 [==============================] - 48s - loss: 0.0412 - acc: 3.0075e-04 - val_loss: 8.4748e-04 - val_acc: 0.0000e+00
Epoch 3/10
3000/3325 [==========================>...] - ETA: 4s - loss: 0.0015 - acc: 3.3333e-04 Epoch 00002: val_loss did not improve
3325/3325 [==============================] - 47s - loss: 0.0024 - acc: 3.0075e-04 - val_loss: 0.0228 - val_acc: 0.0000e+00
Epoch 4/10
3000/3325 [==========================>...] - ETA: 4s - loss: 0.0189 - acc: 3.3333e-04 Epoch 00003: val_loss did not improve
3325/3325 [==============================] - 46s - loss: 0.0177 - acc: 3.0075e-04 - val_loss: 0.0055 - val_acc: 0.0000e+00
Epoch 5/10
3000/3325 [==========================>...] - ETA: 4s - loss: 0.0089 - acc: 3.3333e-04 Epoch 00004: val_loss did not improve
3325/3325 [==============================] - 47s - loss: 0.0095 - acc: 3.0075e-04 - val_loss: 0.0163 - val_acc: 0.0000e+00
Epoch 00004: early stopping
```

今回の場合、Epock 2/10で出力された"weights.01-0.04-0.00.hdf5"が最も精度が高く(val_loss: 8.4748e-04 - val_acc: 0.0000e+00)、以降はむしろ悪化しています。

### ボットの実装

次に、ボットのプログラムです。
[これ(plugins/sin.py)](https://github.com/fullkawa/ml-bot-sample/blob/master/plugins/sin.py)が今回のメインディッシュです。

```python:plugins/sin.py
model_json = open('tensorlog/rnn_model.json', 'r').read()
model = model_from_json(model_json)
```

モデル構造を読み込み、

```python:plugins/sin.py
files = glob.glob('tensorlog/weights.*.hdf5')
model.load_weights(files[-1])
```

学習結果(パラメータ)を読み込みます。
数字が一番大きいファイル＝最も精度が高い学習済みパラメータを読み込んでいます。

これだけで、

```python:plugins/sin.py
predicted = self.model.predict(X, batch_size=1)
```

という具合にsin値を取得することができます。
その前の

```python:plugins/sin.py
X = np.zeros((1,100,1))
for i in range(0, 100):
  X[0, i, 0] = i # 本サンプルでは固定とする
```

はsin値を算出するためのデータを用意しているだけで特に重要な箇所ではありません。しかし、配列のサイズを揃える必要があるのは地味に面倒です。

### 実行結果

```
112-233:ml-bot-sample y.furukawa$ rtmbot
Using TensorFlow backend.
Model loaded.
Weights loaded from tensorlog/weights.01-0.04-0.00.hdf5
READY!
```

今回、ボットの名前を"stag.feec"としました。
話しかけてみます。

<img width="506" alt="Slackキャプチャ" src="https://qiita-image-store.s3.amazonaws.com/0/11338/7f696061-0eb4-b3e6-4d75-e2dacc70b102.png">

無事、sinの値を返すことができました！