Skip to content
Published on

[深層強化学習] 10. Actor-Critic方法:A2Cとハイパーパラメータチューニング

Authors

REINFORCEの分散問題の復習

前の記事でREINFORCEアルゴリズムを見ました。核心問題は勾配推定の高い分散でした。

REINFORCEは全エピソードが終わらないと更新できず(モンテカルロ)、1つのエピソードから計算した勾配のノイズが非常に大きいです。

ベースラインで分散を減らせますが、より根本的な解決策が必要です。


Actor-Criticアーキテクチャ

Actor-Criticは2つの構成要素を結合します:

  • Actor(方策): 状態から行動を選択します。pi(a|s; theta)
  • Critic(価値関数): 現在の状態の価値を評価します。V(s; phi)

核心アイデアはモンテカルロリターンの代わりにTD(Temporal Difference)推定を使用して分散を減らすことです。

REINFORCE vs Actor-Critic

REINFORCE:     grad = log pi(a|s) * G_t         (에피소드 끝까지 기다림)
Actor-Critic:  grad = log pi(a|s) * (r + gamma * V(s') - V(s))  (한 스텝만 필요)

r + gamma * V(s') - V(s)TD誤差またはアドバンテージ推定値と呼びます。V(s)がベースラインの役割を果たしながら同時にリターンの推定値も提供します。


A2C(Advantage Actor-Critic)実装

A2CはActor-Criticの同期化(synchronous)バージョンです。複数の環境を並列に実行して多様な経験を同時に収集します。

ネットワーク構造

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

class A2CNetwork(nn.Module):
    """A2C를 위한 공유 네트워크 (Actor + Critic)"""
    def __init__(self, obs_size, n_actions, hidden_size=256):
        super().__init__()
        self.shared = nn.Sequential(
            nn.Linear(obs_size, hidden_size), nn.ReLU(),
            nn.Linear(hidden_size, hidden_size), nn.ReLU(),
        )
        self.actor = nn.Linear(hidden_size, n_actions)
        self.critic = nn.Linear(hidden_size, 1)

    def forward(self, x):
        features = self.shared(x)
        logits = self.actor(features)
        value = self.critic(features)
        return logits, value

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

Atari用CNN A2C

class A2CCNN(nn.Module):
    """Atari용 CNN 기반 A2C 네트워크"""
    def __init__(self, input_channels, n_actions):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(input_channels, 32, kernel_size=8, stride=4), nn.ReLU(),
            nn.Conv2d(32, 64, kernel_size=4, stride=2), nn.ReLU(),
            nn.Conv2d(64, 64, kernel_size=3, stride=1), nn.ReLU(),
        )
        conv_out_size = self._get_conv_out(input_channels)
        self.fc = nn.Sequential(nn.Linear(conv_out_size, 512), nn.ReLU())
        self.actor = nn.Linear(512, n_actions)
        self.critic = nn.Linear(512, 1)

    def _get_conv_out(self, channels):
        o = self.conv(torch.zeros(1, channels, 84, 84))
        return int(np.prod(o.size()))

    def forward(self, x):
        x = x.float() / 255.0
        conv_out = self.conv(x).view(x.size(0), -1)
        features = self.fc(conv_out)
        return self.actor(features), self.critic(features)

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

N-stepアドバンテージ計算

A2Cでは1ステップではなく複数ステップの報酬を使ってアドバンテージを計算します。これにより偏りと分散のバランスを取ります。

def compute_advantages(rewards, values, dones, next_value, gamma=0.99):
    """N-step 어드밴티지 계산"""
    n_steps = len(rewards)
    returns = []
    advantages = []
    R = next_value
    for t in reversed(range(n_steps)):
        if dones[t]:
            R = 0.0
        R = rewards[t] + gamma * R
        returns.insert(0, R)
        advantages.insert(0, R - values[t])
    returns = torch.tensor(returns, dtype=torch.float32)
    advantages = torch.tensor(advantages, dtype=torch.float32)
    return returns, advantages

GAE(Generalized Advantage Estimation)

GAEは複数の長さのTD誤差を指数加重平均してアドバンテージを推定します。

def compute_gae(rewards, values, dones, next_value, gamma=0.99, gae_lambda=0.95):
    """GAE (Generalized Advantage Estimation) 계산"""
    n_steps = len(rewards)
    advantages = np.zeros(n_steps)
    last_gae = 0.0

    for t in reversed(range(n_steps)):
        if t == n_steps - 1:
            next_val = next_value
        else:
            next_val = values[t + 1]
        if dones[t]:
            next_val = 0.0
            last_gae = 0.0
        delta = rewards[t] + gamma * next_val - values[t]
        advantages[t] = last_gae = delta + gamma * gae_lambda * last_gae

    returns = advantages + np.array(values)
    return torch.tensor(returns, dtype=torch.float32), \
           torch.tensor(advantages, dtype=torch.float32)

GAEのlambdaパラメータは偏り-分散トレードオフを制御します:

  • lambda = 0: 1-step TD(低分散、高偏り)
  • lambda = 1: モンテカルロリターン(高分散、低偏り)
  • lambda = 0.95: 実務で頻繁に使われる値

A2C学習ループ

CartPole A2C

import gymnasium as gym

def train_a2c_cartpole():
    """A2C로 CartPole 학습"""
    N_ENVS = 8
    N_STEPS = 5
    GAMMA = 0.99
    LEARNING_RATE = 7e-4
    VALUE_LOSS_COEF = 0.5
    ENTROPY_COEF = 0.01
    MAX_GRAD_NORM = 0.5
    TOTAL_STEPS = 200000

    envs = gym.make_vec("CartPole-v1", num_envs=N_ENVS)
    obs_size = envs.single_observation_space.shape[0]
    n_actions = envs.single_action_space.n
    device = torch.device("cpu")
    model = A2CNetwork(obs_size, n_actions).to(device)
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)

    obs, _ = envs.reset()
    episode_rewards = np.zeros(N_ENVS)
    completed_rewards = []
    global_step = 0

    while global_step < TOTAL_STEPS:
        batch_obs = []
        batch_actions = []
        batch_log_probs = []
        batch_values = []
        batch_rewards = []
        batch_dones = []
        batch_entropies = []

        for step in range(N_STEPS):
            obs_t = torch.tensor(obs, dtype=torch.float32).to(device)
            with torch.no_grad():
                actions, log_probs, values, entropies = model.get_action_and_value(obs_t)
            next_obs, rewards, terminateds, truncateds, infos = envs.step(actions.numpy())
            dones = np.logical_or(terminateds, truncateds)
            batch_obs.append(obs_t)
            batch_actions.append(actions)
            batch_log_probs.append(log_probs)
            batch_values.append(values)
            batch_rewards.append(rewards)
            batch_dones.append(dones)
            batch_entropies.append(entropies)
            episode_rewards += rewards
            for i, done in enumerate(dones):
                if done:
                    completed_rewards.append(episode_rewards[i])
                    episode_rewards[i] = 0
            obs = next_obs
            global_step += N_ENVS

        with torch.no_grad():
            _, next_value = model(torch.tensor(obs, dtype=torch.float32).to(device))
            next_value = next_value.squeeze(-1)

        values_list = [v.detach().numpy() for v in batch_values]
        returns_list = []
        advantages_list = []
        for env_idx in range(N_ENVS):
            env_rewards = [batch_rewards[t][env_idx] for t in range(N_STEPS)]
            env_values = [values_list[t][env_idx] for t in range(N_STEPS)]
            env_dones = [batch_dones[t][env_idx] for t in range(N_STEPS)]
            env_next_val = next_value[env_idx].item()
            rets, advs = compute_gae(env_rewards, env_values, env_dones, env_next_val, GAMMA)
            returns_list.append(rets)
            advantages_list.append(advs)

        all_log_probs = torch.stack(batch_log_probs).view(-1)
        all_values = torch.stack(batch_values).view(-1)
        all_entropies = torch.stack(batch_entropies).view(-1)
        all_returns = torch.stack(returns_list, dim=1).view(-1)
        all_advantages = torch.stack(advantages_list, dim=1).view(-1)
        all_advantages = (all_advantages - all_advantages.mean()) / (all_advantages.std() + 1e-8)

        policy_loss = -(all_log_probs * all_advantages.detach()).mean()
        value_loss = F.mse_loss(all_values, all_returns.detach())
        entropy_loss = all_entropies.mean()
        total_loss = policy_loss + VALUE_LOSS_COEF * value_loss - ENTROPY_COEF * entropy_loss

        optimizer.zero_grad()
        total_loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), MAX_GRAD_NORM)
        optimizer.step()

        if len(completed_rewards) >= 10 and global_step % 1000 < N_ENVS * N_STEPS:
            mean_reward = np.mean(completed_rewards[-10:])
            print(f"스텝 {global_step}: 평균 보상={mean_reward:.1f}")
            if mean_reward >= 475:
                print(f"스텝 {global_step}에서 해결!")
                break

    envs.close()
    return model, completed_rewards

# model, rewards = train_a2c_cartpole()

ハイパーパラメータチューニング

A2Cの性能はハイパーパラメータに敏感です。各パラメータのガイドラインは以下の通りです:

学習率(Learning Rate)

  • 大きすぎる学習率(1e-2): 学習が不安定で発散する可能性
  • 適切な学習率(7e-4 ~ 1e-3): 速く安定的な学習
  • 小さすぎる学習率(1e-5): 学習が非常に遅く収束に長い時間

エントロピー係数(Entropy Coefficient)

  • エントロピー係数0: 探索なしで速く収束するが局所最適に陥る可能性
  • エントロピー係数0.01: 適切な探索と活用のバランス
  • エントロピー係数0.5: 過度な探索で学習が非常に遅い
  • 推奨範囲: 0.001 ~ 0.05

並列環境数(Number of Environments)

並列環境が多いほど各更新に使われるデータが多様になります:

  • 1環境: 高分散、遅い学習
  • 8環境: 良いバランス(CartPole推奨)
  • 16環境: Atariゲームに適合
  • 32環境: より安定的だがメモリ使用増加

ハイパーパラメータまとめ

パラメータCartPole推奨値Pong推奨値役割
学習率7e-47e-4パラメータ更新サイズ
ガンマ0.990.99未来報酬割引
エントロピー係数0.010.01探索強度
価値損失係数0.50.5Critic学習強度
勾配クリッピング0.50.5学習安定性
N-steps55更新間隔
並列環境数816データ多様性
GAE lambda0.950.95偏り-分散トレードオフ

A2C vs A3C

A3C(Asynchronous Advantage Actor-Critic)はA2Cの非同期バージョンです。

実務ではA2CがA3Cより多く使われます。GPUを活用したバッチ処理が可能で、実装が単純、性能も同等以上だからです。


デバッグのヒント

A2C学習時によく遭遇する問題と解決方法です:

  • 報酬が変化しない: 学習率が小さすぎないか確認、エントロピーが0に収束していないか確認、勾配消失を確認
  • 報酬が急激に下落: 学習率が大きすぎないか確認、勾配クリッピングが適用されているか確認、価値損失が爆発していないか確認
  • エントロピーが0に収束: エントロピー係数を上げる、学習率を下げる、行動空間が正しいか確認
  • 価値損失が減らない: 価値損失係数を上げる、リターン計算が正しいか確認、ガンマが適切か確認

全シリーズまとめ

このシリーズで扱った深層強化学習の核心トピックを整理します。

テーマ核心概念
01強化学習とはMDP、エージェント-環境相互作用、報酬
02OpenAI Gym環境API、ラッパー、ベクトル環境
03PyTorch基礎テンソル、自動微分、ニューラルネットワーク
04Cross-Entropyエリートエピソード選別、CartPole
05ベルマン方程式価値関数、価値反復、Q学習
06DQN経験リプレイ、ターゲットネットワーク
07DQN拡張Double、Dueling、Rainbow
08株式トレーディング金融環境設計、報酬関数
09Policy GradientREINFORCE、分散削減
10Actor-CriticA2C、ハイパーパラメータチューニング

次のステップ

このシリーズで扱えなかった上級トピックです:

  • PPO(Proximal Policy Optimization): 現在最も広く使われている方策ベースアルゴリズム
  • SAC(Soft Actor-Critic): エントロピー正則化を使用したオフポリシーアクター・クリティック
  • モデルベースRL: 環境モデルを学習してサンプル効率を向上
  • マルチエージェントRL: 複数のエージェントが協力/競争する環境
  • RLHF: 人間フィードバックによる強化学習(LLM学習に活用)

まとめ

  1. Actor-Critic: 方策(Actor)と価値(Critic)を同時に学習して分散を削減
  2. A2C: 同期化された並列環境でデータ収集効率を向上
  3. GAE: lambdaで偏り-分散トレードオフを制御するアドバンテージ推定
  4. ハイパーパラメータ: 学習率、エントロピー係数、環境数、N-stepが核心
  5. デバッグ: 報酬、損失、エントロピー、勾配ノルムを持続的にモニタリング

Actor-Critic方法は現代強化学習の基礎です。PPO、SACなど最新アルゴリズムもすべてActor-Critic構造を基盤としています。