강화학습 방법론의 분류
강화학습 알고리즘은 다양한 기준으로 분류할 수 있습니다. 전체 그림을 이해하면 각 알고리즘의 위치를 파악하기 쉽습니다.
모델 기반 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. 전체 에피소드 중 보상이 높은 상위 에피소드들만 선별합니다 (엘리트 에피소드)
3. 엘리트 에피소드에서의 상태-행동 쌍을 이용해 정책을 업데이트합니다
4. 위 과정을 반복합니다
이 방법은 **진화 전략**의 일종으로, 좋은 에피소드를 모방하는 방식으로 정책을 개선합니다.
알고리즘 의사 코드
1. 정책 네트워크 초기화
2. 반복:
a. N개의 에피소드를 현재 정책으로 수집
b. 각 에피소드의 총 보상을 계산
c. 상위 p%의 엘리트 에피소드를 선별 (보상 기준 상위 임계값)
d. 엘리트 에피소드의 (상태, 행동) 쌍으로 정책 네트워크 학습
- 손실 함수: Cross-Entropy Loss (분류 문제로 취급)
e. 평균 보상이 충분히 높으면 종료
CartPole 구현
정책 네트워크 정의
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()
정리
1. **강화학습 분류**: 모델 기반/프리, 가치/정책 기반, 온/오프 폴리시로 분류
2. **Cross-Entropy 방법**: 상위 에피소드를 선별하여 정책을 지도학습 방식으로 업데이트
3. **CartPole**: 약 40~50회 반복으로 최대 보상에 도달 가능
4. **FrozenLake**: 희소 보상 환경에서는 더 많은 에피소드와 낮은 퍼센타일 필요
5. **이론**: Cross-Entropy 손실로 엘리트 에피소드의 행동 분포를 모방
Cross-Entropy 방법은 간단하지만, 더 복잡한 환경에서는 한계가 있습니다. 다음 글에서는 벨만 방정식과 가치 반복법을 통해 더 강력한 알고리즘의 기초를 쌓겠습니다.
현재 단락 (1/252)
강화학습 알고리즘은 다양한 기준으로 분류할 수 있습니다. 전체 그림을 이해하면 각 알고리즘의 위치를 파악하기 쉽습니다.