멀티 에이전트 강화학습이란?
지금까지 우리는 DQN과 PPO를 이용해 단일 에이전트가 게임 환경과 상호작용하며 학습하는 방법을 다뤘습니다. 하지만 실제 게임 환경에서는 여러 플레이어가 동시에 경쟁하거나 협력하는 경우가 많습니다. 이때 필요한 것이 바로 멀티 에이전트 강화학습(MARL, Multi-Agent Reinforcement Learning)입니다.
MARL은 여러 에이전트가 동일한 환경에서 동시에 학습하며, 각 에이전트의 행동이 다른 에이전트의 보상과 전략에 영향을 미칩니다. 1:1 대전 게임, 팀 기반 게임, 또는 경매 시스템 같은 복잡한 시나리오를 모델링할 수 있습니다.
MARL의 핵심 개념
비정상성(Non-Stationarity) 문제
단일 에이전트 강화학습에서는 환경이 고정되어 있지만, MARL에서는 다른 에이전트들이 계속 학습하면서 환경 자체가 변합니다. 에이전트 A가 새로운 전략을 학습하면, 에이전트 B의 입장에서는 환경이 바뀐 것과 같습니다.
학습 패러다임
MARL에는 크게 세 가지 학습 방식이 있습니다:
| 방식 | 설명 | 사용 예시 |
|---|---|---|
| Independent Learning | 각 에이전트가 독립적으로 학습 | 간단한 대전 게임 |
| Centralized Training | 중앙화된 정보로 학습, 분산 실행 | 협력 게임 |
| Communication | 에이전트 간 메시지 교환 | 팀 전략 게임 |
PettingZoo로 MARL 환경 구축하기
PettingZoo는 MARL을 위한 표준 라이브러리로, OpenAI Gym의 멀티 에이전트 버전입니다.
pip install pettingzoo[classic]
pip install stable-baselines3
간단한 대전 게임 환경 만들기
from pettingzoo.classic import tictactoe_v3
import numpy as np
# Tic-Tac-Toe 환경 생성
env = tictactoe_v3.env()
env.reset()
for agent in env.agent_iter():
observation, reward, termination, truncation, info = env.last()
if termination or truncation:
action = None
else:
# 랜덤 행동 선택 (학습 전)
action = env.action_space(agent).sample()
env.step(action)
env.close()
Self-Play로 대전 AI 학습시키기
Self-Play는 에이전트가 자기 자신의 복사본과 대전하며 학습하는 방식입니다. AlphaGo가 사용한 핵심 기법이기도 합니다.
PPO 기반 Self-Play 구현
import torch
import torch.nn as nn
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import DummyVecEnv
from pettingzoo.classic import connect_four_v3
class SelfPlayWrapper:
def __init__(self, env_fn):
self.env = env_fn()
self.agents = {}
def train_agent(self, agent_name, timesteps=100000):
"""특정 에이전트를 Self-Play로 학습"""
# 단일 에이전트 환경으로 변환
single_env = self._make_single_env(agent_name)
model = PPO(
"MlpPolicy",
single_env,
learning_rate=3e-4,
n_steps=2048,
batch_size=64,
n_epochs=10,
verbose=1
)
model.learn(total_timesteps=timesteps)
self.agents[agent_name] = model
return model
def _make_single_env(self, agent_name):
"""PettingZoo 환경을 SB3 호환 환경으로 변환"""
def env_fn():
return SingleAgentEnv(self.env, agent_name, self.agents)
return DummyVecEnv([env_fn])
class SingleAgentEnv:
"""멀티 에이전트 환경을 단일 에이전트 관점으로 변환"""
def __init__(self, pz_env, agent_name, opponent_agents):
self.env = pz_env
self.agent_name = agent_name
self.opponent_agents = opponent_agents
self.observation_space = pz_env.observation_space(agent_name)
self.action_space = pz_env.action_space(agent_name)
def reset(self):
self.env.reset()
# 자신의 차례까지 진행
return self._wait_for_turn()
def step(self, action):
self.env.step(action)
obs, reward, done, info = self._wait_for_turn()
return obs, reward, done, info
def _wait_for_turn(self):
"""상대 에이전트 행동 처리 후 자신의 관찰 반환"""
for agent in self.env.agent_iter():
obs, reward, term, trunc, info = self.env.last()
if agent == self.agent_name:
return obs, reward, term or trunc, info
# 상대는 학습된 모델 또는 랜덤 선택
if agent in self.opponent_agents:
action, _ = self.opponent_agents[agent].predict(obs)
else:
action = self.env.action_space(agent).sample()
self.env.step(action)
Independent Q-Learning 구현
각 에이전트가 독립적으로 Q-Learning을 수행하는 가장 기본적인 MARL 방식입니다.
import torch
import torch.nn as nn
import torch.optim as optim
from collections import defaultdict
import random
class MAQLearning:
def __init__(self, env, lr=0.001, gamma=0.99, epsilon=1.0):
self.env = env
self.agents = env.possible_agents
self.q_networks = {}
self.optimizers = {}
self.epsilon = epsilon
self.gamma = gamma
# 각 에이전트별 Q-Network 생성
for agent in self.agents:
obs_dim = env.observation_space(agent).shape[0]
act_dim = env.action_space(agent).n
self.q_networks[agent] = nn.Sequential(
nn.Linear(obs_dim, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, act_dim)
)
self.optimizers[agent] = optim.Adam(
self.q_networks[agent].parameters(), lr=lr
)
def select_action(self, agent, obs):
"""Epsilon-Greedy 행동 선택"""
if random.random() < self.epsilon:
return self.env.action_space(agent).sample()
with torch.no_grad():
obs_tensor = torch.FloatTensor(obs).unsqueeze(0)
q_values = self.q_networks[agent](obs_tensor)
return q_values.argmax().item()
def train_episode(self):
"""한 에피소드 학습"""
self.env.reset()
episode_rewards = defaultdict(float)
transitions = defaultdict(list)
for agent in self.env.agent_iter():
obs, reward, term, trunc, info = self.env.last()
if term or trunc:
action = None
else:
action = self.select_action(agent, obs)
transitions[agent].append((obs, action, reward))
self.env.step(action)
episode_rewards[agent] += reward
# 각 에이전트 업데이트
for agent in self.agents:
self._update_agent(agent, transitions[agent])
# Epsilon decay
self.epsilon = max(0.01, self.epsilon * 0.995)
return episode_rewards
def _update_agent(self, agent, transitions):
"""Q-Network 업데이트"""
if len(transitions) == 0:
return
for i, (obs, action, reward) in enumerate(transitions):
obs_tensor = torch.FloatTensor(obs).unsqueeze(0)
q_values = self.q_networks[agent](obs_tensor)
q_value = q_values[0, action]
# 다음 상태 Q-value (에피소드 끝이면 0)
if i < len(transitions) - 1:
next_obs = transitions[i + 1][0]
next_obs_tensor = torch.FloatTensor(next_obs).unsqueeze(0)
with torch.no_grad():
next_q = self.q_networks[agent](next_obs_tensor).max()
target = reward + self.gamma * next_q
else:
target = torch.FloatTensor([reward])
loss = nn.MSELoss()(q_value, target)
self.optimizers[agent].backward()
self.optimizers[agent].step()
self.optimizers[agent].zero_grad()
학습 및 평가
# 환경 생성
env = connect_four_v3.env()
ma_learner = MAQLearning(env)
# 학습
for episode in range(5000):
rewards = ma_learner.train_episode()
if episode % 100 == 0:
avg_reward = np.mean(list(rewards.values()))
print(f"Episode {episode}, Avg Reward: {avg_reward:.2f}, Epsilon: {ma_learner.epsilon:.3f}")
# 학습된 에이전트로 대전
env.reset()
for agent in env.agent_iter():
obs, reward, term, trunc, info = env.last()
if term or trunc:
action = None
else:
with torch.no_grad():
obs_tensor = torch.FloatTensor(obs).unsqueeze(0)
action = ma_learner.q_networks[agent](obs_tensor).argmax().item()
env.step(action)
MARL의 도전 과제와 해결책
1. 수렴 불안정성
문제: 모든 에이전트가 동시에 학습하면 환경이 계속 변해 수렴이 어렵습니다.
해결:
– 과거 버전의 에이전트를 상대로 학습 (Self-Play League)
– 학습률을 점진적으로 감소
– Experience Replay Buffer 공유
2. 신용 할당(Credit Assignment)
문제: 팀 게임에서 승리 시 어떤 에이전트의 기여도가 높은지 판단이 어렵습니다.
해결:
– Value Decomposition Networks (VDN)
– QMIX 알고리즘 사용
# QMIX 핵심 아이디어 (간소화)
class QMixer(nn.Module):
def __init__(self, n_agents):
super().__init__()
self.n_agents = n_agents
self.hyper_w1 = nn.Linear(state_dim, n_agents * 32)
self.hyper_w2 = nn.Linear(state_dim, 32)
def forward(self, agent_qs, state):
"""개별 Q-value를 결합하여 팀 Q-value 생성"""
# 하이퍼네트워크로 믹싱 가중치 생성
w1 = torch.abs(self.hyper_w1(state))
w2 = torch.abs(self.hyper_w2(state))
# 단조성 제약으로 개별 기여도 보장
agent_qs = agent_qs.view(-1, 1, self.n_agents)
hidden = torch.relu(torch.matmul(agent_qs, w1.view(-1, self.n_agents, 32)))
q_total = torch.matmul(hidden, w2.view(-1, 32, 1))
return q_total
마무리
이번 편에서는 멀티 에이전트 강화학습의 기초부터 Self-Play, Independent Q-Learning까지 실전 구현 방법을 다뤘습니다. MARL은 비정상성과 신용 할당 같은 고유한 도전 과제가 있지만, PettingZoo와 같은 라이브러리를 활용하면 생각보다 쉽게 시작할 수 있습니다.
다음 편에서는 커리큘럼 학습과 리워드 엔지니어링을 통해 게임 AI의 성능을 극대화하는 방법을 알아보겠습니다. 학습 난이도를 점진적으로 높이고, 보상 함수를 정교하게 설계해 에이전트가 더 빠르고 안정적으로 학습하도록 만들어봅시다!
이 글이 도움이 되셨나요?
Buy me a coffee
답글 남기기