ChainerRLのサンプルを読んでみる(examples/ale/train_dqn_ale.py編)

ChainerRLは、ドキュメントが未整備な部分が多い。例えば、[公式ドキュメント](http://chainerrl.readthedocs.io/en/latest/index.html)を見ても、サンプルコードに乗っているAPIを全て説明しているわけではない感じです。（2017/12/30現在）

なので、サンプルを読んで勉強してみます。

# [examples/ale/train_dqn_ale.py](https://github.com/chainer/chainerrl/blob/015b7dbd5f903fd63e9ee92240d149ad7972d32d/examples/ale/train_dqn_ale.py)

ALE([The Arcade Learning Environment](https://github.com/mgbellemare/Arcade-Learning-Environment))を[DQN](http://neuro-educator.com/rl2/)で学習するプログラムです。

プログラムは短いが、引数の処理などが多いので、学習時に本質的な部分だけを抽出すると以下のような感じになります。

※ 以下、実行できるかは未確認です

```train_dqn_ale_simple.py
def parse_arch(arch, n_actions, activation):
    if arch == 'nature':
        return links.Sequence(
            links.NatureDQNHead(activation=activation),
            L.Linear(512, n_actions),
            DiscreteActionValue)
    elif arch == 'nips':
        return links.Sequence(
            links.NIPSDQNHead(activation=activation),
            L.Linear(256, n_actions),
            DiscreteActionValue)
    elif arch == 'dueling':
        return DuelingDQN(n_actions)
    else:
        raise RuntimeError('Not supported architecture: {}'.format(arch))


def parse_agent(agent):
    return {'DQN': agents.DQN,
            'DoubleDQN': agents.DoubleDQN,
            'PAL': agents.PAL}[agent]


def dqn_phi(screens):
    """Phi (feature extractor) of DQN for ALE

    Args:
      screens: List of N screen objects. Each screen object must be
      numpy.ndarray whose dtype is numpy.uint8.
    Returns:
      numpy.ndarray
    """
    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


def main(args):
    # In training, life loss is considered as terminal states
    env = ale.ALE(args.rom, use_sdl=args.use_sdl)
    misc.env_modifiers.make_reward_clipped(env, -1, 1)
    # In testing, an episode is terminated  when all lives are lost
    eval_env = ale.ALE(args.rom, use_sdl=args.use_sdl,
                       treat_life_lost_as_terminal=False)

    n_actions = env.number_of_actions
    activation = F.relu
    q_func = parse_arch(args.arch, n_actions, F.relu)

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

    opt.setup(q_func)

    rbuf = replay_buffer.ReplayBuffer(10 ** 6)

    explorer = explorers.LinearDecayEpsilonGreedy(
        1.0, 0.1,
        args.final_exploration_frames,
        lambda: np.random.randint(n_actions))
    Agent = parse_agent(args.agent)
    agent = Agent(q_func, opt, rbuf, gpu=args.gpu, gamma=0.99,
                  explorer=explorer, replay_start_size=args.replay_start_size,
                  target_update_interval=args.target_update_interval,
                  clip_delta=args.clip_delta,
                  update_interval=args.update_interval,
                  batch_accumulator='sum', phi=dqn_phi)

    # In testing DQN, randomly select 5% of actions
    eval_explorer = explorers.ConstantEpsilonGreedy(
        5e-2, lambda: np.random.randint(n_actions))
    experiments.train_agent_with_evaluation(
        agent=agent, env=env, steps=args.steps,
        eval_n_runs=args.eval_n_runs, eval_interval=args.eval_interval,
        outdir=args.outdir, eval_explorer=eval_explorer,
        eval_env=eval_env)
```

main()を上から見ていきます。（簡単に意味の分かりそうな所はスキップしています）

## chainerrl.envs.ale.ALE

```train_dqn_ale_simple.py
    # In training, life loss is considered as terminal states
    env = ale.ALE(args.rom, use_sdl=args.use_sdl)
```
ALEの環境を作っています。args.romは、ゲームのロムの文字列です。'Breakout'等を指定します。

## chainerrl.misc.env_modifiers.make_reward_clipped

```train_dqn_ale_simple.py
    misc.env_modifiers.make_reward_clipped(env, -1, 1)
```

`chainerrl.misc.env_modifiers.make_reward_clipped` というAPIです。ドキュメントは見当たりませんでしたが、その名の通り、Rewardを$[-1, 1]$の範囲でクリップしています。

## chainerrl.links

```train_dqn_ale_simple.py
    q_func = parse_arch(args.arch, n_actions, F.relu)
```

```train_dqn_ale_simple.py
def parse_arch(arch, n_actions, activation):
    if arch == 'nature':
        return links.Sequence(
            links.NatureDQNHead(activation=activation),
            L.Linear(512, n_actions),
            DiscreteActionValue)
    elif arch == 'nips':
        return links.Sequence(
            links.NIPSDQNHead(activation=activation),
            L.Linear(256, n_actions),
            DiscreteActionValue)
    elif arch == 'dueling':
        return DuelingDQN(n_actions)
    else:
        raise RuntimeError('Not supported architecture: {}'.format(arch))
```

まず、'nature'が何か？という話ですが、科学雑誌Natureに掲載されたモデル、という事のようです。[こちらに論文へのリンクがあります。](https://deepmind.com/research/dqn/)
`links.Sequence`は、いかにもChainerのAPIのような見た目ですが、`chainerrl.links.sequence`でChainerRLのAPIです。やりたいことは、[chainer.ChainList](https://docs.chainer.org/en/stable/reference/core/generated/chainer.ChainList.html?highlight=ChainList)と似ている気がしますが、さらに抽象化している感じです。
`chainerr.links.NatureDQNHead`は、[Natureに掲載されたDQNの論文](](https://deepmind.com/research/dqn/))(後述の通りこのリンク先が正しいか不明)の最後のFCレイヤの前までのレイヤという意味らしいです。中身は以下の通り、Convolutionを3段と、FCレイヤ1段です。
ちょっと調べてみたのですが、このNatureのモデルというのはどこに描いているんでしょう。。[ここに掲載されている論文](https://deepmind.com/research/dqn/)では、Convolutionは2段のようでして。。Nature

```dqn_head.py
class NatureDQNHead(chainer.ChainList):
    """DQN's head (Nature version)"""

    def __init__(self, n_input_channels=4, n_output_channels=512,
                 activation=F.relu, bias=0.1):
        self.n_input_channels = n_input_channels
        self.activation = activation
        self.n_output_channels = n_output_channels

        layers = [
            L.Convolution2D(n_input_channels, 32, 8, stride=4,
                            initial_bias=bias),
            L.Convolution2D(32, 64, 4, stride=2, initial_bias=bias),
            L.Convolution2D(64, 64, 3, stride=1, initial_bias=bias),
            L.Linear(3136, n_output_channels, initial_bias=bias),
        ]

        super(NatureDQNHead, self).__init__(*layers)

    def __call__(self, state):
        h = state
        for layer in self:
            h = self.activation(layer(h))
        return h
```

次の、`chainerr.links.NIPSDQNHead`というのは、NIPS Deep Learning Workshop 2013で発表されたもので、[こちらの論文です。](https://arxiv.org/abs/1312.5602)
モデルを見ると、Convolutionが2段になっていますね。

```dqn_head.py
class NIPSDQNHead(chainer.ChainList):
    """DQN's head (NIPS workshop version)"""

    def __init__(self, n_input_channels=4, n_output_channels=256,
                 activation=F.relu, bias=0.1):
        self.n_input_channels = n_input_channels
        self.activation = activation
        self.n_output_channels = n_output_channels

        layers = [
            L.Convolution2D(n_input_channels, 16, 8, stride=4,
                            initial_bias=bias),
            L.Convolution2D(16, 32, 4, stride=2, initial_bias=bias),
            L.Linear(2592, n_output_channels, initial_bias=bias),
        ]

        super(NIPSDQNHead, self).__init__(*layers)

    def __call__(self, state):
        h = state
        for layer in self:
            h = self.activation(layer(h))
        return h
```

モデルの最後の`chainerrl.q_functions.DuelingDQN`は、[こちらの論文の](https://arxiv.org/abs/1511.06581)モデルです。引数で、出力チャンネル数を指定できます。(なぜ、NatureやNIPSのモデルをそうしなかったんだろう。。)

```train_dqn_ale_simple.py
    rbuf = replay_buffer.ReplayBuffer(10 ** 6)
```

## chainerrl.replay_buffer

次の、`chainerrl.replay_buffer`は、いわゆる、[Experience Replayay](https://qiita.com/icoxfog417/items/242439ecd1a477ece312#experience-replay) のAPIです。引数でバッファサイズを指定しています。

```train_dqn_ale_simple.py
    explorer = explorers.LinearDecayEpsilonGreedy(
        1.0, 0.1,
        args.final_exploration_frames,
        lambda: np.random.randint(n_actions))
```

## chainerrl.explorers.LinearDecayEpsilionGreed

`chainerrl.explorers`というのは、探索を納めたパッケージのようです。
`chainerrl.explorers.LinearDecayEpsilionGreed`について、Webのドキュメントには記述がありませんでしたが、コード中にはありました。

```epsilon_greedy.py
"""
Epsilon-greedy with linearyly decayed epsilon

    Args:
      start_epsilon: max value of epsilon
      end_epsilon: min value of epsilon
      decay_steps: how many steps it takes for epsilon to decay
      random_action_func: function with no argument that returns action
      logger: logger used
"""
```

Epsilon-Greedy法というのは、確率$\epsilon$で探索、確率$1-\epsilon$でQ値が最大のものを選ぶという手法。[参考](http://nekopuni.holy.jp/2014/08/pythonepsilon-greedy%E6%B3%95/)
この`LinearDecayEpsilionGreed`クラスでは、Epsilonを線形に減少させるようです。
`start_epsilon=1.0`ですので、最初は完全にランダムで、10%まで減少させています。

## chainer.agents

```train_dqn_ale_simple.py
    Agent = parse_agent(args.agent)
    agent = Agent(q_func, opt, rbuf, gpu=args.gpu, gamma=0.99,
                  explorer=explorer, replay_start_size=args.replay_start_size,
                  target_update_interval=args.target_update_interval,
                  clip_delta=args.clip_delta,
                  update_interval=args.update_interval,
                  batch_accumulator='sum', phi=dqn_phi)
```

```train_dqn_ale_simple.py
def parse_agent(agent):
    return {'DQN': agents.DQN,
            'DoubleDQN': agents.DoubleDQN,
            'PAL': agents.PAL}[agent]
```

`chainer.agents` 以下のクラスを生成しています。これは、ドキュメントがあります。

http://chainerrl.readthedocs.io/en/latest/agents.html

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

## chainerrl.explorers.ConstantEpsilonGreedy

`chainerrl.explorers.ConstantEpsilonGreedy`は、先ほどの`chainerrl.explorers.LinearDecayEpsilonGreedy`と違って、Epsilonが減じないものです。
これは、評価に使うものです。5%だけランダムで他は、Q値が最大のものを使って評価しています。
こちらもソース中にドキュメントがありました。

```epsilon_greedy.py
    """Epsilon-greedy with constant epsilon.

    Args:
      epsilon: epsilon used
      random_action_func: function with no argument that returns action
      logger: logger used
    """
```

## chainer.experiments.train_agent_with_evaluation

```train_dqn_ale_simple.py
    experiments.train_agent_with_evaluation(
        agent=agent, env=env, steps=args.steps,
        eval_n_runs=args.eval_n_runs, eval_interval=args.eval_interval,
        outdir=args.outdir, eval_explorer=eval_explorer,
        eval_env=eval_env)
```

最後、`chainer.experiments.train_agent_with_evaluation`で学習と評価をします。
これは、Webにドキュメントがあります。

http://chainerrl.readthedocs.io/en/latest/experiments.html#training-and-evaluation