커리큘럼 학습과 리워드 엔지니어링 – 게임 AI 성능 끌어올리기

들어가며

지금까지 강화학습으로 게임 AI를 만드는 여정을 함께 해왔습니다. OpenAI Gym 환경 설정부터 DQN, PPO, 멀티 에이전트까지 다뤘지만, 실전에서는 알고리즘만으로는 부족한 경우가 많습니다. 복잡한 게임 환경에서 에이전트가 학습에 실패하거나, 수렴 속도가 너무 느리거나, 지역 최적해에 빠지는 문제를 자주 마주치게 됩니다.

이번 시리즈의 마지막 편에서는 커리큘럼 학습(Curriculum Learning)리워드 엔지니어링(Reward Engineering)을 통해 게임 AI의 성능을 획기적으로 끌어올리는 방법을 알아보겠습니다. 이 두 기법은 알고리즘 자체를 바꾸지 않고도 학습 효율을 극적으로 개선할 수 있는 실용적인 접근법입니다.

커리큘럼 학습이란?

개념과 필요성

커리큘럼 학습은 사람이 쉬운 문제부터 단계적으로 학습하듯이, AI 에이전트에게도 난이도를 점진적으로 높여가며 학습시키는 방법입니다. 처음부터 어려운 게임 환경에 에이전트를 투입하면 의미 있는 리워드를 받기 어려워 학습이 진행되지 않는 문제(sparse reward problem)가 발생합니다.

예를 들어 복잡한 미로 게임에서 출구를 찾아야 하는 경우, 무작위 행동으로는 출구에 도달할 확률이 극히 낮습니다. 이때 커리큘럼 학습을 적용하면:

  1. 단계 1: 간단한 3×3 미로에서 출구 찾기
  2. 단계 2: 5×5 미로로 확장
  3. 단계 3: 장애물이 있는 7×7 미로
  4. 단계 4: 최종 목표인 복잡한 15×15 미로

구현 방법

class CurriculumEnv:
    def __init__(self):
        self.stage = 1
        self.success_count = 0
        self.stage_threshold = 10  # 단계 진행 기준

    def get_difficulty(self):
        """현재 단계에 맞는 난이도 설정 반환"""
        configs = {
            1: {'maze_size': 3, 'obstacles': 0, 'time_limit': 50},
            2: {'maze_size': 5, 'obstacles': 2, 'time_limit': 100},
            3: {'maze_size': 7, 'obstacles': 5, 'time_limit': 150},
            4: {'maze_size': 10, 'obstacles': 10, 'time_limit': 200}
        }
        return configs.get(self.stage, configs[4])

    def update_curriculum(self, episode_reward):
        """성공률에 따라 단계 자동 조정"""
        if episode_reward > 0:  # 성공 기준
            self.success_count += 1

        # 일정 횟수 성공 시 다음 단계로
        if self.success_count >= self.stage_threshold:
            self.stage = min(self.stage + 1, 4)
            self.success_count = 0
            print(f"🎓 커리큘럼 단계 상승: Stage {self.stage}")

# PPO와 통합
curriculum = CurriculumEnv()

for episode in range(1000):
    config = curriculum.get_difficulty()
    env = MazeEnv(**config)  # 동적 난이도 적용

    state = env.reset()
    episode_reward = 0

    while not done:
        action = agent.select_action(state)
        next_state, reward, done, _ = env.step(action)
        episode_reward += reward
        state = next_state

    # 단계 진행 여부 판단
    curriculum.update_curriculum(episode_reward)

자동 난이도 조정 (Automatic Curriculum Learning)

더 발전된 형태로, 에이전트의 학습 진도에 따라 난이도를 자동으로 조정하는 방법도 있습니다:

class AdaptiveCurriculum:
    def __init__(self, window_size=100):
        self.window_size = window_size
        self.recent_rewards = deque(maxlen=window_size)

    def adjust_difficulty(self):
        """최근 성과 기반 난이도 자동 조정"""
        if len(self.recent_rewards) < self.window_size:
            return 1.0  # 초기 난이도

        avg_reward = np.mean(self.recent_rewards)

        # 성과가 좋으면 난이도 증가, 나쁘면 감소
        if avg_reward > 0.8:
            return min(self.difficulty + 0.1, 2.0)
        elif avg_reward < 0.3:
            return max(self.difficulty - 0.1, 0.5)
        return self.difficulty

리워드 엔지니어링

Sparse Reward 문제 해결

게임에서 최종 승리/패배만 리워드로 주면 학습이 거의 불가능합니다. 리워드 쉐이핑(Reward Shaping)을 통해 중간 과정에도 피드백을 제공해야 합니다.

Before (Sparse Reward)

def calculate_reward(self, done, win):
    if done:
        return 1.0 if win else -1.0
    return 0.0  # 게임 중에는 아무 정보 없음

After (Dense Reward)

def calculate_reward(self, state, action, next_state):
    reward = 0.0

    # 1. 목표 접근 보상 (거리 기반)
    prev_distance = np.linalg.norm(state.position - state.goal)
    curr_distance = np.linalg.norm(next_state.position - next_state.goal)
    reward += (prev_distance - curr_distance) * 0.1

    # 2. 체크포인트 보상
    if self.reached_checkpoint(next_state):
        reward += 0.5

    # 3. 생존 페널티 (시간 끌기 방지)
    reward -= 0.01

    # 4. 최종 목표 도달
    if self.reached_goal(next_state):
        reward += 10.0

    # 5. 금지 행동 페널티
    if self.hit_wall(action):
        reward -= 0.2

    return reward

리워드 밸런싱

여러 리워드 요소의 가중치를 조정하는 것이 핵심입니다. 잘못된 밸런싱은 의도하지 않은 행동을 유도합니다.

리워드 요소 가중치 목적
목표 도달 +10.0 최종 목표 강조
거리 감소 +0.1 방향성 제공
생존 페널티 -0.01 빠른 해결 유도
체크포인트 +0.5 중간 이정표
벽 충돌 -0.2 나쁜 행동 억제

Intrinsic Motivation (내재적 동기)

외부 리워드가 부족할 때, 에이전트 스스로 탐색 동기를 갖도록 하는 방법:

class CuriosityDrivenAgent:
    def __init__(self):
        self.state_visit_count = {}

    def calculate_intrinsic_reward(self, state):
        """방문 빈도 기반 호기심 보상"""
        state_hash = self.hash_state(state)
        visit_count = self.state_visit_count.get(state_hash, 0)

        # 방문 적을수록 높은 보상
        intrinsic_reward = 1.0 / (visit_count + 1)

        self.state_visit_count[state_hash] = visit_count + 1
        return intrinsic_reward

    def total_reward(self, extrinsic_reward, state):
        """외재적 + 내재적 리워드 결합"""
        intrinsic = self.calculate_intrinsic_reward(state)
        return extrinsic_reward + 0.1 * intrinsic  # 내재적 리워드 가중치 조정

실전 최적화 팁

1. 리워드 정규화

class RewardNormalizer:
    def __init__(self, clip_range=10.0):
        self.running_mean = 0.0
        self.running_std = 1.0
        self.clip_range = clip_range

    def normalize(self, reward):
        # 표준화로 학습 안정화
        normalized = (reward - self.running_mean) / (self.running_std + 1e-8)
        return np.clip(normalized, -self.clip_range, self.clip_range)

2. 멀티스테이지 커리큘럼

실전 게임에서는 여러 스킬을 순차적으로 학습:

curriculum_stages = [
    {'task': 'move', 'episodes': 1000},      # 이동 학습
    {'task': 'avoid', 'episodes': 1500},     # 장애물 회피
    {'task': 'collect', 'episodes': 2000},   # 아이템 수집
    {'task': 'combat', 'episodes': 2500},    # 전투
    {'task': 'full_game', 'episodes': 5000}  # 전체 게임
]

3. 리워드 디버깅

# 리워드 분포 모니터링
import matplotlib.pyplot as plt

def plot_reward_distribution(episode_rewards):
    plt.figure(figsize=(12, 4))

    plt.subplot(1, 2, 1)
    plt.plot(episode_rewards)
    plt.title('Episode Rewards Over Time')

    plt.subplot(1, 2, 2)
    plt.hist(episode_rewards, bins=50)
    plt.title('Reward Distribution')

    plt.tight_layout()
    plt.savefig('reward_analysis.png')

실전 사례: 슈퍼 마리오 AI

커리큘럼 학습과 리워드 엔지니어링을 결합한 실제 예시:

class MarioRewardShaper:
    def calculate_reward(self, info, prev_info):
        reward = 0.0

        # 1. 전진 보상 (오른쪽으로 이동)
        reward += (info['x_pos'] - prev_info['x_pos']) * 0.1

        # 2. 시간 페널티
        reward -= 0.01

        # 3. 적 처치
        if info['enemy_killed'] > prev_info['enemy_killed']:
            reward += 1.0

        # 4. 코인 획득
        if info['coins'] > prev_info['coins']:
            reward += 0.5

        # 5. 죽음 패널티
        if info['life'] < prev_info['life']:
            reward -= 5.0

        # 6. 스테이지 클리어
        if info['flag_get']:
            reward += 50.0

        return reward

# 커리큘럼: 월드 1-1부터 순차적으로
curriculum = ['1-1', '1-2', '1-3', '1-4', '2-1', '2-2', ...]

마무리

강화학습으로 게임 AI를 만드는 5편의 시리즈를 마무리하며, 알고리즘만큼이나 중요한 것이 학습 환경 설계임을 강조하고 싶습니다. DQN이나 PPO 같은 강력한 알고리즘도 잘못된 리워드나 너무 어려운 초기 환경 앞에서는 무용지물이 됩니다.

커리큘럼 학습은 에이전트에게 점진적 학습 경로를 제공하고, 리워드 엔지니어링은 명확한 학습 신호를 전달합니다. 이 두 기법은 실전에서 가장 먼저 시도해야 할 최적화 방법입니다.

핵심 포인트: 알고리즘을 바꾸기 전에, 리워드를 먼저 점검하세요. 에이전트가 학습하지 못하는 이유의 80%는 리워드 설계 문제입니다.

이제 여러분은 게임 AI를 만드는 전 과정을 경험했습니다. 환경 설정, 기초 알고리즘, 고급 알고리즘, 멀티 에이전트, 그리고 성능 최적화까지. 이 지식을 바탕으로 여러분만의 멋진 게임 AI를 만들어보시기 바랍니다. 행복한 코딩 되세요! 🎮🤖

강화학습을 이용해서 게임학습하기 시리즈 (5/5편)

이 글이 도움이 되셨나요?

Buy me a coffee

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

TODAY 136 | TOTAL 136