简介:一份专为多智能体强化学习(MARL)设计的环境工具包,面向强化学习研究者、算法工程师与学生,用于在统一平台中开发、训练和对比多智能体协作与竞争策略。环境覆盖灭火、找宝藏、抓猪、足球、移动箱子、无人机、清洁、仓库等典型任务场景,支持多个智能体并行学习并通过环境反馈与其他智能体行为共同演化策略。压缩包共包含71个文件,其中36个Python源码文件提供各场景的环境实现与测试入口,13个PDF文档说明任务设计与算法原理,13个GIF演示直观展示运行效果,另有若干PNG图片与依赖库文件;整体体积仅3.65MB,轻量易部署。目前已有671人学习使用。通过阅读源码和实验文档,读者可快速理解MARL环境构建逻辑,并基于这些场景开展课程实验、算法对比或论文复现;各模块均提供独立环境与测试脚本,便于按需扩展,是入门与进阶多智能体强化学习的实用参考。
1. 为什么我会把这个 multi-agent 强化学习环境仓库留下来
做多智能体强化学习时,最让人头疼的往往不是算法本身的公式,而是一组能让多个智能体稳定交互的环境。论文里描述的场景要么交互复杂,要么没有开源实现,自己从头写又要处理地图渲染、回合重置、奖励平衡一堆琐事。这个仓库把 13 个多智能体场景打包在一起,每个场景都有 env_xxx.py 和配套的 test_xxx.py,还有对应的 PDF 设计说明和运行演示 GIF,覆盖了救火、寻宝、抓猪、足球、无人机避让、仓库调度、救援等典型任务。对想快速验证 DQN、PPO 或 Actor-Critic 类算法在 multi-agent 环境上效果的研究者和工程师来说,这是一个直接可用的多智能体强化学习环境,不需要再重复造地图轮子。
仓库里每个环境都保持相对独立,你既可以把整个项目当作算法评测基准,也可以只挑其中一个环境做深入改造。比如想研究协作机制,就直接打开 env_Cleaner.py 和 env_FireFighter.py;想对比竞争场景,就去看 env_Soccer.py 和 env_OppositeV2.py。下面我会从环境接口设计、场景分类、训练脚本接入三个层面拆解这个资源,最后一个部分给出自定义环境和验证环境的完整思路。
2. 环境接口设计:从 env_xxx.py 看懂 state-action 与 reward 规范
2.1 reset/step 是多智能体环境的统一入口
打开任意一个 env_xxx.py,都会发现它不是严格意义上的 OpenAI Gym Environment,而是一组更松散的 Python 类。拿 env_Cleaner.py 来说,类里通常包含 init(world_size, obstacles, n_agents) 这类构造参数,以及 reset() 和 step(actions)。与单智能体环境不同,step 里的 actions 是一个列表,长度等于智能体数量,每个智能体对应一个动作编号。reset 返回的是所有智能体的初始位置和一个地图状态。
我一般拿到新环境的第一件事是打开同目录的 test_xxx.py,看它怎么调用环境。几乎所有 test 脚本都遵循同一个模板:先实例化环境,再循环 reset、step,最后把渲染结果打印或保存。下面是一个根据仓库中常见 test_FireFighter.py 风格整理的调用框架:
# test_FireFighter.py 的简化调用结构 import numpy as np from env_FireFighter import FireFighterEnv # 具体类名以文件为准 env = FireFighterEnv(size=7, fires=3, agents=2) obs = env.reset() # obs 里通常包含各智能体位置、火点位置、时间步 for step in range(100): actions = [env.action_space_sample() for _ in range(env.n_agents)] next_obs, rewards, done, info = env.step(actions) env.render() # 有的环境用字符画,有的用 matplotlib 画网格 if done: break这段代码说明三件事:环境的动作必须一次性传入所有智能体的决策;step 返回的 rewards 也是一个列表,与智能体一一对应;done 表示整个回合是否结束,而不是单个智能体是否完成。参数 size、fires、agents 是这个仓库环境常见的构造参数,实际名字以各环境的 PDF 为准。了解这些之后,接算法时只需要把actions换成策略网络的输出即可。
2.2 状态空间与动作空间在代码里怎么表示
这个仓库的环境基本都是网格世界,动作空间通常是离散的 4 个方向(上、下、左、右),少数环境会有“停留”或“推箱子”等额外动作。以 env_MoveBox.py 为例,智能体不仅可以选择移动方向,还可能有一种“推动”动作,这会让动作空间变成 5 或 6 维。而 env_OppositeV2.py 这类对抗环境,动作空间也会保持对称,保证两个智能体有相同的决策自由度。
状态空间则分为两类:一类是相对位置,例如 env_GoTogether 中两个智能体需要汇合,观测可能直接给两者的相对坐标;另一类是完整地图,例如 env_Drones 和 env_Warehouse 会把整张栅格地图拍平成向量,网络输入维度等于grid_width * grid_height * channels,channels 对应障碍物、智能体、目标点等图层。下表是我根据仓库内 PDF 说明整理的常见动作定义习惯:
| 动作编号 | 含义 | 典型环境 |
|---|---|---|
| 0 | 向上移动 | 所有网格环境 |
| 1 | 向下移动 | 所有网格环境 |
| 2 | 向左移动 | 所有网格环境 |
| 3 | 向右移动 | 所有网格环境 |
| 4 | 保持不动/抓取 | CatchPigs、Soccer |
| 5 | 推动箱子 | MoveBox |
这个表只是通用约定,具体到每个环境,我建议直接打印env.action_space或阅读对应 PDF,因为仓库中部分环境允许地形交互,未必完全遵循这张表。比如 Soccer 里的动作可能还包含“射门”方向,不能想当然地当成四方向导航。
2.3 reward 与 done 的设计差异:以 FireFighter 和 Cleaner 为例
多智能体环境里 reward 设计决定了协作还是竞争。FireFighter 的典型设定是 n 个消防员扑灭 n 处火点,每个时间步所有智能体共享一个整体奖励,比如每扑灭一处火给 10 分,步进惩罚为 -0.1。这样设计会让两个智能体天然倾向于分工,因为火点被任意一个扑灭,全队都受益。对应到 Q 学习里,每个智能体看到的reward是相同的,但各自状态不同,所以要小心多智能体 credit assignment 问题——当全场只有总奖励时,某个智能体很难知道自己这一步对团队成功贡献了多少。
Cleaner 环境则不太一样。它的目录下还有 maze.py 和 disjointSet.py,maze 负责生成连通迷宫,disjointSet 用于判断清扫区域是否被完全覆盖。这个环境的 reward 往往按被清扫格子的数量计算,每个格子第一次被扫到才有正奖励,重复清扫只有很小的负奖励。此时多个清洁员如果共享同一张地图,相互之间是“隐性竞争”关系——谁先扫到某格,谁拿到奖励。我在调试这种环境时,会先把累计 reward 打出来,如果总奖励一直不变,多半是 done 条件或者 map 存储更新出了问题。
注意:判断一个环境是协作还是竞争,不能只看名字,要去看 step 函数里 reward 是怎么求和或分配的。同类任务改一行 reward 就能让性质反转。
2.4 一个通用的交互循环模板
为了把任意一个 env_xxx.py 接到强化学习算法上,我会写一个独立的 runner,而不是直接改环境源码。下面这个模板兼容前面提到的所有环境,它假设环境有n_agents属性,动作按批次传入:
def rollout_episode(env, policy, max_steps=200): obs = env.reset() total_rewards = [0.0] * env.n_agents for _ in range(max_steps): actions = policy.select_actions(obs) # 返回长度等于 n_agents 的离散动作 obs, rewards, done, info = env.step(actions) for i, r in enumerate(rewards): total_rewards[i] += r if done: break return total_rewards, info其中policy.select_actions是通用接口,可以替换成随机策略、DQN 的 epsilon-greedy 或 PPO 的 actor 网络输出。注意 rewards 是以 list 返回的,不能直接当标量累加。这个模板解决了我接入新环境时 90% 的重复代码问题,剩下 10% 是不同环境对 done 和 info 定义不同,需要在测试脚本里专门对齐。
3. 场景分类解析:协作、竞争与混合任务怎么在多智能体环境里建模
3.1 协作型场景:CatchPigs、GoTogether 和 Rescue
CatchPigs 字面意思是“抓猪”,从仓库里的 gif 看,两个智能体需要把猪赶到某个区域。这种任务必须协作,因为单边驱赶很难把猪逼到拐角。实现层面,猪的位置一般也维护在地图数组里,智能体走到猪相邻格时会触发“惊吓”机制,让猪往反方向跑。因此 reward 不应该只按“是否抓住”给,通常会加“接近猪”的中间奖励,避免智能体学成互相乱转。在代码里你会在 step 函数中看到对猪位置的条件判断,这里容易出的 bug 是猪被多个智能体同时惊吓时移动方向冲突,我一般会用方向叠加后再归一化的做法处理。
GoTogether 则是一个汇合任务:两个智能体从不同起点出发,目标是同时站到同一格。这个环境的经典陷阱是提前汇合,所以 done 条件往往是“两者同格且时间步大于某个阈值”,而不是第一次同格就停止。Reward 设计上,可以给“距离缩小”的稠密奖励,也可以只给最终成功 +1、不成功 0 的稀疏奖励,用来对比不同算法对 reward 密度的敏感度。如果训练时发现两个智能体一开始就往对方起点冲,说明它们没有学到“等一等再汇合”这个时序约束,此时检查 done 条件是否过于宽松。
Rescue 环境在仓库里同时存在 Python2 和 Python3 两套代码,说明它经历过跨版本迁移。救援场景一般是有若干被困者分布在危险区域,智能体把它们搬运到安全区。这个任务的状态空间相对较大,因为要同时表征被困者位置、智能体位置和危险区扩散范围。如果在旧版 Python2 代码上训练,建议先迁移到 Python3,重点检查xrange、字典迭代和 matplotlib 渲染接口的变化。我在跑这个环境时,会先固定随机种子,确保两次 reset 的地图一致,否则后续调参时很难判断算法进步来自策略优化还是地图难度波动。
3.2 竞争型场景:Soccer 和 Opposite 的对抗关系
Soccer 是典型的零和竞争。仓库内附了 redbot.png 和 bluebot.png 两张机器人图标,说明它用 pygame 或 matplotlib 做可视化较多。足球比赛里一个智能体控球时,另一个要抢断,reward 本质上是对称的:进球方 +1,失球方 -1。如果两个智能体使用同一套参数初始化,很容易出现“互相抵消”的学习信号,我一般会在训练时引入自博弈,或者给双方使用不同的探索率。比如红色方使用 epsilon=0.1,蓝色方使用 epsilon=0.3,这样至少一方能产生较丰富的经验,避免两个策略同时停滞。
Opposite 从名字就能看出是“相对”任务,两个智能体很可能被放置在对称位置,执行相反的导航路径。这种环境最适合测试竞争型 multi-agent 算法的稳定性。它的难点在于,单纯扩大经验回放池会混入大量对手不同策略下的数据,导致策略震荡。常见做法是使用 league 式的多对手采样,但我手头实验时发现,先用固定规则对手做预训练,再切换到自博弈,收敛会快得多。另外注意读取 env_OppositeV2.py 时,V2 可能已经修正了 V1 的奖励对称性问题,如果你的实验对公平性敏感,优先用新版。
3.3 单智能体与多智能体的边界:SingleCatchPigs 和 MoveBox
仓库里还有 SingleCatchPigs,与 CatchPigs 形成对比。Single 版本只有一个智能体去抓猪,猪的行为如果是固定规则,那它本质上是一个单智能体问题,完全可以用 DQN 直接解决,不需要 MARL 算法。但这里把它也收进来,好处是可以直接对比“单智能体训练”和“双智能体训练”在同一场景下的表现差异。我在课程设计中经常让学生先跑 SingleCatchPigs,再切换到 CatchPigs,观察协作带来的收益和训练难度的上升。这种对比实验只需要修改环境参数,不需要改动算法代码,非常适合写进实验报告。
MoveBox 则处在单/多智能体的模糊地带:如果只有一个箱子且只有一个智能体推,那它是单智能体;但仓库中 gif 显示 MoveBox 似乎存在两个智能体合作把箱子推向目标。这种“可调智能体数量”的环境很适合做 scalability 实验——同样一个算法,智能体数量从 1 变到 3,学习曲线会明显变差,这本身就是一个值得写进报告里的结论。需要注意的是,MoveBox 的观测里通常包含箱子的相对坐标,如果多个智能体同时推箱子,步长和碰撞检测都会影响训练稳定性,跑之前先单步调试确认奖励方向一致。
3.4 环境横向对比与选型建议
下面这张表汇总了我对主要环境的理解,方便根据实验目的快速选择环境:
| 环境 | 智能体数 | 任务性质 | 适合验证的算法特性 |
|---|---|---|---|
| FireFighter | 2~3 | 协作 | 公共 reward 下的 credit assignment |
| GoTogether | 2 | 协作 | 稀疏 reward 下的延时汇合 |
| Rescue | 2~4 | 协作 | 动态障碍与多目标调度 |
| CatchPigs | 2 | 协作 | 多智能体围捕策略 |
| SingleCatchPigs | 1 | 单智能体 | 作为 MARL 的基线对比 |
| Soccer | 2 | 竞争 | 自博弈与对手建模 |
| Opposite | 2 | 竞争 | 对称策略与算法稳定性 |
| MoveBox | 1~2 | 混合 | 智能体数量扩展性 |
| Drones | 3+ | 混合 | 碰撞避免与路径规划 |
| Warehouse | 2~5 | 协作 | 多智能体资源分配 |
| Cleaner | 2+ | 混合 | 公共 vs 个体 reward 对比 |
需要注意,我并不建议直接把表格里的特点当作定论,因为仓库里很多环境同时支持修改参数,例如 Cleaner 可以通过改变 reward 模式切换协作和竞争,选型时必须结合测试脚本里的默认参数。选环境时优先看 PDF 说明里的重点章节,比如 Cleaner.pdf 会写明迷宫生成算法,这比读源码更快。
4. 训练脚本的接入与调试:把 test_xxx.py 接到 DQN/PPO 上
4.1 先跑通测试脚本
拿到压缩包后,进入 master 目录,对每个环境直接跑一条命令就行。比如:
cd Multi-Agent-Reinforcement-Learning-Environment-master python test_FireFighter.py注意环境依赖。仓库里大量使用 numpy、matplotlib,少数用到 pygame。如果提示 ModuleNotFoundError: No module named 'pygame',用 pip install pygame 安装即可。运行 test_Soccer.py 前要保证当前目录下有 redbot.png、bluebot.png 这两张图,否则渲染函数会报 IOError。同样,其他 test 脚本也会隐式依赖当前目录,所以不要从别的目录运行脚本,尽量cd到对应环境目录。跑通之后,观察输出图像是否符合预期,比如 FireFighter 里火点是否被正确渲染、智能体是否能在有限步内完成任务。
4.2 把环境接到 DQN / PPO 算法上
由于环境不是 gym 标准接口,接入算法时我会写一个 adapter。以 PPO 为例,用 gym 一样的格式封装 env:
# make_env.py 适配器,把 test 脚本里用到的环境包装成 gym-like 接口 import gym from gym import spaces import numpy as np class MARLWrapper(gym.Env): def __init__(self, env, n_agents): super().__init__() self.env = env self.n_agents = n_agents # 假设所有智能体观测量是固定长度向量,动作为 0~4 的离散值 self.observation_space = spaces.Box(low=0, high=1, shape=(128,), dtype=np.float32) self.action_space = spaces.Discrete(5) def reset(self): obs = self.env.reset() return np.asarray(obs, dtype=np.float32).flatten()[None, :] def step(self, actions): # actions 可以是 shape=(n_agents,) 的整数数组 obs, rewards, done, info = self.env.step(actions.tolist()) obs_flat = np.asarray(obs, dtype=np.float32).flatten()[None, :] return obs_flat, np.mean(rewards), done, info这段代码有几个值得注意的地方:多智能体环境的 obs 通常是 n_agents 个独立观测,这里简单拼接后当作单个 obs 使用;reward 用np.mean(rewards)做平均,这只适用于公共 reward 的协作任务,竞争环境下不能这么处理,否则两个智能体的 reward 直接抵消,训练不出来。观测空间固定为 128 维只是一个示例,实际要以环境里的状态向量长度为准,否则 DQN 网络输入层维度会错。
如果用 stable-baselines3,只需要把上面的 wrapper 传给PPO("MlpPolicy", env)即可。但要记住 SB3 默认只处理单智能体,MARL 场景需要自己维护多个策略副本,或者把多智能体问题转成 centralized training,这部分超出环境本身的范畴,需要另外设计。我一般在调试阶段用这个 wrapper 确认算法能跑,真正做实验时再换成独立的 multi-agent rollout 模块。
4.3 多智能体 rollout 中的数据格式
在写 collect rollout 代码时,我一般会保存每个时间步的完整 transition:所有智能体的 obs、actions、rewards、next_obs、done。使用 numpy 数组一次性存储,而不是 Python list,否则在 1e5 步规模上内存会打爆。
def collect_trajectory(env, policy, max_steps=1000): obs = env.reset() obs_dim = 128 # 以实际环境为准 trajectory = { "obs": np.zeros((max_steps, env.n_agents, obs_dim), dtype=np.float32), "actions": np.zeros((max_steps, env.n_agents), dtype=np.int32), "rewards": np.zeros((max_steps, env.n_agents), dtype=np.float32), } for t in range(max_steps): actions = policy(obs) next_obs, rewards, done, _ = env.step(actions) trajectory["obs"][t] = obs trajectory["actions"][t] = actions trajectory["rewards"][t] = rewards obs = next_obs if done: break return trajectory这个结构可以直接用于后续 computing returns 或 advantage。注意done是全局回合结束,不是每个 agent 自身的 done,所以存储时不需要按 agent 拆分 done,只用判断是否退出循环即可。在多智能体 PPO 中,计算 advantage 时要小心:如果多个 agent 各自估算 value,价值函数的输入是全局 obs 拼接或共享状态,不能独立地用每个 agent 的 reward 去算,否则会忽略队友影响。
4.4 常见运行问题和排查方法
结合我跑这些环境时遇到的坑,列几个常见问题:
- 屏幕一闪而过:很多 test 脚本是即时渲染,循环没有 sleep,加上 matplotlib 的 interactive mode 未开启。改成
plt.pause(0.05)即可。 - Python2 代码:Rescue 环境有 Python2 目录,在 Python3 下会报
print语法错误,使用 2to3 工具转换后再跑。 - 渲染模式报错:部分环境在无显示器的服务器上调用
matplotlib.pyplot会报_tkinter.TclError,在脚本头部添加import matplotlib; matplotlib.use("Agg")可以解决。 - 步数过多不结束:检查环境没有设计最大步数,测试脚本里的
for step in range(100)是唯一限制,算法训练时需要自己按 max_steps 截断。 - 奖励维度不匹配:如果 rewards 返回的是标量,而代码按 list 处理,会报
floatobject is not subscriptable。先打印 type(rewards) 再解析。
5. 扩展自己的 MARL 环境:接口验证与 rollout 检查
5.1 参照 Cleaner 新建一个简单环境
如果想把自定义任务包装成同风格环境,最简单的方式是复制 env_Cleaner.py 作为模板。Cleaner 的代码已经把地图从文件加载、智能体移动、格子清扫、reward 计算、回合终止这几件事分开了。你只需要替换地图生成逻辑和 agent 行为规则。具体来说,保留 reset 和 step 两个方法,在 step 里循环每个 agent 执行移动,再统一更新全局状态;reward 计算放在所有 agent 移动完之后,而不是每移动一个就加一次,这样可以保持和前文一致的“全局奖励”。
自定义环境时还要定义自己的动作映射。建议沿用仓库的 0~4 四方向约定,未来做横向对比时会省去很多麻烦。状态表示上,不要把智能体位置直接放进二维坐标,容易让网络对地图尺寸过拟合。我一般会生成一个 one-hot 图层,把每个智能体、目标、障碍物分别叠在独立的 channel 上,这样再接 CNN 或 MLP 都有较好的泛化性。
5.2 用随机策略做环境自检
在训练任何算法之前,我都会先跑一个随机策略自检。脚本很简单:
python -c "from env_Drones import DronesEnv; env = DronesEnv(); obs = env.reset(); [env.step([env.action_space.sample() for _ in range(env.n_agents)]) for _ in range(50)]; print('random rollout ok')"如果随机策略都能稳定跑完 50 步不出异常,说明环境接口层面没问题。之后再做 reward 范围检查:记录 100 个回合的累计 reward,确认没有 NaN、没有绝对值爆炸。若 reward 一直为 0,回去检查 done 条件是否在第一步就被触发;若某个 agent 的 reward 明显大于其他 agent,要先确认是不是 reward 分配逻辑写错。最后把每步的 rendered frame 存成 gif,仓库里那些 gif 就是用类似方法合成的,这一步能直观判断任务是否可学。
我还会加一个“单步断言”:在 reset 之后连续调用 step 两次,对比前后 obs 是否有变化。如果两次 step 返回一模一样的 obs,说明环境状态没有更新,多半是 actions 没有被真正应用。这种小检查在新增地图或修改移动逻辑时能快速暴露低级错误。拿到完整的轨迹后,把每个 agent 的累计 reward 画成曲线,如果曲线在某个步数突然下降,往往不是算法问题,而是环境给了大额惩罚,这时候回看 PDF 里的奖励设计说明最有效。
本文还有配套的精品资源,点击获取