LoginSignup
3
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-30

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

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

examples/ale/train_dqn_ale.py

ALE(The Arcade Learning Environment)をDQNで学習するプログラムです。

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

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

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に掲載されたモデル、という事のようです。こちらに論文へのリンクがあります。
links.Sequenceは、いかにもChainerのAPIのような見た目ですが、chainerrl.links.sequenceでChainerRLのAPIです。やりたいことは、chainer.ChainListと似ている気がしますが、さらに抽象化している感じです。
chainerr.links.NatureDQNHeadは、Natureに掲載されたDQNの論文(後述の通りこのリンク先が正しいか不明)の最後のFCレイヤの前までのレイヤという意味らしいです。中身は以下の通り、Convolutionを3段と、FCレイヤ1段です。
ちょっと調べてみたのですが、このNatureのモデルというのはどこに描いているんでしょう。。ここに掲載されている論文では、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で発表されたもので、こちらの論文です。
モデルを見ると、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は、こちらの論文のモデルです。引数で、出力チャンネル数を指定できます。(なぜ、NatureやNIPSのモデルをそうしなかったんだろう。。)

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

chainerrl.replay_buffer

次の、chainerrl.replay_bufferは、いわゆる、Experience Replayay の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値が最大のものを選ぶという手法。参考
この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 以下のクラスを生成しています。これは、ドキュメントがあります。

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にドキュメントがあります。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6