☰
MADDPG环境选型与训练:粒子场景、参数调优和踩坑排查
2026/10/13 23:32:50 网站建设 项目流程

简介:面向多智能体强化学习研究与实验的MADDPG粒子环境压缩包,源自DeepMind提出的MADDPG算法,是DDPG在多智能体连续动作空间下的扩展实现,适合算法研究者、研究生及开发者用于协同/竞争策略的验证与对比。环境以粒子模拟智能体间的实时交互,内置“追捕”“合作搬运”等经典场景,完整覆盖状态空间、动作空间、奖励函数与多智能体联动机制,并支持调整智能体数量、视野范围、目标位置等参数,帮助理解不同设置下的学习动态。整个zip包共24个文件,以21个py脚本为主体,负责环境定义、场景搭建、策略模型与训练流程,另有说明文档与配置文件辅助快速上手,体积仅32KB,轻量易部署。资源已有1401人学习下载,适合作为MADDPG算法入门、对比实验和复现研究的实用工具,也可迁移到自动驾驶、机器人协作等集体决策场景。

1. MADDPG 的各种环境:换场景就翻车,先选对再谈训练

训练多智能体强化学习,算法只占一半,另一半是环境。MADDPG 是复现最多的基线算法,但 A同学 的例子很典型:拿着同一套训练代码,跑合作类场景很快收敛,切到对抗类场景奖励曲线直接横盘——问题大多出在这个多智能体强化学习环境选型上。这里说的环境,是粒子类环境集:智能体被抽象成平面上的小圆点,靠速度控制移动,场景覆盖合作、对抗、竞合与通信四类任务。它门槛低、迭代快,单回合只有几十步,特别适合做 MADDPG 的算法对比、消融实验和自定义场景改造。新手能照着把环境装上、跑通、调参数,熟手可以直接拿这套环境当算法验证台,改奖励、加障碍、调角色数量都能在半小时内完成。下面从选型开始,一路讲到加载、训练和踩坑。

2. 环境选型与加载:五类粒子场景的接口与参数

2.1 合作导航场景:simple_spread 的观测与奖励结构

先认识默认场景simple_spread。它解决的问题是:N 个智能体、N 个目标点,智能体不需要指定谁去哪个目标,只要所有目标最终都被覆盖,同时尽量别撞在一起。每个智能体每步获得两项奖励:自己到最近目标点的负距离,以及一个很小的碰撞惩罚项。距离按世界坐标归一化,世界边长通常取 1.0,所以奖励数值天然落在 -1 到 0 量级。训练时不收敛的表现不是大起大落,而是奖励缓慢下行最后停在某个平台,看起来像“在噪声里做布朗运动”。

从观测结构看,每个智能体看到的是拼接向量:自己的位置与速度、其余智能体的相对位置、所有目标点的相对位置,后面还会接通信信道内容。向量长度是变长拼接,取决于智能体数量和目标数量,所以不能写死输入维度。simple_spread的价值在于它不需要通信、不需要显式分工,目标分配是隐式的,MADDPG 靠中心化 Critic 就能学到“各自挑最近目标”的策略。多数复现仓库把它当默认回归测试场景,因为收敛曲线最容易判断算法代码本身有没有写错。我一般建议新手第一个跑通的场景就选它:同质智能体、密集奖励、动作空间小,排错成本最低,也最容易建立“训练流程没问题”的信心。

2.2 对抗与竞合场景:simple_tag、simple_adversary 的难点

simple_tag是捕食者-猎物任务:3 个速度略慢的捕食者追 1 个速度更快的猎物,场地里还有若干不可移动的大圆点充当障碍。捕食者碰到猎物得正奖励,猎物被碰到得负奖励,障碍会挡住移动路线。这个场景里智能体不同质,追与逃的目标完全相反,中心化 Critic 要同时评价两类策略,训练难度比合作场景高一截。经验上同样的步数预算,simple_spread已经收敛,simple_tag可能还在混乱期,追捕方会先学会围堵,猎物才会慢慢学会绕障碍。

simple_adversary则是典型的“欺骗”任务:2 个普通智能体加 1 个对手,场地有 2 个目标点,只有一个普通智能体知道正确目标,它需要向同伴传递信息,同时不希望对手也找到正确位置。这个场景的难点在于信息遮蔽:观测向量里故意不给普通智能体完整信息,所以策略既要有用又要防泄密,中心化 Critic 在这里的优势特别明显,因为评价值函数时可以看到完整状态,但执行策略时只能靠局部观测。它适合验证 MADDPG 对信息博弈的处理能力,也是讨论对手建模时的常用场景。

选型可以按这个难度顺序做压力测试:simple_spread<simple_adversary<simple_tag。想快速验证算法改动先用前者,想看清算法上限用后者。只跑一个合作场景就下结论,结论大概率会过度乐观。

2.3 环境加载两种方式:场景 ID 与配置字典

常见的 MADDPG 复现仓库会把环境代码单独放一个目录,入口是一个环境工厂函数。加载方式主要有两种:按场景 ID 创建,或按配置字典创建。

from environments import make_env # 复现仓库里通用的环境工厂 # 方式一:按场景 ID 创建实例 env = make_env("simple_spread", seed=0) obs = env.reset() print("智能体数量:", env.n) print("第一只智能体观测维度:", obs[0].shape)

make_env内部把场景名字符串解析成对应的场景类,再包一层标准强化学习环境接口:reset()返回所有智能体的观测列表,每个元素是一维数组;step(actions)接收动作列表,返回(obs, rewards, done, info)四元组。seed参数务必要保留,多智能体环境里随机种子同时控制初始位置生成和动作采样,不固定 seed 会让两次训练完全不可比。我排查过一个波动问题,两个版本代码只差一个 seed 传参,收敛曲线差异大到看不出是同一个算法。

# 以可编辑模式安装本地环境包,改了场景代码不用重新安装 pip install -e ./environments python -c "from environments import make_env; print(make_env('simple_spread', seed=0).n)"

第二种加载方式适合批量扫参数,把场景配置做成字典,改一个字段就是一个新实验。

# 方式二:按配置字典创建 scenario_config = { "scenario": "simple_tag", "num_good": 1, # 猎物数量 "num_adversaries": 3, # 捕食者数量 "num_obstacles": 2, # 障碍物数量 "max_steps": 50, # 每回合最大步数 } env = make_env(**scenario_config)

这里的num_good、num_adversaries、num_obstacles直接决定局面复杂度。改数量时要同步放大max_steps,否则捕食者还没追上猎物回合就结束,奖励信号几乎全为 0,等于在噪声里训练。常用场景按用途对号入座:

场景 ID任务类型智能体构成奖励特点适合验证什么
simple_spread合作导航N 个同质智能体密集负距离奖励算法代码正确性、基础收敛
simple_tag追逃竞合捕食者 + 猎物 + 障碍稀疏命中奖励异质策略、多体协同追捕
simple_adversary对抗欺骗好智能体 + 对手信息遮蔽下的对抗奖励信息博弈、对手建模
simple_push对抗推挤推手 + 目标智能体接触与距离混合物理交互下的策略冲突
simple_speaker_listener通信协作说话者 + 听者稀疏目标到达奖励通信协议学习

前三个是绕不开的主力场景,后两个在消融实验里常见,按需取用。

注意:make_env里如果没把 seed 传到场景的reset(),固定 seed 就是空操作。排查随机性问题时,第一步永远是验证种子的传递链。

3. 接入 MADDPG 训练循环:观测、动作与 step 的细节

3.1 观测空间构成与维度打印

粒子环境的观测是变长拼接向量:自己的位置和速度、其余智能体的相对位置、目标点的相对位置,以及可选的通信信道内容。不同场景观测维度不同,把维度写死在网络输入层是新手最常见的翻车点,因为改一个智能体数量,整个输入层尺寸就要跟着变。

# 训练前先打印每个智能体的观测与动作边界 for i in range(env.n): obs_dim = obs[i].shape[0] act = env.action_space[i] if hasattr(act, "shape"): print(f"Agent{i}: obs={obs_dim}D, action=连续(shape={act.shape})") else: print(f"Agent{i}: obs={obs_dim}D, action=离散(n={act.n})")

这段代码的关键是env.action_space的两种形态:连续场景的 action space 带shape属性,离散场景带n属性。MADDPG 原论文默认输出连续动作,但粒子场景里离散动作跑起来往往更稳。写训练脚本前先把这段跑一遍,把观测维度填进网络输入层,比对着 README 里的描述猜可靠得多。后面每次改环境配置,我都强制再跑一次这个打印,避免维度对不上。

3.2 reset/step 主循环与经验回放

多智能体环境与单智能体环境最大的区别是:所有返回值都是列表,列表长度必须与智能体数量一致,少一个都不行。

obs = env.reset() done = [False] * env.n episode_reward = [0.0] * env.n step_count = 0 while not all(done) and step_count < env.max_steps: # 训练阶段用带探索噪声的动作,这里先随机采样占位 actions = [env.action_space[i].sample() for i in range(env.n)] obs_next, rewards, done, info = env.step(actions) for i in range(env.n): episode_reward[i] += rewards[i] # 存入经验回放:(观测, 动作, 奖励, 下一观测, 该智能体是否结束) buffer.add(obs[i], actions[i], rewards[i], obs_next[i], done[i]) obs = obs_next step_count += 1

done列表不会所有智能体同时变 True,回放时每个智能体要独立存储各自的 done,不能用一个全局标志位代替,否则被提前结束的那个智能体经验会被错误标记。max_steps是环境的硬性回合上限,代码里用env.max_steps读取而不是写死数字,因为不同场景上限不一样,换场景时写死数字最容易漏改。info字典里通常放着碰撞次数、覆盖比例等统计量,不参与梯度计算,但用来做日志监控和评估指标很合适。

3.3 连续动作与离散动作的转换

标准 MADDPG 策略输出是连续值,如果目标场景是离散动作,不能直接把原始输出丢进step,否则要么维度报错,要么策略永远学不到正确的离散选择。

import numpy as np def to_env_action(raw_action, action_space): if hasattr(action_space, "shape"): # 连续场景:clip 到 [-1, 1] 后直接使用 return np.clip(raw_action, -1.0, 1.0) else: # 离散场景:连续输出映射为五方向动作 # 动作编码:0=静止, 1=上, 2=下, 3=左, 4=右 action_map = np.array([[0, 0], [0, 1], [0, -1], [-1, 0], [1, 0]]) idx = int(np.argmax(raw_action[:5])) return action_map[idx]

逻辑说明:连续场景的clip保证动作不越界,粒子世界的速度指令范围就是 -1 到 1,超出会被环境忽略;离散场景只看前 5 维输出里哪一维最大,选对应方向。需要注意,离散映射丢失了动作强度信息,两个方向分量都很大时只能二选一,所以离散场景下 MADDPG 收敛通常比连续场景慢,这是环境建模带来的固有限制,不是代码 bug。反过来,如果拿到的是离散环境而你想用连续策略,也可以把离散动作反映射回连续向量空间再训练,两种路线的核心差别在于探索噪声的类型:连续场景用高斯噪声,离散场景更适合用 epsilon 随机采样。

4. 训练参数与奖励设计:收敛速度差十倍的设置

4.1 四个敏感超参数与调参顺序

MADDPG 复现仓库给的默认值是为simple_spread调的,换环境后基本要重扫。最敏感的四个参数如下:

参数常见区间调参方向说明
learning_rate1e-4 ~ 1e-3对抗类场景往小调actor 对打时学习率过大会震荡
buffer_size1e5 ~ 1e6环境越复杂越大粒子环境至少 1e5,太小会让样本去相关失效
batch_size128 ~ 1024智能体越多越大batch 要覆盖全部角色,否则优势角色被稀释
noise_scale0.1 ~ 0.3训练后期衰减探索噪声不衰减,策略永远稳定不下来

调参有个可复现的顺序:固定 random seed,先跑 1000 个回合看趋势,再把学习率减半对比斜率。粒子环境一回合只有几十步,十分钟能试三组参数,没必要一上来做网格搜索。另外要区分两个概念:网络初始化的 seed 和环境初始位置的 seed 要分开管理,只固定其中一个,排查时会把环境随机性误判成算法问题,后面在排查章节会展开讲。

4.2 奖励缩放:放大信号不等于加速收敛

simple_spread的奖励是负距离,天然在 -1 附近。有人喜欢给奖励乘系数来“放大信号”,这是最常见的翻车点:放大后 critic 的 target 值同比变大,actor 更新幅度没跟上,表现为奖励曲线前期冲得高,中期突然崩盘。正确做法是在原始奖励上叠加一个小的稠密引导项,而不是缩放整体量级。

def reward_shaping(rewards, obs, done): shaped = [] for i, r in enumerate(rewards): if not done[i]: # 靠近目标方向的微小正向引导,系数控制在 0.05 以下 shaped_r = r + 0.02 * proximity_bonus(obs[i]) else: shaped_r = r shaped.append(shaped_r) return shaped

proximity_bonus是自定义的贴近度函数,值域 0 到 1。系数 0.02 意味着引导项只占原始奖励的零头,不会压过环境本身的目标信号。经验阈值是 shaping 系数不超过原始奖励量级的 20%,超过这个比例,智能体会学会“刷引导”而不是完成任务,比如原地转圈刷接近度。这种问题从奖励曲线看不出来,要看平均步数是否异常变长,或者轨迹里是否有明显原地打转。

4.3 共享奖励与终止奖励的搭配

MADDPG 每个智能体的 reward 默认是分开的,但粒子环境里很多合作场景的奖励形态决定协作能否涌现。纯个体奖励会退化成各自为战,纯共享奖励会让个别智能体偷懒,因为反正有人完成任务自己也能得分。

# 合作场景的常见改造:共享奖励 + 个体惩罚 shared_term = float(np.mean(rewards)) for i in range(env.n): # 共享部分保证全局目标,个体部分避免挤同一目标 final_reward = 0.7 * shared_term + 0.3 * rewards[i] buffer.add(obs[i], actions[i], final_reward, obs_next[i], done[i])

0.7/0.3 是经验配比:共享占比太高则偷懒体会被掩盖,个体占比太高则退化成独立策略。对抗场景反过来,捕食者之间可以共享奖励,猎物必须用独立奖励,混用会让猎物学到“送死”策略,因为猎物发现负奖励能被共享机制稀释。终止奖励也值得单独处理:粒子环境默认没有显式终止奖励,但“是否在限时内完成任务”本身就是有效信号。done触发时给 -1 失败惩罚或 +1 成功奖励,能显著引导初始探索方向,但注意别让这个常数项压过过程奖励的量级,否则策略会只盯着终局不管过程。

5. 常见问题排查:环境能跑通不等于训练能收敛

下面五条是我在多个项目里实际踩过或替别人排查过的记录,按“现象→原因→解决”展开。前三条偏环境配置,后两条偏训练流程,每一条都值得写进自己的检查清单。

5.1 奖励曲线一路下降,从负值掉到更负

现象:训练simple_spread时奖励曲线不是预期的小幅波动,而是单调下滑,从 -0.5 一路跌到 -3,且没有反弹迹象。

原因:多半是奖励尺度没对齐。有人为了让任务“更有挑战”把world_size从 1.0 改成 2.0,距离变大,负奖励量级直接翻倍,而 critic 的学习率还是按 1.0 尺度调的,估值误差被放大后梯度方向持续漂移。

解决:把world_size改回默认,或者对奖励除以世界边长做归一化。排查时先打印一个随机回合的平均奖励作为标尺,确认环境量级和预期一致再启动训练。我一般也会顺手把 actor 和 critic 的学习率分开设,critic 偏低一点,给估值网络更多稳定时间。

5.2 done 提前触发,回合只跑了两三步

现象:训练日志里回合平均步数只有 3 到 5,reward 大部分为 0,loss 曲线几乎不动。

原因:为了加速训练把max_steps设小,结果追逃场景里捕食者还没接近猎物就回合结束,回放 buffer 里大量 done=True 的短经验让估值目标全是终局值,网络学不到过程信息。

解决:先跑一遍随机策略统计平均回合长度,把max_steps设为它的 3 到 5 倍,保证至少一个完整的“接近-接触-反馈”过程能被采样到。观察日志时盯住 step_count 的分布,如果它贴着max_steps说明上限定得太死,如果远小于上限说明任务本身很快结束,两种情况的调法完全不同。

5.3 step 报错维度不匹配,动作长度与环境要求不一致

现象:env.step(actions)抛 ValueError,提示动作维度不匹配,有时多一维有时少一维。

原因:智能体数量写死。改了num_adversaries或num_good后,动作列表长度仍在按旧配置生成,多智能体环境的动作列表长度必须和env.n严格一致。

解决:统一用env.n动态遍历,禁止在训练脚本里出现for i in range(3)这类硬编码。改完环境配置先跑 3.1 的打印脚本,确认每个智能体的动作空间类型和维度再启动训练。这个检查同样适用于改障碍物数量,某些实现会把障碍物也计入可交互实体的动作索引,导致看似没改智能体数量也要同步调整。

5.4 换 seed 后结果剧烈波动,同一个算法两种结局

现象:同一份代码、同样的参数,换一个 seed 后一个收敛一个不收敛,很难判断算法改动到底有没有效果。

原因:多智能体环境的随机性来自两处:网络初始化与智能体初始位置采样。只固定网络 seed、没固定环境 seed,位置分布每次都不一样,收敛快慢自然剧烈波动,这种随机性经常被误读成“算法不行”或“代码有 bug”。

解决:把 seed 分别传给网络、buffer 和环境工厂,做到三个来源都确定。排查玄学波动时,先固定环境 seed 重跑三次:若波动消失,问题在网络初始化;若波动仍在,问题在环境侧。顺带检查make_env的 seed 是否真的传到了场景的reset(),我在某次排查里发现仓库版本更新后 seed 参数被静默忽略,这种隐性 bug 比调参更耗时间。

5.5 评估回合奖励远高于训练回合,误以为算法很强

现象:评估模式(关噪声)的奖励比训练过程高 30% 以上,感觉模型已经学到位了,换到新 seed 又打回原形。

原因:训练时探索噪声让动作偏离最优轨迹,奖励天然偏低;评估时噪声清零后虚高部分来自噪声消除而不是策略变好。如果评估时又恰好固定了特定初始位置,还会叠加环境偏置,结果更加不可信。

解决:评估时除了关噪声,还要固定一组与训练分布一致的初始位置集合,在多个 seed 下各跑 50 回合取均值。少于 20 回合的评估结果不要写进对比报告。另外把“带噪声评估”作为监控项,如果带噪声评估与不带噪声评估差距过大,说明策略对噪声敏感,需要调低噪声尺度或增大衰减速度。

6. 自定义场景与效果验证:改一个障碍物看清算法边界

想验证算法对“障碍感知”是否敏感,最快的方式不是换一套环境,而是改现有场景的参数。以simple_spread为例,把障碍物数量从 0 改成 2,场景就从纯合作导航变成带避障的合作导航,中心化 Critic 需要额外学会绕行,收敛难度立刻上一个台阶。

custom_env_config = { "scenario": "simple_spread", "num_agents": 3, "num_landmarks": 3, "num_obstacles": 2, # 默认是 0,改成 2 后开始考验避障 "world_size": 0.8, # 世界缩小,智能体密度变大 "max_steps": 40, } env = make_env(**custom_env_config)

改完参数后第一件事不是训练,而是跑一次随机策略做对照基线,记录平均奖励和碰撞次数。这个基线有两个用途:确认环境改动后任务难度确实有变化;给后期判断提供参照——如果训练后只比随机策略高 10%,说明环境里有个没被解决的瓶颈,继续调参数意义不大,应该回到环境设计本身找原因。

验证阶段我习惯把训练和评估写在同一份脚本里,用同一份配置创建两个独立实例,一个开噪声训练,一个关噪声评估。粒子环境下评估指标更适合看成功率而不是平均奖励:合作场景统计目标覆盖比例,追逃场景统计捕食者命中猎物的回合占比,这两类指标比标量奖励更能反映策略的真实水平。

def evaluate(env, policy, episodes=50): cover_success = 0 for _ in range(episodes): obs = env.reset() done = [False] * env.n while not all(done): actions = [policy.select_action(o, noise=0.0) for o in obs] obs, rewards, done, info = env.step(actions) if info["success"]: # 自定义场景脚本里注入的成功标记字段 cover_success += 1 return cover_success / episodes

参数说明:noise=0.0是关键,评估阶段必须关掉探索;episodes=50是粒子环境下的合理下限,少于这个数统计波动太大;info["success"]是自定义场景脚本注入的额外字段,写自己的场景时建议把任务成功与否写进info,比事后从轨迹里反推可靠得多。从那以后,我每次换环境或改场景参数,都强制先跑一遍随机策略基线再决定要不要训练——这个习惯帮我挡掉了至少三次“调了一周参数,最后发现是环境难度设置不合理”的无效劳动。环境是算法能力的边界,先摸清边界再谈训练,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询