ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

多智能体强化学习无人机三维路径规划:MAPPO算法与Python工程实践

多智能体强化学习无人机三维路径规划:MAPPO算法与Python工程实践 简介这是一份面向具备Python、机器学习及强化学习基础的研究人员、工程师和高年级学生的无人机三维路径规划项目实例围绕多智能体强化学习MARL实现多无人机协同避障与高效路径规划。项目重点展示了三维连续空间环境搭建、MAPPO算法的局部观测与集中式价值网络设计、安全约束融入、候选路线评估与自动路线选择机制并配有实时仿真GUI覆盖从模型训练、评估到工程部署验证的完整流程。压缩包为单个docx文档约141KB内含完整程序代码、GUI设计说明和代码详解目录按项目背景、模型架构、代码示例、应用领域等模块组织便于循序渐进研读与实践。已有98人浏览学习适合城市低空物流、森林巡检、应急救援等复杂三维场景下的多无人机协同任务研究也可作为RL连续控制和CTDE等方向的可复现实验基础。1. 多智能体强化学习无人机三维路径规划一套能跑通还带GUI的Python工程如果你是刚开始接触多智能体强化学习MARL又恰好在做无人机三维路径规划大概率会经历三段折腾三维环境写得太简陋训练根本不收敛MAPPO参数调起来像玄学最后想要一个能观察实时决策的可视化界面还得从头拼。这份资源把三条线合进一个Python工程环境是三维连续坐标障碍物用球体、长方体描述无人机有独立的位置、速度、剩余能量和局部观测算法走集中训练、分散执行的CTDE路线策略网络直接输出三维连续动作集中式评论家评估联合状态价值工程还带一个中文GUI支持切换地图、加载模型、单步运行和实时绘制三维路线从训练到展示是一条完整链路。适合拿来做毕业设计、论文实验或者作为多无人机物流配送、电力巡检等场景的预研原型。2. 三维连续环境建模先把无人机的“身体”和“眼睛”搭起来2.1 状态向量与动作更新三维路径规划里的物理约束怎么写强化学习环境要表达的不只是一张三维地图而是一整套状态转移规则。项目采用连续坐标而不是网格离散每架无人机的状态包括位置、速度、目标点、剩余能量和完成标记。动作则定义为三个连续分量分别控制前后、左右、上下方向。之所以把动作定义成速度控制量而不是位移量是为了让策略输出在物理上可解释网络给出的是这个方向上的期望速度环境负责缩放和限幅。class UAV3DEnv: def __init__(self, map_size100.0, max_speed3.0, num_uavs3, dt0.5): self.map_size map_size self.max_speed max_speed self.num_uavs num_uavs self.dt dt self.uav_radius 1.0 self.obstacles [] # 每个元素为 (center, radius) self.uavs [] # 每架无人机含 pos, vel, energy self.targets [] def step(self, actions): rewards, dones [], [] for i, act in enumerate(actions): # 动作先按最大速度缩放再乘时间步长得到位移 vx act[0] * self.max_speed vy act[1] * self.max_speed vz act[2] * self.max_speed pos self.uavs[i][pos] np.array([vx, vy, vz]) * self.dt self.uavs[i][pos] np.clip(pos, 0.0, self.map_size) obs self._build_all_obs() for i in range(self.num_uavs): rewards.append(self._reward(i)) dones.append(self._is_done(i)) return obs, rewards, dones代码逻辑说明step函数先按max_speed把[-1,1]的动作映射成实际速度再乘dt得到这一步的位移np.clip只处理边界不处理障碍物碰撞碰撞判定单独实现。参数上max_speed和dt直接影响训练难度速度太快单步位移大无人机会直接穿过狭窄通道dt太大则碰撞检测不连续。我一般会让单步位移小于最小障碍物半径的三分之一这样轨迹不容易出现“看着合理、实际穿模”的情况。碰撞判定在三维空间里如果全用网格离散计算量会大得离谱。项目采用几何体距离判定球体有天然的解析公式def check_collision(self, pos, uav_idx): # 先查障碍物再查其他无人机 for center, radius in self.obstacles: if np.linalg.norm(pos - np.array(center)) radius self.uav_radius: return True for j in range(self.num_uavs): if j ! uav_idx: other np.array(self.uavs[j][pos]) if np.linalg.norm(pos - other) 2 * self.uav_radius: return True return False说明用球体近似的好处是每次判定只需要一次向量求模计算复杂度O(障碍物数量无人机数量)适合强化学习里高频调用。如果换成真实建筑信息模型建议先用包围球或AABB粗筛再做三角面片精确判断否则训练速度会慢到让人怀疑人生。这里的uav_radius是物理半径在奖励函数里还可以再膨胀一圈用来鼓励无人机远离障碍物而不是贴着表面飞。这个膨胀操作会让训练初期的碰撞率明显下降值得一试。2.2 局部观测拼接固定维度怎么容纳数量可变的邻居和障碍物每架无人机的局部观测包括自身归一化位置、自身速度、目标相对位置、最近障碍物相对位置、最近邻居相对位置和剩余能量。写代码时第一个要解决的问题是维度固定化策略网络是MLP输入维度写死了但不同场景里障碍物和无人机的数量可能不同。项目保留最近的前K个障碍物和前M架邻居不足就补零。def _build_obs(self, uav_idx): pos np.array(self.uavs[uav_idx][pos]) target np.array(self.targets[uav_idx]) obs [] obs.extend(pos / self.map_size) # 自身位置归一化 obs.extend(self.uavs[uav_idx][vel] / self.max_speed) # 速度归一化 obs.extend((target - pos) / self.map_size) # 目标相对位置 obs.extend([self.uavs[uav_idx][energy] / self.max_energy]) obs.extend(self._topk_obstacle_features(pos, K5)) # 最近K个障碍 obs.extend(self._topk_neighbor_features(pos, M3)) # 最近M架邻居 return np.array(obs, dtypenp.float32)这部分没有太多花哨操作但细节决定训练效果。排序后的特征才有意义网络只能看到“最近的障碍物在哪”如果障碍物列表本来无序每次输入的顺序一变网络输入分布就变了训练很难稳定。项目里也在障碍物数量不足K时填充0保证维度固定。为什么要引入邻居相对位置多机避碰不能只靠自身位置推算你还要知道旁边那架无人机的相对方位和距离。把邻居特征放进去策略网络才可能学会“等别人先过”而不是两架无人机在空中互相试探。集中式状态则更直接把所有无人机的局部观测拼成一个长向量送给评论家不要求实时通信只在训练阶段使用。这里我多说一句经验观测归一化尽量在环境内部完成而不是丢给网络加一层BatchNorm。多智能体场景下每个批次的观测分布不一致BatchNorm会引入额外非平稳性训练过程容易时好时坏。把归一化放在环境侧逻辑更简单复现也更省心。3. 策略网络与集中式评论家MAPPO在三维连续动作空间的落地3.1 高斯策略网络输出均值、方差并用tanh限制动作边界MAPPO的策略网络本质是一个连续动作生成器输入局部观测输出三维高斯分布的均值和对数标准差。动作从分布中采样再经过tanh压缩到[-1,1]最后在环境中缩放成实际速度。这样设计的好处是探索和利用可以共存均值决定大致方向标准差控制探索幅度。训练阶段用采样动作保持随机性推理阶段取均值动作避免抖动。class GaussianPolicy(nn.Module): def __init__(self, obs_dim, act_dim3, hidden_dim256): super().__init__() self.trunk nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), ) self.mean_head nn.Linear(hidden_dim, act_dim) self.log_std nn.Parameter(torch.zeros(act_dim)) def forward(self, obs, deterministicFalse): h self.trunk(obs) mean torch.tanh(self.mean_head(h)) # 锁到 [-1,1] 区间 std torch.exp(torch.clamp(self.log_std, min-2.0, max1.0)) if deterministic: return mean dist torch.distributions.Normal(mean, std) action dist.sample() return action, mean, std逻辑说明mean_head前面加tanh把动作均值锁在[-1,1]区间与环境约定一致。log_std作为可学习参数而不是网络输出层是为了让探索方差在训练中变化更平滑clamp限制在[-2,1]可以防止方差过早塌缩到0导致策略彻底失去探索能力。hidden_dim取256对三维连续控制已经够用输入观测一般也就几十维两层MLP足以先把空间关系编码出来。如果你要处理更复杂的编队拓扑再换成图神经网络或注意力机制不迟但训练和调试成本都会上一个台阶建议先把MLP版本跑通。3.2 集中式评论家与轨迹缓存信用分配到底分的是什么集中式评论家的代码看起来朴素到让人怀疑它才是主角class CentralizedCritic(nn.Module): def __init__(self, joint_obs_dim, hidden_dim256): super().__init__() self.net nn.Sequential( nn.Linear(joint_obs_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), # 输出联合状态价值 ) def forward(self, joint_obs): return self.net(joint_obs).squeeze(-1)代码本身不复杂核心在于输入的是joint_obs也就是所有无人机观测拼接起来的全局向量。训练时评论家根据全局向量预测状态的期望回报策略网络更新时通过优势函数间接获得“如果我在当前编队状态下选这个动作整体回报是高是低”的信息。这就缓解了多智能体场景里最难缠的信用分配问题当三架无人机同时移动导致碰撞时评论家能分辨出是哪一架动作的边际影响更大再把批评信号反馈给对应的策略网络。MAPPO的评论家通常预测状态价值V(s)不拼接联合动作。这也意味着轨迹缓存不仅要存每个智能体自己的状态、动作、奖励、下一观测还要存全局联合状态用来计算时序差分的价值目标。我在小规模仿真里会直接用字典缓存一整段轨迹但注意不要边收集边更新否则价值网络的目标一直在动训练方差会被拉得很大。dataclass class Transition: obs: np.ndarray # 单机局部观测 action: np.ndarray # 三维动作 reward: float done: bool joint_obs: np.ndarray # 集中式评论家的输入 next_joint_obs: np.ndarray log_prob: float # 采样该动作的对数概率PPO更新必需轨迹缓存里保存log_prob是PPO系列算法绕不开的步骤。更新时需要通过新旧策略在相同动作上的对数概率之差计算概率比再施加clip限制。如果没有这条记录就没法判断“这个动作按新策略看来是不是比旧策略更可能被选中”策略更新幅度也就无法控制。这一行看起来不起眼却是整个MAPPO更新的地基。3.3 观测归一化与梯度裁剪两个常被忽略的稳定性来源在继续更新公式之前还要提两个容易忽视的环节。强化学习对观测尺度敏感位置在0到100之间、速度在-3到3之间、奖励可能在几十上下这些值直接喂进网络后不同维度的梯度量级可能相差上百倍。常见做法是在环境侧做归一化再把奖励除以滑动平均的绝对值让奖励大致保持在1的量级。梯度裁剪则是对策略网络和评论家网络各自设置max_grad_norm防止某条异常轨迹把参数一步推飞。我一般设max_grad_norm在0.5到1.0之间太小收敛变慢太大约束不住建议作为超参数记下来后面调参时优先排查它。4. MAPPO训练循环与候选路线评估把随机动作变成可执行的飞行策略4.1 从优势计算到梯度更新GAE在三维路径规划里的实际取值强化学习里单步奖励往往是稀疏且有噪声的到达终点给正向奖励碰撞给惩罚但更多时刻是零奖励。直接拿单步奖励做监督信号策略网络梯度会非常不稳定。GAE就是为了解决这个问题的标准工具利用当前价值网络的预测把未来多步的优势累计起来再用lambda参数在偏差和方差之间折中。def compute_gae(rewards, values, next_value, dones, gamma0.99, lam0.95): advantages [] gae 0.0 for t in reversed(range(len(rewards))): if dones[t]: delta rewards[t] - values[t] gae delta # 回合结束不再向后累计 else: delta rewards[t] gamma * next_value - values[t] gae delta gamma * lam * gae # 按时间步反向传播 advantages.append(gae) next_value values[t] advantages.reverse() return np.array(advantages)逻辑说明dones[t]为True说明一个回合结束gae直接重置为delta否则按公式累计到下一步。gamma0.99表示智能体重视长期回报适合路径规划这种需要绕远路避开障碍的任务lam0.95是PPO里常见的默认值越小方差越低但偏差越大越大则相反。如果训练前期策略老是待在原地不敢动多半是优势被压得太小可以把lam调到0.97到0.98试试。算出优势后还要做归一化减去均值除以标准差。这一步不是可选项。多智能体场景里不同无人机的奖励尺度可能不同不归一化会导致某些个体梯度“嗓门大”压过其他人。我自己踩过这个坑三架无人机中一架经常碰撞惩罚方差特别大训练时把另外两架的策略梯度全带偏了归一化之后训练明显变稳。4.2 MAPPO式更新概率比裁剪、熵正则与参数表MAPPO的核心更新公式和单智能体PPO一致只是每个智能体的轨迹独立处理评论家使用联合状态。代码里最关键的是概率比裁剪def mappo_update(actor, critic, batch, clip_eps0.2, lr3e-4, grad_norm0.5): obs, actions, old_log_probs, returns, advantages, joint_obs batch for _ in range(10): mean, std actor(obs) dist torch.distributions.Normal(mean, std) new_log_probs dist.log_prob(actions).sum(-1) ratio torch.exp(new_log_probs - old_log_probs) surr1 ratio * advantages surr2 torch.clamp(ratio, 1.0 - clip_eps, 1.0 clip_eps) * advantages policy_loss -torch.min(surr1, surr2).mean() entropy_loss -dist.entropy().mean() actor_loss policy_loss - 0.01 * entropy_loss # 熵正则 actor_optimizer.zero_grad() actor_loss.backward() nn.utils.clip_grad_norm_(actor.parameters(), grad_norm) actor_optimizer.step() # 评论家单独用MSE拟合回报目标 value_loss F.mse_loss(critic(joint_obs), returns) critic_optimizer.zero_grad() value_loss.backward() nn.utils.clip_grad_norm_(critic.parameters(), grad_norm) critic_optimizer.step()裁剪的含义是ratio是动作在新旧策略下的概率比如果小于0.8或大于1.2min操作会截住对应梯度意思是“这条数据超出可信范围这次更新先不参考它”。clip_eps0.2是PPO里大量使用的默认值对无人机这种高维连续控制同样适用。熵正则系数0.01是为了保留一点探索空间太小容易过早收敛到局部行为比如无人机永远绕固定方向飞行。关于学习率很多MARL项目把策略和评论家都用3e-4但我通常会把评论家学习率调到1e-3、策略保持3e-4。原因是价值网络的目标本身就在移动学习率太低会追不上策略网络过高则容易在裁剪边界附近震荡。训练时如果loss曲线平稳但任务奖励不涨优先检查评论家loss是否收敛到位。常用参数参考如下参数常见值调整方向gamma0.99任务越长越大一般不超过0.995lam0.95优势噪声大往小调策略不进取往大调clip_eps0.2训练不稳往小调收敛过慢往大调entropy_coef0.01策略早熟就往大调grad_norm0.5梯度爆炸往小调数值健康可不调4.3 候选路线评估把“网络输出”升级为“筛选后的决策”即使策略已经收敛单次采样的动作依然不稳定。项目里设计了一套候选路线评估机制对当前观测生成多个带随机扰动的候选动作每个动作小步推演几步按指标打分最后选得分最高的执行。def select_route(actor, obs, env, uav_idx, num_candidates8, rollout_steps5, noise0.2): best_action, best_score None, -1e9 for _ in range(num_candidates): mean actor(obs, deterministicTrue) action np.clip(mean np.random.randn(3) * noise, -1.0, 1.0) # 安全过滤不满足边界、障碍、邻居约束直接跳过 if not env.check_boundary(uav_idx, action) or not env.check_obstacle(uav_idx, action): continue score env.rollout_score(uav_idx, action, stepsrollout_steps) if score best_score: best_score, best_action score, action if best_action is None: best_action actor(obs, deterministicTrue) # 安全回退到均值动作 return best_action这段逻辑值得仔细体会。rollout_score是短期的模拟评估在当前位置按候选动作推演几步计算离目标更近多少、有没有碰撞风险、能耗消耗多少。候选数8到16、rollout 3到5步已经是性价比不错的配置噪声项noise控制在0.2左右太大会采样出完全无关的莽撞动作太小则所有候选都差不多筛选失去意义。安全过滤优先级高于评分一个会撞墙的动作哪怕回报看起来再好也不能选这正是规则安全层与学习策略结合的价值。另一个坑是候选评估一定要在环境的“克隆副本”上做不能直接修改真实环境状态否则会污染训练轨迹。这个细节不处理训练中后期会出现莫名其妙的奖励倒退。5. 避坑记录训练崩溃、GUI假死与候选评估失效的真实翻车现场5.1 训练奖励断崖下跌数值尺度失衡淹没了有效梯度现象训练前几百轮奖励还能缓慢上升到某个节点突然从正数掉到很大的负数区域后续几十轮再也拉不回来。原因多智能体环境里各类奖励不在同一量级。到达目标给100碰撞给-50距离进展可能只有-0.2到0.3。碰撞惩罚和距离奖励一比梯度被少量剧烈惩罚样本主导价值网络反复震荡。加上多机同时移动任何一次碰撞责任的归属很难自动区分集中评论家还没学会联合价值时就被极端奖励带飞。解决先把奖励结构改成可解释的分层结构个体距离进展、安全间距惩罚、碰撞惩罚、目标到达奖励、能耗代价分开保存。计算总奖励前用滑动平均统计每个子项的量级让碰撞惩罚绝对值控制在距离奖励的5到10倍以内。我把碰撞调成-10、到达奖励给20、距离差值系数0.1训练稳定性明显改善。再配合课程学习前几万步不设置障碍先让无人机学会奔向目标之后逐步加入障碍物和邻居避碰。这个“先学方向、再学避障”的顺序几乎成了多智能体强化学习的标配手段能少走很多弯路。5.2 换一张新地图就变路痴模型死记硬背布局而不是学会避障现象训练地图上完成率能跑到95%以上把障碍物位置随机换一换完成率直接掉到20%以下。原因训练时地图布局固定或随机范围太小策略网络其实背下来了固定的障碍坐标。尤其是局部观测传的是绝对坐标而不是相对坐标时模型学到的不是“看到障碍往左绕”而是“在这个坐标点往左拐”换到新场景后这些记忆全部失效。解决回合开始时随机生成障碍物中心位置、半径、无人机初始位置和目标点并保证观测中全部是相对量目标相对位置、障碍物相对位置、邻居相对位置。我还会在评估时固定一套训练中从未出现过的新地图专做泛化测试完成率低于80%就说明观测设计有问题而不是参数问题。如果纯随机地图不够有层次可以把障碍物按“固定可变”两类生成固定布局保证有一个基础环境随机障碍负责压出泛化能力。5.3 GUI点击“开始训练”后窗口卡死主线程被训练循环堵住现象GUI界面点击训练按钮窗口立刻进入未响应状态界面不刷新过几分钟后一次性弹出一堆画面。原因训练循环被直接放到GUI主事件循环所在的线程里。PyTorch的前向、反向传播持续占用CPU或GPU窗口的消息循环无法处理刷新事件于是界面假死。解决把训练放到单独的后台线程里执行GUI线程只负责每隔一段时间读取训练日志、刷新三维画布。如果数据量较大用队列或共享缓冲区传递loss、轨迹坐标更新时加锁。退一步讲如果只是做演示先让训练阶段跑完保存模型参数GUI只做模型加载和路线推理展示效果完全足够还省去线程同步的维护。这也更贴近真实部署流程训练离线做决策在线跑。5.4 候选路线评估选出“纸面最佳”执行两步却撞墙现象候选评估返回的路线分数很高无人机实际沿该方向飞了两步就撞到障碍物。原因评估步长太短只能看到当前一步的风险看不到第二步的转弯。或者扰动采样集中在一个狭窄方向候选动作没有覆盖多个可行走向筛选出来的只是“矮子里拔将军”。解决先把rollout_steps从3调到8以上并把评分函数里的碰撞惩罚权重上调。再把随机扰动的生成改成多方向采样比如在基础动作上分别叠加“向左绕”“向右绕”“向上翻越”等不同偏移确保候选集合内有不同策略。碰撞检查要在候选评估前做一次硬过滤硬过滤没过分数再高也不能选。项目里保留的“安全动作过滤”模块就是最后一道防线推理阶段策略输出先过规则层校验再交给执行对象凡是涉及真实场地飞行都不该省略。5.5 训练后期无人机原地盘旋熵正则与奖励信号同时失效现象训练中期奖励还在上升后期曲线平稳但无人机在新回合里经常长时间原地盘旋到达目标时间明显拉长。原因策略网络熵系数设置太小探索能力随训练推进被压到接近零模型过早锁定了一个局部行为同时奖励函数里缺少对“原地不动”的惩罚盘旋动作虽然没碰撞但也没有触发任何负反馈。解决把熵正则系数从0.01适当提高到0.02到0.05让策略在后期仍保留转向和尝试其他方向的可能。再检查奖励函数里是否有“应该动起来”的信号我通常在距离进展项之上再加一个小的速度激励项例如当前速度与最低速度的差乘以一个很小的系数悬停时能量消耗照样扣但前进距离为零策略就会被推着往前走。训练奖励曲线平稳并不代表策略健康要配合回合平均步数一起看步数越来越长但奖励不降基本就在原地空转。6. 最后的落地技巧把安全动作过滤写成硬性关卡再谈模型上真机6.1 安全动作过滤与模型部署验证这份完整工程把环境建模、策略网络、集中式评论家、训练循环和GUI都封装好了拿到手能直接跑。但真正要把它搬到实地飞行或接入飞控之前还有最后一道工序我认为最值得认真对待——安全动作过滤。无论策略网络输出什么都先经过这样一个过滤器def safe_action_filter(uav, action, env, max_retry5): candidate np.array(action, dtypenp.float32) for _ in range(max_retry): if (env.check_boundary(uav, candidate) and env.check_obstacle(uav, candidate) and env.check_neighbor(uav, candidate)): return candidate candidate candidate * 0.6 # 逐步缩小动作幅度 return np.zeros(3, dtypenp.float32) # 回退为悬停这个过滤器的逻辑很清楚动作不满足安全条件就按0.6倍逐步缩小重试5次仍不过就返回零动作让无人机悬停等待下一轮规划。比起动作不合法直接忽略、什么都不做悬停至少保留了重新决策的机会实际飞行中更安全。参数上0.6和max_retry5不是随便拍的缩得太慢重试次数不够缩得太快动作会过早变成原地停。我通常让重试后还能保留原始意图的30%左右也就是大约0.6的5次方附近。验证模型能不能用我有固定三件套第一加载模型后在固定地图上统计完成率、碰撞次数、平均路径长度第二在随机地图上统计同样的指标两次都合格才算泛化第三看一眼rollout曲线确认最小安全距离没有贴着障碍物半径极限。任何一条不达标都回去查观测设计和奖励权重而不是盲目加大训练步数。模型参数保存时推荐把整个actor网络的state_dict存下来同时把obs_dim、act_dim、hidden_dim等信息存成json一起归档这样过几个月回来重新跑工程不用靠记忆猜网络结构。部署前还要把训练阶段的归一化统计量同步保存下来推理时用同一个均值和标准差做预处理否则训练和推理的观测分布不一致模型表现会凭空掉一大截。从那以后我每次做多智能体路径规划项目都会强制走一遍这套流程先检查奖励分布再检查安全过滤最后才谈调参和部署。希望帮到你。本文还有配套的精品资源点击获取
返回列表