简介针对无人机在复杂三维环境中的自主路径规划需求这份资源提供一套基于深度强化学习的完整系统实例采用近端策略优化算法与三维环境建模覆盖连续动作输出、局部障碍感知、安全距离检测及A星混合路线生成机制适合具备Python及PyTorch基础、从事智能机器人或自动驾驶领域的研究人员、工程师与高校学生参考实践。包体为1个docx文档共126KB体量精简但信息密度较高完整呈现项目背景、挑战分析、模型架构、核心代码示例、训练流程、碰撞检测与路线评价等内容。文档从三维环境状态编码出发依次讲解演员评论家网络设计、广义优势估计、近端策略优化训练过程并给出自动生成候选路线并选择代价最低路径的混合机制可结合GUI进行可视化调试与仿真验证。目前已有91人学习下载适用于城市物流、应急救援或电力巡线等场景可作为无人机路径规划的项目原型与教学案例帮助系统掌握状态特征提取、奖励函数设计及强化学习连续控制关键技术。1. 为什么无人机三维路径规划最终会走到深度强化学习这条路无人机三维路径规划在工程上一直是个“看起来简单、做起来头疼”的问题二维里好用的 A*、RRT 在地图变成 2.5 维或真三维后搜索空间爆炸而且解出来的路径往往像锯齿飞控根本没法直接吃。传统的做法要么先做平滑后处理要么用人工势场撞运气。直到我把深度强化学习DRL搬进来才发现它是目前把“环境感知—决策—连续控制”揉在一起最自然的一套框架不需要预先建精细栅格地图也不需要手工设计一堆转弯半径约束只要奖励函数写得对网络自己能把路径“飞”出来。这个标题里最值钱的一句话其实是“三维路径规划”。二维规划里的强化学习 demo 满天飞但一上三维状态空间、动作空间、奖励稀疏性全变了很多在二维能收敛的方案直接失效。本文从建模、算法选型、代码骨架到 GUI 集成按一套能跑通的最小实例往下拆适合正在做毕设、比赛或者想往无人机自主决策方向转的开发者。你会看到的不只是“怎么调一个 DDPG”还有一块雷达高度图怎么当状态、奖励函数怎么不把网络带沟里、以及 GUI 到底该画什么才有用。这套东西能帮你把“DRL 能做路径规划”这句话变成一台真能跑的仿真器。2. 三维路径规划的 DRL 建模状态空间、动作空间与奖励函数设计2.1 为什么 A* 和 RRT 在三维里显得笨重DRL 的优势到底在哪传统路径规划算法不是不能用而是它们的设计哲学和 DRL 完全不一样。A* 需要你先建一个离散的栅格地图三维栅格一多内存和时间开销是指数级上涨的RRT 虽然不需要显式建图但它只解决“找得到路”的问题不解决“找好路”的问题生成的路径抖动很大而且每次规划结果都不一样没法在一个连续地形里复用经验。DRL 的思路是让智能体自己跟环境交互用试错来学一个从“感知”到“动作”的映射。这意味着同一套网络在换了一个起终点、换了一片地形之后还能基于已有经验快速适应而不是重新搜索一遍。在实际工程里我最看重的一点是 DRL 能天然输出连续动作比如期望航向角变化量和期望俯仰角变化量直接喂给底层飞控省掉了路径平滑这一整层。当然 DRL 也有它的代价训练不稳定、奖励函数难调、收敛慢。这些坑后面专门开一章讲这里先把建模做对否则后面全是白调。2.2 状态空间雷达高度图 自身姿态而不是“整张地图”很多初学者上来就把整个三维地图的体素网格塞进状态空间这是最容易翻车的设计。三维体素地图动辄几万个格子你就算用卷积网络也未必吃得消而且大部分格子是空的对当前决策毫无信息量。我常用的做法是给无人机装一个“虚拟雷达”以当前位置为中心在水平方向按角度采样一圈射线每条射线测出它打到地形或者障碍物之前的距离。再加上当前无人机的偏航角、俯仰角、剩余电量组成了一个低维但信息充分的状态向量。这个设计模仿了真实无人机上激光测距或者视觉深度估计的感知方式迁移到真机上时传感器接口也好对接。状态向量具体定义如下import numpy as np # 雷达参数 num_rays 24 # 水平采样射线数 max_range 80.0 # 最大探测距离米 def generate_state(env, drone_pose): env: 仿真环境提供 raycast 方法 drone_pose: 无人机位置与姿态 [x, y, z, yaw, pitch] distances [] angles np.linspace(0, 2 * np.pi, num_rays, endpointFalse) for angle in angles: # 在水平方向发射一条射线得到撞击距离 hit_dist env.raycast(drone_pose[:3], drone_pose[3] angle) distances.append(min(hit_dist, max_range) / max_range) # 归一化到 [0,1] # 拼接自身状态归一化高度、偏航角正弦/余弦、俯仰角 height_norm drone_pose[2] / env.map_z yaw_sin np.sin(drone_pose[3]) yaw_cos np.cos(drone_pose[3]) pitch_norm drone_pose[4] / (np.pi / 2) state np.concatenate([np.array(distances), [height_norm, yaw_sin, yaw_cos, pitch_norm]]) return state.astype(np.float32)这里之所以把偏航角拆成正弦和余弦是因为角度是循环量直接喂原始角度值会让网络产生“359 度和 0 度差距很大”的错误认知。归一化则能避免雷达距离和高度不在一个量级时梯度被大数值量纲带偏。2.3 动作空间连续增量控制比直接给坐标更安全动作空间有两个主流方案一个是输出下一时刻的绝对坐标另一个是输出相对当前姿态的增量控制。实测下来绝对坐标会让网络学得很费劲因为目标位置变化范围大输出层很难覆盖而且绝对坐标一旦给得离谱仿真里直接穿墙训练直接崩。我采用的方案是输出两个连续量偏航角变化量 Δψ 和俯仰角变化量 Δθ。水平方向用偏航角控制垂直方向用俯仰角控制再加上一个固定巡航速度。这样动作维度只有 2网络容易收敛而且物理意义清晰——每一帧都是在说“我现在要往哪个方向偏、抬头还是低头”。# 动作空间定义在 [-1, 1]再映射到实际变化量 action agent.act(state) # shape(2,) d_yaw action[0] * 0.15 # 最大偏航角变化 0.15 弧度约 8.6° d_pitch action[1] * 0.1 # 最大俯仰角变化 0.1 弧度约 5.7° # 更新无人机姿态并前进 drone_pose[3] d_yaw drone_pose[4] np.clip(drone_pose[4] d_pitch, -0.6, 0.6) drone_pose[0] speed * np.cos(drone_pose[4]) * np.cos(drone_pose[3]) * dt drone_pose[1] speed * np.cos(drone_pose[4]) * np.sin(drone_pose[3]) * dt drone_pose[2] speed * np.sin(drone_pose[4]) * dt注意俯仰角的裁剪非常关键不裁剪的话网络会学出垂直俯冲这种激进动作路径在高度维度上抖动到飞控根本没法跟踪。速度我习惯不放动作空间里固定巡航速度能大幅降低训练难度真机上有专门的调速模块去处理。2.4 奖励函数稀疏奖励为主别再瞎给“每步小奖”奖励函数是三维路径规划里最容易踩雷的部分。新手最喜欢给每步一个小的正向奖励“鼓励它前进”但这会引发一个经典问题无人机学到的是原地盘旋或者反复绕圈因为只要不撞墙、每步都有小奖绕圈成了最优策略。正确的做法是稀疏大奖励 少量辅助引导。到达目标点给一个大的正向奖励撞到障碍物或者飞出边界给一个大的负向奖励然后加一个小的“距离变化量”引导让它感觉到“比上一步更近了”是有利的。这个距离变化量要用指数衰减的形式避免数值过大抢了主奖励的戏份。def compute_reward(state, prev_dist, dist_to_goal, terminal, crashed): reward 0.0 if terminal and dist_to_goal 5.0: reward 100.0 # 到达终点 elif crashed: reward - 50.0 # 碰撞或越界 else: # 距离缩短激励正、负都会产生但幅度小 reward (prev_dist - dist_to_goal) * 2.0 # 轻微的高度惩罚避免飞行器贴着地面或天花板走 z state[-3] # 前面归一化时高度是倒数第二个元素 if z 0.05 or z 0.95: reward - 1.0 return reward距离变化量乘 2.0 是我调出来的一个经验值太大会让网络只盯着“冲向目标”完全不避障太小又等于没有引导在三维地形里有可能一直探索不到终点。高度惩罚是三维特有的问题二维里没有这个维度不加的话你会发现网络经常走极端高度试图钻 bug。3. 算法选型DDPG 在连续控制里的地位以及 TD3 为什么值得换上去3.1 DDPG 的核心机制与 Actor-Critic 结构三维路径规划的动作空间是连续的DQN 系列处理不了连续输出所以要在策略梯度家族里选。DDPGDeep Deterministic Policy Gradient是入门连续控制最经典的选择之一它用 Actor 网络输出确定性动作用 Critic 网络评估这个动作的 Q 值。训练时 Critic 充当裁判Actor 根据裁判的反馈调整自己的策略。DDPG 在无人机路径规划上的表现还算稳定但有一个众所周知的问题Q 值过估计。Critic 网络在更新时用的是 target 网络算出来的 Q 值而 target 网络本身也有误差多次迭代后这个误差会被放大导致 Critic 高估当前策略的真实表现最后学出的策略偏激进——反映在路径上就是贴着障碍物飞特别吓人。我自己的项目里最早用的 DDPG训练到中期路径已经能到达终点但可视化之后发现飞行轨迹离障碍物边缘只有 1 米多稍微有点风扰动就会撞上。这就是过估计导致的危险策略。3.2 TD3 做了哪三件事以及它为什么更适合路径规划TD3Twin Delayed DDPG在 DDPG 基础上做了三个关键修改每一个都是直接针对 DDPG 的软肋设计的第一双 Critic 网络取最小值。两个 Critic 独立估计 Q 值取较小值作为更新目标能压住过估计。第二延迟更新 Actor。Actor 更新频率比 Critic 低比如 Critic 每步都更新Actor 每两步更新一次避免 Actor 在 Critic 还没收敛时就被带偏。第三目标策略平滑。给 target Actor 输出的动作加一点高斯噪声相当于告诉 Critic “同一个状态附近的小扰动都应该有相似的 Q 值”提升泛化能力。如果你把 DDPG 换成 TD3三维路径规划的收敛速度会慢一点但学出来的路径安全裕度明显更大终点成功率也更高。工程上我直接建议跳过 DDPG 上 TD3省掉后面为了过估计问题去反复调参的时间。3.3 一套可复用的 TD3 网络骨架与训练循环最小代码这里给出一套能直接跑的最小 TD3 实现重点看网络结构和训练循环的细节完整的仿真环境需要你自己按前面的状态建模搭。import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import copy class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action): super().__init__() self.fc1 nn.Linear(state_dim, 256) self.fc2 nn.Linear(256, 256) self.out nn.Linear(256, action_dim) self.max_action max_action def forward(self, x): x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) # tanh 把输出压到 [-1, 1]再乘以实际动作范围 return self.max_action * torch.tanh(self.out(x)) class Critic(nn.Module): def __init__(self, state_dim, action_dim): super().__init__() self.fc1 nn.Linear(state_dim action_dim, 256) self.fc2 nn.Linear(256, 256) self.out nn.Linear(256, 1) def forward(self, x, action): x torch.cat([x, action], dim1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.out(x) class TD3Agent: def __init__(self, state_dim, action_dim, max_action, lr3e-4, gamma0.99, tau0.005, policy_noise0.2, noise_clip0.5, delay2): self.actor Actor(state_dim, action_dim, max_action) self.actor_target copy.deepcopy(self.actor) self.actor_optimizer torch.optim.Adam(self.actor.parameters(), lrlr) self.critic1 Critic(state_dim, action_dim) self.critic2 Critic(state_dim, action_dim) self.critic1_target copy.deepcopy(self.critic1) self.critic2_target copy.deepcopy(self.critic2) self.critic_optimizer torch.optim.Adam( list(self.critic1.parameters()) list(self.critic2.parameters()), lrlr) self.gamma gamma self.tau tau self.max_action max_action self.policy_noise policy_noise self.noise_clip noise_clip self.delay delay self.step_count 0 def act(self, state, noise0.1): state torch.FloatTensor(state).unsqueeze(0) action self.actor(state).detach().numpy()[0] # 训练时加探索噪声评估时不加 if noise 0: action np.random.normal(0, noise, sizeaction.shape) return np.clip(action, -self.max_action, self.max_action) def update(self, replay_buffer, batch_size256): self.step_count 1 # 从经验池采样 states, actions, rewards, next_states, dones replay_buffer.sample(batch_size) states torch.FloatTensor(states) actions torch.FloatTensor(actions) rewards torch.FloatTensor(rewards).unsqueeze(1) next_states torch.FloatTensor(next_states) dones torch.FloatTensor(dones).unsqueeze(1) # 目标策略平滑 noise torch.randn_like(actions) * self.policy_noise noise noise.clamp(-self.noise_clip, self.noise_clip) next_actions (self.actor_target(next_states) noise).clamp(-self.max_action, self.max_action) # 双 Critic 取最小 Q 值 target_q1 self.critic1_target(next_states, next_actions) target_q2 self.critic2_target(next_states, next_actions) target_q torch.min(target_q1, target_q2) target_q rewards self.gamma * (1 - dones) * target_q # 更新 Critic current_q1 self.critic1(states, actions) current_q2 self.critic2(states, actions) critic_loss F.mse_loss(current_q1, target_q.detach()) F.mse_loss(current_q2, target_q.detach()) self.critic_optimizer.zero_grad() critic_loss.backward() self.critic_optimizer.step() # 延迟更新 Actor if self.step_count % self.delay 0: actor_loss -self.critic1(states, self.actor(states)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() self.actor_optimizer.step() # 软更新 target 网络 with torch.no_grad(): for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()): target_param.data.copy_(self.tau * param.data (1 - self.tau) * target_param.data) for param, target_param in zip(self.critic1.parameters(), self.critic1_target.parameters()): target_param.data.copy_(self.tau * param.data (1 - self.tau) * target_param.data) for param, target_param in zip(self.critic2.parameters(), self.critic2_target.parameters()): target_param.data.copy_(self.tau * param.data (1 - self.tau) * target_param.data)网络结构上 Actor 和 Critic 都用了两层 256 个神经元的全连接层这个规模对三维路径规划够用了不用上 Transformer。延迟更新为 2意味着 Critic 每步更新、Actor 每两步更新在三维环境中能显著稳训练。经验池采样和训练频率不在代码里体现但要注意每交互 10 步做一次 update频率太低网络学不动太高则数据相关性太强导致不稳定。4. Python 工程化仿真环境、训练主循环、模型持久化4.1 把仿真环境封装成 Gym 风格接口别在训练循环里堆逻辑训练代码和仿真逻辑必须分开。不分开的结果是你想换一张地图得去训练循环里改障碍物代码你想换奖励函数又要去动 step 函数。我习惯把整个仿真环境封装成一个 Gym 风格的 class只暴露 reset、step 两个接口内部管好物理模型、碰撞检测、地图加载。class UAV3DEnv: def __init__(self, map_path, start, goal): self.map load_heightmap(map_path) # 读取高程图 self.start np.array(start, dtypenp.float32) self.goal np.array(goal, dtypenp.float32) self.drone_pose None self.prev_dist None def reset(self): self.drone_pose np.array([self.start[0], self.start[1], self.start[2], 0.0, 0.0]) self.prev_dist np.linalg.norm(self.start[:3] - self.goal) return generate_state(self, self.drone_pose) def step(self, action): # action 是 [d_yaw, d_pitch] d_yaw action[0] * 0.15 d_pitch np.clip(action[1] * 0.1, -0.6, 0.6) # 姿态更新与位置积分代码略与 2.3 节一致 dist np.linalg.norm(self.drone_pose[:3] - self.goal) crashed self.check_collision(self.drone_pose[:3]) done crashed or (dist 5.0) or (self.drone_pose[2] 0.1 or self.drone_pose[2] 90.0) reward compute_reward(generate_state(self, self.drone_pose), self.prev_dist, dist, done, crashed) self.prev_dist dist next_state generate_state(self, self.drone_pose) return next_state, reward, done, {dist: dist}这里的地图文件我用的是二维高程数组每一格存一个高度值无人机在格子上方飞行时通过与周围格子高度插值计算地形碰撞。真实地图可以用高程 TIFF 或者 GeoTIFF 转成 numpy 数组处理方式一样只是要加一步重采样把地图缩放到网格分辨率合适的大小。4.2 训练主循环经验池、探索噪声、保存断点一个都不能少训练主循环要解决三个问题经验怎么存、探索和利用怎么平衡、模型怎么保存。经验池用双端队列实现长度设 100 万——三维地图的状态空间比二维大需要更多样本池子太小会导致旧经验被过早覆盖训练后期反复遗忘。探索噪声用 Ornstein-Uhlenbeck 过程有时会有奇效但实践中我发现高斯噪声在 TD3 里已经够用没必要上 OU。噪声幅度从 0.1 开始随着训练进度线性衰减到 0.02衰减过快会让网络过早固化策略过慢则训练后期震荡。from collections import deque import random replay_buffer deque(maxlen1_000_000) episodes 2000 max_steps 500 noise 0.1 noise_decay 0.9995 for episode in range(episodes): state env.reset() episode_reward 0 for step in range(max_steps): action agent.act(state, noisenoise) next_state, reward, done, info env.step(action) replay_buffer.append((state, action, reward, next_state, float(done))) if len(replay_buffer) batch_size: for _ in range(10): # 每步做 10 次梯度更新 agent.update(replay_buffer, batch_size) state next_state episode_reward reward if done: break noise max(0.02, noise * noise_decay) if episode % 100 0: torch.save(agent.actor.state_dict(), factor_{episode}.pth) torch.save(agent.critic1.state_dict(), fcritic1_{episode}.pth) print(fEpisode {episode}, Reward {episode_reward:.2f}, Distance {info[dist]:.2f})每隔 100 个 episode 保存一次模型这里不用只保存最优模型的方式因为最优标准在训练过程中会变保存断点方便你回退到两周之前的参数重新调。4.3 评估模式关掉噪声跑测试统计终点到达率和平均路径长度训练过程中必须定期评估否则你不知道网络是真学会了还是在过拟合训练地图。评估时把噪声设成 0让 Actor 网络纯确定性输出动作跑 20 次从同一起点到同一终点的任务统计到达率。def evaluate(agent, env, episodes20, max_steps500): success_count 0 path_lengths [] for _ in range(episodes): state env.reset() path [env.drone_pose[:3].copy()] for step in range(max_steps): action agent.act(state, noise0) # 评估不加探索噪声 next_state, reward, done, info env.step(action) path.append(env.drone_pose[:3].copy()) if done and info[dist] 5.0: success_count 1 path_length sum(np.linalg.norm(np.diff(path, axis0), axis1)) path_lengths.append(path_length) break if done: break state next_state print(fSuccess Rate: {success_count / episodes * 100:.1f}%) if path_lengths: print(fAvg Path Length: {np.mean(path_lengths):.2f}m)终点到达率比平均奖励值更可信。我见过奖励值一路涨但终点到达率只有 30% 的情况说明网络找到了刷奖励的技巧但没有真正学会规划。5. GUI 设计让三维路径规划过程看得见、调得了5.1 用 PyQt5 或者 Tkinter选型标准是三维可视化和交互刷新GUI 在这个项目里的角色不是花架子而是训练过程的“黑匣子透视镜”。很多人训练 DRL 时不看过程只看最终 reward curve一旦 reward 涨了但路径是乱的你根本不知道问题出在状态设计还是奖励函数上。一个能实时显示三维路径的 GUI能让你在训练时肉眼发现“它在绕圈”“它在贴地飞”这类机器指标看不出、人一眼就能发现的异常。三维可视化层面PyQt5 加 Matplotlib 的 mplot3d 是最快落地的方案PyQt5 负责窗口、按钮、参数输入框mplot3d 负责画三维地形和轨迹。如果做得更重一点可以用 pyqtgraph 的 GLViewWidget性能更好、交互更流畅但学习成本高一些。我的建议是先 mplot3d 跑通路径点多了卡顿再上 pyqtgraph。5.2 GUI 布局训练控制区与三维显示区分离布局上分成两块左侧控制面板放训练参数和按钮右侧放三维图。按钮至少要包含“开始训练”“暂停训练”“单步评估”“加载模型”“保存当前路径”参数输入框放学习率、噪声初始值、训练步数。这个布局的核心原则是“训练中能改参数、能暂停、能看到当前状态”而不是一个只能看结果的播放器。import sys import matplotlib matplotlib.use(Qt5Agg) from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLineEdit, QLabel, QTextEdit) from PyQt5.QtCore import QThread, pyqtSignal import numpy as np class TrainThread(QThread): # 用信号把训练中间状态传回 GUI 主线程 update_log pyqtSignal(str) update_path pyqtSignal(object) update_episode pyqtSignal(int, float) def __init__(self, env, agent): super().__init__() self.env env self.agent agent self._is_running True def run(self): episodes 1000 for episode in range(episodes): if not self._is_running: break state self.env.reset() path [self.env.drone_pose[:3].copy()] total_reward 0 done False while not done: action self.agent.act(state, noise0.1) next_state, reward, done, info self.env.step(action) path.append(self.env.drone_pose[:3].copy()) total_reward reward state next_state if len(path) 500: break self.update_path.emit(np.array(path)) self.update_episode.emit(episode, total_reward) self.update_log.emit(fEpisode {episode}, Reward {total_reward:.2f}) def stop(self): self._is_running False class MainWindow(QMainWindow): def __init__(self, env, agent): super().__init__() self.env env self.agent agent self.init_ui() def init_ui(self): self.setWindowTitle(UAV 3D Path Planning with DRL) central QWidget() layout QHBoxLayout(central) # 左侧控制面板 control_panel QVBoxLayout() self.start_btn QPushButton(开始训练) self.stop_btn QPushButton(暂停训练) self.lr_input QLineEdit(3e-4) self.episode_input QLineEdit(1000) control_panel.addWidget(QLabel(学习率)) control_panel.addWidget(self.lr_input) control_panel.addWidget(QLabel(训练回合数)) control_panel.addWidget(self.episode_input) control_panel.addWidget(self.start_btn) control_panel.addWidget(self.stop_btn) layout.addLayout(control_panel) # 右侧三维画布 self.figure Figure(figsize(6, 6)) self.canvas FigureCanvas(self.figure) self.ax self.figure.add_subplot(111, projection3d) layout.addWidget(self.canvas) central.setLayout(layout) self.setCentralWidget(central) self.thread None这段代码里 TrainThread 是整个 GUI 的灵魂它把训练循环放到子线程里执行避免训练时界面卡死。如果你把训练直接跑在 GUI 主线程里点“暂停”按钮永远没反应因为主线程被训练占用了。PyQt5 的信号机制在这里负责把子线程的路径数据传回主线程更新画面。三维路径显示区域我一般把地形画成曲面把当前路径画成一条红线和目标点画成绿色五角星。每次收到新的路径数据就清空重画这样能看到路径在训练中的演变过程——一开始是乱七八糟的折线慢慢会变得接近一条合理航线。除了实时路径显示GUI 里还有一个容易忽略但很有用的功能导出路径。训练完成后把当前路径点导成 CSV后面做轨迹平滑或者导入到 PX4 仿真里跑一遍都要用这组数据。6. 训练三维 DRL 路径规划的 5 个避坑记录6.1 奖励值一直增长但路径在绕圈现象训练曲线的 episode reward 稳定上升但 GUI 里显示无人机在原地绕圈或者走一个很大的圆弧。原因奖励函数里“距离缩短”项给得太大网络发现绕一个大圈虽然路径长了但整体距离趋势是向终点靠近的累计奖励反而更高。这是三维环境里特别容易出现的“刷分”行为。解决把距离变化项的系数从 2.0 降到 0.5同时加入“到达终点步数越少奖励越高”的时间惩罚项比如每走一步扣 0.1。这样绕圈策略会因为步数太多变成负收益网络才会放弃这个投机路径。6.2 训练后期突然崩溃Q 值爆炸现象训练到 800 个 episode 左右loss 突然变得巨大随后所有路径都变成乱飞。原因Critic 网络参数更新迭代后出现过估计TD3 的双 Critic 能缓解但不能根除尤其在奖励尺度不一致时。这个问题最常出现在“到达终点给 100碰撞给 -50但距离变化量乘 2”这种奖励数值方差过大的场景。解决把奖励值全部缩放到 [-1, 1] 区间终点奖励设为 1.0碰撞设为 -0.5距离变化量除以一个固定尺度。数值归一化后的奖励让 Critic 网络更容易拟合Q 值爆炸问题基本能消失。另一个保险措施是梯度裁剪torch.nn.utils.clip_grad_norm_ 把梯度范数限制在 10 以内。6.3 状态空间加入太多无用信息导致不收敛现象把三维地图的完整体素网格作为状态训练了 3000 个 episode 还是随机动作水平。原因体素网格维度太高多层全连接网络根本没法从几万个输入里提取有效特征。而且大部分体素是空的网络花大量容量学习“空区域没有意义”这件事。解决换回雷达测距状态。如果觉得雷达信息不够可以堆两层雷达——一层是水平 360 度一层是垂直方向对地测距。24 维水平雷达 5 维垂直雷达 自身状态总共 33 维这个规模全连接网络完全吃得下。后面需要做更复杂的场景可以先用 CNN 处理雷达栅格图但这是后话了。6.4 训练时地图换一张就废掉现象在训练地图上终点到达率 90%换一张新地图直接掉到 20%。原因过拟合训练地图。无人机记住了“这张地图哪里有缝隙可以钻”而不是学会“感知周围环境并绕开障碍”的通用策略。解决训练时每 50 个 episode 随机切换一张地图。在环境类里加一个 load_random_map 方法从地图集里随机选一张。如果只有一张真实地图可以随机旋转、加噪、提升地形复杂度来生成变体。通用性提升的代价是收敛变慢但这是值得的毕竟路径规划系统学了单张地图没实际价值。6.5 真机部署时动作输出抖动剧烈现象仿真里很平滑的路径一上真机或者高保真仿真比如 AirSim飞行器左右摇摆。原因训练时用的状态是精确无噪声的而真机传感器有误差雷达测距噪声会在动作输出上放大。另一个原因是训练时固定步长 dt 太大动作更新频率跟不上真机控制频率。解决训练时给状态加高斯噪声模拟传感器误差。把 dt 从 0.5 秒降到 0.1 秒动作变化量也要相应缩小让网络适应更细粒度的决策节奏。还有一招是在动作输出后加一个一阶低通滤波让期望偏航角变化量不会突变而是平滑过渡。7. 进阶验证从仿真到真机的三步走以及一个评估路径质量的小技巧路径规划做出来之后千万别只停留在 Matplotlib 里画一条线说“成了”。我自己的血泪经验是仿真里怎么飞都是纸面功夫真正要验证这套 DRL 策略能用至少走完下面三步。第一步在更高保真的仿真器里复测。Matplotlib 环境里的物理模型太粗糙了碰撞检测用的只是高度图插值。我会把训练好的 Actor 网络导出为 ONNX 格式或者直接用 Python 接口接进 AirSim 或者 Gazebo让它在一个带真实动力学模型和传感器噪声的环境里再跑一遍。这一步能暴露很多“平面仿真”看不见的问题——比如雷达探测距离有限状态向量里探测不到远处的障碍物网络就会在距离障碍物很近时才反应过来而高保真仿真里的动力学滞后会让无人机来不及转弯。第二步评估路径的工程可行性。路径规划的输出要能被飞控跟踪光看终点到达率不够。我会计算路径的曲率分布和相邻路径点之间的转向角变化量如果转向角突变超过飞机最大转弯速率这条路径在真机上就是废的。这里有个小技巧在评估环境里引入一个简易的飞机运动学模型不只做简单的积分移动而是加一个最大转弯速率约束看规划出来的路径是否在约束范围内能提前淘汰大部分“数学上可行、物理上飞不了”的路径。第三步离线轨迹平滑与重放。即使 DRL 输出的路径已经很平滑直接喂给飞控还是要做一次 B 样条或者 Dubins 曲线拟合把路径点转换成一系列航点指令。我自己用的做法是在 GUI 里加一个“路径导出”功能导出 CSV 后用一个脚本做三次样条插值再检查曲率曲率合格才能进真机测试。def compute_curvature(path): 计算路径上每个点的离散曲率用于评估可飞性 path: ndarray shape (N, 3) curvatures [] for i in range(1, len(path) - 1): p_prev path[i - 1] p_curr path[i] p_next path[i 1] # 用三点估算第一、二阶差分 d1 p_curr - p_prev d2 p_next - p_curr # 离散曲率 |d1 x d2| / (|d1| * |d2| * sin_angle) cross np.linalg.norm(np.cross(d1, d2)) denom np.linalg.norm(d1) * np.linalg.norm(d2) if denom 1e-6: curvatures.append(0.0) else: curvatures.append(2 * cross / denom) return np.array(curvatures) # 使用示例 path np.loadtxt(exported_path.csv, delimiter,) curv compute_curvature(path) print(f最大曲率: {curv.max():.4f}, 平均曲率: {curv.mean():.4f}) if curv.max() 0.1: # 根据机型转弯能力设定阈值 print(路径曲率超限需要平滑处理)如果曲率超限我会回到前面的 B 样条拟合或者调整奖励函数里“平滑度惩罚”这一项。也可以在训练时把前后两步的动作差作为一个小的负向奖励加进去让网络自己学会输出更平滑的动作序列这个做法在实践里最能从根上解决问题。最后唠叨一句训练 DRL 路径规划最忌讳的就是把 reward curve 当唯一指标。你真正要看的是 GUI 里那条三维轨迹长什么样、在几个典型地形上能不能稳定到达、曲率分布合不合理。这套从建模到评估的流程走下来我相信你手里那套 Python 实现就不再只是一个 demo 而已希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?