Chainerで機械学習と戯れる： Jumpで穴を飛び越えろゲーム（自作）を強化学習できるか？


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

前回は [足し算ゲームをChainerを使って強化学習できるか？](http://qiita.com/mokemokechicken/items/aa1535696514bfe51329) で単純な足し算ゲームの強化学習をやってみました。
一応基本はわかってきたつもりなので、ゲームっぽいものに挑戦したいと思います。

そこでゲーム環境を作り、その上でゲームを実装し、そのゲームをChainerにやらせるというアプローチをとることにしました。

ゲーム環境
========

* イメージはASCII文字がシンボルで、画面の大きさが横40x縦24 のゲーム環境
* 入力は「上下左右」と「Aボタン」「Bボタン」。それぞれ独立として、2^6=64パターンの入力がある。
* ゲーム開発時用として、Terminalからのキー入力(ikjl + xz)でプレイできるデバッグモードがある。
	* でも、押しっぱなしにすると入力溜まってしまって辛い・・・ 同時押しできないから辛い・・・
* 学習の様子を見るために、TCP/IPで通信して、直前のプレイの様子を表示する クライント・サーバがある。

という感じです。簡単に言うと、学習中にサーバに接続すれば、以下の様なものがTerminal上で見れるというものです。

<img src="https://dl.dropboxusercontent.com/u/59329025/images/20150719_173407.gif">

AWS上などで実行しておいて、様子を見たい時にクライントを接続して眺めることができるので、暑い夏にPCをホッカホカにしたくない人向けです。
ちなみに `ttygif` などで上記のようなGif Animeも作れます(ttygifすごい！)。

お題: Jumpで穴を飛び越えろゲーム（自作）を強化学習できるか？
=========

上記のGif Animeみたいなゲームです（足し算ゲームよりは進化した・・・）。

ルール
-------

* 左右のキーで左右に動く
* AボタンでJumpする。高さ制限あり(3マスくらい)。Jump中でも左右に自由に動ける。
* P がプレイヤーで 穴に落ちたらゲームオーバー (報酬 -1)
* ターン毎に 報酬 +0.05 
* 自分の真下が穴の場合 報酬 +0.1

### 学習用に設けたルール

* 無効なキーを押したら 報酬 -0.1
	*  例： 上や下、 Bボタン。 左右を同時に押す、等。
	* 一応、何が有効なキーか、くらいは教えてあげてもいいかな、と（収束しないし・・・）。
	* こうしておくと、暫くしたらちゃんと押さなくなりますｗ
* e-Greedyで選択するActionは、上記無効なキーを含まないように調整
	* つまり、それは事前知識として知っている、ということになります（それくらいいいだろう・・・？）。

実験
======

学習モデル
--------

DQNっぽく、 入力が画面の表示データのみ(40*24のでchar codeが 32〜127 まで)。前３フレームと現在のフレームの４フレーム分を入力とします。

ネットワーク構造は何が良いのか全くわからないので、こんな感じになっています。

```
HISTORY_SIZE = 4
# Convolution2DのOutputサイズは (screen_size - ksize) / stride + 1 となる（たぶん）。
chainer_model = FunctionSet(
    l1=F.Convolution2D(HISTORY_SIZE, 3, ksize=1, stride=1),  # 履歴を3つのW*Hのチャネルにする
    l2=F.Linear(JumpGame.WIDTH*JumpGame.HEIGHT*3, 800),      # ので、ここのINは W*H*3になる
    l3=F.Linear(800, 500),
    l4=F.Linear(500, 300),
    l5=F.Linear(300, 64),
)
```

Ugo-Nama さんの
[DQNをPython + Chainerで書いたよ（コード公開しました）](http://qiita.com/Ugo-Nama/items/08c6a5f6a571335972d5#dqn%E3%82%92python--chainer%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%9F%E3%82%88%E3%82%B3%E3%83%BC%E3%83%89%E5%85%AC%E9%96%8B%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F)
も参考にさせてもらい、過去の４フレームを F.Convolution2D でまとめたところまでは良いんじゃないかなと思っていますが、その後3channelで良いのかとか、中間層の構成はこれで良いのかとか、まだ完全に手探りです。中間層ちょっと多いような気がするなぁ（ピクセルの処理みたいなのが一切無いわけだし・・）。

実装
----

[実験に使ったソースコードはこちら](https://github.com/mokemokechicken/PlayingChainer/blob/master/src/game_spike/jump_game.py)になります。

今回からは、サーバ機能などもありましたので、機能毎に分割して作りました。

今後ゲームを追加するときは、 `jump_game.py` の部分だけ作れば良いようになっている、、、つもりです。

ゲームオーバーになる度に、学習状態を保存しているので、途中でBreakしても再開すれば前のModelを自動的にLoadします。ただ、Modelの構造を変更するとLoadでエラーになるので、その時は手動で `~/.game/model/` の下にあるファイルを消すような仕様。。

過去のプレイ結果を取っておいて、バックグラウンドでバッチ学習する仕組み(Experience Replay?)もおいおい作っていきたいと思います。

結果
======

まだよい結果は出てないです。
単に時間がかかるのか、やり方が間違っているのか。。
非常に単純な足し算ゲームでも最適な行動を学習するまでには100万回以上（学習回数でいうと5000万回以上だった気もする・・）かかったしなぁ。。
前回のような厳密な最適行動は不要だとは思いますが、穴に落ちなくなるようになってくれないかなぁと思っています。

結果が出るまでじっと待つ必要があるというのは、何か本当に生物の実験をしているようですねｗ

追記:12時間後
-----------

<img src="https://dl.dropboxusercontent.com/u/59329025/images/20150720_071920.gif">

初期の頃とモデルを変えて１２時間後くらいには上記のようになっていました。
少し賢くなってますねー。

```
HISTORY_SIZE = 4
PATTERN_SIZE = 100

def calc_output_size(screen_size, ksize, stride):
    return (screen_size - ksize) / stride + 1

nw = calc_output_size(JumpGame.WIDTH, 8, 4)   # 9
nh = calc_output_size(JumpGame.HEIGHT, 8, 4)  # 5
chainer_model = FunctionSet(
    l1=F.Convolution2D(HISTORY_SIZE, PATTERN_SIZE, ksize=8, stride=4),
    l2=F.Linear(nw * nh * PATTERN_SIZE, 800),
    l3=F.Linear(800, 400),
    l4=F.Linear(400, 64),
)
```

`F.Convolution2D(4, 3, ksize=1, stride=1)`
から
`F.Convolution2D(4, 100, ksize=8, stride=4)`
にしています。


さいごに
======

次はもう少し違うゲームを作ってみようと思います。
Experience Replayはその後くらいにやってみようかな。

次： [Chainerで機械学習と戯れる: JumpゲームとTreasureゲームをChainerで強化学習した過程と結果のメモ](http://qiita.com/mokemokechicken/items/cc9b2c96f6e4a43c123c)