下面按"算法介绍 → 可运行源码 → 关键实现片段 → 选型与框架"组织。先说明:MARL 没有单一算法,核心是多智能体信用分配和非平稳性问题;主流工程范式是 CTDE(集中训练、分散执行)。
-
问题建模与核心范式
Dec-POMDP:每个智能体只能看到局部观测
o
i
o
i,执行动作
a
i
a
i,共享/个体奖励
r
i
r
i,目标是最大化联合回报。
范式 含义 代表算法
IL 独立学习 每个智能体把自己的Q/Actor当单智能体学,其他智能体视为环境 IQL、独立PPO
CTDE 集中训练分散执行 训练时用全局状态/联合动作,执行时只用局部观测 VDN、QMIX、MADDPG、MAPPO、COMA
通信学习 智能体学习交换消息 CommNet、DIAL、TarMAC
自博弈/种群 智能体与历史版本/种群对抗 PSRO、League Training
2. 主流 MARL 算法速览
2.1 值分解类(合作、离散动作)
算法 核心思想 优缺点
IQL 每个智能体独立 Q-learning 简单,但忽略非平稳性,易震荡
VDN
Q
t
o
t
∑
i
Q
i
(
τ
i
,
a
i
)
Q
tot
=∑
i
Q
i
(τ
i
,a
i
) 可加分解,表达受限
QMIX
Q
t
o
t
f
θ
(
Q
1
,
...
,
Q
n
,
s
)
Q
tot
=f
θ
(Q
1
,...,Q
n
,s),约束
∂
Q
t
o
t
/
∂
Q
i
≥
0
∂Q
tot
/∂Q
i
≥0 表达能力强于VDN,单调约束仍有限制
QTRAN 放松单调约束,用变换分解 理论表达更强,实现复杂
QPLEX 用 advantage 分解 + 单调变换 表达能力强,较稳定
WQMIX 对高价值动作加权,缓解QMIX低估 适合动作价值差异大的任务
QMIX 的混合网络用 hypernetwork 由全局状态生成权重,权重取绝对值保证单调性。
2.2 Actor-Critic 类
算法 核心思想 适用
MADDPG 每个智能体一个 actor;critic 输入所有观测+所有动作,集中评估 连续动作、合作/竞争/混合
COMA 用反事实基线做信用分配:(A_i=Q(s,a)- \sum_{a_i'}\pi_i(a_i' o_i)Q(s,(a_i',a_{-i}))) 离散动作合作
MAPPO PPO + 集中 critic(输入全局状态),actor 共享参数 合作任务,稳定、易调
MASAC SAC + 集中 critic,最大熵 连续动作、探索要求高
2.3 其他方向
通信:CommNet(连续消息平均)、DIAL(离散消息+梯度直通)、TarMAC(注意力通信)。
自博弈:PSRO(策略空间响应预言机)、League Training(AlphaStar)。
离线 MARL:IQL/BCQ/CQL 的多智能体扩展,处理固定数据集。
异构智能体:角色条件化、参数不共享、HAPPO(异构智能体 PPO)。
- 源码实例一:QMIX 完整可运行实现(PyTorch)
下面是一个自包含的教学版 QMIX:自定义 5×5 网格合作环境,2 个智能体合作到达同一目标。依赖仅 numpy、torch,可直接复制运行。
python
qmix_demo.py
import random
from collections import deque
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
-----------------------------
1. 简单合作环境
-----------------------------
class SimpleCoopEnv:
"""2个智能体在 grid x grid 网格中合作到达同一目标。"""
def init (self, n_agents=2, grid=5, max_steps=50):
self.n_agents = n_agents
self.grid = grid
self.max_steps = max_steps
self.n_actions = 4 # 上/下/左/右
self.obs_dim = 6 # 自己xy + 目标xy + 另一个智能体相对xy
self.state_dim = 2 * n_agents + 2
self.moves = np.array(\[-1, 0, 1, 0, 0, -1, 0, 1])
self.reset()
def reset(self):
self.agent_pos = [np.random.randint(self.grid, size=2)
for _ in range(self.n_agents)]
while len({tuple(p) for p in self.agent_pos}) < self.n_agents:
self.agent_pos = [np.random.randint(self.grid, size=2)
for _ in range(self.n_agents)]
self.target = np.random.randint(self.grid, size=2)
self.steps = 0
return self._get_obs(), self._get_state()
def _get_obs(self):
obs = []
for i in range(self.n_agents):
other = self.agent_pos[(i + 1) % self.n_agents]
rel = other - self.agent_pos[i]
obs.append(np.concatenate([
self.agent_pos[i] / self.grid,
self.target / self.grid,
rel / self.grid
]).astype(np.float32))
return np.stack(obs) # [n_agents, obs_dim]
def _get_state(self):
return np.concatenate([
np.concatenate(self.agent_pos) / self.grid,
self.target / self.grid
]).astype(np.float32)
def step(self, actions):
self.steps += 1
prev_dist = [np.abs(p - self.target).sum() for p in self.agent_pos]
for i, a in enumerate(actions):
self.agent_pos[i] = np.clip(
self.agent_pos[i] + self.moves[a], 0, self.grid - 1)
new_dist = [np.abs(p - self.target).sum() for p in self.agent_pos]
# 共享奖励:距离减少给正奖励,全部到达给大奖励
reward = 0.1 * (sum(prev_dist) - sum(new_dist))
all_reach = all(np.array_equal(p, self.target) for p in self.agent_pos)
if all_reach:
reward += 10.0
done = all_reach or self.steps >= self.max_steps
return self._get_obs(), self._get_state(), float(reward), done
-----------------------------
2. 网络:Agent Q + QMIX Mixer
-----------------------------
class AgentQNet(nn.Module):
def init (self, obs_dim, n_actions, hidden=64):
super().init ()
self.net = nn.Sequential(
nn.Linear(obs_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, n_actions)
)
def forward(self, obs):
return self.net(obs)
class QMixer(nn.Module):
"""单调混合网络:Q_tot = f(Q_1,...,Q_n, state),权重取abs保证单调。"""
def init (self, n_agents, state_dim, embed_dim=32, hyper_hidden=64):
super().init ()
self.n_agents = n_agents
self.embed_dim = embed_dim
self.hyper_w1 = nn.Sequential(
nn.Linear(state_dim, hyper_hidden), nn.ReLU(),
nn.Linear(hyper_hidden, n_agents * embed_dim)
)
self.hyper_w2 = nn.Sequential(
nn.Linear(state_dim, hyper_hidden), nn.ReLU(),
nn.Linear(hyper_hidden, embed_dim)
)
self.hyper_b1 = nn.Linear(state_dim, embed_dim)
self.hyper_b2 = nn.Sequential(
nn.Linear(state_dim, hyper_hidden), nn.ReLU(),
nn.Linear(hyper_hidden, 1)
)
def forward(self, agent_qs, states):
# agent_qs: [B, n_agents], states: [B, state_dim]
B = agent_qs.size(0)
w1 = torch.abs(self.hyper_w1(states)).view(B, self.n_agents, self.embed_dim)
b1 = self.hyper_b1(states).view(B, 1, self.embed_dim)
hidden = F.elu(torch.bmm(agent_qs.view(B, 1, self.n_agents), w1) + b1)
w2 = torch.abs(self.hyper_w2(states)).view(B, self.embed_dim, 1)
b2 = self.hyper_b2(states).view(B, 1, 1)
q_tot = torch.bmm(hidden, w2) + b2
return q_tot.view(B)
-----------------------------
3. Replay Buffer
-----------------------------
class ReplayBuffer:
def init (self, capacity=50000):
self.buffer = deque(maxlen=capacity)
def push(self, obs, state, actions, reward, next_obs, next_state, done):
self.buffer.append((obs, state, actions, reward,
next_obs, next_state, done))
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size)
obs, state, actions, reward, next_obs, next_state, done = zip(*batch)
return (
torch.tensor(np.stack(obs), dtype=torch.float32),
torch.tensor(np.stack(state), dtype=torch.float32),
torch.tensor(np.stack(actions), dtype=torch.long),
torch.tensor(reward, dtype=torch.float32).unsqueeze(1),
torch.tensor(np.stack(next_obs), dtype=torch.float32),
torch.tensor(np.stack(next_state), dtype=torch.float32),
torch.tensor(done, dtype=torch.float32).unsqueeze(1),
)
def __len__(self):
return len(self.buffer)
-----------------------------
4. 训练
-----------------------------
def train():
env = SimpleCoopEnv()
n_agents, obs_dim, state_dim, n_actions = (
env.n_agents, env.obs_dim, env.state_dim, env.n_actions)
agent_qnet = AgentQNet(obs_dim, n_actions)
mixer = QMixer(n_agents, state_dim)
target_agent_qnet = AgentQNet(obs_dim, n_actions)
target_mixer = QMixer(n_agents, state_dim)
target_agent_qnet.load_state_dict(agent_qnet.state_dict())
target_mixer.load_state_dict(mixer.state_dict())
params = list(agent_qnet.parameters()) + list(mixer.parameters())
optimizer = torch.optim.Adam(params, lr=5e-4)
buffer = ReplayBuffer()
batch_size = 64
gamma = 0.99
tau = 0.01
epsilon = 1.0
for episode in range(1500):
obs, state = env.reset()
done = False
ep_reward = 0.0
while not done:
actions = []
for i in range(n_agents):
if random.random() < epsilon:
a = random.randrange(n_actions)
else:
with torch.no_grad():
q = agent_qnet(torch.tensor(obs[i]).unsqueeze(0))
a = int(q.argmax(dim=1).item())
actions.append(a)
next_obs, next_state, reward, done = env.step(actions)
buffer.push(obs, state, actions, reward,
next_obs, next_state, float(done))
obs, state = next_obs, next_state
ep_reward += reward
if len(buffer) >= batch_size:
b_obs, b_state, b_act, b_r, b_nobs, b_nstate, b_done = \
buffer.sample(batch_size)
B = b_obs.size(0)
# 当前 Q_tot
agent_qs = []
for i in range(n_agents):
q = agent_qnet(b_obs[:, i]) # [B, n_actions]
q_i = q.gather(1, b_act[:, i].unsqueeze(1)) # [B, 1]
agent_qs.append(q_i)
agent_qs = torch.cat(agent_qs, dim=1) # [B, n_agents]
q_tot = mixer(agent_qs, b_state) # [B]
# 目标 Q_tot:每个智能体取 max_a Q_i_target
with torch.no_grad():
target_qs = []
for i in range(n_agents):
q_next = target_agent_qnet(b_nobs[:, i])
target_qs.append(q_next.max(dim=1, keepdim=True)[0])
target_qs = torch.cat(target_qs, dim=1)
target_q_tot = target_mixer(target_qs, b_nstate) # [B]
y = b_r + gamma * (1 - b_done) * target_q_tot.unsqueeze(1)
loss = F.mse_loss(q_tot.unsqueeze(1), y)
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(params, 5.0)
optimizer.step()
# soft update
for tp, p in zip(target_agent_qnet.parameters(),
agent_qnet.parameters()):
tp.data.copy_(tau * p.data + (1 - tau) * tp.data)
for tp, p in zip(target_mixer.parameters(),
mixer.parameters()):
tp.data.copy_(tau * p.data + (1 - tau) * tp.data)
epsilon = max(0.05, epsilon * 0.995)
if episode % 50 == 0:
print(f"Episode {episode:4d} | Return {ep_reward:7.2f} | eps {epsilon:.3f}")
if name == "main ":
train()
代码要点:
每个智能体共享 AgentQNet 参数(也可不共享,改成 list of networks)。
QMixer 中 torch.abs 保证权重非负,ELU 保持单调,从而满足
∂
Q
t
o
t
/
∂
Q
i
≥
0
∂Q
tot
/∂Q
i
≥0。
目标值用每个智能体 max_a Q_i_target 再混合,符合 QMIX 的 off-policy 训练。
教学版未做 episode 截断 bootstrap 的细节处理,实际工程需注意 done 与 truncated 区分。
3.1 QMIX 训练循环流程图
#mermaid-svg-qiH3CaE6oZoaLhuU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qiH3CaE6oZoaLhuU .error-icon{fill:#552222;}#mermaid-svg-qiH3CaE6oZoaLhuU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qiH3CaE6oZoaLhuU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qiH3CaE6oZoaLhuU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qiH3CaE6oZoaLhuU .marker.cross{stroke:#333333;}#mermaid-svg-qiH3CaE6oZoaLhuU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qiH3CaE6oZoaLhuU p{margin:0;}#mermaid-svg-qiH3CaE6oZoaLhuU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU .cluster-label text{fill:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU .cluster-label span{color:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU .cluster-label span p{background-color:transparent;}#mermaid-svg-qiH3CaE6oZoaLhuU .label text,#mermaid-svg-qiH3CaE6oZoaLhuU span{fill:#333;color:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU .node rect,#mermaid-svg-qiH3CaE6oZoaLhuU .node circle,#mermaid-svg-qiH3CaE6oZoaLhuU .node ellipse,#mermaid-svg-qiH3CaE6oZoaLhuU .node polygon,#mermaid-svg-qiH3CaE6oZoaLhuU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qiH3CaE6oZoaLhuU .rough-node .label text,#mermaid-svg-qiH3CaE6oZoaLhuU .node .label text,#mermaid-svg-qiH3CaE6oZoaLhuU .image-shape .label,#mermaid-svg-qiH3CaE6oZoaLhuU .icon-shape .label{text-anchor:middle;}#mermaid-svg-qiH3CaE6oZoaLhuU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qiH3CaE6oZoaLhuU .rough-node .label,#mermaid-svg-qiH3CaE6oZoaLhuU .node .label,#mermaid-svg-qiH3CaE6oZoaLhuU .image-shape .label,#mermaid-svg-qiH3CaE6oZoaLhuU .icon-shape .label{text-align:center;}#mermaid-svg-qiH3CaE6oZoaLhuU .node.clickable{cursor:pointer;}#mermaid-svg-qiH3CaE6oZoaLhuU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qiH3CaE6oZoaLhuU .arrowheadPath{fill:#333333;}#mermaid-svg-qiH3CaE6oZoaLhuU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qiH3CaE6oZoaLhuU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qiH3CaE6oZoaLhuU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qiH3CaE6oZoaLhuU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qiH3CaE6oZoaLhuU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qiH3CaE6oZoaLhuU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qiH3CaE6oZoaLhuU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qiH3CaE6oZoaLhuU .cluster text{fill:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU .cluster span{color:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qiH3CaE6oZoaLhuU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qiH3CaE6oZoaLhuU rect.text{fill:none;stroke-width:0;}#mermaid-svg-qiH3CaE6oZoaLhuU .icon-shape,#mermaid-svg-qiH3CaE6oZoaLhuU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qiH3CaE6oZoaLhuU .icon-shape p,#mermaid-svg-qiH3CaE6oZoaLhuU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qiH3CaE6oZoaLhuU .icon-shape .label rect,#mermaid-svg-qiH3CaE6oZoaLhuU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qiH3CaE6oZoaLhuU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qiH3CaE6oZoaLhuU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qiH3CaE6oZoaLhuU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
环境采样:epsilon-greedy 选动作
env.step 得到 next_obs / reward / done
存入 ReplayBuffer
buffer 长度 >= batch_size
采样 batch:obs / state / act / r / next_obs / next_state / done
各智能体 AgentQNet 前向,gather 得 Q_i
Mixer 混合:Q_tot = f(Q_1..Q_n, state)
目标网络:每个智能体 max_a Q_i_target
target_mixer 混合得目标 Q_tot
y = r + gamma * (1 - done) * Q_tot_target
MSE 损失:L = MSE(Q_tot, y)
反向传播 + 梯度裁剪 + Adam 更新
soft update 目标网络:theta' = tau*theta + (1-tau)*theta'
整个循环分两条支路:左侧是数据流,环境采样后把转移存入 ReplayBuffer,攒够 batch 再采样;右侧是价值流,当前网络算出各智能体的 Q_i 后经 Mixer 混合成 Q_tot,目标网络用 max 动作算出目标 Q_tot 并加上奖励构成 TD 目标 y,两者做 MSE 后反向传播更新参数,最后用 soft update 缓慢同步目标网络,保证训练稳定。
- 源码实例二:MADDPG 核心实现
MADDPG 适合连续动作。核心是每个智能体一个 actor,critic 看所有智能体的观测和动作。下面给核心类与训练步,环境可替换为 PettingZoo MPE。
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
class Actor(nn.Module):
def init (self, obs_dim, act_dim, hidden=64):
super().init ()
self.net = nn.Sequential(
nn.Linear(obs_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, act_dim), nn.Tanh()
)
def forward(self, obs):
return self.net(obs)
class Critic(nn.Module):
def init (self, total_obs_dim, total_act_dim, hidden=128):
super().init ()
self.net = nn.Sequential(
nn.Linear(total_obs_dim + total_act_dim, hidden), nn.ReLU(),
nn.Linear(hidden, hidden), nn.ReLU(),
nn.Linear(hidden, 1)
)
def forward(self, all_obs, all_act):
return self.net(torch.cat(all_obs, all_act, dim=-1))
class MADDPG:
def init (self, n_agents, obs_dims, act_dims, lr=1e-3, gamma=0.95, tau=0.01):
self.n = n_agents
self.gamma, self.tau = gamma, tau
self.actors = Actor(obs_dims\[i, act_dimsi) for i in range(n_agents)]
self.critics = Critic(sum(obs_dims), sum(act_dims)) for _ in range(n_agents)
self.target_actors = Actor(obs_dims\[i, act_dimsi) for i in range(n_agents)]
self.target_critics = Critic(sum(obs_dims), sum(act_dims)) for _ in range(n_agents)
for i in range(n_agents):
self.target_actorsi.load_state_dict(self.actorsi.state_dict())
self.target_criticsi.load_state_dict(self.criticsi.state_dict())
self.actor_opts = torch.optim.Adam(a.parameters(), lr=lr) for a in self.actors
self.critic_opts = torch.optim.Adam(c.parameters(), lr=lr) for c in self.critics
def act(self, obs_list, noise_scale=0.1):
acts = []
for i in range(self.n):
with torch.no_grad():
a = self.actors[i](torch.tensor(obs_list[i], dtype=torch.float32).unsqueeze(0))
a = a.squeeze(0).numpy()
a = np.clip(a + np.random.normal(0, noise_scale, size=a.shape), -1, 1)
acts.append(a)
return acts
def update(self, batch):
# batch: all_obs [B,n,obs_dim], all_act [B,n,act_dim],
# all_rew [B,n], all_next_obs [B,n,obs_dim], all_done [B,1]
all_obs, all_act, all_rew, all_next_obs, all_done = batch
B = all_obs.size(0)
flat_obs = all_obs.view(B, -1)
flat_act = all_act.view(B, -1)
flat_next_obs = all_next_obs.view(B, -1)
for i in range(self.n):
# ---- Critic 更新 ----
with torch.no_grad():
next_acts = torch.cat(
[self.target_actors[j](all_next_obs[:, j]) for j in range(self.n)],
dim=-1
)
target_q = self.target_critics[i](flat_next_obs, next_acts)
y = all_rew[:, i:i+1] + self.gamma * (1 - all_done) * target_q
q = self.critics[i](flat_obs, flat_act)
critic_loss = F.mse_loss(q, y)
self.critic_opts[i].zero_grad()
critic_loss.backward()
self.critic_opts[i].step()
# ---- Actor 更新 ----
# 当前 agent i 的动作由 actor 生成,其他 agent 用 replay 中的动作
cur_acts = [all_act[:, j] for j in range(self.n)]
cur_acts[i] = self.actors[i](all_obs[:, i])
actor_loss = -self.critics[i](flat_obs, torch.cat(cur_acts, dim=-1)).mean()
self.actor_opts[i].zero_grad()
actor_loss.backward()
self.actor_opts[i].step()
# ---- soft update ----
for tp, p in zip(self.target_actors[i].parameters(), self.actors[i].parameters()):
tp.data.copy_(self.tau * p.data + (1 - self.tau) * tp.data)
for tp, p in zip(self.target_critics[i].parameters(), self.critics[i].parameters()):
tp.data.copy_(self.tau * p.data + (1 - self.tau) * tp.data)
要点:
Critic 输入是所有智能体的观测拼接和动作拼接,这是"集中"部分;执行时 actor 只用局部观测。
Actor 更新时只替换第
i
i 个智能体的动作为当前策略输出,其他智能体动作来自 replay,避免同时更新所有 actor 带来的高方差。
连续动作用 Tanh,探索加高斯噪声;离散动作可改用 Gumbel-Softmax 或 COMA。
- 源码实例三:MAPPO 关键损失
MAPPO = PPO + 集中 critic,合作任务常用,actor 可共享参数。核心是 PPO clipped loss + GAE。
python
def ppo_update(actor, critic, optimizer, batch, clip_eps=0.2, entropy_coef=0.01):
"""
batch 包含:
obs: B, obs_dim 局部观测
state: B, state_dim 全局状态(集中 critic 输入)
actions: B 离散动作
old_logp: B
returns: B 回报
advantages: B 优势(GAE 计算)
"""
obs, state, actions, old_logp, returns, advantages = batch
logits = actor(obs)
dist = torch.distributions.Categorical(logits=logits)
logp = dist.log_prob(actions)
entropy = dist.entropy().mean()
ratio = torch.exp(logp - old_logp)
surr1 = ratio * advantages
surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages
actor_loss = -torch.min(surr1, surr2).mean() - entropy_coef * entropy
values = critic(state).squeeze(-1)
critic_loss = F.mse_loss(values, returns)
loss = actor_loss + 0.5 * critic_loss
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(
list(actor.parameters()) + list(critic.parameters()), 0.5)
optimizer.step()
MAPPO 实践要点:
Actor 输入局部观测,Critic 输入全局状态;合作任务中 actor 常参数共享。
使用 GAE(
λ
λ) 计算优势,训练时多 epoch 小批量更新。
对离散动作用 Categorical,连续动作用 Gaussian + tanh。
价值归一化、观测归一化、advantage 标准化对稳定性很关键。
推荐直接参考官方实现:marlbenchmark/on-policy(MAPPO 作者维护)。
-
选型建议
场景 推荐
合作、离散动作、状态可分解 QMIX / QPLEX,先跑 VDN 做 baseline
合作、连续动作 MAPPO / MASAC / MADDPG
竞争/混合动机 MADDPG、MAPPO(个体奖励)、自博弈
需要信用分配、离散动作 COMA
大规模智能体、通信受限 参数共享 + MAPPO,或通信学习
异构智能体 HAPPO / 角色条件化 MAPPO
只有离线数据 离线 MARL(IQL/BCQ/CQL 的多智能体扩展)
调试顺序:先用 IQL/独立 PPO 跑通环境和奖励;再用 VDN 验证值分解;最后上 QMIX/MAPPO。多智能体训练不稳定时,优先检查:奖励尺度、done/truncated、探索衰减、目标网络更新、batch 内智能体顺序一致性。
-
常用开源框架
框架 内容
PyMARL VDN/QMIX/QTRAN/COMA 经典实现,SMAC 环境
PyMARL2 QMIX 系列改进与更多值分解算法
MAPPO 官方 marlbenchmark/on-policy,MPE/SMAC/Hanabi
MARLlib 统一接口,集成大量 MARL 算法与环境
PettingZoo 多智能体环境标准 API,MPE/SISL 等
SMAC / SMACv2 星际争霸多智能体微操基准
Melting Pot DeepMind 多智能体泛化基准
建议学习路径:PettingZoo MPE 环境 + MAPPO 官方代码入门,再读 PyMARL 的 QMIX 理解值分解,最后用 MARLlib 做算法对比。上面的 QMIX 示例可直接跑通训练循环,适合改环境、改网络做实验。
8. 常见坑与调参经验
上面几节把算法和代码都过了一遍,但真正跑起来时,多数问题不在算法本身,而在工程细节。这里补充几条实践中反复踩到的坑。
奖励尺度与稀疏性。 多智能体共享奖励时,奖励量级直接决定 Q 值范围。若奖励在 0.1 量级而 Q 值累积到上百,MSE 损失会被大 Q 主导,梯度不稳。建议把奖励归一化到 -1, 1 附近,或对 Q 值做 PopArt 式归一化。稀疏奖励场景优先考虑 reward shaping(如基于距离的势函数),但要注意 shaping 项不能改变最优策略。
done 与 truncated 的区分。 教学代码里常把两者合并成一个 done,但 Gymnasium 之后已明确拆开。时间上限导致的截断不应参与 bootstrap 截断,否则目标值会被错误置零,智能体学不到「时间到了但没失败」的语义。正确写法是 y = r + gamma * (1 - done) * Q_target,其中 done 只表示真实终止。
智能体顺序一致性。 值分解和集中 critic 都依赖「第 i 个位置对应第 i 个智能体」。如果 replay buffer 采样后拼接顺序错乱,或者环境 reset 时智能体索引变化,训练会静默失效------损失照降,但策略不涨。建议在 buffer 里显式存 agent_id,或在环境层固定索引。
参数共享的取舍。 同构智能体共享 actor/Q 网络能显著加速收敛并提升泛化,但会抹掉个体差异。异构任务(不同观测维度、不同动作空间)必须不共享;同构但角色不同的任务(如进攻/防守)可用「共享主干 + 角色 embedding」的折中方案。
探索衰减与信用分配。 epsilon 衰减太快会导致早期样本覆盖不足,QMIX 尤其容易陷入局部最优;衰减太慢则后期策略不收敛。经验值是前 20% 训练步保持较高探索,之后线性或指数衰减到 0.05。信用分配方面,COMA 的反事实基线在离散动作、智能体数不多(<10)时效果好,但计算量随动作数线性增长,大规模场景优先用 QMIX/QPLEX。
评估与随机种子。 MARL 方差极大,单次运行的曲线几乎没有参考价值。至少跑 3--5 个随机种子,报告均值与置信区间;评估时关闭探索噪声,并用固定评估环境。很多「算法 A 优于 B」的结论,换个种子就反过来了。
下面这段代码把「多随机种子 + 滑动平均 + 95% 置信区间」封装成一个函数,直接喂入各随机种子的每回合回报即可出图:
python
import numpy as np
import matplotlib.pyplot as plt
def plot_learning_curves(seed_returns, window=50):
"""
seed_returns: list[np.ndarray],每个元素是一个随机种子的每回合回报序列
window: 滑动平均窗口大小
绘制均值曲线并填充 95% 置信区间阴影。
"""
# 对齐长度:按最短的种子截断,保证可以按列堆叠
min_len = min(len(r) for r in seed_returns)
data = np.stack([np.asarray(r[:min_len], dtype=np.float64)
for r in seed_returns]) # [n_seeds, T]
# 滑动平均(用累积和实现,避免逐点循环)
kernel = np.ones(window) / window
smooth = np.stack([np.convolve(row, kernel, mode="valid")
for row in data]) # [n_seeds, T-window+1]
x = np.arange(window - 1, min_len)
mean = smooth.mean(axis=0)
std = smooth.std(axis=0, ddof=1)
n = smooth.shape[0]
# 95% 置信区间:t 分布临界值近似用 1.96(种子数 >= 5 时足够)
ci = 1.96 * std / np.sqrt(n)
plt.figure(figsize=(8, 4.5))
plt.plot(x, mean, color="tab:blue", label=f"mean (n={n} seeds)")
plt.fill_between(x, mean - ci, mean + ci,
color="tab:blue", alpha=0.25, label="95% CI")
plt.xlabel("Episode")
plt.ylabel(f"Return (moving avg, window={window})")
plt.title("Multi-seed Learning Curve")
plt.legend(loc="lower right")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
return mean, ci
接入 QMIX 训练循环只需三步:在 train() 里把每回合的 ep_reward 追加到列表;外层用不同随机种子重复调用 train();最后把各次结果交给绘图函数。
python
# 1) train() 内:每回合结束后记录
# ep_returns.append(ep_reward)
# 2) 外层:跑 5 个随机种子
# seed_returns = [train(seed=s) for s in range(5)]
# 3) 绘图:plot_learning_curves(seed_returns, window=50)
注意 train() 需要接收 seed 参数并在开头调用 random.seed(seed)、np.random.seed(seed)、torch.manual_seed(seed),否则多个种子跑出来的其实是同一条曲线。另外置信区间用的是滑动平均后的序列,window 越大曲线越平滑、阴影越窄,但会牺牲早期阶段的细节,建议同时保留原始曲线的浅色散点作为对照。
9. 总结
本文从 Dec-POMDP 与 CTDE 范式出发,先厘清 MARL 的核心难点是多智能体信用分配与非平稳性,再按值分解与 Actor-Critic 两条主线梳理了 IQL、VDN、QMIX、MADDPG、MAPPO 等代表算法。随后用三个自包含的 PyTorch 实例把 QMIX 的单调混合、MADDPG 的集中 critic、MAPPO 的 clipped loss 落到可运行代码,并配流程图说明训练循环的数据流与价值流。最后补充了奖励尺度、done/truncated、智能体顺序、参数共享、探索衰减与多随机种子评估等工程细节。推荐路径是:先用 IQL 跑通环境与奖励,再用 VDN 验证值分解思路,最后按动作空间上量到 QMIX(离散合作)或 MAPPO/MADDPG(连续/混合)。
10. 参考资料
开源框架与基准
- PyMARL:https://github.com/oxwhirl/pymarl
- PyMARL2:https://github.com/hijkzzz/pymarl2
- MAPPO 官方实现(marlbenchmark/on-policy):https://github.com/marlbenchmark/on-policy
- MARLlib:https://github.com/Replicable-MARL/MARLlib
- PettingZoo:https://pettingzoo.farama.org/
- SMAC / SMACv2:https://github.com/oxwhirl/smac 、https://github.com/oxwhirl/smacv2
- Melting Pot:https://github.com/google-deepmind/meltingpot
原始论文
- QMIX: QMIX: Monotonic Value Function Factorisation for Deep Multi-Agent Reinforcement Learning,Rashid et al.,ICML 2018。
- MADDPG: Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments,Lowe et al.,NeurIPS 2017。
- MAPPO: The Surprising Effectiveness of PPO in Cooperative Multi-Agent Games,Yu et al.,NeurIPS 2022(arXiv:2103.01955)。