カスタムGym環境作成(10) - 川と橋のあるマップを強化学習で攻略

今回は、川と橋のあるマップを強化学習で攻略していきます。

[川と橋を追加したマップイメージ]

川と橋と橋のあるマップを強化学習

前々回に実装したカスタムGym環境(env6.py)を9行目で読み込み、強化学習を行います。

学習アルゴリズムはACKTR(25行目)で、学習ステップ数は128000(28行目)としています。

[ソース]

train6.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 警告を非表示
import os
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env6 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import ACKTR
from stable_baselines.bench import Monitor

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

# 環境の生成
env = MyEnv()
env = Monitor(env, log_dir, allow_early_resets=True)
env = DummyVecEnv([lambda: env])

# モデルの生成
model = ACKTR('MlpPolicy', env, verbose=1)

# モデルの学習
model.learn(total_timesteps=128000)

# モデルの保存
model.save('model6')

学習はなかなかうまくいきませんでした。

学習ステップ数を増やしたり、学習アルゴリズムをPPO2に戻したりしたのですが、それでもうまくゴールまでたどり着いてくれません😑

うまく学習できない場合の対処法

いろいろと学習方法を変更したのですがうまくいかなかったので、カスタムGym環境のほうを見直すことにしました。

報酬を見直して、ゴール時の報酬が少ないのかと思い100から800に変更してみました。

(前々回ソースenv6.pyの91行目がその報酬設定となります)

そうするとあっさりゴールまでたどり着くことができました😅

学習済みモデルを使って攻略

うまくゴールまでたどり着くことができるようになった学習済みモデルを使って、プレイしてみます。

[ソース]

play6.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 警告を非表示
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env6 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import ACKTR

# 環境の生成
env = MyEnv()
env = DummyVecEnv([lambda: env])

# モデルの読み込み
model = ACKTR.load('model6')

# モデルのテスト
state = env.reset()
total_reward = 0
while True:
# 環境の描画
env.render()

# モデルの推論
action, _ = model.predict(state)

# 1ステップの実行
state, reward, done, info = env.step(action)
total_reward += reward
print('reward:', reward, 'total_reward', total_reward)
print('-----------')

print('')
# エピソード完了
if done:
# 環境の描画
print('total_reward:', total_reward)
break

実行結果は以下のようになりました。

[結果]

Loading a model without an environment, this model cannot be trained until it has a valid environment.
☆山山山   山
  川川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-1.]
-----------

S山山山   山
☆ 川川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-2.]
-----------

S山山山   山
 ☆川川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-5.] total_reward [-7.]
-----------

S山山山   山
  ☆川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-5.] total_reward [-12.]
-----------

S山山山   山
  川☆ 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-13.]
-----------

S山山山   山
  川川☆山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-14.]
-----------

S山山山☆  山
  川川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-15.]
-----------

S山山山 ☆ 山
  川川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-16.]
-----------

S山山山  ☆山
  川川 山 G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [-1.] total_reward [-17.]
-----------

S山山山   山
  川川 山☆G
山 三三 山 山
山 川川山   
  山  山山 
山 山 山 山 
山       
  山 山山 山
reward: [800.] total_reward [783.]
-----------

total_reward: [783.]

まっすぐゴールまで向かっていますが、なにか引っ掛かります。

そう、橋を渡らず川を突っ切っているのです😥

確かに距離的には川を渡った方が近いのかもしれませんが、マップを作った時の想定としては橋をわたって欲しかったのです。

報酬的には、川に入ると-5ポイント橋を渡ると-1ポイントなので、橋をわたる方が総報酬としは上になるはずなんですが・・・

次回は、川ではなくきちんと橋を渡ってくれるように調整を行いたいと思います。

(おそらく報酬設定を変えるだけで対応可能だと思うのですが・・・)

カスタムGym環境作成(9) - 川と橋のあるマップをランダム実行

前回記事で、マップに川と橋を追加しカスタムGym環境を実装しました。

[川と橋を追加したマップイメージ]

今回は、そのマップに対してランダムで実行しています。

川と橋と橋のあるマップをランダム実行

ランダム実行するソースは下記の通りです。

2行目でインポートするカスタムGym環境をenv6にしているのが、唯一の変更箇所となります。

[ソース]

random6.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import gym
from env6 import MyEnv

# 環境の生成
env = MyEnv()

# 環境リセット
state = env.reset()

total_reward = 0
while True:
# ランダム行動の取得
action = env.action_space.sample()
# 1ステップの実行
state, reward, done, info = env.step(action)
total_reward += reward
#print('state=', state, 'reward=', reward)
# 環境の描画
env.render()
print('reward:', reward, 'total_reward', total_reward)
print('-----------')
# エピソード完了
if done:
print('done')
break

ランダム実行のため毎回結果が異なります。
10回ほど実行した結果は下記のようになりました。

[結果]

  • 1回目
    total_reward -150
  • 2回目
    total_reward -571
  • 3回目
    total_reward 30
  • 4回目
    total_reward -1139
  • 5回目
    total_reward -1867
  • 6回目
    total_reward -142
  • 7回目
    total_reward -46
  • 8回目
    total_reward -298
  • 9回目
    total_reward -1286
  • 10回目
    total_reward -441

トータル報酬は -1867から30 の範囲となりました。

次回はStable BaselinesACKTRアルゴリズムを使って、強化学習を行ってみます。

カスタムGym環境作成(8) - マップ更新(川と橋を追加)

前回記事にて、2つ目のマップを攻略することができました。

今回は3つ目のマップとして川と橋を追加してみたいと思います。

マップに川と橋を追加

作成するマップイメージは次の通りです。

[川と橋を追加したマップイメージ]

ゴールにたどり着くには、橋を渡っていくコースと山沿いの道をこえるコースを2つがあり、橋を渡るコースのほうが近道になっています。

川への移動に関しては、不可にしようと思ったんのですが、そうすると山超えのコースとの差別化ができないので、川に落ちたイメージで報酬を-5ポイントに設定してみました。

カスタムGym環境の変更点とソースは以下の通りです。

[変更点]

  • 14,15行目
    橋と川の定義を追加。
  • 92-94行目
    川に入った時の判定を追加し、報酬として-5ポイントを返す。

[ソース]

env6.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import sys

import gym
import numpy as np
import gym.spaces

class MyEnv(gym.Env):
FIELD_TYPES = [
'S', # 0: スタート
'G', # 1: ゴール
' ', # 2: 平地
'山', # 3: 山(歩けない)
'☆', # 4: プレイヤー
'三', # 5: 橋
'川', # 6: 川
]
MAP = np.array([
[0, 3, 3, 3, 2, 2, 2, 3],
[2, 2, 6, 6, 2, 3, 2, 1],
[3, 2, 5, 5, 2, 3, 2, 3],
[3, 2, 6, 6, 3, 2, 2, 2],
[2, 2, 3, 2, 2, 3, 3, 2],
[3, 2, 3, 2, 3, 2, 3, 2],
[3, 2, 2, 2, 2, 2, 2, 2],
[2, 2, 3, 2, 3, 3, 2, 3]
])
MAX_STEPS = 200

def __init__(self):
super().__init__()
# action_space, observation_space, reward_range を設定する
self.action_space = gym.spaces.Discrete(4) # 上下左右
self.observation_space = gym.spaces.Box(
low=0,
high=len(self.FIELD_TYPES),
shape=self.MAP.shape
)
self.reset()

def reset(self):
# 諸々の変数を初期化する
self.pos = self._find_pos('S')[0]
self.goal = self._find_pos('G')[0]
self.river = self._find_pos('川')
self.done = False
self.steps = 0
return self._observe()

def step(self, action):
# 1ステップ進める処理を記述。戻り値は observation, reward, done(ゲーム終了したか), info(追加の情報の辞書)
# 左上の座標を(0, 0)とする
if action == 0: # 右移動
next_pos = self.pos + [0, 1]
elif action == 1: # 左移動
next_pos = self.pos + [0, -1]
elif action == 2: # 下移動
next_pos = self.pos + [1, 0]
elif action == 3: # 上移動
next_pos = self.pos + [-1, 0]

if self._is_movable(next_pos):
self.pos = next_pos
moved = True
else:
moved = False

observation = self._observe()
reward = self._get_reward(self.pos, moved)
self.done = self._is_done()
return observation, reward, self.done, {}

def render(self, mode='console', close=False):
for row in self._observe():
for elem in row:
print(self.FIELD_TYPES[elem], end='')
print()

def _close(self):
pass

def _seed(self, seed=None):
pass

def _get_reward(self, pos, moved):
# 報酬を返す。
# - ゴールにたどり着くと 100 ポイント
# - 川に入ったら -5 ポイント
# - 1ステップごとに-1ポイント(できるだけ短いステップでゴールにたどり着きたい)
if moved:
if (self.goal == pos).all():
return 100
for x in self.river:
if (x == pos).all():
return -5
return -1

def _is_movable(self, pos):
# マップの中にいるか、歩けない場所にいないか
return (
0 <= pos[0] < self.MAP.shape[0]
and 0 <= pos[1] < self.MAP.shape[1]
and self.FIELD_TYPES[self.MAP[tuple(pos)]] != '山'
)

def _observe(self):
# マップにプレイヤーの位置を重ねて返す
observation = self.MAP.copy()
observation[tuple(self.pos)] = self.FIELD_TYPES.index('☆')
return observation

def _is_done(self):
# 最大で self.MAX_STEPS まで
if (self.pos == self.goal).all():
return True
elif self.steps > self.MAX_STEPS:
return True
else:
return False

def _find_pos(self, field_type):
return np.array(list(zip(*np.where(self.MAP == self.FIELD_TYPES.index(field_type)))))

次回はこのカスタムGym環境に対して、ランダム実行を行ってみます。

カスタムGym環境作成(7) - ちょっと複雑なマップをACKTRで完全攻略

前回ACKTRアルゴリズムで学習モデルを作成しました。

今回はそのモデルを読み込んで、ちょっとだけ複雑にしたマップを攻略できるかどうか確認します。

ACKTR学習済みモデルを使って攻略

ACKTRアルゴリズムで学習したモデルを読み込んで、カスタム環境にて実行させます。

[ソース]

play5.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 警告を非表示
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env4 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import ACKTR

# 環境の生成
env = MyEnv()
env = DummyVecEnv([lambda: env])

# モデルの読み込み
model = ACKTR.load('model4')

# モデルのテスト
state = env.reset()
total_reward = 0
while True:
# 環境の描画
env.render()

# モデルの推論
action, _ = model.predict(state)

# 1ステップの実行
state, reward, done, info = env.step(action)
total_reward += reward
print('reward:', reward, 'total_reward', total_reward)
print('-----------')

print('')
# エピソード完了
if done:
# 環境の描画
print('total_reward:', total_reward)
break

実行結果は以下のようになりました。

[結果]

Loading a model without an environment, this model cannot be trained until it has a valid environment.
☆山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-1.]
-----------

S山山 山 山G
☆山  山 山 
    山   
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-2.]
-----------

S山山 山 山G
 山  山 山 
☆   山   
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-3.]
-----------

S山山 山 山G
 山  山 山 
 ☆  山   
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-4.]
-----------

S山山 山 山G
 山  山 山 
    山   
山☆山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-5.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山☆山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-6.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山     
 ☆  山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-7.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山     
  ☆ 山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-8.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山     
   ☆山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-9.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山☆    
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-10.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山 ☆   
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-11.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山  山
山 山  ☆  
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-12.]
-----------

S山山 山 山G
 山  山 山 
    山   
山 山山山☆ 山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-13.]
-----------

S山山 山 山G
 山  山 山 
    山☆  
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-14.]
-----------

S山山 山 山G
 山  山 山 
    山 ☆ 
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-15.]
-----------

S山山 山 山G
 山  山 山 
    山  ☆
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [-1.] total_reward [-16.]
-----------

S山山 山 山G
 山  山 山☆
    山   
山 山山山  山
山 山     
    山  山
山山  山山  
山山山 山山 山
reward: [100.] total_reward [84.]
-----------

total_reward: [84.]

まっすぐにスタート地点からゴールまで進んでいることが分かります。

少しだけ複雑にしたマップでもきちんと学習できるようになりました。改善したは次の2点です。

  • 学習済みアルゴリズムを変更
    PPO2からACKTRに学習アルゴリズムを変更しました。
  • 学習ステップ数を5倍に変更
    学習ステップ数を128000から128000*5に変更しました。

カスタム環境を変えて、うまく学習できなくなった場合はこのような変更を行って試行錯誤することが必要になるようです。

また、まったく同じ条件で学習してもうまく学習できる場合とそうでない場合があります。

何度か実行してみて平均報酬平均エピソード長などをグラフ化しきちんと収束(学習)できているかどうかを確認する必要もあります。

AI全般そうかと思いますが、強化学習に関する作業もなかなか地道なものが多いですね😌

カスタムGym環境作成(6) - ちょっと複雑なマップをACKTRで学習・攻略

前回記事にてカスタムGym環境として、ちょっと複雑なマップをPPO(PPO2)アルゴリズムを使って、強化学習してみましたがうまく攻略できませんでした。

今回は学習アルゴリズムをACKTRに変えて、攻略できるかどうかを見ていきたいと思います。

(ACKTRは、以前学習アルゴリズムをいろいろ試していたときにかなり優秀だと感じていたアルゴリズムです。)

ACKTRで強化学習

カスタム環境を読み込み、ACKTRアルゴリズムで学習を行います。

変更箇所は下記の3点です。

  • 12,13行目
    読み込むアルゴリズムをPPO2からACKTRに変更
  • 26,27行目
    使用するアルゴリズムをPPO2からACKTRに変更
  • 39行目
    ステップ実行回数を5倍に増やす。

[ソース]

train4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 警告を非表示
import os
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env4 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
#from stable_baselines import PPO2
from stable_baselines import ACKTR
from stable_baselines.bench import Monitor

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

# 環境の生成
env = MyEnv()
env = Monitor(env, log_dir, allow_early_resets=True)
env = DummyVecEnv([lambda: env])

# モデルの生成
#model = PPO2('MlpPolicy', env, verbose=1)
model = ACKTR('MlpPolicy', env, verbose=1)

# モデルの学習
model.learn(total_timesteps=128000*5)

# モデルの保存
model.save('model4')

[実行結果(途中略)]

---------------------------------
| explained_variance | -0.0363  |(誤差の分散)
| fps                | 12       |(1秒あたりのフレーム数)
| nupdates           | 1        |(更新回数)
| policy_entropy     | 1.39     |(方策のエントロピー)
| policy_loss        | -13.5    |(方策の損失)
| total_timesteps    | 20       |(全環境でのタイムステップ数)
| value_loss         | 122      |(価値関数更新時の平均損失)
---------------------------------
---------------------------------
| ep_len_mean        | 969      |
| ep_reward_mean     | -868     |
| explained_variance | 9.41e-05 |
| fps                | 379      |
| nupdates           | 100      |
| policy_entropy     | 1.28     |
| policy_loss        | -9.35    |
| total_timesteps    | 2000     |
| value_loss         | 51.2     |
---------------------------------
---------------------------------
| ep_len_mean        | 969      |
| ep_reward_mean     | -868     |
| explained_variance | 1.19e-07 |
| fps                | 445      |
| nupdates           | 200      |
| policy_entropy     | 0.499    |
| policy_loss        | -5.09    |
| total_timesteps    | 4000     |
| value_loss         | 44.8     |
---------------------------------
         :
        (略)
         :
----------------------------------
| ep_len_mean        | 17        |
| ep_reward_mean     | 84        |
| explained_variance | 0         |
| fps                | 534       |
| nupdates           | 31800     |
| policy_entropy     | 0.000239  |
| policy_loss        | -0.000114 |
| total_timesteps    | 636000    |
| value_loss         | 42.5      |
----------------------------------
----------------------------------
| ep_len_mean        | 17        |
| ep_reward_mean     | 84        |
| explained_variance | 0         |
| fps                | 534       |
| nupdates           | 31900     |
| policy_entropy     | 0.000267  |
| policy_loss        | -0.000112 |
| total_timesteps    | 638000    |
| value_loss         | 32.2      |
----------------------------------
----------------------------------
| ep_len_mean        | 17        |←平均エピソード長
| ep_reward_mean     | 84        |←平均報酬
| explained_variance | 0         |
| fps                | 534       |
| nupdates           | 32000     |
| policy_entropy     | 0.000279  |
| policy_loss        | -0.000116 |
| total_timesteps    | 640000    |
| value_loss         | 31.2      |
----------------------------------

今回は最終的な平均報酬がプラスになっていて、平均エピソード長も最初に比べて少なく(短く)なっているので、きちんと学習(攻略)できているように思えます。

平均報酬をグラフ化して確認

学習したモデルを実行する前にmonitor.csvを読み込み、グラフ化して平均報酬の遷移を確認します。
(前回ソースlog_graph.pyと全く同じものです。)

[ソース]

log_graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd
import matplotlib.pyplot as plt

# ログファイルの読み込み(報酬,エピソード,経過時間)
df = pd.read_csv('./logs/monitor.csv', names=['r', 'l', 't'])
df = df.drop(range(2)) # 先頭2行を排除

# 報酬グラフの表示
x = range(len(df['r']))
y = df['r'].astype(float)
plt.plot(x, y)
plt.xlabel('episode')
plt.ylabel('reward')
plt.show()

[結果]

マイナス報酬が大きいため判断しにくいのですが、エピソード数が多く、最終的にはマイナスに大きく振れることがないのでうまく学習できているような気がします。

log/monitor.csvを確認すると次のようになっていました。

[monitor.csv(途中略)]

#{"t_start": 1622147186.477496, "env_id": null}
r,l,t
-4,105,6.601282
-1732,1833,10.039687
-7852,7953,24.873165
-5720,5821,35.86051
-4527,4628,44.683237
-27310,27411,96.671992
-2342,2443,101.065658
-305,406,101.811046
         :
        (略)
         :
84,17,1181.376593
84,17,1181.400596
84,17,1181.441152
84,17,1181.466216
84,17,1181.499782
84,17,1181.531546
84,17,1181.566779
84,17,1181.595796

最初はマイナス報酬ばかりでしたが、最終的にはプラス報酬(84)となり、まっすぐゴールに向かっているようです。

次回は、この学習したモデルを使ってきちんと攻略できているかどうかを確認します。

カスタムGym環境作成(5) - ちょっと複雑なマップをPPO(PPO2)で学習・攻略

前回カスタム環境として、簡単なマップを作りスタート地点からゴール地点まで移動する環境を作成しました。

今回はStable BaselinesPPO(PPO2)アルゴリズムを使って、強化学習を行いそのカスタム環境を効率よく攻略してみます。

強化学習

カスタム環境を読み込み、PPO(PPO2)アルゴリズムで学習を行います。

前前回学習した処理(train3_log.py)からの変更点は次の2点のみです。

  • 9行目
    読み込むカスタムGym環境をenv3からenv4に変更
  • 31行目
    学習済みモデルの出力ファイル名をmodel3からmodel4に変更

[ソース]

train4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 警告を非表示
import os
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env4 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import PPO2
from stable_baselines.bench import Monitor

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

# 環境の生成
env = MyEnv()
env = Monitor(env, log_dir, allow_early_resets=True)
env = DummyVecEnv([lambda: env])

# モデルの生成
model = PPO2('MlpPolicy', env, verbose=1)

# モデルの学習
model.learn(total_timesteps=128000)

# モデルの保存
model.save('model4')

[結果(途中略)]

---------------------------------------
| approxkl           | 0.00013371343  |(新しい方策から古い方策へのKullback-Leibler発散尺度)
| clipfrac           | 0.0            |(クリップ範囲ハイパーパラメータが使用される回数の割合
| explained_variance | -0.0241        |(誤差の分散)
| fps                | 405            |(1秒あたりのフレーム数)
| n_updates          | 1              |(更新回数)
| policy_entropy     | 1.3861077      |(方策のエントロピー)
| policy_loss        | -0.00052567874 |(方策の損失)
| serial_timesteps   | 128            |(1つの環境でのタイプステップ数)
| time_elapsed       | 0              |(経過時間)
| total_timesteps    | 128            |(全環境でのタイムステップ数)
| value_loss         | 111.95057      |(価値関数更新時の平均損失)
---------------------------------------
--------------------------------------
| approxkl           | 0.00023907197 |
| clipfrac           | 0.0           |
| explained_variance | 0.00739       |
| fps                | 1362          |
| n_updates          | 2             |
| policy_entropy     | 1.3846728     |
| policy_loss        | -0.001835278  |
| serial_timesteps   | 256           |
| time_elapsed       | 0.316         |
| total_timesteps    | 256           |
| value_loss         | 110.1702      |
--------------------------------------
--------------------------------------
| approxkl           | 0.0002260478  |
| clipfrac           | 0.0           |
| explained_variance | 0.00444       |
| fps                | 1320          |
| n_updates          | 3             |
| policy_entropy     | 1.3818537     |
| policy_loss        | -0.0009469581 |
| serial_timesteps   | 384           |
| time_elapsed       | 0.41          |
| total_timesteps    | 384           |
| value_loss         | 108.967865    |
--------------------------------------
         :
        (略)
         :
--------------------------------------
| approxkl           | 0.003960348   |
| clipfrac           | 0.048828125   |
| ep_len_mean        | 1.24e+04      |←平均エピソード長
| ep_reward_mean     | -1.23e+04     |←平均報酬
| explained_variance | 0.879         |
| fps                | 1433          |
| n_updates          | 998           |
| policy_entropy     | 1.1028377     |
| policy_loss        | -0.0012131184 |
| serial_timesteps   | 127744        |
| time_elapsed       | 92.8          |
| total_timesteps    | 127744        |
| value_loss         | 3.6379788e-11 |
--------------------------------------
--------------------------------------
| approxkl           | 0.0034873062  |
| clipfrac           | 0.0           |
| ep_len_mean        | 1.24e+04      |
| ep_reward_mean     | -1.23e+04     |
| explained_variance | 0.794         |
| fps                | 1385          |
| n_updates          | 999           |
| policy_entropy     | 1.1340904     |
| policy_loss        | -0.0011979407 |
| serial_timesteps   | 127872        |
| time_elapsed       | 92.9          |
| total_timesteps    | 127872        |
| value_loss         | 3.45608e-11   |
--------------------------------------
---------------------------------------
| approxkl           | 0.0054595554   |
| clipfrac           | 0.041015625    |
| ep_len_mean        | 1.24e+04       |
| ep_reward_mean     | -1.23e+04      |
| explained_variance | 0.728          |
| fps                | 1419           |
| n_updates          | 1000           |
| policy_entropy     | 1.1066511      |
| policy_loss        | -0.00089572184 |
| serial_timesteps   | 128000         |
| time_elapsed       | 93             |
| total_timesteps    | 128000         |
| value_loss         | 3.6322945e-11  |
---------------------------------------

平均報酬が最後までマイナスなのが気になります・・・。

平均報酬を確認

学習したモデルを実行する前にmonitor.csvを読み込み、グラフ化して平均報酬の遷移を確認します。
(前回ソースlog_graph.pyと全く同じものです。)

[ソース]

log_graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd
import matplotlib.pyplot as plt

# ログファイルの読み込み(報酬,エピソード,経過時間)
df = pd.read_csv('./logs/monitor.csv', names=['r', 'l', 't'])
df = df.drop(range(2)) # 先頭2行を排除

# 報酬グラフの表示
x = range(len(df['r']))
y = df['r'].astype(float)
plt.plot(x, y)
plt.xlabel('episode')
plt.ylabel('reward')
plt.show()

[結果]

予想していたものとだいぶ違う結果となりました。

表示されているエピソードが7つしかないので結果が収束しているかどうかわかりません・・・というかきっと収束(学習)していないんでしょう。

log/monitor.csvを確認すると次のようになっていました。

[monitor.csv]

#{"t_start": 1622061308.762112, "env_id": null}
r,l,t
-754,855,1.429902
-2622,2723,3.449695
-4777,4878,7.05819
-4072,4173,10.048853
-72982,73083,63.42402
-260,361,63.682051
-332,433,63.981119

確かに結果のでているエピソードは7つしかありませんね。しかも全てマイナス報酬(rの列)となっています。

マップを少しだけ複雑にしただけなので、同じ手法で問題なく攻略してくれると予想していたのですが、何か対策を行う必要がありそうです。

(ちなみにこの学習済みモデルを読み込んで実行したところ、スタート地点から全く移動しないという状況でした😱)

カスタムGym環境作成(4) - マップを少し複雑に

前回までの記事で、簡単なマップに対して強化学習を行い効率のよいルートで攻略できるようになったことが分かりました。

今回はマップを少しだけ複雑にしてみて、それでも強化学習で攻略できるのかを確認したいと思います。

今回作成するマップイメージは下記のようなものです。

[マップイメージ]

センスのかけらもないマップですが、これをカスタムGym環境にしてみます。

カスタムGym環境作成

マップイメージのカスタムGym環境作成するには、今まで使用してきたenv3.pyの配列データMAPを修正するだけです。

また最大ステップ数(MAX_STEPS)を50から200に変更しました。

[ソース]

env4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import sys

import gym
import numpy as np
import gym.spaces

class MyEnv(gym.Env):
FIELD_TYPES = [
'S', # 0: スタート
'G', # 1: ゴール
' ', # 2: 平地
'山', # 3: 山(歩けない)
'☆', # 4: プレイヤー
]
MAP = np.array([
[0, 3, 3, 2, 3, 2, 3, 1],
[2, 3, 2, 2, 3, 2, 3, 2],
[2, 2, 2, 2, 3, 2, 2, 2],
[3, 2, 3, 3, 3, 2, 2, 3],
[3, 2, 3, 2, 2, 2, 2, 2],
[2, 2, 2, 2, 3, 2, 2, 3],
[3, 3, 2, 2, 3, 3, 2, 2],
[3, 3, 3, 2, 3, 3, 2, 3]
])
MAX_STEPS = 200

def __init__(self):
super().__init__()
# action_space, observation_space, reward_range を設定する
self.action_space = gym.spaces.Discrete(4) # 上下左右
self.observation_space = gym.spaces.Box(
low=0,
high=len(self.FIELD_TYPES),
shape=self.MAP.shape
)
self.reset()

def reset(self):
# 諸々の変数を初期化する
self.pos = self._find_pos('S')[0]
self.goal = self._find_pos('G')[0]
self.done = False
self.steps = 0
return self._observe()

def step(self, action):
# 1ステップ進める処理を記述。戻り値は observation, reward, done(ゲーム終了したか), info(追加の情報の辞書)
# 左上の座標を(0, 0)とする
if action == 0: # 右移動
next_pos = self.pos + [0, 1]
elif action == 1: # 左移動
next_pos = self.pos + [0, -1]
elif action == 2: # 下移動
next_pos = self.pos + [1, 0]
elif action == 3: # 上移動
next_pos = self.pos + [-1, 0]

if self._is_movable(next_pos):
self.pos = next_pos
moved = True
else:
moved = False

observation = self._observe()
reward = self._get_reward(self.pos, moved)
self.done = self._is_done()
return observation, reward, self.done, {}

def render(self, mode='console', close=False):
for row in self._observe():
for elem in row:
print(self.FIELD_TYPES[elem], end='')
print()

def _close(self):
pass

def _seed(self, seed=None):
pass

def _get_reward(self, pos, moved):
# 報酬を返す。
# - ゴールにたどり着くと 100 ポイント
# - 1ステップごとに-1ポイント(できるだけ短いステップでゴールにたどり着きたい)
if moved and (self.goal == pos).all():
return 100
else:
return -1

def _is_movable(self, pos):
# マップの中にいるか、歩けない場所にいないか
return (
0 <= pos[0] < self.MAP.shape[0]
and 0 <= pos[1] < self.MAP.shape[1]
and self.FIELD_TYPES[self.MAP[tuple(pos)]] != '山'
)

def _observe(self):
# マップにプレイヤーの位置を重ねて返す
observation = self.MAP.copy()
observation[tuple(self.pos)] = self.FIELD_TYPES.index('☆')
return observation

def _is_done(self):
# 最大で self.MAX_STEPS まで
if (self.pos == self.goal).all():
return True
elif self.steps > self.MAX_STEPS:
return True
else:
return False

def _find_pos(self, field_type):
return np.array(list(zip(*np.where(self.MAP == self.FIELD_TYPES.index(field_type)))))

ランダム実行

上記で作成したカスタム環境を読み込み、ランダムで実行してみます。

修正箇所は2行目の読み込むカスタム環境をenv3からenv4に変更した点のみとなります。

[ソース]

test4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import gym
from env4 import MyEnv

# 環境の生成
env = MyEnv()

# 環境リセット
state = env.reset()

total_reward = 0
while True:
# ランダム行動の取得
action = env.action_space.sample()
# 1ステップの実行
state, reward, done, info = env.step(action)
total_reward += reward
#print('state=', state, 'reward=', reward)
# 環境の描画
env.render()
print('reward:', reward, 'total_reward', total_reward)
print('-----------')
# エピソード完了
if done:
print('done')
break

ランダム実行のため毎回結果が異なります。
10回ほど実行した結果は下記のようになりました。

[結果]

  • 1回目
    total_reward -733
  • 2回目
    total_reward -1259
  • 3回目
    total_reward -1582
  • 4回目
    total_reward -2923
  • 5回目
    total_reward -479
  • 6回目
    total_reward -327
  • 7回目
    total_reward -785
  • 8回目
    total_reward -2065
  • 9回目
    total_reward -314
  • 10回目
    total_reward -235

トータル報酬は -2923から-235 の範囲となりました。

次回はStable BaselinesPPO2アルゴリズムを使って、強化学習を行ってみます。

カスタムGym環境作成(3) - ログ出力とグラフ表示

今回は、学習中のログをファイル出力し取得報酬がどのように変化していくのかをグラフ化して確認してみます。

ログファイル出力

前回学習を行ったソース(train3.py)を変更して、取得報酬をログファイルに出力します。

変更箇所はソースコメントで# 追加としています。

[ソース]

train3_log.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 警告を非表示
import os # 追加
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env3 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import PPO2
from stable_baselines.bench import Monitor # 追加

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

# 環境の生成
env = MyEnv()
env = Monitor(env, log_dir, allow_early_resets=True) # 追加
env = DummyVecEnv([lambda: env])

# モデルの生成
model = PPO2('MlpPolicy', env, verbose=1)

# モデルの学習
model.learn(total_timesteps=12800*10)

# モデルの保存
model.save('model3_log')

コンソールに出力される実行ログには平均エピソード長(ep_len_mean)平均報酬(ep_reward_mean)が追加で表示されるようになります。

[結果]

---------------------------------------
| approxkl           | 9.941752e-05   |
| clipfrac           | 0.0            |
| ep_len_mean        | 248            |←平均エピソード長
| ep_reward_mean     | -148           |←平均報酬
| explained_variance | -0.00303       |
| fps                | 1391           |
| n_updates          | 4              |
| policy_entropy     | 1.375106       |
| policy_loss        | -0.00035295845 |
| serial_timesteps   | 512            |
| time_elapsed       | 0.58           |
| total_timesteps    | 512            |
| value_loss         | 573.96643      |
---------------------------------------
--------------------------------------
| approxkl           | 2.3577812e-05 |
| clipfrac           | 0.0           |
| ep_len_mean        | 248           |
| ep_reward_mean     | -148          |
| explained_variance | 0.00585       |
| fps                | 1301          |
| n_updates          | 5             |
| policy_entropy     | 1.3737429     |
| policy_loss        | 3.933697e-05  |
| serial_timesteps   | 640           |
| time_elapsed       | 0.673         |
| total_timesteps    | 640           |
| value_loss         | 110.2445      |
--------------------------------------
--------------------------------------
| approxkl           | 5.579695e-06  |
| clipfrac           | 0.0           |
| ep_len_mean        | 186           |
| ep_reward_mean     | -85.2         |
| explained_variance | 0.00289       |
| fps                | 1362          |
| n_updates          | 6             |
| policy_entropy     | 1.372483      |
| policy_loss        | 4.9524475e-05 |
| serial_timesteps   | 768           |
| time_elapsed       | 0.771         |
| total_timesteps    | 768           |
| value_loss         | 568.43945     |
--------------------------------------
         :
        (略)
         :
--------------------------------------
| approxkl           | 6.53077e-08   |
| clipfrac           | 0.0           |
| ep_len_mean        | 9             |
| ep_reward_mean     | 92            |
| explained_variance | 1             |
| fps                | 1298          |
| n_updates          | 998           |
| policy_entropy     | 0.010556959   |
| policy_loss        | -8.293893e-05 |
| serial_timesteps   | 127744        |
| time_elapsed       | 94.1          |
| total_timesteps    | 127744        |
| value_loss         | 2.708827e-07  |
--------------------------------------
-------------------------------------
| approxkl           | 0.0059474623 |
| clipfrac           | 0.01171875   |
| ep_len_mean        | 9.02         |
| ep_reward_mean     | 92           |
| explained_variance | 0.998        |
| fps                | 1311         |
| n_updates          | 999          |
| policy_entropy     | 0.0095445085 |
| policy_loss        | -0.014048491 |
| serial_timesteps   | 127872       |
| time_elapsed       | 94.2         |
| total_timesteps    | 127872       |
| value_loss         | 0.026215255  |
-------------------------------------
--------------------------------------
| approxkl           | 5.6422698e-09 |
| clipfrac           | 0.0           |
| ep_len_mean        | 9.02          |←平均エピソード長
| ep_reward_mean     | 92            |←平均報酬
| explained_variance | 1             |
| fps                | 1291          |
| n_updates          | 1000          |
| policy_entropy     | 0.007281434   |
| policy_loss        | -6.94443e-05  |
| serial_timesteps   | 128000        |
| time_elapsed       | 94.3          |
| total_timesteps    | 128000        |
| value_loss         | 0.0016018704  |
--------------------------------------

最終的には平均報酬が92で安定していることがわかります。

また、logフォルダが作成されその中にmonitor.csvファイルが出力されます。
「r,l,t」はそれぞれ「報酬、エピソード長、経過時間」を意味しています。

monitor.csv
1
2
3
4
5
6
#{"t_start": 1621889575.6633413, "env_id": null}
r,l,t
-319,420,1.421082
24,77,1.460983
-104,205,1.627026
(略)

グラフ化

monitor.csvを読み込み、報酬をグラフ化します。

[ソース]

log_graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd
import matplotlib.pyplot as plt

# ログファイルの読み込み(報酬,エピソード,経過時間)
df = pd.read_csv('./logs/monitor.csv', names=['r', 'l', 't'])
df = df.drop(range(2)) # 先頭2行を排除

# 報酬グラフの表示
x = range(len(df['r']))
y = df['r'].astype(float)
plt.plot(x, y)
plt.xlabel('episode')
plt.ylabel('reward')
plt.show()

[結果]

学習はじめはマイナス報酬が大きくなっていますが、学習が進むにつれマイナス報酬が減っていき最終的にはプラス報酬で安定していることが見て取れます。

次回は、カスタムGym環境(env3.py)で定義したマップをもう少し複雑にしても、きちんと学習できるのかどうかを見ていきたいと思います。

カスタムGym環境作成(2) - PPO(PPO2)で学習

前回カスタム環境として、簡単なマップを作りスタート地点からゴール地点まで移動する環境を作成しました。

今回はStable BaselinesPPO(PPO2)アルゴリズムを使って、強化学習を行いそのカスタム環境を効率よく攻略してみます。

強化学習

カスタム環境を読み込み、PPO(PPO2)アルゴリズムで学習を行います。

PPO(PPO2)は計算量を削除するように改良された学習法で、使いやすさと優れたパフォーマンスのバランスがとれているためOpenAIのデフォルトの強化学習アルゴリズムとなっています。

学習したモデルはsaveメソッドでファイルに保存します。

[ソース]

train3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 警告を非表示
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
from env3 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import PPO2

# 環境の生成
env = MyEnv()
env = DummyVecEnv([lambda: env]) # ベクトル化環境でラップ

# モデルの生成
model = PPO2('MlpPolicy', env, verbose=1) # MlpPolicy:入力が特徴量の場合に適した方策

# モデルの学習
model.learn(total_timesteps=12800*10) # 訓練ステップ数を指定

# モデルの保存
model.save('model3') # 学習済みモデルをファイル保存

[結果(1部略)]

--------------------------------------
| approxkl           | 0.00015537896 |(新しい方策から古い方策へのKullback-Leibler発散尺度)
| clipfrac           | 0.0           |(クリップ範囲ハイパーパラメータが使用される回数の割合)
| explained_variance | 0.00605       |(誤差の分散)
| fps                | 276           |(1秒あたりのフレーム数)
| n_updates          | 1             |(更新回数)
| policy_entropy     | 1.3861711     |(方策のエントロピー)
| policy_loss        | -0.0009599341 |(方策の損失)
| serial_timesteps   | 128           |(1つの環境でのタイプステップ数)
| time_elapsed       | 0             |(経過時間)
| total_timesteps    | 128           |(全環境でのタイムステップ数)
| value_loss         | 113.650955    |(価値関数更新時の平均損失)
--------------------------------------
--------------------------------------
| approxkl           | 0.00017739396 |
| clipfrac           | 0.0           |
| explained_variance | -0.00457      |
| fps                | 1085          |
| n_updates          | 2             |
| policy_entropy     | 1.3849432     |
| policy_loss        | 0.0005420535  |
| serial_timesteps   | 256           |
| time_elapsed       | 0.463         |
| total_timesteps    | 256           |
| value_loss         | 332.47357     |
--------------------------------------
---------------------------------------
| approxkl           | 4.4801734e-05  |
| clipfrac           | 0.0            |
| explained_variance | -0.00433       |
| fps                | 1105           |
| n_updates          | 3              |
| policy_entropy     | 1.3839808      |
| policy_loss        | -0.00094374514 |
| serial_timesteps   | 384            |
| time_elapsed       | 0.584          |
| total_timesteps    | 384            |
| value_loss         | 112.30096      |
---------------------------------------
         :
        (略)
         :
--------------------------------------
| approxkl           | 0.004464049   |
| clipfrac           | 0.005859375   |
| explained_variance | 0.995         |
| fps                | 1260          |
| n_updates          | 998           |
| policy_entropy     | 0.005367896   |
| policy_loss        | -0.0047663264 |
| serial_timesteps   | 127744        |
| time_elapsed       | 96.8          |
| total_timesteps    | 127744        |
| value_loss         | 0.0663212     |
--------------------------------------
---------------------------------------
| approxkl           | 1.402045e-09   |
| clipfrac           | 0.0            |
| explained_variance | 1              |
| fps                | 1259           |
| n_updates          | 999            |
| policy_entropy     | 0.005131081    |
| policy_loss        | -2.0012958e-05 |
| serial_timesteps   | 127872         |
| time_elapsed       | 96.9           |
| total_timesteps    | 127872         |
| value_loss         | 0.0038208982   |
---------------------------------------
--------------------------------------
| approxkl           | 3.59563e-10   |
| clipfrac           | 0.0           |
| explained_variance | 1             |
| fps                | 1294          |
| n_updates          | 1000          |
| policy_entropy     | 0.0052420935  |
| policy_loss        | -7.759663e-06 |
| serial_timesteps   | 128000        |
| time_elapsed       | 97            |
| total_timesteps    | 128000        |
| value_loss         | 0.0014512216  |
--------------------------------------

学習済みモデルがmodel3.zipというファイル名で保存されます。

学習したモデルを使って実行

学習したモデルを読み込み、実行します。

[ソース]

play3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 警告を非表示
import warnings
warnings.simplefilter('ignore')
import tensorflow as tf
tf.get_logger().setLevel("ERROR")

import gym
#from env2 import MyEnv
from env3 import MyEnv

from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines import PPO2

# 環境の生成
env = MyEnv()
env = DummyVecEnv([lambda: env])

# モデルの読み込み
model = PPO2.load('model3')

# モデルのテスト
state = env.reset()
total_reward = 0
while True:
# 環境の描画
env.render()

# モデルの推論
action, _ = model.predict(state)

# 1ステップの実行
state, reward, done, info = env.step(action)
total_reward += reward
print('reward:', reward, 'total_reward', total_reward)
print('-----------')

print('')
# エピソード完了
if done:
# 環境の描画
print('total_reward:', total_reward)
break

[結果]

Loading a model without an environment, this model cannot be trained until it has a valid environment.
☆ 山 
 山G 
  山 
山   
reward: [-1.] total_reward [-1.]
-----------

S 山 
☆山G 
  山 
山   
reward: [-1.] total_reward [-2.]
-----------

S 山 
 山G 
☆ 山 
山   
reward: [-1.] total_reward [-3.]
-----------

S 山 
 山G 
 ☆山 
山   
reward: [-1.] total_reward [-4.]
-----------

S 山 
 山G 
  山 
山☆  
reward: [-1.] total_reward [-5.]
-----------

S 山 
 山G 
  山 
山 ☆ 
reward: [-1.] total_reward [-6.]
-----------

S 山 
 山G 
  山 
山  ☆
reward: [-1.] total_reward [-7.]
-----------

S 山 
 山G 
  山☆
山   
reward: [-1.] total_reward [-8.]
-----------

S 山 
 山G☆
  山 
山   
reward: [100.] total_reward [92.]
-----------

total_reward: [92.]

何回実行しても、最短でゴールに向かって進んでいっているのが分かります。きちんと学習できているようです。

適切な環境さえ用意すれば、いろいろな問題を確実に解いてしまう強化学習はやはりすごいですね。


次回は、学習中のログを出力し取得報酬がどのように変化しているのかをグラフ化して確認してみます。

カスタムGym環境作成(1) - ランダム実行

強化学習とは

強化学習とは、エージェント環境の状態に応じてどのように行動すれば報酬を多くもらえるかを求める手法です。

学習データなしに自身の試行錯誤のみで学習するのが特徴になります。

効率のいい学習アルゴリズムは日々進化・公開されており、それを理解するよりもうまく使いこなす方が重要ではないかと感じております。
(アルゴリズムを理解するために必要な呪文のような数式に挫折したという理由もあります😥)

学習アルゴリズムを使いこなすために、独自の学習環境・・・つまりカスタム環境をいろいろと作成できるようなったほうがよいのではないかと思い今回の記事を書くことにしました。

(環境構築等は省略します)

カスタムGym環境作成

今回は2次元マップを独自で作成し、スタート地点からゴール地点までたどり着く環境を作成してみます。

できるだけ単純なマップとするために、移動可能な平地移動できない山だけを並べてみました。

[ソース]

env3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# activate openai_gym
import sys

import gym
import numpy as np
import gym.spaces

class MyEnv(gym.Env):
FIELD_TYPES = [
'S', # 0: スタート
'G', # 1: ゴール
' ', # 2: 平地
'山', # 3: 山(歩けない)
'☆', # 4: プレイヤー
]
MAP = np.array([
[0, 2, 3, 2], # ■ 山 
[2, 3, 1, 2], #  山G 
[2, 2, 3, 2], #   山 
[3, 2, 2, 2], # 山   
])
MAX_STEPS = 50

def __init__(self):
super().__init__()
# action_space, observation_space, reward_range を設定する
self.action_space = gym.spaces.Discrete(4) # 行動空間 上下左右の4種類
self.observation_space = gym.spaces.Box( # 状態空間 地図を表す配列と最大値、最小値を指定
low=0,
high=len(self.FIELD_TYPES),
shape=self.MAP.shape
)
self.reset()

def reset(self):
# 各変数を初期化する
self.pos = self._find_pos('S')[0]
self.goal = self._find_pos('G')[0]
self.done = False
self.steps = 0
return self._observe()

def step(self, action):
# 1ステップ進める処理。
# 戻り値は observation, reward, done(ゲーム終了したか), info(追加の情報の辞書)
# 左上の座標を(0, 0)とする
if action == 0: # 右移動
next_pos = self.pos + [0, 1]
elif action == 1: # 左移動
next_pos = self.pos + [0, -1]
elif action == 2: # 下移動
next_pos = self.pos + [1, 0]
elif action == 3: # 上移動
next_pos = self.pos + [-1, 0]

if self._is_movable(next_pos):
self.pos = next_pos
moved = True
else:
moved = False

observation = self._observe()
reward = self._get_reward(self.pos, moved)
self.done = self._is_done()
return observation, reward, self.done, {}

def render(self, mode='console', close=False):
# コンソールに地図の状態を表示する
for row in self._observe():
for elem in row:
print(self.FIELD_TYPES[elem], end='')
print()

def _close(self):
pass

def _seed(self, seed=None):
pass

def _get_reward(self, pos, moved):
# 報酬を返す。
# - ゴールにたどり着くと 100 ポイント
# - 1ステップごとに-1ポイント(できるだけ短いステップでゴールにたどり着かせるため必要)
if moved and (self.goal == pos).all():
return 100
else:
return -1

def _is_movable(self, pos):
# マップの中にいるか、歩けない場所にいないかをチェック。
return (
0 <= pos[0] < self.MAP.shape[0]
and 0 <= pos[1] < self.MAP.shape[1]
and self.FIELD_TYPES[self.MAP[tuple(pos)]] != '山'
)

def _observe(self):
# マップにプレイヤーの位置を重ねる
observation = self.MAP.copy()
observation[tuple(self.pos)] = self.FIELD_TYPES.index('☆')
return observation

def _is_done(self):
# 最大で self.MAX_STEPS まで
if (self.pos == self.goal).all():
return True
elif self.steps > self.MAX_STEPS:
return True
else:
return False

# 特定のフィールドタイプのポジションを返す。
def _find_pos(self, field_type):
return np.array(list(zip(*np.where(self.MAP == self.FIELD_TYPES.index(field_type)))))

ランダム実行

上記で作成したカスタム環境を読み込み、ランダムで実行してみます。

[ソース]

test3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import gym
from env3 import MyEnv

# カスタム環境の生成
env = MyEnv()

# 環境リセット
state = env.reset()

total_reward = 0
while True:
# ランダム行動の取得
action = env.action_space.sample()
# 1ステップの実行
state, reward, done, info = env.step(action)
total_reward += reward
# 環境の描画
env.render()
print('reward:', reward, 'total_reward', total_reward)
print('-----------')
# エピソード完了
if done:
print('done')
break

結果は次のように表示されます。

星マーク(☆)が移動するプレイヤーを現しています。

[結果(1部略)]

☆ 山 
 山G 
  山 
山   
reward: -1 total_reward -1
-----------
☆ 山 
 山G 
  山 
山   
reward: -1 total_reward -2
-----------
S☆山 
 山G 
  山 
山   
reward: -1 total_reward -3
-----------
S☆山 
 山G 
  山 
山   
reward: -1 total_reward -4
-----------
S☆山 
 山G 
  山 
山   
reward: -1 total_reward -5
-----------
☆ 山 
 山G 
  山 
山   
reward: -1 total_reward -6
-----------
S 山 
☆山G 
  山 
山   
reward: -1 total_reward -7
-----------

(略)

-----------
S 山☆
 山G 
  山 
山   
reward: -1 total_reward -146
-----------
S 山 
 山G☆
  山 
山   
reward: -1 total_reward -147
-----------
S 山☆
 山G 
  山 
山   
reward: -1 total_reward -148
-----------
S 山☆
 山G 
  山 
山   
reward: -1 total_reward -149
-----------
S 山☆
 山G 
  山 
山   
reward: -1 total_reward -150
-----------
S 山 
 山G☆
  山 
山   
reward: -1 total_reward -151
-----------
S 山 
 山☆ 
  山 
山   
reward: 100 total_reward -51
-----------
done

ランダムでプレイヤーを移動させるので、結果は毎回変わりますがトータル報酬(total_reward)は-450~70の範囲でした。

次回はStable BaselinesPPO2アルゴリズムを使って、強化学習を行ってみます。