からっぽのしょこ

読んだら書く!書いたら読む!同じ事は二度調べ(たく)ない

4.2.1:GridWorldクラスの実装:評価と改善に関するメソッド【ゼロつく4のノート】

はじめに

 『ゼロから作るDeep Learning 4 ――強化学習編』の独学時のまとめノートです。初学者の補助となるようにゼロつくシリーズの4巻の内容に解説を加えていきます。本と一緒に読んでください。

 この記事は、4.2.1節の内容です。3×4マスのグリッドワールドのクラスについて確認します。

【前節の内容】

www.anarchive-beta.com

【他の記事一覧】

www.anarchive-beta.com

【この記事の内容】

4.2.1 GridWorldクラスの実装

 3×4マスのグリッドワールドのクラスGridWorldの内部で行われる処理と使い方を確認します。今回は、評価と改善に使うメソッドの処理を確認します。次回は、可視化に使う処理を確認します。

 利用するライブラリを読み込みます。

# 利用するライブラリ
import numpy as np
import matplotlib.pyplot as plt
import matplotlib


 実装済みのGridWorldクラスは、次のようにして読み込めます。

# 読み込み用のライブラリ
import sys

# フォルダパスを指定
sys.path.append('../deep-learning-from-scratch-4-master')

# 実装済みクラスを読み込み
from common.gridworld import GridWorld

 実装済みクラスの読み込みについては「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」を参照してください。

 GridWorldクラスのインスタンスを作成しておきます。

# インスタンスを作成
env = GridWorld()


・initメソッドとインスタンス変数

 インスタンスの作成時に実行される「初期化メソッド」の処理と、「インスタンス変数」の処理を確認します。

 行動番号を格納したリストと、行動番号がキーで行動内容が値のディクショナリを作成します。

# 行動番号を指定
action_space = [0, 1, 2, 3]

# 行動番号と行動内容の対応ディクショナリを指定
action_meaning = {0: 'UP', 1: 'DOWN', 2: 'LEFT', 3: 'RIGHT'}

 縦横方向に広がるグリッドワールドなので、エージェントは、上下左右の4つの行動を取ります。

 次のようにして、行動を順番に取り出せます。

# 行動を抽出
for action in action_space:
    print(action, action_meaning[action])
0 UP
1 DOWN
2 LEFT
3 RIGHT


 ランダムに行動を取るような場面でも同様です。

# ランダムに行動を生成
actions = np.random.choice(action_space, size=10)

# 行動を抽出
for action in actions:
    print(action, action_meaning[action])
1 DOWN
1 DOWN
2 LEFT
2 LEFT
1 DOWN
2 LEFT
0 UP
1 DOWN
2 LEFT
3 RIGHT

 (私の進捗状況ではまだ登場していないので、今後どういう風に使うのか分かってません。)

 グリッドワールドの設定用のNumPy配列とタプルを作成します。

# 各状態(マス)の報酬を指定
reward_map = np.array(
    [[0, 0, 0, 1.0], 
     [0, None, 0, -1.0], 
     [0, 0, 0, 0]]
)

# スタートの位置を指定
start_state = (2, 0)

# ゴールの位置を指定
goal_state = (0, 3)

# 壁の位置を指定
wall_state = (1, 1)

# 現在のエージェントの状態を初期化
agent_state = start_state

 各マス(状態)の報酬をreward_mapとして、配列に指定します。
 スタートの位置をstart_state、ゴールの位置をgoal_state、壁の位置をwall_stateとして、タプルに指定します。壁の位置と報酬がNoneの位置(インデックス)が対応します。
 エージェントの位置(現在の状態)をagent_stateとして、初期値をスタートの位置にします。

 グリッドワールドのサイズは、次のようにして得られます。

# 縦方向のマスの数を取得
height = len(reward_map)
print(height)

# 横方向のマスの数を取得
width = len(reward_map[0])
print(width)

# グリッドワールドの形状を取得
shape = reward_map.shape
print(shape)
3
4
(3, 4)

 reward_mapの0番目の軸の要素数が縦方向のマスの数、1番目の軸の要素数が横方向のマスの数です。

 以上が、初期化メソッドとインスタンス変数で行われる処理です。続いて、実装済みのクラスを試してみましょう。

 各状態に関する値は、インスタンス変数として保存されます。

# 各状態(マス)の報酬を出力
print(env.reward_map)

# スタートの位置を出力
print(env.start_state)

# ゴールの位置を出力
print(env.goal_state)

# 壁の位置を出力
print(env.wall_state)

# 現在の状態(エージェントの位置)を出力
print(env.agent_state)
[[0 0 0 1.0]
 [0 None 0 -1.0]
 [0 0 0 0]]
(2, 0)
(0, 3)
(1, 1)
(2, 0)


 グリッドワールドの形状に関する値も、インスタンスとして出力できます。

# 縦方向のマスの数を出力
print(env.height)

# 横方向のマスの数を出力
print(env.width)

# グリッドワールドの形状を出力
print(env.shape)
3
4
(3, 4)


 以上が、初期化メソッドとインスタンス変数の処理です。次は、全ての行動メソッドを確認します。

・actionsメソッド

 エージェントの全ての行動を出力する「全ての行動メソッド」の処理を確認します。

 行動番号は、インスタンス変数action_spaceに保存されています。

# 行動番号を出力
print(env.action_space)
[0, 1, 2, 3]


 これをメソッドにしたのがactions()です。

# 行動番号を出力
print(env.actions())
[0, 1, 2, 3]


 以上が、全ての行動メソッドの処理です。次は、状態のリセットメソッドを確認します。

・resetメソッド

 現在の状態を初期化して出力する「状態のリセットメソッド」の処理を確認します。

 現在の状態(エージェントの位置)は、インスタンス変数agent_stateに保存されていて、書き換えられます(遷移します)。

# 状態を指定
state = (0, 0)

# 現在の状態を変更
env.agent_state = state
print(env.agent_state)
(0, 0)

 状態の遷移についてはstepメソッドで確認します。

 agent_stateにスタート位置start_stateを代入することで、現在の状態を初期化できます。

# 現在の状態を初期化
env.agent_state = env.start_state
print(env.agent_state)
(2, 0)

 エージェントの位置をスタートの位置に戻すことを意味します。

 これをメソッドにしたのがreset()です。

# 現在の状態を変更
env.agent_state = (0, 0)
print(env.agent_state)

# 初期状態に戻して出力
state = env.reset()
print(state)
print(env.agent_state)
(0, 0)
(2, 0)
(2, 0)

 現在の状態を初期化して、初期状態を出力します。

 以上が、状態のリセットメソッドの処理です。次は、全ての状態メソッドを確認します。

・statesメソッド

 グリッドワールドの全ての状態(全てのマスのインデックス)を順番に出力する「全ての状態メソッド」の処理を確認します。

 states()では、yieldを使って繰り返し値を出力します。まずは、0からn-1の整数を出力する関数を作成して、returnyeildの違いを確認します。

 returnを使って、次のように定義してみます。

# returnにより出力する関数
def f_return(n=3):
    for i in range(n):
        return i


 実装した関数を実行します。

# 実行
print(f_return())
0

 最初の値しか出力されません。これは、returnが実行されると関数内部の処理が終了するためです。

 続いて、yieldを使って定義してみます。

# yieldにより出力する関数
def f_yield(n=3):
    for i in range(n):
        yield i


 そのまま実行すると、generatorオブジェクトが出力されます。

# そのまま実行
print(f_yield())
<generator object f_yield at 0x0000027062A4EAC0>


 generatorオブジェクトの説明は省略しますが、次のようにして使えます。

# for文を使って実行
for i in f_yield():
    print(i)
0
1
2

 0からn-1の整数を出力できました。for文のような繰り返し処理の中で、順番に値が出力されます。

 続いて、メソッド内部の処理を確認します。

 縦と横のサイズを使って、全ての状態を作成します。

# 全ての状態を作成
for h in range(env.height):
    for w in range(env.width):
        state = (h, w)
        print(state)
(0, 0)
(0, 1)
(0, 2)
(0, 3)
(1, 0)
(1, 1)
(1, 2)
(1, 3)
(2, 0)
(2, 1)
(2, 2)
(2, 3)

 縦方向のマス番号(y軸の値)をh、横方向のマス番号(x軸の値)をwとして、各マス番号(2次元配列のインデックス)をタプルに格納して出力します。

 以上が、全ての状態メソッドで行われる処理です。続いて、実装済みのクラスを試してみましょう。

 states()メソッドで全ての状態を出力します。

# 各状態を順番に出力
for state in env.states():
    print(state)
(0, 0)
(0, 1)
(0, 2)
(0, 3)
(1, 0)
(1, 1)
(1, 2)
(1, 3)
(2, 0)
(2, 1)
(2, 2)
(2, 3)


 例えば、出力した状態をインデックスとして使って、対応する報酬を取り出せます。

# 各状態の報酬を順番に出力
for state in env.states():
    print('state', state, ': reward', env.reward_map[state])
state (0, 0) : reward 0
state (0, 1) : reward 0
state (0, 2) : reward 0
state (0, 3) : reward 1.0
state (1, 0) : reward 0
state (1, 1) : reward None
state (1, 2) : reward 0
state (1, 3) : reward -1.0
state (2, 0) : reward 0
state (2, 1) : reward 0
state (2, 2) : reward 0
state (2, 3) : reward 0


 以上が、全ての状態メソッドの処理です。次は、次の状態メソッドを確認します。

・next_stateメソッド

 エージェントの行動により遷移した次の状態を出力する「次の状態メソッド」の処理を確認します。

 まずは、4方向への移動に対応した値を作成します。

# 行動に対応した移動量を設定
action_move_map = [(-1, 0), (1, 0), (0, -1), (0, 1)]
print(action_move_map)
[(-1, 0), (1, 0), (0, -1), (0, 1)]

 現在の位置から左のマスに移動するには、縦軸方向には変化せず(0変化し)、横軸方向に-1変化します。この変化を(0, -1)で表します。右に移動する場合は(0, 1)です。
 ただし、上下の移動については直感的でない変化になります。これは、図4-9の座標系や、報酬の配列reward_mapのインデックスに対応するためです。

# 報酬マップを確認
print(env.reward_map)
[[0 0 0 1.0]
 [0 None 0 -1.0]
 [0 0 0 0]]

 上のマスに移動するには、縦軸方向の値が-1変化します。下に移動する場合は+1変化します。
 これら4つの変化量をリストに格納します。

 リストに格納する順番は、行動内容action_meaningに対応します。

# 行動番号と行動内容の対応を確認
print(env.action_meaning)
{0: 'UP', 1: 'DOWN', 2: 'LEFT', 3: 'RIGHT'}

 上下左右の順です。

 準備ができたので、行動を指定して、次の状態を計算します。

# エージェントの位置(現在の状態)を指定
state = (2, 0)

# 行動を指定
#action = 0 # 上に行動(移動できる)
#action = 1 # 下に行動(移動できない)
action = 2 # 左に行動(移動できない)
#action = 3 # 右に行動(移動できる)

# 行動に対応した変化量を抽出
move = action_move_map[action]
print(move)

# 行動後の位置(次の状態)の候補を計算
next_state = (state[0] + move[0], state[1] + move[1])
print(next_state)
(0, -1)
(2, -1)

 行動に対応した変化量をaction_move_mapから取り出して、現在の状態に足します。
 ただし、壁が存在するため、行動の通りに移動するとは限りません。よって、この時点のnext_stateは、次の状態の候補と言えます。

 そこで、next_stateが、グリッドワールド内のマスであるか、壁のマスでないかを判定します。

# 次の状態のx軸・y軸の値を抽出
ny, nx = next_state
print(ny)
print(nx)

# グリッドワールドの外に移動する場合
if nx < 0 or nx >= env.width or ny < 0 or ny >= env.height:
    # 元の位置のまま
    next_state = state

# 壁のマスに移動する場合
elif next_state == env.wall_state:
    # 元の位置のまま
    next_state = state
print(next_state)
2
-1
(2, 0)

 次の状態の候補next_stateからy軸の値nyとx軸の値nxを取り出して、0より小さい、または横幅width・高さheight以上であれば壁の外なので、元の状態stateに変更します。また、壁のマスwall_stateでも元の状態にします。

 以上が、次の状態メソッドで行われる処理です。続いて、実装済みのクラスを試してみましょう。

 next_state()に現在の状態と行動を指定して、次の状態を出力します。

# 現在の状態を初期化
state = env.reset()
print(state)

# 行動を指定
#action = 0 # 上に行動(移動できる)
#action = 1 # 下に行動(移動できない)
action = 2 # 左に行動(移動できない)
#action = 3 # 右に行動(移動できる)

# 次の状態を出力
next_state = env.next_state(state, action)
print(next_state)
(2, 0)
(2, 0)


 以上が、次の状態メソッドの処理です。次は、報酬メソッドを確認します。

・rewardメソッド

 指定した状態に対応する報酬を出力する「報酬メソッド」の処理を確認します。

 各状態の報酬(NumPy配列)reward_mapに状態(インデックス)を指定すると、対応する報酬を抽出できます。

# 状態を指定
#state = (0, 0) # 通常マス
state = (0, 3) # リンゴの位置
#state = (1, 3) # 爆弾の位置

# 報酬を出力
reward = env.reward_map[state]
print(reward)
1.0


 これをメソッドにしたのがreward()です。

# ダミーの状態を作成
state = '_'

# ダミーの行動を作成
action = '_'

# 状態を指定
next_state = (0, 3)

# 報酬を取得
reward = env.reward(state, action, next_state)
print(reward)
1.0

 報酬メソッドは、報酬関数(数式での表記)$r(s, a, s')$に対応させるために、現在の状態state・行動action・次の状態next_stateの3つの引数を持ちますが、処理に利用するのはnext_stateのみです。

 以上が、報酬メソッドの処理です。次は、ステップメソッドを確認します。

・stepメソッド

 エージェントの行動により、報酬を受け取り状態を遷移する「ステップメソッド」の処理を確認します。

 行動を指定して、状態を遷移(エージェントを移動)し、報酬と次の状態を出力します。

# 現在の状態を指定
state = (0, 2)

# 現在の状態を設定
env.agent_state = state
print(env.agent_state)

# 行動を指定
action = 3

# 次の状態を出力
next_state = env.next_state(state, action)
print(next_state)

# 報酬を抽出
reward = env.reward_map[next_state]
print(reward)

# 現在の状態を更新
env.agent_state = next_state
print(env.agent_state)
(0, 2)
(0, 3)
1.0
(0, 3)

 現在の状態と行動をnext_state()に指定して、次の状態next_stateを出力します。
 next_stateをインデックスとして使って、次の状態の報酬をreward_mapから抽出します。
 現在の状態agent_stateの値を、次の状態の値に変更します。

 この例の問題設定では、ゴールに辿り着くとエピソードが終了します。そのため、次の状態がゴールの位置なのかを判定します。

# ゴールしたかを判定
done = (next_state == env.goal_state)
print(done)
True

 ゴールであればTrue、ゴールでなければFalseになります。

 以上が、ステップメソッドで行われる処理です。続いて、実装済みのクラスを試してみましょう。

 step()に行動を指定して、次の状態・報酬・ゴールかの判定結果を出力します。

# 現在の状態を設定
env.agent_state = (0, 2)
print(env.agent_state)

# 行動を指定
action = 3

# 1ステップの結果を出力
next_step, reward, done = env.step(action)
print(next_step)
print(reward)
print(done)
print(env.agent_state)
(0, 2)
(0, 3)
1.0
True
(0, 3)

 報酬を受け取り、状態が遷移(エージェントが移動)しました。

 続いて、複数ステップを処理してみます。

# 状態を初期化
state = env.reset()
print('start state', state)

# 行動を指定
actions = [3, 3, 0, 2, 3, 3, 0]

# ステップごとに処理
for action in actions:
    # 報酬を得て状態を遷移
    next_step, reward, done = env.step(action)
    print(
        'action', env.action_meaning[action], ':', 
        'state', next_step, ',', 
        'reward', reward, ',', 
        'goal', done
    )
start state (2, 0)
action RIGHT : state (2, 1) , reward 0 , goal False
action RIGHT : state (2, 2) , reward 0 , goal False
action UP : state (1, 2) , reward 0 , goal False
action LEFT : state (1, 2) , reward 0 , goal False
action RIGHT : state (1, 3) , reward -1.0 , goal False
action RIGHT : state (1, 3) , reward -1.0 , goal False
action UP : state (0, 3) , reward 1.0 , goal True


 以上が、ステップメソッドの処理です。次からは、可視化に関するメソッドを確認します。

参考文献


おわりに

 思ったより大変でした。でもよく理解できたと思います。最後に書くのもなんですが、各メソッドを使うタイミングで確認するぐらいでいいのではないでしょうか。

 投稿日の前日に公開された新MVをどうぞ♪


  • 2022/10/24

 「render_qメソッド」について追加する際に「render_vメソッド」の内容を修正して「評価と改善に関するメソッド」から記事を分割しました。

【次節の内容】

www.anarchive-beta.com