news 2026/10/19 6:27:16

MADDPG算法Python实战:多智能体博弈强化学习从理论到工程落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MADDPG算法Python实战:多智能体博弈强化学习从理论到工程落地

简介:面向强化学习入门与进阶学习者,这是一份基于Python与MADDPG的多智能体博弈对抗算法实现,覆盖算法核心代码、训练测试流程与环境配置,适合作为毕业设计、课程设计、工程实训或初期项目立项参考。压缩包内共十四个文件,以Python脚本为主体,包含MADDPG与DDPG算法主体、网络结构定义、经验回放缓冲区、强化学习辅助工具以及独立测试脚本;另附配置文件、说明文档与.gitignore等版本管理辅助文件。整体体积约十九KB,目录结构清晰,便于按模块阅读、修改和二次开发。内容预览显示项目采用工程化目录,并带有测试环境文件,可直接用来理解多智能体对抗场景中的策略训练、评估与调试方法。目前已有267人浏览学习,适合希望系统掌握多智能体强化学习与博弈对抗代码实现的开发者参考。

1. 基于Python的MADDPG算法:把多智能体博弈从论文搬进现实

多智能体博弈对抗看起来是强化学习里的进阶课题,但真正拆开一套完整实现后你会发现,它的核心难点不在数学公式,而在于“每个智能体都在根据你的策略调整自己的策略”——这种非平稳性让单智能体DDPG直接搬过来就崩。这份基于Python的MADDPG工程代码,从环境定义、经验回放到Actor-Critic网络和训练主流程都补齐了,拿来就能跑出对抗博弈效果。适合正在做毕设、课程设计或打算从零接触多智能体强化学习的人,尤其是想把理论落地成可演示项目的开发者。本篇笔记按代码结构层层拆解它怎么用、参数怎么调、以及最容易翻车的几个坑。

2. 代码结构摸底:先读懂mag.py环境与buffer.py的存取

拿到工程包后,第一步不是去盯算法,而是把文件之间的调用关系理顺。这套代码是标准的MADDPG分层结构,含环境模块、算法模块、训练模块和测试模块,先从两个底层模块入手。

2.1 mag.py环境模块:博弈对抗的对抗场景是怎么定义的

环境是所有强化学习项目的起点,mag.py这个文件里定义了一组智能体之间的对抗逻辑,比如追踪者要靠近目标,而目标要逃脱追踪,双方各有自己的观测空间和动作空间。这个场景的博弈对抗性质非常典型:追踪者策略变强,逃脱者也会跟着变强,双方在训练过程中互相“卷”。

class MAG: def __init__(self, n_agents=2, obs_dim=6, act_dim=2): self.n_agents = n_agents self.obs_dim = obs_dim # 每个智能体的观测维度 self.act_dim = act_dim # 每个智能体的动作维度 self.state = None # 全局状态(用于中心化训练) def reset(self): # 随机初始化位置,保证每次episode起点不同 self.state = np.random.rand(self.n_agents, 4) * 2 - 1 return self._get_obs() def step(self, actions): # 根据动作更新位置,计算奖励 rewards = [] for i in range(self.n_agents): # 追踪者靠近目标得正奖励,目标被追上得负奖励 rewards.append(self._cal_reward(i)) return self._get_obs(), rewards, self._is_done()

逻辑说明:reset()里把智能体初始位置随机化,step()根据动作更新位置并返回奖励。这里的obs_dim=6对应位置、速度、相对距离等组合观测,act_dim=2通常是水平/垂直方向上的力或位移。不同智能体的奖励函数是对抗性的,你追我逃的博弈就体现在这里。

参数调整时注意:obs_dim和act_dim必须和后续network.py的输入输出维度对齐,改环境时这两个数字是最容易改漏的。我一般会在step()里顺手把每个智能体的速度钳制在一个上限内,否则训练早期动作输出过大,智能体一步就飞出边界,后面所有逻辑全部失效。

2.2 buffer.py经验回放:多智能体数据怎么存、怎么取

单智能体DDPG的经验回放存储的是(state, action, reward, next_state, done)五元组,多智能体版本则需要存下所有智能体的观测、动作和奖励。buffer.py里实现的正是这种多智能体扩展格式。

class ReplayBuffer: def __init__(self, capacity=1e6): self.buffer = deque(maxlen=int(capacity)) def push(self, obs_all, act_all, rew_all, next_obs_all, done_all): # 一次push存入所有智能体的转移数据 self.buffer.append((obs_all, act_all, rew_all, next_obs_all, done_all)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) obs = torch.cat([x[0] for x in batch], dim=0) act = torch.cat([x[1] for x in batch], dim=0) # ... 其余字段同理 return obs, act, rew, next_obs, done

逻辑说明:push()接收的obs_all是一个列表,每个元素是一个智能体的观测向量;sample()从缓冲区随机抽取batch后按维度拼接。记住这里存的是“一个时刻全体智能体的联合转移”,不是单个智能体独立存,这正是MADDPG中心化训练的数据基础。

参数上,capacity决定经验池容量,一般来说容量越大样本多样性越高,但占用内存也越大。我习惯是1e5到1e6之间,按场景复杂度来。如果训练中loss震荡极其剧烈,优先检查是不是buffer容量太小导致早期经验被过快覆盖,各类不稳定的数据反复冲刷网络。

3. 核心网络与算法逻辑:network.py的Actor-Critic与MADDPG.py的中心化训练

这一章是整个工程的核心。network.py负责搭建Actor-Critic网络结构,MADDPG.py则把环境和网络绑定起来完成中心化训练与去中心化执行的完整闭环。

3.1 network.py网络结构:从感知到动作的全连接决策链

class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden=64): super(Actor, self).__init__() self.fc1 = nn.Linear(obs_dim, hidden) self.fc2 = nn.Linear(hidden, hidden) self.fc3 = nn.Linear(hidden, act_dim) def forward(self, obs): x = torch.relu(self.fc1(obs)) x = torch.relu(self.fc2(x)) # 动作输出用tanh限制在[-1, 1] return torch.tanh(self.fc3(x)) class Critic(nn.Module): def __init__(self, obs_dim_n, act_dim_n, hidden=64): super(Critic, self).__init__() # 输入是全体智能体的观测+全体智能体的动作 input_dim = sum(obs_dim_n) + sum(act_dim_n) self.fc1 = nn.Linear(input_dim, hidden) self.fc2 = nn.Linear(hidden, hidden) self.fc3 = nn.Linear(hidden, 1) def forward(self, obs_all, act_all): x = torch.cat([*obs_all, *act_all], dim=1) x = torch.relu(self.fc1(x)) x = torch.relu(self.fc2(x)) return self.fc3(x)

逻辑说明:Actor网络输入自己的观测,输出连续动作值;Critic网络输入全体智能体的观测和动作,输出一个价值估计Q值。torch.tanh把动作限制在[-1, 1]区间,正好匹配环境里step()对动作的预期范围。这里体现了MADDPG最关键的思想——每个智能体用自己的Actor做决策(去中心化执行),但用全局信息训练Critic(中心化训练)。

参数说明:hidden=64是隐藏层宽度,简单的二维对抗场景64层就够。如果环境状态更复杂,比如加上了障碍物、多队伍混战,可以调到128或256。隐藏层不是越宽越好,过宽会导致收敛变慢且容易过拟合到当前对手策略上,这一点在博弈类环境中特别明显。

3.2 MADDPG.py算法主体:DDPG如何在多智能体场景下升级

MADDPG.py并不是把多个DDPG简单拼在一起,它的核心改进了Critic的输入信息。每个智能体的Critic在训练时能看到所有智能体的动作,但在执行时只用本地Actor。这个改动使得环境非平稳性大幅降低,因为单个智能体策略变化导致的Q值波动可以被全局信息缓解。

class MADDPG: def __init__(self, obs_dim_n, act_dim_n, lr_actor=1e-4, lr_critic=1e-3, gamma=0.95, tau=0.01): self.actors = [Actor(obs_dim_n[i], act_dim_n[i]) for i in range(len(obs_dim_n))] self.critics = [Critic(obs_dim_n, act_dim_n) for _ in range(len(obs_dim_n))] # 对应target网络 self.target_actors = [copy.deepcopy(a) for a in self.actors] self.target_critics = [copy.deepcopy(c) for c in self.critics] def update(self, sample, i): obs, act, rew, next_obs, done = sample # 预测下一时刻所有智能体的动作 next_act = [self.target_actors[j](next_obs[j]) for j in range(len(self.actors))] # 目标Q值 target_Q = rew[i] + gamma * self.target_critics[i](next_obs, next_act) # 当前Q值 current_Q = self.critics[i](obs, act) # Critic损失 critic_loss = nn.MSELoss()(current_Q, target_Q) # Actor损失:最大化当前Q值 actor_loss = -self.critics[i](obs, self.actors[i](obs[i])).mean()

逻辑说明:update()里先计算目标网络输出的下一状态动作和Q值,再用MSE更新Critic;Actor的梯度来自Critic对当前动作的Q值取负。梯度回传链路是Critic → Actor的输出 → Actor的参数,这也是为什么MADDPG能解决连续动作空间博弈问题。tau=0.01是软更新的权重,每次把target网络参数向当前网络挪一点。

lr_actor=1e-4和lr_critic=1e-3这个不对称设置是常用的,因为Critic学习的是价值函数,需要更快收敛才能给Actor提供稳定的梯度信号。如果你发现训练中Critic的loss一直在降但Actor的奖励不涨,很可能是Actor学习率太小,适当调大到5e-4试试。

3.3 DDPG.py和核心逻辑的对比:为什么多智能体不能直接照搬单智能体

工程里同时存在DDPG.py,这个文件很值得单独看。它和MADDPG的代码几乎一样,唯一的区别是Critic输入只包含单个智能体自己的观测和动作。把这套代码跑在mag.py环境上,绝大多数情况都会发散——原因在于对手的策略在训练中不断变化,单个智能体看到的转移数据前后不一致,Q值更新时目标也在漂移。

class DDPG: def update(self, sample, i): # 注意:只用第i个智能体自己的观测动作 obs_i, act_i, rew_i, next_obs_i = sample next_act_i = self.target_actor(next_obs_i) target_Q = rew_i + self.gamma * self.target_critic(next_obs_i, next_act_i) current_Q = self.critic(obs_i, act_i) critic_loss = nn.MSELoss()(current_Q, target_Q)

逻辑说明:对比两段代码可以发现差别只在obs和act的拼接范围。DDPG只用当前智能体自己的信息,MADDPG用全体信息。博弈对抗场景下大家都会学,单智能体视角就是不断追赶一个移动靶,经验回放里的数据分布严重非平稳。所以如果论文里写“基于MADDPG”,实验对比时用DDPG当baseline是最直观的——它展示了非平稳性的破坏力。

4. 训练主流程:main.py的参数配置与mag.py的对抗博弈逻辑

读懂了算法模块,还需要把所有零件拼起来看训练流程。main.py是入口,它负责创建环境、初始化算法、循环训练、保存模型,以及调用测试逻辑。

4.1 main.py训练循环:从随机动作到策略涌现的完整过程

# main.py核心流程 env = MAG(n_agents=2, obs_dim=6, act_dim=2) maddpg = MADDPG([6, 6], [2, 2]) # 两个智能体,各6维观测2维动作 buffer = ReplayBuffer(capacity=1e6) episodes = 5000 batch_size = 1024 for epoch in range(episodes): obs = env.reset() ep_reward = 0 while True: # 每个智能体根据自身观测选动作,这是去中心化执行 actions = [maddpg.actors[i](torch.FloatTensor(obs[i])).detach().numpy() for i in range(env.n_agents)] next_obs, rewards, done = env.step(actions) buffer.push(obs, actions, rewards, next_obs, done) # 经验够多才开始更新 if len(buffer.buffer) > batch_size: for i in range(env.n_agents): batch = buffer.sample(batch_size) maddpg.update(batch, i) obs = next_obs ep_reward += sum(rewards) if done: break if epoch % 100 == 0: print(f"Epoch {epoch}, Reward: {ep_reward}")

逻辑说明:外层循环是回合数,内层循环直到done才结束。每步先让每个智能体的Actor独立产出动作,环境返回交互结果后压入经验池,经验池足够大后再采样更新所有智能体的网络。注意update是对每个智能体分别调用的,每个智能体有独立的Actor、Critic及其target网络。

参数上episodes=5000是训练轮数,二维简单场景跑到3000轮左右就能看到明显的追踪-逃跑行为涌现,5000轮是为了收敛更稳定。batch_size=1024有些偏大,如果显存紧张可以降到512;但batch太小会让梯度噪声大,在博弈场景里容易震荡。

4.2 训练早期阶段的观察:损失与奖励曲线怎么解读才靠谱

很多人在训练初期看到loss或者奖励曲线乱跳就慌了,其实对抗博弈场景的指标波动是常态。我习惯记录三个指标:每个智能体各自的episode奖励、Critic的Q值平均值、以及Actor输出动作的分布范围。

  • 智能体0(追踪者)奖励曲线应该整体抬升,但中间会有平台期甚至回退——因为对手也在变强。
  • 智能体1(逃脱者)奖励一开始会快速上升,再遇到瓶颈,这也是对手策略迭代的反哺。
  • Actor的动作输出应该逐步趋于平滑,如果动作值一直在±1边界上撞来撞去,大概率是奖励尺度没调好。

如果发现两个智能体的奖励都在上涨,说明博弈还没真正形成“对抗”,更像是在合作刷分,这时候要检查奖励函数里是不是缺少了约束项——比如逃脱者应该距离越远奖励越大,而追踪者距离越近奖励越大,双方目标必须互斥。

4.3 模型保存与加载:训练出的策略怎么固化下来

torch.save({ 'actor_0': maddpg.actors[0].state_dict(), 'actor_1': maddpg.actors[1].state_dict(), }, 'checkpoint.pth')

逻辑说明:只保存Actor就够,因为执行阶段不需要Critic。加载时用torch.load配合load_state_dict恢复网络参数,再传给环境做测试。

我习惯每500轮存一个检查点,文件名带epoch编号。原因很简单:博弈对抗的训练曲线经常会有“回退”现象,某个时刻看起来很强的策略,继续训练反而可能被对手针对后变弱,这时候有历史模型可以回滚。

5. 多智能体博弈的常见问题与排查:训练发散、维度不匹配与收敛不稳

这一章写的是我实际跑这套代码时踩过最深的几个坑,每一条都是血泪经验。对照自查,比自己盲调快得多。

5.1 现象:训练一两百轮后loss直接爆炸,动作输出全是NaN

原因:经验回放里混入了异常值,最常见的是环境里出现inf或者位置更新公式除以了相对距离为0的数,导致梯度爆炸。另一个可能是学习率太高,Critic在非平稳目标下直接发散。

解决:先在环境step()里加保护——速度钳制、位置边界约束、相对距离分母加一个极小epsilon。然后检查每个智能体的奖励是否为有限值,用np.isfinite过滤一下再存进buffer。最后把学习率从1e-3降到3e-4,基本能稳住。

5.2 现象:维度不匹配,torch报错说cat操作对不上

原因:修改环境后没有同步修改Actor和Critic的输入维度。比如新增了观测项,obs_dim从6变成8,但Actor __init__里的nn.Linear(6, 64)没改。多智能体场景下这种错误更隐蔽,因为每个智能体的维度可能不同。

解决:写个简单的断言语句放在MADDPG.__init__里,启动时检查obs_dim_n长度是否等于智能体数量,act_dim是否等于动作边界长度。我自己会在训练入口打印一行每个网络的参数shape,肉眼扫一遍再开跑。

5.3 现象:训练很久,Actor的loss一直负得很大但奖励不涨

原因:这里要注意,MADDPG里Actor loss是-Q值,Q值本身会被Critic越估越高,所以Actor loss持续下降是正常的,不能当作收敛指标。真正要看的是环境奖励。“Q值膨胀”在博弈场景很常见,因为每个Critic都在更新,整体价值估计会漂移。

解决:在代码里把Critic的Q值和真实奖励差值打印出来。如果差值越来越大,说明Critic过估计严重,可以给Critic加入L2正则或者调小tau到0.005,让target网络更新更慢,减少价值传播偏差。

5.4 现象:两个智能体奖励一起涨,但一测试就露馅,策略完全没对抗性

原因:奖励函数里追踪者拿到正的奖励条件是“更靠近目标”,逃脱者拿到奖励是“更远离追踪者”,但有些实现会把两个奖励相互混淆,或者设置了共享奖励机制,导致两个智能体一起往一个方向移动就能各自拿分。

解决:检查reward返回的是不是每个智能体独立的奖励,而不是全局平均。另外在环境里加一个可视化逻辑,每次episode打印初始位置和结束位置,看它们是不是真的在互相追逐。这个可视化调试方法比盯着loss曲线直观一百倍。

5.5 现象:换一台机器或者虚拟环境后,运行报ImportError

原因:工程包里没有自带依赖清单,部分模块用了较新的API,比如torch版本不对会导致nn.Utils相关报错。这类环境问题在给导师展示前最容易翻车。

解决:先看pyvenv.cfg里的Python版本,再对照README.md里写的依赖建议,用pip安装指定版本。我一般先建一个干净虚拟环境再逐行跑main.py,缺哪个库装哪个,不做成批量安装,避免版本冲突。

6. 验证模型做对抗:用test.py回放智能体行为并调参

训练完成后,test.py能让你直观看到学出来的策略长什么样。它会加载保存的模型参数,让两个智能体在环境里跑完整episode,打印每一步的位置和动作。这个环节不只是验收成果,更是判断策略质量的试金石。

先跑一个回合观察行为。如果追踪者一直乱转,说明训练没学到有效策略,回去查reward设计;如果追踪者能直线逼近但逃脱者像傻了一样不懂拉开距离,可能是逃脱者的策略还在早期阶段,加载更晚的检查点再试。我常用的验证思路是让两个智能体加载不同epoch的模型,观察先进策略对后进策略的碾压幅度,这能直观判断模型是否真的在博弈。进一步可以改动环境参数,比如把初始化距离拉远,测试追踪者能否长距离追击,或者把速度上限调高,看策略是否有泛化能力。

另一个实用技巧是记录完整episode的位置轨迹,画成折线图。轨迹交叉说明双方确实在周旋,单方向直线移动则说明策略过于简单。如果一切正常,最后一步是调整奖励尺度:把追踪者单步奖励从+0.1改为+1,重训后对比收敛速度。这个流程走完,你对这套代码的理解就不停留在“能跑出来”,而是知道什么参数在改变什么行为——从那以后我每次拿到新的多智能体项目,都会强制走一遍“读环境→建基线→调奖励尺度→对比测试”的流程,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 6:26:47

FPGA+ARM异构FOC电机控制方案设计与调试实践

前阵子我把一台 48V/500W 的永磁同步电机控制程序从单核 MCU 搬到了 SoC FPGA 上,ARM 侧跑应用逻辑,FPGA 侧跑 FOC(磁场定向控制)的核心环路。折腾了大概两周,最大的感受是:FPGAARM 的 FOC 电机控制方案设计…

作者头像 李华
网站建设 2026/10/19 6:25:28

Python课堂行为分析系统:人脸表情识别实战入门

简介:本资源是一套面向计算机专业本科生的高分毕业设计项目——基于Python的人脸表情识别课堂行为检测系统,专为毕设答辩、课程设计及期末大作业打造,解决课堂教学中学生专注度量化分析的实际需求,适合零基础学习者快速上手与二次…

作者头像 李华
网站建设 2026/10/19 6:25:13

Blender与DeepSeek结合:AI辅助动画脚本开发实战指南

1. 为什么“Blender DeepSeek”这个组合值得认真对待第一次看到“Blender DeepSeek 动画制作”这个标题时,我脑子里冒出的第一个念头是:这俩东西是怎么凑到一块儿的?一个是开源三维软件里最能打的全能选手,一个是最近在推理和代…

作者头像 李华
网站建设 2026/10/19 6:24:59

短视频用户兴趣分析毕业设计:Django数据采集到可视化完整实战

先说结论:短视频用户兴趣分析这类题目,在目前的毕业设计里属于“性价比”很高的一档。它不像纯推荐算法那样需要深厚的数学功底,也不像纯管理系统那样缺乏亮点,而是踩在了数据采集、数据清洗、统计分析、可视化展示、Web开发这个完…

作者头像 李华
网站建设 2026/10/19 6:24:49

论文降重复率方法实测:AI改写、翻译倒腾与人工重构谁更有效?

2026届的毕业论文季,我身边几乎每天都有人问同一个问题:网上传的那些降重复率方法,到底哪个是真的有用?说实话,这个话题我憋了很久想写。最近一个月,我专门腾出时间,把市面上传得最凶的五种方案…

作者头像 李华
网站建设 2026/10/19 6:24:34

Spring Boot进阶实战:商城、交友社区、聊天室三大项目技术底座全解析

在Java Web这个方向上,零散功能好做,完整项目难做。这几年我陆续带过不少人做Spring Boot项目,发现最常被选中的题目翻来覆去就是几个:零食销售商城、交友社区在线聊天、聊天室用户管理系统。放在一起看很有意思——一个是纯电商&…

作者头像 李华