見出し画像

Gym Retroにバーチャファイター2を追加して学習する

Gym Retroにバーチャファイター2を追加する』に続きです。Gym Retroにバーチャファイター2を学習します。難易度は「EASY」でROUND 1からクリアを目指します。

1. バーチャファイター2の学習コード

バーチャファイター2を学習するコードは次の通りです。

import retro
import os
import time
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 Vf2Discretizer, CustomRewardAndDoneEnv, callback, log_dir
from stable_baselines.common import set_global_seeds

# 環境の生成
env = retro.make(game='VirtuaFighter2-Genesis', state='VirtuaFighter2.Level1.AkiraVsLau.state')
env = Vf2Discretizer(env) # 行動空間を離散空間に変換
env = StochasticFrameSkip(env, n=4, stickprob=0.25) # スティッキーフレームスキップ
env = Downsample(env, 2) # ダウンサンプリング
env = Rgb2gray(env) # グレースケール
env = FrameStack(env, 4) # フレームスタック
env = ScaledFloatFrame(env) # 状態の正規化

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, learning_rate=0.0000025)

# モデルの読み込み
# model = PPO2.load('./vf2/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()
   time.sleep(1.0/60.0)
   action, _ =  model.predict(state)
   state, reward, done, info = env.step(action)
   if done:
       env.reset()
'''
import gym
import os
import numpy as np
import datetime
import pytz
from stable_baselines.results_plotter import load_results, ts2xy

# 定数
HEALTH_GAUGE = 287840.0
WIN_COUNT = 2
ROUND_START = 1
ROUND_END = 9

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

# 行動空間の変換
class Vf2Discretizer(gym.ActionWrapper):
   # 初期化
   def __init__(self, env):
       super(Vf2Discretizer, self).__init__(env)
       buttons = ["B", "A", "MODE", "START", "UP", "DOWN", "LEFT", "RIGHT", "C", "Y", "X", "Z"]
       actions = [['B'], ['C'], ['A'],
           ['DOWN', 'B'], ['DOWN', 'C'], ['DOWN', 'A'],
           ['LEFT'], ['DOWN'], ['RIGHT'],
           ['UP', 'LEFT'], ['UP'], ['UP', 'RIGHT'],
           ['A', 'B'],
           ['UP', 'B']]
       self._actions = []
       for action in actions:
           arr = np.array([False] * 12)
           for button in action:
               arr[buttons.index(button)] = True
           self._actions.append(arr)
       self.action_space = gym.spaces.Discrete(len(self._actions))

   # 行動の取得
   def action(self, a):
       return self._actions[a].copy()

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

   # リセット
   def reset(self, **kwargs):
       self.round = ROUND_START
       self.win_count1 = 0
       self.win_count2 = 0
       return self.env.reset(**kwargs)

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

       # ラウンド内の勝利数の初期化
       if info['win_count1'] == 0 and info['win_count2'] == 0:
           self.win_count1 = 0
           self.win_count2 = 0
       # 勝利
       if info['win_count1'] > self.win_count1:
           self.win_count1 = info['win_count1']
           rew = 1.0 + info['health_gauge1']/HEALTH_GAUGE
           if self.win_count1 == WIN_COUNT:
               if self.round == ROUND_END:
                   done = True
               else:
                   self.round += 1
       # 敗北
       if info['win_count2'] > self.win_count2:
           self.win_count2 = info['win_count2']
           rew = -info['health_gauge2']/HEALTH_GAUGE
           if self.win_count2 == WIN_COUNT:
               done = True

       if rew != 0 or done:
           print('round, rew, done>',self.round, rew, done)

       # スケールの調整
       return obs, 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 > 100 and (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, best_mean_reward, update_model))

   nupdates += 1
   return True

2. 前処理

今回は以下の「前処理」を追加しています。

・Vf2Discretizer : 行動空間の変更
・StochasticFrameSkip : フレームスキップ
・Downsample : ダウンサンプリング
・Rgb2gray : グレースケール
・FrameStack : フレームスタック
・ScaledFloatFrame : 状態の正規化

3. 完了条件

今回は「完了条件」を次のようにしました。

・ラウンド中の相手に2本取られた時。
・DUALに勝利した時。

4. 報酬関数

今回は「報酬関数」を次のようにしました。
相手に勝利すると0.5*2=1、ノーダメージでクリアすると0.1のボーナス加算になります。

・1本取った時

rew = 0.5 + info['health_gauge1']/HEALTH_GAUGE

・1本取られた時

rew = -info['health_gauge2']/HEALTH_GAUGE

5. 学習パラメータ

平均報酬が安定しなかったので、学習率を小さくしました。

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

6. ROUND 1からの学習の実行

ROUND 1から学習を実行してみました。
1時間程度で平均報酬6.18まで上がりましたが、それ以降は上がりませんでした。ROUND 6のJackyに負けてるようです。

画像3

2019-09-22 21:39:51.017439+09:00, nupdates: 109, mean: 3.77, best_mean: 3.77, model_update: True
    :
2019-09-22 22:54:46.817440+09:00, nupdates: 3479, mean: 5.18, best_mean: 5.18, model_update: True

7. Jackyを倒す訓練の実行

Jackyを倒す訓練のため、ROUND6から学習を実行してみました。stateファイルを変更し、ROUND_STARTを6とします。
2時間程度で平均報酬が1.48まで上がり、Jackyに勝てるようになりました。

画像4

time: 2019-09-23 10:52:23.198121+09:00, nupdates: 109, mean: 0.83, best_mean: 0.83, model_update: True
    :
time: 2019-09-23 13:06:33.692358+09:00, nupdates: 9069, mean: 1.48, best_mean: 2.48, model_update: True

8. Duralを倒す訓練の実行

Duralを倒す訓練のため、ROUND 9から学習を実行してみました。stateファイルを変更し、ROUND_STARTを9とします。
2時間程度で平均報酬が0.85まで上がり、Duralにまぁまぁ勝てるようになりました。

画像1

画像2

time: 2019-09-23 15:30:07.500548+09:00, nupdates: 109, mean: 0.35, best_mean: 0.35, model_update: True
   :
time: 2019-09-23 17:53:02.688691+09:00, nupdates: 7699, mean: 0.85, best_mean: 0.85, model_update: True


9. 今日の結果

できれば1つのモデルで全ラウンドを攻略したいが今の所できていない。
簡単な面を攻略してる間に、後ろの面の攻略を忘れてしまう?
各対戦相手と対戦する確率を等しくしたら効果はある?
層を増やすなどパラメータ調整はまだいろいろありそう。

ひとまず、1ラウンドごとであれば攻略したので今日は終了。


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