多智能体强化学习实战:从 Dec-POMDP 到 QMIX/MADDPG/MAPPO 的可运行 PyTorch 实现

下面按"算法介绍 → 可运行源码 → 关键实现片段 → 选型与框架"组织。先说明:MARL 没有单一算法,核心是多智能体信用分配和非平稳性问题;主流工程范式是 CTDE(集中训练、分散执行)。

  1. 问题建模与核心范式
    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)。

  1. 源码实例一: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 缓慢同步目标网络,保证训练稳定。

  1. 源码实例二: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。

  1. 源码实例三: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 作者维护)。

  1. 选型建议

    场景 推荐

    合作、离散动作、状态可分解 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 内智能体顺序一致性。

  2. 常用开源框架

    框架 内容

    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. 参考资料

开源框架与基准

原始论文

  • 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)。
相关推荐
IT大白鼠2 小时前
彭大帅的AI运维助手——怎么给AI写运维SOP:提示词设计实战
运维·人工智能·彭大帅
不是万能的豆学长4 小时前
filetidy 文件整理助手:一键归类 + 规范重命名(Python 零依赖单文件工具)
大数据·开发语言·python·技术美术
gCode Teacher 格码致知6 小时前
Pandas教学-18:number与numeric的区别
python·pandas
xsd202411187 小时前
3D双目相机 vs 面阵激光雷达:机器人的“眼睛“到底怎么选?
人工智能
梦想不只是梦与想8 小时前
Python 中subprocess 模块:执行外部命令
python·subprocess·调用外部命令
懂点Tech9 小时前
ctfshow MISC入门图片篇(信息附加):misc41解题思路
开发语言·python
2601_965619699 小时前
把开源项目Changelog自动做成更新视频,AI工作流
人工智能·开源·音视频
Json____9 小时前
PHP 突发严重安全漏洞,AI 开始改变网络安全的攻防方式
人工智能·web安全·php·it学习·wwwoop.com
fxss10 小时前
我用AI给娃写了个拼音闯关大冒险
人工智能