深層強化学習①

こんどはこんなのを試してみた。

 

そのままでは面白くないので、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)

  f:id:kazuzo88:20180812171448p:plain

# 初期の方策を決定するパラメータ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にしてみても、コアの部分の理解が深まるわけがなかった。