资讯详情 MADDPG编队控制源码包全解析:训练、调参与避坑实践
📅 2026/10/30 17:06:15
简介这是一份以FCMADDPG命名的多智能体编队控制学习项目基于MADDPG算法实现面向深度强化学习研究者与机器人、无人系统开发者解决多智能体如何通过试错学习形成并维持特定编队的问题。压缩包共18个文件以13个Python脚本为主涵盖算法核心MADDPG与DDPG代理、演员-评论家网络、经验回放缓冲区、参数配置及训练/测试主程序另有4个状态记录txt文件与1个编译缓存pyc整体仅11KB结构紧凑便于快速定位关键模块。目前已有1785人学习下载。通过研读代码读者可掌握MADDPG在多智能体协同场景下的完整实现流程理解确定性策略梯度与集中式训练、分布式执行的具体写法并可直接修改参数和网络结构用于无人机编队、自动驾驶车队等方向的仿真实验与算法改进。1. FCMADDPG 是给谁的一套 MADDPG 编队控制的可运行源码包做无人机编队、无人车队列控制的工程师大概率遇到过这种处境PID 和一致性协议在固定工况下没问题队形一变、智能体数量一变参数就得从头调。FCMADDPG 这个压缩包就是奔着这个痛点去的——它用 MADDPGMulti-Agent Deep Deterministic Policy Gradient多智能体深度确定性策略梯度这套深度强化学习算法做编队控制把队形保持、速度协同和避碰都交给网络去学而不是手写控制律。包内是一个完整的训练与验证工程main.py 负责训练循环maddpg.py 承载多智能体更新逻辑state 和 Controller 定义编队形状与控制器test.py 用来评估训练结果。适合两类人一是想拿一套能跑的 MADDPG 框架做实验的研究生二是想评估深度强化学习能不能替代传统编队控制器的工程师。2. 拆包看链路params、actor_critic 与 DDPGAgent 怎么串起训练2.1 文件清单先分清主程序、网络与辅助模块拿到压缩包第一步不是打开 main.py而是先建立文件地图。这个包的结构很典型算法主体、网络定义、代理封装、经验回放、参数配置各司其职看清职责划分之后调试时才知道去哪一层找问题。文件/目录职责调试时关注的点maddpg.pyMADDPG 训练算法主体多智能体联合 critic 的更新时序main.py训练入口环境初始化和回合循环test.py验证脚本是否加载了保存的权重DDPGAgent.py单智能体 DDPG 代理目标网络、软更新封装actor_critic.pyActor/Critic 网络结构连续动作空间的输出层设计replay_buffer.py经验回放缓冲多智能体经验是否同步采样params.py超参数配置改这里影响最大function.py奖励函数与误差计算队形误差怎么量化Controller/队形控制器每个智能体一个 controllerstate/初始状态与队形数据txt 文件注意维度一致性这个分层和 OpenAI MADDPG 论文的参考实现一脉相承actor_critic.py 是纯网络定义DDPGAgent.py 把网络包装成带优化器和目标网络的代理maddpg.py 再在代理之上做多智能体的协同更新。你可以把 actor_critic 想象成发动机DDPGAgent 是变速箱maddpg 才是方向盘——三者缺一不可但改代码的顺序应该是自下而上先确认网络层没问题再去动代理层最后才碰算法主体。有一点需要提醒Controller 目录下有 controller1.py 到 controller4.py 四个文件对应四个智能体但 maddpg.py 里的智能体数量不一定和 Controller 文件数一致。如果后续扩编队规模这两处的数量要同步改否则会出现「训练时四个智能体执行时五个控制器」这种尴尬的错位。2.2 params.py调超参数的第一站我拆完包第一件事永远是打开 params.py因为它决定了后面所有实验的基调。这个文件把训练参数集中在一起改起来方便但也容易让人乱调。典型内容如下# 训练参数配置 GAMMA 0.95 # 奖励折扣因子越小越短视 TAU 0.01 # 目标网络软更新系数太大容易震荡 LR_ACTOR 1e-4 # actor 学习率编队任务建议别超过 1e-3 LR_CRITIC 1e-3 # critic 学习率一般比 actor 大一个量级 BUFFER_SIZE 100000 # 经验回放容量编队场景建议不小于 5 万 BATCH_SIZE 64 # 采样 batch 大小 MAX_EPISODES 2000 # 最大训练回合数 MAX_STEPS 100 # 每个回合最大步数 NOISE_STD 0.2 # 探索噪声标准差 ACTION_BOUND 1.0 # 动作归一化边界代码逻辑说明一下GAMMA 和 TAU 是强化学习里最敏感的两个系数。GAMMA 太小会让智能体只顾当前几步的队形误差编队会表现得很急、抖动大TAU 太大则目标网络更新过快训练早期 loss 经常直接发散。我个人习惯先把 LR_CRITIC 稳住再调 LR_ACTOR因为 critic 一旦不稳它传回给 actor 的梯度全是噪声actor 学出来的策略自然也是乱的。参数说明里的 ACTION_BOUND 也值得留意。它对应 actor 输出层 Tanh 激活后的取值范围控制器的实际输出会在后续乘以这个边界做缩放。如果你把编队间距拉大智能体需要更大的速度指令才能跟上长机这个值就得跟着改否则控制量永远不够用。2.3 actor_critic.py 与 DDPGAgent.pyCTDE 范式落到代码上MADDPG 的核心思想是 centralized training with decentralized executionCTDE训练时 critic 能看到所有智能体的观测和动作执行时每个 actor 只用自己的观测。这个包里的 actor_critic.py 正是按这个逻辑实现的import torch import torch.nn as nn class Actor(nn.Module): 每个智能体一个 actor只输入自己的观测输出确定性动作 def __init__(self, obs_dim, act_dim, hidden64): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, act_dim), nn.Tanh()) # Tanh 把动作压到 [-1, 1] def forward(self, obs): return self.net(obs) class Critic(nn.Module): 集中式 critic输入所有智能体的观测和动作输出单个 Q 值 def __init__(self, obs_dim, act_dim, n_agents, hidden64): super().__init__() input_dim (obs_dim act_dim) * n_agents self.net nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1)) def forward(self, obs_all, act_all): x torch.cat([obs_all, act_all], dim-1) return self.net(x)这里最关键的细节在 Critic 的输入维度(obs_dim act_dim) * n_agents。这意味着每个智能体的 critic 都接收全场信息才能正确评估「自己这个动作在别人也行动时到底值多少」。我见过有人图省事把 critic 也写成只输入自身观测那本质上退化成了独立 DDPG多智能体的协同性就没了——队形永远学不齐因为每个智能体都在盲猜队友的行动。DDPGAgent.py 则是把 Actor、Critic 各自的在线网络和目标网络打包外加两个优化器。一个合格的 DDPGAgent 类至少要有软更新方法def soft_update(self, target, source, tau): 把 target 网络参数向 source 网络靠近 tau 比例控制学习速度 for tp, sp in zip(target.parameters(), source.parameters()): tp.data.copy_(tau * sp.data (1 - tau) * tp.data)这个软更新是 DDPG 稳定性的关键MADDPG 原样继承了这套机制目标网络不是定期硬拷贝而是每步都向在线网络「蹭」一点点保证训练过程中目标 Q 值不会突然跳变。看 DDPGAgent 的时候我建议顺手确认一下 actor 和 critic 的 target 网络是用同样的 tau 在更新还是各用各的——有些魔改版本会设置两个不同的 tau虽然也能跑但排查问题时容易多一个变量。3. 训练主循环main.py 和 maddpg.py 的分工与更新时序3.1 main.py环境初始化与回合循环main.py 是训练入口负责「造环境、建代理、跑回合、存模型」四件事。典型回合循环长这样# 对应包内 main.py 的主干逻辑 env FormationEnv() # 编队环境读取 state/ 下的初始队形 agents [DDPGAgent(obs_dim, act_dim) for _ in range(N_AGENTS)] maddpg MADDPG(agents) replay_buffer ReplayBuffer(BUFFER_SIZE) for episode in range(MAX_EPISODES): obs env.reset() # 从 state 目录加载初始位置 episode_reward 0 for step in range(MAX_STEPS): actions [agent.act(o, noiseTrue) for agent, o in zip(agents, obs)] next_obs, rewards, done env.step(actions) # Controller 计算实际控制 replay_buffer.add(obs, actions, rewards, next_obs, done) obs next_obs episode_reward sum(rewards) if done: break maddpg.update(replay_buffer.sample(BATCH_SIZE)) # 每回合更新一次 if episode % 100 0: torch.save(maddpg.get_weights(), fmodel_{episode}.pth)这段逻辑里值得注意的有两点。一是noiseTrue训练时 actor 输出要叠加探索噪声MADDPG 通常用的是 Ornstein-Uhlenbeck 噪声它比高斯噪声更适合物理控制任务因为产生的动作序列有时间相关性不会来回剧烈抖动。如果改成高斯噪声动作频谱会偏高频体现在编队上就是智能体一直在原地微颤。二是更新频率这里选择每回合结束后统一更新一次而不是每步更新。编队控制这类连续性任务每步更新会让梯度方向高度相关训练早期容易陷入局部震荡回合级更新的数据多样性更好稳定性也更可预期。实际训练时观察 loss 曲线如果有规律的锯齿波动多半是更新频率和环境步数不匹配。参数上 MAX_EPISODES 和 MAX_STEPS 的乘积就是总交互步数决定了你的数据量。编队控制任务我一般先跑 500 回合看趋势再决定要不要跑满 2000。一上来就 2000 回合调参周期太长而且前几百回合的趋势基本能说明参数方向对不对。3.2 maddpg.py集中式 critic 的更新时序maddpg.py 是这个包的技术核心处理的是「多个 DDPG 代理如何在同一个环境中协同更新」。MADDPG 的做法是为每个智能体维护一个集中式 critic更新时分四步走def update(self, replay_buffer, batch_size): obs, act, rew, next_obs, done replay_buffer.sample(batch_size) # 1. 用目标 actor 生成下一时刻动作全部 agent 都要做 next_act [] for i, agent in enumerate(self.agents): next_act.append(agent.target_actor(next_obs[i])) next_act torch.cat(next_act, dim1) for i, agent in enumerate(self.agents): # 2. 用集中式目标 critic 计算目标 Q 值 target_q agent.target_critic(next_obs_all, next_act) y rew[i] GAMMA * (1 - done) * target_q # 3. critic 损失在线 Q 值逼近目标 Q 值 q agent.critic(obs_all, act_all) critic_loss F.mse_loss(q, y.detach()) # 4. actor 损失用 critic 的 Q 值梯度反传到 actor actor_loss -agent.critic(obs_all, act_with_agent_i).mean()这个更新时序有个容易被忽略的细节计算 target_q 时用的 next_act 必须全部来自目标网络而不能用在线网络。很多魔改版本在这上面偷懒直接用在线 actor 生成下一动作训练到后期目标 Q 值会跟着在线网络一起漂loss 看起来在降编队误差却下不去。另一个重点是 actor 损失的写法。MADDPG 的 actor 梯度不是来自环境奖励而是来自 critic 的 Q 值——计算时把其他智能体的动作当作已观测变量固定住只让当前 agent 的动作参与梯度传播。这意味着 critic 必须能接收「部分替换」的动作输入所以 maddpg.py 里通常有一个比较绕的索引操作把当前 agent 的动作替换进完整动作向量。如果你是第一次读这个文件建议在这个位置加断点确认每个 agent 的梯度只流经自己的 actor 参数这是多智能体算法最容易写串的地方。关于目标网络的同步还有一点maddpg.py 通常会在每次 update 之后统一调用所有 agent 的 soft_update如果你发现模型有选择地同步部分 agent要警惕是不是为了省显存做的偷工减料。训练时 critic 的输入维度会随 agent 数量线性增长四个智能体还好扩展到八个以上时显存压力会明显起来。3.3 replay_buffer.py多智能体经验为什么必须整段存replay_buffer.py 看起来只是个列表加随机采样但在多智能体场景有一个隐性要求经验必须是「整段」存的即一个 transition 里包含所有智能体的 obs、action、reward而不是每个智能体单独存一条。为什么因为 MADDPG 的集中式 critic 在计算 Q 值时需要同时看到所有智能体的观测和动作。如果按智能体拆开存采样时无法保证取到同一时刻的数据critic 输入的各段观测就不是同一个时间戳Q 值估计就废了。我见过把单智能体 replay buffer 直接搬过来的翻车案例训练时 loss 正常下降一验证队形就乱查了半天才发现是 buffer 结构问题。buffer 里通常还会预填充一些随机探索数据让 critic 在第一轮更新前不至于面对全零经验。这个细节对编队任务特别重要因为初始状态下的随机动作往往会让智能体瞬间冲出边界如果没有预填充早期更新会过分强调边界附近的极端样本导致前几百回合的编队误差一直在高位波动。判断预填充是否足够的经验方法看第一个 episode 结束后的 critic_loss 量级如果比后续稳定阶段的 loss 大两个数量级以上说明预填充数据太少。4. 编队形状怎么学出来state 文件、Controller 与奖励函数的配合4.1 state 目录每个 txt 里存的是什么state/ 目录下有 state1.txt 到 state4.txt 四个文件对应四个智能体的初始状态。这类文件在多智能体编队项目里通常存的是位置坐标、速度和期望队形偏移量我按常见格式整理一个示例字段含义示例值x, y初始位置0.0, 0.0vx, vy初始速度0.0, 0.0offset_x, offset_y编队目标偏移-2.0, 1.5这里最容易翻车的是维度对不齐。如果 state1.txt 存的是 6 个数而 actor 网络输入维度定义的是 4运行时会直接报维度错误。我的习惯是在读文件处加一行打印把 np.loadtxt 之后的 shape 打出来核对不要依赖报错信息去猜因为维度错误有时会藏在矩阵乘法的深处报错位置和真正的问题源头差好几层。另外state 文件不仅仅是初始位置它还隐含了编队形状的定义。state2.txt 和 state3.txt 里的偏移量如果是相对 state1 的那么这是一个 leader 加三个 follower 的编队结构如果四个文件都是相对全局坐标的绝对值则是无领导编队。这个区别决定了 maddpg.py 里 critic 的输入要不要包含 leader 的独立通道也决定了 Controller 里 offset 的参考系。4.2 Controller从强化学习动作到可执行指令Controller/ 目录下每个智能体一个控制器作用是把强化学习网络输出的动作转换成真实可执行的控制指令。一个典型的编队控制器会算这样一笔账# controller1.py 的核心逻辑leader 不需要队形补偿 def compute_command(self, state, action): # action 来自 actor 网络范围 [-1, 1] vx action[0] * self.v_max # 缩放到最大速度 vy action[1] * self.v_max # follower 需要叠加期望队形偏移的补偿项 if self.formation_offset is not None: error self.formation_offset - relative_pos vx self.kp * error[0] # 比例补偿防止队形漂移 return np.array([vx, vy])这段代码体现了编队控制里很重要的分层思想网络负责学「聪明」的部分避碰、速度协同控制器负责兜底保证输出可执行、队形偏移不丢。controller1 到 controller4 的区别通常就在 formation_offset 上leader 的偏移是零向量follower 各有各的期望偏移。如果你想把编队从三角改成菱形要改的就是 controllerN.py 里的 offset 常量以及 state 文件里的期望位置网络结构不需要动。这也是用 MADDPG 做编队比手写控制律方便的地方——形状重构的成本从「重新设计控制律」降到了「改几行配置数据」。但要注意队形改了之后奖励函数里的队形误差计算逻辑也要跟着改如果你只改了 Controller 没改 function.py智能体学出来的动作会和预期队形对不上。4.3 function.py 的奖励设计编队任务的核心难点function.py 承载着奖励函数的计算这是整个编队控制里最玄学的部分。一个合理的编队奖励通常包含三到四项def reward_function(state, action, formation_error, collision): r_formation -alpha * np.linalg.norm(formation_error) # 队形误差惩罚 r_velocity -beta * np.linalg.norm(action) # 动作幅度惩罚抑制抖动 r_collision -10.0 if collision else 0.0 # 碰撞硬惩罚 r_maintain 1.0 if formation_error 0.1 else 0.0 # 队形保持成功奖励 return r_formation r_velocity r_collision r_maintain这个设计里有几个坑值得展开。一是奖励尺度的平衡alpha 和 beta 的比值决定了智能体是优先保队形还是优先省能量如果 beta 太大智能体会选择站在原地不动——因为动作幅度为零的惩罚最小编队误差反正也是负的它躺平了。我一般先把 alpha 调大让队形先能聚拢再逐步加 beta。二是成功奖励 r_maintain 的作用。纯惩罚项r_formation的问题是单调且信号弱智能体很难分辨「我在变好」。加一个阈值触发式的正向奖励相当于给学习过程提供了里程碑信号收敛速度会有肉眼可见的提升。这也是 function.py 里最值得按自己场景改的地方不同编队形状对误差的容忍度不一样菱形队形对横向误差更敏感阈值可以设小一点。三是碰撞惩罚的粒度。r_collision 直接给 -10 的硬惩罚在训练早期会让智能体变得过于保守宁可离队也不靠近队友导致队形聚不拢。常见做法是改成距离相关的软惩罚比如当间距小于安全距离时惩罚值随距离线性增大而不是一刀切。这个改动对训练稳定性的提升比调学习率更明显。5. 避坑排查MADDPG 编队训练最常见的五个翻车点这一章集中说一下我在使用这套框架过程中踩过和见过的坑每一条都按「现象 → 原因 → 解决」的结构梳理。5.1 训练开始没多久 loss 变 NaN编队彻底失控现象跑到百来步critic_loss 突然变成 NaN后续所有回合的奖励都是无效值训练只能重开。原因最常见的两个一是 critic 的 Q 值目标里包含了大数值奖励连乘 GAMMA 之后导致梯度爆炸二是学习率设置过高尤其 LR_CRITIC 超过 1e-2 时几乎必炸。解决先把 params.py 里的 LR_CRITIC 降到 1e-4 跑 200 回合确认稳定再逐步回升。同时在奖励函数出口加 np.clip把单步奖励限制在 [-10, 10] 以内给梯度一个上限。这是最省事的后悔药。5.2 队形能聚拢但整体一直漂移现象四个智能体能形成三角形但整个编队在环境中缓慢平移偏离目标区域越来越远。原因奖励函数里只有相对位置误差缺少对编队绝对位置或 leader 位置的约束。智能体发现集体平移不会增加队形误差于是策略往漂移方向收敛——它们学会了「一起错就等于没错」。解决在 function.py 里加上对 leader 位置与期望航迹误差的惩罚项或者把奖励改成以 leader 为中心坐标系下的 follower 误差。注意这个修改要同步调整 state 文件里 offset 的参考系否则队形计算会互相矛盾。5.3 单智能体正常、多智能体协同乱现象单独跑每个 controller 都正常但四个一起就不成队形互相绕圈偶尔碰撞。原因十有八九是 replay buffer 拆开存了经验导致集中式 critic 采样时拿到的是不同时间戳的观测。这在前面 replay_buffer.py 一节已经强调过是多智能体版本最容易踩的结构性坑。解决检查 buffer 的 add 方法确认传入的是一个包含所有智能体数据的整体 transition 元组而不是逐 agent 存储。修完这个协同问题通常会消失一半剩下的再查奖励函数有没有给足协同信号。5.4 读 state 文件报维度错误现象main.py 一启动就报 ValueError提示数组维度不匹配或者矩阵乘法的维度对不上报错堆栈指向 actor_critic 网络。原因state1.txt 到 state4.txt 的行列数与 actor 网络输入维度不一致。常见的是第一次打开包的人把 txt 文件里的空格或换行格式改了或者把偏移量列删了。解决写一个三行脚本把四个文件全部读出来打印 shape对着 params 里的观测维度逐个核对比盯着报错信息猜快得多。另外检查一下四个文件的列数是否一致别人给的资源里偶尔会出现某个文件少一列的情况。5.5 同样的参数第二次训练结果差很远现象第一次跑到 800 回合编队误差已经降到预期重开一次训练结果完全不同指标时好时坏。原因MADDPG 对随机种子非常敏感。环境初始化噪声、探索噪声、网络权重初始化任何一个不固定结果都会飘。这是深度强化学习的常态不是代码 bug也不用怀疑资源有问题。解决main.py 入口强制设置随机种子包括 numpy、torch、以及环境内部的 random 模块三处都要设。验证测试时必须关掉探索噪声再评估否则每次跑出来的指标都带随机性没法横向比较。注意以上排查顺序是我实际用的套路——先查 buffer 结构再查维度再查奖励尺度最后才怀疑超参数。顺序反了容易被 NaN 这类表象带偏浪费一整天在没用的调参上。6. 验证与扩展test.py 跑通之后再改三处训练结束后test.py 的工作是加载模型、关掉探索噪声、跑若干个验证回合然后输出队形误差和碰撞次数的统计。一个合格的验证脚本至少要包含两部分一是逐回合的队形误差曲线数据二是最终队形的可视化输出。如果你只想快速确认模型没学废看一个指标就够——验证回合里队形误差的均值是否下降到了训练早期的三分之一以下同时碰撞次数不为零的话优先处理碰撞。跑通 test.py 之后我建议你做三件扩展每一件都只需要动一小块代码。第一件是把固定编队改成队形切换。目前 Controller 里的 formation_offset 是写死的常量可以把它改成随时间切换的变量比如前 300 回合要求三角队形、后 300 回合要求菱形队形。这样训练出来的策略比纯粹保持单个队形有价值得多也更接近真实任务中编队需要变换阵型的需求。改动位置在 controller 的 offset 读取处和 function.py 的误差计算处网络和 maddpg.py 完全不用动。第二件是给 critic 增加局部观测的遮罩实验。MADDPG 的集中式 critic 在训练时需要全量信息但你可以通过在 maddpg.py 的更新函数里对 obs_all 做切片模拟「critic 只看部分邻居」的情况测试编队精度随观测范围的衰减规律。这个结果在写论文或做方案汇报时会很有说服力——它量化了信息共享程度和编队性能之间的关系。第三件是把奖励函数里的距离度量从 L2 范数换成带权重的距离让编队对纵向和横向误差有不同的敏感度。很多实际编队场景对前后间距的容忍度比左右间距大这个改动只涉及 function.py 里的一行计算却能显著影响学出来的队形形态。关于编队控制学习这件事我自己的血泪经验是不要迷信网络结构编队任务八成的问题出在奖励设计和数据流上。从那以后我每次跑 MADDPG 编队实验都强制走一遍固定流程——先验证 state 维度对得上再确认 buffer 是整段存再检查奖励的惩罚项数值不超界最后才开训练。这套流程帮我避掉了一半以上的无效训练希望帮到你。本文还有配套的精品资源点击获取