からっぽのしょこ

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

2.2:環境とエージェントの定式化【ゼロつく4のノート】

はじめに

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

 この記事は、2.2節の内容です。環境とエージェントのやり取り(状態遷移・報酬・方策)の計算について確認します。

【前節の内容】

www.anarchive-beta.com

【他の記事一覧】

www.anarchive-beta.com

【この記事の内容】

2.1 MDPとは

 MDPについては本を読んでください。

2.1.2 エージェントと環境のやりとり:(1)

 時刻$t$における状態を$S_t$で表します。状態$S_t$において(時刻$t$において)エージェントが取る行動を$A_t$とします。行動$A_t$によって得られる報酬を$R_t$とします。報酬を得ると、時刻が1つ進み$t + 1$となり、次の状態$S_{t+1}$となります。

 つまり、初期の時刻を$t = 0$として「$S_0$」→「$A_0$」→「$R_0$」→「$S_1$」→「$A_1$」→「$R_1$」→「$S_2$」→「$A_2$」→「$R_2$」→・・・→「$S_t$」→「$A_t$」→「$R_t$」→「$S_{t+1}$」→「$A_{t+1}$」→「$R_{t+1}$」→・・・と続きます。

 次節では、それぞれのやりとりを関数または確率分布で表現します。

2.2 環境とエージェントの定式化

 「状態遷移」「報酬」「方策」を式で表します。
 前節では、時刻$t$における状態・行動を$S_t, A_t$としました。状態・行動の実際の値や内容を$s, a$で表します。例えば図2-6において、状態$s$として取り得る値はL1からL5、$a$として取り得る値はLeftとRightです。

2.2.1 状態遷移

 状態$s$において、行動$a$を行い、状態$s'$に遷移することを考えます。

 決定論的に決まる場合は、次の関数で表現します。

$$ s' = f(s, a) $$

 これを状態遷移関数と呼びます。

 確率論的に決める場合は、次の確率(確率分布)で表現します。

$$ p(s' | s, a) $$

 これを状態遷移確率と呼びます。

2.2.2 報酬関数

 状態$s$において、行動$a$を行い、状態$s'$に遷移したとき得られる報酬を考えます。

 決定論的に決まる場合は、次の関数で表現します。

$$ r(s, a, s') $$

 これを報酬関数と呼びます。

 遷移後の状態$s'$だけで報酬が決まる場合は、次のように表現できます。

$$ r(s') $$


 本では、確率的に決める場合は扱いません。

2.2.3 エージェントの方策

 状態$s$において行う行動$a$について考えます。

 決定論的に決まる場合は、次の関数で表現します。

$$ a = \mu(s) $$

 これを決定的方策と呼びます。

 確率論的に決まる場合は、次の確率で表現します。

$$ \pi(a | s) $$

 これを確率的方策と呼びます。

・ エージェントと環境のやりとり:(2)

 2.1.2項で、状態$S_t$において、エージェントの行動$A_t$によって報酬$R_t$が得られ、状態$S_{t+1}$に遷移する過程・・・→「$S_t$」→「$A_t$」→「$R_t$」→「$S_{t+1}$」→・・・を確認しました。
 この節では次のことを確認しました。ある状態$s$において「方策」により行動$a$が決まります。状態$s$と行動$a$に従う「報酬関数」により得られる報酬$r$が決まり、また状態$s$と行動$a$に従う「状態遷移」により次の状態$s'$になります。

 つまり、各時刻において・・・→「$S_t = s$」→($a = \mu(s)$または$\pi(a | s)$)→「$A_t = a$」→($r = r(s, a, s')$または$r = r(s')$)→「$R_t = r$」→($s' = f(s, a)$または$p(s' | s, a)$)→「$S_{t+1} = s'$」→・・・と続きます。

 (問題設定によりますが、)「状態遷移」「報酬」「方策」はどの時刻$t = 0, 1, 2, \dots$であっても関数や確率分布は変わりません。よって、次の関係が成り立ちます。

$$ \begin{aligned} f(s, a) &= f(S_t = s, A_t = a) \\ p(s' | s, a) &= p(S_{t+1} = s' | S_t = s, A_t = a) \\ r(s, a, s') &= r(S_t = s, A_t = a, S_{t+1} = s') \\ \mu(s) &= \mu(S_t = s) \\ \pi(a | s) &= \pi(A_t = a | S_t = s) \end{aligned} $$

 時刻に依存しないので、条件等から時刻$t$の情報を外せます。

・関数と分布の比較

 最後に、「決定論的に決まる」と「確率論的に決まる」ということを、関数と確率分布のグラフからイメージしてみます(分かりにくければ飛ばしてください)。

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

# 利用するライブラリ
import numpy as np
from scipy.stats import norm, binom # ガウス分布, 二項分布
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

 SciPyライブラリのstatsモジュールからガウス分布(正規分布)normを使います。
 アニメーションの作成に、MatplotlibライブラリのanimationモジュールからFuncAnimationを使います。不要であれば省略してください。

・決定論的な変化

 まずは、$x$の関数により$y$が決まる様子をグラフで確認します。

 例として、2次関数のグラフを作成します

# x軸の値を作成
x = np.arange(-5, 5.1, 0.1)

# y軸の値を計算(関数を指定)
f_x = x**2

# 曲線上の点のx軸の値を指定
x_val = 3

# 曲線上の点のy軸の値を計算(関数を指定)
y_val = x_val**2

# 関数のグラフを作成
plt.figure(figsize=(8, 6))
plt.plot(x, f_x) # 関数の曲線
plt.scatter(x_val, y_val, color='red', s=100) # 曲線上の点
plt.vlines(x=x_val, ymin=np.min(f_x)-1, ymax=x_val**2, color='red', linestyle=':') # x軸の補助線
plt.hlines(y=y_val, xmin=np.min(x)-0.5, xmax=x_val, color='red', linestyle=':') # y軸の補助線
plt.xlabel('x')
plt.ylabel('y = f(x)')
plt.suptitle('$f(x) = x^2$', fontsize=20)
plt.title('(x, f(x))=('+str(x_val)+', '+str(np.round(y_val, 2))+')', loc='left') # 曲線上の点の値
plt.grid()
plt.xlim(np.min(x)-0.5, np.max(x)+0.5)
plt.ylim(np.min(f_x)-1, np.max(f_x)+1)
plt.show()

関数のイメージ

 青色の曲線が2次関数のグラフです。関数によってグラフの形状が決まります。
 曲線上の赤色の点を見ると、$x$の値に従い$y$が1つに決まるのが分かります。

 $x$の変化による$y$の変化をアニメーションで確認します。

・作図コード(クリックで展開)

# x軸の値を作成
x = np.arange(-5, 5.1, 0.1)

# y軸の値を計算(関数を指定)
f_x = x**2

# 図を初期化
fig = plt.figure(figsize=(8, 6))
plt.suptitle('$f(x) = x^2$', fontsize=20)

# 作図処理を関数として定義
def update(i):
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のx軸の値を取得
    x_val = x[i]
    
    # 曲線上の点のy軸の値を計算
    y_val = x_val**2
    
    # 関数のグラフを作成
    plt.plot(x, f_x) # 関数の曲線
    plt.scatter(x_val, y_val, color='red', s=100) # 曲線上の点
    plt.vlines(x=x_val, ymin=np.min(f_x)-1, ymax=x_val**2, color='red', linestyle=':') # x軸の補助線
    plt.hlines(y=y_val, xmin=np.min(x)-0.5, xmax=x_val, color='red', linestyle=':') # y軸の補助線
    plt.xlabel('x')
    plt.ylabel('y = f(x)')
    plt.title('(x, f(x))=('+str(np.round(x_val, 1))+', '+str(np.round(y_val, 2))+')', loc='left') # 曲線上の点の値
    plt.grid()
    plt.xlim(np.min(x)-0.5, np.max(x)+0.5)
    plt.ylim(np.min(f_x)-1, np.max(f_x)+1)

# gif画像を作成
anime = FuncAnimation(fig, update, frames=len(x), interval=100)

# gif画像を保存
anime.save('function.gif')

関数のイメージ


・確率論的な変化

 次は、$x$をパラメータとして$y$の分布が決まる様子をグラフで確認します。

 離散値の場合の例として、二項分布のグラフを作成します。

# xの値(試行回数)を指定
x_val = 10

# x軸の値を作成
y = np.arange(20.1)

# y軸の値を計算(確率分布を指定)
p_y = binom.pmf(k=y, n=x_val, p=0.5)

# x軸の値(分布が取り得る値)を指定
y_val = 5

# 指定した値となる確率を計算(確率分布を指定)
p_val = binom.pmf(k=y_val, n=x_val, p=0.5)

# 分布のグラフを作成
plt.figure(figsize=(8, 6))
plt.bar(y, p_y, alpha=0.8, edgecolor='red', zorder=0) # 確率分布のバー
plt.scatter(y_val, p_val, color='orange', s=100, zorder=1) # 棒グラフ上の点
plt.vlines(x=y_val, ymin=0.0, ymax=p_val, color='orange', linestyle=':') # x軸の補助線
plt.hlines(y=p_val, xmin=np.min(y), xmax=y_val, color='orange', linestyle=':') # y軸の補助線
plt.xlabel('y')
plt.ylabel('p(y|x)')
plt.xticks(ticks=y)
plt.suptitle('p(y | x) = Bin(y | x, 0.5)', fontsize=20)
plt.title('(x, y, p(y|x))=('+str(x_val)+', '+str(y_val)+', '+str(np.round(p_val, 2))+')', loc='left') # 棒グラフ上の点の値
plt.grid()
plt.xlim(np.min(y)-0.5, np.max(y)+0.5)
plt.ylim(0.0, 0.5)
plt.show()

確率分布のイメージ

 赤色の枠線のバーが二項分布のグラフです。確率分布によってグラフの形状が決まりますが、パラメータ$x$の値によってもグラフの形状が変化します。
 $y$が取り得る値の可能性(ばらつき具合)を分布(棒グラフ)が表します。$x$の値に従い$y$の値の決まりやすさが変わるのが分かります。
 バー(上のオレンジ色の点)は、ある値$y$となる確率を表します。

 $x$の変化による$y$の分布の変化をアニメーションで確認します。

・作図コード(クリックで展開)

# x(試行回数)年て利用する値を指定
x_vals = np.arange(40.1)

# x軸の値を作成
y = np.arange(20.1)

# x軸の値(分布が取り得る値)を指定
y_val = 5

# 図を初期化
fig = plt.figure(figsize=(8, 6))
plt.suptitle('p(y | x) = Bin(y | x, 0.5)', fontsize=20)

# 作図処理を関数として定義
def update(i):
    # 前フレームのグラフを初期化
    plt.cla()
    
    # i番目のパラメータを取得
    x_val = x_vals[i]
    
    # y軸の値を計算(確率分布を指定)
    p_y = binom.pmf(k=y, n=x_val, p=0.5)
    
    # 指定した値となる確率を計算(確率分布を指定)
    p_val = binom.pmf(k=y_val, n=x_val, p=0.5)
    
    # 分布のグラフを作成
    plt.bar(y, p_y, alpha=0.8, edgecolor='red', zorder=0) # 確率分布のバー
    plt.scatter(y_val, p_val, color='orange', s=100, zorder=1) # 棒グラフ上の点
    plt.vlines(x=y_val, ymin=0.0, ymax=p_val, color='orange', linestyle=':') # x軸の補助線
    plt.hlines(y=p_val, xmin=np.min(y), xmax=y_val, color='orange', linestyle=':') # y軸の補助線
    plt.xlabel('y')
    plt.ylabel('p(y|x)')
    plt.xticks(ticks=y)
    plt.title('(x, y, p(y|x))=('+str(x_val)+', '+str(y_val)+', '+str(np.round(p_val, 2))+')', loc='left') # 棒グラフ上の点の値
    plt.grid()
    plt.xlim(np.min(y)-0.5, np.max(y)+0.5)
    plt.ylim(0.0, 0.5)

# gif画像を作成
anime = FuncAnimation(fig, update, frames=len(x_vals), interval=100)

# gif画像を保存
anime.save('distribution.gif')

確率分布のイメージ

 $y$が様々な値を取るため、期待値を求めます(全ての値を考慮します)。

 1つの値しかとらない確率分布を用意することで、関数と同じ結果を得られます(確率分布で関数を表現できます)。

# x軸の値を作成
y = np.arange(10.1)

# y軸の値を計算(確率分布を指定)
p_y = np.zeros_like(y)

# x軸の値(分布が取り得る値)を指定
y_val = 6

# 指定した値となる確率を計算(確率分布を指定)
p_y[y_val] = 1.0

# 分布のグラフを作成
plt.figure(figsize=(8, 6))
plt.bar(y, p_y, alpha=0.8, edgecolor='red', zorder=0) # 確率分布のバー
plt.scatter(y_val, 1.0, color='orange', s=100, zorder=1) # 棒グラフ上の点
plt.vlines(x=y_val, ymin=0.0, ymax=1.0, color='orange', linestyle=':') # x軸の補助線
plt.hlines(y=1.0, xmin=np.min(y), xmax=y_val, color='orange', linestyle=':') # y軸の補助線
plt.xlabel('y')
plt.ylabel('p(y|x)')
plt.xticks(ticks=y)
plt.suptitle('y = f(x) = p(y | x)', fontsize=20)
plt.title('(y, p(y|x))=('+str(y_val)+', 1)', loc='left') # 棒グラフ上の点の値
plt.grid()
plt.xlim(np.min(y)-0.5, np.max(y)+0.5)
plt.show()

確率分布を関数として扱う場合のイメージ

 このグラフ(確率分布)は、$y$が6となる確率が1で、それ以外の値となる確率が0の分布と言えます。これだと$x$が考慮されていないので不十分ですが、必ず$y$が1つの値に決まる確率分布$p(y | s)$を設定することで、$y = f(x)$と同じ結果を表現できます。

参考文献


おわりに

 もう少し分かりやすくなると思ってグラフのくだりを書き足したのですが、いまいちピンときませんね。もったいないので残してしまいましたが、ん?となった方はさっさと忘れてください。

【次節の内容】

www.anarchive-beta.com