简介这是一份基于MADDPG多智能体深度确定性策略梯度的博弈对抗算法Python实现项目源自个人期末大作业最终成绩98分代码完整、经过严格调试下载后即可直接运行。资源面向计算机相关专业正在完成课程设计、期末大作业或需要进行项目实战练习的学生重点演示了如何将DDPG扩展至多智能体博弈对抗场景实现可复现的完整工程既能用于巩固多智能体强化学习理论也可直接作为课程设计演示程序。压缩包共13个文件其中10个Python脚本构成核心代码覆盖DDPG、MADDPG、神经网络、经验回放、训练工具、环境测试等功能模块结构清晰便于按需深入阅读同时配套txt说明文档和cfg配置文件用于查看运行说明与Python环境依赖。整个项目仅15KB体型紧凑、依赖简单学习者可在此基础上替换环境、调整奖励函数快速扩展为自己的课程设计或毕业设计代码分层合理、可读性强适合复盘高分项目的工程组织方式。目前已有385人学习、使用该项目对于希望获得高分项目参考、以最短时间上手MADDPG落地实现的读者是一份难得的源码资料。1. 先把这份MADDPG源码读明白多智能体博弈对抗的完整闭环拿到一份标注“下载即用”的多智能体博弈对抗算法源码第一反应别急着跑训练脚本。这份基于MADDPG的Python项目解决的核心问题是当多个智能体在同一个环境里互相博弈每个智能体的策略都在改变其他人的状态转移单智能体强化学习算法DDPG、TD3、PPO会因环境非平稳性而失效。MADDPG通过“集中式训练、分布式执行”让每个智能体在训练时看到全局信息、在执行时只用本地观测从而学到稳定的对抗策略。项目代码是完整闭环——从经验回放buffer、Actor-Critic网络到训练主循环和测试脚本全部就位适合做课程设计、期末大作业或刚入门多智能体强化学习想跑通第一个项目的同学。源码结构清晰能直接看到MADDPG与DDPG的差异落在哪里。2. MADDPG的原理与代码结构集中式训练、分布式执行怎么落地2.1 从DDPG到MADDPG为什么单智能体方法在多智能体场景会翻车先回忆DDPG的设定。DDPG是深度确定性策略梯度算法维护一个Actor网络输出连续动作一个Critic网络评估当前状态下动作的价值。Actor用确定性策略梯度更新Critic用TD误差更新两者配合目标网络稳定训练。这套方法在单智能体连续控制任务上表现稳定比如机械臂控制、自动驾驶的纵向决策因为环境的状态转移只由自己的动作决定本质上是一个标准的马尔可夫决策过程。但把它直接搬到多智能体博弈里问题就出现了。设想一个两个智能体互相追逃的场景追捕者策略在变强逃跑者为了活命也在调整策略。对追捕者来说逃跑者的行为就是环境的一部分而这个“环境”每时每刻都在变。DDPG的Critic在计算Q值时假设环境转移是固定的它把对手的动作当成噪声的一部分忽略掉训练出来的Q值在数学上就不成立。实际表现就是训练曲线乱跳智能体学不到有意义的对抗策略跑几百个回合都看不到上升趋势。MADDPG的解决方案是Centralized Training with Decentralized Execution也就是常说的CTDE。训练阶段每个智能体的Critic可以拿到所有智能体的观测和动作相当于站在上帝视角评估当前的局势。Actor则保持独立只用自己本地的观测输出动作。这样Critic能感知到其他智能体策略的变化从而缓解环境非平稳带来的偏差。执行阶段不需要任何通信每个智能体各自决策这一点对实际部署非常友好。多智能体强化学习领域里CTDE已经成为主流框架MADDPG是最经典的落地范式之一。项目里的mag.py就是multi-agent game的缩写负责组织多个智能体的交互流程。rl_utils.py这类工具文件一般放通用函数比如折扣回报计算、one-hot转换、经验预处理。把这些文件对应到算法流程里读代码的速度会快很多。2.2 项目文件角色拆解main.py、MADDPG.py、network.py各管什么拿到项目先别急着跑把文件之间的关系捋清楚后面调参才知道改哪里。这个压缩包里的核心文件大致分成四层入口层、算法层、网络层和环境与测试层。文件职责关键内容main.py训练入口初始化环境、智能体、缓冲区跑训练循环并保存模型MADDPG.py算法主体管理所有智能体的Actor与Critic、目标网络软更新、梯度更新network.py网络结构定义Actor和Critic的MLP结构激活函数与观测动作拼接buffer.py经验回放存储多智能体联合状态、联合动作、奖励与终止信号DDPG.py单智能体基线DDPG实现用于对比或作为理解MADDPG的参照mag.py / test_env.py博弈环境定义对抗场景的观测、动作边界、奖励函数与终止条件ceshi.py / test.py测试脚本加载已训练模型观察智能体实际对抗表现文件命名在不同版本里可能有出入比如mag.py也可能叫env.py但职责划分基本一致。我的建议是从main.py开始追踪整个调用链把“环境怎么建、每个回合的交互怎么写、更新频率设多少”这几个问题搞清楚其他文件自然就顺了。训练主循环常见写法如下# main.py 训练主循环核心片段 for episode in range(episode_num): # 控制总回合数 obs env.reset() # 返回所有智能体的观测列表 episode_reward np.zeros(n_agents) # 记录本回合各智能体累计奖励 for step in range(max_steps): # 限制单回合最大步数 actions [] for i in range(n_agents): action maddpg.get_action(obs[i], noise_scale0.1) actions.append(action) next_obs, rewards, dones, _ env.step(actions) buffer.push(obs, actions, rewards, next_obs, dones) obs next_obs episode_reward rewards if buffer.size() batch_size: for _ in range(update_times): batch buffer.sample(batch_size) maddpg.update(batch) if episode % 100 0: print(fEpisode {episode}, reward: {episode_reward})几个关键点要解释清楚。noise_scale控制探索强度前期要大一些让智能体尝试不同动作后期要逐步减小让策略稳定下来这是一个从探索到利用的退火过程。update_times表示每次采样后更新几次参数增加它可以加速学习但太高会让策略在最新经验上过拟合反而加剧震荡。env.step返回的rewards是长度为n_agents的数组说明每个智能体收到的是独立奖励——这是博弈对抗里很关键的设计智能体不是共享回报而是各自最大化自己的收益。2.3 Actor-Critic网络的参数细节维度拼接与激活函数network.py是理解整个算法的钥匙。Actor网络输入单个智能体的观测维度输出对应动作空间的连续值通常用tanh把动作压缩到[-1, 1]区间再在外部乘上环境要求的动作缩放系数。Critic网络则完全不同它的输入是所有智能体的观测拼接再加上所有智能体的动作拼接输出一个标量Q值。这个拼接动作决定了Critic是否真的“集中式”。下面是常见的Actor定义# network.py 中的Actor网络 class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim64): super(Actor, self).__init__() self.fc1 nn.Linear(obs_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, act_dim) self.tanh nn.Tanh() def forward(self, obs): # 两层ReLU隐藏层后接tanh输出保证动作在[-1, 1]区间 x torch.relu(self.fc1(obs)) x torch.relu(self.fc2(x)) return self.tanh(self.fc3(x))代码逻辑很直接两层64维全连接加ReLU激活最后一层输出动作维度后接tanh。注意hidden_dim是可以调的观测维度高、博弈关系复杂时加宽到128或256有助于表达更复杂的策略但也会增加训练难度。Critic的定义稍复杂因为它要处理拼接输入# network.py 中的Critic网络 class Critic(nn.Module): def __init__(self, obs_dim, act_dim, hidden_dim64): super(Critic, self).__init__() self.fc1 nn.Linear(obs_dim act_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, 1) def forward(self, obs, act): # obs和act都是包含所有智能体信息的拼接张量 x torch.cat([obs, act], dim-1) x torch.relu(self.fc1(x)) x torch.relu(self.fc2(x)) return self.fc3(x)这里最容易写错的就是Critic的输入维度。obs是全部智能体观测的拼接act是全部智能体动作的拼接合并后过一层全连接。如果只传入自己的观测和动作Critic就没有全局视野退化成DDPG非平稳性问题会原样保留。改造网络结构时先打印每个智能体的obs_dim和act_dim用sum()算清楚拼接后的维度再决定fc1的输入尺寸这个过程能避免大量返工。提示tanh输出范围是[-1, 1]但实际环境里的动作范围往往不是这个区间。如果追逃场景里速度控制是[0, 2]需要在Actor输出后乘缩放系数并确保缩放逻辑与环境action_space的上界一致否则智能体永远打不到边界动作。3. 跑通项目的完整流程环境配置、训练执行与结果验证3.1 环境配置与依赖安装pyvenv.cfg里的版本线索项目里有一个pyvenv.cfg文件这是Python虚拟环境的配置文件记录了虚拟环境对应的系统Python路径和版本。看到这个文件说明作者是在虚拟环境里开发运行的。打开它能看到Python版本线索比如3.8还是3.9这个信息直接决定你怎么建环境。版本不匹配是很多新手翻车的第一步尤其是torch与Python版本不兼容的报错错起来毫无头绪。依赖一般集中在torch、numpy和gym或pygame这类环境库上。如果项目用自定义的mag.py作为环境可能连gym都不需要但要确保环境文件的底层依赖都装好。我的习惯是先用命令检查项目根目录有没有requirements.txt没有就手动追踪import的第三方库。下面是完整的环境搭建流程# 用虚拟环境隔离避免污染系统Python python -m venv maddpg_env source maddpg_env/bin/activate # Windows下用 maddpg_env\Scripts\activate pip install torch numpy gym matplotlib python test_env.py # 先跑环境自检脚本验证维度如果test_env.py输出正常说明环境接口没问题再跑main.py开始训练。如果test_env.py报错先别往下走维度错位在训练时会被放大到极其难排查。VSCode里配置Python环境时记得把解释器指向maddpg_env这个虚拟环境的路径否则你装的依赖全部白费训练时依然报ModuleNotFoundError。3.2 训练主循环从env.reset()到buffer回放的调用链跑起来之前理解main.py、MADDPG.py、buffer.py三者间的调用链决定了你改代码时改动点落在哪里。整体流程是环境构造完成后main.py拿到每个智能体的观测调用MADDPG的get_action得到动作把动作交给env.step推进仿真得到新观测、奖励和终止信号。这组数据被压入经验回放buffer等积累到一定量再抽样交给MADDPG.update更新网络参数。buffer.py在多智能体场景下有一个很容易忽略的细节。它存的不是单条(state, action, reward, next_state)而是一组联合数据所有智能体的观测列表、动作列表、奖励列表和终止标志。抽样时也是一次抽一整条联合数据保证Critic计算时能看到全局信息。如果改成按单智能体方式存储和抽样Critic拿不到其他智能体的动作集中式训练就名存实亡。MADDPG.py里的update函数是训练的核心它要同时处理当前网络和目标网络两套Actor-Critic。目标网络的作用是让Q值更新的目标保持相对稳定每隔一定步数用软更新方式缓慢拉近与当前网络的距离。软更新逻辑通常长这样# MADDPG.py 中的软更新逻辑 def soft_update(self, tau0.01): for target_param, param in zip(self.actor_target.parameters(), self.actor.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data) for target_param, param in zip(self.critic_target.parameters(), self.critic.parameters()): target_param.data.copy_(tau * param.data (1.0 - tau) * target_param.data)tau是软更新系数取0.01是常见值代表目标网络每次只向当前网络移动1%。tau太大会让目标Q值跟随当前网络剧烈变化训练容易振荡太小则目标网络更新过慢前期收敛偏慢。不同训练阶段可以考虑调tau前期用稍大的值加速学习后期调小让策略稳定。训练时每隔一定步数调用一次soft_update具体频率由main.py里的update_times间接决定。3.3 超参数表与收敛判断学习率、gamma、buffer容量怎么调项目里可能已经给了一套能跑通的默认参数但换场景后必须重新调。下表是我在多智能体博弈项目里常用的参数区间可以直接对照参数常见取值影响Actor学习率1e-3 ~ 1e-4过大策略震荡过小收敛慢Critic学习率1e-3 ~ 1e-4一般与Actor同量级或略大gamma0.9 ~ 0.99越大越考虑长期收益buffer容量1e5 ~ 1e6太小遗忘旧经验太大采样到过时数据batch_size128 ~ 512过小梯度噪声大过大训练慢噪声初始值0.1 ~ 0.3探索强度后期衰减到0.01左右软更新tau0.01 ~ 0.05目标网络更新速度判断训练是否收敛别只看loss核心是每个智能体的回合奖励曲线。博弈对抗场景的奖励曲线通常不是单调上升的因为对手也在变强。合理现象是曲线前期剧烈波动中期出现上升或保持稳定后期逐渐平稳。如果两条智能体的奖励曲线长期一高一低且不改变说明策略已经定型优势方没有进步空间这时候考虑调大探索噪声或调整奖励函数。提示多智能体对抗的训练曲线比单智能体更“玄学”几百回合内没上升不代表死局。先确认奖励值的量级奖励在个位数和千位数的场景适配的学习率完全不同。4. 训练避坑指南不收敛、维度错位与结果不可复现4.1 现象Critic loss 不降反升Q值越估越大如果发现critic loss一直在涨Q值的绝对值不断膨胀先检查奖励的数值量级。MADDPG的critic用TD误差更新当奖励数值很大时Q值也被放大MSE loss自然居高不下。解决办法是先对reward做归一化或缩小reward权重让Q值维持合理范围再考虑调学习率。另一种常见原因是目标网络更新没有生效。检查软更新代码是否真的被调用有没有在update函数里遗漏target_param的赋值。有人定义了target网络但忘了在训练循环里调soft_update结果target网络一直是随机初始化的权重critic学得再好target Q值也是乱跳的。排查时在迭代里打印一行target网络参数的均值看它是否在缓慢变化即可确认。第三种情况是buffer里存了过多终止状态样本。如果terminal后没有重置环境就继续采样错误的done信号会进入buffer训练时目标Q值被错误置零策略会以为提前终止能得到更好结果行为变得激进且不稳定。处理方式是确保每一步都能拿到正确的done标志并在回合结束后及时重置环境。4.2 现象维度报错频繁拼接张量反复对不上多智能体项目的维度错位是我见过最多的翻车点报错一般是RuntimeError: size mismatch。出现这类错误第一步打印每个智能体的obs_dim和act_dim确认观测是numpy数组而非listlist拼接和数组拼接的行为完全不同经常在这里埋雷。维度错位的常见原因有两个。第一Critic输入拼接顺序不一致。训练时按[agent1_obs, agent2_obs]顺序拼接算target Q值时换成了[agent2_obs, agent1_obs]维度虽然对但语义错乱训练出的Q值没有意义。第二batch维度处理出错。从buffer抽样出来是(batch, n_agents, dim)的结构要转成(batch, n_agents * dim)再喂给网络少一个reshape就报错。解决思路是写一个shape断言函数在网络forward里打印关键张量的shape跑一个回合确认后再开始长时间训练。我在这里吃过亏维度问题在单个回合内不暴露要等buffer积累到一定量抽样时才触发等触发时已经跑了几百回合浪费时间。4.3 现象训练曲线震荡剧烈时好时坏多智能体博弈对抗的reward曲线天然是震荡的因为每个智能体都在动态调整策略。但如果震荡到完全看不出趋势先检查探索噪声设置。噪声过大智能体大部分时间在随机游走策略只是偶然碰到的好结果噪声衰减太慢后期策略也稳不下来。另一个隐藏因素是buffer容量。容量过大时早期随机策略的经验占比太高后期采样到大量旧经验拖慢甚至干扰当前学习容量过小则经验多样性不足智能体容易在局部策略里打转。我的习惯是前期用较大噪声加较小buffer等策略有一定雏形后逐步调大buffer保留更成熟的经验。训练过程中把噪声衰减曲线和reward曲线画在一起看能明显看出相关性。4.4 现象换环境后完全不收敛项目自带的对抗环境能跑通但换成自己的博弈场景后学不动先别怀疑算法优先检查环境接口。MADDPG要求环境返回的obs和action都是连续值如果场景里有离散动作或稀疏观测需要先做映射或编码把离散动作转成one-hot或连续代理值。再看奖励函数是否提供了有效梯度。如果奖励只在回合结束时给一次中间步骤全是0MADDPG的critic很难学出有意义的Q值因为TD误差只在少数几步非零。常见做法是加入势能函数或中间奖励比如追逃博弈里根据距离变化给连续奖励让智能体每一步都获得反馈。这是把算法从玩具环境搬到实际问题时必经的工程改造也是课程设计拿高分和低分的分水岭。4.5 现象固定随机种子但结果不可复现固定了seed但不同机器上结果差异巨大在多智能体强化学习里很常见。原因是浮点数运算的微小偏差被策略的不稳定性不断放大。固定seed要同时设三层random.seed、numpy.random.seed、torch.manual_seed如果用了CUDA还要设torch.cuda.manual_seed_all并关闭cudnn的自动调优。换机器后性能变化明显时优先检查batch_size。GPU和CPU上同一个batch_size的行为不同batch越小梯度噪声越大对多智能体这种多损失函数叠加的训练影响更明显。其次是线程数PyTorch的CPU并行线程会影响非确定性设置torch.set_num_threads(1)能减少一部分波动。做实验对比时固定这些条件比反复调参更重要否则分不清策略改善是真实的还是随机波动。5. 进阶玩法把这套MADDPG迁移到自己的博弈场景5.1 自定义环境接口观测列表、动作列表与独立奖励把项目自带的博弈环境换成自己的场景核心是让环境接口符合MADDPG的约定reset返回n_agents个观测组成的列表step接收n_agents个动作组成的列表返回的奖励也必须是列表。很多人在这里翻车环境只返回全局奖励所有智能体共享同一个reward这会破坏博弈对抗的前提——每个智能体必须有自己的利益。以最简单的追逃场景为例一个追一个逃观测是相对位置和速度动作是水平和垂直方向的速度控制# 自定义追逃环境接口骨架 class PursuitEvasion: def __init__(self): self.obs_dim [4, 4] self.act_dim [2, 2] def reset(self): self.pos_p np.random.uniform(-1, 1, 2) self.pos_e np.random.uniform(-1, 1, 2) return [np.concatenate([self.pos_p, self.pos_e]), np.concatenate([self.pos_e, self.pos_p])] def step(self, actions): done np.linalg.norm(self.pos_p - self.pos_e) 0.1 reward_p 1.0 if done else 0.0 reward_e -1.0 if done else 0.0 return obs, [reward_p, reward_e], [done, done], {}奖励塑形是迁移时最值得花时间的部分。追捕者抓住目标拿正奖励逃跑者被抓住拿负奖励对抗才成立。如果两个奖励都设成正数两个智能体可能演化出“一起站着不动”的投机行为这就是reward hacking的一种。给每个智能体设计独立奖励、适当加连续中间奖励是提高训练质量和稳定性的关键做法。5.2 评估与模型保存固定种子测试和关键文件训练完成后评估分两个层面。数值层面固定测试回合数统计平均回合奖励和胜率可视化层面把test.py里的决策过程打印出来看动作是否合理。测评估算要固定随机种子和初始位置保证每次测试的初始条件一致否则几次评估结果差异会误导你对模型质量的判断。模型保存时注意保存的应该是当前网络和target网络两个部分否则下次加载还要重新软更新很久才能恢复。我一般在每个智能体奖励达到新高的回合保存一次actor参数文件名带上回合数和奖励值方便后续筛选最优版本。从那以后我每拿到一个MADDPG项目第一件事不是跑训练而是先打印main.py、MADDPG.py、network.py、buffer.py的调用链标注每个张量的shape确认Critic的输入确实包含全部智能体的观测和动作。这个习惯帮我避开了一堆维度错位和时间浪费。项目里的默认超参数是作者在自己的环境上调出来的换场景后务必按前面的避坑清单重新对齐。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?