ChainerRLでブロック崩しを学習する

CSGAdventCalendar最終日です。
ChainerRLを使ってブロック崩しの学習をさせるチュートリアルをやりました。
実装はGoogleColaboratoryを使いました。

![ylt2p-aihop.gif](https://qiita-image-store.s3.amazonaws.com/0/261202/a3d1ea80-2ffb-6cfa-612c-71edd3234f2f.gif)


# ChainerRLとは
Chainerを使って実装していた深層強化学習アルゴリズムを”ChainerRL”というライブラリとしてまとめて公開したもの。
以下のような最近の深層強化学習アルゴリズムを共通のインタフェースで使えるよう実装している。

- Deep Q-Network (Mnih et al., 2015)
- Double DQN (Hasselt et al., 2016)
- Normalized Advantage Function (Gu et al., 2016)
- (Persistent) Advantage Learning (Bellemare et al., 2016)
- Deep Deterministic Policy Gradient (Lillicrap et al., 2016)
- SVG(0) (Heese et al., 2015)
- Asynchronous Advantage Actor-Critic (Mnih et al., 2016)
- Asynchronous N-step Q-learning (Mnih et al., 2016)
- Actor-Critic with Experience Replay (Wang et al., 2017)
- etc.

# 準備
### ChainerRLのインストール
```sh
!apt-get -qq -y update
# Install Chainer and CuPy!
!apt-get -qq -y install libcusparse8.0 libnvrtc8.0 libnvtoolsext1 > /dev/null
!ln -snf /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so.8.0 /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so
!pip install cupy-cuda80 chainer
# Install ChainerRL and OpenAI Gym
!apt-get -qq -y install xvfb freeglut3-dev ffmpeg cmake swig zlib1g-dev> /dev/null
!pip -q install chainerrl atari-py gym 'gym[atari]' 'gym[box2d]'  pyglet pyopengl pyvirtualdisplay
```
### Google driveとの接続
atari のゲームを DQNで学習させると、とても時間がかかります。そのため、Google drive に経過を保存できるようする。
次のコードセルを実行し、以下の手順で Google アカウントの認証を行います。

- URLが表示されるのでそれをクリック
- Google アカウントにログイン
- 表示されるトークンをコピー
- このノートに戻って、テキストボックスにそのトークンを貼り付け
- 再度URLが表示されるのでそれをクリック
- このノートに戻って、テキストボックスにそのトークンを貼り付け

```sh
!apt-get install -y -qq software-properties-common python-software-properties module-init-tools > /dev/null
!add-apt-repository -y ppa:alessandro-strada/ppa 2>&1 > /dev/null
!apt-get update -qq 2>&1 > /dev/null
!apt-get -y install -qq google-drive-ocamlfuse fuse > /dev/null

from google.colab import auth
auth.authenticate_user()

# Generate creds for the Drive FUSE library.
from oauth2client.client import GoogleCredentials
creds = GoogleCredentials.get_application_default()
import getpass
!google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret} < /dev/null 2>&1 | grep URL
vcode = getpass.getpass()
!echo {vcode} | google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret}

!mkdir -p drive
!google-drive-ocamlfuse drive
```

### 環境の準備
```python
import chainer
from chainer import functions as F
from chainer import links as L
import chainerrl
from chainerrl.envs import ale
import numpy as np
```

chainerrl.experiments.prepare_output_dir ではログ出力のディレクトリを設定します。
ale.ALEで環境を作ります。学習用の環境と、バリデーション用の環境を作ります。
また、学習用の環境は、報酬を -1〜1の範囲にクリップするため、chainerrl.misc.env_modifiers.make_reward_clippedを呼び出します。

```python
outdir = chainerrl.experiments.prepare_output_dir(None, "drive/dqn_out")

ROM = "breakout"
TRAIN_SEED = 0
TEST_SEED = 2 ** 16 - 1 - TRAIN_SEED

env = ale.ALE(ROM, use_sdl=False, seed=TRAIN_SEED)
chainerrl.misc.env_modifiers.make_reward_clipped(env, -1, 1)
eval_env = ale.ALE(ROM, use_sdl=False,
                       treat_life_lost_as_terminal=False,
                       seed=TEST_SEED)

n_actions = env.number_of_actions

q_func = chainerrl.links.Sequence(
            chainerrl.links.NatureDQNHead(),
            L.Linear(512, n_actions),
            chainerrl.action_value.DiscreteActionValue)

# Use the same hyper parameters as the Nature paper's
optimizer = chainer.optimizers.RMSpropGraves(lr=2.5e-4, alpha=0.95, momentum=0.0, eps=1e-2)

optimizer.setup(q_func)

rbuf = chainerrl.replay_buffer.ReplayBuffer(10 ** 6)

explorer = chainerrl.explorers.LinearDecayEpsilonGreedy(
        1.0, 0.1,
        10 ** 6,
        lambda: np.random.randint(n_actions))

# In testing DQN, randomly select 5% of actions
eval_explorer = chainerrl.explorers.ConstantEpsilonGreedy(5e-2, lambda: np.random.randint(n_actions))

def dqn_phi(screens):
    assert len(screens) == 4
    assert screens[0].dtype == np.uint8
    raw_values = np.asarray(screens, dtype=np.float32)
    # [0,255] -> [0, 1]
    raw_values /= 255.0
    return raw_values

agent = chainerrl.agents.DQN(q_func, optimizer, rbuf, gpu=0, gamma=0.99,
                explorer=explorer, replay_start_size=5 * 10 ** 4,
                target_update_interval=10 ** 4,
                clip_delta=True,
                update_interval=4,
                batch_accumulator='sum', phi=dqn_phi)
```

# 学習
```python
import sys
STEPS = 10 ** 7
def step_hook(env, agent, step):
  sys.stdout.write("\r{} / {} steps.".format(step, STEPS))
  sys.stdout.flush()
    
chainerrl.experiments.train_agent_with_evaluation(
    agent=agent, env=env, steps=STEPS,
    eval_n_runs=10, eval_interval=10 ** 5,
    outdir=outdir, eval_explorer=eval_explorer,
    eval_env=eval_env, step_hooks=[step_hook])
```

# モデルのリロード
```python
import pandas as pd
import glob
import os
model_files = glob.glob("drive/dqn_out/*/*/model.npz")
model_files.sort(key=os.path.getmtime)
last_model_dir = os.path.dirname(model_files[-1])
last_model_dir

import chainer
from chainer import functions as F
from chainer import links as L
import chainerrl
from chainerrl.envs import ale
import numpy as np

ROM = "breakout"
TRAIN_SEED = 0
TEST_SEED = 2 ** 16 - 1 - TRAIN_SEED

env = ale.ALE(ROM, use_sdl=False, seed=TRAIN_SEED)
chainerrl.misc.env_modifiers.make_reward_clipped(env, -1, 1)
eval_env = ale.ALE(ROM, use_sdl=False,
                       treat_life_lost_as_terminal=False,
                       seed=TEST_SEED)

n_actions = env.number_of_actions
q_func = chainerrl.links.Sequence(
            chainerrl.links.NatureDQNHead(),
            L.Linear(512, n_actions),
            chainerrl.action_value.DiscreteActionValue)

# Use the same hyper parameters as the Nature paper's
optimizer = chainer.optimizers.RMSpropGraves(lr=2.5e-4, alpha=0.95, momentum=0.0, eps=1e-2)

optimizer.setup(q_func)

rbuf = chainerrl.replay_buffer.ReplayBuffer(10 ** 6)

explorer = chainerrl.explorers.LinearDecayEpsilonGreedy(
        1.0, 0.1,
        10 ** 6,
        lambda: np.random.randint(n_actions))

def dqn_phi(screens):
    assert len(screens) == 4
    assert screens[0].dtype == np.uint8
    raw_values = np.asarray(screens, dtype=np.float32)
    # [0,255] -> [0, 1]
    raw_values /= 255.0
    return raw_values

agent = chainerrl.agents.DQN(q_func, optimizer, rbuf, gpu=0, gamma=0.99,
                explorer=explorer, replay_start_size=5 * 10 ** 4,
                target_update_interval=10 ** 4,
                clip_delta=True,
                update_interval=4,
                batch_accumulator='sum', phi=dqn_phi)
agent.load(last_model_dir)
```

# 学習結果の確認
```python
import pandas as pd
import glob
import os
score_files = glob.glob("drive/dqn_out/*/scores.txt")
score_files.sort(key=os.path.getmtime)
score_file = score_files[-1]
df = pd.read_csv(score_file, delimiter='\t' )
df
```
結果が下記です。
![スクリーンショット 2018-09-05 23.38.41.png](https://qiita-image-store.s3.amazonaws.com/0/261202/d78a4c82-065f-be61-3ae9-3bca748ca60a.png)

グラフ化します。

```python
df[["max", "median", "mean", "stdev", "min"]].plot()
```
![スクリーンショット 2018-09-05 23.40.54.png](https://qiita-image-store.s3.amazonaws.com/0/261202/0304f90b-2f91-de7b-5160-61196a3e2d89.png)

# 実行結果の確認
```python
frames = []
for i in range(10):
    obs = env.reset()
    done = False
    R = 0
    t = 0
    while not done:
        action = agent.act(obs)
        obs, r, done, _ = env.step(action)
        frames.append(env.ale.getScreenRGB())
        R += r
        t += 1
    print('test episode:', i, 'R:', R)
    agent.stop_episode()
```
アニメーションの作成
最初のやつです。

```python
import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
from IPython.display import HTML

fig = plt.figure(figsize=(5, 5))
plt.axis('off')

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig, images, interval=30, repeat_delay=1)

HTML(ani.to_jshtml())
```
![ylt2p-aihop.gif](https://qiita-image-store.s3.amazonaws.com/0/261202/333e0514-a836-27aa-f47b-f66cf5972ccf.gif)

# 終わりに
チュートリアルそのまま行うだけで簡単にDQNで学習とわかりやすい形で結果の可視化ができました。
色々いじって遊びたいと思います。