简介:这是一份面向计算机专业课程设计、期末大作业及项目实战的多智能体协同围捕算法Python实现,适合需要完成高分毕设,或想系统学习多智能体规划、强化学习与机器人仿真控制的学习者。项目以Voronoi剖分与MADDPG为核心方案,覆盖单出口、多出口、凸多边形环境、多智能体协作围捕等典型实验场景,同时给出单双智能体运动控制、碰撞检测、共线判定、围捕效果可视化等配套代码,可直接运行调试,并结合项目说明与README梳理完整算法流程。资源共14个文件,包含13个Python源码与1个Markdown说明文档,压缩包仅45KB,但模块划分清晰,涉及区域划分、多智能体训练、出口决策、避障判断与Gazebo仿真联动等,便于按需复用和二次开发。目前已有174人学习下载,适合正在完成课程设计、期末大作业或希望积累多智能体协同算法实战经验的中级学习者。
1. 多智能体协同围捕算法:为什么“围得住”比“追得快”更有工程价值
提到多智能体协同围捕算法的 Python 实现,很多人的第一反应是让机器人或者游戏 NPC 去“追”目标。实际做下来你会发现问题恰恰相反:单个智能体追一个会转弯的目标,基本追不上;真正有效的策略是提前在目标四周形成包围圈,用协同把目标的逃逸方向封死。这也是“各种环境下多智能体协同围捕算法的实现 python 源码+项目说明(高分项目)”这类题目在课程设计和毕业设计里常被选中的原因——它有明确的问题定义、可见的仿真效果,又能拆成环境建模、任务分配、运动控制三个独立模块来写项目说明。适合的人群包括正在做多机器人集群仿真、游戏 AI 行为树、无人机围堵任务,或者单纯想拿一套完整源码快速跑通多智能体流程的 Python 开发者。标题里的“各种环境”,落到代码层面其实是四类变化:静态障碍、目标变速、局部观测、多目标。理解了这四类,你就能把一套核心算法扩成多个实验场景。
2. 围捕问题的数学模型:围捕半径、出生点与围捕点分配
2.1 把追逃问题转成包围问题:三个核心参数
在设计围绕围捕算法之前,必须先明确“什么算围捕成功”。单智能体追击里成功条件很常见:追捕者与目标距离小于捕获半径即成功。多智能体围捕则要多加一个约束——不只是距离近,还要保证目标找不到足够大的逃逸间隙。一个可复现的度量方法是:以目标为圆心计算每个智能体的极角,相邻智能体之间形成角度空隙,取最大空隙角作为“逃逸开口”。当目标到每个智能体的距离都小于捕获半径,且最大空隙角小于给定阈值时,判定为围捕成功。
这样一来,算法要调的核心参数就清楚了:捕获半径 R_c、最大允许空隙角 θ_enc、虚拟围捕半径 R_enc。R_enc 是智能体围绕目标展开理想圆周的半径,取值通常比 R_c 大 1.5 到 3 倍。如果 R_enc 太大,包围圈松散,目标很容易从空隙中溜走;如果太小,智能体彼此挤在一起,内部排斥力会让队形扭曲。另一个容易被忽视的速度约束:单智能体要求追捕者速度必须大于目标速度,但围捕场景不严格需要,只要整体速度不比目标慢太多即可,因为围捕点分配会提前截住目标去路。
| 参数 | 默认值 | 建议范围 | 作用 |
|---|---|---|---|
| N | 5 | 3~8 | 参与围捕的智能体数量 |
| v_p / v_e | 1.6 | 1.2~2.5 | 追捕者与目标最大速度比值 |
| R_enc | 4.0 | 2.5~6.0 | 虚拟围捕半径 |
| R_dead | 0.35 | 0.2~0.6 | 排斥力死区半径 |
| dt | 0.03 | 0.01~0.05 | 仿真步长 |
| θ_enc | 60° | 45°~90° | 允许的最大逃逸空隙角 |
2.2 用 Python 搭建仿真环境:障碍、边界与随机种子
我习惯把仿真环境单独抽成一个类,这样后面做“各种环境”的实验就不用反复改动主逻辑。环境负责三件事:生成随机障碍、判定点是否在边界内、维护随机种子。随机种子特别重要,后面调参时如果你不固定种子,每次跑出的障碍布局都不一样,完全没法判断是参数改善还是运气变好。这属于调试时最基础的血泪经验。
环境代码这样写就够用:
import numpy as np class Env2D: def __init__(self, seed=42, size=20.0, obstacle_num=8): self.rng = np.random.default_rng(seed) self.size = size self.obstacle_num = obstacle_num self.obstacles = [] for _ in range(obstacle_num): r = self.rng.uniform(0.6, 1.6) x = self.rng.uniform(-size/2 + 2.5, size/2 - 2.5) y = self.rng.uniform(-size/2 + 2.5, size/2 - 2.5) self.obstacles.append(np.array([x, y, r])) def in_bounds(self, pos): margin = 0.5 return (abs(pos[0]) <= self.size/2 - margin and abs(pos[1]) <= self.size/2 - margin)逻辑说明:障碍生成范围刻意避开边界附近 2.5 个单位,避免障碍与环境边界重叠导致智能体出生即撞墙。in_bounds里的 margin 是边界软约束缓冲带,给后面的边界排斥力留出处理空间。随机种子通过numpy.random.default_rng(seed)创建,比旧式np.random.seed()更适合多实验并行,因为每个环境实例拥有独立的随机流。
2.3 围捕点分配:匈牙利算法与最小化总距离
围捕点分配是“协同”二字的关键。常见做法是以目标当前位置为圆心,在虚拟围捕半径 R_enc 上均匀生成 N 个候选点,再用匈牙利算法把 N 个智能体匹配到 N 个点上,目标是让总移动距离最小。这一步在 Python 里直接调用scipy.optimize.linear_sum_assignment即可,不需要自己写增广路径算法。
from scipy.optimize import linear_sum_assignment def assign_capture_points(agent_positions, target_pos, R_enc): n = len(agent_positions) angles = np.linspace(0.0, 2.0 * np.pi, n, endpoint=False) capture_points = np.array([ [target_pos[0] + R_enc * np.cos(a), target_pos[1] + R_enc * np.sin(a)] for a in angles ]) costs = np.array([ [np.linalg.norm(ag - pt) for pt in capture_points] for ag in agent_positions ]) row_idx, col_idx = linear_sum_assignment(costs) return capture_points[col_idx]逻辑说明:linear_sum_assignment返回的是行索引与列索引的最优配对,capture_points[col_idx]按智能体顺序返回配好的围捕点。这里有一个坑:均匀生成围捕点时,第一个点朝向固定角度 0°,如果目标旁边恰好有障碍,会导致某个围捕点生成在障碍内部。我通常会在分配后加一个检查,把所有距离障碍过近的围捕点整体旋转一个角度重新分配,直到所有点可通行。
3. 多智能体协同围捕算法的核心源码:从控制力到调度循环
3.1 控制力分解:目标吸引力、群体排斥力与边界力
围捕点分配完成后,每个智能体其实都在朝自己的目标点移动。要让队形看起来自然,最简单的控制方案是“势力场法”,也是多智能体系统协同群集运动控制里最经典的做法——把控制量拆成三个力的叠加:朝向围捕点的吸引力、智能体之间的排斥力、环境边界排斥力。这和 Reynolds 集群规则里的“分离、对齐、聚合”本质是同一套思想,只是把聚合目标换成了动态围捕点。
def compute_forces(agent, capture_point, agents, env, cfg): force = np.zeros(2) # 吸引力:指向当前围捕点 to_target = capture_point - agent.pos dist_pt = np.linalg.norm(to_target) if dist_pt > 1e-6: force += cfg["k_att"] * to_target / dist_pt # 排斥力:智能体之间保持安全距离 for other in agents: if other.id == agent.id: continue diff = agent.pos - other.pos dist = np.linalg.norm(diff) if cfg["R_dead"] < dist < cfg["R_safe"]: force += cfg["k_rep"] * diff / (dist * dist + 1e-6) # 边界软约束力 margin = 1.0 limit = env.size / 2 - margin for axis in range(2): if agent.pos[axis] > limit: force[axis] -= cfg["k_bound"] * (agent.pos[axis] - limit) elif agent.pos[axis] < -limit: force[axis] -= cfg["k_bound"] * (agent.pos[axis] + limit) return force逻辑说明:吸引力只给了方向,没有给距离比例,因为围捕点距离远时应该全速赶路,接近时靠速度限制自然减速。排斥力的分母带了平方项,模拟距离越近推力越强的物理直觉。这里最关键的是R_dead死区参数:当两个智能体距离小于R_dead时,不再施加排斥力。如果不设死区,近距离高增益会让两个智能体在一个仿真步长内弹开很远,下一帧又被吸引回来,产生高频抖动,这是典型的调参翻车现场。
3.2 目标速度预测:让围捕点“追着跑”而不守株待兔
如果围捕点的圆心始终取目标当前位置,目标一移动,整个包围圈就要重新分配,智能体疲于奔命。更高效的做法是用目标速度做简单一阶预测:围捕圆心取目标当前位置加上目标速度乘以前瞻时间。这个前瞻时间要根据目标速度动态调整,目标跑得快时加大前瞻,目标减速时缩小。
class TargetState: def __init__(self, pos, vel): self.pos = np.array(pos, dtype=float) self.vel = np.array(vel, dtype=float) def predict(self, lead_time): return self.pos + self.vel * lead_time def compute_lead_time(target, agents): avg_dist = np.mean([np.linalg.norm(a.pos - target.pos) for a in agents]) base_lead = 0.4 speed_factor = np.linalg.norm(target.vel) / 2.0 return base_lead + speed_factor * 0.3 + avg_dist * 0.01逻辑说明:这里的前瞻时间不是固定值,而是动态计算的。平均距离越大,说明包围圈越松散,需要更靠前的围捕圆心来提前截住目标。目标速度越快,前瞻时间也越大。实际调试中发现,基础前瞻 0.4 秒配合速度因子 0.3 秒,在大多数二维仿真场景里都能得到比较稳定的包围效果。l时间窗口过长,围捕点会冲到目标前方很远,出现智能体集体跑过头的情况。
3.3 帧循环里的时序:感知→分配→控制→判定
整个围捕源码的主循环可以整理成四个阶段。感知阶段更新每个智能体对目标位置的观测;分配阶段按需重新计算围捕点;控制阶段计算合力并更新位置;判定阶段检查围捕是否成功或超时。分配不需要每帧执行,因为围捕点频繁切换反而会导致队形不稳,我一般每 10 帧或当目标移动距离超过半个围捕半径时才重新分配。
def run_capture_simulation(agents, target, env, cfg, max_frames=2000): frame = 0 last_reassign = -100 assign_interval = int(cfg.get("assign_interval", 10)) history = [] while frame < max_frames: if frame - last_reassign >= assign_interval: lead_time = compute_lead_time(target, agents) center = target.predict(lead_time) capture_points = assign_capture_points( [a.pos for a in agents], center, cfg["R_enc"]) last_reassign = frame forces = [ compute_forces(ag, capture_points[i], agents, env, cfg) for i, ag in enumerate(agents) ] for i, ag in enumerate(agents): ag.update(forces[i], cfg["dt"]) target.update(agents, cfg["dt"]) if check_capture_success(agents, target, cfg): return frame, True history.append({ "frame": frame, "agents": [a.pos.copy() for a in agents], "target": target.pos.copy(), }) frame += 1 return frame, False逻辑说明:target.update里面实现目标的逃逸策略,通常会让目标朝当前最大角度空隙的中点移动,模拟一个会找缺口逃跑的聪明目标。check_capture_success就是第 2 章提到的角度空隙判定。把history存下来是为了最后可视化重放,有了这个列表你就能在 matplotlib 里逐帧绘制,不用重新跑一遍仿真。
4. 不同环境下算法适配:障碍、变速目标与局部观测
4.1 静态障碍下的绕行策略:障碍膨胀与切向力
“各种环境”里被问得最多的是静态障碍。纯势力场法有一个原生缺陷:智能体靠近障碍时,排斥力把智能体推开,但目标在障碍正后方时,围捕点穿过了障碍,智能体被夹在障碍和围捕点之间来回振荡。处理办法有两个:一是把障碍物半径膨胀一个安全量,二是给势力场加一个沿障碍切线的分量。
def obstacle_avoidance_force(agent, env, cfg): force = np.zeros(2) for obs in env.obstacles: ox, oy, radius = obs diff = agent.pos - obs[:2] dist = np.linalg.norm(diff) safe_r = radius + cfg["obstacle_margin"] if dist < safe_r: # 径向排斥,方向是远离障碍中心 if dist > 1e-6: force += cfg["k_obs"] * (safe_r - dist) * diff / dist # 切向力,让智能体沿障碍边缘滑过去 tangent = np.array([-diff[1], diff[0]]) force += cfg["k_tangent"] * tangent / (np.linalg.norm(tangent) + 1e-6) return force逻辑说明:径向排斥力的强度用(safe_r - dist)加权,靠近障碍时推力线性增大,避免瞬间跳飞。切向力的作用很微妙,它不直接把智能体推离,而是引导智能体绕障碍边缘滑动,配合吸引力就能形成绕行路径。障碍膨胀量obstacle_margin我一般取 0.15 到 0.25,太大等于把可通行区域大幅压缩,太小则智能体仍然会蹭到障碍边缘。
4.2 目标突然加速或急停:放宽预测窗口与复位围捕半径
目标的行为模型如果是随机速度突变,基础预测策略会失效。比如目标原本以 1.0 速度匀速移动,下一帧突然加速到 2.5,那围捕出发点的位置可能落后目标一大截。检测速度突变不能用瞬时速度差,因为目标速度本身就有波动。常见做法是保存目标最近 5 帧的速度序列,计算标准差,标准差超过设定阈值时认为发生突变。
class AdaptiveTarget(TargetState): def __init__(self, pos, vel): super().__init__(pos, vel) self.vel_history = [] def update(self, agents, dt, cfg): escape_dir = compute_escape_direction(agents, self.pos) self.vel = escape_dir * self.speed self.vel_history.append(self.vel.copy()) if len(self.vel_history) > 5: self.vel_history.pop(0) speed_var = np.std([v[0] for v in self.vel_history]) if speed_var > cfg["vel_change_threshold"]: self.speed = np.clip(self.speed * 1.3, cfg["vmin"], cfg["vmax"]) else: self.speed = np.clip(self.speed, cfg["vmin"], cfg["vmax"]) self.pos += self.vel * dt逻辑说明:这里做了一个简化:速度突变时目标速度放大 1.3 倍,模拟目标被逼急后突然冲刺。配套的围捕侧改法是提高分配频率:检测到目标速度突变后,强制下一帧重新分配围捕点,并把围捕半径从 4.0 临时缩小到 3.0。收紧包围圈可以抵消目标冲刺带来的逃脱风险,这个“检测异常 → 重置关键参数”的思路很适合写进项目说明,它展示了算法适应不同环境的能力。
4.3 局部观测条件:用距离与方位角估计目标状态
更接近真实落地的环境设定是局部观测,智能体无法直接读取目标全局坐标,只能拿到相对距离和方位角。这种情况下的常见做法是给每个智能体配一个简单估计器:用自身位置加上距离和角度的观测,反推目标位置。
def estimate_target_pos(agent_pos, measured_dist, measured_angle): return np.array([ agent_pos[0] + measured_dist * np.cos(measured_angle), agent_pos[1] + measured_dist * np.sin(measured_angle) ])逻辑说明:多个智能体对同一个目标分别估计,会得到多个位置候选。实际流程中我会把这些候选做加权平均,权重取观测质量的倒数,简单理解就是离目标越近的智能体,估计结果越可信。这里需要注意一个坑:如果所有智能体都在目标同一侧,估计结果会在目标切向方向产生系统性偏移,包围圈整体偏向一边。缓解办法是让智能体在移动过程中分担部分观测更新,而不是各自独立估计。局部观测模式下,围捕点分配频率要比全局观测更高,我一般每 5 帧重分配一次,因为目标位置的估计噪声会不断累积。
4.4 多目标场景:按密度聚类后分组围捕
多目标场景的难点不是让智能体去追多个目标,而是如何把智能体分组。最省事的做法是 k-means 聚类:把所有目标位置聚成 K 簇,再按每个簇内目标数量分配智能体数量。簇离得越远、目标越分散,需要的智能体越多。分配原则很简单:每组智能体数量与目标数量成正比。
from sklearn.cluster import KMeans def assign_agent_groups(agent_positions, target_positions, n_groups): kmeans = KMeans(n_clusters=n_groups, random_state=42).fit(target_positions) labels = kmeans.labels_ group_ids = np.zeros(len(agent_positions), dtype=int) for g in range(n_groups): target_count = np.sum(labels == g) total = len(agent_positions) ratio = target_count / len(target_positions) group_ids[int(g * len(agent_positions) / n_groups):] = g return group_ids, kmeans.cluster_centers_逻辑说明:代码里的group_ids是给每个智能体打分组标签,随后主循环里只对同组智能体执行围捕点分配。这个写法有一个问题:如果某组智能体被数量不足填满,围捕圈会开口。项目说明里我会建议在分组后检查每组智能体数量是否不少于 3,不足则从其他组借调距离最近的智能体。多目标场景成功率的统计口径也和单目标不同,通常要求所有目标都被捕才算成功,这一个条件会直接影响调参方向。
5. 避坑排查:智能体抖动、卡障碍与模拟速度慢的 7 个经典坑
5.1 智能体直接飞出地图边界
现象:仿真跑到几百帧后,某个智能体的坐标突然超出边界,随后整个队伍失控。原因:控制力里只有吸引和排斥,没有边界约束,当目标逃到边界附近时,围捕点生成在边界外,智能体一路追出去。解决:环境类的in_bounds只负责判定,真正的约束要到compute_forces里加边界软力。我见过不少源码直接把边界处理放在位置更新后硬截断,这样会破坏力的连续性,造成位置跳变,软边界力是更稳的解法。
5.2 智能体在高增益下高频抖动
现象:智能体轨迹呈锯齿状,每帧位置都在小幅反复横跳,画面看起来像筛糠。原因:控制力增益k_att太大,或者仿真步长 dt 太大。吸引力是恒定方向力,位置更新后目标点相对位置变化,下一帧力方向反转,就形成振荡。解决:优先调小k_att,从 2.0 降到 1.2;再把 dt 从 0.05 降到 0.03。如果仍然抖动,检查排斥力死区R_dead是否设置,近距排斥无死区是最容易产生高频抖动的设计。
5.3 智能体重叠与穿模
现象:两个智能体的位置距离几乎为 0,甚至完全重合。原因:斥力公式diff / dist^2在距离极小时会产生巨大推力,但速度钳制限制了单帧位移,如果速度过大仍然会一步跨过安全距离。解决:除了斥力之外,加一个显式碰撞修正,位置更新后检查所有智能体对,如果距离小于安全直径则强制拉开。
def resolve_overlap(agents, min_dist=0.6): for i in range(len(agents)): for j in range(i + 1, len(agents)): diff = agents[j].pos - agents[i].pos dist = np.linalg.norm(diff) if dist < min_dist and dist > 1e-6: push = (min_dist - dist) / 2 agents[i].pos -= diff / dist * push agents[j].pos += diff / dist * push逻辑说明:这段修正要把“碰撞修复”和“斥力防止碰撞”区别开。斥力是预防性的,碰撞修复是事后补救。两者配合才能保证智能体在快速移动时不重叠。min_dist取 0.6 是因为智能体本身的可视半径我设为 0.4,留一点视觉余量。
5.4 围捕点分配严重不平衡
现象:智能体整体偏在目标一侧,另一侧空隙很大,但系统不认为围捕失败,因为距离条件满足。原因:匈牙利算法优化的是“总距离最小”,可能会为了节省总路程让两个智能体同时留在左侧,右侧空出来。解决:改成“瓶颈分配”,即最小化最大分配距离,或者干脆在生成围捕点时优先在最大角度空隙处放一个点,然后再均匀排布其余点。
def build_capture_points_with_priority(agents, target_pos, R_enc): angles = [] n = len(agents) gap_center = compute_max_gap_center(agents, target_pos) angles.append(gap_center) for k in range(n - 1): angles.append(gap_center + 2 * np.pi * (k + 1) / n) return np.array([ [target_pos[0] + R_enc * np.cos(a), target_pos[1] + R_enc * np.sin(a)] for a in angles ])逻辑说明:compute_max_gap_center算了当前目标周围最大的逃逸空隙的中心角度,把第一个围捕点放在那里,其他点按圆周方向均匀补齐。这样即使智能体数量少,也能优先封住最危险的出口。这个修正看似简单,但对围捕成功率的提升非常明显,属于整个源码里性价比最高的一处改动。
5.5 目标躲到障碍背后导致“集体围观”
现象:目标静止在障碍物正后方,智能体因排斥力无法靠近,全部停在障碍前一段距离外围成一个半圆,但围捕判定一直不成功。原因:势力场法没有路径规划能力,排斥力把智能体阻在了障碍外,围捕点又穿过障碍生成在目标侧。解决:除了前面说的切向力,还要在检测到围捕点与智能体之间连线段穿越障碍时,临时生成一个绕行中转点。
def create_waypoint_if_blocked(agent_pos, capture_point, env): for obs in env.obstacles: center = obs[:2] radius = obs[2] if segment_blocked(agent_pos, capture_point, center, radius): vec = capture_point - center tang = np.array([-vec[1], vec[0]]) return center + tang * radius * 1.5 return capture_point逻辑说明:segment_blocked用点到直线距离判断线段是否穿过圆形障碍。如果穿过,就取障碍圆心到围捕点方向的切线方向生成一个中转点。智能体会先朝中转点移动,绕过障碍后再转向真正的围捕点。这种方式比完整 A* 路径规划轻量,又比纯势场可靠,适合课程设计与源码演示。
5.6 模拟帧率低:智能体数量一多就卡
现象:智能体超过 10 个后,每帧耗时明显上升,超过 20 个几乎跑不动。原因:势力场计算是 O(n²) 的,每个智能体要遍历所有其他智能体算排斥力,还有围捕点分配每帧都在跑匈牙利算法。解决:两点优化。排斥力计算用网格哈希,只查相邻网格单元里的智能体;围捕点分配不要每帧跑,设定距离变化阈值,超过阈值才重新分配。
def build_spatial_grid(agents, cell_size): grid = {} for ag in agents: key = (int(ag.pos[0] // cell_size), int(ag.pos[1] // cell_size)) grid.setdefault(key, []).append(ag) return grid逻辑说明:cell_size取排斥力最大作用距离,这样任意一个智能体只需要检查自己所在网格的 8 个邻居网格,避开了全量遍历。这个优化在智能体数量超过 15 个时效果非常明显,帧耗时能降到原来的五分之一以下。
5.7 复现实验失败:两次运行结果完全不一样
现象:同一套参数,第一次成功率 80%,第二次只剩 40%。原因:环境障碍生成用的随机流没固定,目标初始速度也是随机的,导致两次实验的难度完全不同。解决:环境、目标、智能体初始位置全部用同一个随机种子生成,并且把初始状态保存下来。
def save_experiment_state(filepath, agents, target, env): np.savez( filepath, agent_positions=np.array([a.pos for a in agents]), agent_velocities=np.array([a.vel for a in agents]), target_position=target.pos, target_velocity=target.vel, obstacles=np.array(env.obstacles), seed=env.seed )逻辑说明:保存初始状态后,每次实验都是同一个开局,参数调优才有可比性。项目说明里这一块通常写“实验可复现性设计”,在评分时也是加分项。
6. 验证与进阶:统计围捕成功率、扫描参数与导出回放
6.1 围捕成功率的统计口径
围捕成功率不是跑一次看结果,要做蒙特卡洛统计。我习惯固定障碍布局,目标初始速度随机,跑 30 到 50 次,记录三个指标:成功率、平均围捕时间、平均最大空隙角。统计代码很简单,但统计口径必须提前定,否则不同实验之间没法横向比较。
def evaluate_policy(vmax, R_enc, trials=30): success_count = 0 time_list = [] for trial in range(trials): env = Env2D(seed=100 + trial) agents = create_agents(env, n=5, vmax=vmax) target = create_target(env) end_frame, success = run_capture_simulation( agents, target, env, {"vmax": vmax, "R_enc": R_enc}) if success: success_count += 1 time_list.append(end_frame) return success_count / trials, np.mean(time_list)逻辑说明:注意每个 trial 的种子不固定但可复现,用100 + trial来保证 30 次试验的初始环境互不相同,但每次运行结果一致。成功率低于 60% 时不要急着调参数,先检查围捕点的角度覆盖条件,再检查速度配置。
6.2 参数扫描:用网格搜索找稳定区间
调参过程中我用得最多的方法是网格扫描,原理上和你写 python 量化交易策略代码时做参数回测一样,把候选参数组合全部跑一遍,用表格对比表现。常见的扫法是对R_enc和v_p做双层循环,每组参数跑 20 次试验,输出成功率和平均围捕时间。
import itertools def grid_search(): candidates = { "R_enc": [2.5, 3.0, 4.0, 5.0], "vmax": [1.2, 1.6, 2.0], } results = [] for R_enc, vmax in itertools.product( candidates["R_enc"], candidates["vmax"]): rate, avg_time = evaluate_policy(vmax, R_enc, trials=20) results.append((R_enc, vmax, rate, avg_time)) print(f"R_enc={R_enc}, vmax={vmax}, 成功率={rate:.2f}, 平均时间={avg_time:.1f}") return results逻辑说明:网格扫描的代价是智能体数量越多越慢,实时调整不要直接跑全量组合。可以先跑 5 次试验快速筛掉明显不行的区间,再对稳定区间跑 20 次做最终确认。实际经验是R_enc在 3.0 到 4.0 区间、vmax在目标速度的 1.5 到 2.0 倍时成功率最高,超出这个范围稳定性下降很快。
6.3 可视化重放与状态导出
仿真结束后把历史轨迹画出来,是判断算法是否真的“协同”最直观的方式。主循环里保存的history列表,包含每一帧智能体和目标的位置,我用 matplotlib 逐帧绘制成动画,也经常导出成npz文件给别人复现。导出格式和前面save_experiment_state保持一致,只追加一维索引即可。
一个我强烈建议养成的验证习惯:在写项目说明时,把智能体轨迹重叠图和角度空隙变化图画出来。图中能看到智能体是否真的形成了包围圈,还是各自乱撞,这比几十行文字说明更有说服力。我自己调试时会在每 50 帧打印一次当前最大角度空隙,配合可视化一起看,遇到成功率波动大时能快速定位到底是在哪个阶段出了缺口。希望这套源码思路和避坑经验能帮到你,让你跑起多智能体协同围捕仿真时少走一些我走过的弯路。
本文还有配套的精品资源,点击获取