見出し画像

Stable Baselinesでソニックの1面を攻略するまでの記録

Stable Baselinesでソニックの1面を攻略するまでの記録です。(先人の知恵を使ってるので、そこまで苦労はしてない)

1. 学習開始時のコード

「ソニック・ザ・ヘッジホッグ」の学習開始時のコードは次の通りです。

・sonic.py
・util.py
・baselines/common/*

「baselines/common/*」は学習環境のコードで利用しているOpenAI Gymの環境ラッパーが含まれています。

【sonic.py】

import retro
import os
from stable_baselines import PPO2
from stable_baselines.common.policies import CnnPolicy
from stable_baselines.common.vec_env import DummyVecEnv
from baselines.common.retro_wrappers import *
from stable_baselines.bench import Monitor
from util import CustomRewardAndDoneEnv, callback, log_dir
from stable_baselines.common import set_global_seeds

# 環境の生成
env = retro.make(game='SonicTheHedgehog-Genesis', state='GreenHillZone.Act1')
env = SonicDiscretizer(env) # 行動空間を離散空間に変換
env = StochasticFrameSkip(env, n=4, stickprob=0.25) # スティッキーフレームスキップ
env = Downsample(env, 2) # ダウンサンプリング
env = Rgb2gray(env) # グレースケール
env = FrameStack(env, 4) # フレームスタック
env = ScaledFloatFrame(env) # 状態の正規化
env = TimeLimit(env, max_episode_steps=4500) # 5分タイムアウト
env = CustomRewardAndDoneEnv(env) # カスタム報酬関数・完了条件
env = Monitor(env, log_dir, allow_early_resets=True)
print('状態空間: ', env.observation_space)
print('行動空間: ', env.action_space)

# シードの指定
env.seed(0)
set_global_seeds(0)

# ベクトル環境
env = DummyVecEnv([lambda: env])

# モデルの生成
model = PPO2(policy=CnnPolicy, env=env, verbose=0)

# モデルの読み込み
# model = PPO2.load('logs/best_model.pkl', env=env, verbose=0)

# モデルの学習
print('train...')
model.learn(total_timesteps=20000000, callback=callback)

# テスト
'''
print('test...')
state = env.reset()
while True:
  env.render()
  action, _ = model.predict(state)
  state, reward, done, info = env.step(action)
  if done:
     env.reset()
'''

【util.py】

import gym
import os
import numpy as np
import datetime
import pytz
from stable_baselines.results_plotter import load_results, ts2xy

# パラメータ
GOAL_X = 9600
REWARD_SCALE = 0.1


# ログフォルダの生成
log_dir = './logs/'
os.makedirs(log_dir, exist_ok=True)


# CustomRewardAndDoneラッパー
class CustomRewardAndDoneEnv(gym.Wrapper):
   # 初期化
   def __init__(self, env):
       super(CustomRewardAndDoneEnv, self).__init__(env)
       self._cur_x = 0
       self._max_x = 0

   # リセット
   def reset(self, **kwargs):
       self._cur_x = 0
       self._max_x = 0
       return self.env.reset(**kwargs)

   # ステップ
   def step(self, action):
       state, rew, done, info = self.env.step(action)

       # 完了条件のカスタマイズ
       if info['lives'] == 2 or info['x'] > GOAL_X:
           done = True

       # 報酬関数のカスタマイズ(Δx)
       rew = info['x'] - self._cur_x
       self._cur_x = info['x']

       # 報酬関数のカスタマイズ(Δmax(x))
       # self._cur_x = info['x']
       # rew = max(0, self._cur_x - self._max_x)
       # self._max_x = max(self._max_x, self._cur_x)

       # スケールの調整
       rew *= REWARD_SCALE
       return state, rew, done, info

# コールバック
best_mean_reward = -np.inf
nupdates = 1
def callback(_locals, _globals):
   global nupdates
   global best_mean_reward
   # print('callback:', nupdates)

   # 10更新毎
   if (nupdates + 1) % 10 == 0:
       # 平均エピソード長、平均報酬の取得
       x, y = ts2xy(load_results(log_dir), 'timesteps')
       if len(x) > 0:
           # 最近10件の平均報酬
           mean_reward = np.mean(y[-10:])

           # 平均報酬がベスト報酬以上の時はエージェントを保存
           update_model = mean_reward > best_mean_reward
           if update_model:
               best_mean_reward = mean_reward
               _locals['self'].save(log_dir + 'best_model.pkl')

           # ログ
           print("time: {}, nupdates: {}, mean: {:.2f}, best_mean: {:.2f}, model_update: {}".format(
               datetime.datetime.now(pytz.timezone('Asia/Tokyo')),
               nupdates, mean_reward/REWARD_SCALE, best_mean_reward/REWARD_SCALE, update_model))

   nupdates += 1
   return True

2. 学習開始時のコードの解説

OpenAI Retro Contestの技術レポート」と記事「Attempting to Beat Sonic the Hedgehog with Reinforcement Learning」を参考にしました。

チューニングのポイントは次の4つです。

・完了条件
・報酬関数
・前処理
・学習アルゴリズムのパラメータ

◎完了条件
「完了条件」と「報酬関数」は、CustomRewardAndDoneEnvラッパーで定義します。「完了条件」はライフが1減った時(2になった時)か、クリアした時(X座標が9600になった時)としました。

       # 完了条件のカスタマイズ
       if info['lives'] == 2 or info['x'] > GOAL_X:
           done = True

◎報酬関数
「報酬関数」は「OpenAI Retro Contestの技術レポート」で「進捗したX座標の値」(Δx)と「進捗したX座標の最大値」(Δmax(x))の2つが提案されてました。初期状態としては少しでも前に進むよう、「進捗したX座標の値」(Δx)を指定しました。

       # 報酬関数のカスタマイズ(Δx)
       rew = info['x'] - self._cur_x
       self._cur_x = info['x']

       # 報酬関数のカスタマイズ(Δmax(x))
       # self._cur_x = info['x']
       # rew = max(0, self._cur_x - self._max_x)
       # self._max_x = max(self._max_x, self._cur_x)

「報酬のスケール」は、一般的には最大1.0くらいと言われており、ソニックの最大速度は「6」なので、「0.1」倍にしました。

REWARD_SCALE = 0.1
rew *= REWARD_SCALE

◎前処理
「前処理」は「環境ラッパー」で行います。はじめに、「OpenAI Retro Contestの技術レポート」と記事「Attempting to Beat Sonic the Hedgehog with Reinforcement Learning」でおすすめされてる、以下のラッパーを追加しました。

・SonicDiscretizerラッパー
・StochasticFrameSkipラッパー
・TimeLimitラッパー

それに加えて、主にDQNとかで定番のラッパーも追加しました。

・Downsampleラッパー
・Rgb2grayラッパー
・FrameStackラッパー
・ScaledFloatFrameラッパー

そして、「完了条件」と「報酬関数」をカスタマイズするCustomRewardAndDoneEnvラッパーと、ログを出力するためのMonitorラッパーも追加しました。

# 環境の生成
env = retro.make(game='SonicTheHedgehog-Genesis', state='GreenHillZone.Act1')
env = SonicDiscretizer(env) # 行動空間を離散空間に変換
env = StochasticFrameSkip(env, n=4, stickprob=0.25) # スティッキーフレームスキップ
env = Downsample(env, 2) # ダウンサンプリング
env = Rgb2gray(env) # グレースケール
env = FrameStack(env, 4) # フレームスタック
env = ScaledFloatFrame(env) # 状態の正規化
env = TimeLimit(env, max_episode_steps=4500) # 5分タイムアウト
env = CustomRewardAndDoneEnv(env) # カスタム報酬関数・完了条件
env = Monitor(env, log_dir, allow_early_resets=True)

◎学習アルゴリズムのパラメータ
基本は「PPO」のデフォルトがいいらしい。ということでひとまずデフォルトのままとしました。

3. 学習開始時のコードの学習結果

学習開始時のコードの学習結果は次のようになりました。
報酬の最大値はいい感じに上がって行きましたが、5200あたりで停止、このまままる10時間くらい学習させても超えられず状態。

train...
time: 2019-08-17 06:12:46.357061+09:00, nupdates: 9, mean: 511.01, best_mean: 511.01, model_update: True
time: 2019-08-17 06:13:02.002098+09:00, nupdates: 19, mean: 2487.53, best_mean: 2487.53, model_update: True
time: 2019-08-17 06:13:17.833765+09:00, nupdates: 29, mean: 4656.24, best_mean: 4656.24, model_update: True
time: 2019-08-17 06:13:33.379113+09:00, nupdates: 39, mean: 4531.43, best_mean: 4656.24, model_update: False
:
time: 2019-08-17 06:59:42.521180+09:00, nupdates: 1839, mean: 4987.01, best_mean: 4987.01, model_update: False
time: 2019-08-17 06:59:58.021217+09:00, nupdates: 1849, mean: 5139.78, best_mean: 5139.78, model_update: True
time: 2019-08-17 07:00:13.580089+09:00, nupdates: 1859, mean: 5203.11, best_mean: 5203.11, model_update: True
time: 2019-08-17 07:00:28.921812+09:00, nupdates: 1869, mean: 4633.82, best_mean: 5203.11, model_update: False
:

テスト実行してみると、ループのところで立ち往生...

画像1

4. 報酬関数の変更

ループを越えるのに必要な助走が覚えられてない。ということで、報酬関数を「進捗したX座標の値」(Δx)から「進捗したX座標の最大値」(Δmax(x))に変更することにしました。

       # 報酬関数のカスタマイズ(Δx)
       # rew = info['x'] - self._cur_x
       # self._cur_x = info['x']

       # 報酬関数のカスタマイズ(Δmax(x))
       self._cur_x = info['x']
       rew = max(0, self._cur_x - self._max_x)
       self._max_x = max(self._max_x, self._cur_x)

学習結果は次の通り。10時間くらい学習させても、5200のループは未だ超えられず、いろいろ試すようになったせいか平均報酬が上がったり下がったり...

train...
time: 2019-08-17 18:54:45.516582+09:00, nupdates: 9, mean: 702.00, best_mean: 702.00, model_update: True
time: 2019-08-17 18:54:57.026673+09:00, nupdates: 19, mean: 702.00, best_mean: 702.00, model_update: False
time: 2019-08-17 18:55:08.106501+09:00, nupdates: 29, mean: 702.00, best_mean: 702.00, model_update: False
:
time: 2019-08-17 19:13:50.412395+09:00, nupdates: 1019, mean: 4670.40, best_mean: 4903.20, model_update: False
time: 2019-08-17 19:14:01.852627+09:00, nupdates: 1029, mean: 5238.60, best_mean: 5238.60, model_update: True
:
time: 2019-08-17 19:26:25.440679+09:00, nupdates: 1689, mean: 1806.50, best_mean: 5478.10, model_update: False
:

5. 学習率の変更

Unity ML-Agentsの解説に「報酬が継続的に増加しないような場合は学習率の値を小さくするとよい」とあったので、小さくして試してみました。

model = PPO2(policy=CnnPolicy, env=env, verbose=0, learning_rate=0.000025)

学習結果は次の通り。1時間半の学習で無事5200のループを超えて、3時間の学習でゴールまで到達。無事に学習成功!

train...
time: 2019-08-18 08:14:11.095958+09:00, nupdates: 9, mean: 483.00, best_mean: 483.00, model_update: True
time: 2019-08-18 08:14:22.529804+09:00, nupdates: 19, mean: 483.00, best_mean: 483.00, model_update: False
time: 2019-08-18 08:14:33.938286+09:00, nupdates: 29, mean: 483.00, best_mean: 483.00, model_update: False
:
time: 2019-08-18 08:25:26.359581+09:00, nupdates: 599, mean: 4044.20, best_mean: 4187.80, model_update: False
time: 2019-08-18 08:25:37.896864+09:00, nupdates: 609, mean: 5270.00, best_mean: 5270.00, model_update: True
time: 2019-08-18 08:25:49.474086+09:00, nupdates: 619, mean: 5270.00, best_mean: 5270.00, model_update: False
time: 2019-08-18 08:26:00.801843+09:00, nupdates: 629, mean: 5270.00, best_mean: 5270.00, model_update: False
time: 2019-08-18 08:26:12.249971+09:00, nupdates: 639, mean: 5270.00, best_mean: 5270.00, model_update: False
time: 2019-08-18 08:26:23.695401+09:00, nupdates: 649, mean: 5371.40, best_mean: 5371.40, model_update: True
:
time: 2019-08-18 09:36:11.706082+09:00, nupdates: 4319, mean: 5725.10, best_mean: 5725.10, model_update: True
time: 2019-08-18 09:36:23.220276+09:00, nupdates: 4329, mean: 5725.10, best_mean: 5725.10, model_update: False
time: 2019-08-18 09:36:34.756688+09:00, nupdates: 4339, mean: 6129.50, best_mean: 6129.50, model_update: True
time: 2019-08-18 09:36:46.292796+09:00, nupdates: 4349, mean: 6489.80, best_mean: 6489.80, model_update: True
time: 2019-08-18 09:36:57.788906+09:00, nupdates: 4359, mean: 6957.80, best_mean: 6957.80, model_update: True
time: 2019-08-18 09:37:09.466959+09:00, nupdates: 4369, mean: 6958.10, best_mean: 6958.10, model_update: True
time: 2019-08-18 09:37:20.939411+09:00, nupdates: 4379, mean: 7327.90, best_mean: 7327.90, model_update: True
:
time: 2019-08-18 11:26:44.001411+09:00, nupdates: 10069, mean: 9613.20, best_mean: 9613.20, model_update: True
time: 2019-08-18 11:26:55.688520+09:00, nupdates: 10079, mean: 9611.70, best_mean: 9613.20, model_update: False
time: 2019-08-18 11:27:07.465563+09:00, nupdates: 10089, mean: 9613.00, best_mean: 9613.20, model_update: False
time: 2019-08-18 11:27:18.965852+09:00, nupdates: 10099, mean: 9613.00, best_mean: 9613.20, model_update: False
:

画像2


この記事が気に入ったらサポートをしてみませんか?