Q学習やSARSAなどの価値ベースの手法は価値関数を学習し、そこから方策を導きます。一方、方策勾配法(Policy Gradient Methods)は方策そのものをパラメトリックに表現し、勾配上昇法で直接最適化します。連続行動空間や確率的方策が自然に扱える点が大きな利点です。
本記事の内容
- 方策のパラメトリック表現
- 目的関数 $J(\bm{\theta})$ の定義
- 方策勾配定理の導出
- REINFORCEアルゴリズム
- ベースラインによる分散低減
- Pythonでの実装
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
方策のパラメトリック表現
方策をパラメータ $\bm{\theta}$ で表します。
$$ \pi_{\bm{\theta}}(a|s) = P(A_t = a | S_t = s; \bm{\theta}) $$
離散行動空間: ソフトマックス方策
状態 $s$ での行動 $a$ の選好度(preference)を $h(s, a; \bm{\theta})$ として:
$$ \pi_{\bm{\theta}}(a|s) = \frac{\exp(h(s, a; \bm{\theta}))}{\sum_{a’} \exp(h(s, a’; \bm{\theta}))} $$
連続行動空間: ガウス方策
$$ \pi_{\bm{\theta}}(a|s) = \frac{1}{\sqrt{2\pi}\sigma} \exp\left(-\frac{(a – \mu_{\bm{\theta}}(s))^2}{2\sigma^2}\right) $$
ここで $\mu_{\bm{\theta}}(s)$ はパラメータ $\bm{\theta}$ で決まる平均値です。
目的関数
方策の良さを測る目的関数 $J(\bm{\theta})$ を定義します。エピソディックタスクでは、開始状態からの期待収益とします。
$$ J(\bm{\theta}) = E_{\pi_{\bm{\theta}}}[G_0] = E_{\pi_{\bm{\theta}}}\left[\sum_{t=0}^{T-1} \gamma^t R_{t+1}\right] $$
方策勾配法の目標は、$J(\bm{\theta})$ を最大化するパラメータ $\bm{\theta}$ を見つけることです。
$$ \bm{\theta} \leftarrow \bm{\theta} + \alpha \nabla_{\bm{\theta}} J(\bm{\theta}) $$
方策勾配定理
目的関数の勾配を計算するのが方策勾配法の核心です。
方策勾配定理の主張
$$ \boxed{\nabla_{\bm{\theta}} J(\bm{\theta}) = E_{\pi_{\bm{\theta}}}\left[\sum_{t=0}^{T-1} \nabla_{\bm{\theta}} \log \pi_{\bm{\theta}}(A_t|S_t) \cdot G_t\right]} $$
導出(1エピソードのシンプルなケース)
$\tau = (S_0, A_0, R_1, S_1, A_1, R_2, \dots, S_{T-1}, A_{T-1}, R_T)$ を軌道(trajectory)とします。
軌道の確率は:
$$ P(\tau | \bm{\theta}) = \mu(S_0) \prod_{t=0}^{T-1} \pi_{\bm{\theta}}(A_t|S_t) P(S_{t+1}|S_t, A_t) $$
ここで $\mu(S_0)$ は初期状態分布です。
目的関数:
$$ J(\bm{\theta}) = E_{\tau \sim \pi_{\bm{\theta}}}[R(\tau)] = \sum_{\tau} P(\tau|\bm{\theta}) R(\tau) $$
ここで $R(\tau) = \sum_{t=0}^{T-1} \gamma^t R_{t+1}$ は軌道の総収益です。
勾配を計算します。
$$ \begin{align} \nabla_{\bm{\theta}} J(\bm{\theta}) &= \nabla_{\bm{\theta}} \sum_{\tau} P(\tau|\bm{\theta}) R(\tau) \\ &= \sum_{\tau} \nabla_{\bm{\theta}} P(\tau|\bm{\theta}) R(\tau) \\ &= \sum_{\tau} P(\tau|\bm{\theta}) \frac{\nabla_{\bm{\theta}} P(\tau|\bm{\theta})}{P(\tau|\bm{\theta})} R(\tau) \\ &= \sum_{\tau} P(\tau|\bm{\theta}) \nabla_{\bm{\theta}} \log P(\tau|\bm{\theta}) \cdot R(\tau) \end{align} $$
3行目で対数微分のトリック(log-derivative trick)$\nabla f / f = \nabla \log f$ を使いました。
$\log P(\tau|\bm{\theta})$ を展開します。
$$ \begin{align} \log P(\tau|\bm{\theta}) &= \log \mu(S_0) + \sum_{t=0}^{T-1} \log \pi_{\bm{\theta}}(A_t|S_t) + \sum_{t=0}^{T-1} \log P(S_{t+1}|S_t, A_t) \end{align} $$
$\bm{\theta}$ に依存するのは $\pi_{\bm{\theta}}$ の項のみなので:
$$ \nabla_{\bm{\theta}} \log P(\tau|\bm{\theta}) = \sum_{t=0}^{T-1} \nabla_{\bm{\theta}} \log \pi_{\bm{\theta}}(A_t|S_t) $$
したがって:
$$ \nabla_{\bm{\theta}} J(\bm{\theta}) = E_{\tau}\left[\left(\sum_{t=0}^{T-1} \nabla_{\bm{\theta}} \log \pi_{\bm{\theta}}(A_t|S_t)\right) R(\tau)\right] $$
さらに因果性を考慮すると(時刻 $t$ の行動は $t$ 以前の報酬に影響しない):
$$ \nabla_{\bm{\theta}} J(\bm{\theta}) = E_{\tau}\left[\sum_{t=0}^{T-1} \nabla_{\bm{\theta}} \log \pi_{\bm{\theta}}(A_t|S_t) \cdot G_t\right] $$
ここで $G_t = \sum_{k=t}^{T-1} \gamma^{k-t} R_{k+1}$ は時刻 $t$ 以降の収益です。
環境の遷移確率 $P(s’|s,a)$ が消えている点が重要です。モデルフリーで勾配を推定できます。
REINFORCEアルゴリズム
REINFORCE(Williams, 1992)は方策勾配定理に基づく最も基本的なアルゴリズムです。
アルゴリズム
- 方策 $\pi_{\bm{\theta}}$ に従ってエピソードを生成: $S_0, A_0, R_1, \dots, S_{T-1}, A_{T-1}, R_T$
- 各時刻 $t$ について収益を計算: $G_t = \sum_{k=t}^{T-1} \gamma^{k-t} R_{k+1}$
- パラメータを更新: $\bm{\theta} \leftarrow \bm{\theta} + \alpha \gamma^t \nabla_{\bm{\theta}} \log \pi_{\bm{\theta}}(A_t|S_t) G_t$
問題点: 高い分散
$G_t$ は確率的であり、その分散が大きいと学習が不安定になります。
ベースラインによる分散低減
方策勾配定理において、収益 $G_t$ から状態のみに依存する関数 $b(S_t)$(ベースライン)を引いても勾配の期待値は変わりません。
$$ \nabla_{\bm{\theta}} J(\bm{\theta}) = E\left[\sum_{t=0}^{T-1} \nabla_{\bm{\theta}} \log \pi_{\bm{\theta}}(A_t|S_t) \cdot (G_t – b(S_t))\right] $$
不偏性の証明
$$ \begin{align} E_{A_t \sim \pi_{\bm{\theta}}}[\nabla \log \pi_{\bm{\theta}}(A_t|S_t) \cdot b(S_t)] &= b(S_t) \sum_a \nabla \pi_{\bm{\theta}}(a|S_t) \\ &= b(S_t) \nabla \sum_a \pi_{\bm{\theta}}(a|S_t) \\ &= b(S_t) \nabla 1 = 0 \end{align} $$
$\sum_a \pi(a|s) = 1$ の勾配はゼロなので、ベースラインを引いてもバイアスは生じません。
ベースラインの選択
最も一般的なベースラインは状態価値関数 $V(S_t)$ です。このとき $G_t – V(S_t)$ はアドバンテージ(advantage)と呼ばれ、その行動が平均的な行動よりどれだけ良いかを表します。
Pythonでの実装
CartPoleでのREINFORCE
import numpy as np
import matplotlib.pyplot as plt
class SimpleCartPole:
"""簡易CartPole環境(OpenAI Gym不要)"""
def __init__(self):
self.gravity = 9.8
self.masscart = 1.0
self.masspole = 0.1
self.total_mass = self.masscart + self.masspole
self.length = 0.5
self.polemass_length = self.masspole * self.length
self.force_mag = 10.0
self.tau = 0.02
self.theta_threshold = 12 * np.pi / 180
self.x_threshold = 2.4
self.state = None
def reset(self):
self.state = np.random.uniform(-0.05, 0.05, size=4)
return self.state.copy()
def step(self, action):
x, x_dot, theta, theta_dot = self.state
force = self.force_mag if action == 1 else -self.force_mag
costheta = np.cos(theta)
sintheta = np.sin(theta)
temp = (force + self.polemass_length * theta_dot**2 * sintheta) / self.total_mass
thetaacc = (self.gravity * sintheta - costheta * temp) / (
self.length * (4.0/3.0 - self.masspole * costheta**2 / self.total_mass))
xacc = temp - self.polemass_length * thetaacc * costheta / self.total_mass
x = x + self.tau * x_dot
x_dot = x_dot + self.tau * xacc
theta = theta + self.tau * theta_dot
theta_dot = theta_dot + self.tau * thetaacc
self.state = np.array([x, x_dot, theta, theta_dot])
done = (abs(x) > self.x_threshold or abs(theta) > self.theta_threshold)
reward = 1.0 if not done else 0.0
return self.state.copy(), reward, done
class PolicyNetwork:
"""線形ソフトマックス方策"""
def __init__(self, n_features, n_actions):
self.n_features = n_features
self.n_actions = n_actions
# パラメータ(重み)
self.theta = np.zeros((n_features, n_actions))
def softmax(self, x):
exp_x = np.exp(x - np.max(x))
return exp_x / np.sum(exp_x)
def get_action_probs(self, state):
"""状態から行動確率を計算"""
logits = state @ self.theta
return self.softmax(logits)
def select_action(self, state):
"""確率的に行動を選択"""
probs = self.get_action_probs(state)
action = np.random.choice(self.n_actions, p=probs)
return action, probs
def compute_log_grad(self, state, action):
"""∇log π(a|s; θ) を計算"""
probs = self.get_action_probs(state)
# ソフトマックスの勾配: ∂logπ(a|s)/∂θ_{ij} = (1_{j=a} - π(j|s)) * s_i
grad = np.zeros_like(self.theta)
for j in range(self.n_actions):
indicator = 1.0 if j == action else 0.0
grad[:, j] = (indicator - probs[j]) * state
return grad
def reinforce(env, n_episodes=1000, alpha=0.01, gamma=0.99, use_baseline=False):
"""REINFORCEアルゴリズム"""
policy = PolicyNetwork(n_features=4, n_actions=2)
baseline_value = 0.0 # 移動平均ベースライン
rewards_history = []
for episode in range(n_episodes):
state = env.reset()
states, actions, rewards = [], [], []
# エピソードの生成
for t in range(500):
action, _ = policy.select_action(state)
next_state, reward, done = env.step(action)
states.append(state)
actions.append(action)
rewards.append(reward)
state = next_state
if done:
break
T = len(rewards)
total_reward = sum(rewards)
rewards_history.append(total_reward)
# 収益 G_t の計算
G = np.zeros(T)
G[-1] = rewards[-1]
for t in range(T - 2, -1, -1):
G[t] = rewards[t] + gamma * G[t + 1]
# ベースラインの更新
if use_baseline:
baseline_value = 0.99 * baseline_value + 0.01 * np.mean(G)
# パラメータ更新
for t in range(T):
advantage = G[t] - baseline_value if use_baseline else G[t]
grad = policy.compute_log_grad(states[t], actions[t])
policy.theta += alpha * (gamma ** t) * advantage * grad
return rewards_history
# --- 実行と比較 ---
np.random.seed(42)
env = SimpleCartPole()
rewards_no_baseline = reinforce(env, n_episodes=1000, alpha=0.005,
gamma=0.99, use_baseline=False)
np.random.seed(42)
env = SimpleCartPole()
rewards_with_baseline = reinforce(env, n_episodes=1000, alpha=0.005,
gamma=0.99, use_baseline=True)
# --- 可視化 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 学習曲線
window = 50
smooth_no_bl = np.convolve(rewards_no_baseline, np.ones(window)/window, mode='valid')
smooth_with_bl = np.convolve(rewards_with_baseline, np.ones(window)/window, mode='valid')
axes[0].plot(smooth_no_bl, 'b-', linewidth=1.5, label='REINFORCE', alpha=0.8)
axes[0].plot(smooth_with_bl, 'r-', linewidth=1.5, label='REINFORCE + Baseline', alpha=0.8)
axes[0].set_xlabel('Episode', fontsize=12)
axes[0].set_ylabel('Total Reward (moving avg)', fontsize=12)
axes[0].set_title('REINFORCE on CartPole', fontsize=13)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)
# 報酬の分布比較(後半500エピソード)
axes[1].hist(rewards_no_baseline[500:], bins=30, alpha=0.5, label='No baseline', color='blue')
axes[1].hist(rewards_with_baseline[500:], bins=30, alpha=0.5, label='With baseline', color='red')
axes[1].set_xlabel('Total Reward', fontsize=12)
axes[1].set_ylabel('Count', fontsize=12)
axes[1].set_title('Reward Distribution (last 500 episodes)', fontsize=13)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
方策勾配の直感的な可視化
import numpy as np
import matplotlib.pyplot as plt
# --- 方策勾配の直感的な説明 ---
# 2腕バンディット問題での方策パラメータの軌跡
# バンディットの報酬確率
true_probs = [0.3, 0.7] # 行動0: 30%, 行動1: 70%
# ソフトマックス方策
def softmax_policy(theta):
exp_theta = np.exp(theta - np.max(theta))
return exp_theta / np.sum(exp_theta)
# REINFORCE
np.random.seed(0)
theta = np.array([0.0, 0.0])
alpha = 0.1
n_steps = 300
theta_history = [theta.copy()]
prob_history = [softmax_policy(theta)[1]]
for step in range(n_steps):
probs = softmax_policy(theta)
action = np.random.choice(2, p=probs)
reward = 1.0 if np.random.random() < true_probs[action] else 0.0
# 方策勾配更新
grad = np.zeros(2)
for a in range(2):
indicator = 1.0 if a == action else 0.0
grad[a] = (indicator - probs[a]) * reward
theta += alpha * grad
theta_history.append(theta.copy())
prob_history.append(softmax_policy(theta)[1])
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# π(a=1|θ) の変化
axes[0].plot(prob_history, 'b-', linewidth=1.5)
axes[0].axhline(y=0.7, color='red', linestyle='--', label='Optimal (0.7)', alpha=0.7)
axes[0].set_xlabel('Step', fontsize=12)
axes[0].set_ylabel('π(a=1)', fontsize=12)
axes[0].set_title('Policy Gradient: Action Probability', fontsize=13)
axes[0].legend(fontsize=11)
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(0, 1)
# θ パラメータ空間での軌跡
theta_arr = np.array(theta_history)
axes[1].plot(theta_arr[:, 0], theta_arr[:, 1], 'b-', linewidth=0.5, alpha=0.5)
axes[1].plot(theta_arr[0, 0], theta_arr[0, 1], 'go', markersize=10, label='Start')
axes[1].plot(theta_arr[-1, 0], theta_arr[-1, 1], 'ro', markersize=10, label='End')
axes[1].set_xlabel(r'$\theta_0$', fontsize=12)
axes[1].set_ylabel(r'$\theta_1$', fontsize=12)
axes[1].set_title('Parameter Space Trajectory', fontsize=13)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
まとめ
本記事では、方策勾配法の理論と実装について解説しました。
- 方策のパラメトリック表現: $\pi_{\bm{\theta}}(a|s)$ を直接最適化する
- 方策勾配定理: $\nabla J = E[\sum_t \nabla \log \pi_{\bm{\theta}}(A_t|S_t) \cdot G_t]$
- 対数微分のトリック: 環境モデルを必要としない勾配推定を可能にする
- REINFORCE: モンテカルロ的な方策勾配アルゴリズム
- ベースライン: 勾配推定の分散を低減し、学習を安定化させる
次のステップとして、Actor-Critic法(方策勾配 + 価値関数の組み合わせ)への発展が考えられます。