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

[ChainerRLのサンプルを読んでみる(examples/ale/train_dqn_ale.py編)](https://qiita.com/ikeyasu/items/56040e913b48b0cb1e24)の続きです。
（2017/12/31現在、ChainerRLのAPIドキュメントが簡素なので、解説してみる記事です。）

今度は、OpenAI Gym の方のサンプルを見てみます。

https://github.com/chainer/chainerrl/blob/fcf16f9e93fa3f3b6e020fe7d1bee328de0322ea/examples/gym/train_dqn_gym.py

# examples/gym/train_dqn_gym.py

例によって、Trainingの部分だけを切り出してみました。が、そんなに短くなっていないです。

```train_dqn_gym_simple.py
def main(args):
    args.outdir = experiments.prepare_output_dir(
        args, args.outdir, argv=sys.argv)
    print('Output files are saved in {}'.format(args.outdir))

    def clip_action_filter(a):
        return np.clip(a, action_space.low, action_space.high)

    def make_env(for_eval):
        env = gym.make(args.env)
        if args.monitor:
            env = gym.wrappers.Monitor(env, args.outdir)
        if isinstance(env.action_space, spaces.Box):
            misc.env_modifiers.make_action_filtered(env, clip_action_filter)
        if not for_eval:
            misc.env_modifiers.make_reward_filtered(
                env, lambda x: x * args.reward_scale_factor)
        if ((args.render_eval and for_eval) or
                (args.render_train and not for_eval)):
            misc.env_modifiers.make_rendered(env)
        return env

    env = make_env(for_eval=False)
    timestep_limit = env.spec.tags.get(
        'wrapper_config.TimeLimit.max_episode_steps')
    obs_space = env.observation_space
    obs_size = obs_space.low.size
    action_space = env.action_space

    if isinstance(action_space, spaces.Box):
        action_size = action_space.low.size
        # Use NAF to apply DQN to continuous action spaces
        q_func = q_functions.FCQuadraticStateQFunction(
            obs_size, action_size,
            n_hidden_channels=args.n_hidden_channels,
            n_hidden_layers=args.n_hidden_layers,
            action_space=action_space)
        # Use the Ornstein-Uhlenbeck process for exploration
        ou_sigma = (action_space.high - action_space.low) * 0.2
        explorer = explorers.AdditiveOU(sigma=ou_sigma)
    else:
        n_actions = action_space.n
        q_func = q_functions.FCStateQFunctionWithDiscreteAction(
            obs_size, n_actions,
            n_hidden_channels=args.n_hidden_channels,
            n_hidden_layers=args.n_hidden_layers)
        # Use epsilon-greedy for exploration
        explorer = explorers.LinearDecayEpsilonGreedy(
            args.start_epsilon, args.end_epsilon, args.final_exploration_steps,
            action_space.sample)

    # Draw the computational graph and save it in the output directory.
    chainerrl.misc.draw_computational_graph(
        [q_func(np.zeros_like(obs_space.low, dtype=np.float32)[None])],
        os.path.join(args.outdir, 'model'))

    opt = optimizers.Adam()
    opt.setup(q_func)

    rbuf_capacity = 5 * 10 ** 5
    if args.episodic_replay:
        if args.minibatch_size is None:
            args.minibatch_size = 4
        if args.prioritized_replay:
            betasteps = (args.steps - args.replay_start_size) \
                // args.update_interval
            rbuf = replay_buffer.PrioritizedEpisodicReplayBuffer(
                rbuf_capacity, betasteps=betasteps)
        else:
            rbuf = replay_buffer.EpisodicReplayBuffer(rbuf_capacity)
    else:
        if args.minibatch_size is None:
            args.minibatch_size = 32
        if args.prioritized_replay:
            betasteps = (args.steps - args.replay_start_size) \
                // args.update_interval
            rbuf = replay_buffer.PrioritizedReplayBuffer(
                rbuf_capacity, betasteps=betasteps)
        else:
            rbuf = replay_buffer.ReplayBuffer(rbuf_capacity)

    def phi(obs):
        return obs.astype(np.float32)

    agent = DQN(q_func, opt, rbuf, gpu=args.gpu, gamma=args.gamma,
                explorer=explorer, replay_start_size=args.replay_start_size,
                target_update_interval=args.target_update_interval,
                update_interval=args.update_interval,
                phi=phi, minibatch_size=args.minibatch_size,
                target_update_method=args.target_update_method,
                soft_update_tau=args.soft_update_tau,
                episodic_update=args.episodic_replay, episodic_update_len=16)

    if args.load:
        agent.load(args.load)

    eval_env = make_env(for_eval=True)

    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_env=eval_env,
        max_episode_len=timestep_limit)

```

上から見ていきます

## chainerrl.experiments.prepare_output_dir

```train_dqn_gym_simple.py
    args.outdir = experiments.prepare_output_dir(
        args, args.outdir, argv=sys.argv)
    print('Output files are saved in {}'.format(args.outdir))
```

`chainerrl.experiments.prepare_output_dir`はログ等の出力用のディレクトリをつくるための関数です。[公式ドキュメント](http://chainerrl.readthedocs.io/en/latest/)にはありませんが、ソース中にドキュメントがありました。
（この出力が、Chainerの"result/log"と同じなら、ChainerUIが使えるんですけどね。。）

```prepare_output_dir.py
    """Prepare a directory for outputting training results.

    An output directory, which ends with the current datetime string,
    is created. Then the following information is saved into the directory:

        args.txt: command line arguments
        command.txt: command itself
        environ.txt: environmental variables

    Additionally, if the current directory is under git control, the following
    information is saved:

        git-head.txt: result of `git rev-parse HEAD`
        git-status.txt: result of `git status`
        git-log.txt: result of `git log`
        git-diff.txt: result of `git diff`

    Args:
        args (dict or argparse.Namespace): Arguments to save
        user_specified_dir (str or None): If str is specified, the output
            directory is created under that path. If not specified, it is
            created as a new temporary directory instead.
        argv (list or None): The list of command line arguments passed to a
            script. If not specified, sys.argv is used instead.
        time_format (str): Format used to represent the current datetime. The
        default format is the basic format of ISO 8601.
    Returns:
        Path of the output directory created by this function (str).
    """
```

## chianerrl.misc.env_modifiers.make_action_filtered

```train_dqn_gym_simple.py
    def make_env(for_eval):
        env = gym.make(args.env)
        if args.monitor:
            env = gym.wrappers.Monitor(env, args.outdir)
        if isinstance(env.action_space, spaces.Box):
            misc.env_modifiers.make_action_filtered(env, clip_action_filter)
        if not for_eval:
            misc.env_modifiers.make_reward_filtered(
                env, lambda x: x * args.reward_scale_factor)
        if ((args.render_eval and for_eval) or
                (args.render_train and not for_eval)):
            misc.env_modifiers.make_rendered(env)
        return env

    env = make_env(for_eval=False)
```

`gym`は、[OpenAI Gym](https://github.com/openai/gym)のパッケージで、ChainerRLのAPIではありません。`gym.make`については[こちらを](https://github.com/openai/gym#environments)参照してください。`gym.wrappers.Monitor`についてはドキュメントが無いのですが、ログのようなもののようです。[この辺り](https://qiita.com/Matchlab/items/18569ad3713d1c658da8)を見れば分かると思います

`chianerrl.misc.env_modifiers.make_action_filtered`は、その名の通りActionをフィルターするもので、入力されたアクションに対して、アクションフィルターの関数（上記コードの場合Lambda）を適用します。

## chianerrl.misc.env_modifiers.make_rendered

先ほどのコードにもう一つ、`chainerrl.misc.env_modifiers.make_rendered`というAPIがありました。
これは以下のような実装です。

```env_modifiers.py
def make_rendered(env, *render_args, **render_kwargs):
    base_step = env.step
    base_close = env.close

    def step(action):
        ret = base_step(action)
        env.render(*render_args, **render_kwargs)
        return ret

    def close():
        env.render(*render_args, close=True, **render_kwargs)
        base_close()

    env.step = step
    env.close = close
```

Open AI Gym の `step`と`close`を書き換えているようです。`step`については、[ここ](https://github.com/openai/gym#basics)に記述があります。
ようは、`step`が呼ばれる度に`render` をしよう、`close`が呼ばれたら一度`render`してウインドウをCloseしよう、という書き換えをしているようです。

## OpenAI Gym 関連

以下は、ChainerRLではなく、OpenAI GymのAPI達です。

```train_dqn_gym_simple.py
    timestep_limit = env.spec.tags.get(
        'wrapper_config.TimeLimit.max_episode_steps')
    obs_space = env.observation_space
    obs_size = obs_space.low.size
    action_space = env.action_space
```

`env.spec.tags.get`は、envの値を取るAPIの用です。タグのリスト等は見つからないのですが。。`'wrapper_config.TimeLimit.max_episode_steps'`で、1エピソード中のステップ数の最大値を取得しています。
`env.observation_space`は、環境を観測するときのオブジェクトを返し、`env.action_space`は、行動のオブジェクトを返します。これらは、`Box(4,)`か`Discrete(2)`が返ってきます。[こちらの'Spaces'に詳しくかかれてます](https://gym.openai.com/docs/)。

## chainerrl.q_functions

Open AI Gymの方もドキュメントの記述はあまりないのですが、、`Box()`はN次元の空間を持ち、`Discrete()`は1次元のようです。
次のコードでは、どちらのタイプかによって、Q関数の構造を変えています。

```train_dqn_gym_simple.py
    if isinstance(action_space, spaces.Box):
        action_size = action_space.low.size
        # Use NAF to apply DQN to continuous action spaces
        q_func = q_functions.FCQuadraticStateQFunction(
            obs_size, action_size,
            n_hidden_channels=args.n_hidden_channels,
            n_hidden_layers=args.n_hidden_layers,
            action_space=action_space)
        # Use the Ornstein-Uhlenbeck process for exploration
        ou_sigma = (action_space.high - action_space.low) * 0.2
        explorer = explorers.AdditiveOU(sigma=ou_sigma)
    else:
        n_actions = action_space.n
        q_func = q_functions.FCStateQFunctionWithDiscreteAction(
            obs_size, n_actions,
            n_hidden_channels=args.n_hidden_channels,
            n_hidden_layers=args.n_hidden_layers)
        # Use epsilon-greedy for exploration
        explorer = explorers.LinearDecayEpsilonGreedy(
            args.start_epsilon, args.end_epsilon, args.final_exploration_steps,
            action_space.sample)
```

`chainerrl.q_functions.FCQuadraticStateQFunction`は、コード中にドキュメントがありました。
その名の通り、Fully-connected で、2次元(Quadratic)のStateを持つ、Q関数のようです。

```chainerrl/q_functions/state_q_functions.py
    """Fully-connected state-input continuous Q-function.

    Args:
        n_input_channels: number of input channels
        n_dim_action: number of dimensions of action space
        n_hidden_channels: number of hidden channels
        n_hidden_layers: number of hidden layers
        action_space: action_space
        scale_mu (bool): scale mu by applying tanh if True
    """
```

もう一方の`chainerrl.q_functions.FCStateQFunctionWithDiscreteAction`もその名の通り、Discrete Action（離散的な行動）のためのQ関数のようです。

```
    """Fully-connected state-input Q-function with discrete actions.

    Args:
        n_dim_obs: number of dimensions of observation space
        n_dim_action: number of dimensions of action space
        n_hidden_channels: number of hidden channels
        n_hidden_layers: number of hidden layers
    """
```

## chainerrl.explorers

上のコードには、`explorers.AdditiveOU`と`explorers.LinearDecayEpsilonGreedy`も使われていました。

`action_space` が、`Box` の場合に使われている`explorers.AdditiveOU`のコード中のドキュメントは以下の通りです。

```chainerrl/explorers/additive_ou.py
    """Additive Ornstein-Uhlenbeck process.

    Used in https://arxiv.org/abs/1509.02971 for exploration.

    Args:
        mu (float): Mean of the OU process
        theta (float): Friction to pull towards the mean
        sigma (float or ndarray): Scale of noise
        start_with_mu (bool): Start the process without noise
    """
```

https://arxiv.org/abs/1509.02971 を見てみると、"Continuous control with deep reinforcement learning" という論文である事が分かります。
よく分かっていないのですが、`action_space`が`Box`なので、行動空間がN次元で連続である、という事でいいんでしょうか。ちなみに、"Ornstein-Uhlenbeck process"は、[平均回帰過程](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%AB%E3%83%B3%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%A4%E3%83%B3%EF%BC%9D%E3%82%A6%E3%83%BC%E3%83%AC%E3%83%B3%E3%83%99%E3%83%83%E3%82%AF%E9%81%8E%E7%A8%8B)です。

一方、`explorers.LinearDecayEpsilonGreedy`は、$\epsilon$を低減させる、普通のEpsilon-greedyです。[ChainerRLのサンプルを読んでみる(examples/ale/train_dqn_ale.py編)](https://qiita.com/ikeyasu/items/56040e913b48b0cb1e24)にも出てきました。

```chainerrl/explorers/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
    """
```

# chainerrl.misc.draw_computational_graph

次にある`chainerrl.misc.draw_computational_graph`はq関数のモデルをPNGファイルにして保存しています。

```train_dqn_gym_simple.py
    # Draw the computational graph and save it in the output directory.
    chainerrl.misc.draw_computational_graph(
        [q_func(np.zeros_like(obs_space.low, dtype=np.float32)[None])],
        os.path.join(args.outdir, 'model'))
```

# chainerrl.replay_buffer

次は、Replay bufferの生成をしています。
大きく分けて、Episodic Replayかどうか、と、Prioritizedかどうかで、以下の4つのAPIがあります。

* `chainerrl.replay_buffer.PrioritizedEpisodicReplayBuffer`
* `chainerrl.replay_buffer.EpisodicReplayBuffer`
* `chainerrl.replay_buffer.PrioritizedReplayBuffer`
* `chainerrl.replay_buffer.ReplayBuffer`

```train_dqn_gym_simple.py
   if args.episodic_replay:
        if args.minibatch_size is None:
            args.minibatch_size = 4
        if args.prioritized_replay:
            betasteps = (args.steps - args.replay_start_size) \
                // args.update_interval
            rbuf = replay_buffer.PrioritizedEpisodicReplayBuffer(
                rbuf_capacity, betasteps=betasteps)
        else:
            rbuf = replay_buffer.EpisodicReplayBuffer(rbuf_capacity)
    else:
        if args.minibatch_size is None:
            args.minibatch_size = 32
        if args.prioritized_replay:
            betasteps = (args.steps - args.replay_start_size) \
                // args.update_interval
            rbuf = replay_buffer.PrioritizedReplayBuffer(
                rbuf_capacity, betasteps=betasteps)
        else:
            rbuf = replay_buffer.ReplayBuffer(rbuf_capacity)
```

PrioritizedReplayBufferは、以下の論文のようです。

Prioritized Experience Replay
https://arxiv.org/abs/1511.05952

エピソード記憶については、どの論文、という事ではないようですが、エピソード毎にReplayBufferを保存している手法のようです。

# chainerrl.agents.DQN

DQNのAgentです。こちらにはドキュメントがあります。

http://chainerrl.readthedocs.io/en/latest/agents.html?highlight=DQN

```train_dqn_gym_simple.py
    agent = DQN(q_func, opt, rbuf, gpu=args.gpu, gamma=args.gamma,
                explorer=explorer, replay_start_size=args.replay_start_size,
                target_update_interval=args.target_update_interval,
                update_interval=args.update_interval,
                phi=phi, minibatch_size=args.minibatch_size,
                target_update_method=args.target_update_method,
                soft_update_tau=args.soft_update_tau,
                episodic_update=args.episodic_replay, episodic_update_len=16)
```

# chainerrl.experiments.train_agent_with_evaluation

最後、chainer.experiments.train_agent_with_evaluationで学習と評価をします。
これは、Webにドキュメントがあります。
http://chainerrl.readthedocs.io/en/latest/experiments.html#training-and-evaluation

[ChainerRLのサンプルを読んでみる(examples/ale/train_dqn_ale.py編)](https://qiita.com/ikeyasu/items/56040e913b48b0cb1e24)にも出てきました。


```train_dqn_gym_simple.py
    eval_env = make_env(for_eval=True)

    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_env=eval_env,
        max_episode_len=timestep_limit)
```