ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多智能体强化学习无人机三维路径规划:从建模到可视化全解析

多智能体强化学习无人机三维路径规划:从建模到可视化全解析 简介面向人工智能、机器人、无人机控制等领域研究人员与工程师的Python实战项目基于多智能体强化学习MARL构建无人机三维路径规划系统。系统采用MAPPO算法构建三维连续空间环境通过局部观测与集中式价值网络实现协同策略训练支持多无人机自主避障与高效路线自动筛选可适配城市低空物流、森林巡检、应急救援等典型三维场景。资源包为单个docx文档141KB内含完整程序代码、GUI设计说明与代码详解覆盖环境建模、障碍物判定、高斯策略网络、集中式价值网络、轨迹缓存与优势计算、MAPPO式训练更新及自动路线选择评估等核心模块。目前已有98人学习。借助文档中的GUI仿真系统读者可深入理解奖励函数设计、安全约束融入与候选动作评估机制掌握从理论到工程部署的全流程为后续算法迭代与系统集成提供可复现的实验基础。1. 多智能体强化学习做无人机三维路径规划为什么单机规划器在编队里频繁失灵多智能体强化学习MARL做无人机三维路径规划很多人第一反应是问A*、RRT、人工势场这些无人机路径规划算法都成熟了为什么还要折腾强化学习答案是这些经典算法给的是“单机静态环境”的解法。把场景换成 35 架无人机同时在障碍稠密的三维空间里飞它们要么重规划频率过高要么互相把对方路径堵死。MARL 的价值不是“算得更快”而是让每架无人机在训练中学会跟队友妥协——谁先过、谁避让、谁去占位这是传统规划器给不了的能力。这篇内容从问题建模、Python 训练代码、PyQt5 可视化到实际踩坑把一套可直接扩展的 MARL 三维路径规划系统完整讲清楚。适合在做编队飞行、协同巡检、物流配送仿真的工程师直接参考。2. 把三维路径规划建模成 MARL 问题状态空间、动作空间与奖励函数的落地设计MARL 项目最容易在第一步就翻车直接把单智能体强化学习的“状态-动作-奖励”套进多机场景结果训练到一半发现每架无人机都在争同一个目标。这一章先把问题定义清楚后面所有代码都建立在这套定义上。2.1 为什么不用集中式规划器动态编队与局部观测的现实约束先回答一个必然要面对的问题既然计算资源够为什么不用一个集中式规划器统一给所有无人机算路径集中式 A* 或带约束的优化求解器例如混合整数规划确实能算全局最优解但代价是状态维度随无人机数量指数膨胀而且它对环境动态变化非常敏感——障碍物或任务目标一改整个联合规划要重跑一遍无人机在飞行中根本等不起这个重规划周期。另一个被忽略的问题是通信假设。集中式规划默认所有信息汇聚到一个中心节点但实际编队飞行里每架无人机只能通过机间链路拿到邻居的相对位置和目标信息局部观测是天然的约束。MARL 的 CTDE集中训练、分布式执行框架正好卡在这个点上训练阶段用一个中心 Critic 看全局信息帮助每个智能体学会协作部署阶段每个智能体只用自己的 Actor 网络和局部观测做决策。这就是为什么多无人机路径规划场景里MARL 比集中式求解器更贴合实际工程约束。2.2 状态空间怎么定义三维坐标归一化、相对目标与邻居信息多智能体强化学习的状态空间设计不能直接堆叠绝对坐标否则同一张地图换个起点网络输出就乱了。我常用的做法是给每架无人机构造一个“自身视角”的观测向量全部做归一化处理让模型对地图尺寸和初始位置不敏感。import numpy as np class Uav3DEnv: def __init__(self, num_uavs4, bounds(100, 100, 50)): self.num_uavs num_uavs self.bounds np.array(bounds, dtypenp.float32) self.uavs [] def _get_state(self, uav_idx): my_pos self.uavs[uav_idx] target self.targets[uav_idx] state [] # 自身位置归一化到 [0,1]避免数值尺度过大 state.extend(my_pos / self.bounds) # 到目标的相对向量同样归一化 state.extend((target - my_pos) / self.bounds) # 最近障碍物距离除以 50 做尺度压缩 state.append(self._nearest_obstacle_dist(uav_idx) / 50.0) # 当前无人机剩余油箱/电量简单归一化 state.append(self.energy[uav_idx] / 100.0) # 邻居相对位置按通信范围内的机间距离填充 for j in range(self.num_uavs): if j ! uav_idx: rel (self.uavs[j] - my_pos) / self.bounds state.extend(rel) return np.array(state, dtypenp.float32)这段代码的要点在状态组成自身位置、目标相对位置、障碍物距离、电量、邻居相对位置。其中“目标相对位置”比绝对目标坐标泛化能力强得多换目标点不需要重训练邻居相对位置则是在训练中学会机间避碰和编队保持的关键字段。归一化不是可选项真实三维地图里坐标动辄上百米Q 值网络第一层就会吃进尺度差异巨大的输入梯度更新很容易被大数值维度带偏。如果你后续想接入无人机视觉感知常规做法是在这个状态向量基础上拼接视觉特征向量但要注意视觉特征维度高DQN 这类离散动作算法会明显更难收敛建议先保持低维状态跑通再接感知模块。2.3 动作空间离散 6 方向还是连续速度控制多智能体强化学习的动作空间选择直接影响算法选型这个要在项目开始前就定死。做无人机三维路径规划常见有两种动作设计离散动作6 个标准方向前、后、左、右、上、下加一个悬停动作维度低DQN 和其变体可以直接用训练稳定、算力要求低适合 35 架无人机的编队避障验证。连续动作每个智能体输出三维速度向量 (vx, vy, vz) 或水平速度、垂直速度、偏航角动作维度 34必须用 MADDPG、SAC 这类连续控制算法精度高但训练时间翻倍且调参难度明显上升。我一般建议第一次做 MARL 路径规划先用离散 6 方向跑通整套流程验证奖励函数和状态设计是否合理再切换到连续动作升级精度。很多公开的无人机仿真项目里离散动作配合共享 DQN 就能达到不错的编队避障效果没必要一上来就上连续控制。2.4 奖励函数是训练成败的胜负手到达、避障、协作三项的权重建议奖励函数是 MARL 里最像玄学的地方但它其实是可拆解的。三维路径规划场景下我通常把奖励分成四个独立部分每一部分单独计算并打日志训练时哪项出问题一眼就能看出来。def _compute_reward(self, uav_idx, prev_pos, new_pos, hit_obstacle, out_of_bounds): target self.targets[uav_idx] prev_dist np.linalg.norm(prev_pos - target) new_dist np.linalg.norm(new_pos - target) reward 0.0 # 1) 距离塑形靠近目标给正奖励远离给负奖励 reward 0.5 * (prev_dist - new_dist) # 2) 到达目标一次性大奖励并终止该智能体 if new_dist 3.0: reward 10.0 self.done[uav_idx] True # 3) 碰撞惩罚撞障碍物或撞机给强负奖励并终止 if hit_obstacle: reward - 50.0 self.done[uav_idx] True # 4) 越界惩罚飞出地图边界 if out_of_bounds: reward - 30.0 self.done[uav_idx] True # 5) 能耗惩罚鼓励走短路径动作幅度越大代价越高 energy np.linalg.norm(self.last_actions[uav_idx]) reward - 0.05 * energy return reward几个权重需要特别说明。距离塑形系数 0.5 不能太小也不能太大太小导致无人机原地打转太大会让无人机贴着障碍物边缘“蹭”向目标因为近距离跨越障碍物带来的距离收益超过碰撞惩罚。到达奖励 10.0 与碰撞惩罚 50.0 的比例也很关键如果到达奖励远大于碰撞惩罚智能体会学到“顶着碰撞风险莽向目标”反过来则会过于保守停在原地不动。我的经验是从 1:5 的比例起步调先保证智能体敢动再逐步增大碰撞惩罚。更重要的是协作奖励。上面这段代码只有单机奖惩编队场景一定要加机间距离约束。常见做法是两机距离小于安全阈值 1.5 米时双方各扣一次惩罚距离在 5 到 15 米之间时给一个小的正向奖励鼓励编队保持。这个“安全距离带”的上下界要根据无人机尺寸和飞行速度定不要照搬别人的数值。奖励各项权重记录在一张表里方便复现对比奖励项典型值作用距离塑形系数0.3~0.8引导持续接近目标到达奖励10~20最终目标达成碰撞惩罚30~100障碍物与机间避碰越界惩罚20~50保持在地图边界内能耗惩罚系数0.02~0.1抑制绕路和无效动作3. 用 Python 从零跑通多智能体 DQN环境搭建、共享经验回放与训练循环这一章直接给出可运行的 Python 实现。为了让代码能在普通电脑上跑起来先用多智能体共享参数的 DQN 作为基线算法逻辑清楚硬件要求低跑通之后再讲升级到 MADDPG 的关键改动。3.1 三维环境搭建栅格地图、障碍物与无人机运动学环境是无人机仿真的第一步也是后面所有代码的地基。这里实现一个轻量三维环境包含位置更新、碰撞检测和边界检查。import numpy as np import random class Uav3DEnv: def __init__(self, num_uavs4, bounds(100, 100, 50), targetsNone): self.num_uavs num_uavs self.bounds np.array(bounds, dtypenp.float32) if targets is None: self.targets [np.array([80, 80, 40], dtypenp.float32) for _ in range(num_uavs)] else: self.targets [np.array(t, dtypenp.float32) for t in targets] # 障碍物用球体中心坐标表示半径为固定值 2.0 self.obstacles [np.array([40, 40, 20], dtypenp.float32), np.array([60, 30, 30], dtypenp.float32), np.array([30, 60, 35], dtypenp.float32)] self.uavs [] self.done [] self.last_actions [] self.energy [] def reset(self): self.uavs [] self.done [False] * self.num_uavs self.energy [100.0] * self.num_uavs self.last_actions [np.zeros(1) for _ in range(self.num_uavs)] # 起始区域集中在左下角目标在右上角形成明显的路径规划压力 for _ in range(self.num_uavs): self.uavs.append(np.array([ random.uniform(0, 5), random.uniform(0, 5), random.uniform(0, 5)], dtypenp.float32)) return [self._get_state(i) for i in range(self.num_uavs)] def step(self, actions): step_size 2.0 offset_map { 0: [1, 0, 0], 1: [-1, 0, 0], 2: [0, 1, 0], 3: [0, -1, 0], 4: [0, 0, 1], 5: [0, 0, -1], 6: [0, 0, 0] # 悬停通常只用于调试 } new_uavs [] rewards [] for i in range(self.num_uavs): if self.done[i]: rewards.append(0.0) new_uavs.append(self.uavs[i]) continue offset np.array(offset_map[int(actions[i])], dtypenp.float32) new_pos self.uavs[i] offset * step_size hit_obs self._hit_obstacle(new_pos) out self._out_of_bounds(new_pos) hit_uav self._hit_uav(i, new_pos) self.last_actions[i] offset if not (hit_obs or out or hit_uav): new_uavs.append(new_pos) else: new_uavs.append(self.uavs[i]) # 撞了就不动 rewards.append(self._compute_reward(i, self.uavs[i], new_uavs[-1], hit_obs or hit_uav, out)) self.energy[i] - 0.1 self.uavs new_uavs return [self._get_state(i) for i in range(self.num_uavs)], rewards, self.done这个环境类把无人机运动学简化成了“固定步长移动”每步 2 米6 方向离散动作。如果你的项目对运动学精度有要求可以改成连续速度 加速度限制但训练复杂度会上去。障碍物用球体中心点表示、统一半径 2.0是为了让碰撞检测用欧氏距离一行算完真实地形可以用更复杂的网格模型替换 _hit_obstacle 内部实现。边界检查_out_of_bounds要严格处理任何一维坐标超出 [0, bounds[i]] 就判越界。曾经遇到一个案例是边界判断用了没加无人机贴边滑行不出界奖励函数在边界附近产生错误的局部最优所以边界检查建议写成pos[i] 0.0 or pos[i] self.bounds[i]。3.2 共享参数 DQN 与经验回放每个智能体独立采样、共同学习多智能体场景最朴素的做法是让所有无人机共享同一个 DQN 网络参数每架无人机用自己的状态和动作去采样经验统一放进同一个回放缓冲区。这个做法在无人机数量不多35 架时收敛效果好训练速度快代码也简单。它本质上把“每架无人机的经验”当作独立的样本来用经验效率比单智能体更高。import torch import torch.nn as nn import torch.optim as optim class DQN(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim) ) def forward(self, x): return self.net(x) class ReplayBuffer: def __init__(self, capacity100000): self.capacity capacity self.buffer [] def push(self, s, a, r, s_, d): self.buffer.append((s, a, r, s_, d)) if len(self.buffer) self.capacity: self.buffer.pop(0) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) s torch.FloatTensor([b[0] for b in batch]) a torch.LongTensor([b[1] for b in batch]).unsqueeze(1) r torch.FloatTensor([b[2] for b in batch]) s_ torch.FloatTensor([b[3] for b in batch]) d torch.FloatTensor([b[4] for b in batch]) return s, a, r, s_, d共享参数为什么能成立因为每架无人机的状态空间和动作空间是同构的它们学习的目标本质上也是同一个策略模式——靠近目标、躲开障碍、保持编队。共享参数相当于让所有智能体把经验汇集在一起训练一个“公共大脑”这在同构智能体场景里是一个合理且高效的简化。但要注意如果无人机分工不同比如一架负责侦察、一架负责打击这个假设就不成立了需要为不同角色分配独立网络。3.3 训练循环epsilon 衰减、目标网络同步与损失计算训练循环是所有强化学习项目里最容易“看起来在跑、实际没在学”的部分。下面是完整的训练主循环重点在于经验采样的频率和目标网络同步的时机。def train(): state_dim 4 4 1 1 (env.num_uavs - 1) * 3 # 按 _get_state 计算 action_dim 7 net DQN(state_dim, action_dim) target_net DQN(state_dim, action_dim) target_net.load_state_dict(net.state_dict()) optimizer optim.Adam(net.parameters(), lr1e-3) buffer ReplayBuffer(100000) gamma 0.99 batch_size 128 target_update_freq 200 for episode in range(2000): obs env.reset() total_reward 0.0 # epsilon 从 1.0 线性降到 0.05前 500 局保持探索 eps max(0.05, 1.0 - episode / 400) for step in range(300): actions [] for i in range(env.num_uavs): if random.random() eps: actions.append(random.randrange(action_dim)) else: with torch.no_grad(): q net(torch.FloatTensor(obs[i]).unsqueeze(0)) actions.append(int(torch.argmax(q).item())) new_obs, rewards, dones env.step(actions) for i in range(env.num_uavs): buffer.push(obs[i], actions[i], rewards[i], new_obs[i], dones[i]) obs new_obs total_reward sum(rewards) # 经验够了才开始训练网络 if len(buffer.buffer) batch_size: s, a, r, s_, d buffer.sample(batch_size) q_pred net(s).gather(1, a) with torch.no_grad(): q_next target_net(s_).max(1, keepdimTrue).values y r.unsqueeze(1) gamma * q_next * (1 - d.unsqueeze(1)) loss nn.MSELoss()(q_pred, y) optimizer.zero_grad() loss.backward() optimizer.step() # 每 200 步同步一次目标网络 if step % target_update_freq 0: target_net.load_state_dict(net.state_dict()) if all(dones): break if episode % 50 0: print(fepisode {episode}, reward {total_reward:.2f}, eps {eps:.2f})几个参数值得解释。epsilon 衰减速度决定了探索和利用的平衡衰减太快会陷入局部最优衰减太慢则迟迟不收敛上面从 1.0 到 0.05、约 400 局完成衰减只是一个起点如果你的地图更大、障碍更密要适当拉长到 600800 局。目标网络同步频率 200 步也是经验值——太频繁会让训练不稳定太久则目标值滞后严重。损失用普通 MSELoss 就够了不需要花哨的变体。这里有个非常隐蔽的坑env.step(actions)里面actions是 Pythonint列表但DQN网络输出的是 logits直接argmax拿的是动作索引。如果你在step()里拿这个索引查offset_map一定要确认动作编号对齐动作维度设计成 76 方向 悬停时索引 6 对应悬停这个映射错位是训练期最常见也最难排查的 bug。3.4 升级到 MADDPG连续动作与集中式 Critic 的关键改动离散 DQN 跑通之后如果发现 6 方向步进太粗糙或者需要输出连续速度向量就该升级到 MADDPG。它的核心思想是每个智能体有自己的 Actor 网络策略但训练时共享一个集中式 Critic 网络Critic 输入所有智能体的观测和动作从而部分解决多智能体环境的非平稳性问题。class Actor(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, action_dim), nn.Tanh() ) def forward(self, x): return self.net(x) class Critic(nn.Module): def __init__(self, state_dims, action_dims): super().__init__() # 输入是所有智能体的状态和动作拼接 total_in sum(state_dims) sum(action_dims) self.net nn.Sequential( nn.Linear(total_in, 128), nn.ReLU(), nn.Linear(128, 128), nn.ReLU(), nn.Linear(128, 1) ) def forward(self, all_states, all_actions): x torch.cat(all_states all_actions, dim1) return self.net(x)Critic 拿全局信息Actor 只用局部观测这正好对应前面说的 CTDE 模式。MADDPG 训练时每个智能体动作会加一个高斯噪声来保证探索而不是 DQN 的 epsilon 贪心。常见的做法是噪声标准差从 1.0 逐步衰减到 0.1这个衰减速度也属于“靠调”的超参数——噪声太大策略粗野太小则探索不足。Critic 的学习率一般比 Actor 低一个数量级例如 Actor 用 1e-4、Critic 用 1e-3否则 Critic 更新过快会让整个训练崩掉。4. PyQt5 做三维路径规划 GUI训练曲线、实时轨迹回放与模型加载推理代码和算法跑通之后项目要能给别人看、能调参、能复现就需要一个图形界面。很多人觉得 GUI 是加分项不是必选项但做无人机路径规划系统没有可视化就无法判断训练是否合理——你没法确认无人机是真的在避障还是绕了远路。这一章讲用 PyQt5 搭建一个二维面板 三维轨迹回放的可视化工具兼顾训练监控和结果演示。4.1 技术选型为什么选 PyQt5 而不是 TkinterPython 做桌面 GUITkinter 最省事但不能满足三维绘制需求它没有原生 3D 组件用 Canvas 自己画投影又浪费时间。PyQt5 配合 Matplotlib 的 FigureCanvas 是更成熟的组合。import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, \ QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QLabel, QTextEdit from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import matplotlib matplotlib.use(Qt5Agg)这个组合的优点是训练曲线和三维轨迹统一用 Matplotlib 渲染样式可以完全复用之前的绘图代码PyQt5 的信号槽机制天然适合训练线程和界面线程之间的数据分发。如果你有 Web 前端经验也可以考虑用 Gradio 或 Flask 做浏览器端可视化但桌面工具在离线环境和快速原型上仍然更顺手。4.2 界面布局控制面板、训练日志与三维绘图区界面布局按“左侧控制、右上曲线、右下轨迹”的三分区来设计。控制面板放训练参数学习率、回合数、epsilon 衰减系数、开始/停止按钮、模型保存和加载按钮右上区域用 Matplotlib 画奖励曲线和碰撞率曲线右下区域用三维 Axes 画无人机轨迹和障碍物。class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(MARL Uav Path Planning) self.resize(1280, 800) central QWidget() self.setCentralWidget(central) layout QHBoxLayout(central) # 左侧控制面板 left QVBoxLayout() self.lr_input QLineEdit(0.001) self.episode_input QLineEdit(2000) left.addWidget(QLabel(Learning Rate)) left.addWidget(self.lr_input) left.addWidget(QLabel(Episodes)) left.addWidget(self.episode_input) self.btn_start QPushButton(开始训练) self.btn_save QPushButton(保存模型) self.btn_load QPushButton(加载模型) left.addWidget(self.btn_start) left.addWidget(self.btn_save) left.addWidget(self.btn_load) self.log QTextEdit() self.log.setReadOnly(True) left.addWidget(self.log) left.addStretch() # 右侧绘图区 right QVBoxLayout() self.fig Figure(figsize(10, 6)) self.canvas FigureCanvas(self.fig) self.ax_curve self.fig.add_subplot(121) self.ax_3d self.fig.add_subplot(122, projection3d) right.addWidget(self.canvas) layout.addLayout(left, 1) layout.addLayout(right, 4)布局里最容易犯的错是让 QTextEdit 占用大量垂直空间挤压绘图区。用addStretch()把日志组件顶上去保证右侧画布是主视觉区域。另外三维绘图的投影参数要提前设好ax_3d.set_xlim(0, 100)、set_ylim(0, 100)、set_zlim(0, 50)并且固定视角如果每次刷新都自动调整视角轨迹动画会剧烈抖动没法看。4.3 训练线程与信号槽为什么训练循环坚决不能放主线程PyQt5 应用最容易翻车的地方就是训练卡死界面。强化学习训练循环是长时间阻塞操作放进主线程的槽函数里Qt 事件循环被堵住界面的按钮、文本、绘图全部冻结。正确做法是用 QThread 跑训练通过信号把训练数据发回主线程更新界面。from PyQt5.QtCore import QThread, pyqtSignal class TrainWorker(QThread): update_curve pyqtSignal(dict) update_log pyqtSignal(str) def __init__(self, env, net, episodes, lr): super().__init__() self.env env self.net net self.episodes episodes self.lr lr self.running True def run(self): # 训练循环里每 10 个 episode 发一次信号 for ep in range(self.episodes): if not self.running: break avg_reward, arrive_rate, collision_rate self._one_episode() self.update_curve.emit({ episode: ep, avg_reward: avg_reward, arrive_rate: arrive_rate, collision_rate: collision_rate }) self.update_log.emit(fep {ep}: reward{avg_reward:.2f}) def _one_episode(self): # 训练单个回合的逻辑返回统计指标 pass def stop(self): self.running False信号槽的关键在于update_curve信号带着字典参数主线程对应的槽函数只负责更新 Matplotlib 画布不执行任何训练逻辑。训练线程里要每隔若干 episode 检查self.running否则你点了“停止训练”按钮线程不会立刻停下来还得等当前 episode 跑完。4.4 三维轨迹绘制动态更新技巧与坐标系设定三维绘制的性能是 GUI 的一大瓶颈。最直接的做法是每帧ax_3d.clear()然后重新绘制所有轨迹架子小、代码简单但轨迹超过几百个点之后明显掉帧。我常用的优化技巧是轨迹降采样每个回合只画最近 50 个位置点历史轨迹用淡色线条当前帧用亮色线条。def draw_trajectory(self, all_positions_list, obstaclesNone): # all_positions_list: 每架无人机的位置历史列表 self.ax_3d.clear() self.ax_3d.set_xlim(0, 100) self.ax_3d.set_ylim(0, 100) self.ax_3d.set_zlim(0, 50) # 先画障碍物固定形状和颜色 if obstacles: for obs in obstacles: self.ax_3d.scatter(obs[0], obs[1], obs[2], cr, s40, markero) # 再画每条轨迹用不同颜色区分无人机 colors [b, g, y, m, c] for idx, pos_history in enumerate(all_positions_list): recent pos_history[-50:] # 只取最近 50 帧 if len(recent) 1: xs [p[0] for p in recent] ys [p[1] for p in recent] zs [p[2] for p in recent] c colors[idx % len(colors)] self.ax_3d.plot(xs, ys, zs, colorc, alpha0.8) self.ax_3d.scatter(xs[-1], ys[-1], zs[-1], colorc, s20) self.canvas.draw_idle()坐标系设定这里要提一句三维建模坐标系的选择仿真环境里地图原点设在地面左下角Z 轴向上这和真实无人机的地固坐标系如 ENU 东北天存在差异。如果你后面要接真实飞控数据或倾斜摄影三维模型需要做坐标转换但纯仿真阶段统一用地图自身的右手坐标系即可不要在 GUI 里混用两种坐标系否则轨迹显示旋转九十度会很难排查。5. 多无人机 MARL 训练的 5 个翻车现场现象、原因与排查手段训练多智能体强化学习的路上一定会遇到几个反复出现的坑这里整理 5 个我自己踩过也给别人排查过的问题每一条都按“现象、原因、解决”展开可以直接对着自己的训练日志逐条比对。5.1 奖励尺度失衡训练中 loss 变成 NaN曲线瞬间消失现象训练跑到几百步之后loss 突然变成 nan奖励曲线断崖式下跌之后网络输出全是常数。原因奖励函数里到达奖励 10、碰撞惩罚 -50加上距离塑形数值范围横跨 -50 到 10Q 值回归目标不稳定梯度爆炸触发。解决把所有奖励项按比例缩到同数量级。我的做法是碰撞惩罚从 50 降到 10到达奖励从 10 降到 5距离塑形系数从 0.5 降到 0.2让各分项都在 ±10 以内同时在优化器上加上梯度裁剪nn.utils.clip_grad_norm_(net.parameters(), 10.0)。如果已经出现 NaN先重置模型再用缩小后的奖励重新训练不用在 NaN 的模型上继续调。5.2 距离塑形盖过碰撞惩罚无人机贴着障碍物边缘飞现象训练完成后看轨迹无人机贴着障碍物表面绕行最近距离只有 0.1 米虽然没碰撞但极度危险。原因距离塑形给的是“接近目标”的正奖励障碍物边缘的路径虽然危险但距离收益每步都在累积最终超过了对碰撞风险的惩罚预期。解决给障碍物设置安全缓冲距离碰撞检测半径从 2.0 米扩大到 3.0 米并把“障碍物距离太近”作为每步的持续惩罚。也就是说不是撞上才惩罚而是靠近就扣分。修改后无人机会主动保持安全距离而不是贴边“蹭”过去。5.3 epsilon 衰减过快训练曲线平平的到达率一直是 0现象训练几千 episode总奖励一直在 -200 附近波动没有上升趋势。原因epsilon 从 1.0 到 0.05 衰减的窗口太短前 200 局就把探索几乎关掉了策略还没探索到到达目标的路径就过早进入纯利用阶段陷入“原地转圈”的行为。解决把衰减窗口从 400 局拉长到 800 局同时给每个 episode 的初始状态加入随机扰动确保探索阶段能看到更多地图变化。判断探索是否足够的标准前 20% 的训练里无人机的平均步长应该明显大于随机策略的平均步长如果一开始就收敛到很短的轨迹大概率是探索不足。5.4 共享经验回放导致编队行为失调两架无人机反复互撞现象训练中单机避障表现不错但两架无人机相遇时互不相让反复碰撞到达率一直上不去。原因共享 DQN 的经验回放里每架无人机只用自己的状态做决策看不到其他智能体的动作。两个智能体同时冲向同一个目标时没有一个机制让它们学会“礼让”。解决在奖励函数里加入机间距离惩罚项两机距离小于 3 米双方各扣 2 分同时把交互协商写入状态空间确保一定程度上的协作信号。除了奖励还要考虑把动作“共享给队友”作为状态输入常见做法是把邻居无人机的最近动作向量拼进状态。5.5 模型过拟合到固定地图换障碍物布局后性能暴跌现象训练时固定使用同一张障碍物地图训练结束时到达率 90% 以上把障碍物位置随机改一下到达率掉到 30%。原因网络记住了特定障碍物坐标下的路径模式而不是“避开障碍物并到达目标”的通用策略。解决每个 episode 重置障碍物位置让训练分布覆盖足够多的地图变化。障碍物坐标从训练初始的固定值改为uniform(20, 80)随机采样地图边界保持固定。这个方法会增加训练难度前几百局会明显不稳定但最终策略的泛化能力远比固定地图强。6. 进阶用法从独立 DQN 迁到 MADDPG 参数表以及仿真收敛后的验证套路把基线 DQN 跑通之后你可以沿着两个方向继续深入一是升级算法到 MADDPG二是建立一套严格的验证流程确保策略不是在仿真里“蒙对”的。MADDPG 迁移是我最常用的升级路径。它跟 DQN 的核心差异在训练方式前面已经给了 Actor 和 Critic 的网络结构关键参数可以按下面这张表起步参数DQN 基线值MADDPG 建议值Actor 学习率无共用 1e-31e-4Critic 学习率1e-31e-3探索噪声 σepsilon 线性衰减0.5 起步每 1000 步 ×0.99目标网络软更新 τ硬同步每 200 步0.01折扣因子 γ0.990.95更长时域任务用 0.99这个表格的出发点Critic 负责价值评估学得快一点Actor 要稳学得慢一点。软更新 τ0.01 是标准起步值目标网络平滑跟随不会像硬同步那样出现周期性振荡。探索噪声用 Ornstein-Uhlenbeck 过程也可以但简单的零均值高斯噪声已经够用。训练收敛后最容易被忽略的是验证环节。我习惯的做法是准备一张“从没出现在训练集里”的随机地图跑 20 个测试 episode统计三项指标到达率、碰撞率、平均飞行路径长度。到达率低于 60% 说明策略泛化不足碰撞率高于 5% 就要回去检查机间避碰的奖励权重平均航迹长度如果大幅超过起点到终点的直线距离说明能耗惩罚系数太小无人机在绕路。这三项指标要固定下来每次调参后都跑同一组测试地图否则你没法判断这次改动到底是变好了还是变差了。另一个很好用的进阶技巧是课程学习先在地图小、障碍物少、无人机数量少的环境训练几万步再逐步增加环境复杂度。多智能体强化学习对初始环境的敏感度极高直接上复杂环境经常是训练不收敛的罪魁祸首。我每次做这类项目都会把奖励函数拆成独立日志文件奖励函数的分项贡献单独打印哪一项异常一眼就能看到。养成这个习惯之后很多看起来像“玄学”的不收敛问题最后都能定位到具体某一项奖励的设计失误。希望帮到你。本文还有配套的精品资源点击获取
返回列表