Skip to content
Published on

[深層強化学習] 09. Policy Gradient:方策ベースの強化学習

Authors

価値ベース vs 方策ベース

これまで扱ったDQN系の方法は**価値ベース(value-based)**アプローチでした。Q関数を学習し、Q値が最も高い行動を選択する間接的な方式です。

**方策ベース(policy-based)**方法は方策を直接パラメータ化して最適化します。方策ネットワーク pi(a|s; theta) が各状態で行動の確率分布を出力します。

価値ベースの限界

  1. 離散行動に限定: DQNは連続行動空間に直接適用が困難
  2. 決定的方策: Q値のargmaxを取るため確率的方策を自然に表現しにくい
  3. 収束不安定性: 価値関数の小さな変化が方策の急激な変化を引き起こす可能性

方策ベースの長所

  1. 連続行動空間: ガウシアン方策などで自然に連続行動を扱える
  2. 確率的方策: 探索が方策に内蔵されており、別途のイプシロンスケジュールが不要
  3. 収束保証: 局所最適解への収束が理論的に保証(適切な学習率下で)
  4. 部分観測環境: 確率的方策が部分観測問題でより自然

方策の表現(Policy Representation)

離散行動:ソフトマックス方策

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

class DiscretePolicyNetwork(nn.Module):
    """이산 행동 공간을 위한 정책 네트워크"""
    def __init__(self, obs_size, n_actions, hidden_size=128):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_size, hidden_size), nn.ReLU(),
            nn.Linear(hidden_size, hidden_size), nn.ReLU(),
            nn.Linear(hidden_size, n_actions),
        )

    def forward(self, x):
        return self.net(x)

    def get_action_prob(self, state):
        logits = self.forward(state)
        probs = F.softmax(logits, dim=-1)
        return probs

    def select_action(self, state):
        probs = self.get_action_prob(state)
        dist = torch.distributions.Categorical(probs)
        action = dist.sample()
        log_prob = dist.log_prob(action)
        return action.item(), log_prob

連続行動:ガウシアン方策

class ContinuousPolicyNetwork(nn.Module):
    """연속 행동 공간을 위한 가우시안 정책 네트워크"""
    def __init__(self, obs_size, action_size, hidden_size=128):
        super().__init__()
        self.shared = nn.Sequential(
            nn.Linear(obs_size, hidden_size), nn.ReLU(),
            nn.Linear(hidden_size, hidden_size), nn.ReLU(),
        )
        self.mean_head = nn.Linear(hidden_size, action_size)
        self.log_std_head = nn.Linear(hidden_size, action_size)

    def forward(self, x):
        features = self.shared(x)
        mean = self.mean_head(features)
        log_std = self.log_std_head(features).clamp(-20, 2)
        std = log_std.exp()
        return mean, std

    def select_action(self, state):
        mean, std = self.forward(state)
        dist = torch.distributions.Normal(mean, std)
        action = dist.sample()
        log_prob = dist.log_prob(action).sum(dim=-1)
        return action.detach().numpy(), log_prob

Policy Gradient導出

目的関数

方策ベース方法の目標は期待累積報酬を最大化することです:

J(theta) = E_pi[ sum_t gamma^t * r_t ]

この目的関数をthetaについて微分して勾配を求める必要があります。

Policy Gradient定理

核心的な結果は以下の通りです:

grad J(theta) = E_pi[ sum_t grad log pi(a_t | s_t; theta) * G_t ]

ここでG_tは時点tからの割引累積報酬です。

この定理の意味を直感的に説明すると:

  • 高い報酬を受けた行動: log piの勾配方向にパラメータを更新し、その行動の確率を高める
  • 低い報酬を受けた行動: 反対方向に更新し、その行動の確率を下げる

導出の核心

導出の核心トリックは「対数微分トリック(log-derivative trick)」です:

grad pi(a|s; theta) = pi(a|s; theta) * grad log pi(a|s; theta)

これにより期待値形式に変換でき、サンプリングによる近似が可能になります。


REINFORCEアルゴリズム

REINFORCEは最も基本的なPolicy Gradientアルゴリズムです。モンテカルロ方式で全エピソードを収集した後更新します。

アルゴリズム疑似コード

1. 정책 네트워크 pi(a|s; theta) 초기화
2. 반복:
   a. 현재 정책으로 에피소드 하나를 수집
      -스텝에서 (s_t, a_t, r_t, log pi(a_t|s_t))를 기록
   b.  시점의 할인 누적 보상 G_t를 계산
   c. 정책 그래디언트 계산:
      loss = -sum_t log pi(a_t|s_t) * G_t
   d. 역전파로 theta 업데이트

CartPole REINFORCE実装

import gymnasium as gym
import torch
import torch.optim as optim

def train_reinforce_cartpole():
    """REINFORCE로 CartPole 학습"""
    env = gym.make("CartPole-v1")
    obs_size = env.observation_space.shape[0]
    n_actions = env.action_space.n

    policy = DiscretePolicyNetwork(obs_size, n_actions, hidden_size=128)
    optimizer = optim.Adam(policy.parameters(), lr=0.001)
    gamma = 0.99
    rewards_history = []

    for episode in range(1000):
        log_probs = []
        rewards = []
        obs, _ = env.reset()

        while True:
            obs_tensor = torch.tensor([obs], dtype=torch.float32)
            action, log_prob = policy.select_action(obs_tensor)
            next_obs, reward, terminated, truncated, _ = env.step(action)
            log_probs.append(log_prob)
            rewards.append(reward)
            obs = next_obs
            if terminated or truncated:
                break

        returns = []
        G = 0
        for r in reversed(rewards):
            G = r + gamma * G
            returns.insert(0, G)

        returns = torch.tensor(returns, dtype=torch.float32)
        if len(returns) > 1:
            returns = (returns - returns.mean()) / (returns.std() + 1e-8)

        log_probs_tensor = torch.stack(log_probs)
        policy_loss = -(log_probs_tensor * returns).sum()

        optimizer.zero_grad()
        policy_loss.backward()
        optimizer.step()

        total_reward = sum(rewards)
        rewards_history.append(total_reward)

        if episode % 50 == 0:
            mean_reward = np.mean(rewards_history[-50:])
            print(f"에피소드 {episode}: 보상={total_reward:.0f}, 평균={mean_reward:.1f}")
            if mean_reward >= 475:
                print(f"에피소드 {episode}에서 해결!")
                break

    env.close()
    return policy, rewards_history

# policy, history = train_reinforce_cartpole()

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

問題:高い分散

基本REINFORCEの勾配推定は分散が非常に高いです。1つのエピソードから計算した勾配が非常にノイジーで学習が不安定です。

解決:ベースライン関数

勾配から定数ベースラインbを引いても期待値は変わりませんが分散は減ります:

grad J(theta) = E_pi[ sum_t grad log pi(a_t|s_t; theta) * (G_t - b) ]

最も一般的なベースラインは**状態価値関数V(s)**です。

class PolicyWithBaseline(nn.Module):
    """베이스라인이 있는 정책 네트워크"""
    def __init__(self, obs_size, n_actions, hidden_size=128):
        super().__init__()
        self.shared = nn.Sequential(nn.Linear(obs_size, hidden_size), nn.ReLU())
        self.policy_head = nn.Sequential(nn.Linear(hidden_size, hidden_size), nn.ReLU(), nn.Linear(hidden_size, n_actions))
        self.value_head = nn.Sequential(nn.Linear(hidden_size, hidden_size), nn.ReLU(), nn.Linear(hidden_size, 1))

    def forward(self, x):
        features = self.shared(x)
        return self.policy_head(features), self.value_head(features)

    def select_action(self, state):
        logits, value = self.forward(state)
        probs = F.softmax(logits, dim=-1)
        dist = torch.distributions.Categorical(probs)
        action = dist.sample()
        return action.item(), dist.log_prob(action), value

探索問題とエントロピーボーナス

早期収束問題

方策ベース方法は高い報酬を受けた行動の確率を素早く高めるため、十分に探索する前に次善の方策に収束することがあります。

エントロピーボーナス

方策のエントロピーを損失関数に追加すると探索を促進できます:

total_loss = policy_loss + value_loss_coef * value_loss - entropy_coef * entropy

エントロピーが高いということは行動確率が均等であることを意味するので、エントロピーを最大化すると探索が促進されます。

def compute_entropy_loss(logits):
    """정책 엔트로피 계산"""
    probs = F.softmax(logits, dim=-1)
    log_probs = F.log_softmax(logits, dim=-1)
    entropy = -(probs * log_probs).sum(dim=-1)
    return entropy.mean()

分散削減技法の比較

技法説明分散削減効果
リターン正規化G_tを平均0、分散1に正規化中程度
ベースラインアドバンテージ G_t - V(s_t) を使用高い
時間依存ベースライン未来の報酬のみ考慮高い
GAE複数ステップのアドバンテージの加重平均非常に高い

まとめ

  1. 方策ベース方法: 方策を直接パラメータ化して最適化するアプローチ
  2. Policy Gradient定理: 期待報酬の勾配を対数確率とリターンの積で表現
  3. REINFORCE: 最も基本的なモンテカルロPolicy Gradientアルゴリズム
  4. ベースライン: 価値関数をベースラインとして使用して分散を大きく削減
  5. エントロピーボーナス: 方策のエントロピーを高めて早期収束を防止
  6. 限界: 高い分散、オンポリシー学習による低いデータ効率

REINFORCEはCartPoleのような簡単な環境ではうまく動作しますが、Pongのような複雑な環境では学習が非常に遅いです。次の記事ではこの問題を解決するActor-Critic方法を扱います。