深層強化学習①
こんどはこんなのを試してみた。
そのままでは面白くないので、3×3の迷路を4×4にしてみた。
# Jupyterでmatplotlibを使用する宣言と、使用するライブラリの定義 import numpy as np import matplotlib.pyplot as plt %matplotlib inline
# 初期位置での迷路の様子 # 図を描く大きさと、図の変数名を宣言 fig = plt.figure(figsize=(5, 5)) ax = plt.gca() # 赤い壁を描く plt.plot([0, 1], [3, 3], color='red', linewidth=2) plt.plot([1, 1], [2, 3], color='red', linewidth=2) plt.plot([1, 2], [2, 2], color='red', linewidth=2) plt.plot([2, 2], [2, 3], color='red', linewidth=2) plt.plot([3, 3], [3, 1], color='red', linewidth=2) plt.plot([1, 4], [1, 1], color='red', linewidth=2) # 状態を示す文字S0~S15を描く plt.text(0.5, 3.5, 'S0', size=14, ha='center') plt.text(1.5, 3.5, 'S1', size=14, ha='center') plt.text(2.5, 3.5, 'S2', size=14, ha='center') plt.text(3.5, 3.5, 'S3', size=14, ha='center') plt.text(0.5, 2.5, 'S4', size=14, ha='center') plt.text(1.5, 2.5, 'S5', size=14, ha='center') plt.text(2.5, 2.5, 'S6', size=14, ha='center') plt.text(3.5, 2.5, 'S7', size=14, ha='center') plt.text(0.5, 1.5, 'S8', size=14, ha='center') plt.text(1.5, 1.5, 'S9', size=14, ha='center') plt.text(2.5, 1.5, 'S10', size=14, ha='center') plt.text(3.5, 1.5, 'S11', size=14, ha='center') plt.text(0.5, 0.5, 'S12', size=14, ha='center') plt.text(1.5, 0.5, 'S13', size=14, ha='center') plt.text(2.5, 0.5, 'S14', size=14, ha='center') plt.text(3.5, 0.5, 'S15', size=14, ha='center') plt.text(0.5, 3.3, 'START', ha='center') plt.text(3.5, 0.3, 'GOAL', ha='center') # 描画範囲の設定と目盛りを消す設定 ax.set_xlim(0, 5) ax.set_ylim(0, 5) plt.tick_params(axis='both', which='both', bottom='off', top='off', labelbottom='off', right='off', left='off', labelleft='off') # 現在値S0に緑丸を描画する line, = ax.plot([0.5], [3.5], marker="o", color='g', markersize=60)
# 初期の方策を決定するパラメータtheta_0を設定 # 行は状態0~14、列は移動方向で↑、→、↓、←を表す theta_0 = np.array([[np.nan, 1, np.nan, np.nan], # s0 [np.nan, 1, 1, 1], # s1 [np.nan, 1, 1, 1], # s2 [np.nan, np.nan, 1, 1], # s3 [np.nan, np.nan, 1, np.nan], # s4 [1, np.nan, np.nan, np.nan], # s5 [1, np.nan, 1, np.nan], # s6 [1, np.nan, 1, np.nan], # s7 [1, 1, 1, np.nan], # s8 [np.nan, 1, np.nan, 1], # s9 [1, np.nan, np.nan, 1], # s10 [1, np.nan, np.nan, np.nan], # s11 [1, 1, np.nan, np.nan], # s12 [np.nan, 1, np.nan, 1], # s13 [np.nan, 1, np.nan, 1], # s14、※s15はゴールなので、方策はなし
# 初期の行動価値関数Qを設定 [a, b] = theta_0.shape # 行と列の数をa, bに格納 Q = np.random.rand(a, b) * theta_0 # *theta0をすることで要素ごとに掛け算をし、壁方向がnanになる
# 方策パラメータtheta_0をランダム方策piに変換する関数の定義 def simple_convert_into_pi_from_theta(theta): '''単純に割合を計算する''' [m, n] = theta.shape # thetaの行列サイズを取得 pi = np.zeros((m, n)) for i in range(0, m): pi[i, :] = theta[i, :] / np.nansum(theta[i, :]) # 割合の計算 pi = np.nan_to_num(pi) # nanを0に変換 return pi # ランダム行動方策pi_0を求める pi_0 = simple_convert_into_pi_from_theta(theta_0)
# ε-greedy法を実装 def get_action_and_s_next(s, Q, epsilon, pi_0): direction = ["up", "right", "down", "left"] # 行動を決める if np.random.rand() < epsilon: # εの確率でランダムに動く next_direction = np.random.choice(direction, p=pi_0[s, :]) else: # Qの最大値の行動を採用する next_direction = direction[np.nanargmax(Q[s, :])] # 決めた行動で次の状態を決める if next_direction == "up": action = 0 s_next = s - 4 # 上に移動するときは状態の数字が3小さくなる elif next_direction == "right": action = 1 s_next = s + 1 # 右に移動するときは状態の数字が1大きくなる elif next_direction == "down": action = 2 s_next = s + 4 # 下に移動するときは状態の数字が3大きくなる elif next_direction == "left": action = 3 s_next = s - 1 # 左に移動するときは状態の数字が1小さくなる return [action, s_next]
# Sarsaによる行動価値関数Qの更新 def Sarsa(s, a, r, s_next, a_next, Q, eta, gamma): if s_next == 15: # ゴールした場合 Q[s, a] = Q[s, a] + eta * (r - Q[s, a]) else: Q[s, a] = Q[s, a] + eta * (r + gamma * Q[s_next, a_next] - Q[s, a]) return Q
# Sarsaで迷路を解く関数の定義、状態と行動の履歴および更新したQを出力 def goal_maze_ret_s_a_Q(Q, epsilon, eta, gamma, pi_0): s = 0 # スタート地点 s_a_history = [[0, np.nan]] # エージェントの移動を記録するリスト while (1): # ゴールするまでループ [a, s_next] = get_action_and_s_next(s, Q, epsilon, pi_0) s_a_history[-1][1] = a # 現在の状態(つまり一番最後なのでindex=-1)に行動を代入 s_a_history.append([s_next, np.nan]) # 次の状態を代入。行動はまだ分からないのでnanにしておく # 報酬を与え, 次の行動を求めます if s_next == 15: r = 1 # ゴールにたどり着いたなら報酬を与える a_next = np.nan else: r = 0 [a_next, _] = get_action_and_s_next(s_next, Q, epsilon, pi_0) # 実際行動しないですが、次の行動a_nextを求めます。 # 返り値の_は、その変数は無視するという意味です # 価値関数を更新 Q = Sarsa(s, a, r, s_next, a_next, Q, eta, gamma) # 終了判定 if s_next == 15: # ゴール地点なら終了 break else: s = s_next return [s_a_history, Q]
# Sarsaで迷路を解く eta = 0.1 # 学習率 gamma = 0.9 # 時間割引率 epsilon = 0.5 # ε-greedy法の初期値 v = np.nanmax(Q, axis=1) # 状態ごとに価値の最大値を求める is_continue = True episode = 1 while is_continue: # is_continueがFalseになるまで繰り返す print("エピソード:" + str(episode)) # ε-greedyの値を少しずつ小さくする epsilon = epsilon / 2 # Sarsaで迷路を解き、移動した履歴と更新したQを求める [s_a_history, Q] = goal_maze_ret_s_a_Q(Q, epsilon, eta, gamma, pi_0) # 状態価値の変化 new_v = np.nanmax(Q, axis=1) # 状態ごとに価値の最大値を求める print(np.sum(np.abs(new_v - v))) # 状態価値の変化を出力 v = new_v print("迷路を解くのにかかったステップ数は" + str(len(s_a_history) - 1) + "です") # 100エピソード繰り返す episode = episode + 1 if episode > 100: break
エピソード:1
2.6735992695202087
迷路を解くのにかかったステップ数は462です
エピソード:2
0.14481707033237906
迷路を解くのにかかったステップ数は24です
エピソード:3
0.057744543398545234
迷路を解くのにかかったステップ数は12です
エピソード:4
0.17546859237689333
迷路を解くのにかかったステップ数は44です
エピソード:5
0.37518605278714223
迷路を解くのにかかったステップ数は110です
エピソード:6
0.06477644009690636
迷路を解くのにかかったステップ数は12です
エピソード:7
0.05632477012963749
迷路を解くのにかかったステップ数は10です
# エージェントの移動の様子を可視化します # 参考URL http://louistiao.me/posts/notebooks/embedding-matplotlib-animations-in-jupyter-notebooks/ from matplotlib import animation from IPython.display import HTML def init(): # 背景画像の初期化 line.set_data([], []) return (line,) def animate(i): # フレームごとの描画内容 state = s_a_history[i][0] # 現在の場所を描く x = (state % 4) + 0.5 # 状態のx座標は、4で割った余り+0.5 y = 3.5 - int(state / 4) # y座標は4で割った商を3.5から引く line.set_data(x, y) return (line,) # 初期化関数とフレームごとの描画関数を用いて動画を作成 anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len( s_a_history), interval=200, repeat=False) HTML(anim.to_jshtml())
4×4にしてみても、コアの部分の理解が深まるわけがなかった。