ChainerRLは、ドキュメントが未整備な部分が多い。例えば、公式ドキュメントを見ても、サンプルコードに乗っているAPIを全て説明しているわけではない感じです。(2017/12/30現在)
なので、サンプルを読んで勉強してみます。
examples/ale/train_dqn_ale.py
ALE(The Arcade Learning Environment)をDQNで学習するプログラムです。
プログラムは短いが、引数の処理などが多いので、学習時に本質的な部分だけを抽出すると以下のような感じになります。
※ 以下、実行できるかは未確認です
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
# 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
misc.env_modifiers.make_reward_clipped(env, -1, 1)
chainerrl.misc.env_modifiers.make_reward_clipped
というAPIです。ドキュメントは見当たりませんでしたが、その名の通り、Rewardを$[-1, 1]$の範囲でクリップしています。
chainerrl.links
q_func = parse_arch(args.arch, n_actions, F.relu)
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
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段になっていますね。
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のモデルをそうしなかったんだろう。。)
rbuf = replay_buffer.ReplayBuffer(10 ** 6)
chainerrl.replay_buffer
次の、chainerrl.replay_buffer
は、いわゆる、Experience Replayay のAPIです。引数でバッファサイズを指定しています。
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 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
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)
def parse_agent(agent):
return {'DQN': agents.DQN,
'DoubleDQN': agents.DoubleDQN,
'PAL': agents.PAL}[agent]
chainer.agents
以下のクラスを生成しています。これは、ドキュメントがあります。
# 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 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
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にドキュメントがあります。