00 Multi-Agent Sistemler
Gerçek dünya problemlerinin büyük çoğunluğu tek ajan değil, birden fazla karar vericinin etkileşimi içerir.
Tekil ajan RL, ortamın sadece bir karar vericiye sahip olduğunu varsayar. Oysa trafik sinyalizasyonu, çok oyunculu oyunlar, otonom araç filoleri, piyasa simülasyonları ve robot ekipleri gibi uygulamalarda birden fazla ajan eş zamanlı olarak gözlem yapar, eylem seçer ve çevreyi dönüştürür. Bu durum tekil RL'nin temel varsayımını kırar: diğer ajanlar da öğreniyor olduğu için ortam artık stasyoner değildir.
Ajan A'nın policy'si değiştikçe ajan B'nin gözlem-eylem-ödül dağılımı da değişir. Her ajanın bakış açısından ortam Markov değildir; standart tekil RL algoritmalarının yakınsama garantileri geçerliliğini yitirir.
01 MARL Formal Tanım
Stokastik oyun çerçevesi, multi-agent RL'nin matematiksel temelidir.
Multi-agent ortamı Stokastik Oyun (Markov Oyunu) olarak modellenir: G = (N, S, A, P, R, γ)
Nash Equilibrium: Hiçbir ajanın tek taraflı policy değişikliğiyle kazancını artıramayacağı durum. MARL'ın teorik hedefi Nash Equilibrium'a yakınsamaktır; pratikte hesaplama bakımından son derece maliyetlidir.
Partial Observability: Gerçek uygulamalarda her ajan yalnızca kısmi gözlem oᵢ alır. Oyun POSG (Partially Observable Stochastic Game) çerçevesine taşınır; politikalar yerel gözlem geçmişine dayalı olmak zorundadır.
import numpy as np
class SimpleCoopEnv:
"""2 ajan, ortak grid hedefi. Ödül: +10 hedefe, -0.1 her adım."""
def __init__(self, size=5):
self.size = size
self.moves = {0:(0,1), 1:(0,-1), 2:(1,0), 3:(-1,0)}
def reset(self):
self.pos = [np.array([0,0]), np.array([0,1])]
self.goal = np.array([self.size-1, self.size-1])
return [np.concatenate([p, self.goal]) for p in self.pos]
def step(self, actions):
for i, a in enumerate(actions):
self.pos[i] = np.clip(
self.pos[i] + np.array(self.moves[a]), 0, self.size-1)
done = all(np.array_equal(p, self.goal) for p in self.pos)
r = 10.0 if done else -0.1
return [np.concatenate([p, self.goal]) for p in self.pos], [r,r], done
02 Centralized Training, Decentralized Execution
CTDE paradigması, eğitimde global bilgiyi kullanır; çalışma zamanında her ajan yalnızca kendi gözlemleriyle karar verir.
MARL'ın temel paradoksu şudur: eğitimde ne kadar çok bilgi paylaşılırsa koordinasyon o kadar iyi öğrenilir; ancak çalışma zamanında her ajan yalnızca yerel gözlemlerine erişebilir. CTDE bu gerilimi çözer.
Eğitim zamanı: Critic, global state s ve tüm ajanların eylemlerini a = (a¹,…,aᴺ) girdi olarak alır. Gradyanlar çok daha doğru hesaplanır.
Çalışma zamanı: Her ajanın policy'si yalnızca kendi yerel gözlemine oᵢ erişir. Merkezi bir koordinatör yoktur.
Eğitim Centralized Critic: Q(s, a¹, a², …, aᴺ) — global bilgi ↓ Policy gradyanlarını hesapla, actor ağırlıklarını güncelle Deploy πᵢ(aᵢ | oᵢ) — sadece yerel gözlem, merkezi iletişim YOK
QMIX, VDN, MAPPO, MADDPG, COMA — hepsi CTDE paradigmasını farklı biçimlerde uygular. Ortak nokta: eğitimde merkezi bilgi, çalışmada dağıtık karar verme.
03 QMIX ve VDN — Value Decomposition
Value decomposition, joint Q fonksiyonunu bireysel Q fonksiyonlarının kombinasyonu olarak öğrenerek ölçeklenebilirlik sağlar.
VDN: Q_joint(s,a) = Σᵢ Qᵢ(oᵢ,aᵢ). Toplamsal ayrıştırma; ajanlar arası karmaşık etkileşimleri modelleyemez.
QMIX: Joint Q değeri bireysel Q değerlerinin monoton kombinasyonudur: ∂Q_joint/∂Qᵢ ≥ 0. Bu kısıt "argmax yerel ∈ argmax global" garantisini korur. Hypernetwork ile üretilen ağırlıklar global state'e koşulludur.
import torch, torch.nn as nn
class QMIXMixer(nn.Module):
def __init__(self, n_agents, state_dim, embed=32):
super().__init__()
self.n = n_agents; self.e = embed
self.hw1 = nn.Sequential(
nn.Linear(state_dim, embed), nn.ReLU(),
nn.Linear(embed, n_agents * embed))
self.hw2 = nn.Sequential(
nn.Linear(state_dim, embed), nn.ReLU(),
nn.Linear(embed, embed))
self.hb1 = nn.Linear(state_dim, embed)
self.hb2 = nn.Sequential(
nn.Linear(state_dim, embed), nn.ReLU(), nn.Linear(embed, 1))
def forward(self, qs, s):
# qs: (B, n_agents) s: (B, state_dim)
bs = qs.shape[0]
w1 = torch.abs(self.hw1(s)).view(bs, self.n, self.e)
b1 = self.hb1(s).view(bs, 1, self.e)
h = torch.nn.functional.elu(
torch.bmm(qs.unsqueeze(1), w1) + b1)
w2 = torch.abs(self.hw2(s)).view(bs, self.e, 1)
b2 = self.hb2(s).view(bs, 1, 1)
return (torch.bmm(h, w2) + b2).squeeze()
04 MADDPG — Continuous Action Multi-Agent
MADDPG, DDPG'yi CTDE paradigmasıyla çok ajanlı sürekli eylem uzayına taşır.
Discrete action space'lerde QMIX güçlü çalışır. Ancak robotik manipülasyon, çok-drone navigasyon veya ekonomik simülasyonlar gibi uygulamalar sürekli eylem uzayı gerektirir. Her ajan i için ayrı bir actor πᵢ(aᵢ|oᵢ) ve centralized critic Qᵢ(x, a¹,…,aᴺ) eğitilir. Critic eğitim sırasında diğer ajanların eylemlerini de görür; non-stationarity'yi doğrudan ele alır.
import torch, torch.nn as nn
class Actor(nn.Module):
def __init__(self, obs_dim, act_dim, h=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, h), nn.ReLU(),
nn.Linear(h, h), nn.ReLU(),
nn.Linear(h, act_dim), nn.Tanh())
def forward(self, x): return self.net(x)
class CentralCritic(nn.Module):
def __init__(self, total_obs, total_act, h=128):
super().__init__()
self.net = nn.Sequential(
nn.Linear(total_obs + total_act, h), nn.ReLU(),
nn.Linear(h, h), nn.ReLU(),
nn.Linear(h, 1))
def forward(self, obs, acts):
return self.net(torch.cat([obs, acts], dim=-1)).squeeze(-1)
def actor_loss(i, actors, critic, batch, N):
"""Ajan i için MADDPG policy kaybı."""
obs = [batch[f"o{j}"] for j in range(N)]
acts = [actors[j](obs[j]) if j == i
else actors[j](obs[j]).detach()
for j in range(N)]
return -critic(
torch.cat(obs, -1), torch.cat(acts, -1)).mean()
05 Self-Play — AlphaZero'dan OpenAI Five'a
Self-play, bir ajanı kendi geçmiş versiyonlarıyla rekabet ettirerek kademeli olarak güçlendirir.
Self-play, dış insan verisi veya el yazımı heuristicler gerektirmeden süper insan performansına ulaşabilir.
AlphaZero: Satranç ve Go'da self-play + MCTS kombinasyonuyla sıfırdan Grandmaster seviyesi. Policy ve value fonksiyonunu paylaşan tek bir sinir ağı.
OpenAI Five: Dota 2'de 5v5 tam oyun. Her ajan 20.000 yıllık self-play deneyimi gördü. Reward shaping ve observation featurization kritik.
League Training (AlphaStar): main, exploiter ve league exploiter ajanların oluşturduğu pool ile stratejik cycling'e karşı koruma.
import random, copy
class SelfPlayPool:
"""Geçmiş policy snapshot havuzu. Stratejik cycling'i azaltır."""
def __init__(self, model, max_size=20, save_every=1000):
self.pool = [copy.deepcopy(model).eval()]
self.max_size = max_size
self.save_every = save_every
self.steps = 0
def step(self, model):
self.steps += 1
if self.steps % self.save_every == 0:
snap = copy.deepcopy(model).eval()
if len(self.pool) >= self.max_size: self.pool.pop(0)
self.pool.append(snap)
def opponent(self, current, p_latest=0.5):
if random.random() < p_latest: return current
return random.choice(self.pool)
06 PettingZoo API
PettingZoo, multi-agent ortamlar için Gymnasium'un çok-ajan karşılığıdır.
env.last() ile gözlem, env.step(action) ile eylem gönderilir.
dict olarak tüm eylemler gönderilir; dict olarak tüm ödüller alınır.
from pettingzoo.classic import chess_v6
from pettingzoo.butterfly import cooperative_pong_v5
# AEC API — satranç (sıra tabanlı)
env = chess_v6.env()
env.reset(seed=42)
for agent in env.agent_iter():
obs, r, term, trunc, info = env.last()
if term or trunc:
action = None
else:
action = env.action_space(agent).sample(
mask=obs["action_mask"])
env.step(action)
env.close()
# Parallel API — cooperative pong (eş zamanlı)
env = cooperative_pong_v5.parallel_env()
obs, _ = env.reset(seed=42)
while env.agents:
actions = {a: env.action_space(a).sample() for a in env.agents}
obs, rews, terms, truncs, _ = env.step(actions)
env.close()
supersuit.pettingzoo_env_to_vec_env_v1(env) ile PettingZoo ortamlarını Gymnasium uyumlu vektörleştirilmiş ortama dönüştürür; Stable-Baselines3 ve RLlib entegrasyonunu kolaylaştırır.
07 Communication in MARL
Differentiable iletişim kanalları, öğrenilmiş mesajlarla ajan koordinasyonunu güçlendirir.
CommNet: Her ajan komşularının hidden state ortalamasını kendi gizli katmanına ekler. Basit ve ölçeklenebilir, ancak yalnızca ortalama ile sınırlı.
TarMAC: Attention-tabanlı iletişim. Her ajan key-value çiftiyle mesajını yönlendirir; alıcılar yalnızca kendilerine yönelik mesajları okur.
DIAL (Differentiable Inter-Agent Learning): Eğitimde mesajlar sürekli değerler olarak backprop'tan geçer; çalışma zamanında diskretize edilir.
import torch, torch.nn as nn
class CommNetStep(nn.Module):
"""
Her ajana diğerlerinin hidden ortalamasını mesaj olarak iletir.
hidden_states shape: (n_agents, batch, hidden_dim)
"""
def __init__(self, h):
super().__init__()
self.enc = nn.Linear(h, h)
self.com = nn.Linear(h * 2, h)
def forward(self, hs):
n = hs.shape[0]
msgs = self.enc(hs)
mean = (msgs.sum(0, keepdim=True) - msgs) / (n - 1 + 1e-8)
return torch.tanh(self.com(torch.cat([hs, mean], dim=-1)))
08 Pratik: PettingZoo Chess ile Self-Play Eğitimi
chess_v6 AEC ortamında self-play döngüsü, snapshot havuzu ve winrate takibi.
pip install "pettingzoo[classic]" chess torch
import torch, torch.nn as nn
import numpy as np
import copy, random
from pettingzoo.classic import chess_v6
class ChessNet(nn.Module):
def __init__(self, obs_shape, n_act):
super().__init__()
flat = int(np.prod(obs_shape))
self.net = nn.Sequential(
nn.Flatten(),
nn.Linear(flat, 512), nn.ReLU(),
nn.Linear(512, 256), nn.ReLU(),
nn.Linear(256, n_act))
def act(self, obs_t, mask):
logits = self.net(obs_t)
logits[~torch.BoolTensor(mask)] = -1e9
return logits.argmax().item()
def play_game(env, p0, p1):
env.reset()
pols = {"player_0": p0, "player_1": p1}
totals = {"player_0": 0.0, "player_1": 0.0}
for ag in env.agent_iter():
obs, r, done, trunc, _ = env.last()
totals[ag] += r
if done or trunc:
env.step(None); continue
t = torch.FloatTensor(obs["observation"]).unsqueeze(0)
env.step(pols[ag].act(t, obs["action_mask"]))
return totals
def train(n_ep=300, pool_max=10, save_every=25):
env = chess_v6.env()
env.reset()
obs_sh = env.observation_space("player_0")["observation"].shape
n_act = env.action_space("player_0").n
model = ChessNet(obs_sh, n_act)
pool = [copy.deepcopy(model).eval()]
w = l = d = 0
for ep in range(n_ep):
opp = random.choice(pool)
key = random.choice(["player_0", "player_1"])
p0, p1 = (model, opp) if key == "player_0" else (opp, model)
res = play_game(env, p0, p1)
r = res[key]
if r > 0: w += 1
elif r < 0: l += 1
else: d += 1
if (ep + 1) % save_every == 0:
if len(pool) >= pool_max: pool.pop(0)
pool.append(copy.deepcopy(model).eval())
tot = w + l + d
print(f"[{ep+1:3d}] W:{w} L:{l} D:{d} WR:{w/max(tot,1):.1%}")
env.close()
torch.save(model.state_dict(), "chess_sp.pt")
print("Kaydedildi: chess_sp.pt")
if __name__ == "__main__":
train()
300 episode sonunda model erken dönem rastgele politikaya karşı %55–65 kazanma oranına ulaşmalıdır. Gerçek güçlü ajan için ResNet + MCTS mimarisi ve binlerce episode gerekir; bu örnek self-play döngüsünün iskelet yapısını gösterir.