- Authors

- Name
- Youngju Kim
- @fjvbn20031
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-4 | 7e-4 | パラメータ更新サイズ |
| ガンマ | 0.99 | 0.99 | 未来報酬割引 |
| エントロピー係数 | 0.01 | 0.01 | 探索強度 |
| 価値損失係数 | 0.5 | 0.5 | Critic学習強度 |
| 勾配クリッピング | 0.5 | 0.5 | 学習安定性 |
| N-steps | 5 | 5 | 更新間隔 |
| 並列環境数 | 8 | 16 | データ多様性 |
| GAE lambda | 0.95 | 0.95 | 偏り-分散トレードオフ |
A2C vs A3C
A3C(Asynchronous Advantage Actor-Critic)はA2Cの非同期バージョンです。
実務ではA2CがA3Cより多く使われます。GPUを活用したバッチ処理が可能で、実装が単純、性能も同等以上だからです。
デバッグのヒント
A2C学習時によく遭遇する問題と解決方法です:
- 報酬が変化しない: 学習率が小さすぎないか確認、エントロピーが0に収束していないか確認、勾配消失を確認
- 報酬が急激に下落: 学習率が大きすぎないか確認、勾配クリッピングが適用されているか確認、価値損失が爆発していないか確認
- エントロピーが0に収束: エントロピー係数を上げる、学習率を下げる、行動空間が正しいか確認
- 価値損失が減らない: 価値損失係数を上げる、リターン計算が正しいか確認、ガンマが適切か確認
全シリーズまとめ
このシリーズで扱った深層強化学習の核心トピックを整理します。
| 回 | テーマ | 核心概念 |
|---|---|---|
| 01 | 強化学習とは | MDP、エージェント-環境相互作用、報酬 |
| 02 | OpenAI Gym | 環境API、ラッパー、ベクトル環境 |
| 03 | PyTorch基礎 | テンソル、自動微分、ニューラルネットワーク |
| 04 | Cross-Entropy | エリートエピソード選別、CartPole |
| 05 | ベルマン方程式 | 価値関数、価値反復、Q学習 |
| 06 | DQN | 経験リプレイ、ターゲットネットワーク |
| 07 | DQN拡張 | Double、Dueling、Rainbow |
| 08 | 株式トレーディング | 金融環境設計、報酬関数 |
| 09 | Policy Gradient | REINFORCE、分散削減 |
| 10 | Actor-Critic | A2C、ハイパーパラメータチューニング |
次のステップ
このシリーズで扱えなかった上級トピックです:
- PPO(Proximal Policy Optimization): 現在最も広く使われている方策ベースアルゴリズム
- SAC(Soft Actor-Critic): エントロピー正則化を使用したオフポリシーアクター・クリティック
- モデルベースRL: 環境モデルを学習してサンプル効率を向上
- マルチエージェントRL: 複数のエージェントが協力/競争する環境
- RLHF: 人間フィードバックによる強化学習(LLM学習に活用)
まとめ
- Actor-Critic: 方策(Actor)と価値(Critic)を同時に学習して分散を削減
- A2C: 同期化された並列環境でデータ収集効率を向上
- GAE: lambdaで偏り-分散トレードオフを制御するアドバンテージ推定
- ハイパーパラメータ: 学習率、エントロピー係数、環境数、N-stepが核心
- デバッグ: 報酬、損失、エントロピー、勾配ノルムを持続的にモニタリング
Actor-Critic方法は現代強化学習の基礎です。PPO、SACなど最新アルゴリズムもすべてActor-Critic構造を基盤としています。