LabHub

ブログ

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

한국어English日本語

強化学習方法論の分類

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

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

価値ベース vs 方策ベース

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

分類アルゴリズム例
価値ベース、オフポリシー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の特性

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

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は方策ネットワークが出力する行動確率です。

方法の長所と短所

長所

短所


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

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

コメント

まだコメントはありません。

ログインするとコメントできます