ARTICLE DETAIL

资讯详情

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

多智能体协同围捕Python源码解析:Voronoi与MADDPG实践

多智能体协同围捕Python源码解析:Voronoi与MADDPG实践 简介面向计算机专业课程设计与期末大作业的高分项目提供多智能体协同围捕算法在多种仿真环境下的Python实现与项目说明适合正在做课程设计、期末大作业或需要项目实战练习的学习者。内容覆盖单出口、多出口、凸环境、封闭环境等典型围捕场景分别对应独立策略脚本算法层面集成Voronoi区域划分与MADDPG等方法并附Gazebo/Unicycle类仿真测试入口、共线判断等辅助工具可直接运行验证围捕效果。压缩包共14个文件13个.py源码文件用于不同场景的策略实现、仿真测试与工具函数1个README.md说明文档梳理代码结构、环境依赖和运行步骤整体仅45KB轻量易部署便于快速修改地图与参数开展对比实验。目前已有173人学习下载项目经导师指导并获98分认可代码组织清晰、场景划分明确既可直接作为课程作业基础也可用于算法对比与扩展实验。1. 多智能体协同围捕这份 Python 源码包到底解决了什么围捕问题在机器人竞赛和课程设计里出镜率极高一群追击者要在未知或已知地图里拦住一个逃逸目标。难点从来不是单体的追击算法而是多个追击者之间怎么不撞车、不重复覆盖、不互相扯后腿。这份源码包把问题拆成了几何决策和强化学习两条路线文件命名直接对应场景——单出口、多出口、凸环境、Gazebo 仿真、Husky 机器人模型。比较实用的一点是它不是只给一个跑通的 demo而是把 Voronoi 剖分、MADDPG 训练、运动学模型全部串在一个工程里适合正在做课程设计、期末大作业的学生直接拿来做 baseline也适合想研究围捕策略但不想从零搭环境的从业者做二次开发。单出口环境里围捕可以简化为一个分区问题Voronoi 剖分把地图划给最近的追击者让每个追兵只负责自己区域内的逃逸者多出口环境则退化成任务分配问题谁离出口近谁去堵门。源码里voronoi_single_exit.py、multi_multi_exit.py、voronoi_convex_env.py这几个文件分别对应不同难度。仿真层用 unicycle 运动模型简化动力学同时保留pygazebo_unicycle_test.py做 Gazebo 接入方便后续迁移到真实机器人平台。我在复现这个包的时候发现真正值得花时间的不是算法本身而是搞清楚每个文件在流程里负责哪一段。2. 围捕问题的几何解法Voronoi 剖分与多出口任务分配2.1 为什么围捕首选 Voronoi 剖分而不是单纯追踪围捕和单纯追逐的本质区别在于追逐只有一个目标点围捕要的是“拦截区间”。追击者速度通常和逃逸者相当直接从后面追大概率追不上正确做法是预测逃逸者可能的逃跑方向提前到路径上堵截。Voronoi 剖分在这里的价值是它天然解决了两个问题追击者之间的空间划分和逃逸者的归属判定。每个追击者根据当前位置生成一个 Voronoi 单元逃逸者落在哪个单元里就由该单元的追击者负责拦截其他追击者继续向逃逸者的预测位置移动形成包围网而不过度集中。源码里voronoi_API.py封装了剖分接口我拆开看了下它的设计思路核心是把 SciPy 的Voronoi类包了一层输出每个追击者的多边形区域和当前逃逸者所在区域编号。这样做的好处是上层策略不需要关心几何计算的细节只需要查 API 拿结果。2.2 voronoi_single_exit.py单出口场景的完整实现拆解单出口场景是最经典的围捕环境地图边界只有一个缺口逃逸者最终必须从出口离开追击者要做的是在逃逸者到达出口之前形成包围圈。voronoi_single_exit.py的结构大致是环境初始化、Voronoi 剖分更新、追击者控制律、围捕完成判定四段。控制律用的是“向逃逸者所在 Voronoi 区域边界移动”的策略而不是直接追逃逸者当前位置。# voronoi_single_exit.py 核心逻辑简化版 def update_voronoi_regions(pursuers, evader): # pursuers: 追击者位置列表 [[x1, y1], [x2, y2], ...] # evader: 逃逸者当前位置 [x, y] points np.array(pursuers [evader]) vor Voronoi(points) # 找到逃逸者属于哪个追击者的 Voronoi 单元 evader_region vor.point_region[-1] # 遍历所有追击者判断哪个的 region 包含 evader_region for i, region_idx in enumerate(vor.point_region[:-1]): if region_idx evader_region: pursuer_idx i break # 返回每个追击者应该移动到的目标点 targets [] for i in range(len(pursuers)): if i pursuer_idx: # 负责追击的单元向逃逸者当前位置移动 targets.append(evader) else: # 其他单元向逃逸者可能逃跑的方向移动形成包围 targets.append(predict_intercept_point(pursuers[i], evader)) return targets这段代码里Voronoi(points)把追击者和逃逸者全部丢进去做剖分point_region数组记录每个输入点对应的区域编号。判断逃逸者落在哪个单元的逻辑是用区域编号匹配比较直接。这里有个细节注意一下实际项目中追击者的目标点不是直接设成逃逸者位置而是加了一个预测偏移量predict_intercept_point这个函数内部一般会用到逃逸者的速度方向如果逃逸者是匀速直线运动直接沿速度方向外推一个单位时间即可。参数方面追击者数量建议在 3 到 6 个之间。少于 3 个形不成包围网多于 6 个在狭小地图里 Voronoi 单元会过度碎片化每个追击者分到的区域太小反而出现频繁换目标的问题。2.3 multi_multi_exit.py 与 voronoi_convex_env.py多出口是任务分配而非几何剖分多出口场景比单出口复杂一个量级。逃逸者有多个可选的逃跑路线追击者如果平均分布在地图里每个出口的防守力量都不足如果集中守一个出口逃逸者从另一个出口溜走。multi_multi_exit.py的解决思路是动态优先级分配每个追击者计算自己到各个出口的时间成本然后按最小成本原则分配出口同时保留一个机动追击者不参与固定防守而是追在逃逸者身后施压把逃逸者往防守最薄弱但已经被封堵的出口方向赶。# multi_multi_exit.py 出口分配伪代码 def assign_exits(pursuers, exits, evader_pos): # exits: 出口位置列表 [[x1, y1], [x2, y2], ...] assignments {} unassigned list(range(len(pursuers))) for exit_idx, exit_pos in enumerate(exits): # 计算每个未分配追击者到该出口的距离 distances [] for p_idx in unassigned: dist np.linalg.norm(pursuers[p_idx] - exit_pos) # 加上一个惩罚项如果追击者离逃逸者太远防守该出口的优先级降低 evader_dist np.linalg.norm(evader_pos - exit_pos) adjusted dist 0.3 * evader_dist distances.append((adjusted, p_idx)) distances.sort() # 取距离最小的追击者防守该出口 _, best_pursuer distances[0] assignments[exit_idx] best_pursuer unassigned.remove(best_pursuer) return assignments这段分配逻辑里有个值得学的细节距离计算叠加了0.3 * evader_dist这个惩罚项。如果逃逸者已经离某个出口很近追击者即使离出口远也得优先过去补防因为围捕的时间窗比路径成本更关键。0.3 这个权重系数不是拍脑袋定的原作者在 README 里提到这个值是拿实际场景试出来的权重太大追击者会在地图里乱窜太小则逃逸者到出口时防守还没就位。voronoi_convex_env.py则是凸环境下的围捕测试地图是凸多边形没有内部障碍物Voronoi 剖分不会产生退化单元可以当作干净的算法验证环境。我一般建议第一次跑这个包的读者从这个文件入手因为它不需要处理边缘碰撞和剖分退化逻辑链路最清晰。3. 运动模型与仿真层unicycle 模型到 Gazebo 的接入3.1 unicycle_test.py单周期运动模型的离散实现源码包把运动学层单独拆成了unicycle_test.py。unicycle 模型也叫独轮车模型状态量只有位置和朝向(x, y, theta)控制量是线速度v和角速度omega。这个模型是移动机器人最常用的简化差速驱动机器人比如两轮底盘经过简单变换就可以当成 unicycle 用所以源码里用它来做 2D 仿真非常合适。# unicycle_test.py 运动学更新 def unicycle_step(state, v, omega, dt): state: [x, y, theta] 当前位置和朝向 v: 线速度 m/s omega: 角速度 rad/s dt: 时间步长 s x, y, theta state x_next x v * np.cos(theta) * dt y_next y v * np.sin(theta) * dt theta_next theta omega * dt return np.array([x_next, y_next, theta_next])dt的选择直接影响仿真稳定性和真实性。源码里默认dt0.1即 10Hz 控制频率这个频率对于室内移动机器人是合理的下限。如果你只是跑算法验证dt0.05更快更稳但也意味着每个仿真回合重复计算更多次。我实际跑下来发现如果dt调到0.2以上追击者会出现明显的运动轨迹抖动因为角速度控制跟不上 Voronoi 区域更新的频率追击者会在两个区域的边界来回振荡——这个现象后面避坑章节还会提到。3.2 pygazebo_unicycle_test.py 与 husky_voronoi.py从 2D 仿真到机器人平台pygazebo_unicycle_test.py是 Gazebo 仿真的入口文件。它做的事情不是重新实现一套运动学而是把 unicycle 的输出转换成 Gazebo 里机器人的速度指令。源码里用的转换方式本质上是 unicycle 到差速模型的映射左轮速度等于v - omega * L/2右轮速度等于v omega * L/2其中L是轮距。如果你用的是 Husky 这类四轮机器人husky_voronoi.py里做了额外的轮速分配但核心思路一致。Gazebo 仿真的最大价值不是视觉上的“像不像真车”而是引入了 2D 仿真里没有的物理约束加速度限制、轮子打滑、地面摩擦不一致。这些约束对围捕算法的影响是致命的——2D 场景里追击者说加速就加速Gazebo 里电机有响应时间追击者的实际速度曲线是滞后的。这个滞后意味着 Voronoi 剖分更新的频率必须低于控制频率否则追击者会一直追着自己“应该去的位置”跑永远追不上。# pygazebo_unicycle_test.py 速度指令转换 def publish_speed_command(pub, state, target_pos, K_p0.8, K_w2.0, max_v0.5): # state: [x, y, theta] # target_pos: 目标位置 [x_t, y_t] dx target_pos[0] - state[0] dy target_pos[1] - state[1] # 计算期望线速度距离越近速度越小避免过冲 dist np.sqrt(dx**2 dy**2) v K_p * dist v min(v, max_v) # 线速度限幅 # 计算期望角速度角度差映射到 [-pi, pi] target_theta np.arctan2(dy, dx) angle_diff target_theta - state[2] angle_diff np.arctan2(np.sin(angle_diff), np.cos(angle_diff)) omega K_w * angle_diff # 转换成左右轮速 wheel_base 0.5 # 单位米Husky 轮距约 0.5m left_wheel v - omega * wheel_base / 2.0 right_wheel v omega * wheel_base / 2.0 return left_wheel, right_wheelK_p和K_w分别控制线速度和角速度增益。K_p0.8意味着距离 1 米时期望速度是0.8 m/sK_w2.0表示 1 弧度角度差对应2.0 rad/s角速度。这两个参数的整定逻辑是K_p太大追击者会冲过头K_w太小转向慢导致绕路。源码在 Gazebo 跑的时候用了个小技巧——把K_p适当调小0.6左右因为物理引擎里存在惯性控制器如果再激进机器人会在地上画圈。4. MADDPG 强化学习层从规则策略到学习策略4.1 maddpg 目录里的算法结构与围捕任务的适配源码包里的maddpg目录是整个项目里最有分量的部分。MADDPGMulti-Agent Deep Deterministic Policy Gradient是目前多智能体强化学习里最常用的算法之一核心思想是“集中训练、分散执行”。训练阶段每个智能体的 Critic 网络能看到所有智能体的状态和动作用来评估全局好坏执行阶段每个智能体的 Actor 网络只看自己的局部观测。围捕任务对 MADDPG 是个天然适配的场景因为追击者之间的配合很重要——一个追击者的动作会改变其他追击者的 Voronoi 区域边界。如果用独立 DDPG 训练每个智能体把其他追击者当成环境噪声训练基本不收敛MADDPG 的全局 Critic 恰恰解决了这个问题。# maddpg/agent.py 训练逻辑核心 def train_step(agent, replay_buffer, all_actors, all_critics): # 从回放池采样 batch replay_buffer.sample() # 计算每个智能体的目标动作 next_actions [] for i, actor in enumerate(all_actors): next_actions.append(actor(batch.obs_next[i])) # 用全局 Critic 计算目标 Q 值 target_q all_critics.target(batch.obs_next_all, next_actions) # 当前 Q 值 current_q agent.critic(batch.obs_all, batch.actions_all) # Critic loss: 做时序差分更新 critic_loss MSE(current_q, batch.reward gamma * target_q) # Actor loss: 最大化当前策略下的 Q 值 actor_loss -agent.critic(batch.obs_all, agent.actor(batch.obs_i)).mean() return critic_loss, actor_loss我这里只写了单智能体的训练步实际maddpg目录里的代码会在这个基础上迭代多次因为每个智能体的 Actor 和 Critic 都要更新而且目标网络的软更新参数tau设置会影响训练稳定性。源码里tau0.01这是大多数 MADDPG 实现的默认值软更新意味着目标网络的参数每次只向当前网络移动 1%数学上保证训练过程平稳。4.2 强化学习与 Voronoi 剖分的结合方式这个项目值得学习的地方是把 MADDPG 用在了 Voronoi 剖分之上而不是让它从零学围捕。具体做法是Voronoi 剖分先给出每个追击者应该负责的方向区间MADDPG 的输出不是“往哪个方向走”而是“在多用力地往那个方向走”。这样做的工程意义是大幅缩小探索空间——如果让智能体直接学坐标输出动作空间是连续的二维平面收敛极慢加上 Voronoi 约束后动作空间退化成“朝向给定目标点运动的速度大小”变成一个一维控制问题。# maddpg 动作与 Voronoi 区域结合 def get_action(agent, obs, voronoi_target): # obs: 智能体局部观测包含自身位置和速度 # voronoi_target: Voronoi 剖分给出的目标方向 (dx, dy) norm_target voronoi_target / np.linalg.norm(voronoi_target) # 网络输出一个标量控制量 scale范围 [-1, 1] scale agent.actor(obs) # 实际动作 方向 * 控制量 action norm_target * scale return action这里的scale输出范围是[-1, 1]实际执行时映射到[0, v_max]。负值意味着智能体学到“这个方向不该去”这在围捕里是有意义的——追击者如果发现自己所在 Voronoi 区域里根本没有逃逸者最好的策略是原地等待而不是乱冲。这个设计比较精妙因为它让强化学习只负责做加减速判断方向和路径规划完全交给几何方法解决。4.3 训练调参的实践记录reward 设计与收敛判定MADDPG 在围捕任务里最重要的超参数不是网络结构而是 reward 函数的设计。源码里对追击者的 reward 分成了四项到达围捕位置的正奖励、与逃逸者距离缩小的正奖励、与其他追击者碰撞的负奖励、每次决策步的时间惩罚。这种分层设计的思路值得借鉴——单个奖励信号表达不了“既要形成包围又要保持距离”的复合目标。我复现的时候第一版只用了距离奖励结果追击者全部挤成一团全部堆在逃逸者身边——它们发现缩短距离能拿高奖励但没有机制惩罚互相重叠。加上碰撞负奖励后追击者学会了保持间距但这个间距到底多大才合适纯靠训练收敛去摸索。源码里设置的碰撞距离阈值是0.3m低于这个距离触发惩罚。这个阈值对应 Voronoi 剖分的物理意义追击者之间的最小安全距离不能小于运动模型的转弯半径。训练是否收敛不要只看 loss 曲线。MADDPG 的 actor loss 在围捕任务里通常不会降到很低因为智能体之间在互相竞争——一个追击者的位置会让另一个追击者的 Q 值偏高或偏低。这个项目的 README 建议看一个更实际的指标围捕成功率。跑 100 个随机初始化的回合统计逃逸者被围住的比例成功率超过 80% 才算训练真正完成。这个验证思路比看 reward 曲线更靠谱因为 reward 数值大小高度依赖你设的权重。5. 避坑与常见问题排查从几何退化到训练不收敛的五个实测踩坑记录5.1 追击者数量过少时 Voronoi 区域退化现象追击者 2 个、逃逸者 1 个时voronoi_convex_env.py跑起来逃逸者轻松从两个追击者中间穿过去围捕成功率直接归零。原因两个点到一条直线的 Voronoi 剖分退化成了两个半平面分界线是两点连线的垂直平分线。逃逸者只要沿着这条分界线走两个追击者永远把它当成“对方的责任区域”形成互相推诿的僵局。解决追击者数量至少 3 个且初始位置不要大致分布在一条直线上。我一般会在初始化函数里加一个最小夹角判定任何三个追击者的位置构成三角形面积低于某个阈值就重新随机生成。5.2 逃逸者与追击者位置共线导致 judge_collinear.py 报错现象运行judge_collinear.py时某些随机种子下直接抛异常或者输出错误的判定结果。原因当逃逸者和两个追击者恰好共线时向量叉积为零判断“是否在安全区域内”的逻辑出现边界条件溢出。源码里的judge_collinear.py本质是判断逃逸者的位置是否处于两个追击者的夹角之间夹角为零时无意义。解决给叉积结果加一个极小阈值epsilon1e-6把“完全共线”的情况当作“不在夹角内”处理。更稳妥的做法是在位置更新后主动加一个小扰动避免数值上严格共线。5.3 Gazebo 仿真中追击者原地打转现象pygazebo_unicycle_test.py在 Gazebo 里跑追击者到达目标点附近后不停原地旋转姿态在 180 度和 -180 度之间反复跳变。原因角度差计算时用了np.arctan2返回[-pi, pi]区间控制器对角度做比例控制角度在边界处跳变导致角速度指令不断翻转。2D 仿真里没有这个问题因为纯几何计算对角度连续性不敏感但物理引擎里角速度指令是有符号的跳变会让机器人物理上反复转向。解决把角度差映射到[-pi, pi]之后再判断如果目标点距离小于某个阈值比如0.05m直接把角速度置零只保留一个很小的线速度做位置微调。5.4 MADDPG 训练不收敛reward 震荡剧烈现象maddpg目录里的训练脚本跑了 2 万步reward 还是忽高忽低围捕成功率几乎为零。原因大概率是回放缓冲区replay buffer太小旧经验被快速覆盖Critic 学不到稳定的全局价值函数。另一个常见原因是学习率设置不当lr1e-2对于 MADDPG 来说偏高参数更新步长太大导致网络权重振荡。解决把回放缓冲区从默认的1e4提到1e6学习率降到1e-3数量级。我实测下来batch_size1024比256更容易让 Critic 稳定因为全局 Q 值的方差本身就大样本量太小估计不准。5.5 多出口场景追击者防守分配震荡现象multi_multi_exit.py里追击者在两个出口之间来回切换目标甚至在一次决策周期内频繁改变防守对象导致整体队形混乱。原因出口分配基于最小距离追击者位置稍微变化就会改变距离排序。特别是当逃逸者位于地图中央时逃逸者到各个出口的距离相近惩罚项0.3 * evader_dist会让追击者的分配结果对位置噪声极其敏感。解决给出口分配加一个“冷却时间”机制每个追击者分配到一个出口后至少在 N 个决策周期内不重新分配。N 一般取 10 到 20这个时间窗口足够追击者移动到防守位置。另外把惩罚项系数从0.3适当提高可以降低震荡频次但要注意别把追击者全吸引到逃逸者同侧。6. 验证方法与进阶从围捕成功率到故障注入测试围捕算法跑通之后检验工作是否到位的唯一标准不是代码能跑而是围捕成功率的统计意义是否成立。我习惯的做法是固定随机种子跑 100 回合统计三类结果围捕成功回合数、逃逸者逃出地图边界回合数、超时回合数。成功率超过 80% 只能算及格90% 以上才符合课程设计“高分项目”的档位。统计时注意把初始位置和逃逸者速度方向的随机性保留那些“全部随机种子都成功”的测试样本价值不大因为说明场景还没覆盖到算法的薄弱区。进阶验证有个思路值得试试故障注入。这个方法不是你想的那种高强度测试而是刻意制造“某个追击者失效”的情况观察剩余智能体能否快速重新分配 Voronoi 区域继续维持围捕网。源码里的 Voronoi 剖分有个天然优势——剖分是动态更新的某个追击者失效后它的区域自动被相邻追击者瓜分不需要显式的协作协议。我在复现时做过一组对比实验4 个追击者训练完成后中途禁用一个围捕成功率从 92% 掉到 61%这个下降幅度说明算法对追击者数量的变化是敏感的但还没有到完全失效的程度。多出口场景做故障注入更有意思失效的追击者守护的出口会瞬间空掉逃逸者会立刻转向那个出口剩余追击者大概率需要 3 到 5 个决策周期才能重新完成出口分配。验证完算法稳定性再看代码级的优化点这个源码包里有两个地方值得按你的需求去改写。一个是judge_collinear.py的判定逻辑它目前的功能是判断逃逸者是否在两个追击者的夹角里这个算法可以扩展成“判断逃逸者是否在三个追击者形成的包围三角形内”——如果逃逸者同时位于三个追击者的 Voronoi 区域顶点构成的凸包内那么从几何意义上围捕已经完成可以直接触发终止条件。另一个是运动模型层unicycle 模型只适合验证算法逻辑如果你后续要做真实机器人部署需要把控制指令的发布频率和轨迹平滑性考虑进去。我一般在真实平台会加一个速度规划层把 unicycle 输出的速度目标平滑成梯形速度曲线避免加速度跳变损坏电机。对于想把这个项目写到简历上的读者我的建议是把默认的围捕完成距离0.2m修改成相对评价指标——追击者与逃逸者距离小于“逃逸者最大速度的一秒行程”这样算法自适应性会强很多。举个例子逃逸者最大速度0.5m/s时围捕完成距离是0.5m如果换场景把逃逸者速度改成1.0m/s完成距离自动变成1.0m不需要手动调参。这个项目拆下来我觉得真正拉开高分和及格分差距的是你能不能把“为什么这样设计”讲清楚而不只是把代码跑通。我从那次以后每次拿到源码包都会强制自己先过一遍文件命名和依赖关系把每个模块在算法流程里的位置画出来再动手改代码这套流程能省掉大量排错时间希望帮到你。本文还有配套的精品资源点击获取
返回列表