ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多智能体追逃博弈强化学习实战:Gym环境与DQN/MADDPG实现

多智能体追逃博弈强化学习实战:Gym环境与DQN/MADDPG实现 简介这是一份基于Python与gym框架的多智能体追逃博弈强化学习平台源代码面向正在完成课程设计、期末大作业的计算机相关专业学生也适合需要强化学习项目实战的开发者借鉴。项目经导师指导并通过认可属于高分课程设计成果。资源包共40个文件以27个Python源码文件为主辅以pyc预编译文件、依赖环境清单、项目说明文档及配置文件整体约79KB轻量易读。代码中可见二维/三维环境模块、空战博弈环境以及测试脚本等结构覆盖从环境定义到智能体交互的关键环节可直接对照学习或二次扩展。目前已有146人学习下载对于希望快速掌握gym环境下多智能体博弈建模、或需要一套完整可运行的大作业代码模板的读者而言能有效降低起步门槛并借助清晰的目录结构快速定位功能模块。1. 多智能体追逃博弈强化学习平台一份把 MARL 落地成可运行课程设计的 Python 源码多智能体追逃博弈强化学习平台是本科课程设计里最容易被高估、也最值得拆解的一类题目。这套基于 gym 框架的 Python 源码把追捕者与逃逸者的博弈封装成标准环境接口同时给出 DQN 基线和 MADDPG 扩展两套算法。它不只是交作业的素材状态空间怎么编排、共享奖励还是个体奖励、距离塑形系数怎么配、训练到哪一步算真正收敛这些单智能体教程里含糊带过的细节在这份代码里都有完整落地的答案。适合正在写强化学习课程设计、毕业设计或者第一次接触多智能体强化学习MARL的开发者直接拿去跑。2. 环境层怎么拆从 gym 接口到追逃博弈的状态与奖励设计2.1 为什么用 gym 而不是自己写 while 循环刚入门时我也干过这事把游戏逻辑和训练逻辑写在一个 while 循环里状态是几个零散变量动作靠 if 判断。改一次地图大小要翻半天代码换算法等于重写。gym 的价值不是多了一个类而是把「环境」和「算法」之间的契约固定下来reset 返回初始观测step 接收动作返回观测、奖励、done、infoobservation_space 和 action_space 描述数据边界。算法层只认这四个接口游戏规则随便改。追逃博弈的特殊之处在于环境里有两类角色追捕者看的是相对坐标逃逸者看的是距离变化两者共用同一个 step但各自的观测和奖励由环境按角色分发。这份代码的环境层是这么起头的import numpy as np import gym from gym import spaces class PursuitEvasionEnv(gym.Env): 多追一逃的网格世界环境接口对齐 gym 规范 def __init__(self, grid_size12, n_pursuers3, n_evaders1, capture_dist1.5, max_steps300): super().__init__() self.grid_size grid_size self.n_pursuers n_pursuers self.n_evaders n_evaders self.capture_dist capture_dist self.max_steps max_steps # obs 自己坐标(2) 全部逃逸者相对坐标 同伴相对坐标 obs_dim 2 * (1 n_evaders n_pursuers - 1) self.observation_space spaces.Box( low-grid_size, highgrid_size, shape(obs_dim,), dtypenp.float32) # 0 原地等待1-4 上下左右移动 self.action_space spaces.Discrete(5) self.step_count 0这里的 obs_dim 是动态算出来的你加一个逃逸者向量自动变长训练脚本完全不用动。Box 的 low 和 high 设成地图边长限制的是数值边界后面喂给网络之前还要再归一化一次后面讲。action_space 用 Discrete(5) 而不是 Discrete(4)是因为原地等待在合围场景里很重要——追捕者不总是需要移动学会「堵位置」比学会「追直线」高级得多。2.2 观测空间与动作空间先定形状再写代码观测用什么形式直接决定算法能不能收敛。这份代码选的是全局相对坐标每个追捕者拿到的是「自己的绝对坐标 所有其他智能体的相对坐标」。相对坐标比绝对坐标好在平移不变性——网络不需要记住地图坐标系换个出生点照样工作。你要是拿绝对坐标训练换一张地图或改一下 reset 的出生范围模型的表现立刻崩。如果地图做得更大或者想模拟真实场景里的局部感知也有人会把观测换成局部视野栅格比如周围 5×5 的 0/1 占用图再用卷积网络处理。栅格观测的地图泛化性好但观测维度高、训练慢课程设计里没必要一上来就上卷积。三种方案的取舍如下观测方案维度动作空间适合场景收敛难度全局相对坐标2*(1n_en_p-1)Discrete(5)小地图、课程设计低局部视野栅格2kkDiscrete(5)大地图、部分可观测研究高全局坐标速度4nBox(-1,1,(2,))连续动作博弈中动作空间同理网格地图用离散动作连续平面上用 Box 控制速度和方向。选题是课程设计的话建议别碰连续动作MADDPG 的连续版本要在 actor 输出层加 tanh还要配合 OU 噪声做探索调参成本直接翻倍不是加分项而是劝退项。这里还有一个我踩过的细节相对坐标范围是 [-grid_size, grid_size]直接喂网络数值跨度太大收敛很慢。我在 _get_obs 里会把每个相对坐标除以 grid_size归一化到 [-1, 1] 再返回。也就是说 observation_space 里声明的是边界范围实际输入是归一化后的值——space 声明的是「合法边界」不是「输入值域」这两个概念别混。2.3 奖励函数稀疏奖励和距离塑形的取舍追逃博弈最经典的坑是「追捕者前期完全拿不到奖励」。只给捕获奖励10×10 的地图里三个追捕者纯靠随机探索几百轮可能一次都抓不到梯度根本传不回去。所以项目在稀疏奖励之外加了一层距离塑形追捕者每步拿「离逃逸者的最近距离缩短量」作为即时奖励捕获成功再叠加大额奖励。逃逸者的奖励是反过来它会想尽办法把和追捕者的最近距离拉大。这里有平衡问题——双方奖励系数差太多弱的一方永远学不动结果就是要么逃逸者出生即被围要么追捕者永远追不上。项目里一般把逃逸者的距离权重设成追捕者的 1.2 到 1.5 倍用速度系数补偿数量劣势。step 函数里奖励是这么算的def step(self, actions): self.step_count 1 self._apply_actions(actions) dists self._pairwise_distances() # (n_p, n_e) 距离矩阵 min_dists dists.min(axis0) # 每个逃逸者离最近追捕者多远 dist_delta self.prev_min_dists - min_dists reward_p dist_delta * 1.0 (min_dists self.capture_dist) * 10.0 reward_e (-dist_delta) * 1.5 - (min_dists self.capture_dist) * 10.0 done bool((min_dists self.capture_dist).any()) \ or self.step_count self.max_steps self.prev_min_dists min_dists.copy() return self._get_obs(), {pursuer: reward_p, evader: reward_e}, done, {}reward_p 左边的 dist_delta 是塑形项右边是捕获大奖 10.0比例大概是 1:10。塑形项的幅度绝对不能盖过主奖励否则智能体会发现「原地抖动也能靠距离变化刷分」然后彻底摆烂。reward_e 用 -dist_delta让逃逸者把「拉大距离」当成直接目标。done 的判断用.any()而不是.all()——只要有一个逃逸者被抓这一局就该结束写错的话整局会拖满 max_steps训练效率差一个数量级。step 的 reward 返回一个字典按角色分发算法层按 key 取就行。注意改奖励函数后必须从头重训不存在微调续训的便宜事。3. 算法层怎么选从 DQN 到 MADDPG 的落地路径3.1 单智能体基线每个追捕者一个 DQN环境写完之后先别直接上多智能体算法。我的习惯是先跑通一个基线每个追捕者独立持有一个 DQN把其他智能体都当成环境的一部分。这样有两个好处一是验证环境接口没写错二是给后续 MADDPG 提供一个对比下限——如果高级算法的表现还不如三个独立 DQN说明是实现的问题不是算法的问题。DQN 的核心组件就四样一个 eval 网络算 Q 值一个 target 网络延迟同步一个 replay buffer 打破样本相关性一个 epsilon-greedy 控制探索。训练更新这一段是整个项目里最难写对的地方def dqn_update(agent, batch): state torch.FloatTensor(batch[state]) action torch.LongTensor(batch[action]).unsqueeze(1) reward torch.FloatTensor(batch[reward]).unsqueeze(1) next_state torch.FloatTensor(batch[next_state]) done torch.FloatTensor(batch[done]).unsqueeze(1) q_value agent.q_net(state).gather(1, action) # 取本次动作对应的 Q with torch.no_grad(): max_next agent.target_net(next_state).max(1, keepdimTrue)[0] target reward agent.gamma * max_next * (1 - done) loss F.mse_loss(q_value, target) agent.optimizer.zero_grad() loss.backward() agent.optimizer.step()gather 是按 batch 里每条样本实际执行的动作把对应的 Q 值捞出来只更新这个动作的 Q。target 计算里的(1 - done)是关键的掩码——终止状态后面没有未来收益不乘这个掩码Q 值会被系统性高估训练后期会莫名炸掉。target 网络不参与梯度计算每几百步用 eval 网络的参数软更新一次。三个追捕者各持一个这样的 DQN经验可以共享网络必须独立。注意一个坑三个网络初始权重必须不同否则对称初始化会让所有追捕者学出完全一样的策略三个人追同一条线合围永远不可能发生。下面 5.4 节这条坑我再展开讲。3.2 MADDPG集中训练、分布执行的真正多智能体解法三个独立 DQN 的局限很明显追逃是零和对抗环境追捕者的观测里同伴在动、逃逸者在学整个环境对某个追捕者来说是非平稳的。DQN 的经验回放假设环境分布稳定这在多智能体场景下不成立——这也是为什么三个 DQN 练到后期会原地踏步。MADDPG 的思路是集中训练、分布执行actor 只用自己的局部观测选动作critic 在训练时拿到所有智能体的观测和动作拼接成全局状态来打分。每个追捕者不知道同伴的策略但 critic 知道梯度就能绕过非平稳性。核心更新代码def maddpg_update(agents, replay_buffer, batch): obs_all torch.cat([batch[obs][i] for i in range(len(agents))], dim1) act_all torch.cat([batch[action][i] for i in range(len(agents))], dim1) for i, agent in enumerate(agents): # 计算 next Q 时所有智能体的 next action 都要用目标 actor 生成 next_act [a.target_actor(batch[next_obs][j]) for j, a in enumerate(agents)] next_q_in torch.cat( [torch.cat([batch[next_obs][j] for j in range(len(agents))], dim1), torch.cat(next_act, dim1)], dim1) target_q batch[reward][i] agent.gamma * \ agent.target_critic(next_q_in) * (1 - batch[done][i]) q agent.critic(obs_all, act_all) critic_loss F.mse_loss(q, target_q.detach()) agent.critic_optimizer.zero_grad() critic_loss.backward() agent.critic_optimizer.step()这段和 DQN 最大的差别在 next_act 的拼接算 next Q 时一个智能体都不能漏漏掉一个全局状态就不完整critic 学到的价值是偏的。另一个麻烦是离散动作——MADDPG 原版是连续动作如果坚持用 Discrete(5)actor 输出要先过 Gumbel-Softmax或者干脆把动作放宽成连续值再用 argmax 离散化。课程设计阶段我建议先把 DQN 基线跑通并拿到能看的指标MADDPG 作为进阶加分项来调别一上来就两个算法一起debug。3.3 训练循环与超参数清单两份算法的外层训练循环是一样的reset 拿初始观测循环 step攒经验定期更新。差异只在 agent 内部。统一的主循环大致是for episode in range(total_episodes): obs env.reset() done False while not done: actions [agent.choose_action(agent_obs, epsilon) for agent, agent_obs in zip(agents, obs[pursuer])] next_obs, reward, done, _ env.step(actions) for i, agent in enumerate(agents): agent.store_transition(obs[pursuer][i], actions[i], reward[pursuer][i], done) obs next_obs if replay_buffer.size() batch_size: maddpg_update(agents, replay_buffer) if episode % 500 0: for agent in agents: agent.soft_update(tau0.01)这里面最容易翻车的是 epsilon 的退火节奏前 20% 的 episode 让它从 1.0 线性降到 0.1后面保持小探索。退太快智能体没来得及探索到合围路径退太慢策略不够 greedy训练日志指标虚高。常用超参可以直接照抄参数推荐值说明learning_rate1e-3critic 可降到 1e-4更稳gamma0.95单局最多 300 步0.99 没必要epsilon1.0 - 0.1前 20% episode 线性退火buffer_size50000网格地图经验多样性有限batch_size12864 也能跑128 更稳soft_update tau0.01每 500 episode 同步一次多智能体训练某种程度上是门玄学但超参数翻车造成的假象比算法翻车多得多。两个算法用同一套参数跑一个收敛一个发散先怀疑实现差异再怀疑参数。4. 源码包怎么跑目录结构、入口脚本与自定义参数4.1 目录结构与模块职责拿到这份源码先别急着跑 train.py。花十分钟过一遍目录弄清楚每个文件是干什么的后面排错至少省半天。这类多智能体平台项目的目录结构大多是下面这个样子pursuit_evasion_marl/ ├── envs/ │ ├── __init__.py │ └── pursuit_evasion_env.py ├── agents/ │ ├── base_agent.py │ ├── dqn_agent.py │ └── maddpg_agent.py ├── config.py ├── train.py ├── evaluate.py ├── requirements.txt └── 运行说明.md各文件的职责如下表文件职责你该关注的点envs/pursuit_evasion_env.py游戏逻辑、观测/奖励/done 计算改地图尺寸、改人数都在这里agents/dqn_agent.py单智能体基线训练更新验证环境正确性的第一站agents/maddpg_agent.py集中训练分布执行加分项最后的难关config.py全部超参与路径配置训练前先改这里train.py训练主循环、模型保存输出 models/ 下的权重evaluate.py加载权重、跑评估指标判断收敛与否的唯一标准容易忽略的一点agents 目录里几乎一定有个 base_agent.py它定义 choose_action、store_transition、save_model 这些公共接口。如果你后面想自己加一个 PPO 或者 SAC继承这个基类会省非常多事——这也是这份代码能叫「平台」而不是「一次性脚本」的关键设计。接手别人的代码时先看基类比先看具体算法更高效。4.2 三步跑通训练环境配置是第一个坎。requirements.txt 里的依赖尽量用 conda 装别直接用最新版 gym。我一般这么建环境、装依赖、跑训练conda create -n marl python3.9 -y conda activate marl pip install -r requirements.txt python train.py --algo dqn --episodes 2000 --save-dir ./models python evaluate.py --algo dqn --model-path ./models/dqn_best.pth --render前两步是重建干净环境第三条装依赖后面两条分别是训练和评估。train.py 的 --episodes 决定训练轮数课程设计里 2000 轮 DQN 在小地图上足够看到明显的收敛趋势--save-dir 是模型输出目录训练过程会自动挑表现最好的 checkpoint 存成 *_best.pth。evaluate.py 的 --render 打开可视化窗口你能直接看到追捕者和逃逸者在地图上的运动轨迹——这是判断「是不是真学会了」最直观的手段比盯 loss 曲线诚实得多。如果 train.py 跑起来一直报 import 错误八成是 gym 版本不兼容两成是 Python 版本和 torch 对不上。先把 conda 环境按上面命令重建一遍再跑一次环境配置问题大多能消掉。我习惯装 gym 0.21.x 这种和老接口兼容的版本盯着 requirements.txt 锁好的版本来别手滑升级成最新版。提示改完 config.py 里的任何环境参数都要从头重训之前存的模型权重不能继续用。4.3 想改地图和人数改哪几个参数课程设计被问得最多的问题就是「能不能换个场景、换个难度」。答案是在 config.py 里改参数就行不用动算法代码# config.py ENV_PARAMS { grid_size: 12, # 地图边长调大训练明显变慢 n_pursuers: 3, # 追捕者数量 n_evaders: 1, # 逃逸者数量 capture_dist: 1.5, # 判定捕获的最近距离 max_steps: 300, # 单局最大步数 }这里有个必须记住的联动n_evaders 或 n_pursuers 一改2.1 节里说的 obs_dim 会自动变但已经训练好的模型权重输入维度已经固定直接加载会报尺寸不匹配。所以「改参数 → 重新训练」是铁律。capture_dist 建议保持在网格边长的 1/8 到 1/10太大变成出生即捕获太小训练难度陡增。另外要注意把 grid_size 调大以后逃逸者会慢慢学会往角落钻——这不是 bug是奖励函数的边界效应角落能把追捕者的逼近方向限制在一侧逃逸者用距离塑形刷到正收益的策略是自然涌现。想避免这种玩法在 reset 时把逃逸者出生点限定在地图中央区域或者给奖励函数加一个「靠近边界扣分」的项都能缓解。5. 避坑记录版本冲突、奖励翻车与评估虚高的五个现场这一章是我反复跑这套代码攒下来的血泪经验。每条都是真实发生过的翻车现场按「现象 → 原因 → 解决」写遇到类似问题直接照方抓药。5.1 gym 版本升级导致环境 import 直接崩现象跑 train.py 直接报错要么是 unpacking a sequence of size 5要么是 gym.make 找不到注册的环境。原因gym 在 0.26 之后把 step 的返回值从 4 个改成 5 个新增了 terminated 和 truncated 两个布尔量同时环境注册机制也有改动。requirements.txt 如果没锁死版本直接 pip install gym 装到新版和源码里的旧式接口就对不上。解决锁版本。我一般装 gym 0.21.x这是和这套代码兼容性最好的搭配。如果非要留在新版 gym环境类里的 step 要改成返回五元组训练循环里也要按 (obs, reward, terminated, truncated, info) 解包改动面很大课程设计阶段完全不值得。5.2 距离塑形系数过大训练曲线像心电图现象reward 曲线每几十轮就大起大落训练几千轮后平均奖励还在零附近晃一看就是没学会。原因塑形项的系数太大每步距离变化的奖励和捕获大奖已经处于同一量级。智能体发现「靠近一步刷个 3 分」比「费半天劲抓到刷 10 分」更划算于是策略变成追着逃逸者反复横跳永远不完成真正合围。解决把塑形系数压到主奖励的 1/10 以下。项目默认的 1.0 配 10.0 就是这个比例。你自己改 reward 时记住一条经验线主奖励至少是每步塑形奖励的 8 到 10 倍。改完系数必须从头重训曲线才会从「毛刺多」变成「整体下行后收敛」。5.3 done 条件用 all() 判断episode 永远拖满 300 步现象训练日志里平均 episode 长度稳定在 max_stepsloss 下降也正常但评估时捕获率是零。原因多逃逸者场景下用dists.min(axis0) capture_dist再调.all()只要有一个逃逸者没被抓到done 就一直是 False整局被 max_steps 截断。结果训练样本里绝大多数是「没抓到」的长序列智能体学会了在 300 步内周旋而不是真正完成捕获。解决判断改成.any()任何一个逃逸者被捕获立刻结束本局。同时建议在 info 里记一下是谁被抓、第几步抓到的评估时能分开统计。写完 done 判断最好单独写个小脚本随机撒几个初始位置跑一局手动确认 done 的触发时机对不对再开正式训练。5.4 共享网络导致三个追捕者策略完全同化现象三个追捕者的权重文件大小一模一样运动轨迹高度重合三个人永远追在同一条线上逃逸者轻松溜走。原因三个 agent 要么共用了同一个 Q 网络实例要么各自建了网络但用了相同权重初始化。对称博弈下从相同初值出发、吃相同分布的经验梯度方向也相同结果就是学出一模一样的策略。合围需要差异化三个人都抢同一条追击路线合围就永远不存在。解决每个 agent 独立创建网络并且用不同的随机种子初始化。更稳妥的是给三个追捕者在 reset 时分配不同的初始位置区域让探索经验天然有差异。代码里对每个 agent 单独调一遍 torch.manual_seed 即可这类问题立刻消失。5.5 评估时忘了关探索指标虚高现象训练日志最后一轮平均奖励已经不错把模型单独拿出来跑 100 局捕获率只有训练时的一半。原因训练用的是 epsilon-greedy日志里的奖励是在带探索噪声的条件下统计的评估时如果继续沿用训练的选动作函数epsilon 没置零随机动作拖低了真实水平。更隐蔽的翻车是评估脚本忘了加载权重直接拿随机初始化的网络在跑。解决evaluate.py 里强制把 epsilon 设成 0.0选动作只走 argmax 分支加载模型前打印一遍网络参数的均值或校验值确认和训练保存的一致。评估至少跑 100 局再算平均捕获率50 局以内波动太大结论不可信。6. 验证与调参习惯用分场景评估判断智能体是不是真学会6.1 训练曲线别只看奖励要看捕获率训练日志里的平均奖励是「感觉指标」它受探索噪声和 reward 尺度影响很大曲线向下不代表学坏向上也不代表学会。真正能用来验收的是捕获率用贪心策略跑 100 局统计成功捕获的局数占比。这个数字超过 80%基本可以认定策略可用低于 50%说明训练还没到位别急着换模型。6.2 分场景评估与可视化回放evaluate.py 的核心逻辑其实很短success 0 for _ in range(100): obs env.reset() while True: actions [a.greedy_action(o) for a, o in zip(agents, obs[pursuer])] obs, reward, done, _ env.step(actions) if done: success int(env.captured_any) break print(fcapture_rate {success / 100:.2f})greedy_action 是选动作时固定走 argmax 分支env.captured_any 是环境里记录本局是否完成捕获的标志位。如果捕获率达标但回放时发现逃逸者总往同一个死角跑那是边界 reward 的产物可以在 reset 里限制出生点属于调参玄学范畴不影响主线结论。从那以后我每次跑完一套多智能体训练都强制自己走一遍「100 局 greedy 评估 → 分场景统计 → 可视化回放」的验收流程缺一步都不算通过。这份源码里的 train.py 和 evaluate.py 就是按这个流程配好的你拿到后直接改 config 里的参数就能复现出训练曲线和捕获率指标。希望帮到你。本文还有配套的精品资源点击获取
返回列表