
简介面向具备Python与深度学习基础的研究者、工程师及高校学生这份资源提供了一个基于深度强化学习的无人机三维路径规划项目实例采用PPO算法与近端策略优化覆盖三维环境建模、连续动作控制、障碍检测和混合式路线生成综合解决城市物流、应急救援等场景下的路径规划问题。压缩包内仅一个docx文件大小126KB将完整程序、GUI可视化设计与详细代码说明整合于同一文档中便于阅读和复现。目前已有90人学习/浏览。文档目录按项目背景、模型架构、代码示例、训练流程、路线评价等模块展开重点讲解了状态编码、演员-评论家网络、广义优势估计、碰撞检测和代价最低路线选择读者可据此掌握从仿真训练到部署验证的全流程。同时设计了混合A*搜索与强化学习的路线机制兼顾全局最优性和安全性适合作为科研、教学与工程原型开发的高质量参考。1. 无人机三维路径规划当传统A*遇上密集障碍城市巡检、灾后侦察这类场景里无人机要飞的不是一张平面地图而是一个充满楼体、塔吊、线缆的三维空间。传统A和RRT在低维静态环境中表现不错一旦地图分辨率提高、障碍数量增加A的栅格搜索会指数膨胀RRT则因随机采样在狭窄通道中收敛缓慢。深度强化学习DRL换了一种思路不做显式搜索而是靠反复试错把“状态-动作”映射压进神经网络运行时一次前向推理就能输出下一步飞行指令。这套系统用Python可以完整落地从环境仿真、SAC算法训练到GUI航迹显示在一台普通PC上就能跑通。下面就从MDP建模开始顺一遍完整的实现路径。2. 将三维路径规划建模成马尔可夫决策过程2.1 三维状态空间设计位置、速度与目标偏差DRL训练的第一步是定义状态。对于无人机三维路径规划最基础的状态是当前位置与当前速度。更高信息量的做法是把目标点相对位置也放进状态里这样网络不需要记忆目标坐标只需要学习“往哪个方向飞能缩小差距”这个策略本身。def _build_state(self): 构造9维状态向量并按量纲归一化 rel self.goal - self.pos # 目标相对位移 state np.zeros(9) state[0:3] (self.pos - self.map_center) / self.map_scale # 位置归一化 state[3:6] self.vel / self.max_speed # 速度归一化 state[6:9] rel / np.linalg.norm(self.goal - self.start) # 相对目标比例 return state这段代码里前三个维度是归一化位置中间三个维度是归一化速度最后三个维度是目标相对位移的比例值。当无人机在起点时state[6:9]恰好是单位向量越接近目标这个向量越短网络能明确感知“还差多远”。归一化的意义在于把不同物理量的量纲统一否则位置特征的范围是0~100速度特征的范围只有0~5梯度更新会被大面积的位置分量主导。设计状态空间有一个容易踩的坑不要把目标绝对坐标放进状态。目标绝对坐标随地图变化换一张地图后模型基本失效相对偏差天然具有迁移性换地图后只需重新归一化策略依然可用。因此我一般建议目标信息全部采用相对表示。障碍信息同理如果传感器能提供局部障碍距离也应拼接到状态向量后部作为追加维度。2.2 连续动作空间与无人机运动学约束动作空间的设计取决于飞行器的动力学模型。旋翼无人机常用三维加速度指令作为动作固定翼则要额外加入最小速度约束和俯仰角限制。初学者最容易犯的错是把动作当作位置增量直接加给坐标这样训练出的轨迹像“瞬移”完全不满足飞控的积分逻辑。def step(self, action): # action: [ax, ay, az]由tanh压缩到[-1, 1]后放大 acc action * self.max_accel self.vel np.clip(self.vel acc * self.dt, -self.max_speed, self.max_speed) self.pos self.pos self.vel * self.dt done False if self._check_collision(): done True if np.linalg.norm(self.goal - self.pos) self.goal_threshold: done True return self._build_state(), self._compute_reward(), done加速度指令经tanh压到[-1, 1]后用max_accel放大保证策略网络输出的数值范围可控。dt是固定仿真步长我通常取0.5秒每步先更新速度再更新位置。到达判定阈值goal_threshold设为地图边长的3%~5%比较合理太小会拖慢训练太大会让最终航迹明显偏离目标点。这里还需要限制单步位移不超过栅格尺寸的一半否则会出现“一步跨过障碍物”的穿模问题。2.3 奖励塑形稀疏引导与密集引导的权衡纯稀疏奖励只有到达100、碰撞-100在三维空间中几乎没法训练随机探索轨迹撞上目标的概率太低。工程实现里普遍采用密集奖励每走一步根据当前状态给出反馈告诉智能体刚才是变好了还是变坏了。奖励项计算方式作用建议权重距离变化量d_old - d_new引导接近目标1.0~2.0碰撞惩罚常量学习避开障碍-50~-100平滑性惩罚相邻动作差值抑制抖振-0.1~-0.5到达奖励常量正向激励100距离奖励用“相对变化量”而不是“当前距离取负”原因在于当无人机需要绕行障碍时当前距离一定暂时变大若直接惩罚距离会诱导无人机直线撞向障碍物而惩罚“距离比上一步大”只惩罚后退不会强迫直线逼近。2.3.1 奖励函数原型代码def _compute_reward(self, prev_dist, new_dist, action, prev_action, collide): r (prev_dist - new_dist) * self.w_dist if collide: r self.collision_penalty # 相邻动作变化量过大时扣分避免轨迹抖振 if prev_action is not None: jerk np.linalg.norm(action - prev_action) r - jerk * self.w_smooth if new_dist self.goal_threshold: r self.arrive_bonus return rcollision_penalty如果设到-1000网络会把大片区域视为禁区无人机干脆停在起点不动如果只设-10碰撞就成了轻微扣分路径会贴着障碍物边缘擦过。平滑性惩罚的量级要比距离权重低一个数量级否则网络为了不生硬转向而选择极度保守的动作会绕出很远的弧线。到这里MDP四要素状态、动作、转移、奖励齐了接下来封装成可训练的环境类。3. 用Python实现三维路径规划仿真环境3.1 基于NumPy的三维栅格与碰撞检测实际工程不会直接从真实点云训练DRL仿真到现实的差距会毁掉策略。通用做法是先把环境离散成占据栅格值为1表示障碍物0表示自由空间后续的碰撞检测全部查询栅格。def build_grid_map(map_size_meters, resolution, obstacles): 把连续坐标地图离散成占据栅格 map_size_meters: 地图边长米 resolution: 栅格分辨率如1.0表示每格1米 obstacles: 球形障碍物列表元素为 {center: ndarray, radius: float} dim int(map_size_meters / resolution) grid np.zeros((dim, dim, dim), dtypenp.uint8) for ob in obstacles: c (ob[center] / resolution).astype(int) r max(1, int(ob[radius] / resolution)) # 计算球体包围盒并做边界裁剪 x0, x1 np.clip(c[0] - r, 0, dim - 1), np.clip(c[0] r 1, 0, dim) y0, y1 np.clip(c[1] - r, 0, dim - 1), np.clip(c[1] r 1, 0, dim) z0, z1 np.clip(c[2] - r, 0, dim - 1), np.clip(c[2] r 1, 0, dim) xx, yy, zz np.ogrid[x0:x1, y0:y1, z0:z1] mask ((xx - c[0])**2 (yy - c[1])**2 (zz - c[2])**2) r**2 grid[xx, yy, zz] np.logical_or(grid[xx, yy, zz], mask) return grid用np.ogrid生成三维索引网格再通过广播机制一次性算出球体内部所有格点避免Python层面的三层for循环。x0/x1这类边界裁剪很重要障碍物贴着地图边缘时索引不会越界。分辨率建议设为1到2米太高会显著增加栅格存储量和每次碰撞查询的开销太低则会让小障碍物在栅格中消失。提示如果规划步长大于栅格尺寸无人机一步可能直接穿过障碍物所在格。步长应控制在栅格尺寸的一半以内或者在step中做线性插值的连续碰撞检测。3.2 环境类接口设计完整可运行的UAV3DEnv为了让环境能直接对接Stable-Baselines3这类框架我按Gym风格实现reset和step。下面的类是完整版本包含了前面设计的归一化状态、奖励函数和碰撞检测。class UAV3DEnv: def __init__(self, grid_map, start, goal, resolution1.0, dt0.5, max_speed5.0, max_accel3.0): self.grid grid_map self.resolution resolution self.start np.array(start, dtypefloat) self.goal np.array(goal, dtypefloat) self.dt dt self.max_speed max_speed self.max_accel max_accel self.goal_threshold 3.0 self.pos self.start.copy() self.vel np.zeros(3) self.prev_action None self.map_center np.array(grid_map.shape) * resolution / 2 self.map_scale float(np.max(grid_map.shape)) * resolution def reset(self): self.pos self.start.copy() self.vel np.zeros(3) self.prev_action None return self._build_state() def step(self, action): action np.clip(action, -1.0, 1.0) prev_dist np.linalg.norm(self.goal - self.pos) # 加速度积分到速度速度积分到位置 self.vel np.clip(self.vel action * self.max_accel * self.dt, -self.max_speed, self.max_speed) self.pos self.pos self.vel * self.dt collide self._check_collision() new_dist np.linalg.norm(self.goal - self.pos) done bool(collide or new_dist self.goal_threshold) reward self._compute_reward(prev_dist, new_dist, action, self.prev_action, collide) self.prev_action action.copy() return self._build_state(), reward, done, {} def _build_state(self): rel self.goal - self.pos state np.zeros(9) state[0:3] (self.pos - self.map_center) / self.map_scale state[3:6] self.vel / self.max_speed state[6:9] rel / np.linalg.norm(self.goal - self.start) return state def _check_collision(self): idx np.floor(self.pos / self.resolution).astype(int) if np.any(idx 0) or np.any(idx np.array(self.grid.shape)): return True return bool(self.grid[tuple(idx)] 1) def _compute_reward(self, prev_dist, new_dist, action, prev_action, collide): r (prev_dist - new_dist) * 1.5 if collide: r - 50.0 if prev_action is not None: r - np.linalg.norm(action - prev_action) * 0.3 if new_dist self.goal_threshold: r 100.0 return float(r)step返回的是四元组(state, reward, done, info)info里可以附带当前距离、步数等日志用于训练时的监控。reset时prev_action必须复位为None否则第一个episode的奖励会错误地惩罚一次不存在的“动作变化”。_check_collision把无人机坐标映射到栅格索引查询当前位置是否为障碍同时对越界做了保护。这里的动作直接传给 _compute_reward 计算平滑项所以action传入时需要是原始的[-1,1]值不要提前乘max_accel。3.3 用Matplotlib渲染三维地图与规划结果训练时快速确认无人机是否在绕障碍、是否卡在墙角Matplotlib足够。虽然交互性一般但零依赖、随处可跑适合做顶层调试。import matplotlib.pyplot as plt def render_path(env, path, obstaclesNone): fig plt.figure(figsize(10, 8)) ax fig.add_subplot(111, projection3d) path np.asarray(path) ax.plot(path[:, 0], path[:, 1], path[:, 2], b-, linewidth2, labelPlanned Path) ax.scatter(*env.start, colorr, s80, markero, labelStart) ax.scatter(*env.goal, colorg, s100, marker*, labelGoal) if obstacles: for ob in obstacles: u np.linspace(0, 2 * np.pi, 20) v np.linspace(0, np.pi, 20) x ob[center][0] ob[radius] * np.outer(np.cos(u), np.sin(v)) y ob[center][1] ob[radius] * np.outer(np.sin(u), np.sin(v)) z ob[center][2] ob[radius] * np.outer(np.ones(np.size(u)), np.cos(v)) ax.plot_surface(x, y, z, colorgray, alpha0.4) ax.set_xlabel(X); ax.set_ylabel(Y); ax.set_zlabel(Z) ax.legend() plt.show()障碍物用plot_surface画成半透明球体路径用蓝色实线叠加。显示时注意地图长宽比如果地图是100×100×60这种非立方体坐标轴默认比例会把球形障碍物拉伸成椭球。可以在绘制后调用ax.set_box_aspect((1.0, 1.0, 0.6))校正比例。环境与可视化就绪后下一步是算法选型和训练。4. 深度强化学习算法选型与Python训练实现4.1 各DRL算法对比为什么无人机的三维路径规划更适合SAC不同算法适应不同任务选型直接决定训练效率和最终路径质量。算法动作类型样本效率调参难度连续运动控制适配度DQN离散低中不适合离散动作丢失方向精度DDPG连续中高可用超参数敏感Q值易过估计TD3连续中高中可用比DDPG稳定PPO连续/离散中低可用适合并行采样策略偏保守SAC连续高中很适配自动温度系数减少手动调熵DDPG和TD3都是Actor-Critic结构TD3通过Clipped Double-Q抑制过估计训练更稳定。但它们共同的弱点是探索噪声固定训练初期容易在障碍密集区域陷入局部最优。SAC引入熵正则项让策略在训练过程中动态调节探索强度这在三维空间奖励稀疏的场景下很关键。大量飞行控制与路径规划实践也验证了SAC在连续控制任务上的收敛速度和稳定性优势。4.2 SAC的Actor-Critic网络结构定义SAC由Actor网络、两个Critic网络和温度系数alpha组成。Actor输出动作分布的均值和对数标准差Critic估计状态-动作价值温度系数控制探索程度。import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.fc1 nn.Linear(state_dim, hidden) self.fc2 nn.Linear(hidden, hidden) self.mean nn.Linear(hidden, action_dim) self.log_std nn.Linear(hidden, action_dim) def forward(self, s): x F.relu(self.fc1(s)) x F.relu(self.fc2(x)) mu self.mean(x) log_std torch.clamp(self.log_std(x), -20, 2) # 限制探索方差范围 return mu, log_std def select_action(self, s, deterministicFalse): 返回动作deterministicTrue 时用均值不采样 mu, log_std self.forward(s) if deterministic: return torch.tanh(mu) std log_std.exp() z mu torch.randn_like(mu) * std return torch.tanh(z) def evaluate(self, s): 训练时调用返回动作与对数概率 log_pi用于熵更新 mu, log_std self.forward(s) std log_std.exp() dist torch.distributions.Normal(mu, std) z dist.rsample() # 重参数化采样支持反向传播 action torch.tanh(z) log_pi dist.log_prob(z) - torch.log(1 - action.pow(2) 1e-6) return action, log_pi.sum(dim-1, keepdimTrue)Actor采用双层ReLU网络输出均值和对数标准差。log_std限制在[-20, 2]避免探索噪声过大或过小。tanh输出把动作压到[-1, 1]与环境里的动作限幅完全匹配。select_action的deterministic模式用于训练结束后的航线生成不做随机采样。evaluate方法里用rsample()重参数化让采样过程可以回传梯度这也是SAC能高效训练的关键之一。Critic网络结构更简洁输入是状态与动作拼接后的向量输出一个标量Q值class Critic(nn.Module): def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.net nn.Sequential( nn.Linear(state_dim action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, s, a): return self.net(torch.cat([s, a], dim-1))两个Critic结构完全一致但参数独立训练时取两者较小的Q值作为目标这一设计有效抑制了Q函数过估计导致的策略退化。4.3 训练循环与重要超参数SAC的训练循环包含四步环境交互、经验存储、随机采样、梯度更新。温度系数alpha通常采用自动调整让策略期望熵维持在目标熵附近。def train_one_step(batch, actor, critic_1, critic_2, target_c1, target_c2, optimizer_a, optimizer_c, alpha, gamma0.99): s, a, r, s_, done batch with torch.no_grad(): a_, log_pi_ actor.evaluate(s_) target_q1 target_c1(s_, a_) target_q2 target_c2(s_, a_) target_q torch.min(target_q1, target_q2) - alpha * log_pi_ y r gamma * (1 - done) * target_q # done 标记不引导下一步 q1 critic_1(s, a) q2 critic_2(s, a) critic_loss F.mse_loss(q1, y) F.mse_loss(q2, y) optimizer_c.zero_grad() critic_loss.backward() optimizer_c.step() a_new, log_pi_new actor.evaluate(s) q_new torch.min(critic_1(s, a_new), critic_2(s, a_new)) actor_loss (alpha * log_pi_new - q_new).mean() optimizer_a.zero_grad() actor_loss.backward() optimizer_a.step() return critic_loss.item(), actor_loss.item()目标网络target_c1和target_c2是Critic的深拷贝每步通过软更新向在线网络靠近软更新系数tau取0.005。done标记参与TD目标计算终止状态不使用next state否则会把终点的Q值错误传播到前一状态。Actor的损失由两项组成alpha乘log_pi_new代表熵减去q_new代表价值二者平衡了探索与利用。超参数推荐取值影响学习率3e-4过高导致训练震荡过低收敛慢replay buffer200000~1000000太小容易遗忘旧经验batch size256影响梯度稳定性tau软更新0.005目标网络平滑程度gamma折扣因子0.99长期路径收益占比每episode最大步数200防止无碰撞死循环训练完成后需要离线评估。把Actor切成deterministic模式从起点跑一个episode记录每一步的位置得到一条完整路径曲线。这条轨迹就是GUI展示和后续飞控执行的最终航迹。5. 三维路径规划GUI设计与可视化技巧5.1 GUI的功能拆分与界面结构做GUI前先想清楚使用者是谁。调试训练过程的工程师需要损失曲线、当前episode路径回放对外演示则需要把障碍物、起点终点、飞行轨迹放在同一三维场景里。我习惯拆成两个面板左侧是训练监控区右侧是三维路径展示区。运行后同时看到“学得怎么样”和“飞得怎么样”。PyQt5在Windows和Linux下分发相对省心嵌入Matplotlib三轮可视化也比较成熟。5.2 用PyQt5内嵌Matplotlib实现三维路径展示import numpy as np import sys from PyQt5 import QtWidgets from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class PathViewer(QtWidgets.QWidget): def __init__(self, parentNone): super().__init__(parent) self.fig Figure(figsize(6, 5)) self.ax self.fig.add_subplot(111, projection3d) self.canvas FigureCanvas(self.fig) layout QtWidgets.QVBoxLayout(self) layout.addWidget(self.canvas) def draw_path(self, path, start, goal, obstacles): self.ax.clear() path np.asarray(path) self.ax.plot(path[:, 0], path[:, 1], path[:, 2], b-, linewidth1.8) self.ax.scatter(*start, cr, s60, labelstart) self.ax.scatter(*goal, cg, s80, marker*, labelgoal) # 绘制半透明球形障碍物 for ob in obstacles: u np.linspace(0, 2 * np.pi, 20) v np.linspace(0, np.pi, 20) x ob[center][0] ob[radius] * np.outer(np.cos(u), np.sin(v)) y ob[center][1] ob[radius] * np.outer(np.sin(u), np.sin(v)) z ob[center][2] ob[radius] * np.outer(np.ones(len(u)), np.cos(v)) self.ax.plot_surface(x, y, z, colorgray, alpha0.3) self.ax.set_xlabel(x); self.ax.set_ylabel(y); self.ax.set_zlabel(z) self.ax.legend() self.canvas.draw()FigureCanvas把Matplotlib的figure对象挂在Qt窗体上draw_path接收已完成规划的路径点序列一次性绘制。地图非立方体时要调用self.ax.set_box_aspect()校准比例否则球形障碍物会被拉伸。另一个细节是让Actor在deterministic模式下先把整条episode跑完路径坐标存为数组后再送入draw_path而不是在每个GUI事件里做网络前向这样界面刷新速度与推理耗时解耦操作更流畅。5.3 路径导出与几何质量检查GUI里点击“导出航线”后把路径写成CSV。列名统一为x,y,z文件头记录地图尺寸与栅格分辨率方便飞控侧解析。导出前先做一次几何质量检查逐点计算相邻三点夹角的余弦值余弦值突然变负说明转弯过急真实飞行中会掉速甚至触发飞控保护。def check_turn_rate(path, min_cos0.2): 检查路径中转弯过急的航点min_cos越小允许越大转角 bad_points [] for i in range(1, len(path) - 1): v1 path[i] - path[i - 1] v2 path[i 1] - path[i] norm np.linalg.norm(v1) * np.linalg.norm(v2) if norm 1e-9: continue cos_val np.dot(v1, v2) / norm if cos_val min_cos: bad_points.append(i) return bad_pointsbad_points返回急转点索引。在GUI里把这些问题航点用红色散点直接标到三维图上比看数据表直观得多只需加一行self.ax.scatter(path[bad_points, 0], path[bad_points, 1], path[bad_points, 2], cred, s30)。若急转点过多优先提高奖励函数中平滑项权重重新训练只有个别急转点时对该局部区段做B样条平滑即可。这套流程跑通后从地图栅格化、SAC训练到GUI可视化与轨迹质量校验整条链路就完整闭环了。本文还有配套的精品资源点击获取