この記事は、これから投稿予定の強化学習関連記事で利用する環境について解説します。
理論の説明とかはしないです。
pythonでの実装を通して理解していきます。
あまりライブラリを使わずに作成します。(numpyは使うよ)
Gymnasiumを参考に設計しているので、ある程度の互換性があります。
今回のシリーズは初学者向けなので環境を自作しますが、今後の学習では、様々な環境を用意してくれるgymnasiumというライブラリを利用することをおすすめします
準備
今回使うライブラリです。
import numpy as np
from time import sleep
from IPython.display import clear_output
import random
numpy: 行列計算を簡単に行うライブラリ
time.sleep: n秒待つ関数(ループとかで1秒おきにしたいときに使える)
Ipython.display.clear_output: ipynbの出力を消す関数
環境の設計
クラスに必要な属性とメソッド
今回作成する環境は、迷路です。
ゲーム機で「迷路脱出ゲーム」を遊んでいる状況を考えてみてください。
遊ぶために必要なものは何でしょうか?
以下の要素が必要そうですね。
- 迷路のマップ
- プレイヤーの位置
- プレイヤーの移動機能
- マップやプレイヤーの確認機能
そのため、今回実装する環境クラスは以下のように設計すると良さそうです。
属性
- マップ
- プレイヤーの位置
- 取れる行動
メソッド
- プレイヤーの移動(とれる行動から選択)
- マップ(プレイヤーの位置情報を含む)の確認
ここで、強化学習は何度も迷路脱出のシュミレーションを繰り返すため、初期化を簡単にできてほしいです。
したがって、以下のように属性とメソッドを追加します。
属性
- マップの初期状態
- マップ
- プレイヤーの位置
- 取れる行動
メソッド
- リセット
- プレイヤーの移動(とれる行動から選択)
- マップ(プレイヤーの位置情報を含む)の確認
動作の流れ
実際にどのように動作すればいいでしょうか?
リセットの機能は、マップを初期状態で上書きすればできそうです。
プレイヤーの移動について考えてみましょう。
例えば、右に行きたい場合は、座標のx軸に+1すれば良さそうです。
これは、現在の座標(x, y)に(1,0)を加算して、その結果の(x+1,y)を次の座標とすれば同じ操作をすることができます。
今後強化学習をすることに備え、次の座標に応じて報酬などの情報を返すようにしておきましょう。
一般に、プログラミングでは座標を(y, x)の順で表現します。
また、y座標が小さい点は上側にあり、大きい点は下側にあります。
そのためコード上では、下に移動するときは、(1, 0)を加算することになります。
理由は、多次元配列の生成・指定方法にあるのですが、長くなるので説明は省きます。
マップの確認はどう実装するといいでしょうか?
マップの確認方法はたくさんありますが、今回は簡単に実装できる以下の方法を用います。
- マップを複製し、そこにプレイヤーの位置などを書き込んでいきます
- マップの左上から読み込んでいき、対応した文字列に変換していきます
- 全て変換し終えたら文字列全体を表示します
これによって、人間が認識しにくい数値の情報を、認識しやすい文字列形式に変換できました。
アスキーアートみたいになります
環境の実装
実装した環境クラスの全体コードです。
今回マップは変化しませんが、今後は変化をつけていく予定なので、マップの初期化を行っています。
また、今後スムーズにgymnasiumに移行できるように、引数や返値をgymnasiumに似せています。
class Env:
def __init__(self, initial_map, with_view=False):
# 0: 通路, 1: 壁, 2: 開始地点, 3: ゴール
self.initial_map = initial_map
self.h, self.w = self.initial_map.shape
self.reset()
self.action_space = [0, 1, 2, 3]
self.action_n = len(self.action_space)
self.action_map = {
0: np.array([-1, 0]), # 'up'
1: np.array([1, 0]), # 'down'
2: np.array([0, -1]), # 'left'
3: np.array([0, 1]), # 'right'
}
if with_view:
print("map")
print(f"shape: {self.h}x{self.w}")
print("start position: ", np.argwhere(self.map == 2)[0])
print("goal position: ", np.argwhere(self.map == 3)[0])
print("action")
print("""
0 up(-1, 0)
↑
2 left(0, -1) ←[]→ 3 right(0, 1)
↓
1 down(1, 0)
""")
def reset(self):
self.map = self.initial_map.copy()
# 開始始点をmapから読み取る
self.agent_position = np.argwhere(self.map == 2)[0]
# gymnasiumしぐさ
observation = self.agent_position
info = None
return observation, info
def step(self, action: int):
motion = self.action_map[action]
# 進んだ先の座標を計算
next = self.agent_position + motion
# 範囲外に出ると反対側に移動する
next = next[0]%self.h, next[1]%self.w
# 報酬計算
if self.map[next[0], next[1]] in [0, 2]: # 通路
reward = -1
terminated = False
elif self.map[next[0], next[1]] == 1: # 壁
reward = -10
terminated = False
next = self.agent_position # # 壁に当たったら元の位置に戻す
elif self.map[next[0], next[1]] == 3: # ゴール
reward = 1000
terminated = True
else:
raise ValueError("invalid map element") # 0-3以外はないはず
self.agent_position = next
# gymnasiumしぐさ
observation = self.agent_position
truncated = False
info = None
return observation, reward, terminated, truncated, info
def view(self, override=True):
map = self.map.copy()
map[self.agent_position[0], self.agent_position[1]] = 9
view_str = ""
for i in range(self.h):
view_str += '\n'
for j in range(self.w):
if map[i, j] == 9: # agent
view_str += " A "
elif map[i, j] == 2: # 開始地点
view_str += " S "
elif map[i, j] == 3: # ゴール
view_str += " G "
elif map[i, j] == 1: # 壁
view_str += "[ ]"
elif map[i, j] == 0: # 通路
view_str += " "
else:
view_str += "err"
if override:
clear_output() # これのおかげでパラパラ漫画になる
print(view_str)
gymnasium風にするため、余分に返値を増やしましたが、中身は以下のようになることを想定しています。
observation: 環境の観測(今回はエージェントのx軸, y軸)
reward: 行動による報酬
terminated: ゴールなどによる正常終了
truncated: 時間制限などによる異常終了
info: デバッグ情報
observationはnextからではなく、envの属性であるself.agent_positionからとった方がそれっぽいと思って今回のような実装にしています。
また、actionはエージェントやユーザが利用しやすいインデックスとなっており、motionが実際のベクトルとなっています。
actionとmotionの関係は、ユーザIDとユーザ名みたいなものですね。
ユーザIDとユーザ名を結びつけるユーザリストと同じ役割をaction_mapが担っています。
使用例
環境の作成はこんな感じにします。
# 0: 通路, 1: 壁, 2: 開始地点, 3: ゴール
easy_map = np.array([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 2, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 0, 3, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
])
hard_map = np.array([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 2, 1, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 3, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
])
env = Env(hard_map, with_view=True)
こんなマップができます。
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ] A [ ] [ ] [ ]
[ ] [ ] [ ][ ] [ ]
[ ] [ ] [ ] [ ] [ ]
[ ] [ ] [ ] [ ] [ ]
[ ] [ ][ ] [ ]
[ ] [ ] [ ] [ ] [ ]
[ ] [ ][ ][ ] [ ][ ] [ ]
[ ] [ ] [ ] G [ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]
動かすときはこんな感じです。
actions = [1, 1, 1, 1, 3, 3, 3, 3, 0, 0, 0, 0, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1]
env.reset()
for action in actions:
env.step(action)
env.view()
sleep(0.5)
Aが動いてゴールに向かう様子をパラパラ漫画で見れると思います。
おまけ
今回の環境はgymnasium互換になるように心がけて設計しました。
pymnasiumの公式サイトにあるサンプルコードに似せて書くと以下のようになります。
# Initialise the environment
env = Env(easy_map)
observation, info = env.reset()
env.view()
for _ in range(1000):
# this is where you would insert your policy
action = random.choice(env.action_space)
# step (transition) through the environment with the action
# receiving the next observation, reward and if the episode has terminated or truncated
observation, reward, terminated, truncated, info = env.step(action)
env.view()
# If the episode has ended then we can reset to start a new episode
if terminated or truncated:
observation, info = env.reset()
sleep(0.3)