简介:这是一套面向强化学习研究者与多智能体系统开发者的Python开源平台,基于OpenAI Gym框架实现多智能体追逃博弈(MAPE-G)仿真环境,适用于机器人协作、无人机对抗、网络安全策略建模等场景,适合具备Python编程基础及强化学习入门知识的中高级学习者。资源共41个文件,含27个核心Python源码(覆盖3D/2D飞行仿真环境envs、dogFightEnv追逃逻辑、test模块验证脚本)、9个编译缓存文件(pyc)、1个README.md说明文档、1个LICENSE授权文件、1个requirements.txt依赖清单及1张环境示意图png,整体压缩包仅79KB,轻量易部署。已有245人学习下载,代码高度模块化,支持灵活替换DQN、策略梯度等算法,可快速定制智能体角色、调整环境参数并开展训练评估;目录结构清晰分层,含FlightGearEnv、JsbSimEnv等真实感仿真接口,便于理解多智能体动态交互机制与MARL工程实践路径。
1. 这不是玩具环境:一个能跑通 MARL 训练闭环的多智能体追逃平台,专为算法验证和策略对比而生
你有没有试过在gym里搭一个多智能体追逃场景,结果卡在状态同步上三天?或者刚写完 reward 函数,发现两个智能体互相“穿模”、reward 爆负、训练曲线像心电图?这不是玄学——是缺一个从环境建模、动作解耦、观测隔离到 reward 分层设计都经过实测打磨的底层平台。这个 Python 实现的多智能体追逃博弈强化学习平台,不是 demo,也不是教学示例,而是一个可直接接入 PPO、MAPPO、QMIX 等主流 MARL 算法框架的生产级 gym 兼容环境。它用纯 NumPy + PyGame(可选渲染)实现物理建模,支持动态障碍物、异构智能体(追者/逃者能力不对称)、局部观测掩码、通信带宽限制模拟等真实约束。适合正在做 MARL 算法复现、策略鲁棒性测试、或需要在可控环境下做消融实验的从业者。如果你手头已有 RL 训练脚本,替换env = gym.make(...)这一行就能跑起来——前提是,你得先搞懂它的状态空间怎么切、动作怎么对齐、以及为什么默认不启用全局观测。
2. 环境结构与核心机制:为什么它不是简单复制单智能体 gym 的逻辑?
这个平台不是把多个CartPole-v1拼在一起。它重构了 gym 的交互契约:单次step()返回的是 N 个智能体的联合动作响应,而非单个标量。这意味着你必须理解它的三层抽象:环境层(world model)、智能体层(agent interface)、接口层(gym wrapper)。下面拆解关键设计选择及其工程依据。
2.1 状态空间:连续坐标 + 相对观测 + 掩码机制,拒绝“上帝视角”
环境默认提供两种观测模式:'global'(全图坐标,仅用于 debug)和'local'(默认,每个智能体仅感知半径 R 内的其他实体)。但真正关键的是relative encoding + occlusion mask:
- 所有坐标均以当前智能体位置为原点归一化(如逃者看到追者位置是
(dx, dy),而非(x1, y1)); - 若视线被障碍物阻挡,对应目标的观测向量置为
[0, 0, 0](x, y, type),并额外输出一个occlusion_mask布尔数组; - 每个智能体观测向量长度固定为
max_entities × 3 + 2(+2 是自身速度分量),避免变长输入破坏 batch 训练稳定性。
提示:
max_entities在初始化时通过env = PursuitEvasionEnv(n_pursuers=2, n_evaders=1, max_entities=5)显式指定,它决定了观测向量维度。若运行时报Observation shape mismatch,八成是这里没对齐你的网络输入层。
2.2 动作空间:离散控制 + 连续微调双模式,适配不同算法需求
平台支持两种动作模式,通过action_mode='discrete'(默认)或'continuous'切换:
- Discrete 模式:每个智能体输出 5 类动作 —
NOOP,UP,DOWN,LEFT,RIGHT,对应单位步长移动。这是为 QMIX、VDN 等值分解算法准备的; - Continuous 模式:输出
[vx, vy] ∈ [-1.0, 1.0]²,由环境内部做速度裁剪与碰撞检测。适合 PPO、SAC 等策略梯度方法。
二者底层物理模型一致:采用带阻尼的二阶运动方程
$$ \dot{v} = a - \gamma v $$
其中a是动作施加的加速度,γ=0.1是默认阻尼系数。这比纯一阶移动更符合真实机器人动力学,也使策略学习更具泛化性。
# 初始化时显式声明动作模式 env = PursuitEvasionEnv( n_pursuers=3, n_evaders=2, action_mode='continuous', # ← 关键开关 max_speed=0.05, # 连续模式下最大速度(归一化单位/step) dt=0.1 # 物理仿真时间步长 )参数说明:
max_speed:防止智能体“瞬移”,值过大会导致碰撞检测失效;dt:越小越精确,但计算开销线性上升;0.1是精度与效率的实测平衡点;- 切换模式后,
env.action_space自动变为Box(-1,1,(2,))或Discrete(5),无需手动修改网络头。
2.3 Reward 设计:分层稀疏 + 即时惩罚,解决 MARL 中的 credit assignment 难题
追逃任务天然稀疏——只有抓到或逃脱才给大 reward。但纯稀疏 reward 会让策略梯度消失。本平台采用三级 reward 结构:
| 层级 | 触发条件 | 数值范围 | 作用 |
|---|---|---|---|
| 即时层 | 每 step 移动距离 > 0.01 | -0.01 | 防止智能体“冻结” |
| 协作层 | 追者间距离 <cohesion_radius | +0.02 × (1 - dist/radius) | 鼓励围堵阵型 |
| 终局层 | 追者触碰逃者(成功捕获) | +5.0 | 主 reward |
| 终局层 | 逃者抵达安全区 | +3.0 | 主 reward |
| 惩罚层 | 追者撞墙 / 逃者撞障碍 | -1.0 | 强化边界意识 |
注意:所有 reward 均按智能体独立计算并返回
list[float],非 scalar。例如 3 追者 2 逃者环境,step()返回的reward是长度为 5 的列表,索引 0~2 对应追者,3~4 对应逃者。这是 MAPPO、COMA 等算法要求的格式。
3. 快速上手:三步接入你现有的 RL 训练流程
你不需要重写整个训练循环。只要确认你的算法框架支持gym.vector.VectorEnv或能批量处理list[obs],就能无缝集成。以下是基于 Stable-Baselines3 的最小可行接入方案(其他框架同理)。
3.1 环境封装:用 VectorEnv 实现高效并行采样
Stable-Baselines3 的PPO默认只接受单环境,但 MARL 训练必须批量采样以降低 variance。我们用SubprocVecEnv封装多个独立实例:
from stable_baselines3 import PPO from gym.vector import SubprocVecEnv import numpy as np def make_env(rank, seed=0): def _init(): env = PursuitEvasionEnv( n_pursuers=2, n_evaders=1, action_mode='discrete', render_mode=None, # 关闭渲染提升速度 max_steps=256 # 每 episode 最大步数 ) env.reset(seed=seed + rank) return env return _init # 创建 8 个并行环境 env = SubprocVecEnv([make_env(i) for i in range(8)]) # 注意:此处 obs_shape 是 (n_agents, obs_dim),不是 (obs_dim) # SB3 会自动 flatten,但你要确保 policy 网络能处理 batched agent dim model = PPO( "MlpPolicy", env, verbose=1, n_steps=2048, # 总步数 = 8 env × 2048 = 16384 batch_size=512, gamma=0.99, learning_rate=3e-4 )逻辑说明:
SubprocVecEnv启动 8 个独立进程,每个运行一个PursuitEvasionEnv实例;n_steps=2048指每个进程收集 2048 步数据,因此每次 update 实际使用8×2048=16384个 transition;MlpPolicy会自动将(batch, n_agents, obs_dim)输入 reshape 为(batch×n_agents, obs_dim),但你的自定义 policy 必须显式处理 agent 维度(见 4.2 节)。
3.2 自定义 Policy:为多智能体输出设计网络头
SB3 的MlpPolicy默认输出单个 action,但我们需要为每个智能体输出独立动作。最简方案是继承BasePolicy并重写forward():
import torch as th from torch import nn from stable_baselines3.common.policies import BasePolicy class MultiAgentPolicy(BasePolicy): def __init__(self, observation_space, action_space, **kwargs): super().__init__(observation_space, action_space, **kwargs) # 假设 obs_shape = (n_agents, obs_dim), action_space.n = 5 self.n_agents = observation_space.shape[0] self.obs_dim = observation_space.shape[1] self.action_dim = action_space.n self.feature_extractor = nn.Sequential( nn.Linear(self.obs_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU() ) self.actor = nn.Linear(128, self.action_dim) # 每 agent 一个 head self.critic = nn.Linear(128, 1) def forward(self, obs: th.Tensor) -> tuple: # obs: (batch, n_agents, obs_dim) → (batch×n_agents, obs_dim) batch_size = obs.shape[0] obs_flat = obs.view(-1, self.obs_dim) features = self.feature_extractor(obs_flat) # (batch×n_agents, 128) logits = self.actor(features).view(batch_size, self.n_agents, -1) # (batch, n_agents, 5) values = self.critic(features).view(batch_size, self.n_agents, -1).mean(dim=1) # (batch, 1) return logits, values参数说明:
obs.view(-1, self.obs_dim)是关键:将多智能体观测展平,复用单智能体网络结构;logits.view(...)恢复 agent 维度,供后续Categorical分布采样;values.mean(dim=1)对所有 agent 的 critic 输出取均值,作为全局 state value(MAPPO 标准做法)。
3.3 训练启动与日志监控:如何验证它真在学?
别急着跑 1000 万步。先用eval_freq=10000和n_eval_episodes=5做轻量评估:
from stable_baselines3.common.callbacks import EvalCallback eval_env = SubprocVecEnv([make_env(i+100) for i in range(4)]) eval_callback = EvalCallback( eval_env, best_model_save_path='./logs/best_model/', log_path='./logs/', eval_freq=10000, n_eval_episodes=5, deterministic=True, render=False ) model.learn(total_timesteps=500000, callback=eval_callback)关键验证点:
- 查看
./logs/evaluations.npz中的r(episode reward)是否从-200(随机策略)缓慢上升至-50以上; - 用
env.render()启动单环境可视化,观察追者是否开始形成夹击角(而非乱跑); - 检查
tensorboard --logdir ./logs/中train/entropy_loss是否稳定下降——熵太高说明策略未收敛。
4. 避坑指南:五个血泪经验总结出的高频翻车点
MARL 环境调试成本远高于单智能体。以下问题均来自某高校实验室在复现该平台时的真实报错日志,已定位到根因并验证修复方案。
4.1 现象:ValueError: Observation not in space,但env.observation_space.contains(obs)返回True
- 原因:
gym的contains()方法对Box空间只检查 shape 和 dtype,不校验 NaN 或 inf。而本平台在障碍物遮挡时会将部分观测置零,若物理引擎数值不稳定(如dt过大),可能产生inf速度,污染后续观测。 - 解决:在
step()返回前强制 clip:# 在 env.step() 内部末尾添加 obs = np.nan_to_num(obs, nan=0.0, posinf=1.0, neginf=-1.0)
4.2 现象:训练中 reward 突然全为0.0,且info['done']始终为False
- 原因:
max_steps设置过小(如<128),导致 episode 在追逃发生前就强制 truncation。此时reward数组全为即时层小负值,但info中无terminal_observation,算法误判为“未结束”。 - 解决:
max_steps必须 ≥2×map_width / min_speed。例如地图宽 1.0,最小速度 0.02,则max_steps ≥ 100;建议设为256或512。
4.3 现象:多进程训练时OSError: [Errno 24] Too many open files
- 原因:
SubprocVecEnv为每个子进程创建独立的 PyGame 窗口句柄(即使render_mode=None,PyGame 初始化仍占用 fd)。Linux 默认ulimit -n为 1024,8 进程 × 每个 128 fd = 1024,刚好爆满。 - 解决:在启动脚本开头添加:
或彻底禁用 PyGame 初始化(修改ulimit -n 4096 python train.pyenv.__init__()中if render_mode is not None: pygame.init()为if render_mode == 'human': ...)。
4.4 现象:逃者策略学会“贴墙不动”,追者永远无法捕获
- 原因:reward 设计缺陷——当
cohesion_radius过大时,追者为获得协作 reward 会优先聚拢,反而扩大包围圈空隙;同时逃者发现静止时instant_reward损失最小。 - 解决:动态调整
cohesion_radius,或改用距离倒数 reward:# 替换原协作 reward 计算 avg_dist = np.mean([np.linalg.norm(p1 - p2) for p1 in pursuer_pos for p2 in pursuer_pos if not np.array_equal(p1,p2)]) cohesion_reward = 0.05 / (avg_dist + 1e-3) # 距离越近 reward 越高
4.5 现象:PPO训练 loss 波动剧烈,clip_range频繁触发
- 原因:多智能体动作空间未对齐。例如追者用
discrete,逃者用continuous,但env.action_space只返回一个 space,导致 policy 输出维度错乱。 - 解决:必须统一所有智能体的动作模式。平台不支持混合动作空间——这是 deliberate design choice(有意为之),因为混合动作极大增加 credit assignment 复杂度。如需异构,应将逃者动作离散化(如
EVADE_LEFT,EVADE_RIGHT)。
5. 进阶技巧:用 replay buffer 可视化分析策略失败模式
训练卡在某个 reward plateau?别只盯 tensorboard。我习惯用 replay buffer 抽样回放,定位具体失败场景。本平台内置ReplayBuffer导出接口,配合 Matplotlib 可生成带轨迹热力图的诊断图。
5.1 导出 episode replay 数据
在训练循环中,每 10 个 episode 保存一次完整轨迹:
from collections import defaultdict import pickle def save_episode_replay(env, episode_data, path): # episode_data = { # 'observations': list[np.ndarray], # (n_agents, obs_dim) # 'actions': list[np.ndarray], # (n_agents,) # 'rewards': list[np.ndarray], # (n_agents,) # 'dones': list[bool], # } with open(path, 'wb') as f: pickle.dump(episode_data, f) # 在 callback 中调用 if episode % 10 == 0: save_episode_replay(env, current_episode_buffer, f'./replays/ep_{episode}.pkl')5.2 可视化轨迹与 reward 热力图
import matplotlib.pyplot as plt import numpy as np def plot_episode_replay(pkl_path, map_size=1.0): with open(pkl_path, 'rb') as f: data = pickle.load(f) # 提取追者(前 n_p)和逃者(后 n_e)轨迹 n_p = 2 n_e = 1 pursuer_trajs = [[] for _ in range(n_p)] evader_trajs = [[] for _ in range(n_e)] for obs in data['observations']: for i in range(n_p): # obs[i] 是第 i 个追者的观测,其中前 2 维是自身 x,y pursuer_trajs[i].append(obs[i, :2]) for i in range(n_e): evader_trajs[i].append(obs[n_p+i, :2]) # 绘制 fig, ax = plt.subplots(figsize=(8, 8)) colors = ['red', 'orange', 'blue'] for i, traj in enumerate(pursuer_trajs): traj = np.array(traj) ax.plot(traj[:, 0], traj[:, 1], 'o-', color=colors[i], markersize=2, label=f'Pursuer {i}') for i, traj in enumerate(evader_trajs): traj = np.array(traj) ax.plot(traj[:, 0], traj[:, 1], 's-', color=colors[n_p+i], markersize=3, label=f'Evader {i}') ax.set_xlim(0, map_size) ax.set_ylim(0, map_size) ax.set_aspect('equal') ax.legend() ax.grid(True, alpha=0.3) plt.title(f"Episode Replay: {pkl_path.split('_')[-1].split('.')[0]}") plt.savefig(pkl_path.replace('.pkl', '_traj.png'), dpi=300, bbox_inches='tight')5.3 分析 reward 分布定位瓶颈
对data['rewards']做 agent-level 统计,生成 reward 来源分布饼图:
| Agent Type | Avg Reward/Step | 主要来源 | 问题线索 |
|---|---|---|---|
| Pursuer 0 | -0.12 | 即时层(-0.01×12) | 移动不足,可能卡在角落 |
| Pursuer 1 | +0.03 | 协作层(+0.02×1.5) | 有效协同,但终局 reward 为 0 |
| Evader 0 | -0.08 | 惩罚层(-1.0×0.08) | 频繁撞墙,需加强边界 reward |
从那以后我每次遇到 reward plateau,都强制走一遍 replay buffer 抽样 + 轨迹绘制 + reward 拆解三件套。它比调 learning rate 有用十倍——因为你能亲眼看到策略在“做什么”,而不是猜它“应该做什么”。希望帮到你。
本文还有配套的精品资源,点击获取