- Authors

- Name
- Youngju Kim
- @fjvbn20031
강화학습 방법론의 분류
강화학습 알고리즘은 다양한 기준으로 분류할 수 있습니다. 전체 그림을 이해하면 각 알고리즘의 위치를 파악하기 쉽습니다.
모델 기반 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. 정책 네트워크 초기화
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의 관찰은 정수 하나(현재 위치)이므로, 신경망에 입력하기 위해 원핫 인코딩을 사용합니다.
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 손실의 수학적 의미는 두 확률 분포 사이의 거리를 측정하는 것입니다.
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()
정리
- 강화학습 분류: 모델 기반/프리, 가치/정책 기반, 온/오프 폴리시로 분류
- Cross-Entropy 방법: 상위 에피소드를 선별하여 정책을 지도학습 방식으로 업데이트
- CartPole: 약 40~50회 반복으로 최대 보상에 도달 가능
- FrozenLake: 희소 보상 환경에서는 더 많은 에피소드와 낮은 퍼센타일 필요
- 이론: Cross-Entropy 손실로 엘리트 에피소드의 행동 분포를 모방
Cross-Entropy 방법은 간단하지만, 더 복잡한 환경에서는 한계가 있습니다. 다음 글에서는 벨만 방정식과 가치 반복법을 통해 더 강력한 알고리즘의 기초를 쌓겠습니다.