PyGameによる強化学習シミュレーションの動的表示
R. S. Sutton & A. G. Barto (2018). Reinforcement learnig, 2nd ed. The MIT Press. pp. 25-32に説明されている強化学習のシミュレーションをPyGameを用いて動的に表示してみた。
先ず、アルゴリズムの確認のために、Sutton & Burto (2018)のFigure 2.2の上のグラフを再現するスクリプトをリスト1に示すようにように作成した。なお、本ウェブサイトのスクリプトファイルは、まとめて圧縮ファイルscriptfiles.zipとしている。リスト1のスクリプトを実行すると図1のグラフを得た。図1のグラフは、SuttonらのFigure 2.2の上のものと同じと見做されるので、リスト1のスクリプトはSuttonらの説明のアルゴリズムを表しているものと考えた。
図1
リスト1のスクリプトは、Sutton & Barto (2018)のA simple bandit algorithm (p.32)に基づいており、変数名も出来るだけ合わせた。
強化を表すincremental formulaは、リスト1では次のコードで表されている。
Qt[a] += (R - Qt[a]) / Nt[a] # 平均rewardの更新
リスト1では、シミュレーションの結果の最後のまとめがrewardの平均値のグラフとして表示されるだけで、個々の機械(machine)の学習の途中経過は分からない。PythonのライブラリPyGameを利用して、途中経過をアニメーションのように表示するスクリプトをリスト2のように作成してみた。リスト2のスクリプトファイルは圧縮ファイルscriptfiles.zipにまとめた。
アルゴリズムは、基本的にはリスト1のスクリプトもリスト2のスクリプトも同じである。図1では個々のmachineの振る舞いが分からないので、リスト2では、個々のmachineのrewardの軌跡も描画している。
まず、PyGameによる描画画面は、次のコードで設定している。
screen = pg.display.set_mode((1200, 800))
用意した画面の大きさに合わせて、横軸と縦軸の単位を次の関数で調整している。
def x_scale(x):
"""
[0, 1]区間を画面の横サイズに合わせて変換
"""
return 200 + 800 * x
def y_scale(y):
"""
[0, 1]区間を画面の縦サイズに合わせて変換
"""
return 700 - 600 * y
シミュレーションはステップ単位で進むので、シミュレーションの開始の初期は図2のようなグラフになる。
図2
εの各値に対してn_mcs = 1000台のmachineが用意されているが、グラフの折れ線が重なり過ぎないように各εに対して10台のグラフが描かれている。
ステップが進むと図3のようになる。
図3
ステップは、n_simu = 1000ステップまで進む。
全ステップが終了すると、各εごとの1000台のmachine平均値のグラフが表示される(図4)。
図4
PyGameによるグラフ(図4の左側後ろのグラフ)ではepsilon=0.1のグラフが青色で分かり難い。Matplotlibによる描画(図4の右側前のグラフ)も用意した。Matplotlibによる描画では、各εに対応するmachineをランダムに10台選び、グラフの描画はalpha=0.1として線の透明度を調整している。
プログラムの終了は、matplotlibによる描画画面を閉じてから、PyGameによる描画画面を閉じる。
"""
R. S.
Sutton and A. G. Barto (2018)
Reinforcement Learnig, 2nd ed., 25-32.
"""
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(123) # シミュレーションの再現性のために値を指定して初期化
k = 10
RsDic = {}
Rsave = {}
n_simu = 1000
ep_lst = [0.0, 0.01, 0.1]
n_mcs = 1000
for ep in ep_lst:
RsDic[ep] = []
for i_mcs in range(n_mcs):
if
i_mcs % 100 == 0:
print(f'{ep}/ep
{i_mcs}/{n_mcs}')
Qt =
np.zeros(k)
# Mean reward actually
received
Nt =
np.zeros(k)
q_star = rng.normal(0, 1 , k)
Rt =
[]
for
i in range(n_simu):
if rng.uniform(0.0, 1.0) < ep:
a = rng.choice(np.arange(k))
# Exploration
else:
a = np.argmax(Qt)
# Greedy
Nt[a] += 1
R = rng.normal(q_star[a], 1, 1)[0]
Qt[a] += (R - Qt[a]) / Nt[a] # 平均rewardの更新
Rt.append(R)
RsDic[ep].append(Rt)
ep_c = {0.0:'g', 0.01:'r', 0.1:'b'}
for ep in ep_lst:
Rsave[ep] =
np.mean(RsDic[ep], axis=0)
for ep in ep_lst:
plt.plot(np.arange(n_simu),
Rsave[ep], c=ep_c[ep], lw=2,
label=fr'$\epsilon$={ep}')
plt.xlabel('Steps')
plt.ylabel('Average reward')
plt.legend()
plt.show()
"""
R. S.
Sutton and A. G. Barto (2018)
Reinforcement Learnig, 2nd ed., 25-32.
"""
import pygame as pg
import numpy as np
import matplotlib.pyplot as plt
import sys
rng = np.random.default_rng(123) # 値を指定して乱数の初期化
k = 10
# k-armed testbed
n_steps = 1000 # ステップ数
n_mcns = 2000 # 条件当たりのmachineの数
pg.init()
screen = pg.display.set_mode((1200, 800))
pg.display.set_caption('My Simple
Simulation')
screen.fill((255,255,255))
pg.display.flip()
def x_scale(x):
"""
[0, 1]区間を画面の横サイズに合わせて変換
"""
return 200 + 800 * x
def y_scale(y):
"""
[0, 1]区間を画面の縦サイズに合わせて変換
"""
return 700 - 600 * y
"""
座標軸の描画
"""
pg.draw.line(screen, (0,0,0),
(x_scale(0), y_scale(0)),
(x_scale(1), y_scale(0)), 2)
pg.draw.line(screen, (0,0,0),
(x_scale(0), y_scale(0)),
(x_scale(0), y_scale(1)), 2)
pg.draw.line(screen, (0,0,0),
(x_scale(0), y_scale(0)),
(x_scale(0), y_scale(-0.03)), 2)
pg.draw.line(screen, (0,0,0),
(x_scale(1), y_scale(0)),
(x_scale(1), y_scale(-0.03)), 2)
pg.draw.line(screen, (0,0,0),
(x_scale(0), y_scale(0)),
(x_scale(-0.02), y_scale(0)), 2)
my_font = pg.font.SysFont(None, 40)
img_str = my_font.render('1', True,
(0,0,0), None)
rect_str = img_str.get_rect()
screen.blit(img_str,
(x_scale(0)-rect_str.width//2, y_scale(0)+rect_str.height))
img_str =
my_font.render(fr'{n_steps}', True, (0,0,0), None)
rect_str = img_str.get_rect()
screen.blit(img_str,
(x_scale(1)-rect_str.width//2,
y_scale(0)+rect_str.height))
img_str = my_font.render('Steps',
True, (0,0,0), None)
rect_str = img_str.get_rect()
screen.blit(img_str,
(x_scale(0.5)-rect_str.width//2,
y_scale(0)+rect_str.height*2))
for iy in np.arange(-4, 6.1, 1):
pg.draw.line(screen,
(0,0,0), (x_scale(0), y_scale((iy+4.0)/10.0)),
(x_scale(-0.02),
y_scale((iy+4.0)/10.0)), 2)
img_str =
my_font.render(fr'{iy:.1f}', True, (0,0,0), None)
rect_str =
img_str.get_rect()
screen.blit(img_str,
(x_scale(-0.03)-rect_str.width,
y_scale((iy+4.0)/10.0)-rect_str.height/2))
img_str = my_font.render('Indivisual
and average rewards', True, (0,0,0), None)
img_str = pg.transform.rotate(img_str,
90)
rect_str = img_str.get_rect()
screen.blit(img_str,
(x_scale(-0.1)-rect_str.width,
y_scale(0.5)-rect_str.height/2))
pg.display.flip()
"""
Epsilonの値と表示色のリスト
"""
ep_lst = [0.0, 0.01, 0.1]
ep_clr = {0.0:(0,255,0),
0.01:(255,0,0), 0.1:(20,0,255)}
for i, ep in enumerate(ep_lst):
img_str =
my_font.render(f'epsilon={ep}', True, ep_clr[ep], None)
rect_str =
img_str.get_rect()
screen.blit(img_str,
(x_scale(i/3), y_scale(1.1)))
Rt = {}
for ep in ep_lst:
Rt[ep] = {}
for i_mcn in range(n_mcns):
Rt[ep][i_mcn] = []
Nt = {}
for ep in ep_lst:
Nt[ep] = {}
for i_mcn in range(n_mcns):
Nt[ep][i_mcn] = {}
for
a in range(k):
Nt[ep][i_mcn][a] = 0
Qt = {}
# Mean reward actually
received
for ep in ep_lst:
Qt[ep] = {}
for i_mcn in range(n_mcns):
Qt[ep][i_mcn] = np.zeros(k)
q_star = {}
for ep in ep_lst:
q_star[ep] = {}
for i_mcn in range(n_mcns):
q_star[ep][i_mcn] = rng.normal(0, 1, k)
for i_step in range(n_steps):
if i_step % 100 == 0:
print(f'i_step
= {i_step}/{n_steps}')
for ep in ep_lst:
for
i_mcn in range(n_mcns):
if rng.uniform(0.0, 1.0) < ep:
a = rng.choice(np.arange(k))
else:
a = np.argmax(Qt[ep][i_mcn])
Nt[ep][i_mcn][a]
+= 1
R = rng.normal(q_star[ep][i_mcn][a], 1, 1)[0]
Qt[ep][i_mcn][a] += (R - Qt[ep][i_mcn][a]) / Nt[ep][i_mcn][a]
Rt[ep][i_mcn].append(R)
if i_step > 0:
if i_mcn <10:
# 先頭の10台のみReward曲線を描画
pg.draw.line(screen, ep_clr[ep],
(x_scale((i_step)/n_steps),
y_scale((Rt[ep][i_mcn][i_step-1]+4.0)/10.0)),
(x_scale((i_step+1)/n_steps),
y_scale((Rt[ep][i_mcn][i_step]+4.0)/10.0)), 1)
pg.display.flip()
for e in
pg.event.get(): # eventの処理で表示が止まるのを避ける
if
e.type == pg.QUIT: # Close Windowボタン
pg.quit()
sys.exit()
if
e.type == pg.KEYDOWN:
if e.key == pg.K_ESCAPE:
# Escape キー
pg.quit()
sys.exit()
"""
平均reward曲線の表示
"""
for ep in ep_lst:
Rtemp = []
for i_mcn in range(n_mcns):
Rtemp.append(Rt[ep][i_mcn])
Rmean = np.mean(Rtemp,
axis=0)
for i_step in range(1,
n_steps):
pg.draw.line(screen, ep_clr[ep],
(x_scale((i_step)/n_steps),
y_scale((Rmean[i_step-1]+1.0)/4)),
(x_scale((i_step+1)/n_steps),
y_scale((Rmean[i_step]+1.0)/4)), 3)
pg.display.flip()
"""
Matplotlibによる描画
"""
plt.figure(figsize=(8, 6))
for ep in ep_lst:
"""
各epsilon条件において10台をランダムに選んで表示
"""
for i_mcn in
rng.choice(range(n_mcns), 10, replace=False, ):
plt.plot(Rt[ep][i_mcn], c=np.array(ep_clr[ep])/255.0, alpha=0.1)
"""
平均reward曲線の描画
"""
for ep in ep_lst:
Rtemp = []
for i_mcn in range(n_mcns):
Rtemp.append(Rt[ep][i_mcn])
Rmean = np.mean(Rtemp,
axis=0)
plt.plot(range(len(Rmean)),
Rmean, c=np.array(ep_clr[ep])/255.0, lw=3,
label=fr'$\epsilon$={ep}')
plt.xlabel('Steps', fontsize=18)
plt.ylabel('Individual and average
rewards', fontsize=18)
plt.legend(fontsize=16)
plt.show()
ck_0 = True
while ck_0:
for e in pg.event.get():
if
e.type == pg.QUIT:
ck_0 = False
if
e.type == pg.KEYDOWN:
if e.key == pg.K_ESCAPE:
# Escape キー
ck_0 = False
pg.quit()