からっぽのしょこ

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

6.5:サンプルモデル版のQ学習【ゼロつく4のノート】

はじめに

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

 この記事は、6.5節の内容です。サンプリング版のQ学習を実装します。

【前節の内容】

www.anarchive-beta.com

【他の記事一覧】

www.anarchive-beta.com

【この記事の内容】

6.5 分布モデルとサンプルモデル

 ここまでは、分布モデルで実装しました。この節では、サンプルモデルで実装することを考えます。

6.5.2 サンプルモデル版のQ学習

 6.4節で実装したQ学習をサンプルモデルで実装します。

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

# ライブラリを読み込み
import numpy as np
from collections import defaultdict


 また、3×4マスのグリッドワールドのクラスGridWorldを読み込みます。

# 実装済みのクラスと関数を読み込み
import sys
sys.path.append('../deep-learning-from-scratch-4-master')
from common.gridworld import GridWorld
from common.utils import argmax, greedy_probs

 実装済みクラスの読み込みについては1巻の3.6.1項、GridWorldクラスについては4.2.1項、argmax関数とgreedy_probs関数については5.4.3項を参照してください。

処理の確認

 サンプルモデル版のQlearningAgentクラスのget_actionメソッドの内部で行う処理を確認します。他のメソッドについては6.4.3項を参照してください。

 例として、ランダムな値の行動価値関数を作成しておきます。

# (仮の)現在の状態を設定
state = (1, 2)

# 行動の種類数を指定
action_size = 4

# (仮の)行動価値関数を作成
Q = {(state, action): np.random.rand() for action in range(action_size)}
print(list(Q.keys()))
print(np.round(list(Q.values()), 3))
[((1, 2), 0), ((1, 2), 1), ((1, 2), 2), ((1, 2), 3)]
[0.363 0.102 0.08  0.904]

 現在の状態stateにおける上下左右の4つの行動に対する値をディクショナリに格納します。

 行動価値が最大の行動を抽出します。

# 現在の状態の全ての行動価値を取得
qs = [Q[state, action] for action in range(action_size)]
print(np.round(qs, 3))

# 行動価値が最大の行動を取得
max_action = np.argmax(qs)
print(max_action)
[0.363 0.102 0.08  0.904]
3

 リスト内包表記を使って、行動価値関数Qから状態stateにおける全ての行動価値を取り出してqsとします。
 qsのインデックスが行動番号に対応するので、np.argmax()で最大値のインデックスを取得します。
 これがgreedy法における処理です。

 続いて、ε-greedy法により行動を決定します。

# ランダムに行動する確率を指定
epsilon = 0.1
#epsilon = 0.9

# ε-greedy法により行動を決定:式(6.11)
if np.random.rand() < epsilon:
    # ランダムに行動を出力
    action = np.random.choice(action_size)
else:
    # 現在の状態の全ての行動価値を取得
    qs = [Q[state, a] for a in range(action_size)]

    # 行動価値が最大の行動を取得
    action = np.argmax(qs)
print(action)
3

 0から1の一様乱数をnp.random.rand()を生成して、ランダムに行動する確率epsilonと比較します。epsilonより小さければ($\epsilon$未満の確率で)、np.random.choice()でランダムな行動を出力します。epsilon以上であれば($1-\epsilon$の確率で)、行動価値が最大となる行動を出力します。

 ただし、np.argmax()は、値が同じだと最小のインデックスを返すのでした。

# (仮の)行動価値のリストを作成
qs = [0, 0, -1, 0]

# 行動価値が最大の行動を抽出
print(np.argmax(qs))
0


 そこで、argmax()を使います。

# 繰り返し行動価値が最大の行動を抽出
for _ in range(10):
    print(argmax(qs))
0
3
3
1
3
3
3
0
3
1

 値が同じインデックスからランダムに返します。
 (本では紹介されていませんが、utils.pyから読み込むgreedy_probs()の内部では、np.argmax()ではなくargmax()が使われています。QLearningAgentクラスでも、argmax()を使わないと上にばかりに進もうとするエージェントになります。)

 以上が、サンプリング版のQ学習による方策制御を行うエージェントの処理です。

実装

 処理の確認ができたので、Q学習におけるサンプルモデル版のエージェントをクラスとして実装します。

# サンプリング版Q学習によるエージェントの実装
class QLearningAgent:
    # 初期化メソッドの定義
    def __init__(self):
        # パラメータを指定
        self.gamma = 0.9 # 収益の計算用の割引率
        self.alpha = 0.8 # 状態価値の計算用の学習率
        self.epsilon = 0.1 # ランダムに行動する確率
        self.action_size = 4 # 行動の種類数
        
        # オブジェクトを初期化
        self.Q = defaultdict(lambda: 0) # 行動価値関数
    
    # 行動メソッドの定義
    def get_action(self, state):
        # ε-greedy法により行動を決定:式(6.11)
        if np.random.rand() < self.epsilon:
            # ランダムに行動を出力
            return np.random.choice(self.action_size)
        else:
            # 現在の状態の行動価値を取得
            qs = [self.Q[state, a] for a in range(self.action_size)]
            
            # 行動価値が最大の行動を出力
            #return np.argmax(qs)
            return argmax(qs)
    
    # 更新メソッドの定義
    def update(self, state, action, reward, next_state, done):
        # ゴールの場合
        if done:
            # 次の状態・行動の行動価値の最大値を0に設定
            next_q_max = 0
        # ゴール以外の場合
        else:
            # 次の状態・行動の行動価値を取得
            next_qs = [self.Q[next_state, a] for a in range(self.action_size)]
            
            # 次の状態・行動の行動価値の最大値を取得
            next_q_max = max(next_qs)
        
        # 現在の行動価値関数を更新:式(6.14)
        target = reward + self.gamma * next_q_max
        self.Q[state, action] += (target - self.Q[state, action]) * self.alpha


 実装したクラスを試してみましょう。

 環境(グリッドワールド)とエージェントのインスタンスを作成して、1エピソードの処理を行います。

# 環境・エージェントのインスタンスを作成
env = GridWorld()
agent = QLearningAgent()

# 行動の表示用のリストを作成
arrows = ['↑', '↓', '←', '→']

# 最初の状態を設定
state = env.start_state

# 時刻(試行回数)を初期化
t = 0

# 1エピソードのシミュレーション
while True:
    # 時刻をカウント
    t += 1

    # ε-greedy法により行動を決定
    action = agent.get_action(state)

    # サンプルデータを取得
    next_state, reward, done = env.step(action)

    # 現在の状態・行動の行動価値関数を更新:式(6.14)
    agent.update(state, action, reward, next_state, done)

    # サンプルデータを表示
    print(
        't=' + str(t) + 
        ', S_t=' + str(state) + 
        ', A_t=' + arrows[action] + 
        ', S_t+1=' + str(next_state) + 
        ', R_t=' + str(reward)
    )
    
    # ゴールに着いたらエピソードを終了
    if done:
        break

    # 状態を更新
    state = next_state
t=1, S_t=(2, 0), A_t=←, S_t+1=(2, 0), R_t=0
t=2, S_t=(2, 0), A_t=←, S_t+1=(2, 0), R_t=0
t=3, S_t=(2, 0), A_t=→, S_t+1=(2, 1), R_t=0
t=4, S_t=(2, 1), A_t=↓, S_t+1=(2, 1), R_t=0
t=5, S_t=(2, 1), A_t=←, S_t+1=(2, 0), R_t=0
t=6, S_t=(2, 0), A_t=↑, S_t+1=(1, 0), R_t=0
t=7, S_t=(1, 0), A_t=↑, S_t+1=(0, 0), R_t=0
t=8, S_t=(0, 0), A_t=↓, S_t+1=(1, 0), R_t=0
t=9, S_t=(1, 0), A_t=→, S_t+1=(1, 0), R_t=0
t=10, S_t=(1, 0), A_t=←, S_t+1=(1, 0), R_t=0
t=11, S_t=(1, 0), A_t=→, S_t+1=(1, 0), R_t=0
t=12, S_t=(1, 0), A_t=↑, S_t+1=(0, 0), R_t=0
t=13, S_t=(0, 0), A_t=↑, S_t+1=(0, 0), R_t=0
t=14, S_t=(0, 0), A_t=←, S_t+1=(0, 0), R_t=0
t=15, S_t=(0, 0), A_t=↓, S_t+1=(1, 0), R_t=0
t=16, S_t=(1, 0), A_t=←, S_t+1=(1, 0), R_t=0
t=17, S_t=(1, 0), A_t=↓, S_t+1=(2, 0), R_t=0
t=18, S_t=(2, 0), A_t=→, S_t+1=(2, 1), R_t=0
t=19, S_t=(2, 1), A_t=↓, S_t+1=(2, 1), R_t=0
t=20, S_t=(2, 1), A_t=←, S_t+1=(2, 0), R_t=0
t=21, S_t=(2, 0), A_t=↑, S_t+1=(1, 0), R_t=0
t=22, S_t=(1, 0), A_t=↓, S_t+1=(2, 0), R_t=0
t=23, S_t=(2, 0), A_t=↓, S_t+1=(2, 0), R_t=0
t=24, S_t=(2, 0), A_t=↓, S_t+1=(2, 0), R_t=0
t=25, S_t=(2, 0), A_t=→, S_t+1=(2, 1), R_t=0
t=26, S_t=(2, 1), A_t=→, S_t+1=(2, 2), R_t=0
t=27, S_t=(2, 2), A_t=→, S_t+1=(2, 3), R_t=0
t=28, S_t=(2, 3), A_t=→, S_t+1=(2, 3), R_t=0
t=29, S_t=(2, 3), A_t=↓, S_t+1=(2, 3), R_t=0
t=30, S_t=(2, 3), A_t=→, S_t+1=(2, 3), R_t=0
t=31, S_t=(2, 3), A_t=↑, S_t+1=(1, 3), R_t=-1.0
t=32, S_t=(1, 3), A_t=←, S_t+1=(1, 2), R_t=0
t=33, S_t=(1, 2), A_t=↑, S_t+1=(0, 2), R_t=0
t=34, S_t=(0, 2), A_t=→, S_t+1=(0, 3), R_t=1.0

 agentget_action()で挙動方策に従い行動して、envstep()で状態を遷移し報酬を出力します。
 得られたサンプルデータ(現在の状態・行動・報酬・次の状態)を使って、agentupdate()で現在の状態と行動の行動価値関数を計算します。
 ゴールマスに着くとdoneTrueに設定されるので、breakでループ処理を終了します。

 行動価値関数をヒートマップで確認します。

# 行動価値関数のヒートマップと方策ラベルを作図
env.render_q(q=agent.Q)

1エピソード更新した行動価値関数のヒートマップと方策ラベル

 render_q()内部のnp.argmax()の仕様で、行動価値が等しいとインデックスが最小の行動がラベルで表示されます。

 状態を指定して、greedy化した方策とε-greedy化した方策を作成します。

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

# ランダムに行動する確率を指定
epsilon = 0.1

# 行動価値関数を確認
print([agent.Q[state, action] for action in range(agent.action_size)])

# greedy法による方策を作成
pi_state = greedy_probs(agent.Q, state, 0.0)
print(pi_state)

# ε-greedy法による方策を作成
b_state = greedy_probs(agent.Q, state, epsilon)
print(b_state)
[0, 0, 0, 0.8]
{0: 0.0, 1: 0.0, 2: 0.0, 3: 1.0}
{0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}


 全ての状態でgreedy化した方策を格納したディクショナリを作成します。

# 状態ごとにgreedy化
pi = {state: greedy_probs(agent.Q, state, 0.0) for state in env.states()}

# 状態ごとに表示
for state in env.states():
    print('------ state : ' + str(state) + ' ------')
    print('action value : ' + str([np.round(agent.Q[state, action], 5) for action in range(agent.action_size)]))
    print('max action   : ' + str(np.argmax([agent.Q[state, action] for action in range(agent.action_size)])))
    print('probs        : ' + str(pi[state]))
------ state : (0, 0) ------
action value : [0.0, 0.0, 0.0, 0]
max action   : 0
probs        : {0: 0.0, 1: 1.0, 2: 0.0, 3: 0.0}
------ state : (0, 1) ------
action value : [0, 0, 0, 0]
max action   : 0
probs        : {0: 0.0, 1: 0.0, 2: 0.0, 3: 1.0}
------ state : (0, 2) ------
action value : [0, 0, 0, 0.8]
max action   : 3
probs        : {0: 0.0, 1: 0.0, 2: 0.0, 3: 1.0}
------ state : (0, 3) ------
action value : [0, 0, 0, 0]
max action   : 0
probs        : {0: 0.0, 1: 1.0, 2: 0.0, 3: 0.0}
------ state : (1, 0) ------
action value : [0.0, 0.0, 0.0, 0.0]
max action   : 0
probs        : {0: 0.0, 1: 1.0, 2: 0.0, 3: 0.0}
------ state : (1, 1) ------
action value : [0, 0, 0, 0]
max action   : 0
probs        : {0: 1.0, 1: 0.0, 2: 0.0, 3: 0.0}
------ state : (1, 2) ------
action value : [0.0, 0, 0, 0]
max action   : 0
probs        : {0: 0.0, 1: 0.0, 2: 1.0, 3: 0.0}
------ state : (1, 3) ------
action value : [0, 0, 0.0, 0]
max action   : 0
probs        : {0: 0.0, 1: 1.0, 2: 0.0, 3: 0.0}
------ state : (2, 0) ------
action value : [0.0, 0.0, 0.0, 0.0]
max action   : 0
probs        : {0: 0.0, 1: 1.0, 2: 0.0, 3: 0.0}
------ state : (2, 1) ------
action value : [0, 0.0, 0.0, 0.0]
max action   : 0
probs        : {0: 0.0, 1: 0.0, 2: 0.0, 3: 1.0}
------ state : (2, 2) ------
action value : [0, 0, 0, 0.0]
max action   : 0
probs        : {0: 1.0, 1: 0.0, 2: 0.0, 3: 0.0}
------ state : (2, 3) ------
action value : [-0.8, 0.0, 0, 0.0]
max action   : 1
probs        : {0: 0.0, 1: 1.0, 2: 0.0, 3: 0.0}

 行動価値が等しい場合は、greedy_probs()内部のargmax()によって、$1 - \epsilon$の確率で取る行動がランダムに決まります(probsの値(確率)が1.0の行動とmax actionが異なる場合があります)。

 続いて、全ての状態でε-greedy化した方策を格納したディクショナリを作成します。

# ランダムに行動する確率を指定
epsilon = 0.1

# 状態ごとにε-greedy化
b = {state: greedy_probs(agent.Q, state, epsilon) for state in env.states()}

# 状態ごとに表示
for state in env.states():
    print('------ state : ' + str(state) + ' ------')
    print('action value : ' + str([np.round(agent.Q[state, action], 5) for action in range(agent.action_size)]))
    print('max action   : ' + str(np.argmax([agent.Q[state, action] for action in range(agent.action_size)])))
    print('probs        : ' + str(b[state]))
------ state : (0, 0) ------
action value : [0.0, 0.0, 0.0, 0]
max action   : 0
probs        : {0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}
------ state : (0, 1) ------
action value : [0, 0, 0, 0]
max action   : 0
probs        : {0: 0.925, 1: 0.025, 2: 0.025, 3: 0.025}
------ state : (0, 2) ------
action value : [0, 0, 0, 0.8]
max action   : 3
probs        : {0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}
------ state : (0, 3) ------
action value : [0, 0, 0, 0]
max action   : 0
probs        : {0: 0.025, 1: 0.925, 2: 0.025, 3: 0.025}
------ state : (1, 0) ------
action value : [0.0, 0.0, 0.0, 0.0]
max action   : 0
probs        : {0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}
------ state : (1, 1) ------
action value : [0, 0, 0, 0]
max action   : 0
probs        : {0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}
------ state : (1, 2) ------
action value : [0.0, 0, 0, 0]
max action   : 0
probs        : {0: 0.025, 1: 0.025, 2: 0.925, 3: 0.025}
------ state : (1, 3) ------
action value : [0, 0, 0.0, 0]
max action   : 0
probs        : {0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}
------ state : (2, 0) ------
action value : [0.0, 0.0, 0.0, 0.0]
max action   : 0
probs        : {0: 0.025, 1: 0.025, 2: 0.025, 3: 0.925}
------ state : (2, 1) ------
action value : [0, 0.0, 0.0, 0.0]
max action   : 0
probs        : {0: 0.925, 1: 0.025, 2: 0.025, 3: 0.025}
------ state : (2, 2) ------
action value : [0, 0, 0, 0.0]
max action   : 0
probs        : {0: 0.925, 1: 0.025, 2: 0.025, 3: 0.025}
------ state : (2, 3) ------
action value : [-0.8, 0.0, 0, 0.0]
max action   : 1
probs        : {0: 0.025, 1: 0.925, 2: 0.025, 3: 0.025}


 以上で、サンプリング版のQ学習のエージェントを実装できました。分布モデル版と内部の処理は異なりますが、クラスとして利用する処理は同じなので、6.4節のコードで方策制御を行えます。

 この章では、TD法を実装しました。次章では、ニューラルネットワークによるQ学習を実装します。

参考文献


おわりに

 数式は出てこないし目新しい処理も出てこないので、手元のノートには書きつつ記事にするつもりはなかったのですが、argmax()に関して自分用のメモとしても書き残しておいた方がいいかなと思って一応記事としてアップしておくことにしました。
 これで6章完了です!お疲れ様でした。いよいよニューラルネットに進みます。

【次節の内容】

www.anarchive-beta.com