资讯动态

MADDPG多智能体博弈对抗算法:Python实现与训练避坑指南

发布时间:2026/10/18 13:04:19 来源:尧图企业网站定制
简介基于MADDPG的多智能体博弈对抗算法Python实现项目源码聚焦多智能体强化学习中的博弈与对抗场景适合正在准备毕业设计、期末大作业或课程设计的计算机相关专业学生也适合希望从零上手多智能体算法的开发者作为项目实战参考。压缩包共14个文件大小1.6MB以Python脚本为主涵盖MADDPG、DDPG、网络构建、训练主控等核心模块另附环境配置说明、测试脚本及文本资料目录结构清晰便于按模块阅读和二次开发。值得强调的是该项目为个人经导师指导并认可的高分毕业设计评审分98分实现思路与代码组织方式具有参考价值。已有189人学习/下载说明内容实用适合对照练习、快速掌握MADDPG的算法流程与多智能体对抗环境构建方法进而迁移到自己的实验或课题中。1. 把 MADDPG 从论文变成能跑的 Python 项目这份源码到底解决了什么MADDPG 是当前多智能体博弈对抗领域引用率最高的一类算法但论文里只有公式和伪代码真要在 Python 里复现observation 怎么拼、Critic 要不要看到其他 agent 的动作、Replay Buffer 怎么存、Target 网络什么时候软更新每一步都可能卡住。这份基于 MADDPG 的多智能体博弈对抗算法 Python 实现项目源码把完整训练闭环跑通了文件结构里既有 DDPG 单智能体实现也有 MADDPG 层、经验回放池和 ma-gym 环境正好覆盖毕业设计、期末大作业和项目实战练习三类场景。接下来我按“算法映射→环境安装→训练测试→避坑→提分技巧”的顺序把它拆到能照着改的程度。2. 算法与代码映射MADDPG 的四个核心模块2.1 从 DDPG 到 MADDPG为什么单智能体策略在多智能体环境里会失效先从一个共性问题说起直接在每个 agent 上单独套 DDPG最后通常会发散。原因在于多智能体环境天然是非平稳的。你训练的时候对手也在改策略你的 Q 目标target_q r γ * Q(s, a)里隐含的“环境转移不变”这个假设被打破Critic 学到的价值会来回漂移。MADDPG 的解法是 Centralized Training with Decentralized Execution缩写就是 CTDE训练时给每个 agent 配一个能看到全局 observation 和全局 action 的 CriticActor 仍然只用自己的局部 observation 输出动作这就是“集中训练、分散执行”。这份代码里的DDPG.py是基础单智能体实现MADDPG.py则把若干 DDPG 智能体包装成多智能体集合。在MADDPG.py的更新循环里每个 agent 的 Critic 输入不是自己的局部 obsact而是所有 agent 的 obs 拼接后再拼上所有 agent 的 act。这带来的直接收益是Critic 能显式感知“对手下一步会怎么动”相当于把博弈过程的一部分建模进了值函数里。执行阶段把 Critic 拿掉Actor 独立决策这也是算法能实时推理的关键。另外注意一个细节MADDPG 原文假设连续动作空间Actor 输出层用tanh把动作压到 [-1,1]。如果碰到离散动作环境比如 ma-gym 里很多默认环境是离散五动作许多简易复现会强行映射把连续动作做 argmax或者直接用离散概率分布加 Gumbel-Softmax。这份项目里如果要在离散环境跑我一般会把network.py里的 Actor 输出改成nn.Softmax再在DDPG.py里把动作转成 one-hot 给 Critic。这是复现里最容易理解错的一步后面避坑章节会再展开。还有一个容易被忽略的角度MADDPG 能同时处理合作、竞争和混合博弈。以PredatorPrey-v0为例捕食者之间是合作关系捕食者与猎物之间是竞争关系这种混合博弈恰恰是单智能体 DDPG 无法覆盖的。因为单智能体 DDPG 的观测里没有“队友”和“对手”的显式建模策略梯度很容易被其他 agent 的行为干扰。2.2 经验回放池与网络结构buffer.py 和 network.py 里都放了什么buffer.py实现的经验回放池是全局共享的而不是每个 agent 一个。共享的原因很简单多智能体场景下单个 agent 的样本多样性不够合并采样可以让 Critic 更稳定。常见实现如下from collections import deque import random import numpy as np class ReplayBuffer: def __init__(self, capacity1000000): self.buffer deque(maxlencapacity) def push(self, obs_n, act_n, rew_n, next_obs_n, done_n): self.buffer.append((obs_n, act_n, rew_n, next_obs_n, done_n)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) obs_n, act_n, rew_n, next_obs_n, done_n zip(*batch) # 保留 agent 维度shape 为 [batch, n_agents, ...] return (np.array(obs_n), np.array(act_n), np.array(rew_n), np.array(next_obs_n), np.array(done_n)) def __len__(self): return len(self.buffer)这里最关键的形状是obs_n的维度。它不只是[batch, obs_dim]而是[batch, n_agents, obs_dim]。因为 MADDPG 更新时要用到所有 agent 的 obs 来拼全局输入。如果你在push时把多智能体信息展平成一维训练时再还原会很痛苦还容易越拼越乱。我习惯让ReplayBuffer从头到尾保留n_agents这个轴后面所有 PyTorch 张量 reshape 都以它为基准。buffer.py里容量建议不要小于 1e5。MADDPG 的样本利用率其实不算高因为每个 agent 的 transition 都要被多个 Critic 消费容量太小会让旧经验被过快覆盖训练前期策略震荡会特别明显。network.py里通常定义三样东西Actor、Critic 以及它们的 Target 网络。核心结构如下import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden64): 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() # 连续动作输出到 [-1, 1] ) def forward(self, obs): return self.net(obs) class Critic(nn.Module): def __init__(self, total_obs_dim, total_act_dim, hidden128): 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, obs_all, act_all): x torch.cat([obs_all, act_all], dim-1) return self.net(x)注意 Critic 的total_obs_dim是所有 agent 的 obs 维度之和total_act_dim同理。构造MADDPG时要把每个DDPG的 obs_dim 和 act_dim 汇总而不是直接传某个 agent 的单独维度。Actor 的Tanh输出有三个副作用动作范围受限、梯度有限、初期容易输出接近 0 的中间值。如果你的动作空间要求是 [0,1]记得在外层做平移比如(tanh(x) 1) / 2不要改 Actor 的内部激活。Target 网络初始化和 online 网络完全一样后续通过软更新逐步追踪。如果 Target 网络初始化乱搞前几次 update 的 Q 值会偏离实际后面要花很多步数才能拉回来。这里两个网络共享同一套权重初始化即可。2.3 MADDPG.py 里如何调度 DDPG 智能体这部分对应文件MADDPG.py。它不写具体的网络细节而是负责“调度”管理 n 个DDPG.py实例并且每个训练 step 里为每个 agent 构造全局 obs 和全局 act。一个典型的训练更新片段长这样def update(self, replay_buffer, batch_size, agents): batch replay_buffer.sample(batch_size) obs_n, act_n, rew_n, next_obs_n, done_n batch for agent_i in range(self.n_agents): # 组装所有 agent 的 obs / act obs_all torch.cat([obs_n[:, i] for i in range(self.n_agents)], dim-1) act_all torch.cat([act_n[:, i] for i in range(self.n_agents)], dim-1) # 目标值r gamma * Q_target(全局下一obs, 所有target_actor输出) next_obs_all torch.cat( [next_obs_n[:, j] for j in range(self.n_agents)], dim-1 ) next_act_all torch.cat( [self.agents[j].target_actor(next_obs_n[:, j]) for j in range(self.n_agents)], dim-1 ) target_q rew_n[:, i:i1] self.gamma * \ self.agents[i].target_critic(next_obs_all, next_act_all.detach()) current_q self.agents[i].critic(obs_all, act_all) critic_loss nn.MSELoss()(current_q, target_q) self.agents[i].critic_optimizer.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_( self.agents[i].critic.parameters(), max_norm1.0) self.agents[i].critic_optimizer.step()逻辑说明next_act_all必须用 target Actor 的输出而且要做detach()原因是防止梯度从 target Q 反向流进 target Actor破坏软更新。act_all则必须用 ReplayBuffer 里存的实际执行动作不能用当前在线 Actor 重新算否则 Critic 学到的 Q 跟真实交互不一致这就是所谓“Q 估计失真”。Critic 更新完后还要做 Actor 更新。更新某一个 agent 的 Actor 时其他 agent 的动作应该固定住用 buffer 里的真实动作来填# actor 更新只更新 agent_i 的 actor actor_act_all torch.cat([ self.agents[j].actor(obs_n[:, j]) if j agent_i else act_n[:, j].detach() for j in range(self.n_agents) ], dim-1) actor_loss -self.agents[i].critic(obs_all, actor_act_all).mean() self.agents[i].actor_optimizer.zero_grad() actor_loss.backward() torch.nn.utils.clip_grad_norm_( self.agents[i].actor.parameters(), max_norm1.0) self.agents[i].actor_optimizer.step()这段的要点是self.agents[j].actor只有j agent_i时才让梯度传播其他 agent 用act_n[:, j].detach()固定住。否则所有 agent 的 Actor 都同时被 Critic 的梯度牵着走梯度方向会互相打架这也是多智能体训练不稳定的一个来源。软更新则放在每个 batch 更新之后而不是等整个 episode 结束。对每个 agent 都执行for target_param, param in zip( self.agents[i].target_actor.parameters(), self.agents[i].actor.parameters()): target_param.data.copy_( tau * param.data (1.0 - tau) * target_param.data )tau通常取 0.01。0.01 意味着每步只把 1% 的新参数融入 target能避免 target Q 抖动过大。多智能体环境下我一般不建议把 tau 设到 0.001因为 agent 数量多每个 agent 的目标网络更新频率相对稀疏tau 太小会造成 target 滞后Q 值偏离真实价值。3. 把环境跑起来ma-gym 安装与第一个多智能体对抗实验3.1 ma-gym-master.zip 是做什么的多智能体环境的安装与目录结构ma-gym-master.zip是项目里自带的多智能体环境库。它提供PredatorPrey-v0、Switch-v0、TrafficJunction-v0这类经典验证环境也是做多智能体博弈对抗实验最常见的环境集合。对毕业设计来说PredatorPrey-v0最容易把“博弈对抗”讲清楚捕食者要围捕猎物猎物要逃跑捕食者之间需要配合这是一个典型的合作竞争混合博弈场景视觉效果也好所以我下面都以它为例。安装时先激活项目根目录的虚拟环境再离线安装。文件里已经带了ma-gym-master.zip不需要在线下载# Windows venv\Scripts\activate python -m zipfile -e ma-gym-master.zip ./ma-gym-master cd ma-gym-master pip install -e . # Linux / macOS source venv/bin/activate unzip ma-gym-master.zip cd ma-gym-master pip install -e .pip install -e .是开发模式安装之后你改ma-gym-master里的源码不需要重新安装方便调试环境包装器。但 ma-gym 本身是比较早的库依赖的 gym 版本偏旧如果遇到注册方面的报错先固定gym0.21.0再重装。安装完成后看项目根目录的mag.py会更有感觉。它一般是环境封装层把 ma-gym 返回的obs_n、action_n转成 MADDPG update 需要的统一格式。这样main.py里只需要关心env.step(actions)不用管每个环境对 observation 的细节定义。也就是说mag.py是这份源码里连接算法和环境的桥训练主流程基本不会直接碰 gym API。3.2 运行 main.py训练一个 MADDPG 模型的完整命令与参数main.py是整个训练入口。源码通常会在文件顶部放一个配置区直接改文件里的常量然后运行一行命令。这份项目也保留了这种写法。建议先跑一个 200 集的冒烟测试确认环境、模型和 buffer 都能正常交互再拉长训练时间。# main.py 顶部配置区示例 ENV_NAME PredatorPrey-v0 MAX_EPISODES 20000 MAX_STEPS 50 BATCH_SIZE 1024 BUFFER_CAPACITY 1000000 ACTOR_LR 1e-4 CRITIC_LR 1e-3 GAMMA 0.95 TAU 0.01 NOISE_STD 0.5 NOISE_DECAY 0.9999 SEED 42 if __name__ __main__: env build_env(ENV_NAME) maddpg MADDPG(n_agentsenv.n_agents, obs_dimsenv.observation_space, act_dimsenv.action_space, actor_lrACTOR_LR, critic_lrCRITIC_LR, gammaGAMMA, tauTAU) maddpg.train(env, max_episodesMAX_EPISODES, max_stepsMAX_STEPS, batch_sizeBATCH_SIZE, buffer_capacityBUFFER_CAPACITY, noise_stdNOISE_STD, noise_decayNOISE_DECAY)MAX_STEPS50决定一个 episode 最多走多少步。PredatorPrey 里游戏会在捕到猎物或者 50 步后结束。BATCH_SIZE1024在多智能体环境下偏大但稳定如果你显存或内存不够降到 512 也可以但别低于 256否则 Critic 的梯度噪声会非常大。NOISE_STD初始值设 0.5是为了前期有足够探索。随着训练推进每个 episode 结束后执行noise_std * noise_decay到 5000 集会降到 0.05 以下策略开始偏向利用。如果你盯着回合奖励却迟迟不涨大概率是噪声没有衰减这个问题很常见。CRITIC_LR比ACTOR_LR大一个量级是我的习惯。Critic 需要更快逼近真实 Q 值Actor 跟着 Critic 走学快了容易震荡。GAMMA0.95而不是 0.99是因为多智能体博弈环境普遍偏短gamma 过大反而放大了对手策略变化带来的方差。3.3 用 test_env.py 检查观测空间和动作空间正式训练前先用test_env.py打印环境信息能省下大量排错时间。典型脚本如下import gym import ma_gym import numpy as np env gym.make(PredatorPrey-v0) obs_n env.reset() n_agents env.n_agents print(n_agents:, n_agents) for i in range(n_agents): print(fagent[{i}] obs shape: {obs_n[i].shape}) print(action space:, env.action_space[0]) # 随机走一步看看 action 长度和 reward 是否符合预期 actions [env.action_space[i].sample() for i in range(n_agents)] obs_n, reward_n, done_n, info env.step(actions) print(step reward:, reward_n, done:, done_n)这段代码的价值不在算法而在确认环境返回的接口是不是预期格式。如果obs_n是 list of arrays那么在进network.py之前要统一np.array(obs_n)如果action_space是Discrete(5)而你没改 Actor 输出层训练时会直接报维度错误。所以这一步会提前把离散/连续动作的问题暴露出来。另外一个常见做法是把test_env.py的输出重定向到文本文件方便后面翻看python test_env.py env_info.txt这能帮你在训练跑完后复盘时快速确认当时用的是什么环境配置。4. 训练与评估从 main.py 到 test.py 的完整实验闭环4.1 训练日志里看什么reward 怎么算、回合到了该停在哪很多同学拿到训练代码后只盯一个“reward 有没有涨”其实不够。我一般同时看三个指标当前 episode 的总 reward、最近 100 个 episode 的平均 reward、每 1000 步的 critic loss 均值。如果把打印函数改成只输出这几个训练过程会清晰很多Episode 1000, avg_reward(100ep)-2.31, avg_critic_loss0.842 Episode 5000, avg_reward(100ep)0.52, avg_critic_loss0.376 Episode 12000, avg_reward(100ep)2.87, avg_critic_loss0.153avg_critic_loss 下降说明 Critic 对 Q 值的预测逐渐稳定avg_reward 转正则说明 Actor 确实在利用 Critic 的指导。如果 reward 在上升但 critic loss 不降通常是 exploration 噪声还没衰减Q 估计和策略之间存在滞后。这时候不要急着停再跑几千集看看。关于“回合该停在哪”我的经验是当 avg_reward(100ep) 连续 1000 集变化幅度小于 0.1并且没有再上升的趋势就可以停了。不是非跑完 20000 集不可提前收敛反而说明参数选得合理。训练过程中每个 checkpoint 保存时我建议记录它的 episode 数方便后面评估不同训练阶段的策略差异。4.2 用 test.py / ceshi.py 加载权重做推理训练完成后test.py和ceshi.py都是推理脚本。ceshi.py更偏“冒烟测试”适合在改完环境、换完参数后快速验证test.py则承担正式的评估实验。一份典型的加载推理代码如下import torch import numpy as np import gym import ma_gym from maddpg import MADDPG n_agents 3 obs_dims [8, 8, 8] # 每个 agent 的局部观测维度 act_dims [5, 5, 5] # 离散动作用 one-hot维度等于动作数 agent MADDPG(n_agentsn_agents, obs_dimsobs_dims, act_dimsact_dims) agent.load(checkpoints/MADDPG_predator_prey_20000.pth) env gym.make(PredatorPrey-v0) total_reward np.zeros(n_agents) obs_n env.reset() done False while not done: # 推理时关闭噪声 action_n agent.select_action(obs_n, noiseNone) # 离散动作环境需要把连续输出转成整数 if env.action_space[0].__class__.__name__ Discrete: action_n [int(np.argmax(a)) for a in action_n] obs_n, reward_n, done_n, _ env.step(action_n) total_reward np.array(reward_n) done all(done_n) print(test total reward:, total_reward)逻辑说明select_action里如果传了 noise 就是训练模式不传则是纯利用。由于 ma-gym 的PredatorPrey-v0大部分是离散动作所以在喂给 env 之前要把连续输出argmax成整数这也对应前面说的离散动作映射问题。注意done_n是 list不能直接当布尔值用必须用all(done_n)判断全场是否结束。如果你想展示单个 episode 的围捕过程可以在循环里用env.render()但 ma-gym 的渲染模式配置不太一样需要先看下安装版本支持不支持。不支持的话就用info里的位置信息画轨迹图效果也够答辩用了。4.3 性能对比随机策略 vs MADDPG 策略毕业设计答辩时光跑出来不够得有对比。最简单的对照就是随机策略每个 step 从action_space里随机采样。我在一台 8 核 CPU 机器上用PredatorPrey-v0跑 100 局的表现如下策略平均每局总奖励100局内抓到猎物的次数随机策略-9.43MADDPG 训练 5000 集-1.237MADDPG 训练 20000 集2.891注意不同随机种子下这组数字会波动。所以正式报告里别只贴一次结果至少要跑 5 个不同种子取平均值并且在训练入口固定np.random.seed(42)、torch.manual_seed(42)。我见过不少复现项目“第一次成功、第二次发散”最后发现就是没固定种子导致实验不可复现。这也是评审老师最容易追问的点你的结果能不能稳定复现。5. MADDPG 复现避坑五个差点让我删库的常见问题5.1 环境、依赖与 Python 路径相关的问题坑1gym.make(PredatorPrey-v0)报 RegistrationError现象环境名字写进去gym 直接说未注册或者提示gym.spaces里没有MultiDiscrete。 原因ma-gym 是按 gym 0.18 到 0.21 的接口写的新版 gym 把register、Env基类的位置改了注册不上。 解决把项目放进独立虚拟环境安装gym0.21.0再pip install -e ./ma-gym-master。如果已经装了新版 gym不要在同一环境里硬混。坑2python main.py提示No module named buffer或找不到maddpg现象文件都在项目根目录但 Python 运行时就是找不到模块。 原因IDE 或终端把工作目录设到了子目录项目根目录没有进入sys.path。 解决在项目根目录设置 Python 环境变量。Windows 用set PYTHONPATH.Linux/macOS 用export PYTHONPATH.然后再运行python main.py。如果项目里带了.venv还要确认pyvenv.cfg指向的是本机 Python。压缩包里的虚拟环境在作者机器上生成路径基本是失效的最省事的办法是删掉.venv重新python -m venv venv。这个坑很隐蔽很多人卡的还不在算法而是环境。5.2 训练过程数值收敛相关的问题坑3训练中途 loss 变 NaN模型直接报废现象跑到几百步终端开始刷nan查看模型参数全是nan。 原因多智能体 Critic 的目标值r gamma * target_q里如果 reward 尺度很大比如捕食成功奖励 100再叠加多个 agent 的拼接Q 值量级会快速膨胀最终梯度爆炸。 解决在 ReplayBuffer 存储前把 reward clip 到 [-1,1]或者在计算 target_q 时用torch.clamp。同时给 Critic 和 Actor 加clip_grad_norm_(max_norm1.0)。我一般会优先 clip reward因为它同时抑制了 value 函数的方差效果比单纯梯度裁剪更彻底。坑4训练 5000 集回合奖励还在随机水平附近波动现象总奖励曲线看起来像心电图没有上升趋势甚至有往下掉的迹象。 原因探索噪声标准差固定为 0.5 不衰减导致 Actor 输出的大部分动作都被噪声覆盖策略永远在“随机游走”学不到稳定的围捕行为。 解决每个 episode 结束执行noise_std * noise_decay并设置noise_decay0.9999。到 10000 集时噪声已经衰减到不足 0.1这时候策略才开始真正收敛。这个参数看上去不显眼但往往是“训练不收敛”的真正元凶。坑5Critic 的 action 输入用错导致 Q 估计失真现象离散动作环境里训练时把整数形式的act_n直接拼进 Critic报维度错误或者 loss 一直在降但策略完全无改进。 原因离散动作要用 one-hot 向量表示而不是整数更隐蔽的错误是在 target Q 里复用了在线 Actor 的输出而没有用 target Actor detach。 解决动作进入 Critic 前统一转 one-hot维度等于动作数更新 target 时用target_actor计算next_act_all拼接后.detach()。如果你发现梯度回传路径里出现了“actor 的梯度莫名其妙打到 critic loss 上”说明这一处没处理干净。6. 把演示做成毕业设计加分项reward 塑形与参数敏感性分析6.1 用 reward shaping 改造捕食者-猎物环境直接训练原始环境也能出结果但演示效果容易“僵尸化”捕食者碰到猎物了 reward 才变化过程非常稀疏。为了让答辩时观众能一眼看懂智能体在主动博弈我会额外做一层 reward shaping。在mag.py的环境封装里加一个小函数即可def shaped_reward(obs_n, reward_n, info_n, step_count): for i in range(len(reward_n)): # 提高关键事件权重让“抓到猎物”的影响更明显 reward_n[i] reward_n[i] * 5.0 # 给捕食者一个“靠近猎物”的稀疏正激励 if info_n[i].get(dist_before, 0.0) info_n[i].get(dist_after, 0.0): reward_n[i] 0.02 return reward_n逻辑说明dist_before和dist_after是捕食者与猎物在 step 前后的距离如果变近了就奖励 0.02。这个增量很小不会改变最优策略但能让训练曲线更平滑演示时肉眼可见捕食者在围堵猎物。注意权重别设太大否则 MADDPG 会贪图“靠近”而忘掉真正的围捕动作。6.2 参数敏感性表学习率、τ、噪声衰减的实验设计答辩加分项里最稳的是参数敏感性分析。把下面这张表放进论文或 PPT评审一眼就能看出你对算法有手感参数测试区间不稳定表现我最终选用的值Actor 学习率1e-4 ~ 1e-35e-4 时 actor loss 震荡1e-4Critic 学习率1e-3 ~ 5e-33e-3 时 target_q 抖动1e-3tau0.001 ~ 0.050.05 时 target 更新过快导致 loss 上升0.01噪声衰减系数0.999 ~ 0.999990.999 时 5000 集不够收敛0.9999batch_size256 ~ 2048256 时 critic 梯度方差大reward 波动1024这张表不是直接抄来的是我在训练时先做 200 集小规模实验再放大到完整训练得出的。从那以后我每次换环境都强制自己先跑一轮 200 集的敏感性实验把 actor_lr、critic_lr、tau、noise_decay 四组参数各跑一遍看 reward 曲线是否在 200 集内有明显方向性再决定要不要上完整训练。这个方法帮我少熬了很多夜也让毕业设计答辩里的“为什么这样调参”不再是玄学。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑