強化学習のTD制御手法には大きく分けてOn-policyとOff-policyの2つのアプローチがあります。前者の代表的なアルゴリズムが SARSA です。SARSAは、エージェントが実際に従う方策(行動方策)そのものを改善していくOn-policy手法であり、学習中の安全性やオンライン性能に優れた特徴を持ちます。
SARSAという名前は、更新に使う5つの要素 $(S_t, A_t, R_{t+1}, S_{t+1}, A_{t+1})$ の頭文字を並べたものです。この命名が示す通り、SARSAでは「次の状態で実際に選んだ行動」の価値を用いて更新を行います。この一見小さな違いが、Q学習とは質的に異なる学習結果をもたらします。
本記事の内容
- SARSAの名前の由来と更新則の導出
- Q学習との本質的な違い(On-policy vs Off-policy)
- Expected SARSAの導出と性質
- n-step SARSAと適格度トレース(SARSA(λ))
- Cliff WalkingおよびWindy GridWorldでのPython実装と比較実験
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
SARSAとは
名前の由来
SARSAの名前は、1ステップの更新に必要な5つの要素の頭文字に由来します。
$$ (S_t, \; A_t, \; R_{t+1}, \; S_{t+1}, \; A_{t+1}) $$
すなわち、現在の State(状態)、現在の Action(行動)、得られた Reward(報酬)、次の State(状態)、次の Action(行動)です。Q学習が $(S_t, A_t, R_{t+1}, S_{t+1})$ の4つの要素しか必要としないのに対し、SARSAでは 次の行動 $A_{t+1}$ が明示的に必要 である点が決定的に異なります。
SARSAの更新則
行動価値関数 $Q^\pi(s, a)$ のベルマン方程式は次の通りです。
$$ Q^\pi(s, a) = \mathbb{E}_\pi \left[ R_{t+1} + \gamma Q^\pi(S_{t+1}, A_{t+1}) \mid S_t = s, A_t = a \right] $$
Q学習では右辺を $R_{t+1} + \gamma \max_{a’} Q(S_{t+1}, a’)$ で近似しましたが、SARSAでは方策 $\pi$ に従って選んだ 実際の次行動 $A_{t+1}$ を使います。この期待値を1サンプルで近似し、確率的近似の枠組みで更新則を導出します。
ステップ1: ベルマン方程式のサンプルベース近似
$$ Q^\pi(S_t, A_t) \approx R_{t+1} + \gamma Q^\pi(S_{t+1}, A_{t+1}) $$
ここで $A_{t+1} \sim \pi(\cdot \mid S_{t+1})$ です。
ステップ2: 現在の推定値をターゲットに向けて学習率 $\alpha$ で更新
$$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[ R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) – Q(S_t, A_t) \right] $$
これが SARSAの更新則 です。TD誤差は
$$ \delta_t = R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) – Q(S_t, A_t) $$
と定義されます。
On-policyである理由
SARSAがOn-policyである理由を明確にしましょう。更新則のターゲット部分を見ると
$$ \text{ターゲット} = R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) $$
であり、$A_{t+1}$ は 現在の方策 $\pi$(たとえばε-greedy方策)に従って実際に選んだ行動 です。つまり、SARSAは「今の方策に従ったらどうなるか」を学習しています。
一方、Q学習のターゲットは $R_{t+1} + \gamma \max_{a’} Q(S_{t+1}, a’)$ であり、$\max$ は 貪欲方策 に対応します。行動方策がε-greedyであっても、ターゲットは貪欲方策で評価するため、Off-policyです。
| SARSA(On-policy) | Q学習(Off-policy) | |
|---|---|---|
| ターゲットの次行動 | $Q(S_{t+1}, A_{t+1})$(方策 $\pi$ で選択) | $\max_{a’} Q(S_{t+1}, a’)$(貪欲方策) |
| 学習対象 | 現在の方策 $\pi$ の価値 | 最適方策 $\pi^*$ の価値 |
| 方策改善の方法 | $Q$ に基づくε-greedyが自然に改善 | $Q$ の $\max$ が暗黙的に最適方策 |
Q学習との本質的な違い
Q学習とSARSAの更新則を並べて比較しましょう。
$$ \begin{align} \text{Q学習:} \quad & Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[ R_{t+1} + \gamma \max_{a’} Q(S_{t+1}, a’) – Q(S_t, A_t) \right] \\ \text{SARSA:} \quad & Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[ R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) – Q(S_t, A_t) \right] \end{align} $$
違いは $\max_{a’} Q(S_{t+1}, a’)$ と $Q(S_{t+1}, A_{t+1})$ の部分のみです。しかし、この違いが以下の重要な帰結をもたらします。
1. 安全性の考慮
SARSAはε-greedy方策に従って選んだ行動の価値を使うため、「ランダムに危険な行動を取る可能性」を価値関数に反映します。その結果、危険な状態の近くの価値が低く推定され、安全な方策を学習する傾向があります。
2. 学習中のオンライン性能
Q学習は最適方策を直接学びますが、ε-greedy探索中は最適方策通りに行動しないため、学習中に崖に落ちるなどの事故が多発します。SARSAはε-greedy方策自体の価値を学ぶため、探索のリスクを織り込んだ方策を返し、学習中の性能が安定します。
3. 収束先の違い
$\varepsilon > 0$ が固定の場合: – Q学習: 最適方策 $\pi^*$ の行動価値 $Q^*$ に収束 – SARSA: ε-greedy方策の行動価値 $Q^\pi$ に収束($\varepsilon$ に依存)
$\varepsilon \to 0$(GLIE条件を満たす減衰型ε-greedy)の場合: – 両者とも $Q^*$ に収束
Expected SARSAの導出
SARSAの更新では、次の行動 $A_{t+1}$ のサンプリングに起因する分散があります。この分散を低減するのが Expected SARSA です。
導出
SARSAのターゲットは
$$ R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) $$
ですが、$A_{t+1}$ は方策 $\pi$ からサンプルされるので、この項の期待値は
$$ \mathbb{E}_\pi [R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) \mid S_{t+1}] = R_{t+1} + \gamma \sum_{a’} \pi(a’ \mid S_{t+1}) Q(S_{t+1}, a’) $$
です。Expected SARSAでは、サンプル $Q(S_{t+1}, A_{t+1})$ の代わりにこの期待値を直接計算します。
$$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[ R_{t+1} + \gamma \sum_{a’} \pi(a’ \mid S_{t+1}) Q(S_{t+1}, a’) – Q(S_t, A_t) \right] $$
Expected SARSAの性質
Expected SARSAの重要な性質をまとめます。
分散の低減: サンプリングの確率性を解析的に平均化するため、SARSAよりも分散が小さくなります。
$$ \text{Var}[\text{Expected SARSA target}] \leq \text{Var}[\text{SARSA target}] $$
Q学習との関係: ε-greedy方策のもとでExpected SARSAの期待値を展開すると
$$ \sum_{a’} \pi(a’ \mid s’) Q(s’, a’) = (1 – \varepsilon) \max_{a’} Q(s’, a’) + \frac{\varepsilon}{|\mathcal{A}|} \sum_{a’} Q(s’, a’) $$
$\varepsilon = 0$ のとき、Expected SARSAは $\max_{a’} Q(s’, a’)$ に一致し、Q学習と等価 になります。つまり、Expected SARSAはSARSAとQ学習を連続的につなぐアルゴリズムと見なせます。
n-step SARSAの一般化
1ステップのSARSAをn-stepに一般化すると、TDとモンテカルロ法のスペクトラム上を移動できます。
n-step収益
n-stepのSARSAターゲット(n-step収益)は次のように定義されます。
$$ G_t^{(n)} = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots + \gamma^{n-1} R_{t+n} + \gamma^n Q(S_{t+n}, A_{t+n}) $$
これを用いた更新則は
$$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[ G_t^{(n)} – Q(S_t, A_t) \right] $$
です。$n = 1$ のとき通常のSARSA、$n \to \infty$ のときモンテカルロ法に一致します。
バイアスと分散のトレードオフ
n-stepの $n$ を大きくすると
- バイアスが減少: ブートストラップの割合が減り、推定値への依存度が下がる
- 分散が増加: より多くのランダムな報酬をサンプルするため
これは1-step TD(低分散・高バイアス)とモンテカルロ法(高分散・低バイアス)の中間を $n$ で制御できることを意味します。
SARSA(λ)と適格度トレース
n-step SARSAの自然な拡張として、全ての $n$ に対するn-step収益の加重平均を考えるのが SARSA(λ) です。
λ-収益
パラメータ $\lambda \in [0, 1]$ を導入して、n-step収益の幾何加重平均を定義します。
$$ G_t^\lambda = (1 – \lambda) \sum_{n=1}^{\infty} \lambda^{n-1} G_t^{(n)} $$
重み $(1 – \lambda)\lambda^{n-1}$ の総和が1になることは容易に確認できます。
$$ (1 – \lambda) \sum_{n=1}^{\infty} \lambda^{n-1} = (1 – \lambda) \cdot \frac{1}{1 – \lambda} = 1 $$
$\lambda = 0$ のとき $G_t^\lambda = G_t^{(1)}$(1-step SARSA)、$\lambda = 1$ のとき $G_t^\lambda = G_t$(モンテカルロ収益)に対応します。
適格度トレース
$G_t^\lambda$ を直接計算するにはエピソード全体が必要ですが、適格度トレース(Eligibility Trace) を使うと、各ステップで逐次的に更新できます。
各状態-行動ペア $(s, a)$ に対して適格度トレース $e_t(s, a)$ を保持します。
$$ e_t(s, a) = \begin{cases} \gamma \lambda \, e_{t-1}(s, a) + 1 & \text{if } s = S_t \text{ and } a = A_t \\ \gamma \lambda \, e_{t-1}(s, a) & \text{otherwise} \end{cases} $$
そして、全ての $(s, a)$ について一括更新します。
$$ Q(s, a) \leftarrow Q(s, a) + \alpha \, \delta_t \, e_t(s, a) $$
ここで $\delta_t = R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) – Q(S_t, A_t)$ は通常のSARSA TD誤差です。
直感的には、適格度トレースは「最近訪れた状態-行動ペア」を記憶する仕組みです。$\gamma \lambda$ のべき乗で指数的に減衰するため、最近の経験ほど強く更新されます。
SARSAのアルゴリズム
アルゴリズム: SARSA(On-policy TD制御)
- $Q(s, a)$ を任意に初期化(終端状態では $Q(\text{terminal}, \cdot) = 0$)
- 各エピソードについて繰り返し:
- 初期状態 $S$ を観測
- $Q$ に基づくε-greedy方策で行動 $A$ を選択
- 各ステップについて繰り返し($S$ が終端状態でない間):
- 行動 $A$ を実行し、報酬 $R$ と次状態 $S’$ を観測
- $Q$ に基づくε-greedy方策で次行動 $A’$ を選択
- $Q(S, A) \leftarrow Q(S, A) + \alpha \left[ R + \gamma Q(S’, A’) – Q(S, A) \right]$
- $S \leftarrow S’$, $A \leftarrow A’$
Q学習との違いに注意してください。SARSAでは ステップ2でAを選択 し、ループ内でも 先に次行動A’を選択してから 更新を行います。
Pythonでの実装
Cliff Walking環境でのSARSA実装
import numpy as np
import matplotlib.pyplot as plt
class CliffWalking:
"""Cliff Walking環境(4x12グリッド)"""
def __init__(self):
self.rows = 4
self.cols = 12
self.start = (3, 0)
self.goal = (3, 11)
self.cliff = [(3, c) for c in range(1, 11)]
self.actions = [0, 1, 2, 3] # 上, 下, 左, 右
self.action_effects = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}
def reset(self):
self.state = self.start
return self.state
def step(self, action):
dr, dc = self.action_effects[action]
new_r = np.clip(self.state[0] + dr, 0, self.rows - 1)
new_c = np.clip(self.state[1] + dc, 0, self.cols - 1)
self.state = (new_r, new_c)
if self.state in self.cliff:
self.state = self.start
return self.state, -100, False
elif self.state == self.goal:
return self.state, -1, True
else:
return self.state, -1, False
def epsilon_greedy(Q, state, epsilon, n_actions):
"""ε-greedy方策による行動選択"""
r, c = state
if np.random.random() < epsilon:
return np.random.randint(n_actions)
else:
return np.argmax(Q[r, c])
def sarsa(env, episodes=500, alpha=0.5, gamma=1.0, epsilon=0.1):
"""SARSAアルゴリズム"""
Q = np.zeros((env.rows, env.cols, len(env.actions)))
rewards_per_episode = []
for ep in range(episodes):
state = env.reset()
# SARSAでは最初に行動を選択(S, A が必要)
action = epsilon_greedy(Q, state, epsilon, len(env.actions))
total_reward = 0
done = False
while not done:
# 行動を実行してR, S'を取得
next_state, reward, done = env.step(action)
total_reward += reward
# 次の行動A'をε-greedyで選択
next_action = epsilon_greedy(Q, next_state, epsilon, len(env.actions))
# SARSA更新: Q(S,A) ← Q(S,A) + α[R + γQ(S',A') - Q(S,A)]
r, c = state
nr, nc = next_state
td_target = reward + gamma * Q[nr, nc, next_action] * (1 - done)
td_error = td_target - Q[r, c, action]
Q[r, c, action] += alpha * td_error
# S ← S', A ← A'
state = next_state
action = next_action
rewards_per_episode.append(total_reward)
return Q, rewards_per_episode
def expected_sarsa(env, episodes=500, alpha=0.5, gamma=1.0, epsilon=0.1):
"""Expected SARSAアルゴリズム"""
Q = np.zeros((env.rows, env.cols, len(env.actions)))
n_actions = len(env.actions)
rewards_per_episode = []
for ep in range(episodes):
state = env.reset()
total_reward = 0
done = False
while not done:
r, c = state
action = epsilon_greedy(Q, state, epsilon, n_actions)
next_state, reward, done = env.step(action)
nr, nc = next_state
total_reward += reward
# Expected SARSAターゲット: Σπ(a'|s')Q(s',a')
expected_q = 0
greedy_action = np.argmax(Q[nr, nc])
for a in range(n_actions):
if a == greedy_action:
prob = 1 - epsilon + epsilon / n_actions
else:
prob = epsilon / n_actions
expected_q += prob * Q[nr, nc, a]
td_target = reward + gamma * expected_q * (1 - done)
Q[r, c, action] += alpha * (td_target - Q[r, c, action])
state = next_state
rewards_per_episode.append(total_reward)
return Q, rewards_per_episode
def q_learning(env, episodes=500, alpha=0.5, gamma=1.0, epsilon=0.1):
"""Q学習(比較用)"""
Q = np.zeros((env.rows, env.cols, len(env.actions)))
rewards_per_episode = []
for ep in range(episodes):
state = env.reset()
total_reward = 0
done = False
while not done:
r, c = state
action = epsilon_greedy(Q, state, epsilon, len(env.actions))
next_state, reward, done = env.step(action)
nr, nc = next_state
total_reward += reward
td_target = reward + gamma * np.max(Q[nr, nc]) * (1 - done)
Q[r, c, action] += alpha * (td_target - Q[r, c, action])
state = next_state
rewards_per_episode.append(total_reward)
return Q, rewards_per_episode
# 3手法の比較実験
n_runs = 50
n_episodes = 500
sarsa_all = np.zeros((n_runs, n_episodes))
expected_sarsa_all = np.zeros((n_runs, n_episodes))
q_learning_all = np.zeros((n_runs, n_episodes))
for run in range(n_runs):
_, rewards_s = sarsa(CliffWalking(), episodes=n_episodes)
_, rewards_e = expected_sarsa(CliffWalking(), episodes=n_episodes)
_, rewards_q = q_learning(CliffWalking(), episodes=n_episodes)
sarsa_all[run] = rewards_s
expected_sarsa_all[run] = rewards_e
q_learning_all[run] = rewards_q
# 移動平均
window = 10
def smooth(data, w):
return np.convolve(np.mean(data, axis=0), np.ones(w)/w, mode='valid')
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(smooth(sarsa_all, window), label='SARSA', linewidth=2)
ax.plot(smooth(expected_sarsa_all, window), label='Expected SARSA', linewidth=2)
ax.plot(smooth(q_learning_all, window), label='Q-Learning', linewidth=2)
ax.set_xlabel('Episode', fontsize=12)
ax.set_ylabel('Sum of Rewards (averaged over runs)', fontsize=12)
ax.set_title('Cliff Walking: SARSA vs Expected SARSA vs Q-Learning', fontsize=14)
ax.legend(fontsize=12)
ax.set_ylim(-150, -10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Windy GridWorld環境での実装
Windy GridWorldは、特定の列に「横風」が吹いており、エージェントが上方向に押される環境です。SARSAの動作を確認するのに適した標準的な問題です。
import numpy as np
import matplotlib.pyplot as plt
class WindyGridWorld:
"""Windy GridWorld環境(Sutton & Barto, Example 6.5)"""
def __init__(self):
self.rows = 7
self.cols = 10
self.start = (3, 0)
self.goal = (3, 7)
# 各列の風の強さ(上方向への押し上げ量)
self.wind = [0, 0, 0, 1, 1, 1, 2, 2, 1, 0]
self.actions = [0, 1, 2, 3] # 上, 下, 左, 右
self.action_effects = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}
def reset(self):
self.state = self.start
return self.state
def step(self, action):
dr, dc = self.action_effects[action]
# 風の効果を加味(上方向 = 行番号が減少)
new_r = self.state[0] + dr - self.wind[self.state[1]]
new_c = self.state[1] + dc
new_r = np.clip(new_r, 0, self.rows - 1)
new_c = np.clip(new_c, 0, self.cols - 1)
self.state = (new_r, new_c)
if self.state == self.goal:
return self.state, 0, True
else:
return self.state, -1, False
def sarsa_windy(env, episodes=500, alpha=0.5, gamma=1.0, epsilon=0.1):
"""SARSAによるWindy GridWorldの学習"""
Q = np.zeros((env.rows, env.cols, len(env.actions)))
steps_per_episode = []
for ep in range(episodes):
state = env.reset()
action = epsilon_greedy(Q, state, epsilon, len(env.actions))
steps = 0
done = False
while not done:
next_state, reward, done = env.step(action)
next_action = epsilon_greedy(Q, next_state, epsilon, len(env.actions))
r, c = state
nr, nc = next_state
Q[r, c, action] += alpha * (
reward + gamma * Q[nr, nc, next_action] * (1 - done) - Q[r, c, action]
)
state = next_state
action = next_action
steps += 1
steps_per_episode.append(steps)
return Q, steps_per_episode
# Windy GridWorldでの学習
env_windy = WindyGridWorld()
Q_windy, steps = sarsa_windy(env_windy, episodes=500, alpha=0.5, gamma=1.0, epsilon=0.1)
# 学習した方策の経路を取得
def get_trajectory(env, Q):
"""学習した方策で1エピソード実行し、経路を取得"""
state = env.reset()
trajectory = [state]
done = False
max_steps = 100
for _ in range(max_steps):
r, c = state
action = np.argmax(Q[r, c])
state, _, done = env.step(action)
trajectory.append(state)
if done:
break
return trajectory
trajectory = get_trajectory(WindyGridWorld(), Q_windy)
fig, axes = plt.subplots(1, 2, figsize=(16, 5))
# ステップ数の推移(累積タイムステップ vs エピソード数)
cumulative_steps = np.cumsum(steps)
# エピソード番号 vs 累積ステップ数(Sutton & Bartoの図6.3に相当)
axes[0].plot(cumulative_steps, range(len(steps)))
axes[0].set_xlabel('Time Steps', fontsize=12)
axes[0].set_ylabel('Episodes', fontsize=12)
axes[0].set_title('Windy GridWorld: SARSA Learning Progress', fontsize=14)
axes[0].grid(True, alpha=0.3)
# 学習した経路の可視化
traj_rows = [s[0] for s in trajectory]
traj_cols = [s[1] for s in trajectory]
# 風の強さを背景色で表示
wind_grid = np.zeros((env_windy.rows, env_windy.cols))
for c in range(env_windy.cols):
wind_grid[:, c] = env_windy.wind[c]
axes[1].imshow(wind_grid, cmap='Blues', alpha=0.3, origin='upper')
axes[1].plot(traj_cols, traj_rows, 'ro-', markersize=6, linewidth=2, label='Trajectory')
axes[1].plot(env_windy.start[1], env_windy.start[0], 'gs', markersize=15, label='Start')
axes[1].plot(env_windy.goal[1], env_windy.goal[0], 'r*', markersize=20, label='Goal')
# 風の強さを列の下に表示
for c in range(env_windy.cols):
if env_windy.wind[c] > 0:
axes[1].annotate(f'↑{env_windy.wind[c]}', (c, env_windy.rows - 0.3),
ha='center', fontsize=10, color='blue')
axes[1].set_xlim(-0.5, env_windy.cols - 0.5)
axes[1].set_ylim(env_windy.rows - 0.5, -0.5)
axes[1].set_xticks(range(env_windy.cols))
axes[1].set_yticks(range(env_windy.rows))
axes[1].grid(True, alpha=0.3)
axes[1].set_title('Windy GridWorld: Learned Path', fontsize=14)
axes[1].legend(fontsize=10)
axes[1].set_aspect('equal')
plt.tight_layout()
plt.show()
# ステップ数の収束状況
print(f"最初の10エピソードの平均ステップ数: {np.mean(steps[:10]):.1f}")
print(f"最後の10エピソードの平均ステップ数: {np.mean(steps[-10:]):.1f}")
print(f"学習した経路のステップ数: {len(trajectory) - 1}")
SARSA(λ)の実装
適格度トレースを用いたSARSA(λ)の実装例です。
import numpy as np
import matplotlib.pyplot as plt
class CliffWalking:
"""Cliff Walking環境(再掲)"""
def __init__(self):
self.rows = 4
self.cols = 12
self.start = (3, 0)
self.goal = (3, 11)
self.cliff = [(3, c) for c in range(1, 11)]
self.actions = [0, 1, 2, 3]
self.action_effects = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}
def reset(self):
self.state = self.start
return self.state
def step(self, action):
dr, dc = self.action_effects[action]
new_r = np.clip(self.state[0] + dr, 0, self.rows - 1)
new_c = np.clip(self.state[1] + dc, 0, self.cols - 1)
self.state = (new_r, new_c)
if self.state in self.cliff:
self.state = self.start
return self.state, -100, False
elif self.state == self.goal:
return self.state, -1, True
else:
return self.state, -1, False
def sarsa_lambda(env, episodes=500, alpha=0.5, gamma=1.0,
epsilon=0.1, lam=0.9):
"""SARSA(λ)アルゴリズム(適格度トレース使用)"""
n_actions = len(env.actions)
Q = np.zeros((env.rows, env.cols, n_actions))
rewards_per_episode = []
for ep in range(episodes):
# 適格度トレースの初期化(各エピソード開始時にゼロ)
e = np.zeros_like(Q)
state = env.reset()
r, c = state
if np.random.random() < epsilon:
action = np.random.randint(n_actions)
else:
action = np.argmax(Q[r, c])
total_reward = 0
done = False
while not done:
next_state, reward, done = env.step(action)
nr, nc = next_state
total_reward += reward
# 次の行動をε-greedyで選択
if np.random.random() < epsilon:
next_action = np.random.randint(n_actions)
else:
next_action = np.argmax(Q[nr, nc])
# TD誤差の計算
r_s, c_s = state
delta = reward + gamma * Q[nr, nc, next_action] * (1 - done) - Q[r_s, c_s, action]
# 適格度トレースの更新(累積トレース)
e[r_s, c_s, action] += 1
# 全状態-行動ペアを一括更新
Q += alpha * delta * e
# トレースの減衰
e *= gamma * lam
state = next_state
action = next_action
rewards_per_episode.append(total_reward)
return Q, rewards_per_episode
# λの異なるSARSA(λ)の比較
lambdas = [0.0, 0.3, 0.6, 0.9]
n_runs = 30
n_episodes = 300
fig, ax = plt.subplots(figsize=(12, 6))
for lam in lambdas:
all_rewards = np.zeros((n_runs, n_episodes))
for run in range(n_runs):
_, rewards = sarsa_lambda(CliffWalking(), episodes=n_episodes, lam=lam, alpha=0.3)
all_rewards[run] = rewards
mean_rewards = np.mean(all_rewards, axis=0)
window = 10
smoothed = np.convolve(mean_rewards, np.ones(window)/window, mode='valid')
ax.plot(smoothed, label=f'SARSA(λ={lam})', linewidth=2)
ax.set_xlabel('Episode', fontsize=12)
ax.set_ylabel('Sum of Rewards', fontsize=12)
ax.set_title('Cliff Walking: SARSA(λ) for Different λ Values', fontsize=14)
ax.legend(fontsize=12)
ax.set_ylim(-200, -10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
結果の考察
Cliff Walking環境 では、SARSAはQ学習に比べて学習中の報酬が安定しています。SARSAは崖から離れた安全な経路を学習する傾向があり、ε-greedy探索中に崖に落ちるリスクを反映した方策を返します。
Expected SARSA はSARSAよりも分散が小さく、安定した学習を実現します。特にα(学習率)が大きい場合にその差が顕著になります。
Windy GridWorld では、SARSAは風の効果を考慮した最適経路を効率的に学習します。累積タイムステップとエピソード数の関係から、学習の進行が確認できます。
SARSA(λ) では、λの値が大きいほどモンテカルロ法に近づき、信用割当が長い時間スパンで行われます。しかし、λが大きすぎると分散が増加し、学習が不安定になることがあります。
まとめ
本記事では、SARSAについて理論と実装の両面から解説しました。
- SARSAは On-policy TD制御 であり、更新則 $Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha [R + \gamma Q(S’, A’) – Q(S, A)]$ で現在の方策の価値を学習します
- Q学習との違いは「次行動を実際の方策から選ぶ(SARSA)か、$\max$ で選ぶ(Q学習)か」の一点です
- On-policyの利点として、探索中のリスクを考慮した安全な方策を学習できます
- Expected SARSA は次行動の期待値を解析的に計算することで分散を低減し、$\varepsilon = 0$ でQ学習と一致します
- n-step SARSA はTDとモンテカルロ法の中間を制御し、SARSA(λ) は適格度トレースで効率的に実装できます
次のステップとして、以下の記事も参考にしてください。