Skip to content
Published on

[深層強化学習] 04. Cross-Entropy方法でCartPoleを解く

Authors

強化学習方法論の分類

強化学習アルゴリズムは様々な基準で分類できます。全体像を理解すると各アルゴリズムの位置を把握しやすくなります。

モデルベース vs モデルフリー

  • モデルベース(Model-Based): 環境の遷移確率と報酬関数を知っているか学習します。これを利用して計画(planning)を行います。
  • モデルフリー(Model-Free): 環境モデルなしで直接経験から学習します。大部分の実用的な深層強化学習がここに該当します。

価値ベース vs 方策ベース

  • 価値ベース(Value-Based): 状態や行動の価値を推定し、それに基づいて行動を選択します。DQNが代表的です。
  • 方策ベース(Policy-Based): 方策自体を直接パラメータ化して最適化します。REINFORCE、PPOなどが該当します。
  • アクター・クリティック(Actor-Critic): 価値ベースと方策ベースの利点を結合します。

オンポリシー vs オフポリシー

  • オンポリシー(On-Policy): 現在の方策で収集したデータのみで学習します。データ効率は低いですが安定的です。
  • オフポリシー(Off-Policy): 過去の方策や他の方策で収集したデータも活用します。データ効率が高いです。
分類アルゴリズム例
価値ベース、オフポリシーDQN, Double DQN
方策ベース、オンポリシーREINFORCE, PPO
アクター・クリティック、オンポリシーA2C, A3C
アクター・クリティック、オフポリシーSAC, DDPG, TD3

Cross-Entropy方法の核心アイデア

Cross-Entropy方法は強化学習で最も単純なアルゴリズムの1つです。核心アイデアは以下の通りです:

  1. 現在の方策で複数のエピソードを実行する
  2. 全エピソードの中から報酬が高い上位エピソードのみ選別する(エリートエピソード)
  3. エリートエピソードの状態-行動ペアを使って方策を更新する
  4. 上記の過程を繰り返す

この方法は進化戦略の一種で、良いエピソードを模倣する方式で方策を改善します。

アルゴリズム疑似コード

1. 정책 네트워크 초기화
2. 반복:
   a. N개의 에피소드를 현재 정책으로 수집
   b.  에피소드의 총 보상을 계산
   c. 상위 p%의 엘리트 에피소드를 선별 (보상 기준 상위 임계값)
   d. 엘리트 에피소드의 (상태, 행동) 쌍으로 정책 네트워크 학습
      - 손실 함수: Cross-Entropy Loss (분류 문제로 취급)
   e. 평균 보상이 충분히 높으면 종료

CartPole実装

方策ネットワーク定義

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gymnasium as gym
from collections import namedtuple

# 에피소드 데이터 구조
Episode = namedtuple('Episode', ['reward', 'steps'])
EpisodeStep = namedtuple('EpisodeStep', ['observation', 'action'])

class PolicyNetwork(nn.Module):
    """Cross-Entropy 방법을 위한 정책 네트워크"""
    def __init__(self, obs_size, hidden_size, n_actions):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, n_actions),
        )

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

エピソード収集

def generate_batch(env, net, batch_size, device="cpu"):
    """현재 정책으로 에피소드 배치를 생성"""
    batch = []
    episode_reward = 0.0
    episode_steps = []
    obs, _ = env.reset()
    sm = nn.Softmax(dim=1)

    while True:
        obs_tensor = torch.tensor(np.array([obs]), dtype=torch.float32).to(device)
        action_probs = sm(net(obs_tensor))
        action_probs_np = action_probs.data.cpu().numpy()[0]

        # 확률에 따라 행동 선택
        action = np.random.choice(len(action_probs_np), p=action_probs_np)

        next_obs, reward, terminated, truncated, _ = env.step(action)
        episode_reward += reward
        episode_steps.append(EpisodeStep(observation=obs, action=action))

        if terminated or truncated:
            batch.append(Episode(reward=episode_reward, steps=episode_steps))
            episode_reward = 0.0
            episode_steps = []
            next_obs, _ = env.reset()

            if len(batch) >= batch_size:
                break

        obs = next_obs

    return batch

エリートエピソードフィルタリング

def filter_elite_episodes(batch, percentile):
    """상위 percentile%의 엘리트 에피소드 선별"""
    rewards = [episode.reward for episode in batch]
    reward_threshold = np.percentile(rewards, percentile)

    elite_observations = []
    elite_actions = []

    for episode in batch:
        if episode.reward >= reward_threshold:
            for step in episode.steps:
                elite_observations.append(step.observation)
                elite_actions.append(step.action)

    return (
        torch.tensor(np.array(elite_observations), dtype=torch.float32),
        torch.tensor(np.array(elite_actions), dtype=torch.long),
        reward_threshold,
        np.mean(rewards),
    )

学習ループ

def train_cross_entropy_cartpole():
    """Cross-Entropy 방법으로 CartPole 학습"""
    # 하이퍼파라미터
    HIDDEN_SIZE = 128
    BATCH_SIZE = 16
    PERCENTILE = 70
    LEARNING_RATE = 0.01
    GOAL_REWARD = 475  # CartPole-v1의 목표 보상

    env = gym.make("CartPole-v1")
    obs_size = env.observation_space.shape[0]
    n_actions = env.action_space.n

    net = PolicyNetwork(obs_size, HIDDEN_SIZE, n_actions)
    optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE)
    loss_fn = nn.CrossEntropyLoss()

    for iteration in range(100):
        # 1. 에피소드 배치 생성
        batch = generate_batch(env, net, BATCH_SIZE)

        # 2. 엘리트 에피소드 선별
        elite_obs, elite_actions, reward_threshold, mean_reward = \
            filter_elite_episodes(batch, PERCENTILE)

        # 3. 정책 네트워크 학습
        optimizer.zero_grad()
        action_logits = net(elite_obs)
        loss = loss_fn(action_logits, elite_actions)
        loss.backward()
        optimizer.step()

        print(
            f"반복 {iteration:3d}: "
            f"평균 보상={mean_reward:6.1f}, "
            f"임계값={reward_threshold:6.1f}, "
            f"손실={loss.item():.4f}"
        )

        if mean_reward >= GOAL_REWARD:
            print(f"목표 달성! 평균 보상: {mean_reward:.1f}")
            break

    env.close()
    return net

# 학습 실행
trained_net = train_cross_entropy_cartpole()

予想出力

반복   0: 평균 보상=  21.3, 임계값=  23.0, 손실=0.6897
반복   1: 평균 보상=  25.1, 임계값=  27.0, 손실=0.6753
...
반복  20: 평균 보상= 152.4, 임계값= 185.0, 손실=0.5832
...
반복  40: 평균 보상= 421.8, 임계값= 500.0, 손실=0.5106
반복  45: 평균 보상= 487.2, 임계값= 500.0, 손실=0.4923
목표 달성! 평균 보상: 487.2

学習済みエージェントの評価

def evaluate_agent(env_name, net, n_episodes=100, render=False):
    """학습된 에이전트의 성능을 평가"""
    render_mode = "human" if render else None
    env = gym.make(env_name, render_mode=render_mode)
    rewards = []

    for _ in range(n_episodes):
        obs, _ = env.reset()
        total_reward = 0

        while True:
            obs_tensor = torch.tensor(np.array([obs]), dtype=torch.float32)
            with torch.no_grad():
                action_logits = net(obs_tensor)
            action = action_logits.argmax(dim=1).item()

            obs, reward, terminated, truncated, _ = env.step(action)
            total_reward += reward

            if terminated or truncated:
                break

        rewards.append(total_reward)

    env.close()
    print(f"평가 결과 ({n_episodes}회):")
    print(f"  평균 보상: {np.mean(rewards):.1f} +/- {np.std(rewards):.1f}")
    print(f"  최소/최대: {np.min(rewards):.1f} / {np.max(rewards):.1f}")

# evaluate_agent("CartPole-v1", trained_net)

FrozenLake実装

FrozenLakeは4x4の氷の上でスタート地点からゴール地点まで到達する問題です。滑りやすい氷面のため意図した方向に動かないことがあります。

FrozenLakeの特性

  • 状態:16個(4x4グリッドの各位置)
  • 行動:4個(上、下、左、右)
  • 報酬:ゴール到達時1、穴に落ちると0(エピソード終了)
  • 滑りやすさ:意図した方向には1/3の確率で移動、残り2/3は横に滑る

ワンホットエンコーディングで状態表現

FrozenLakeの観測は整数1つ(現在位置)なので、ニューラルネットワークへの入力のためにワンホットエンコーディングを使用します。

class DiscreteOneHotWrapper(gym.ObservationWrapper):
    """이산 관찰을 원핫 벡터로 변환하는 래퍼"""
    def __init__(self, env):
        super().__init__(env)
        n = env.observation_space.n
        self.observation_space = gym.spaces.Box(
            low=0.0, high=1.0, shape=(n,), dtype=np.float32
        )

    def observation(self, observation):
        result = np.zeros(self.observation_space.shape[0], dtype=np.float32)
        result[observation] = 1.0
        return result

FrozenLakeにCross-Entropyを適用

FrozenLakeではいくつかの修正が必要です。報酬が非常に希薄(ゴール到達時のみ1)であるためです。

def train_cross_entropy_frozenlake():
    """Cross-Entropy 방법으로 FrozenLake 학습"""
    HIDDEN_SIZE = 128
    BATCH_SIZE = 100  # 더 많은 에피소드 필요
    PERCENTILE = 30   # 더 낮은 퍼센타일 (성공 에피소드가 적으므로)
    LEARNING_RATE = 0.001

    env = DiscreteOneHotWrapper(gym.make("FrozenLake-v1"))
    obs_size = env.observation_space.shape[0]
    n_actions = env.action_space.n

    net = PolicyNetwork(obs_size, HIDDEN_SIZE, n_actions)
    optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE)
    loss_fn = nn.CrossEntropyLoss()

    for iteration in range(200):
        batch = generate_batch(env, net, BATCH_SIZE)
        elite_obs, elite_actions, reward_threshold, mean_reward = \
            filter_elite_episodes(batch, PERCENTILE)

        # 엘리트 에피소드가 없는 경우 건너뛰기
        if len(elite_obs) == 0:
            continue

        optimizer.zero_grad()
        action_logits = net(elite_obs)
        loss = loss_fn(action_logits, elite_actions)
        loss.backward()
        optimizer.step()

        if iteration % 10 == 0:
            print(
                f"반복 {iteration:3d}: "
                f"평균 보상={mean_reward:.3f}, "
                f"임계값={reward_threshold:.3f}, "
                f"손실={loss.item():.4f}"
            )

        if mean_reward > 0.8:
            print(f"목표 달성! 평균 보상: {mean_reward:.3f}")
            break

    env.close()
    return net

# trained_net_fl = train_cross_entropy_frozenlake()

Cross-Entropy方法の理論的背景

なぜ「Cross-Entropy」なのか

この方法の名前は損失関数に由来します。エリートエピソードの行動を教師あり学習の正解ラベルとして扱い、方策ネットワークの出力と比較する際にCross-Entropy損失を使用します。

Cross-Entropy損失の数学的意味は2つの確率分布間の距離を測定することです:

H(p, q) = -sum( p(x) * log(q(x)) )

ここでpはエリートエピソードの行動分布(正解)、qは方策ネットワークが出力する行動確率です。

方法の長所と短所

長所

  • 実装が非常に簡単
  • ハイパーパラメータが少ない
  • CartPoleのような簡単な問題で速く収束
  • 安定した学習が可能

短所

  • エピソードが終わらないと学習できない(オンライン学習不可)
  • 報酬が希薄な環境では効果的でない
  • 複雑な環境ではスケーラビリティが不足
  • 連続行動空間には直接適用が困難

ハイパーパラメータの影響

def experiment_hyperparameters():
    """다양한 하이퍼파라미터 조합 실험"""
    configs = [
        {"batch_size": 8, "percentile": 70, "hidden": 64},
        {"batch_size": 16, "percentile": 70, "hidden": 128},
        {"batch_size": 32, "percentile": 80, "hidden": 128},
        {"batch_size": 16, "percentile": 50, "hidden": 256},
    ]

    for i, config in enumerate(configs):
        print(f"\n=== 설정 {i+1}: batch={config['batch_size']}, "
              f"percentile={config['percentile']}, hidden={config['hidden']} ===")

        env = gym.make("CartPole-v1")
        obs_size = env.observation_space.shape[0]
        n_actions = env.action_space.n

        net = PolicyNetwork(obs_size, config["hidden"], n_actions)
        optimizer = optim.Adam(net.parameters(), lr=0.01)
        loss_fn = nn.CrossEntropyLoss()

        for iteration in range(50):
            batch = generate_batch(env, net, config["batch_size"])
            elite_obs, elite_actions, threshold, mean_reward = \
                filter_elite_episodes(batch, config["percentile"])

            if len(elite_obs) == 0:
                continue

            optimizer.zero_grad()
            loss = loss_fn(net(elite_obs), elite_actions)
            loss.backward()
            optimizer.step()

            if mean_reward >= 475:
                print(f"  반복 {iteration}에서 목표 달성!")
                break

        env.close()

# experiment_hyperparameters()

まとめ

  1. 強化学習分類: モデルベース/フリー、価値/方策ベース、オン/オフポリシーで分類
  2. Cross-Entropy方法: 上位エピソードを選別して方策を教師あり学習方式で更新
  3. CartPole: 約40〜50回の反復で最大報酬に到達可能
  4. FrozenLake: 希薄報酬環境ではより多くのエピソードと低いパーセンタイルが必要
  5. 理論: Cross-Entropy損失でエリートエピソードの行動分布を模倣

Cross-Entropy方法は簡単ですが、より複雑な環境では限界があります。次の記事ではベルマン方程式と価値反復法を通じてより強力なアルゴリズムの基礎を築きます。