はじめに
『ゼロから作るDeep Learning 4 ――強化学習編』の独学時のまとめノートです。初学者の補助となるようにゼロつくシリーズの4巻の内容に解説を加えていきます。本と一緒に読んでください。
この記事は、6.4節の内容です。Q学習による行動価値関数を推定します。
【前節の内容】
【他の記事一覧】
【この記事の内容】
6.4 Q学習
Q学習により行動価値関数と方策を推定(方策を制御)します。
6.4.2 ベルマン最適方程式とQ学習
まずは、Q学習による行動価値関数の更新式を導出します。
数式の確認
行動価値関数のベルマン最適方程式を確認して、Q学習による計算式を導出します。
ベルマン最適方程式
行動価値関数のベルマン方程式は、次の式でした(3.3.2項)。
ここで、$\gamma$は割引率で$0 \leq \gamma \leq 1$の値を指定します。
ベルマン最適方程式は、次の式になりました(3.4.2項)。
次の時刻の行動に関して、max演算子により最適方策(行動価値関数が最大となる行動)を取ります。
Q学習の更新式
ベルマン最適方程式(3.18)を、状態遷移確率$p(s' | s, a)$による期待値の項に変形します。
$r(s, a, s') + \gamma \max_{a'} q_{*}(s', a')$の期待値を、「次の状態のサンプル$S_{t+1}$」と「報酬のサンプル$R_t$」
を用いて求めます。
$R_t + \gamma \max_{a'} Q(S_{t+1}, a')$の期待値を、指数移動平均で近似します。
Q学習による行動価値関数の更新式が得られました。$\alpha$は学習率で$0 < \alpha < 1$の値を指定します。またこの式は、DP法による状態価値関数の更新式(6.9)と同様にして展開できます。
SARSAでは、「次の時刻の行動$A_{t+1}$」をサンプリングして計算しました。Q学習では、「行動価値が最大となる行動」を用いて計算します。
方策の更新式は、方策オン型のSARSAと同じです。
6.4.3 Q学習の実装
次は、Q学習により行動価値関数と方策の推定を行うエージェントを実装します。
利用するライブラリを読み込みます。
# ライブラリを読み込み import numpy as np from collections import defaultdict # 追加ライブラリ import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap from matplotlib.animation import FuncAnimation
更新推移をアニメーションで確認するのにmatplotlib
のモジュールを利用します。不要であれば省略してください。
また、3×4マスのグリッドワールドのクラスGridWorld
を読み込みます。
# 実装済みのクラスと関数を読み込み import sys sys.path.append('../deep-learning-from-scratch-4-master') from common.gridworld import GridWorld from common.utils import greedy_probs
実装済みクラスの読み込みについては「3.6.1:MNISTデータセットの読み込み【ゼロつく1のノート(Python)】 - からっぽのしょこ」、GridWorld
クラスについては「4.2.1:GridWorldクラスの実装:評価と改善に関するメソッド【ゼロつく4のノート】 - からっぽのしょこ」「4.2.1:GridWorldクラスの実装:可視化に関するメソッド【ゼロつく4のノート】 - からっぽのしょこ」、greedy_probs
関数については「5.4.3-5:モンテカルロ法による方策反復法の実装【ゼロつく4のノート】 - からっぽのしょこ」を参照してください。
処理の確認
QlearningAgent
クラスのupdate
メソッドの内部で行う処理を確認します。他のメソッドについては「5.3:モンテカルロ法による方策評価の実装【ゼロつく4のノート】 - からっぽのしょこ」を参照してください。
例として、ランダムな値の行動価値関数を作成しておきます。
# (仮の)現在の状態を設定 state = (0, 1) # (仮の)次の状態を設定 next_state = (0, 2) # 行動の種類数を指定 action_size = 4 # (仮の)行動価値関数を作成 Q = {(s, a): np.random.rand() for s in [state, next_state] for a in range(action_size)} print(list(Q.keys())) print(np.round(list(Q.values()), 3))
[((0, 1), 0), ((0, 1), 1), ((0, 1), 2), ((0, 1), 3), ((0, 2), 0), ((0, 2), 1), ((0, 2), 2), ((0, 2), 3)]
[0.277 0.531 0.34 0.326 0.779 0.393 0.07 0.282]
現在の状態state
と次の状態next_state
ごとに、上下左右の4つの行動に対する値をディクショナリに格納します。
現在の状態の行動価値関数の最大値を取り出します。
# (仮の)ゴールフラグを設定 done = False #done = True # ゴールの場合 if done: # 次の状態・行動の行動価値の最大値を0に設定 next_q_max = 0 # ゴール以外の場合 else: # 次の状態・行動の行動価値を取得 next_qs = [Q[next_state, a] for a in range(action_size)] # 次の状態・行動の行動価値の最大値を取得 next_q_max = max(next_qs) print(np.round(next_qs, 3)) print(next_q_max)
[0.779 0.393 0.07 0.282]
0.7794000999453246
リスト内包表記を使って、行動価値関数Q
から、状態next_state
における全ての行動価値を取り出してnext_qs
とします。
next_qs
の最大値をnext_q_max
とします。ただし、現在の状態がゴールマスのときは、行動価値を0
にします。
現在の状態・行動の行動価値を計算して、値を更新します。
# (仮の)行動を設定 action = 1 # (仮の)報酬を設定 reward = 1 # 収益の計算用の割引率を指定 gamma = 0.9 # 状態価値の計算用の学習率 alpha = 0.01 # TDターゲットを計算 target = reward + gamma * next_q_max # 現在の状態・行動の行動価値関数を更新:式(6.14) Q[state, action] += (target - Q[state, action]) * alpha print(Q[state, action])
0.5427542325626015
「現在の状態state
と行動action
」をキーとして、式(6.14)により行動価値を計算して、「現在の状態・行動の行動価値Q[state, action]
」を更新します。
以上が、Q学習による方策制御を行うエージェントの処理です。
実装
処理の確認ができたので、Q学習におけるエージェントをクラスとして実装します。
# Q学習によるエージェントの実装 class QLearningAgent: # 初期化メソッドの定義 def __init__(self): # パラメータを指定 self.gamma = 0.9 # 収益の計算用の割引率 self.alpha = 0.8 # 状態価値の計算用の学習率 self.epsilon = 0.1 # ランダムに行動する確率 self.action_size = 4 # 行動の種類数 # オブジェクトを初期化 random_actions = {0: 0.25, 1: 0.25, 2: 0.25, 3: 0.25} # 確率論的方策 self.pi = defaultdict(lambda: random_actions) # ターゲット方策 self.b = defaultdict(lambda: random_actions) # 挙動方策 self.Q = defaultdict(lambda: 0) # 行動価値関数 # 行動メソッドの定義 def get_action(self, state): # 現在の状態の挙動方策の確率分布を取得 action_probs = self.b[state] # 確率分布 actions = list(action_probs.keys()) # 行動番号 probs = list(action_probs.values()) # 行動確率 # 確率論的方策に従う行動を出力 return np.random.choice(actions, p=probs) # 更新メソッドの定義 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 # greedy法によりターゲット方策を更新 self.pi[state] = greedy_probs(self.Q, state, 0) # ε-greedy法により挙動方策を更新:式(6.11) self.b[state] = greedy_probs(self.Q, state, self.epsilon)
実装したクラスを試してみましょう。
環境(グリッドワールド)とエージェントのインスタンスを作成して、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,11) 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, 1), R_t=0
t=3, S_t=(2, 1), A_t=→, S_t+1=(2, 2), R_t=0
t=4, S_t=(2, 2), A_t=↑, S_t+1=(1, 2), R_t=0
t=5, S_t=(1, 2), A_t=↓, S_t+1=(2, 2), R_t=0
t=6, S_t=(2, 2), A_t=↓, S_t+1=(2, 2), R_t=0
t=7, S_t=(2, 2), A_t=→, S_t+1=(2, 3), R_t=0
t=8, S_t=(2, 3), A_t=→, S_t+1=(2, 3), R_t=0
t=9, S_t=(2, 3), A_t=↑, S_t+1=(1, 3), R_t=-1.0
t=10, S_t=(1, 3), A_t=←, S_t+1=(1, 2), R_t=0
t=11, S_t=(1, 2), A_t=→, S_t+1=(1, 3), R_t=-1.0
t=12, S_t=(1, 3), A_t=↓, S_t+1=(2, 3), R_t=0
t=13, S_t=(2, 3), A_t=←, S_t+1=(2, 2), R_t=0
t=14, S_t=(2, 2), A_t=↓, S_t+1=(2, 2), R_t=0
t=15, S_t=(2, 2), A_t=↑, S_t+1=(1, 2), R_t=0
t=16, S_t=(1, 2), A_t=↑, S_t+1=(0, 2), R_t=0
t=17, S_t=(0, 2), A_t=↓, S_t+1=(1, 2), R_t=0
t=18, S_t=(1, 2), A_t=↑, S_t+1=(0, 2), R_t=0
t=19, S_t=(0, 2), A_t=↓, S_t+1=(1, 2), R_t=0
t=20, S_t=(1, 2), A_t=↓, S_t+1=(2, 2), R_t=0
t=21, S_t=(2, 2), A_t=↑, S_t+1=(1, 2), R_t=0
t=22, S_t=(1, 2), A_t=↑, S_t+1=(0, 2), R_t=0
t=23, S_t=(0, 2), A_t=→, S_t+1=(0, 3), R_t=1.0
agent
のget_action()
で挙動方策に従い行動して、env
のstep()
で状態を遷移し報酬を出力します。
得られたサンプルデータ(現在の状態・行動・報酬・次の状態)を使って、agent
のupdate()
で現在の状態と行動の行動価値関数と方策を計算します。
ゴールマスに着くとdone
がTrue
に設定されるので、break
でループ処理を終了します。
行動価値関数をヒートマップで確認します。
# 行動価値関数のヒートマップと方策ラベルを作図
env.render_q(q=agent.Q)
render_q()
内部のnp.argmax()
の仕様で、行動価値が等しいとインデックスが最小の行動がラベルで表示されます。
以上で、Q学習のエージェントを実装できました。
・Q学習による方策制御
最後に、Q学習により行動価値関数を推定して、更新の推移を確認します。
推定
Q学習により行動価値関数と方策を繰り返し更新します。
# 環境・エージェントのインスタンスを作成 env = GridWorld() agent = QLearningAgent() # エピソード数を指定 episodes = 1000 # 推移の可視化用のリストを初期化 trace_Q = [{(state, action): agent.Q[(state, action)] for state in env.states() for action in env.action_space}] # 初期値を記録 # 繰り返しシミュレーション for episode in range(episodes): # 状態を初期化 state = env.reset() # 時刻(試行回数)を初期化 t = 0 # 1エピソードのシミュレーション while True: # 時刻をカウント t += 1 # ε-greedy法により行動を決定 action = agent.get_action(state) # サンプルデータを取得 next_state, reward, done = env.step(action) # 現在の状態・行動の行動価値関数・方策を更新:式(6.14,11) agent.update(state, action, reward, next_state, done) # ゴールに着いた場合 if done: # 更新値を記録 trace_Q.append(agent.Q.copy()) # 総時刻を表示 print('episode '+str(episode+1) + ': T='+str(t)) # エピソードを終了 break # 状態を更新 state = next_state
episode 1: T=130
episode 2: T=21
episode 3: T=93
episode 4: T=34
episode 5: T=12
(省略)
episode 996: T=5
episode 997: T=5
episode 998: T=5
episode 999: T=8
episode 1000: T=5
スタートマスからε-greedy法により行動し、ゴールマスに着くまでを1エピソードとします。エピソードごとに、GridWorld
クラスのreset()
メソッドで状態を初期化し(エージェントをスタートマスに戻し)ます。
episodes
に指定した回数のシミュレーションを行い、時刻ごとに繰り返しagent
のupdate()
で現在の状態と行動の行動価値関数と方策を更新します。
推移の確認用に、行動価値関数の更新値をtrace_Q
に格納していきます。
推定した行動価値関数をヒートマップと方策ラベルで確認します。
# 行動価値関数のヒートマップを作図
env.render_q(q=agent.Q)
結果の解釈については本を参照してください。
更新推移の可視化
ここまでで、繰り返しの更新処理を確認しました。続いて、途中経過をアニメーションで確認します。
行動価値関数のヒートマップのアニメーションを作成します。
・作図コード(クリックで展開)
# グリッドマップのサイズを取得 xs = env.width ys = env.height # 状態価値の最大値・最小値を取得 qmax = max([max(trace_Q[i].values()) for i in range(len(trace_Q))]) qmin = min([min(trace_Q[i].values()) for i in range(len(trace_Q))]) # 色付け用に最大値・最小値を再設定 qmax = max(qmax, abs(qmin)) qmin = -1 * qmax qmax = 1 if qmax < 1 else qmax qmin = -1 if qmin > -1 else qmin # カラーマップを設定 color_list = ['red', 'white', 'green'] cmap = LinearSegmentedColormap.from_list('colormap_name', color_list) # 図を初期化 fig = plt.figure(figsize=(12, 9), facecolor='white') # 図の設定 plt.suptitle('Q Learning', fontsize=20) # 全体のタイトル # 作図処理を関数として定義 def update(i): # 前フレームのグラフを初期化 plt.cla() # i回目の更新値を取得 Q = trace_Q[i] # マス(状態)ごとに処理 for state in env.states(): # 行動ごとに処理 for action in env.action_space: # インデックスを取得 y, x = state # 報酬を抽出 r = env.reward_map[y, x] # 報酬がある場合 if r != 0 and r is not None: # 報酬ラベル用の文字列を作成 txt = 'R ' + str(r) # ゴールの場合 if state == env.goal_state: # 報酬ラベルにゴールを追加 txt = txt + ' (GOAL)' # 報酬ラベルを描画 plt.text(x=x+0.1, y=ys-y-0.9, s=txt, ha='left', va='bottom', fontsize=15) # ゴールの場合 if state == env.goal_state: # 描画せず次の状態へ continue # 作図用のx軸・y軸の値を設定 tx, ty = x, ys-y-1 # 行動ごとの三角形の頂点を設定 action_map = { 0: ((0.5+tx, 0.5+ty), (1.0+tx, 1.0+ty), (tx, 1.0+ty)), # 上 1: ((tx, ty), (1.0+tx, ty), (0.5+tx, 0.5+ty)), # 下 2: ((tx, ty), (0.5+tx, 0.5+ty), (tx, 1.0+ty)), # 左 3: ((0.5+tx, 0.5+ty), (1.0+tx, ty), (1.0+tx, 1.0+ty)) # 右 } # 行動ごとの価値ラベルのプロット位置を設定 offset_map = { 0: (0.5, 0.75), # 上 1: (0.5, 0.25), # 下 2: (0.25, 0.5), # 左 3: (0.75, 0.5) # 右 } # 壁の場合 if state == env.wall_state: # 壁を描画 rect = plt.Rectangle(xy=(tx, ty), width=1, height=1, fc=(0.4, 0.4, 0.4, 1.0)) # 長方形を作成 plt.gca().add_patch(rect) # 重ねて描画 # (よく分からない) elif state in env.goal_state: plt.gca().add_patch(plt.Rectangle(xy=(tx, ty), width=1, height=1, fc=(0.0, 1.0, 0.0, 1.0))) # 壁以外の場合 else: # 行動価値を抽出 tq = Q[(state, action)] # 行動価値を0から1に正規化 color_scale = 0.5 + (tq / qmax) / 2 # 三角形を描画 poly = plt.Polygon(action_map[action],fc=cmap(color_scale)) # 三角形を作成 plt.gca().add_patch(poly) # 重ねて描画 # プロット位置の調整値を取得 offset = offset_map[action] # 行動価値ラベルを描画 plt.text(x=tx+offset[0], y=ty+offset[1], s=str(np.round(tq, 3)), ha='center', va='center', size=15) # 行動価値ラベル # グラフの設定 plt.xticks(ticks=np.arange(xs)) # x軸の目盛位置 plt.yticks(ticks=np.arange(ys), labels=ys-np.arange(ys)-1) # y軸の目盛位置 plt.xlim(xmin=0, xmax=xs) # x軸の範囲 plt.ylim(ymin=0, ymax=ys) # y軸の範囲 plt.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False) # 軸ラベル plt.grid() # グリッド線 plt.title('episode:'+str(i), loc='left') # タイトル # gif画像を作成 anime = FuncAnimation(fig=fig, func=update, frames=len(trace_Q), interval=50) # gif画像を保存 anime.save('ch6_4.gif')
各エピソードで更新した行動価値をtrace_Q
から取り出してヒートマップを描画する処理を関数update()
として定義して、FuncAnimation()
でアニメーション(gif画像)を作成します。
行動価値関数の更新値の推移を折れ線グラフで確認します。
・作図コード(クリックで展開)
# 行動ラベルを設定 arrows = ['↑', '↓', '←', '→'] # 状態価値関数の推移を作図 plt.figure(figsize=(15, 10), facecolor='white') for state in env.states(): for action in range(agent.action_size): # 更新値を抽出 q_vals = [trace_Q[i][(state, action)] for i in range(episodes+1)] # 各状態の価値の推移を描画 plt.plot(np.arange(episodes+1), q_vals, alpha=0.5, label='$Q_i(L_{'+str(state[0])+','+str(state[1])+'},'+arrows[action]+')$') plt.xlabel('episode') plt.ylabel('action-value') plt.suptitle('Q Learning', fontsize=20) plt.title('$\gamma='+str(agent.gamma) + ', \\alpha='+str(agent.alpha)+'$', loc='left') plt.grid() plt.legend(loc='upper left', bbox_to_anchor=(1, 1), ncol=2) plt.show()
行番号を$h$、列番号を$w$として各マスを$L_{h,w}$で表します(図4-9)。また、$i$回目の行動価値を$Q_i(L_{h,w}, A)$で表します。
各曲線の縦軸の値が、ヒートマップの色に対応します。
この節では、Q学習により方策制御を行うエージェントを実装して、最適方策を求めました。またここまでは、分布モデルで実装しました。次節では、サンプルモデルで実装することを考えます。
参考文献
おわりに
怒涛の4連続「導出→実装→推定」で中々ハードでしたね。次で6章ラストです。なんと次は数式が出てきません。
2022年11月11日は、モーニング娘。'22の櫻井梨央さんの17歳のお誕生日です!そして新曲の公開日です!(右側の方です。)
1人加入の新メンバーの初参加曲でタイトルがHappy birthday to Me!で誕生日に公開とは何重にもおめでたいですね。
【次節の内容】