カスタム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アルゴリズムを使って、強化学習を行ってみます。

ブロックチェーン - ブルーム・フィルタ

ブルーム・フィルタの役割はプライバシー保護

ビットコインは不特定多数の参加者によって利用されているネットワークなので、参加者のプライバシー保護のため、個人を特定されないように工夫されています。

ブルーム・フィルタを使えば、ビットコイン・アドレスがネットワーク上に送信されないため、個人のウォレット・アドレスにも結びつかず、ビットコインを盗み取ろうとする人にアドレスが漏えいしません

ただし検索パターンで取引の検証を行うため、確率的な検証しかできませんし、問い合わせたフルノードのブロックチェーンか改ざんされているかもしれません。

こうした問題に対応するため、ランダムに選択した複数のフル・ノードに問い合わせて集めたマークル・パスを比較することで、目的のトランザクションが特定のブロックに記録されていることを検証できます。

正確性とプライバシー保護のレベル

ブルーム・フィルタと同じハッシュ関数を使えば、特定の入力に対して同じハッシュ値が出力されますので、どのノードで計算しても同じ結果がえられます。

正確性とプライバシー保護のレベルは、ブルーム・フィルタの長さハッシュ関数の個数を調整することで変えられます。

正確性が高まりすぎると特定のビットコイン・アドレスがウォレット内にあることが知られてしまい、プライバシー保護を高めると多数のビットコイン・アドレスがにマッチしてしまい特定できなくなります。

また、特定の検証は相手のビットコイン・アドレスを知っている前提で行われます。

このアドレスを知らないと、理論上送金が確定できず取引できません。

そのため第三者の取引について調べようとすると、フル・ノードを構築してブロックに含まれるトランザクションを読み解く必要があります。

ブロック生成時の指標 - ハッシュパワーとハッシュレート

ハッシュパワーとハッシュレート

マイニングには、ハッシュパワーとハッシュレートという指標があります。

  • ハッシュパワー
    1ブロックの生成に要した時間。
  • ハッシュレート
    1秒間にハッシュ関数で算出されたハッシュ値の数。

ビットコインのハッシュパワーは理論上、1ブロックの生成に10分かかるとしたら1時間で6ブロック生成されることになります。

しかし、実際は1時間ちょうどではなくかなり時間が前後します。

ブロック生成性能の比較

ビットコイン以外の仮想通貨や仮想通貨以外の用途で使われるブロックチェーンでも、ブロック生成の指標としてハッシュパワーとハッシュレートが利用できます。

マイニング・マシーンのブロック生成性能を比較する時によく利用されます。

ただし、これらの指標は異なる仮想通貨やブロック生成アルゴリズム間の比較に利用することはできません。

ブロックチェーン - アニメーションでメカニズムを可視化

ブロックが生成されてブロックチェーンになることを理屈では分かっていても、なかなかイメージしにくいかもしれません。

そのような時はブロックチェーンを可視化するサイトが便利です。

ビットコインのマイニング・トランザクション、ブロックなどの情報を確認できるサイトがあります。

基本的にブロックチェーンを参照したり、マイニング中のブロックの中身をアニメーションにして表示したりしているだけです。

誰でもブロックチェーンの中身を参照することができるのも分散管理台帳の特徴です。

ウェブ・アプリケーション開発のスキルがあればこうしたサイトを自作することも可能です。

ブロックがつながる様子が分かるchainFlyer

ビットコインの多数のトランザクションがブロックとしてマイニングされる様子をアニメーションで確認できるのがchainFlyerです。

chainFlyer

ブロックが生成されてブロックチェーンに追加される様子をイメージ画像でみることができます。

実際のトランザクションがどれくらい生成されているのかをイメージしやすいかと思います。

見ていて面白いBITBonkers!

BITBonkers!でもトランザクションを可視化できます。

BITBonkers!

キューブがブロックでボールがトランザクションになり、サイズが送金額になります。

参加ノードをリアルタイムに把握できるBitnodes

Bitnodesはビットコイン・ネットワークに参加しているノードをリアルタイムに確認できます。

Bitnodes

IPアドレス、国名や都市名を見ることができます。

米国や欧州が特に活発にビットコインを利用していることが見て取れます。

ブロックの中を詳しく調べられるBLOCHCHAIN

特定のブロック番号の詳しい情報を調べたいのであればBLOCHCHAINが便利です。

サイトにアクセスして、右上にあるサーチにブロック番号を入力して検索することができます。

BLOCHCHAIN

概要とハッシュ項目の情報がブロック・ヘッダに相当し、下にあるトランザクション番号が個々の取引履歴に相当します。

ブロック・ヘッダ(概要とハッシュの項目)には前後のブロックのハッシュ値、ブロック高、中継所、難易度、ノンス、バージョンなどのたくさんの情報が見れます。