方策勾配定理の導出と実装

強化学習のエージェントが「次にどの行動を取るか」を決める方策(policy) をどう改善すればよいでしょうか。教師あり学習なら「正解ラベル」を見て損失を計算できますが、強化学習では「正解の行動」が事前にわかりません。迷路を解くロボットを想像してみてください。誰も「ここで右に曲がれ」とは教えてくれません。ロボットは自分で試行錯誤し、ゴールにたどり着けたかどうかという結果だけを手がかりに、「どの分岐でどちらに進むべきか」という判断の方針を自力で改善しなければなりません。

「良い行動の確率を上げ、悪い行動の確率を下げる」という直感的なアイデアを数学的に定式化したのが、方策勾配定理(Policy Gradient Theorem) です。期待報酬を方策のパラメータで微分する方法を与えるこの定理は、深層強化学習の理論的基盤であり、PPO、A3C、SACなどの現代的なアルゴリズムの出発点です。

この定理の驚くべき点は、環境の動作原理(状態遷移確率)を全く知らなくても勾配を計算できることです。たとえばロボットが物理的な環境で行動する場合、重力や摩擦の正確な式を知らなくても、「実際にやってみた結果」だけから方策を改善できます。これがモデルフリー強化学習の核心であり、方策勾配法が実世界の問題に広く応用される理由です。

方策勾配法を理解すると、以下のような応用が開けます。

  • ロボット制御: 連続的な行動空間でのロボットの動作学習。DQNのような離散行動を前提とする手法では扱えない連続的な力やトルクの制御が可能になります
  • ゲームAI: AlphaGoやAtariゲームでのAIの行動決定。AlphaGoでは方策ネットワークの学習に方策勾配が使われています
  • 推薦システム: ユーザーの長期的な満足度を最大化する推薦アルゴリズム。即座のクリック率だけでなく、長期的なエンゲージメントを考慮できます
  • 自然言語生成: RLHF(Reinforcement Learning from Human Feedback)における言語モデルの微調整。ChatGPTの学習過程でもPPO(方策勾配法の一種)が使われています

本記事の内容

  • 強化学習の基本概念と方策の定式化
  • 方策勾配定理の完全な導出
  • REINFORCEアルゴリズム
  • ベースラインによる分散低減の理論
  • PyTorchによるCartPole環境での実装

前提知識

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

強化学習の基本概念

マルコフ決定過程(MDP)

方策勾配法の議論に入る前に、強化学習の枠組みを明確にしておきましょう。

強化学習の問題はマルコフ決定過程(MDP)$(\mathcal{S}, \mathcal{A}, P, R, \gamma)$ として定式化されます。「マルコフ」とは、「次の状態は現在の状態と行動だけで決まり、過去の履歴には依存しない」というマルコフ性を意味します。

  • $\mathcal{S}$: 状態空間。エージェントが観測する環境の状態の集合。たとえばCartPoleでは「カートの位置・速度・ポールの角度・角速度」の4次元ベクトルです
  • $\mathcal{A}$: 行動空間。エージェントが取れる行動の集合。CartPoleでは「左に押す」「右に押す」の2つです
  • $P(s’|s, a)$: 状態遷移確率。状態 $s$ で行動 $a$ をとったとき、状態 $s’$ に遷移する確率。環境の物理法則に相当します
  • $R(s, a)$: 報酬関数。状態 $s$ で行動 $a$ をとったときに得られる即時報酬。CartPoleではポールが立っている限り報酬1が得られます
  • $\gamma \in [0, 1)$: 割引率。将来の報酬をどれだけ重視するか。$\gamma = 0.99$ なら100ステップ先の報酬は現在の約37%($0.99^{100} \approx 0.37$)の価値を持ちます

エージェントの目標は、割引累積報酬の期待値を最大化する方策を見つけることです。割引率 $\gamma$ を導入する理由は2つあります。第一に、将来の報酬は現在の報酬より不確実であるため割り引くのが合理的です。第二に、$\gamma < 1$ でなければ無限ホライズンでの累積報酬が発散してしまいます。

方策の定義

方策 $\pi$ は、状態 $s$ が与えられたときに行動 $a$ を選ぶ確率分布です。

$$ \pi_\theta(a|s): \text{パラメータ } \theta \text{ を持つ方策。状態 } s \text{ での行動 } a \text{ の確率} $$

確率的方策(stochastic policy)は行動に確率分布を対応させます。たとえば、離散行動空間ではソフトマックスで各行動の確率を出力し、連続行動空間では正規分布の平均と分散を出力します。

方策を日常的に例えると、チェスのプレイヤーの「思考のクセ」に相当します。同じ盤面(状態)でも、攻撃的なプレイヤーは攻撃的な手(行動)を高い確率で選び、防御的なプレイヤーは安全な手を選びます。方策の学習は、「勝率を上げるように思考のクセを改善する」ことに相当します。

目的関数

方策 $\pi_\theta$ の性能は、期待収益 $J(\theta)$ で評価されます。

$$ \begin{equation} J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}\left[\sum_{t=0}^{T} \gamma^t R(s_t, a_t)\right] \end{equation} $$

ここで $\tau = (s_0, a_0, s_1, a_1, \ldots, s_T)$ は方策 $\pi_\theta$ に従って生成される軌道(trajectory) です。軌道の確率は

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

方策勾配法の目標は、$\nabla_\theta J(\theta)$ を計算し、勾配上昇法で $\theta$ を更新して $J(\theta)$ を最大化することです。具体的には、各ステップで $\theta \leftarrow \theta + \alpha \nabla_\theta J(\theta)$ と更新します($\alpha$ は学習率)。これは教師あり学習での勾配降下法と同じ発想ですが、損失を最小化するのではなく報酬を最大化するため「勾配上昇」となります。

ここで難しいのは、$J(\theta)$ の勾配を計算することです。期待値は方策 $\pi_\theta$ が生成する軌道の分布の下で取られており、この分布自体が $\theta$ に依存しています。さらに、状態遷移確率 $P$ は未知です(モデルフリーの設定)。この困難をどう乗り越えるかが、方策勾配定理の本題です。

強化学習の基本概念を整理したところで、次に方策勾配定理の核心となる勾配の導出に進みましょう。

方策勾配定理の導出

期待報酬の勾配

$J(\theta) = \mathbb{E}_{\tau \sim p_\theta}[R(\tau)]$ の勾配を計算したいのですが、期待値は方策のパラメータ $\theta$ に依存する分布 $p_\theta(\tau)$ の下で取られているため、直接微分することができません。$\theta$ を変えると分布自体が変わるため、通常の微分の交換が単純には適用できません。

ここで、対数微分トリック(log-derivative trick, score function estimator, REINFORCE trick とも呼ばれます)を使います。任意の確率分布 $p_\theta$ に対して、対数の微分の連鎖律から次の恒等式が成り立ちます。

$$ \nabla_\theta p_\theta(\tau) = p_\theta(\tau) \nabla_\theta \ln p_\theta(\tau) $$

これは $\nabla_\theta \ln p_\theta(\tau) = \frac{\nabla_\theta p_\theta(\tau)}{p_\theta(\tau)}$ を変形しただけですが、この単純な変形が大きな威力を持ちます。右辺に $p_\theta(\tau)$ が掛かっているため、期待値の形に書き直すことができるのです。

この恒等式を使って $\nabla_\theta J(\theta)$ を計算します。まず、期待値を積分として書きます。

$$ \nabla_\theta J(\theta) = \nabla_\theta \int p_\theta(\tau) R(\tau) \, d\tau = \int \nabla_\theta p_\theta(\tau) R(\tau) \, d\tau $$

ここで微分と積分の交換が可能であることを用いました(適切な正則条件の下で成り立ちます)。対数微分トリックを適用すると

$$ = \int p_\theta(\tau) \nabla_\theta \ln p_\theta(\tau) R(\tau) \, d\tau = \mathbb{E}_{\tau \sim p_\theta}\left[\nabla_\theta \ln p_\theta(\tau) R(\tau)\right] $$

これで勾配が期待値の形になりました。期待値はサンプリングで近似できるため、実際に軌道を生成すれば勾配を推定できます。

次に $\nabla_\theta \ln p_\theta(\tau)$ を展開します。軌道 $\tau$ の確率の対数を展開すると

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

$\theta$ で微分すると、初期状態分布 $p(s_0)$ は環境が決めるものなので $\theta$ に依存しません。同様に、状態遷移確率 $P(s_{t+1}|s_t, a_t)$ も環境の物理法則であり $\theta$ に依存しません。したがってこれらの項の $\theta$ に関する勾配はゼロになります。

$$ \nabla_\theta \ln p_\theta(\tau) = \sum_{t=0}^{T-1} \nabla_\theta \ln \pi_\theta(a_t|s_t) $$

これが方策勾配定理の核心的な結果です。環境のダイナミクス(状態遷移確率 $P$)を知らなくても、方策の対数確率の勾配だけから方策の勾配を計算できるのです。環境のモデルが未知でも、方策のパラメータに対する勾配は計算可能 — これがモデルフリーな強化学習が可能になる根拠です。

最終的な方策勾配は

$$ \begin{equation} \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}\left[\sum_{t=0}^{T-1} \nabla_\theta \ln \pi_\theta(a_t|s_t) R(\tau)\right] \end{equation} $$

直感的な解釈

この勾配の式は直感的に理解できます。

$\nabla_\theta \ln \pi_\theta(a_t|s_t)$ は「行動 $a_t$ の確率を上げるパラメータの更新方向」を示します。これは最尤推定における対数尤度の勾配と全く同じ形です。$R(\tau)$ は軌道全体の報酬であり、この勾配の「重み」の役割を果たします。

  • 報酬が高い軌道 ($R(\tau)$ が大きい) → その軌道で選んだ行動の確率を強く上げる
  • 報酬が低い軌道 ($R(\tau)$ が小さい) → その軌道で選んだ行動の確率を弱くしか上げない(結果的に相対的に下がる)

「うまくいった試行を繰り返しやすくする」という強化学習の基本原理そのものです。

教師あり学習との対比で考えると理解が深まります。教師あり学習では正解ラベルが「この行動を取れ」と直接指示します。方策勾配法では、正解の行動はわかりませんが、「実際にやってみた結果、どれくらい良かったか」という報酬をフィードバックとして使います。報酬が大きいほどその行動列を強く強化する — これは試行錯誤による学習の数学的な表現です。

因果関係を考慮した改良

式(2)の勾配では、全ての行動に軌道全体の報酬 $R(\tau)$ が掛けられています。しかし、よく考えると、時刻 $t$ の行動は時刻 $t$ より前の報酬に影響を与えることはできません。時刻3で右に曲がったことが、時刻1で得た報酬を変えることはありえないからです。因果関係を正しく反映すると、各行動にはその行動以降に得られた報酬だけを掛けるべきです。

数学的には、$t’ < t$ の場合に $\mathbb{E}[\nabla_\theta \ln \pi_\theta(a_t|s_t) \cdot R(s_{t'}, a_{t'})]=0$ となることが示せます($R(s_{t'}, a_{t'})$ は $a_t$ に依存しないため)。したがって

$$ \begin{equation} \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta}\left[\sum_{t=0}^{T-1} \nabla_\theta \ln \pi_\theta(a_t|s_t) \sum_{t’=t}^{T-1} \gamma^{t’-t} R(s_{t’}, a_{t’})\right] \end{equation} $$

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

$$ G_t = \sum_{t’=t}^{T-1} \gamma^{t’-t} R(s_{t’}, a_{t’}) $$

この改良は「報酬を未来のものだけに限定しただけ」ですが、勾配推定の分散を低減する効果があります。軌道全体の報酬を使うとノイズが大きくなりますが、因果関係に合った報酬だけを使えば、各行動に対するフィードバックがより正確になります。リターン $G_t$ は再帰的な関係 $G_t = R(s_t, a_t) + \gamma G_{t+1}$ を満たすため、逆順に効率的に計算できます。

方策勾配定理を導出できたところで、次にこれを実際のアルゴリズムに落とし込んだREINFORCEを見ていきましょう。

REINFORCEアルゴリズム

アルゴリズム

REINFORCE(ウィリアムズ, 1992)は、方策勾配定理を直接実装した最もシンプルな方策勾配アルゴリズムです。名前は「REward Increment = Nonnegative Factor $\times$ Offset Reinforcement $\times$ Characteristic Eligibility」の頭文字に由来しています。

アルゴリズムの手順は非常にシンプルです。

  1. 現在の方策 $\pi_\theta$ でエピソードを1回実行し、軌道 $(s_0, a_0, r_0, s_1, a_1, r_1, \ldots)$ を収集
  2. 各時刻のリターン $G_t = \sum_{t’=t}^{T-1} \gamma^{t’-t} r_{t’}$ を逆順に計算
  3. パラメータを更新: $\theta \leftarrow \theta + \alpha \sum_t \nabla_\theta \ln \pi_\theta(a_t|s_t) G_t$

ステップ3の更新は、方策勾配定理の期待値をモンテカルロ法(1エピソード分のサンプル)で近似したものです。REINFORCEはon-policyアルゴリズム、つまり「現在の方策で集めたデータだけを使って方策を更新する」手法です。一度使ったデータは方策の更新後に捨て、新しい方策で再度データを収集します。このため、サンプル効率(データの再利用効率)は低くなります。

勾配推定の分散問題

REINFORCEの最大の問題は、勾配推定の分散が非常に大きいことです。

直感的に、強化学習では「良い軌道もあれば悪い軌道もある」ので、サンプルごとの勾配の向きが大きくバラつきます。たとえばCartPole環境でも、たまたま最初にポールが大きく傾いた場合はすぐに失敗し(低い報酬)、偶然バランスが保てた場合は長く続きます(高い報酬)。このランダム性がそのまま勾配の推定のばらつきになります。

少数のサンプルから推定した勾配は、真の勾配とは大きくずれている可能性が高く、学習が遅くなったり不安定になったりします。分散を $\sigma^2$ とすると、勾配推定の精度は $O(\sigma / \sqrt{N})$($N$ はサンプル数)で改善しますが、分散自体を減らす方が圧倒的に効率的です。

この分散を低減する方法がベースラインです。

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

ベースラインの理論

ベースラインの考え方は、日常的な場面で自然に使われています。テストで80点を取ったとき、それが「良い成績」かどうかは平均点(ベースライン)に依存します。平均が60点なら80点は優秀ですが、平均が90点なら80点は平均以下です。方策勾配のベースラインも同じ発想です。報酬の絶対値ではなく、「期待される報酬と比べてどれだけ良かったか(悪かったか)」を評価基準にします。

勾配の式に $\theta$ に依存しない任意の関数 $b(s_t)$(ベースライン)を引いても、勾配の期待値は変わりません。

$$ \begin{equation} \nabla_\theta J(\theta) = \mathbb{E}\left[\sum_t \nabla_\theta \ln \pi_\theta(a_t|s_t) (G_t – b(s_t))\right] \end{equation} $$

なぜ期待値が変わらないかを証明します。ベースライン $b(s)$ は行動 $a$ に依存しない(状態 $s$ のみの関数)ため、期待値の外に出せます。

$$ \mathbb{E}_{a \sim \pi_\theta(\cdot|s)}\left[\nabla_\theta \ln \pi_\theta(a|s) b(s)\right] = b(s) \sum_a \pi_\theta(a|s) \frac{\nabla_\theta \pi_\theta(a|s)}{\pi_\theta(a|s)} $$

ここで対数微分トリックを逆に適用して $\nabla_\theta \ln \pi_\theta(a|s) = \frac{\nabla_\theta \pi_\theta(a|s)}{\pi_\theta(a|s)}$ を使いました。$\pi_\theta(a|s)$ が約分されて

$$ = b(s) \sum_a \nabla_\theta \pi_\theta(a|s) = b(s) \nabla_\theta \underbrace{\sum_a \pi_\theta(a|s)}_{=1} = 0 $$

確率の合計は常に1なので、その勾配はゼロです。したがってベースラインを引いても期待値は変わりませんが、分散は変わります。

ベースラインがなぜ分散を低減するかを直感的に理解しましょう。ベースラインなしでは、全ての報酬が正の値であった場合(たとえばCartPoleでは報酬は常に1)、全ての行動の確率が上がる方向に更新されます。うまくいった行動もそうでない行動も区別なく強化されてしまいます。ベースラインを引くことで、「平均より良い行動($G_t > b$)の確率を上げ、平均より悪い行動($G_t < b$)の確率を下げる」という明確なコントラストが生まれ、勾配のノイズが低減されます。

最適なベースライン

分散を最小化するベースラインは、状態 $s$ での期待リターン、すなわち状態価値関数 $V^{\pi}(s)$ に近い値です。

$$ b(s_t) \approx V^{\pi}(s_t) = \mathbb{E}\left[G_t | s_t\right] $$

$G_t – V^{\pi}(s_t)$ はアドバンテージ(advantage)と呼ばれ、「この行動が平均と比べてどれだけ良かったか」を表します。

$$ A^{\pi}(s_t, a_t) = G_t – V^{\pi}(s_t) $$

アドバンテージが正なら「平均より良い行動」、負なら「平均より悪い行動」です。アドバンテージを使った勾配は

$$ \nabla_\theta J(\theta) = \mathbb{E}\left[\sum_t \nabla_\theta \ln \pi_\theta(a_t|s_t) A^{\pi}(s_t, a_t)\right] $$

実際には $V^{\pi}(s)$ も未知なので、別のニューラルネットワーク(バリューネットワーク、critic と呼ばれます)で推定します。このニューラルネットワークは状態 $s$ を入力として期待リターンの推定値 $\hat{V}(s)$ を出力し、実際のリターン $G_t$ との二乗誤差 $(G_t – \hat{V}(s_t))^2$ を最小化するように学習します。

方策ネットワーク(actor)が「どの行動をとるか」を決め、バリューネットワーク(critic)が「今の状態はどれくらい良いか」を評価する。この構造がActor-Critic法です。actorとcriticが協力して学習することで、ベースラインなしのREINFORCEと比べて大幅に効率的な学習が可能になります。

ベースラインの理論を理解したところで、次にPyTorchで方策勾配法を実装してCartPole環境を解いてみましょう。

PyTorchによるREINFORCEの実装

CartPole環境

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.distributions import Categorical
import numpy as np
import matplotlib.pyplot as plt
import gymnasium as gym

torch.manual_seed(42)

# --- 方策ネットワーク ---
class PolicyNetwork(nn.Module):
    def __init__(self, state_dim, action_dim, hidden_dim=128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, action_dim),
        )

    def forward(self, state):
        logits = self.net(state)
        return Categorical(logits=logits)

    def get_action(self, state):
        """状態から行動をサンプリング"""
        state_tensor = torch.FloatTensor(state).unsqueeze(0)
        dist = self.forward(state_tensor)
        action = dist.sample()
        log_prob = dist.log_prob(action)
        return action.item(), log_prob

方策ネットワークは、状態(CartPoleでは4次元: カートの位置、速度、ポールの角度、角速度)を入力として受け取り、各行動(左に押す/右に押す)のlogitを出力します。Categorical 分布でソフトマックス確率に変換し、そこから行動をサンプリングします。log_prob はバックプロパゲーション用の対数確率です。

# --- REINFORCE with Baseline ---
class ValueNetwork(nn.Module):
    def __init__(self, state_dim, hidden_dim=128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 1),
        )

    def forward(self, state):
        return self.net(state).squeeze(-1)

def compute_returns(rewards, gamma=0.99):
    """リターン G_t の計算"""
    returns = []
    G = 0
    for r in reversed(rewards):
        G = r + gamma * G
        returns.insert(0, G)
    return torch.tensor(returns, dtype=torch.float32)

# --- 環境とモデルの初期化 ---
env = gym.make('CartPole-v1')
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n

policy = PolicyNetwork(state_dim, action_dim)
value_net = ValueNetwork(state_dim)
optimizer_policy = optim.Adam(policy.parameters(), lr=1e-3)
optimizer_value = optim.Adam(value_net.parameters(), lr=1e-3)
gamma = 0.99

ベースライン用のバリューネットワーク ValueNetwork を追加しています。このネットワークは状態から期待リターン(状態価値)を予測します。compute_returns 関数はエピソードの報酬列から割引累積報酬を逆順に計算します。

# --- 学習ループ ---
n_episodes = 1000
episode_rewards = []
episode_rewards_baseline = []

# REINFORCE without baseline
policy_no_bl = PolicyNetwork(state_dim, action_dim)
optimizer_no_bl = optim.Adam(policy_no_bl.parameters(), lr=1e-3)

for episode in range(n_episodes):
    # === ベースラインなし ===
    state, _ = env.reset(seed=episode)
    log_probs_no_bl = []
    rewards_no_bl = []

    done = False
    while not done:
        action, log_prob = policy_no_bl.get_action(state)
        next_state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
        log_probs_no_bl.append(log_prob)
        rewards_no_bl.append(reward)
        state = next_state

    returns_no_bl = compute_returns(rewards_no_bl, gamma)
    returns_normalized = (returns_no_bl - returns_no_bl.mean()) / (
        returns_no_bl.std() + 1e-8)

    policy_loss_no_bl = 0
    for lp, G in zip(log_probs_no_bl, returns_normalized):
        policy_loss_no_bl -= lp * G

    optimizer_no_bl.zero_grad()
    policy_loss_no_bl.backward()
    optimizer_no_bl.step()
    episode_rewards.append(sum(rewards_no_bl))

    # === ベースラインあり ===
    state, _ = env.reset(seed=episode)
    log_probs = []
    rewards = []
    states = []

    done = False
    while not done:
        states.append(state)
        action, log_prob = policy.get_action(state)
        next_state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
        log_probs.append(log_prob)
        rewards.append(reward)
        state = next_state

    returns = compute_returns(rewards, gamma)
    states_tensor = torch.FloatTensor(np.array(states))
    values = value_net(states_tensor).detach()

    # アドバンテージ = リターン - ベースライン(価値関数)
    advantages = returns - values
    advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

    # 方策の更新
    policy_loss = 0
    for lp, adv in zip(log_probs, advantages):
        policy_loss -= lp * adv

    optimizer_policy.zero_grad()
    policy_loss.backward()
    optimizer_policy.step()

    # バリューネットワークの更新
    values_pred = value_net(states_tensor)
    value_loss = F.mse_loss(values_pred, returns)
    optimizer_value.zero_grad()
    value_loss.backward()
    optimizer_value.step()

    episode_rewards_baseline.append(sum(rewards))

env.close()

学習ループでは2つの方策を並列に学習しています。「ベースラインなし」は素朴なREINFORCEで、リターンをそのまま使います(ただし正規化あり)。「ベースラインあり」はバリューネットワークでベースラインを推定し、アドバンテージを計算して使います。

# --- 結果の可視化 ---
fig, axes = plt.subplots(1, 3, figsize=(16, 5))

# (a) 学習曲線の比較
ax = axes[0]
window = 50
rewards_smooth = np.convolve(episode_rewards,
                              np.ones(window)/window, mode='valid')
rewards_bl_smooth = np.convolve(episode_rewards_baseline,
                                 np.ones(window)/window, mode='valid')
ax.plot(rewards_smooth, label='REINFORCE (no baseline)', alpha=0.8)
ax.plot(rewards_bl_smooth, label='REINFORCE (with baseline)', alpha=0.8)
ax.set_xlabel('Episode', fontsize=12)
ax.set_ylabel('Episode Reward', fontsize=12)
ax.set_title('Learning Curves', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.axhline(500, color='red', linestyle='--', alpha=0.5,
           label='Max reward')

# (b) 報酬の分散の比較
ax = axes[1]
window_var = 100
var_no_bl = [np.var(episode_rewards[max(0,i-window_var):i+1])
             for i in range(len(episode_rewards))]
var_bl = [np.var(episode_rewards_baseline[max(0,i-window_var):i+1])
          for i in range(len(episode_rewards_baseline))]
ax.plot(var_no_bl, label='Without baseline', alpha=0.7)
ax.plot(var_bl, label='With baseline', alpha=0.7)
ax.set_xlabel('Episode', fontsize=12)
ax.set_ylabel('Reward Variance', fontsize=12)
ax.set_title('Variance of Episode Rewards', fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)

# (c) 方策の改善過程
ax = axes[2]
state_example = torch.FloatTensor([[0.0, 0.0, 0.05, 0.0]])
with torch.no_grad():
    dist = policy(state_example)
    probs = dist.probs.numpy()[0]
ax.bar(['Left', 'Right'], probs, color=['steelblue', 'coral'],
       alpha=0.8)
ax.set_ylabel('Action Probability', fontsize=12)
ax.set_title('Learned Policy (pole tilted right)', fontsize=13)
ax.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.savefig('policy_gradient_result.png', dpi=150, bbox_inches='tight')
plt.show()

この可視化から、方策勾配法の学習特性を確認できます。

  1. 左図(学習曲線): ベースラインなし(青)とベースラインあり(オレンジ)のREINFORCEの報酬推移を比較しています。ベースラインありの方が学習が速く、安定しています。CartPole-v1の最大報酬は500ステップであり、良好な方策はこの上限に近づきます

  2. 中央図(報酬の分散): 学習中の報酬の分散を比較しています。ベースラインありの方が分散が一貫して低く、これは勾配推定の分散が低減されていることの反映です。分散が低い方が勾配の「信号対雑音比」が高く、効率的な学習が可能になります

  3. 右図(学習された方策): ポールが右に少し傾いた状態での行動確率です。「右に押す」確率が高くなっていれば、ポールを立て直す方向に正しく行動していることを意味します

方策勾配法の発展と限界

REINFORCEからの発展

REINFORCEは方策勾配法の出発点ですが、実用上はいくつかの限界があります。最も深刻なのはサンプル効率の低さです。1エピソード分のデータで勾配を推定するため、分散が大きく、多くのエピソードが必要になります。この問題に対して、いくつかの重要な改良が提案されてきました。

Actor-Critic法: 先述のように、バリューネットワークを使ってベースラインを推定する手法です。リターン $G_t$ の代わりに、TD誤差 $\delta_t = r_t + \gamma \hat{V}(s_{t+1}) – \hat{V}(s_t)$ をアドバンテージの推定値として使うことで、エピソード全体の終了を待たずにステップごとの更新が可能になります。

PPO(Proximal Policy Optimization): 方策の更新幅を制限することで学習の安定性を大幅に改善した手法です。方策比率 $r_t(\theta) = \pi_\theta(a_t|s_t) / \pi_{\theta_{\text{old}}}(a_t|s_t)$ をクリッピングすることで、1回の更新で方策が大きく変わりすぎることを防ぎます。実装がシンプルで性能が良いため、現在最も広く使われている方策勾配アルゴリズムの一つです。

SAC(Soft Actor-Critic): エントロピー正則化を導入した手法です。報酬の最大化だけでなく、方策のエントロピー(行動の多様性)も同時に最大化します。これにより探索が促進され、連続行動空間での性能が向上します。

方策勾配法 vs 価値ベース手法

強化学習のアルゴリズムは大きく「方策勾配法」と「価値ベース手法」(DQNなど)に分類されます。方策勾配法の主な利点は、連続的な行動空間を自然に扱えること、確率的方策を直接学習できること、そして収束の理論的保証があることです。一方、価値ベース手法はサンプル効率が高く(経験再生による過去データの再利用が可能)、離散行動空間では安定した学習が可能です。

実用上は、両者の利点を組み合わせたActor-Critic系の手法が主流となっています。

まとめ

本記事では、方策勾配定理の導出とREINFORCEアルゴリズムの実装について解説しました。

  • 方策勾配定理は期待報酬の勾配を $\nabla_\theta J = \mathbb{E}[\sum_t \nabla_\theta \ln \pi_\theta(a_t|s_t) G_t]$ として与え、環境モデル(状態遷移確率)を知らなくても計算可能
  • 対数微分トリックにより、$\theta$ に依存する分布の期待値の勾配をサンプリングで近似可能にする
  • REINFORCEは方策勾配定理を直接実装した最もシンプルなアルゴリズムだが、分散が大きいという問題がある
  • ベースラインを導入することで、勾配推定のバイアスを変えずに分散を低減でき、学習を効率化する
  • 状態価値関数 $V(s)$ が最適なベースラインの近似であり、これがActor-Critic法へとつながる
  • PPOやSACなどの現代的なアルゴリズムは、方策勾配定理を基盤としつつ、更新幅の制限やエントロピー正則化などの改良を加えたもの

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