TensorFlowで機械学習と戯れる: LSTMでユーザの行動を予測するモデルを作る実験


はじめに
===========

LSTM(Long Short Term Memory) は時系列データの学習によく用いられているものです。 [わかるLSTM ～ 最近の動向と共に](http://qiita.com/t_Signull/items/21b82be280b46f467d1b)の記事がとても詳細に説明されています。
上記でも解説されていますが、LSTMは例えば「今までの単語列を入力として、もっともらしい次の単語を予測する」というような「文章の生成」に使うことができます。


このLSTMを使って、例えば、Webサービスにアクセスする **「ユーザの行動」を「単語」** とみなせば **「ユーザの一連の行動」は「文章」** とみなせるわけで、 **「こういう行動を取ってきた人は、次にこういう行動を行う」**という予測モデルが作れないかと考えました。
この予測モデルが作れれば、あとは文章生成のような形で **行動生成** ができます。つまり、ある種の **ユーザ行動のシミュレーション** ができることになります。
ユーザがある程度アクセスすれば、 **このまま離脱する確率が20％** とか **次も訪問してくれる確率が30%** と分類することが可能になり、施策を選択するための判断材料としても使えそうです。この予測モデルにとって重要な性質は、 **生成した行動の確率分布が実際に分布に近いこと** かと思います。

今回はLSTMで実際にそういう確率分布的に近い予測モデルが作れるのか実験してみました。

問題設定
=========

擬似的なユーザの行動の定義
---------------------

* ユーザの行動は 0~9 で表されるとします。 10 は行動が終了したことを意味します。
* 一連のユーザの行動は、 1で始まり 10で終わります
	* 1 が行動開始、 10が行動終了 
* 任意の行動後、一定割合で 10に遷移します。つまり「一連の行動」の長さは可変です。
* 基本的に 「前の行動+1 mod 10」の行動を行いますが、確率的にそうではない行動も行います

このようなデータを生成するプログラム（擬似データ生成プログラム）を作り、そのデータを学習させます。

モデルの評価
---------

上記のユーザの行動を再現するモデルを作成するわけですが、３つの観点からモデルを評価します。

* 「一連の行動」の長さの分布
* 「行動」の出現頻度
* ２つの連続した「行動」の出現頻度

分布の近さ（距離）を評価するために、 Jensen-Shannon Divergence(以降、JSDと呼びます)というのを用いました。後はグラフで目で見て判断します。

参考: [numpyでのKLダイバージェンスとJensen-Shannonダイバージェンスの実装](http://blog.vingow.com/tech/numpy-jensen-shannon/)

実験まとめ
======
結論から言うと、今回の問題については良い予測モデルができました。
下記の図は、それぞれ1万回分の「真のアルゴリズム(擬似データ生成プログラム）で生成したデータ」と「予測モデルで生成したデータ」を比較評価したものです。

1つ目のグラフが「一連の行動の長さ」をPlotしたもの、
2つ目のグラフが「0~10の行動」の出現頻度を比較したもの、
3つ目のグラフが「２つの連続した行動」の出現頻度を比較したもの、になります。

![pad.ns-25.bs-40.nl-2.hs-20-figure.png](https://qiita-image-store.s3.amazonaws.com/0/11124/eaa0af25-1369-7072-c027-a125de69c9ca.png "figure.png")

多少ずれている感じはありますが、なかなか再現できているのではないかと思っています。

実験詳細
=======
ここからはコードや色々試行錯誤したことをだだっと書いていきます。備忘録も兼ねて。

プログラム
----------
### 擬似データ生成プログラム
https://gist.github.com/mokemokechicken/7e512a0a97193bd7420c
にある `generate_seq()` で生成しています。

```python
def generate_seq():
    ret = [1]
    while True:
        rand = random.random()
        if rand < 0.1:
            ret.append(10)  # End Of Sequence
            return ret
        elif rand < 0.6:
            ret.append((ret[-1] + 1) % 10)
        else:
            n = (ret[-1] % 5) + 1  # 1~5
            ref = ret[-min(n, len(ret))]  # 直近のn番目の数字
            ret.append((ref + 1) % 10)
```

### LSTMのモデル作成
最終的には
https://gist.github.com/mokemokechicken/1e9107a75928c2ee1cb2
https://gist.github.com/mokemokechicken/026d2a674223b4a74df1
のような感じで作っています。

LSTM の学習のさせ方は、他のDeepLearningなどの時と少し違うので最初戸惑いました。

### LSTMモデルからデータの生成
最終的には
https://gist.github.com/mokemokechicken/3caa4ef52f2bb33760a7
となりました。

最初は、１つ１つSamplingしていたのですが、結構時間がかかるので、まとめてSamplingする版も作りました。10倍くらい速くなりました。

### データの比較評価
https://gist.github.com/mokemokechicken/8a82e18fb384db5352b1
となりました。

### まとめて実行するスクリプト
上記のソースコードを `src/rnn/*` などにおいて、こんな感じのスクリプトで実行します。
https://gist.github.com/mokemokechicken/974ab2b35b35716bca53
https://gist.github.com/mokemokechicken/079e9bc4d7f3c58ec492

試行錯誤
-------

最初はとにかく単純になにも考えずにやってみました。
![figure.png](https://qiita-image-store.s3.amazonaws.com/0/11124/3ce964bc-cc8e-6224-405e-c01664cd2bed.png "figure.png")

すると、「長さ」の分布がかなり違うものができました。予測モデルの方の、長さが短い部分の割合が大きくなっているので、「速攻で行動終了」という感じになっています。

これには 今回のLSTMならではの学習方法に関係があるのではないかと思いました。

### LSTMの学習

TensorFlowのサンプルにある PTBModel というのを参考に作ったのですが、
これは基本的に全ての文章(≒単語の並び)(≒ユーザの行動)を１つのシーケンスにした入力を取ります。
そこから 行が`batch_size` になるように折り返して、さらに `num_steps`という単位で学習していきます。イメージとして下記のような感じです。

![PTBModel-Learn.png](https://qiita-image-store.s3.amazonaws.com/0/11124/fa4b4ea7-528f-0322-221a-40a679dafd70.png "PTBModel-Learn.png")

batch_size を使うのは計算の高速化のためですが、単純に折り返すために 「7から始まる一連の行動」なども学習してしまいます。今回は、「始まり〜終わり」があって、その長さなども重要視していましたが、その観点が失われている気がします。

### padding?
そこで「一連の行動」がnum_steps の長さの整数倍になるような padding を入れられるようにしました。すると、 「７から始まる」というようなデータはなくなります。

でも、いま改めて良く考えると、 batch_len の途中で切れないような paddingにすべきだった・・・のかな。また今度考えよう。。

### 色々なパラメータの試行錯誤
主なパラメータとして下記のものがあります。

* batch_size
* num_steps
* LSTMのノード数やレイヤー数
* 学習回数

色々試したのですが、法則のようなものがまだ掴めてないです。
今回は学習回数は固定したうえで

* batch_size は 40 くらいがよい
* num_steps は 20~25 くらいが良い
* ノード数は20前後（多くてもダメ）、Layer数は１よりは２が良い

という感じです。
LSTMのノード数が多い場合は、学習回数を大きくすれば良いのかもしれません。
まあ、現段階である程度満足行く結果だったので、そこまでやりませんでした。

さいごに
=======

単純なパターンであれば、かなり近い分布を生成できるモデルが学習できることがわかりました。

実際のアプリケーションに使うにはもっと複雑な状況になりますし、まだ色々不明な振る舞いが残っているので、課題は多そうです。

まあ、LSTMが少しわかってきて良かったです。