☰
多智能体强化学习做无人机三维路径规划:MADDPG环境建模、训练与GUI实战
2026/10/12 1:15:56 网站建设 项目流程

简介:本资源面向具备Python与强化学习基础的研究人员、工程师及高年级本科生、研究生,提供一套基于多智能体强化学习(MARL)的无人机三维路径规划完整项目实例。内容围绕三维连续空间环境建模、MAPPO算法协同策略训练、局部观测与集中式价值网络设计、安全约束融入、候选路线评估与自动选择机制展开,可实现多无人机在复杂障碍环境中的自主避障与高效路径规划,适用于城市低空物流、森林巡检、应急救援等协同任务场景。资源包共1个docx文件,约141KB,以文档形式系统呈现项目背景、模型架构、代码示例与应用领域,便于按目录检索学习。已有98人学习下载。读者可借此掌握奖励函数设计、信用分配、集中训练分散执行等关键技术,并借助完整程序、GUI设计与代码详解,完成从算法研发到工程部署的全流程验证,为科研原型或工程预研提供可复现的实验基础。

1. 多智能体强化学习做无人机三维路径规划:为什么单机 DRL 在编队场景里会翻车

三架无人机从不同起点出发,要在同一空域里穿过三栋楼之间的缝隙,同时避开彼此的气流扰动,最后在 40 秒内同时抵达三个目标点。这个场景用单机深度强化学习(DRL)跑,十有八九会翻车:每架无人机各自为政,A 机为了抄近路切进 B 机的航线,B 机紧急避让又撞上 C 机的尾流,最后三架全在楼缝里打转。问题不在算法不够深,而在「多智能体强化学习(MARL)」这个框架本身——它要解决的是多个决策主体在共享环境里如何协调策略,而不是把单机算法复制三份。这篇笔记就围绕 MARL 做无人机三维路径规划,把状态空间怎么设计、奖励怎么分配、训练怎么收敛、GUI 怎么搭这条链路讲透,适合已经跑过单机 DRL、想往编队协同方向推进的 Python 工程师。

2. MARL 路径规划的环境建模:状态、动作、奖励三件套怎么定

2.1 三维栅格与连续动作空间的取舍

无人机三维路径规划的第一步不是写网络,而是决定环境怎么表示。常见做法有两类:三维栅格离散化和连续坐标空间。栅格法把空域切成 N×N×N 的立方体,每个格子标记为可通行或障碍,动作空间是 6 个方向(上下左右前后)或 26 个邻域方向。优点是实现简单、碰撞检测直接查表;缺点是路径呈锯齿状,无人机实际飞起来要频繁急转,能耗高。

连续空间用 (x, y, z) 坐标加速度向量表示状态,动作是三维加速度或速度增量。路径平滑、更贴近真实飞控,但碰撞检测要做几何求交,训练时样本效率低。我一般会选连续空间,因为三维路径规划的实际需求是「飞得稳」而不是「走得通」。如果只是做算法验证,栅格法起步更快。

状态向量建议包含四类信息:自身位置与速度(6 维)、最近 K 个障碍物的相对位置与距离(3K 维)、其他智能体的相对位置与速度(6(N-1) 维)、目标点相对位置(3 维)。K 取 5 到 8 够用,太多会让网络输入维度爆炸,太少则避障信息不足。

import numpy as np def build_state(drone, obstacles, teammates, target, K=6): """ 构造单个无人机的状态向量 drone: [x, y, z, vx, vy, vz] obstacles: [[x, y, z], ...] 所有障碍物中心 teammates: [[x, y, z, vx, vy, vz], ...] 其他无人机 target: [x, y, z] """ state = list(drone) # 自身 6 维 # 最近 K 个障碍物的相对位置 obs_arr = np.array(obstacles) rel = obs_arr - np.array(drone[:3]) dist = np.linalg.norm(rel, axis=1) idx = np.argsort(dist)[:K] for i in idx: state.extend(rel[i].tolist()) # 不足 K 个用 0 补齐,保证维度固定 for _ in range(K - len(idx)): state.extend([0.0, 0.0, 0.0]) # 其他智能体相对状态 for tm in teammates: state.extend((np.array(tm[:3]) - np.array(drone[:3])).tolist()) state.extend(tm[3:].tolist()) # 目标点相对位置 state.extend((np.array(target) - np.array(drone[:3])).tolist()) return np.array(state, dtype=np.float32)

这段代码的关键在于维度固定:不管空域里有多少障碍物,状态向量长度始终一致,网络才能稳定训练。K=6是经验值,空域障碍密集时可以加到 8,但要注意输入维度增加会拖慢收敛。np.argsort按距离排序取最近的,比随机采样靠谱得多。

2.2 奖励函数:稀疏奖励是 MARL 训练不收敛的头号原因

奖励设计直接决定策略能不能学出来。最朴素的写法是「到目标给 +100,撞障碍给 -100,其他时候 0」。这种稀疏奖励在单机场景都要跑几十万步,多机场景下更糟——每架无人机都在等别人先动,探索效率极低。

我一般用分层奖励:距离引导 + 避障惩罚 + 协同奖励 + 终端奖励。距离引导用势能函数,每步给-0.01 * 到目标距离,让无人机有持续向目标靠近的动力。避障惩罚在距离障碍物小于安全半径时给负值,且随距离减小而增大。协同奖励是 MARL 特有的,用来鼓励编队保持合理间距——太近给惩罚,太远也给惩罚,中间区间给正奖励。

def compute_reward(drone, prev_dist, curr_dist, min_obs_dist, teammate_dists, reached, collided, safe_r=2.0, ideal_sep=5.0): reward = 0.0 # 距离引导:靠近目标给正奖励 reward += (prev_dist - curr_dist) * 1.0 # 避障惩罚 if min_obs_dist < safe_r: reward -= (safe_r - min_obs_dist) * 10.0 # 协同奖励:保持理想间距 for d in teammate_dists: if d < ideal_sep * 0.5: reward -= 2.0 # 太近,有碰撞风险 elif d > ideal_sep * 2.0: reward -= 0.5 # 太远,失去协同意义 else: reward += 0.2 # 终端奖励 if reached: reward += 100.0 if collided: reward -= 100.0 return reward

参数说明:safe_r=2.0是安全半径,按无人机尺寸和空域密度调;ideal_sep=5.0是理想间距,编队密集时调到 3.0,松散编队调到 8.0。距离引导系数 1.0 和避障惩罚系数 10.0 的比例关系很重要——避障权重太低会撞,太高则无人机不敢靠近障碍物,绕远路。

2.3 用 Gym 风格封装多机环境

把环境封装成标准接口,后面接任何 MARL 算法都方便。核心是reset()返回所有智能体的初始状态,step(actions)接收所有智能体的联合动作,返回下一状态、奖励、done 标志。

class DroneSwarmEnv: def __init__(self, n_drones=3, n_obstacles=8, bounds=(0, 50)): self.n = n_drones self.bounds = bounds self.obstacles = self._random_obstacles(n_obstacles) self.reset() def reset(self): self.positions = [np.random.uniform(0, 10, 3) for _ in range(self.n)] self.velocities = [np.zeros(3) for _ in range(self.n)] self.targets = [np.array([45, 45, 30]) + i * 3 for i in range(self.n)] self.steps = 0 return [self._get_state(i) for i in range(self.n)] def step(self, actions): # actions: list of 3D acceleration for i in range(self.n): self.velocities[i] = np.clip( self.velocities[i] + actions[i] * 0.1, -3, 3) self.positions[i] = np.clip( self.positions[i] + self.velocities[i] * 0.1, self.bounds[0], self.bounds[1]) self.steps += 1 rewards, dones = [], [] for i in range(self.n): d = np.linalg.norm(self.positions[i] - self.targets[i]) reached = d < 1.5 collided = self._check_collision(i) rewards.append(compute_reward( self.positions[i], d, d, self._min_obs_dist(i), self._teammate_dists(i), reached, collided)) dones.append(reached or collided or self.steps > 500) return [self._get_state(i) for i in range(self.n)], rewards, dones, {}

step里速度裁剪到 ±3 m/s、位置裁剪到空域边界,是防止无人机飞出仿真范围。self.steps > 500是超时终止,避免无效回合拖长训练。这些边界条件看着琐碎,但少了任何一个,训练曲线都会出现莫名其妙的震荡。

3. 选 MADDPG 还是 QMIX:算法选型与训练循环落地

3.1 为什么我优先选 MADDPG 而不是 QMIX

MARL 算法分两大类:基于值分解的(QMIX、VDN)和基于策略梯度的(MADDPG、MAPPO)。值分解方法要求联合 Q 值能分解成个体 Q 值的单调组合,适合离散动作、同质智能体;无人机路径规划是连续动作、每个智能体观测不同,MADDPG 更合适。

MADDPG 的核心是「集中训练、分散执行」:每个智能体有一个 Actor 网络只用自己的局部观测做决策,还有一个 Critic 网络在训练时能看到所有智能体的观测和动作。这样既保证了执行时不需要全局信息(实际飞控拿不到),又让 Critic 能评估联合动作的好坏,缓解了非平稳性问题。

网络结构上,Actor 用 3 层全连接(256-256-3),输出用 tanh 限制在 [-1, 1] 再乘以最大加速度。Critic 输入是「所有智能体观测拼接 + 所有智能体动作拼接」,输出一个标量 Q 值。经验回放池存的是联合转移(obs_all, actions_all, rewards_all, next_obs_all, dones)。

3.2 训练循环:从随机探索到策略收敛

训练循环的骨架是:每个 episode 重置环境,每步每个智能体根据 Actor 加噪声选动作,环境返回奖励和下一状态,存入回放池,从池里采样一批更新网络。

import torch import torch.nn as nn import torch.optim as optim from collections import deque import random class Actor(nn.Module): def __init__(self, obs_dim, act_dim, hidden=256): 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()) def forward(self, x): return self.net(x) class Critic(nn.Module): def __init__(self, total_obs, total_act, hidden=256): super().__init__() self.net = nn.Sequential( nn.Linear(total_obs + total_act, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1)) def forward(self, obs, act): return self.net(torch.cat([obs, act], dim=-1)) class MADDPG: def __init__(self, n_agents, obs_dim, act_dim, lr=1e-3, gamma=0.95, tau=0.01, buffer_size=100000): self.n = n_agents self.gamma = gamma self.tau = tau self.actors = [Actor(obs_dim, act_dim) for _ in range(n_agents)] self.critics = [Critic(obs_dim * n_agents, act_dim * n_agents) for _ in range(n_agents)] self.target_actors = [Actor(obs_dim, act_dim) for _ in range(n_agents)] self.target_critics = [Critic(obs_dim * n_agents, act_dim * n_agents) for _ in range(n_agents)] # 同步初始参数 for i in range(n_agents): self.target_actors[i].load_state_dict(self.actors[i].state_dict()) self.target_critics[i].load_state_dict(self.critics[i].state_dict()) self.actor_opts = [optim.Adam(a.parameters(), lr=lr) for a in self.actors] self.critic_opts = [optim.Adam(c.parameters(), lr=lr) for c in self.critics] self.buffer = deque(maxlen=buffer_size) def select_actions(self, obs_list, noise=0.2): actions = [] for i, obs in enumerate(obs_list): with torch.no_grad(): a = self.actors[i](torch.FloatTensor(obs)).numpy() a = np.clip(a + np.random.normal(0, noise, size=a.shape), -1, 1) actions.append(a) return actions def update(self, batch_size=256): if len(self.buffer) < batch_size: return batch = random.sample(self.buffer, batch_size) obs, acts, rews, next_obs, dones = zip(*batch) obs = torch.FloatTensor(np.array(obs)) # (B, n, obs_dim) acts = torch.FloatTensor(np.array(acts)) # (B, n, act_dim) rews = torch.FloatTensor(np.array(rews)) # (B, n) next_obs = torch.FloatTensor(np.array(next_obs)) dones = torch.FloatTensor(np.array(dones)) obs_flat = obs.view(batch_size, -1) acts_flat = acts.view(batch_size, -1) next_obs_flat = next_obs.view(batch_size, -1) for i in range(self.n): # 目标动作 with torch.no_grad(): next_acts = torch.stack( [self.target_actors[j](next_obs[:, j]) for j in range(self.n)], dim=1).view(batch_size, -1) target_q = self.target_critics[i](next_obs_flat, next_acts) y = rews[:, i:i+1] + self.gamma * target_q * (1 - dones[:, i:i+1]) q = self.critics[i](obs_flat, acts_flat) critic_loss = nn.MSELoss()(q, y) self.critic_opts[i].zero_grad() critic_loss.backward() self.critic_opts[i].step() # Actor 更新:最大化 Critic 对当前动作的评分 curr_acts = torch.stack( [self.actors[j](obs[:, j]) for j in range(self.n)], dim=1 ).view(batch_size, -1) actor_loss = -self.critics[i](obs_flat, curr_acts).mean() self.actor_opts[i].zero_grad() actor_loss.backward() self.actor_opts[i].step() # 软更新目标网络 for tp, p in zip(self.target_actors[i].parameters(), self.actors[i].parameters()): tp.data.copy_(self.tau * p.data + (1 - self.tau) * tp.data) for tp, p in zip(self.target_critics[i].parameters(), self.critics[i].parameters()): tp.data.copy_(self.tau * p.data + (1 - self.tau) * tp.data)

几个参数要重点说:gamma=0.95比单机常用的 0.99 小,因为多机场景下远期回报不确定性大,折扣太慢会让 Critic 估计方差过高。tau=0.01是目标网络软更新系数,太大训练不稳,太小收敛慢。noise=0.2是探索噪声,训练前期可以设 0.3 加速探索,后期降到 0.05 让策略稳定。

回放池存的是所有智能体的联合数据,采样时也是一整条联合转移一起取,不能拆开——这是 MADDPG 和独立 DDPG 的本质区别。如果每个智能体各存各的池子,Critic 就看不到其他智能体的动作,退化成 Independent DDPG,协同效果直接消失。

3.3 训练收敛的判断与调参顺序

训练时盯三个指标:每回合总奖励、到达目标的比例、平均碰撞次数。健康曲线是总奖励在前 500 回合快速上升,之后缓慢爬升并趋于平稳;到达率从 0 涨到 80% 以上;碰撞次数从每回合十几次降到个位数。

如果奖励不涨,按这个顺序排查:先看奖励尺度是否合理(终端奖励 100 对比每步引导 0.01,比例差太多会让引导信号被淹没);再看 Critic loss 是否爆炸(爆炸就降学习率或加梯度裁剪);最后看探索噪声是否过大(噪声太大策略永远在抖)。我踩过的坑是 Critic 输入维度算错——obs_dim * n_agents里如果 obs_dim 本身已经包含了队友信息,就会重复计算,网络学出来的东西全是噪声。

4. GUI 设计与可视化:让训练过程不再是黑匣子

4.1 用 PyQt5 搭实时监控界面

训练跑起来看不见过程,调参全靠猜。我一般用 PyQt5 搭一个简单界面:左边是三维空域的俯视图,用 matplotlib 嵌入,实时画出无人机位置、障碍物、目标点;右边是训练曲线,显示奖励和到达率随回合的变化。

import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLabel) from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure class TrainMonitor(QMainWindow): def __init__(self, env, agent): super().__init__() self.env = env self.agent = agent self.setWindowTitle("MARL 无人机路径规划监控") central = QWidget() self.setCentralWidget(central) layout = QHBoxLayout(central) # 左侧:三维轨迹俯视图 self.fig_scene = Figure(figsize=(5, 5)) self.canvas_scene = FigureCanvasQTAgg(self.fig_scene) layout.addWidget(self.canvas_scene) # 右侧:训练曲线 + 控制按钮 right = QVBoxLayout() self.fig_curve = Figure(figsize=(5, 3)) self.canvas_curve = FigureCanvasQTAgg(self.fig_curve) right.addWidget(self.canvas_curve) self.btn = QPushButton("开始训练") self.btn.clicked.connect(self.start_training) right.addWidget(self.btn) self.status = QLabel("等待开始") right.addWidget(self.status) layout.addLayout(right) self.rewards_history = [] def start_training(self): obs = self.env.reset() episode_reward = 0 for step in range(500): actions = self.agent.select_actions(obs, noise=0.1) obs, rewards, dones, _ = self.env.step(actions) episode_reward += sum(rewards) if step % 5 == 0: self.draw_scene() QApplication.processEvents() if any(dones): break self.rewards_history.append(episode_reward) self.draw_curve() self.status.setText(f"回合奖励: {episode_reward:.1f}") def draw_scene(self): self.fig_scene.clear() ax = self.fig_scene.add_subplot(111) for obs in self.env.obstacles: ax.scatter(obs[0], obs[1], c='gray', marker='s', s=80) for i, pos in enumerate(self.env.positions): ax.scatter(pos[0], pos[1], c=f'C{i}', s=60, label=f'UAV{i}') for i, tgt in enumerate(self.env.targets): ax.scatter(tgt[0], tgt[1], c=f'C{i}', marker='*', s=150) ax.set_xlim(0, 50); ax.set_ylim(0, 50) ax.set_xlabel("X (m)"); ax.set_ylabel("Y (m)") ax.legend(loc='upper right', fontsize=8) self.canvas_scene.draw() def draw_curve(self): self.fig_curve.clear() ax = self.fig_curve.add_subplot(111) ax.plot(self.rewards_history, 'b-', linewidth=1) ax.set_xlabel("Episode"); ax.set_ylabel("Total Reward") ax.grid(True, alpha=0.3) self.canvas_curve.draw()

QApplication.processEvents()是关键,没有它界面会在训练循环里卡死,画布不刷新。step % 5 == 0控制刷新频率,每步都画会拖慢训练十倍以上。三维场景这里用俯视图代替,因为 matplotlib 的 3D 投影在实时刷新时性能很差;要看高度变化,可以单独加一个高度-时间曲线。

4.2 训练日志与回放:出问题时能倒查

GUI 是给人看的,日志是给排查用的。每回合记录:回合编号、总奖励、每架无人机的到达情况、最小障碍物距离、平均队友间距。存成 CSV,训练完用 pandas 一拉就能看出哪架无人机拖后腿。

import csv def log_episode(path, episode, rewards, dones, min_obs_dists, sep_dists): with open(path, 'a', newline='') as f: writer = csv.writer(f) writer.writerow([episode, sum(rewards), int(dones[0]), int(dones[1]), int(dones[2]), min(min_obs_dists), np.mean(sep_dists)])

回放功能更直接:把训练好的模型加载进来,跑一个 episode,把每步的位置存下来,用 matplotlib 的FuncAnimation逐帧播放。调策略时看回放比看曲线直观得多——你能看到无人机是在哪个楼角开始犹豫、在哪次避让后偏离了航线。

5. 避坑与排查:MARL 路径规划里最容易翻车的五件事

5.1 奖励不收敛,曲线像心电图

现象:训练几千回合,总奖励在正负之间来回跳,到达率始终上不去。

原因:多智能体环境下奖励信号被多个来源污染,距离引导、避障惩罚、协同奖励的尺度没对齐。最常见的是避障惩罚系数设得太大,无人机学到「原地不动最安全」的退化解。

解决:先把协同奖励关掉,只留距离引导和终端奖励,确认单机能到目标;再逐步加回避障惩罚,每次加完观察 200 回合;最后加协同奖励。每一步都确认奖励曲线是上升的,再进下一步。

5.2 智能体学会「摆烂」,全部挤在起点不动

现象:训练一段时间后,所有无人机都停在起点附近小幅抖动,谁也不往目标飞。

原因:这是 MARL 里的懒惰智能体问题。如果 Critic 对联合动作的评估被某个智能体的噪声主导,其他智能体的梯度信号就被淹没,Actor 学不到有效策略。

解决:给每个智能体单独算优势函数,而不是共用全局奖励;或者在奖励里加一个「个人进度」项,每架无人机靠近自己目标就给正奖励,不依赖队友。另外检查 Critic 输入是否真的包含了所有智能体的动作——漏掉任何一个,联合评估就失效。

5.3 训练后期突然崩溃,奖励断崖下跌

现象:前 2000 回合训练正常,奖励稳步上升,然后突然掉到谷底再也起不来。

原因:通常是目标网络软更新系数 tau 设得太大,或者学习率在后期没有衰减,Critic 过拟合了早期经验。另一个常见原因是回放池太小,后期采样到的全是近期高奖励样本,分布偏移。

解决:tau 从 0.01 降到 0.005;学习率加余弦退火,从 1e-3 降到 1e-4;回放池至少存 10 万条联合转移。如果还崩,检查是不是有智能体飞出了边界导致状态向量出现 NaN。

5.4 仿真里飞得好,换一组障碍物就撞

现象:训练空域里到达率 90%,把障碍物位置随机换一批,到达率掉到 30%。

原因:过拟合到特定障碍物布局。状态向量里只放了最近 K 个障碍物的相对位置,如果训练时空域障碍物分布单一,网络学到的避障策略没有泛化性。

解决:训练时每 50 回合重新随机障碍物位置和数量,范围在 5 到 12 个之间;状态向量里加入障碍物的尺寸信息,而不只是中心点;如果条件允许,用域随机化把障碍物形状也随机化。

5.5 GUI 卡死或画布不刷新

现象:点开始训练后界面无响应,或者画布一直显示初始状态。

原因:训练循环跑在主线程里,阻塞了 Qt 的事件循环;或者draw()调用后没有触发重绘。

解决:把训练放到QThread里跑,通过信号槽把位置数据传给主线程画图;如果不想用多线程,至少在循环里定期调QApplication.processEvents()。画布刷新用canvas.draw_idle()比canvas.draw()更流畅,后者每次都全量重绘。

6. 进阶技巧:用课程学习把 3 机编队扩到 8 机

3 架无人机训练收敛后,直接加到 8 架,大概率训不动——状态维度从 6+18+12+3 涨到 6+18+42+3,联合动作空间指数级膨胀,Critic 根本估不准。我一般用课程学习分阶段扩:第一阶段 2 架、无障碍、目标点固定;第二阶段 3 架、加 3 个障碍;第三阶段 5 架、障碍随机;第四阶段 8 架、完整场景。每个阶段训练到到达率 80% 再进下一阶段,前一阶段的 Actor 参数直接迁移过去做初始化。

def curriculum_train(agent, env_class, stages): """ stages: [(n_drones, n_obstacles, target_episodes), ...] """ for n_d, n_o, episodes in stages: env = env_class(n_drones=n_d, n_obstacles=n_o) for ep in range(episodes): obs = env.reset() for step in range(500): actions = agent.select_actions(obs, noise=max(0.05, 0.3 - ep*0.001)) next_obs, rewards, dones, _ = env.step(actions) agent.buffer.append((obs, actions, rewards, next_obs, dones)) agent.update() obs = next_obs if any(dones): break print(f"阶段完成: {n_d}机 {n_o}障碍")

噪声衰减用max(0.05, 0.3 - ep*0.001),保证每个阶段前期充分探索、后期稳定。迁移时注意:Actor 的输入维度如果因为队友数量变化而改变,需要重新初始化输入层,但隐藏层和输出层可以复用——这能省掉大量训练时间。

验证泛化性我有个习惯:训练完固定三组随机种子,各跑 100 个 episode,看到达率的均值和方差。均值高但方差大,说明策略不稳定,得回去查奖励函数;均值低,说明训练不充分或状态设计有缺陷。这个习惯帮我省了很多次「以为训好了结果一换场景就崩」的后悔药。

最后说个血泪经验:MARL 调参不要一次改多个变量。我早期同时改学习率、噪声、奖励系数,结果训练崩了根本不知道是哪个引起的。后来改成每次只动一个参数,跑 500 回合看曲线,虽然慢,但每一步都可追溯。希望帮到你。

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

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

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

立即咨询