04. 몬테카를로 방법 (Monte Carlo Methods)

04. 몬테카를로 방법 (Monte Carlo Methods)

난이도: ⭐⭐ (기초)

학습 목표

  • 몬테카를로(MC) 방법의 기본 개념 이해
  • 모델 프리(Model-Free) 학습의 의미 파악
  • First-visit MC와 Every-visit MC의 차이 이해
  • MC 정책 평가 및 제어 알고리즘 구현
  • 탐험의 중요성과 해결 방법 학습

1. 몬테카를로 방법이란?

1.1 개요

몬테카를로(Monte Carlo, MC) 방법실제 경험(에피소드)으로부터 가치 함수를 추정하는 방법입니다. 환경 모델 없이 학습하는 모델 프리(Model-Free) 방법입니다.

1.2 DP vs MC 비교

특성 동적 프로그래밍 (DP) 몬테카를로 (MC)
환경 모델 필요 (P, R 알아야 함) 불필요
학습 방식 계산 (부트스트래핑) 샘플링 (경험)
업데이트 시점 매 스텝 가능 에피소드 종료 후
적용 환경 에피소딕/연속 에피소딕만

1.3 핵심 아이디어

$$V(s) \approx \text{평균}(\text{상태 } s \text{에서 시작한 에피소드들의 리턴})$$

에피소드 1: S₀ → S₁ → S₂ → ... → 종료, G₁ = 10
에피소드 2: S₀ → S₃ → S₁ → ... → 종료, G₂ = 8
에피소드 3: S₀ → S₂ → S₁ → ... → 종료, G₃ = 12

V(S₀) = (10 + 8 + 12) / 3 = 10

2. 리턴 (Return) 계산

2.1 에피소드에서 리턴 계산

def calculate_returns(episode, gamma=0.99):
    """
    에피소드에서 각 상태의 리턴 계산

    Args:
        episode: [(state, action, reward), ...] 형태의 리스트
        gamma: 할인율

    Returns:
        returns: {state: return} 딕셔너리
    """
    G = 0  # 리턴 초기화
    returns = {}

    # 역순으로 계산 (효율적인 계산)
    for t in range(len(episode) - 1, -1, -1):
        state, action, reward = episode[t]
        G = reward + gamma * G  # 할인된 리턴
        returns[t] = (state, G)

    return returns


# 예시
episode = [
    ('s0', 'right', -1),
    ('s1', 'right', -1),
    ('s2', 'right', 10),  # 목표 도달
]

returns = calculate_returns(episode, gamma=0.9)
for t, (state, G) in returns.items():
    print(f"t={t}: {state}, G={G:.2f}")

# 출력:
# t=2: s2, G=10.00
# t=1: s1, G=8.00  (= -1 + 0.9 * 10)
# t=0: s0, G=6.20  (= -1 + 0.9 * 8)

3. MC 정책 평가

3.1 First-visit MC vs Every-visit MC

방법 설명
First-visit MC 에피소드에서 상태 s를 처음 방문했을 때만 리턴 기록
Every-visit MC 에피소드에서 상태 s를 방문할 때마다 리턴 기록
에피소드: S₀  S₁  S₂  S₁  S₃ (종료)
                           
               방문    번째 방문

First-visit: S₁의  방문만 카운트
Every-visit: S₁의 모든 방문 카운트

3.2 First-visit MC 구현

import numpy as np
from collections import defaultdict

def first_visit_mc_prediction(env, policy, n_episodes=10000, gamma=0.99):
    """
    First-visit MC 정책 평가

    Args:
        env: Gymnasium 환경
        policy: 정책 함수 policy(state) -> action
        n_episodes: 에피소드 수
        gamma: 할인율

    Returns:
        V: 상태 가치 함수
    """
    # 각 상태의 리턴 합과 방문 횟수
    returns_sum = defaultdict(float)
    returns_count = defaultdict(int)
    V = defaultdict(float)

    for episode_num in range(n_episodes):
        # 에피소드 생성
        episode = []
        state, _ = env.reset()
        done = False

        while not done:
            action = policy(state)
            next_state, reward, terminated, truncated, _ = env.step(action)
            episode.append((state, action, reward))
            state = next_state
            done = terminated or truncated

        # First-visit: 각 상태의 첫 방문 인덱스 찾기
        visited = set()
        G = 0

        # 역순으로 리턴 계산
        for t in range(len(episode) - 1, -1, -1):
            state_t, action_t, reward_t = episode[t]

            G = gamma * G + reward_t

            # First-visit 체크
            if state_t not in visited:
                visited.add(state_t)
                returns_sum[state_t] += G
                returns_count[state_t] += 1
                V[state_t] = returns_sum[state_t] / returns_count[state_t]

        if (episode_num + 1) % 1000 == 0:
            print(f"Episode {episode_num + 1}/{n_episodes}")

    return dict(V)


# 사용 예시
import gymnasium as gym

env = gym.make('Blackjack-v1')

def random_policy(state):
    """랜덤 정책"""
    return env.action_space.sample()

V = first_visit_mc_prediction(env, random_policy, n_episodes=50000)
print(f"\n추정된 상태 수: {len(V)}")

3.3 Every-visit MC 구현

def every_visit_mc_prediction(env, policy, n_episodes=10000, gamma=0.99):
    """
    Every-visit MC 정책 평가
    """
    returns_sum = defaultdict(float)
    returns_count = defaultdict(int)
    V = defaultdict(float)

    for episode_num in range(n_episodes):
        episode = []
        state, _ = env.reset()
        done = False

        while not done:
            action = policy(state)
            next_state, reward, terminated, truncated, _ = env.step(action)
            episode.append((state, action, reward))
            state = next_state
            done = terminated or truncated

        G = 0

        # Every-visit: 모든 방문에서 업데이트
        for t in range(len(episode) - 1, -1, -1):
            state_t, action_t, reward_t = episode[t]

            G = gamma * G + reward_t

            # 모든 방문 카운트
            returns_sum[state_t] += G
            returns_count[state_t] += 1
            V[state_t] = returns_sum[state_t] / returns_count[state_t]

    return dict(V)

4. MC 정책 제어

4.1 Exploring Starts (ES)

모든 상태-행동 쌍에서 에피소드가 시작할 수 있다고 가정합니다.

def mc_exploring_starts(env, n_episodes=100000, gamma=0.99):
    """
    MC with Exploring Starts

    모든 (s, a) 쌍이 시작점이 될 수 있음을 가정
    """
    n_states = env.observation_space.n
    n_actions = env.action_space.n

    # Q 함수와 방문 횟수 초기화
    Q = defaultdict(lambda: np.zeros(n_actions))
    returns_sum = defaultdict(lambda: np.zeros(n_actions))
    returns_count = defaultdict(lambda: np.zeros(n_actions))

    # 정책 (탐욕적)
    policy = defaultdict(lambda: 0)

    for episode_num in range(n_episodes):
        # Exploring Starts: 랜덤한 상태와 행동으로 시작
        start_state = env.observation_space.sample()
        start_action = env.action_space.sample()

        # 에피소드 생성
        episode = []
        state = start_state
        action = start_action

        # 첫 스텝
        env.reset()
        env.unwrapped.s = state  # 상태 강제 설정 (환경에 따라 다름)
        next_state, reward, terminated, truncated, _ = env.step(action)
        episode.append((state, action, reward))
        state = next_state
        done = terminated or truncated

        # 나머지 에피소드
        while not done:
            action = policy[state]
            next_state, reward, terminated, truncated, _ = env.step(action)
            episode.append((state, action, reward))
            state = next_state
            done = terminated or truncated

        # 리턴 계산 및 Q 업데이트
        G = 0
        visited = set()

        for t in range(len(episode) - 1, -1, -1):
            state_t, action_t, reward_t = episode[t]
            G = gamma * G + reward_t

            if (state_t, action_t) not in visited:
                visited.add((state_t, action_t))
                returns_sum[state_t][action_t] += G
                returns_count[state_t][action_t] += 1
                Q[state_t][action_t] = (returns_sum[state_t][action_t] /
                                        returns_count[state_t][action_t])

                # 정책 개선 (탐욕적)
                policy[state_t] = np.argmax(Q[state_t])

    return Q, policy

4.2 ε-greedy 정책

Exploring Starts는 현실적이지 않으므로, ε-greedy 정책을 사용합니다.

$$\pi(a|s) = \begin{cases} 1 - \epsilon + \frac{\epsilon}{|A|} & \text{if } a = \arg\max_{a'} Q(s, a') \\ \frac{\epsilon}{|A|} & \text{otherwise} \end{cases}$$

def epsilon_greedy_policy(Q, state, n_actions, epsilon=0.1):
    """
    ε-탐욕 행동 선택

    Args:
        Q: 행동 가치 함수
        state: 현재 상태
        n_actions: 행동 수
        epsilon: 탐험 확률

    Returns:
        action: 선택된 행동
    """
    if np.random.random() < epsilon:
        # 탐험: 랜덤 행동
        return np.random.randint(n_actions)
    else:
        # 활용: 최선의 행동
        return np.argmax(Q[state])

4.3 On-policy MC 제어

def mc_on_policy_control(env, n_episodes=100000, gamma=0.99,
                         epsilon=0.1, epsilon_decay=0.9999):
    """
    On-policy MC 제어 (ε-greedy)

    Args:
        env: Gymnasium 환경
        n_episodes: 에피소드 수
        gamma: 할인율
        epsilon: 탐험율
        epsilon_decay: epsilon 감소율

    Returns:
        Q: 행동 가치 함수
        policy: 학습된 정책
    """
    n_actions = env.action_space.n

    Q = defaultdict(lambda: np.zeros(n_actions))
    returns_sum = defaultdict(lambda: np.zeros(n_actions))
    returns_count = defaultdict(lambda: np.zeros(n_actions))

    episode_rewards = []

    for episode_num in range(n_episodes):
        episode = []
        state, _ = env.reset()
        done = False
        total_reward = 0

        # ε-greedy 정책으로 에피소드 생성
        while not done:
            action = epsilon_greedy_policy(Q, state, n_actions, epsilon)
            next_state, reward, terminated, truncated, _ = env.step(action)

            episode.append((state, action, reward))
            total_reward += reward

            state = next_state
            done = terminated or truncated

        episode_rewards.append(total_reward)

        # Q 업데이트 (First-visit)
        G = 0
        visited = set()

        for t in range(len(episode) - 1, -1, -1):
            state_t, action_t, reward_t = episode[t]
            G = gamma * G + reward_t

            if (state_t, action_t) not in visited:
                visited.add((state_t, action_t))
                returns_sum[state_t][action_t] += G
                returns_count[state_t][action_t] += 1
                Q[state_t][action_t] = (returns_sum[state_t][action_t] /
                                        returns_count[state_t][action_t])

        # epsilon 감소
        epsilon = max(0.01, epsilon * epsilon_decay)

        if (episode_num + 1) % 10000 == 0:
            avg_reward = np.mean(episode_rewards[-1000:])
            print(f"Episode {episode_num + 1}: avg_reward = {avg_reward:.3f}, "
                  f"epsilon = {epsilon:.4f}")

    # 최종 탐욕적 정책
    policy = {}
    for state in Q:
        policy[state] = np.argmax(Q[state])

    return dict(Q), policy, episode_rewards

5. Off-policy MC (Importance Sampling)

5.1 Off-policy 학습이란?

  • On-policy: 행동 정책 = 목표 정책 (같은 정책으로 탐험하고 학습)
  • Off-policy: 행동 정책 ≠ 목표 정책 (다른 정책으로 탐험하고 학습)
행동 정책 (Behavior Policy) b: 데이터 수집용
목표 정책 (Target Policy) π: 학습하고자 하는 최적 정책

5.2 중요도 샘플링 (Importance Sampling)

$$\mathbb{E}_b[X] = \sum_x x \cdot b(x) = \sum_x x \cdot \frac{\pi(x)}{b(x)} \cdot b(x) = \mathbb{E}_b\left[\frac{\pi(X)}{b(X)} X\right]$$

중요도 샘플링 비율: $$\rho_{t:T-1} = \prod_{k=t}^{T-1} \frac{\pi(A_k | S_k)}{b(A_k | S_k)}$$

def importance_sampling_ratio(episode, target_policy, behavior_policy, t):
    """
    중요도 샘플링 비율 계산

    ρ = π(a₀|s₀)/b(a₀|s₀) × π(a₁|s₁)/b(a₁|s₁) × ...
    """
    rho = 1.0

    for k in range(t, len(episode)):
        state, action, _ = episode[k]
        pi_prob = target_policy(state, action)  # π(a|s)
        b_prob = behavior_policy(state, action)  # b(a|s)

        if b_prob == 0:
            return 0  # 행동 정책에서 불가능한 행동

        rho *= pi_prob / b_prob

    return rho

5.3 Off-policy MC 구현

def mc_off_policy_prediction(env, target_policy, behavior_policy,
                              n_episodes=100000, gamma=0.99):
    """
    Off-policy MC 정책 평가 (Weighted Importance Sampling)

    Args:
        target_policy: 목표 정책 (결정적) - π(s) -> a
        behavior_policy: 행동 정책 - b(s) -> a (ε-greedy 등)
    """
    Q = defaultdict(lambda: np.zeros(env.action_space.n))
    C = defaultdict(lambda: np.zeros(env.action_space.n))  # 가중치 합

    for episode_num in range(n_episodes):
        # 행동 정책으로 에피소드 생성
        episode = []
        state, _ = env.reset()
        done = False

        while not done:
            action = behavior_policy(state)
            next_state, reward, terminated, truncated, _ = env.step(action)
            episode.append((state, action, reward))
            state = next_state
            done = terminated or truncated

        G = 0
        W = 1.0  # 중요도 샘플링 가중치

        # 역순 처리
        for t in range(len(episode) - 1, -1, -1):
            state_t, action_t, reward_t = episode[t]
            G = gamma * G + reward_t

            # 가중 중요도 샘플링 업데이트
            C[state_t][action_t] += W
            Q[state_t][action_t] += W / C[state_t][action_t] * (G - Q[state_t][action_t])

            # 목표 정책에서의 행동
            target_action = target_policy(state_t)

            # 행동이 목표 정책과 다르면 중단 (결정적 정책의 경우)
            if action_t != target_action:
                break

            # 중요도 비율 업데이트
            # π(a|s) = 1 (결정적), b(a|s) = 행동 정책 확률
            b_prob = behavior_policy_prob(state_t, action_t)  # 구현 필요
            W = W * 1.0 / b_prob

    return dict(Q)

6. 블랙잭 예제

6.1 블랙잭 환경

import gymnasium as gym
import numpy as np
from collections import defaultdict

# 블랙잭 환경
# 상태: (플레이어 합, 딜러 오픈카드, 사용 가능한 에이스)
# 행동: 0 = stick (패 유지), 1 = hit (카드 추가)

env = gym.make('Blackjack-v1', sab=True)  # sab: Sutton and Barto 버전

print("상태 공간:", env.observation_space)
print("행동 공간:", env.action_space)

6.2 블랙잭에서 MC 학습

def learn_blackjack(n_episodes=500000, gamma=1.0, epsilon=0.1):
    """블랙잭에서 MC 제어"""

    env = gym.make('Blackjack-v1', sab=True)

    Q = defaultdict(lambda: np.zeros(2))
    returns_sum = defaultdict(lambda: np.zeros(2))
    returns_count = defaultdict(lambda: np.zeros(2))

    def get_action(state, Q, epsilon):
        if np.random.random() < epsilon:
            return env.action_space.sample()
        return np.argmax(Q[state])

    wins = 0
    for ep in range(n_episodes):
        episode = []
        state, _ = env.reset()
        done = False

        while not done:
            action = get_action(state, Q, epsilon)
            next_state, reward, terminated, truncated, _ = env.step(action)
            episode.append((state, action, reward))
            state = next_state
            done = terminated or truncated

        if episode[-1][2] == 1:  # 승리
            wins += 1

        # Q 업데이트
        G = 0
        visited = set()

        for t in range(len(episode) - 1, -1, -1):
            state_t, action_t, reward_t = episode[t]
            G = gamma * G + reward_t

            if (state_t, action_t) not in visited:
                visited.add((state_t, action_t))
                returns_sum[state_t][action_t] += G
                returns_count[state_t][action_t] += 1
                Q[state_t][action_t] = (returns_sum[state_t][action_t] /
                                        returns_count[state_t][action_t])

        if (ep + 1) % 50000 == 0:
            win_rate = wins / (ep + 1)
            print(f"Episode {ep + 1}: 승률 = {win_rate:.3f}")

    env.close()
    return Q


def visualize_blackjack_policy(Q):
    """블랙잭 정책 시각화"""
    print("\n=== 사용 가능한 에이스가 없을 때 ===")
    print("딜러 카드:  A  2  3  4  5  6  7  8  9  10")
    print("-" * 50)

    for player_sum in range(21, 10, -1):
        row = f"합계 {player_sum:2d}:  "
        for dealer in range(1, 11):
            state = (player_sum, dealer, False)
            if state in Q:
                action = np.argmax(Q[state])
                row += "H  " if action == 1 else "S  "
            else:
                row += "?  "
        print(row)

    print("\n=== 사용 가능한 에이스가 있을 때 ===")
    print("딜러 카드:  A  2  3  4  5  6  7  8  9  10")
    print("-" * 50)

    for player_sum in range(21, 11, -1):
        row = f"합계 {player_sum:2d}:  "
        for dealer in range(1, 11):
            state = (player_sum, dealer, True)
            if state in Q:
                action = np.argmax(Q[state])
                row += "H  " if action == 1 else "S  "
            else:
                row += "?  "
        print(row)


# 학습 실행
Q = learn_blackjack(n_episodes=500000)
visualize_blackjack_policy(Q)

7. MC 방법의 장단점

7.1 장점

장점 설명
모델 프리 환경 모델 불필요
편향 없음 부트스트래핑 없이 실제 리턴 사용
직관적 평균 리턴 계산으로 간단함
에피소드 독립 에피소드별 병렬 처리 가능

7.2 단점

단점 설명
높은 분산 전체 리턴의 분산이 큼
에피소드 종료 필요 연속 태스크에 부적합
느린 학습 에피소드 끝까지 기다려야 함
탐험 문제 방문하지 않은 상태 학습 불가

7.3 DP vs MC 요약

                    DP                    MC
              ─────────────          ─────────────
필요 정보     환경 모델              경험 (에피소드)

업데이트      V(s) ← Σ P(s'|s,a)    V(s) ← 평균(G)
방식          [R + γV(s')]

편향          편향 있음              편향 없음
              (부트스트랩)           (실제 리턴)

분산          낮음                   높음
              (기대값 계산)          (샘플 분산)

적용 환경     유한 MDP               에피소딕 태스크

8. 요약

핵심 개념

개념 설명
몬테카를로 경험으로부터 학습
First-visit 첫 방문만 카운트
Every-visit 모든 방문 카운트
On-policy 행동 정책 = 목표 정책
Off-policy 행동 정책 ≠ 목표 정책
Importance Sampling 분포 보정을 위한 가중치

MC 정책 평가 공식

$$V(s) = \frac{1}{N(s)} \sum_{i=1}^{N(s)} G_i(s)$$

MC Q-함수 업데이트

$$Q(s, a) \leftarrow Q(s, a) + \frac{1}{N(s,a)} (G - Q(s, a))$$


9. 연습 문제

  1. First-visit vs Every-visit: 동일한 상태를 3번 방문하는 에피소드에서 두 방법의 차이를 계산하세요.

  2. 탐험 문제: ε-greedy에서 ε=0이면 어떤 문제가 발생하나요?

  3. Importance Sampling: 목표 정책이 결정적이고 행동 정책이 ε-greedy일 때, 중요도 비율이 발산할 수 있는 경우는?

  4. 수렴 속도: MC의 분산이 높은 이유와 해결 방법을 설명하세요.


다음 단계

다음 레슨 05_TD_Learning.md에서는 DP의 부트스트래핑과 MC의 샘플링을 결합한 TD 학습을 배웁니다.


참고 자료

  • Sutton & Barto, "Reinforcement Learning: An Introduction", Chapter 5
  • David Silver's RL Course, Lecture 4: Model-Free Prediction
  • Gymnasium Blackjack
to navigate between lessons