主题
多智能体强化学习
多智能体强化学习(Multi-Agent RL,MARL)研究多个智能体在同一环境中同时学习与决策的问题,涵盖合作(团队协作完成任务)、竞争(零和博弈)和混合关系。由于其他智能体也在学习、策略不断变化,从单个智能体的视角看,环境是非平稳的,这使得 MARL 比单智能体 RL 更具挑战。此外还有联合动作空间随智能体数量指数增长、多智能体信用分配等问题。常见框架是集中式训练、分散式执行(CTDE),代表算法有 VDN、QMIX、MADDPG 和 MAPPO;竞争场景中的代表性成果包括 AlphaStar(星际争霸 II)和 OpenAI Five(Dota 2)。
基本概念
马尔可夫博弈
单智能体的 MDP 推广到多智能体,就是马尔可夫博弈(Markov game,又称随机博弈,Littman, 1994):
:智能体集合; :智能体 的动作空间,联合动作 ; :状态转移取决于所有智能体的联合动作; :每个智能体可以有自己的奖励函数。
每个智能体的目标是最大化自己的期望回报
合作、竞争与混合
| 类型 | 奖励结构 | 例子 | 解概念 |
|---|---|---|---|
| 完全合作 | 所有智能体共享奖励 | 多机器人协作搬运、星际争霸微操(SMAC) | 最大化团队回报 |
| 完全竞争 | 零和 | 围棋、德州扑克一对一 | 极小极大 / 纳什均衡 |
| 混合 | 一般和 | 自动驾驶车辆交互、经济博弈 | 纳什均衡、相关均衡等 |
在博弈中,“最优”不再是单一概念。纳什均衡是指:任何一个智能体单方面改变策略都无法提高自己的回报。一个博弈可能有多个纳什均衡,其中有的对所有人都更好(帕累托占优),有的更差。
独立学习(IQL)及其问题
最简单的方法是独立 Q 学习(Independent Q-Learning, Tan, 1993):每个智能体把其他智能体当作环境的一部分,各自独立地运行 Q-learning 或 DQN。
问题:
- 非平稳性:其他智能体的策略在变,从智能体
的角度看, 和 都在变化,Q-learning 的收敛保证不再成立。 - 经验回放失效:缓冲区中的旧数据是在其他智能体旧策略下产生的,可能已经过时。
- 协调困难:在有多个均衡的合作博弈中,独立学习者可能收敛到较差的均衡。
尽管如此,独立学习因为简单、可扩展,仍然是重要的基线,而且在一些任务上效果出人意料地好(例如 IPPO,de Witt et al., 2020)。
代码:攀爬博弈中的协调失败
攀爬博弈(Climbing Game,Claus & Boutilier, 1998)是一个经典的两人合作矩阵博弈,两个智能体获得相同的奖励:
| 智能体 2:动作 0 | 动作 1 | 动作 2 | |
|---|---|---|---|
| 智能体 1:动作 0 | 11 | −30 | 0 |
| 动作 1 | −30 | 7 | 6 |
| 动作 2 | 0 | 0 | 5 |
最优联合动作是 (0, 0),奖励 11。但它旁边是 −30 的“悬崖”:如果对方没有配合,选动作 0 的代价极大。下面比较两种学习者(各 100 个随机种子,每次 5000 轮,ε 从 1 线性衰减到 0.05):
- 独立 Q 学习:每个智能体只学习自己 3 个动作的价值
; - 联合动作 Q 学习:一个集中式学习者学习 9 个联合动作的价值
。
py
"""多智能体玩具实验:攀爬博弈(Climbing Game, Claus & Boutilier 1998)。
两个智能体各选一个动作,得到相同的团队奖励。比较“独立 Q 学习”与“联合动作学习”。"""
import numpy as np
# 行 = 智能体 1 的动作,列 = 智能体 2 的动作;最优联合动作是 (0, 0),奖励 11
PAYOFF = np.array([[11, -30, 0],
[-30, 7, 6],
[0, 0, 5]], dtype=float)
def independent_q(episodes=5000, alpha=0.1, seed=0):
rng = np.random.default_rng(seed)
q1, q2 = np.zeros(3), np.zeros(3) # 每个智能体只看自己的动作价值
for t in range(episodes):
eps = max(0.05, 1.0 - t / (0.8 * episodes))
a1 = rng.integers(3) if rng.random() < eps else int(np.argmax(q1))
a2 = rng.integers(3) if rng.random() < eps else int(np.argmax(q2))
r = PAYOFF[a1, a2]
q1[a1] += alpha * (r - q1[a1]) # 单步博弈:目标就是即时奖励
q2[a2] += alpha * (r - q2[a2])
return int(np.argmax(q1)), int(np.argmax(q2))
def joint_q(episodes=5000, alpha=0.1, seed=0):
rng = np.random.default_rng(seed)
q = np.zeros((3, 3)) # 集中式:学习联合动作价值 Q(a1, a2)
for t in range(episodes):
eps = max(0.05, 1.0 - t / (0.8 * episodes))
if rng.random() < eps:
a1, a2 = rng.integers(3), rng.integers(3)
else:
a1, a2 = np.unravel_index(np.argmax(q), q.shape)
q[a1, a2] += alpha * (PAYOFF[a1, a2] - q[a1, a2])
return tuple(int(x) for x in np.unravel_index(np.argmax(q), q.shape))
for name, fn in [("独立 Q 学习", independent_q), ("联合动作 Q 学习", joint_q)]:
outcomes = [fn(seed=s) for s in range(100)]
rewards = [PAYOFF[a] for a in outcomes]
hit = np.mean([o == (0, 0) for o in outcomes])
print(f"{name:10s} | 收敛到最优 (0,0) 的比例 {hit:.2f} | 最终联合动作平均奖励 {np.mean(rewards):5.2f}")1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
实际运行输出:
text
独立 Q 学习 | 收敛到最优 (0,0) 的比例 0.00 | 最终联合动作平均奖励 5.13
联合动作 Q 学习 | 收敛到最优 (0,0) 的比例 1.00 | 最终联合动作平均奖励 11.001
2
2
独立 Q 学习在 100 个种子中一次也没有找到最优联合动作,平均只得到 5.13 的奖励:87 次停在 (2, 2)(奖励 5),13 次停在 (1, 2)(奖励 6),都是“安全但平庸”的结果。原因是:对智能体 1 来说,动作 0 的价值是对智能体 2 所有动作取平均的结果,在对方还在随机探索时,
这个例子说明了 MARL 的核心矛盾:独立学习可扩展但难以协调,集中学习能协调但联合动作空间随智能体数量指数增长。CTDE 框架试图兼顾两者。
集中式训练、分散式执行(CTDE)
CTDE 的思想:
- 训练时:可以使用全局信息(全局状态、所有智能体的动作和观测),例如学习一个集中式的价值函数;
- 执行时:每个智能体只根据自己的局部观测做决策。
这在仿真中训练、在真实世界部署的场景中很自然。CTDE 主要有两条路线:价值分解(VDN、QMIX)和集中式 Critic(MADDPG、MAPPO)。
价值分解:VDN、QMIX
对完全合作任务,希望学习联合动作价值
即各自贪心的结果恰好是联合最优。
VDN(Sunehag et al., 2018)假设联合价值是个体价值之和:
QMIX(Rashid et al., 2018)放宽为单调组合:用一个混合网络把个体 Q 值组合成
单调性保证了 IGM 条件。混合网络的权重由全局状态通过超网络(hypernetwork)生成,并取绝对值保证非负。QMIX 在星际争霸多智能体挑战(SMAC)上取得了很好的效果。但单调性也限制了表达能力——攀爬博弈这样的收益结构就无法被单调分解准确表示,后续的 QTRAN、QPLEX 等工作尝试解决这一问题。
MADDPG 与 MAPPO
MADDPG(Lowe et al., 2017)把 DDPG 扩展到多智能体:每个智能体有自己的 Actor
MAPPO(Yu et al., 2022)把 PPO 用于多智能体:每个智能体的策略使用局部观测,价值函数使用全局状态(集中式 Critic),通常在同类智能体之间共享参数。论文表明,经过合理调参的 MAPPO 在 SMAC、Hanabi 等基准上与专门设计的离策略方法相当甚至更好——又一个“实现细节很重要”的例子。
代码:simple_spread 上的 IPPO 与 MAPPO
我们在 MPE 的 simple_spread 合作任务上实现 IPPO(独立 PPO)和 MAPPO。环境中有 3 个智能体和 3 个地标,智能体需要分散开、各自覆盖一个地标,同时避免相互碰撞;每个智能体的观测是 18 维(自身速度与位置、各地标和其他智能体的相对位置等),动作是 5 个离散动作,每回合 25 步。奖励由“所有地标被覆盖的程度”(全体共享)和“自身碰撞惩罚”(局部)各占一半组成。
MPE 已从 PettingZoo 中独立出来,需要
pip install mpe2,接口与 PettingZoo 的 Parallel API 相同。
两种方法使用完全相同的设置:3 个智能体共享一个策略网络(输入拼接智能体编号的 one-hot),每轮迭代采集 32 个回合(2400 个智能体步)、做 5 个 epoch × 4 个小批量的 PPO 更新,共 1000 轮迭代(80 万个环境步)。唯一区别是价值网络的输入:
- IPPO:
,只看自己的局部观测; - MAPPO:
,训练时看到所有智能体的观测(集中式评论家);执行时只用策略网络,仍然是分散式的。
py
"""多智能体 PPO:IPPO 与 MAPPO 在 MPE simple_spread(3 个智能体合作覆盖 3 个地标)上的对比。
两者都让 3 个智能体共享同一个策略网络(输入加上智能体编号的 one-hot),区别只在评论家(价值网络):
IPPO:V(o_i),只看自己的局部观测;MAPPO:V(o_1, o_2, o_3, i),训练时看到所有智能体的观测(CTDE)。
依赖:pip install mpe2(MPE 已从 PettingZoo 中独立出来)。
用法:python ippo_mappo_spread.py [ippo|mappo] [种子];DQN_QUICK=1 时只跑很短的版本用于检查代码。"""
import os
import sys
import time
import numpy as np
import torch
import torch.nn as nn
from mpe2 import simple_spread_v3
from torch.distributions import Categorical
QUICK = os.environ.get("DQN_QUICK") == "1"
torch.set_num_threads(1)
N, T = 3, 25 # 智能体数量、每回合步数
EPISODES_PER_ITER, ITERS = 32, (10 if QUICK else 1000)
GAMMA, LAM, LR, CLIP, EPOCHS, N_MB, ENT = 0.99, 0.95, 7e-4, 0.2, 5, 4, 0.01
def make_env():
return simple_spread_v3.parallel_env(N=N, max_cycles=T, continuous_actions=False)
def mlp(i, o, std=1.0):
net = nn.Sequential(nn.Linear(i, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, o))
for m in net:
if isinstance(m, nn.Linear):
nn.init.orthogonal_(m.weight, np.sqrt(2)); nn.init.zeros_(m.bias)
nn.init.orthogonal_(net[-1].weight, std)
return net
def run(method, seed):
torch.manual_seed(seed); np.random.seed(seed)
env = make_env(); agents = env.possible_agents
obs_dim, n_act = env.observation_space(agents[0]).shape[0], env.action_space(agents[0]).n
eye = torch.eye(N)
actor = mlp(obs_dim + N, n_act, 0.01)
critic_in = (obs_dim * N if method == "mappo" else obs_dim) + N
critic = mlp(critic_in, 1)
opt = torch.optim.Adam(list(actor.parameters()) + list(critic.parameters()), lr=LR, eps=1e-5)
def critic_input(o): # o: [..., N, obs_dim]
if method == "mappo":
g = o.reshape(*o.shape[:-2], 1, N * obs_dim).expand(*o.shape[:-2], N, N * obs_dim)
return torch.cat([g, eye.expand(*o.shape[:-2], N, N)], -1)
return torch.cat([o, eye.expand(*o.shape[:-2], N, N)], -1)
t0, log, ep_count = time.time(), [], 0
for it in range(1, ITERS + 1):
B = EPISODES_PER_ITER
O = torch.zeros(B, T, N, obs_dim); A = torch.zeros(B, T, N, dtype=torch.long)
LP = torch.zeros(B, T, N); R = torch.zeros(B, T, N); V = torch.zeros(B, T + 1, N)
for b in range(B):
o, _ = env.reset(seed=seed * 1_000_000 + ep_count); ep_count += 1
for t in range(T):
ot = torch.as_tensor(np.stack([o[a] for a in agents]), dtype=torch.float32)
with torch.no_grad():
d = Categorical(logits=actor(torch.cat([ot, eye], -1)))
act = d.sample(); V[b, t] = critic(critic_input(ot)).squeeze(-1)
O[b, t], A[b, t], LP[b, t] = ot, act, d.log_prob(act)
o, r, term, trunc, _ = env.step({a: int(act[i]) for i, a in enumerate(agents)})
R[b, t] = torch.as_tensor([r[a] for a in agents], dtype=torch.float32)
ot = torch.as_tensor(np.stack([o[a] for a in agents]), dtype=torch.float32)
with torch.no_grad(): # 回合是被 max_cycles 截断的,最后一步用 V(s_T) 自举
V[b, T] = critic(critic_input(ot)).squeeze(-1)
adv = torch.zeros(B, T, N); last = torch.zeros(B, N)
for t in reversed(range(T)):
delta = R[:, t] + GAMMA * V[:, t + 1] - V[:, t]
last = delta + GAMMA * LAM * last; adv[:, t] = last
ret = adv + V[:, :T]
flat = lambda x: x.reshape(B * T, *x.shape[2:])
Of, Af, LPf, ADVf, RETf = map(flat, (O, A, LP, adv, ret))
for _ in range(EPOCHS):
perm = torch.randperm(B * T)
for j in perm.chunk(N_MB):
o_, a_, lp_, ad_, rt_ = Of[j], Af[j], LPf[j], ADVf[j], RETf[j]
ad_ = (ad_ - ad_.mean()) / (ad_.std() + 1e-8)
d = Categorical(logits=actor(torch.cat([o_, eye.expand(len(j), N, N)], -1)))
ratio = (d.log_prob(a_) - lp_).exp()
pg = -torch.min(ratio * ad_, ratio.clamp(1 - CLIP, 1 + CLIP) * ad_).mean()
v_loss = ((critic(critic_input(o_)).squeeze(-1) - rt_) ** 2).mean()
loss = pg + 0.5 * v_loss - ENT * d.entropy().mean()
opt.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(list(actor.parameters()) + list(critic.parameters()), 0.5); opt.step()
# 每个智能体一个回合的累计奖励(3 个智能体取平均)
log.append(R.sum(1).mean().item())
if it % (5 if QUICK else 25) == 0:
print(f" {method} 种子{seed} 迭代{it:4d} 环境步数 {it * B * T:7d} 最近25次迭代平均回合回报 {np.mean(log[-25:]):7.2f} ({time.time() - t0:.0f}s)", flush=True)
return log
def random_baseline(episodes=200):
env = make_env(); rng = np.random.default_rng(0); tot = []
for e in range(episodes):
env.reset(seed=e); G = 0.0
for _ in range(T):
_, r, _, _, _ = env.step({a: int(rng.integers(5)) for a in env.agents}); G += np.mean(list(r.values()))
tot.append(G)
return np.mean(tot)
if __name__ == "__main__":
methods, seeds = ("ippo", "mappo"), ([0] if QUICK else [0, 1])
if len(sys.argv) == 3:
methods, seeds = (sys.argv[1],), [int(sys.argv[2])]
else:
print(f"随机策略的平均回合回报(200 回合):{random_baseline():.2f}")
for m in methods:
for sd in seeds:
log = run(m, sd)
print(f"{m:5s} 种子{sd}: 最后 25 次迭代平均回合回报 {np.mean(log[-25:]):.2f}")1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
实际运行结果(“回合回报”为每个智能体 25 步奖励之和、再对 3 个智能体取平均;表中为最近 25 轮迭代的平均值,越接近 0 越好;随机策略在 200 个回合上的平均为 −27.00):
| 方法 | 种子 | 8 万步 | 20 万步 | 40 万步 | 60 万步 | 80 万步 |
|---|---|---|---|---|---|---|
| IPPO | 0 | −19.96 | −19.83 | −18.07 | −17.17 | −16.44 |
| IPPO | 1 | −20.17 | −20.04 | −19.09 | −17.72 | −16.31 |
| MAPPO | 0 | −20.39 | −19.45 | −18.36 | −17.49 | −16.67 |
| MAPPO | 1 | −19.93 | −19.43 | −18.44 | −17.14 | −16.36 |
结果解读:
- 两种方法都明显优于随机策略(约 −16.4 对 −27.0),并且到 80 万步时曲线仍在缓慢上升,没有完全收敛;更长的训练会得到更好的结果。
- IPPO 与 MAPPO 最终几乎没有差别:MAPPO 在 20 万步时略快(两种子平均 −19.44 对 −19.94),但到 60 万、80 万步时差距已小于同一方法不同种子之间的差距。在这个任务里,每个智能体的局部观测已经包含了所有地标和其他智能体的相对位置,集中式评论家能额外提供的信息有限。这与 de Witt 等(2020)和 Yu 等(2022)的观察一致:参数共享的独立 PPO 往往已经是很强的基线。
- 只用了 2 个种子、一个小任务,不能据此得出“MAPPO 没用”的结论;在局部观测信息更少、需要更多协调的任务(如 SMAC 的部分地图)上,集中式价值函数通常更有帮助。
竞争场景:自博弈
在两人零和博弈中,自博弈(self-play)是最成功的方法:智能体与自己(或自己的历史版本)对弈。AlphaGo Zero / AlphaZero 通过自博弈 + MCTS 在围棋上超越人类;AlphaStar(Vinyals et al., 2019)使用联盟训练(league training)在星际争霸 II 中达到宗师级水平;OpenAI Five 使用大规模自博弈 PPO 在 Dota 2 中击败世界冠军队伍。
常用环境:PettingZoo、SMAC
- PettingZoo:Farama 基金会维护的多智能体环境标准 API(相当于多智能体版的 Gymnasium),包含 MPE(多智能体粒子环境)、Atari 多人游戏、棋类等。
- SMAC / SMACv2:基于星际争霸 II 的合作多智能体微操基准,是价值分解方法的主要测试平台。
- Google Research Football、Hanabi Learning Environment、Melting Pot 等。
实践建议与常见误区
- 从独立学习基线开始:IQL / IPPO 往往比想象中强。
- 参数共享:同质智能体共享网络参数(并把智能体编号作为输入)能大幅提高样本效率。
- 误区:把单智能体算法直接套用:要注意非平稳性对经验回放等组件的影响。
- 误区:只看平均回报:在竞争场景中,应该用对多种对手的胜率、可利用度(exploitability)等指标评估。
小结
- MARL 用马尔可夫博弈建模,面临非平稳性、联合动作空间爆炸、信用分配和均衡选择等问题。
- 在 simple_spread 上,参数共享的 IPPO 与 MAPPO 都把平均回合回报从随机的 −27.0 提高到约 −16.4,两者差异不显著。
- 独立学习简单但难以协调;我们的攀爬博弈实验中,独立 Q 学习 100 次中 0 次找到最优联合动作,联合动作学习 100 次全部找到。
- CTDE 是主流框架:价值分解(VDN、QMIX)和集中式 Critic(MADDPG、MAPPO)。
- 竞争场景中,自博弈是最成功的方法。
练习题
- 在攀爬博弈中,把 −30 改为 −5,独立 Q 学习的结果会如何变化?
- 实现“宽容学习者”(lenient learner):只用较大的奖励更新 Q 值(例如忽略低于当前估计的奖励),观察能否在攀爬博弈中找到最优解。
- 用 VDN 的加法分解拟合攀爬博弈的收益矩阵,能否准确表示?为什么?
- 在
ippo_mappo_spread.py中取消参数共享(每个智能体一个独立的策略网络),比较学习速度。 - 解释为什么 MADDPG 的集中式 Critic 缓解了非平稳性问题。
参考文献
- Littman, M. L. (1994). Markov games as a framework for multi-agent reinforcement learning. ICML.
- Tan, M. (1993). Multi-agent reinforcement learning: Independent vs. cooperative agents. ICML.
- Claus, C., & Boutilier, C. (1998). The dynamics of reinforcement learning in cooperative multiagent systems. AAAI.
- Lowe, R. et al. (2017). Multi-Agent Actor-Critic for Mixed Cooperative-Competitive Environments. NeurIPS. arXiv:1706.02275
- Sunehag, P. et al. (2018). Value-Decomposition Networks For Cooperative Multi-Agent Learning. AAMAS. arXiv:1706.05296
- Rashid, T. et al. (2018). QMIX: Monotonic Value Function Factorisation for Deep Multi-Agent Reinforcement Learning. ICML. arXiv:1803.11485
- Vinyals, O. et al. (2019). Grandmaster level in StarCraft II using multi-agent reinforcement learning. Nature, 575, 350–354.
- de Witt, C. S. et al. (2020). Is Independent Learning All You Need in the StarCraft Multi-Agent Challenge? arXiv:2011.09533
- Yu, C. et al. (2022). The Surprising Effectiveness of PPO in Cooperative, Multi-Agent Games. NeurIPS Datasets and Benchmarks. arXiv:2103.01955
- Albrecht, S. V., Christianos, F., & Schäfer, L. (2024). Multi-Agent Reinforcement Learning: Foundations and Modern Approaches. MIT Press.