ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MADDPG编队控制源码包全解析:训练、调参与避坑实践

MADDPG编队控制源码包全解析:训练、调参与避坑实践

简介:这是一份以FCMADDPG命名的多智能体编队控制学习项目,基于MADDPG算法实现,面向深度强化学习研究者与机器人、无人系统开发者,解决多智能体如何通过试错学习形成并维持特定编队的问题。压缩包共18个文件,以13个Python脚本为主,涵盖算法核心(MADDPG与DDPG代理)、演员-评论家网络、经验回放缓冲区、参数配置及训练/测试主程序,另有4个状态记录txt文件与1个编译缓存pyc,整体仅11KB,结构紧凑便于快速定位关键模块。目前已有1785人学习下载。通过研读代码,读者可掌握MADDPG在多智能体协同场景下的完整实现流程,理解确定性策略梯度与集中式训练、分布式执行的具体写法,并可直接修改参数和网络结构,用于无人机编队、自动驾驶车队等方向的仿真实验与算法改进。

1. FCMADDPG 是给谁的:一套 MADDPG 编队控制的可运行源码包

做无人机编队、无人车队列控制的工程师,大概率遇到过这种处境:PID 和一致性协议在固定工况下没问题,队形一变、智能体数量一变,参数就得从头调。

FCMADDPG 这个压缩包就是奔着这个痛点去的——它用 MADDPG(Multi-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/队形控制器每个智能体一个 controller
state/初始状态与队形数据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.py:CTDE 范式落到代码上

MADDPG 的核心思想是 centralized training with decentralized execution(CTDE):训练时 critic 能看到所有智能体的观测和动作,执行时每个 actor 只用自己的观测。这个包里的 actor_critic.py 正是按这个逻辑实现的:

import torch import torch.nn as nn class Actor(nn.Module): """每个智能体一个 actor,只输入自己的观测,输出确定性动作""" def __init__(self, obs_dim, act_dim, hidden=64): 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, hidden=64): 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, noise=True) 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(), f"model_{episode}.pth")

这段逻辑里值得注意的有两点。一是noise=True,训练时 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, dim=1) 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.0
vx, vy初始速度0.0, 0.0
offset_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 是整段存,再检查奖励的惩罚项数值不超界,最后才开训练。这套流程帮我避掉了一半以上的无效训练,希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进