方策勾配法の理論と導出を解説して実装する

Q学習やSARSAといった価値ベースの手法では、行動価値関数 $Q(s, a)$ を推定し、そこから間接的に方策(行動の選び方)を導出していました。これに対して 方策勾配法(Policy Gradient Method) は、方策そのものをパラメータ化し、目的関数の勾配を用いて方策パラメータを直接最適化する手法です。

方策勾配法は、連続行動空間や確率的方策を自然に扱えるという大きな利点があり、現代の深層強化学習(PPO, SAC, A3Cなど)の基盤となっています。本記事では、方策勾配定理の完全な証明から始めて、REINFORCEアルゴリズム、ベースラインによる分散削減、そしてActor-Critic法の導入まで丁寧に解説します。

本記事の内容

  • 方策のパラメータ化 $\pi_\theta(a \mid s)$ の考え方
  • 目的関数 $J(\theta)$ の定義
  • 方策勾配定理 $\nabla J(\theta) = \mathbb{E}[\sum \nabla \log \pi_\theta \cdot Q^\pi]$ の完全な証明
  • REINFORCEアルゴリズム
  • ベースラインによる分散削減(バイアスを生まないことの証明)
  • Actor-Critic法の導入
  • CartPole環境でのPython実装

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

方策のパラメータ化

価値ベース手法の限界

Q学習やSARSAでは、行動価値関数 $Q(s, a)$ をテーブルで表現し、ε-greedy方策で行動を選択しました。しかし、この方法にはいくつかの限界があります。

  1. 連続行動空間: $Q(s, a)$ のテーブルは離散的な行動空間でしか使えない
  2. 確率的最適方策: じゃんけんのように確率的な方策が最適な場合、greedy方策では対応できない
  3. 方策の微分不可能性: ε-greedy方策は $\arg\max$ を含むため微分できず、勾配法が使えない

パラメータ化方策

方策 $\pi$ をパラメータ $\theta$ で直接パラメータ化します。

$$ \pi_\theta(a \mid s) = P(A_t = a \mid S_t = s, \theta) $$

ここで $\pi_\theta(a \mid s)$ は状態 $s$ で行動 $a$ を選ぶ確率です。たとえば離散行動空間では、ソフトマックス方策がよく使われます。

$$ \pi_\theta(a \mid s) = \frac{\exp(h(s, a, \theta))}{\sum_{a’} \exp(h(s, a’, \theta))} $$

ここで $h(s, a, \theta)$ は状態-行動の特徴量に対する線形関数やニューラルネットワークの出力です。

連続行動空間では、ガウス方策が一般的です。

$$ \pi_\theta(a \mid s) = \frac{1}{\sqrt{2\pi}\sigma_\theta(s)} \exp\left( -\frac{(a – \mu_\theta(s))^2}{2\sigma_\theta(s)^2} \right) $$

ここで $\mu_\theta(s)$ は平均、$\sigma_\theta(s)$ は標準偏差で、いずれもパラメータ $\theta$ に依存します。

目的関数の定義

方策 $\pi_\theta$ の良さを測る目的関数 $J(\theta)$ を定義します。エピソード型タスクでは、初期状態 $s_0$ からの期待累積割引報酬として定義するのが一般的です。

$$ J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \sum_{t=0}^{T} \gamma^t R_{t+1} \right] = \mathbb{E}_{\tau \sim \pi_\theta} [R(\tau)] $$

ここで $\tau = (s_0, a_0, r_1, s_1, a_1, r_2, \ldots)$ は軌跡(trajectory)、$R(\tau) = \sum_{t=0}^{T} \gamma^t r_{t+1}$ は軌跡に沿った累積割引報酬です。

軌跡の確率は

$$ P(\tau \mid \theta) = p(s_0) \prod_{t=0}^{T} \pi_\theta(a_t \mid s_t) \, p(s_{t+1} \mid s_t, a_t) $$

と書けます。ここで $p(s_0)$ は初期状態分布、$p(s_{t+1} \mid s_t, a_t)$ は環境の遷移確率です。

目的は $J(\theta)$ を最大化する $\theta$ を見つけることです。

$$ \theta^* = \arg\max_\theta J(\theta) $$

方策勾配定理の証明

対数微分トリック

証明の鍵となるのが 対数微分トリック(Log-derivative trick) です。任意の確率分布 $p(x \mid \theta)$ について

$$ \nabla_\theta p(x \mid \theta) = p(x \mid \theta) \nabla_\theta \log p(x \mid \theta) $$

が成り立ちます。これは対数の微分 $\nabla \log f = \nabla f / f$ を変形しただけです。

$$ \nabla_\theta \log p(x \mid \theta) = \frac{\nabla_\theta p(x \mid \theta)}{p(x \mid \theta)} $$

$$ \therefore \quad \nabla_\theta p(x \mid \theta) = p(x \mid \theta) \, \nabla_\theta \log p(x \mid \theta) $$

このトリックにより、$\nabla p$ を $p \cdot \nabla \log p$ に変換でき、期待値の形で表現できるようになります。

方策勾配の導出(軌跡ベース)

目的関数の勾配を計算します。

$$ \begin{align} \nabla_\theta J(\theta) &= \nabla_\theta \mathbb{E}_{\tau \sim \pi_\theta}[R(\tau)] \\ &= \nabla_\theta \int P(\tau \mid \theta) R(\tau) \, d\tau \\ &= \int \nabla_\theta P(\tau \mid \theta) \, R(\tau) \, d\tau \end{align} $$

ここで対数微分トリックを適用します。

$$ \begin{align} &= \int P(\tau \mid \theta) \, \nabla_\theta \log P(\tau \mid \theta) \, R(\tau) \, d\tau \\ &= \mathbb{E}_{\tau \sim \pi_\theta} \left[ \nabla_\theta \log P(\tau \mid \theta) \, R(\tau) \right] \end{align} $$

次に $\nabla_\theta \log P(\tau \mid \theta)$ を展開します。

$$ \log P(\tau \mid \theta) = \log p(s_0) + \sum_{t=0}^{T} \left[ \log \pi_\theta(a_t \mid s_t) + \log p(s_{t+1} \mid s_t, a_t) \right] $$

$\theta$ で微分すると、$p(s_0)$ と $p(s_{t+1} \mid s_t, a_t)$ は $\theta$ に依存しないため消えます。

$$ \nabla_\theta \log P(\tau \mid \theta) = \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) $$

したがって

$$ \boxed{ \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \, R(\tau) \right] } $$

これが 方策勾配定理 の軌跡ベースの表現です。

重要な帰結: 環境モデルが不要

上の結果で極めて重要な点は、勾配の計算に 環境の遷移確率 $p(s’ \mid s, a)$ が一切含まれない ことです。必要なのは方策 $\pi_\theta$ の勾配 $\nabla_\theta \log \pi_\theta$ のみです。これにより、環境のモデルを知らなくても(モデルフリー)、サンプルした軌跡から勾配を推定できます。

より精緻な形: 因果性の利用

上の式では $R(\tau) = \sum_{t’=0}^{T} \gamma^{t’} r_{t’+1}$ ですが、時刻 $t$ の行動は時刻 $t$ より前の報酬に影響を与えません。この 因果性(causality) を利用すると、分散を削減できます。

$$ \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \left( \sum_{t’=t}^{T} \gamma^{t’-t} r_{t’+1} \right) \right] $$

括弧内の $\sum_{t’=t}^{T} \gamma^{t’-t} r_{t’+1}$ は時刻 $t$ 以降の割引累積報酬であり、$G_t$(リターン)と呼ばれます。

$$ \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \, G_t \right] $$

方策勾配定理(状態-行動形式)

定常状態分布 $d^\pi(s)$ を用いた表現もあります。

$$ \nabla_\theta J(\theta) = \sum_s d^\pi(s) \sum_a \nabla_\theta \pi_\theta(a \mid s) \, Q^\pi(s, a) $$

ここで $d^\pi(s) = \sum_{t=0}^{\infty} \gamma^t P(S_t = s \mid \pi)$ は割引状態訪問頻度です。対数微分トリックを逆に適用すると

$$ \nabla_\theta J(\theta) = \mathbb{E}_{s \sim d^\pi, a \sim \pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a \mid s) \, Q^\pi(s, a) \right] $$

が得られます。この形が最も広く引用される方策勾配定理の表現です。

REINFORCEアルゴリズム

方策勾配定理を直接利用したモンテカルロ法的なアルゴリズムが REINFORCE(Williams, 1992)です。

$Q^\pi(s_t, a_t)$ をサンプルリターン $G_t$ で近似します。

$$ \nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} \sum_{t=0}^{T_i} \nabla_\theta \log \pi_\theta(a_t^{(i)} \mid s_t^{(i)}) \, G_t^{(i)} $$

ここで $N$ は軌跡のサンプル数です。パラメータ更新は

$$ \theta \leftarrow \theta + \alpha \nabla_\theta J(\theta) $$

アルゴリズム: REINFORCE

  1. 方策パラメータ $\theta$ を初期化
  2. 各エピソードについて繰り返し:
  3. 方策 $\pi_\theta$ に従って軌跡 $\tau = (s_0, a_0, r_1, \ldots, s_T)$ を生成
  4. 各時刻 $t$ でリターン $G_t = \sum_{t’=t}^{T} \gamma^{t’-t} r_{t’+1}$ を計算
  5. $\theta \leftarrow \theta + \alpha \sum_{t=0}^{T} \gamma^t \nabla_\theta \log \pi_\theta(a_t \mid s_t) \, G_t$

ベースラインによる分散削減

分散の問題

REINFORCEの勾配推定は不偏ですが、$G_t$ の分散が非常に大きく、学習が不安定になりがちです。報酬が常に正の場合、全ての行動の確率が増加する方向に更新されてしまい、相対的な差しか意味を持ちません。

ベースラインの導入

任意の状態のみに依存する関数 $b(s)$ を ベースライン として導入し、$G_t$ から引きます。

$$ \nabla_\theta J(\theta) = \mathbb{E} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t \mid s_t) \left( G_t – b(s_t) \right) \right] $$

ベースラインがバイアスを生まないことの証明

ベースライン $b(s)$ を引いても勾配の期待値が変わらないこと、つまりバイアスを生まないことを証明します。

証明すべきことは

$$ \mathbb{E}_{a \sim \pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a \mid s) \, b(s) \right] = 0 $$

です。$b(s)$ は $a$ に依存しないため、$b(s)$ を外に出せます。

$$ \begin{align} \mathbb{E}_{a \sim \pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a \mid s) \, b(s) \right] &= b(s) \sum_a \pi_\theta(a \mid s) \nabla_\theta \log \pi_\theta(a \mid s) \\ &= b(s) \sum_a \pi_\theta(a \mid s) \frac{\nabla_\theta \pi_\theta(a \mid s)}{\pi_\theta(a \mid s)} \quad (\because \text{対数微分の定義}) \\ &= b(s) \sum_a \nabla_\theta \pi_\theta(a \mid s) \\ &= b(s) \, \nabla_\theta \sum_a \pi_\theta(a \mid s) \\ &= b(s) \, \nabla_\theta 1 \quad (\because \sum_a \pi_\theta(a \mid s) = 1) \\ &= 0 \end{align} $$

よって、ベースライン $b(s)$ を引いても勾配の期待値は変わらず(バイアスゼロ)、分散のみを変化させます。$\square$

最適ベースライン

分散を最小化する最適なベースラインは、理論的には

$$ b^*(s) = \frac{\mathbb{E}\left[(\nabla_\theta \log \pi_\theta)^2 \, G_t \right]}{\mathbb{E}\left[(\nabla_\theta \log \pi_\theta)^2 \right]} $$

ですが、実用上は 状態価値関数 $V^\pi(s)$ をベースラインとすることが最も一般的です。

$$ G_t – V^\pi(s_t) \approx Q^\pi(s_t, a_t) – V^\pi(s_t) = A^\pi(s_t, a_t) $$

ここで $A^\pi(s, a)$ は アドバンテージ関数(Advantage Function) と呼ばれ、「行動 $a$ が平均的な行動に比べてどれだけ良いか」を表します。アドバンテージ関数を使うことで、良い行動は正、悪い行動は負の信号が送られ、学習が効率的になります。

Actor-Critic法

動機

REINFORCEはモンテカルロ法的な推定を行うため、エピソード全体が終了しないと更新できず、分散も大きいという問題があります。Actor-Critic法 は、$Q^\pi(s, a)$ や $V^\pi(s)$ をニューラルネットワーク(Critic)で近似し、方策(Actor)の更新に利用する手法です。

構造

Actor-Critic法は2つのネットワークを持ちます。

  • Actor(行動者): 方策 $\pi_\theta(a \mid s)$ をパラメータ $\theta$ で表現
  • Critic(批評者): 価値関数 $V_w(s)$ をパラメータ $w$ で表現

更新則

時刻 $t$ でTD誤差を計算します。

$$ \delta_t = R_{t+1} + \gamma V_w(S_{t+1}) – V_w(S_t) $$

このTD誤差 $\delta_t$ はアドバンテージ関数 $A^\pi(s_t, a_t)$ の不偏推定量です。これを用いて

Actorの更新:

$$ \theta \leftarrow \theta + \alpha_\theta \, \nabla_\theta \log \pi_\theta(a_t \mid s_t) \, \delta_t $$

Criticの更新:

$$ w \leftarrow w – \alpha_w \, \nabla_w (R_{t+1} + \gamma V_w(S_{t+1}) – V_w(S_t))^2 $$

つまり

$$ w \leftarrow w + \alpha_w \, \delta_t \, \nabla_w V_w(S_t) $$

Actor-Critic法はTD学習をベースとするため、各ステップで更新でき(オンライン学習)、REINFORCEよりも分散が小さいという利点があります。一方、バイアスが導入される可能性がありますが、実用上は分散の低減効果がバイアスの悪影響を上回ることが多いです。

Pythonでの実装: CartPole環境でのREINFORCE

CartPole環境は、台車の上に立てた棒が倒れないように台車を左右に動かすタスクです。方策勾配法の入門として最も適した環境のひとつです。

import numpy as np
import matplotlib.pyplot as plt

# --- CartPole環境の簡易実装 ---
class CartPole:
    """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  # 角度閾値(12度)
        self.x_threshold = 2.4  # 位置閾値

    def reset(self):
        """環境リセット: 状態 = [x, x_dot, theta, theta_dot]"""
        self.state = np.random.uniform(-0.05, 0.05, size=4)
        return self.state.copy()

    def step(self, action):
        """行動(0: 左, 1: 右)を実行"""
        x, x_dot, theta, theta_dot = self.state
        force = self.force_mag if action == 1 else -self.force_mag

        cos_theta = np.cos(theta)
        sin_theta = np.sin(theta)

        # 運動方程式
        temp = (force + self.polemass_length * theta_dot**2 * sin_theta) / self.total_mass
        theta_acc = (self.gravity * sin_theta - cos_theta * temp) / (
            self.length * (4.0/3.0 - self.masspole * cos_theta**2 / self.total_mass))
        x_acc = temp - self.polemass_length * theta_acc * cos_theta / self.total_mass

        # オイラー法による状態更新
        x += self.tau * x_dot
        x_dot += self.tau * x_acc
        theta += self.tau * theta_dot
        theta_dot += self.tau * theta_acc

        self.state = np.array([x, x_dot, theta, theta_dot])

        # 終了判定
        done = bool(
            x < -self.x_threshold or x > self.x_threshold
            or theta < -self.theta_threshold or theta > self.theta_threshold
        )
        reward = 1.0 if not done else 0.0

        return self.state.copy(), reward, done

# --- 方策ネットワーク(ソフトマックス線形方策) ---
class SoftmaxPolicy:
    """ソフトマックス線形方策"""
    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):
        """数値安定なソフトマックス"""
        e_x = np.exp(x - np.max(x))
        return e_x / e_x.sum()

    def get_action_probs(self, state):
        """状態から行動確率を計算"""
        logits = state @ self.theta  # (n_actions,)
        probs = self._softmax(logits)
        return probs

    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_grad_log_pi(self, state, action):
        """∇_θ log π_θ(a|s) を計算(ソフトマックス方策の場合)"""
        probs = self.get_action_probs(state)
        # ∇_θ log π = φ(s) (e_a - π) (ソフトマックスの勾配)
        # e_a: 行動aの位置が1のone-hotベクトル
        one_hot = np.zeros(self.n_actions)
        one_hot[action] = 1.0
        grad = np.outer(state, one_hot - probs)  # (n_features, n_actions)
        return grad

# --- REINFORCE アルゴリズム ---
def reinforce(env, n_episodes=2000, gamma=0.99, alpha=0.01):
    """REINFORCEアルゴリズム(ベースラインなし)"""
    policy = SoftmaxPolicy(n_features=4, n_actions=2)
    rewards_history = []

    for ep in range(n_episodes):
        state = env.reset()
        episode_states = []
        episode_actions = []
        episode_rewards = []
        done = False

        # エピソードの生成
        while not done:
            action, _ = policy.select_action(state)
            next_state, reward, done = env.step(action)

            episode_states.append(state)
            episode_actions.append(action)
            episode_rewards.append(reward)

            state = next_state

        # リターンの計算(後ろから)
        T = len(episode_rewards)
        G = np.zeros(T)
        G[-1] = episode_rewards[-1]
        for t in range(T - 2, -1, -1):
            G[t] = episode_rewards[t] + gamma * G[t + 1]

        # パラメータ更新
        for t in range(T):
            grad = policy.compute_grad_log_pi(episode_states[t], episode_actions[t])
            policy.theta += alpha * (gamma ** t) * G[t] * grad

        rewards_history.append(sum(episode_rewards))

    return policy, rewards_history

# --- REINFORCE with Baseline ---
def reinforce_baseline(env, n_episodes=2000, gamma=0.99, alpha_theta=0.01, alpha_w=0.05):
    """REINFORCEアルゴリズム(ベースライン付き)"""
    policy = SoftmaxPolicy(n_features=4, n_actions=2)
    # ベースライン(状態価値関数)の重み
    w = np.zeros(4)  # V_w(s) = w^T s(線形近似)
    rewards_history = []

    for ep in range(n_episodes):
        state = env.reset()
        episode_states = []
        episode_actions = []
        episode_rewards = []
        done = False

        while not done:
            action, _ = policy.select_action(state)
            next_state, reward, done = env.step(action)

            episode_states.append(state)
            episode_actions.append(action)
            episode_rewards.append(reward)

            state = next_state

        # リターンの計算
        T = len(episode_rewards)
        G = np.zeros(T)
        G[-1] = episode_rewards[-1]
        for t in range(T - 2, -1, -1):
            G[t] = episode_rewards[t] + gamma * G[t + 1]

        # パラメータ更新
        for t in range(T):
            s_t = episode_states[t]
            baseline = w @ s_t  # V_w(s_t)
            advantage = G[t] - baseline

            # Actor(方策)の更新
            grad = policy.compute_grad_log_pi(s_t, episode_actions[t])
            policy.theta += alpha_theta * (gamma ** t) * advantage * grad

            # Critic(ベースライン)の更新
            w += alpha_w * (G[t] - baseline) * s_t

        rewards_history.append(sum(episode_rewards))

    return policy, rewards_history

# --- 実験の実行 ---
np.random.seed(42)
env = CartPole()

# REINFORCEの実行
policy_no_bl, rewards_no_bl = reinforce(env, n_episodes=3000, alpha=0.005)

# REINFORCE with Baselineの実行
np.random.seed(42)
env2 = CartPole()
policy_bl, rewards_bl = reinforce_baseline(env2, n_episodes=3000, alpha_theta=0.005, alpha_w=0.01)

# 結果の可視化
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# 学習曲線の比較
window = 100
def moving_avg(data, w):
    return np.convolve(data, np.ones(w)/w, mode='valid')

axes[0].plot(moving_avg(rewards_no_bl, window), label='REINFORCE', alpha=0.8)
axes[0].plot(moving_avg(rewards_bl, window), label='REINFORCE + Baseline', alpha=0.8)
axes[0].set_xlabel('Episode', fontsize=12)
axes[0].set_ylabel(f'Average Reward (window={window})', fontsize=12)
axes[0].set_title('REINFORCE: Learning Curves on CartPole', fontsize=14)
axes[0].legend(fontsize=12)
axes[0].grid(True, alpha=0.3)

# エピソード長の推移(生の値)
axes[1].scatter(range(len(rewards_bl)), rewards_bl, s=1, alpha=0.3, label='Episode Length')
axes[1].plot(moving_avg(rewards_bl, window), 'r-', linewidth=2, label=f'Moving Avg (w={window})')
axes[1].set_xlabel('Episode', fontsize=12)
axes[1].set_ylabel('Episode Length', fontsize=12)
axes[1].set_title('REINFORCE + Baseline: Episode Lengths', fontsize=14)
axes[1].legend(fontsize=12)
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 学習後の方策のテスト
def test_policy(env, policy, n_tests=20):
    """学習済み方策のテスト"""
    total_rewards = []
    for _ in range(n_tests):
        state = env.reset()
        total_reward = 0
        done = False
        steps = 0
        while not done and steps < 500:
            action, _ = policy.select_action(state)
            state, reward, done = env.step(action)
            total_reward += reward
            steps += 1
        total_rewards.append(total_reward)
    return total_rewards

test_results = test_policy(CartPole(), policy_bl, n_tests=50)
print(f"学習後のテスト結果(50回平均): {np.mean(test_results):.1f} ± {np.std(test_results):.1f} ステップ")

Actor-Criticの実装

REINFORCEにTD学習を組み合わせたActor-Criticの実装例です。

import numpy as np
import matplotlib.pyplot as plt

class CartPole:
    """CartPole環境(再掲)"""
    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

    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
        cos_theta = np.cos(theta)
        sin_theta = np.sin(theta)
        temp = (force + self.polemass_length * theta_dot**2 * sin_theta) / self.total_mass
        theta_acc = (self.gravity * sin_theta - cos_theta * temp) / (
            self.length * (4.0/3.0 - self.masspole * cos_theta**2 / self.total_mass))
        x_acc = temp - self.polemass_length * theta_acc * cos_theta / self.total_mass
        x += self.tau * x_dot
        x_dot += self.tau * x_acc
        theta += self.tau * theta_dot
        theta_dot += self.tau * theta_acc
        self.state = np.array([x, x_dot, theta, theta_dot])
        done = bool(
            x < -self.x_threshold or x > self.x_threshold
            or theta < -self.theta_threshold or theta > self.theta_threshold
        )
        reward = 1.0 if not done else 0.0
        return self.state.copy(), reward, done

def actor_critic(env, n_episodes=3000, gamma=0.99,
                 alpha_actor=0.005, alpha_critic=0.01):
    """Actor-Criticアルゴリズム(線形近似)"""
    n_features = 4
    n_actions = 2

    # Actor: ソフトマックス方策
    theta = np.zeros((n_features, n_actions))
    # Critic: 線形価値関数 V(s) = w^T s
    w = np.zeros(n_features)

    rewards_history = []

    def softmax(x):
        e_x = np.exp(x - np.max(x))
        return e_x / e_x.sum()

    for ep in range(n_episodes):
        state = env.reset()
        total_reward = 0
        done = False
        I = 1.0  # 割引率の累積

        while not done:
            # 行動の選択
            logits = state @ theta
            probs = softmax(logits)
            action = np.random.choice(n_actions, p=probs)

            # 環境との相互作用
            next_state, reward, done = env.step(action)
            total_reward += reward

            # TD誤差の計算
            v_current = w @ state
            v_next = 0 if done else w @ next_state
            delta = reward + gamma * v_next - v_current

            # Criticの更新
            w += alpha_critic * delta * state

            # Actorの更新
            one_hot = np.zeros(n_actions)
            one_hot[action] = 1.0
            grad_log_pi = np.outer(state, one_hot - probs)
            theta += alpha_actor * I * delta * grad_log_pi

            I *= gamma
            state = next_state

        rewards_history.append(total_reward)

    return theta, w, rewards_history

# Actor-Criticの実行
np.random.seed(42)
env_ac = CartPole()
theta_ac, w_ac, rewards_ac = actor_critic(env_ac, n_episodes=3000)

# 可視化
fig, ax = plt.subplots(figsize=(10, 6))

window = 100
avg_rewards = np.convolve(rewards_ac, np.ones(window)/window, mode='valid')
ax.plot(avg_rewards, linewidth=2, label='Actor-Critic')
ax.set_xlabel('Episode', fontsize=12)
ax.set_ylabel(f'Average Reward (window={window})', fontsize=12)
ax.set_title('Actor-Critic on CartPole', fontsize=14)
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
ax.axhline(y=200, color='r', linestyle='--', alpha=0.5, label='Solved threshold')
ax.legend(fontsize=12)
plt.tight_layout()
plt.show()

print(f"最後100エピソードの平均報酬: {np.mean(rewards_ac[-100:]):.1f}")

結果の考察

CartPole環境での実験から、以下の知見が得られます。

REINFORCE vs REINFORCE with Baseline: ベースラインを導入することで、学習の安定性が大幅に改善されます。ベースラインなしのREINFORCEは学習が遅く、報酬の分散が大きいですが、ベースラインを導入すると、エピソード長が着実に伸びていきます。

Actor-Critic: 各ステップでオンライン更新を行うため、エピソード終了を待つ必要がなく、REINFORCEよりも学習が速い傾向があります。ただし、Criticの近似誤差によるバイアスが導入される可能性があります。

まとめ

本記事では、方策勾配法について理論的な導出から実装まで解説しました。

  • 方策勾配法は方策 $\pi_\theta$ を直接パラメータ化し、目的関数 $J(\theta)$ の勾配を用いて最適化します
  • 方策勾配定理 $\nabla J(\theta) = \mathbb{E}[\nabla \log \pi_\theta \cdot Q^\pi]$ により、環境モデルを知らなくても勾配を推定できます
  • 対数微分トリック が証明の鍵であり、環境の遷移確率が勾配から消えるのが重要な帰結です
  • REINFORCE はモンテカルロ的にリターン $G_t$ を使う最もシンプルな方策勾配法です
  • ベースライン $b(s)$ を引いてもバイアスは生じず($\sum_a \nabla \pi_\theta = \nabla 1 = 0$ であるため)、分散を大幅に削減できます
  • Actor-Critic法 は価値関数(Critic)でTD誤差を計算し、方策(Actor)の更新に使うことで、オンライン学習と分散削減を両立します

次のステップとして、以下の記事も参考にしてください。