资讯动态

DQN三维无人机避障航迹规划:原理、实现与调参实战

发布时间:2026/10/14 6:10:43 来源:尧图企业网站定制
简介面向无人机避障与深度强化学习研究者的完整实现包基于DQN算法解决三维环境中无人机自主航迹规划问题。压缩包共15个文件以Python源码为主涵盖模型定义、经验回放缓冲区、无人机仿真环境与训练脚本同时包含两个预训练权重文件可直接加载验证另有航迹动图、效果图及中英文说明文档整体仅2.01MB结构紧凑。目前已有788人学习。借助该资源可系统掌握DQN的状态空间、动作空间和奖励函数设计思路了解经验回放与固定目标网络如何提升训练稳定性通过运行代码可观察航迹逐步收敛过程并基于现有模型做动态障碍、多机协同等方向的扩展。适合作为算法入门参照也是无人机导航项目二次开发的轻量级基础代码。1. DQN在三维无人机避障航迹规划里到底解决了什么问题拿二维栅格地图做A*谁都会但把无人机放进三维空间障碍物不规则、目标点会移动、传感器还有噪声传统的全局规划就变得很脆弱。基于DQN的三维无人机避障航迹规划是把避障决策变成一次次“看到状态 - 选择动作”的强化学习过程网络直接读位置、目标方向、传感器测距这些状态输出每个动作的预估价值训练完之后只需要喂状态就能做出决策不需要重新搜索路径。它适合做仿真验证、低空避障、末端物流路径预研的开发者特别是那种调PID时被规则列表逼疯、转而想用网络自动学策略的人。2. 把三维避障航迹规划改造成DQN能吃的马尔可夫决策过程DQN不认地图它只认状态向量。它面对的是马尔可夫决策过程每一步决策只需要依赖当前状态而不需要记住整条历史轨迹。这就要求状态向量必须包含足够的信息否则同一个状态对应完全不同的后果Q值更新会来回震荡。下面把状态、动作、训练目标三件事拆开讲。2.1 状态空间无人机自身位置、目标方向与传感器测距的合理组合状态至少包含三类信息无人机当前位姿、目标点相对位置、局部障碍物感知。常见做法是用一个一维数组把它们拼起来维度不宜太高十几维到几十维都常见过高会让网络在有限样本下难以收敛过低又会因为感知不到障碍物而穿模。我一般会把状态拆成三段前3维是无人机位置在仿真空间里的归一化坐标中间3维是目标点相对无人机的位置和距离方向后6维是前后左右上下六个方向的障碍物距离。障碍物距离不需要给精确值给一个归一化的“当前方向几格内是否有障碍”反而更稳定因为雷达噪声在真实环境里很大DQN对精确测距值并不敏感。状态向量最终大概长这样位置pos / grid_size压到0~1目标相对位置(target - pos) / grid_size同样压到0~1六向近距探测每个方向从本格开始向外扫碰到障碍物或边界就返回一个0~1的接近程度值注意位置和目标相对位置必须归一化。很多人在这一步偷懒直接把原始坐标扔进网络结果训练初期loss被不同量纲带偏后面调奖励函数时你根本分不清是奖励问题还是输入尺度问题。归一化不需要多复杂每个量除以空间尺寸就行。传感器探测的范围也要想清楚。探测半径太小无人机只有撞到跟前才看到障碍物探测半径太大状态里一大半是0信息稀疏网络学不到区分度。在24格左右的空间里探测半径4到6格是比较常用的区间。2.2 动作空间离散化偏航、俯仰与速度而不是输出连续油门很多人一上来就让DQN输出连续油门和舵量结果网络在一个小区域里反复震荡训练几小时都飞不出去。DQN本质是离散动作价值评估连续控制更适合DDPG、TD3这类连续动作算法所以做避障航迹规划时我习惯把动作离散成“偏航角增量、俯仰角增量、速度档位”的组合。常见做法是保持巡航速度不变只让飞行器在偏航角[-30°, 0°, 30°]和俯仰角[-10°, 0°, 10°]里组合出9个动作。下面这个表是典型动作编码动作编号偏航角增量(度)俯仰角增量(度)含义0-30-10左转并下降1-300左转平飞2-3010左转并爬升30-10直行下降400直行平飞5010直行爬升630-10右转并下降7300右转平飞83010右转并爬升9个动作已经能覆盖大多数避障场景。动作空间越小探索越容易收敛越快动作空间越大机动性越强但需要更长训练步数。如果发现避障成功率一直上不去先砍动作数量别急着加网络层数。需要提醒的是很多人先用六方向网格移动来验证算法就是上下左右前后选一格移动等DQN能飞了再把动作换成真实的偏航俯仰组合。这个思路没问题但网格移动的动作空间和连续飞行的动力学差别很大网格上收敛的策略换到连续模型里往往不能直接用只能用来验证网络和奖励逻辑有没有问题。2.3 DQN的训练目标从Q值更新到经验回放DQN要做的事是拟合一个Q函数 Q(s, a)表示在状态s下选择动作a后未来累计奖励的期望。每次无人机走一步得到一条经验 (s, a, r, s)然后用目标网络计算的 r γ max_a Q_target(s, a) 作为回归目标让在线网络去逼近这个值。这就是强化学习里经典的“用估计去更新估计”如果不做任何限制训练会发散。所以两个机制必须加。一是经验回放把最近N步经验存起来随机抽样打破连续经验之间的相关性二是目标网络每隔一定步数把在线网络的参数复制过去让回归目标在一段时间内保持稳定。这两样是所有DQN避障项目的地基少了任何一个loss曲线会像过山车一样乱跳。折扣因子γ建议取0.98到0.99之间。三维避障的回合一般不会特别长γ太接近1会让Q值对远期事件过度敏感γ太小又会让无人机只盯着眼前几步绕到一半看不到目标。经验回放容量不用无限大几万条足够太老的策略经验也会干扰当前学习。3. 用Python从零搭一个DQN避障训练最小实现这里给一套能直接跑通的简化实现。为了把代码量压到最小环境用体素网格离散运动真实飞行器后面再替换成连续运动学模型。重点是先把DQN的输入、输出、奖励反馈完整跑起来。3.1 简化三维环境体素障碍物与六向移动环境是一个24x24x24的网格空间随机生成4个长方块障碍物起点在角落目标在远处。动作为6个方向移动一步。状态用位置、目标相对位置、六个方向障碍物探测拼接而成。import numpy as np class UAVEnv: def __init__(self, grid_size24, targetNone): self.grid_size grid_size np.random.seed(0) # 随机生成 4 个长方块障碍物 self.obstacles np.zeros((grid_size, grid_size, grid_size)) for _ in range(4): x0, y0, z0 np.random.randint(4, 18, size3) x1 min(grid_size, x0 np.random.randint(2, 6)) y1 min(grid_size, y0 np.random.randint(2, 6)) z1 min(grid_size, z0 np.random.randint(2, 6)) self.obstacles[x0:x1, y0:y1, z0:z1] 1 self.target target if target is not None else np.array([20., 20., 20.]) self.obs_radius 4 self.reset() def reset(self): self.pos np.array([2., 2., 2.], dtypefloat) self.last_dist np.linalg.norm(self.pos - self.target) return self._state() def _scan(self): # 六个方向的障碍物接近程度0 表示无障碍1 表示紧贴障碍 scans [] for delta in [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]]: hit 0.0 for r in range(1, self.obs_radius 1): coord self.pos np.array(delta, dtypefloat) * r if coord.min() 0 or coord.max() self.grid_size: hit 1.0 break if self.obstacles[tuple(coord.astype(int))] 1: hit 1.0 - (r - 1) / self.obs_radius break scans.append(hit) return np.array(scans, dtypefloat) def _state(self): # 状态 归一化位置 归一化目标相对位置 六向探测 pos_norm self.pos / self.grid_size rel_norm (self.target - self.pos) / self.grid_size return np.concatenate([pos_norm, rel_norm, self._scan()]) def step(self, action): dirs [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]] new_pos self.pos np.array(dirs[action], dtypefloat) # 边界惩罚撞墙直接结束 if new_pos.min() 0 or new_pos.max() self.grid_size: return self._state(), -2.0, True, {} # 碰撞惩罚撞障碍物直接结束 if self.obstacles[tuple(new_pos.astype(int))] 1: return self._state(), -10.0, True, {} dist_new np.linalg.norm(new_pos - self.target) reward (self.last_dist - dist_new) * 0.5 self.last_dist dist_new self.pos new_pos done dist_new 2 reward 20.0 if done else 0.0 return self._state(), reward, done, {}这套环境的奖励逻辑很直白离目标更近就给正奖励撞障碍物或边界直接结束并给大惩罚。注意碰撞分支里没有更新last_dist因为回合已经终止不影响下一轮。探测半径obs_radius设成4意味着无人机最多提前感知4格外的障碍物网格里一个动作走一格这个反应距离基本够用。如果你把障碍物设计得更大探测半径也需要跟着加大。3.2 DQN网络与经验回放两层全连接足够起步三维避障状态维度不高网络不需要很深。一个输入层加两个128维全连接层输出维度等于动作数6足够拟合大多数简单地形。经验回放用双端队列实现容量5万条随机采样时把numpy数组转成PyTorch张量。import torch import torch.nn as nn from collections import deque import random class DQN(nn.Module): def __init__(self, state_dim12, action_dim6): 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, capacity50000): self.memory deque(maxlencapacity) def push(self, s, a, r, s2, d): self.memory.append((s, a, r, s2, d)) def sample(self, batch_size): batch random.sample(self.memory, 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]).unsqueeze(1) s2 torch.FloatTensor([b[3] for b in batch]) d torch.FloatTensor([b[4] for b in batch]).unsqueeze(1) return s, a, r, s2, d经验回放里存的d是回合结束标志计算目标Q值时用来屏蔽终点后的预测值。这里用.unsqueeze(1)把一维数组变成列向量是为了和Q值张量的形状对齐。容量5万条对这个小环境来说已经足够如果你的空间更大、探索步数更长可以加到10万但不要无脑堆容量太大回放里老样本太多更新方向会被带偏。3.3 训练主循环目标网络同步与探索率调度训练参数我习惯这样设Adam优化器学习率1e-3批大小128折扣因子0.99目标网络每100步同步一次探索率从1.0按指数衰减到0.05。探索率衰减速度很关键衰减太快早期找不到奖励衰减太慢后期策略不稳定。def train(episodes1500): env UAVEnv() online DQN() target DQN() target.load_state_dict(online.state_dict()) buf ReplayBuffer() opt torch.optim.Adam(online.parameters(), lr1e-3) gamma 0.99 batch_size 128 sync_interval 100 epsilon_start, epsilon_min, epsilon_decay 1.0, 0.05, 0.995 for ep in range(episodes): s env.reset() total_reward 0 done False epsilon max(epsilon_min, epsilon_start * (epsilon_decay ** ep)) while not done: if random.random() epsilon: a random.randint(0, 5) else: with torch.no_grad(): q online(torch.FloatTensor(s)) a q.argmax().item() s2, r, done, _ env.step(a) buf.push(s, a, r, s2, done) s s2 total_reward r if len(buf.memory) batch_size: s_b, a_b, r_b, s2_b, d_b buf.sample(batch_size) q_now online(s_b).gather(1, a_b) # 计算目标Q值时把 target 网络包在 no_grad 里 with torch.no_grad(): q_next target(s2_b).max(1, keepdimTrue).values target_q r_b gamma * q_next * (1 - d_b) loss nn.MSELoss()(q_now, target_q) opt.zero_grad() loss.backward() opt.step() if ep % sync_interval 0: target.load_state_dict(online.state_dict()) if ep % 50 0: print(fep {ep}, reward {total_reward:.2f}, eps {epsilon:.2f})这段代码把DQN的骨架完整串起来了。注意gather是按动作索引取出对应的Q值max是取目标网络估计的最大Q值。探索率按回合数指数衰减每次取max(epsilon_min, ...)保证后期仍然有少量随机动作防止过早锁定局部策略。如果你想先验证代码能不能跑把episodes改成100然后把打印间隔改小只要reward曲线不再一直下跌就说明基本功底没问题。之后再加大回合数开始正式调奖励函数。4. 奖励函数参数调优从稀疏到稠密差的不只是一行代码奖励函数是DQN避障项目里最玄学的部分。它不是加一行公式就有用参数之间会互相打架。4.1 稀疏奖励的“稀疏”到底有多稀疏只在到达目标时给20碰撞时给-10其余步奖励0这叫稀疏奖励。在小网格里看起来没问题但DQN要在几十步里不断试错随机动作找到目标的概率极低网络根本等不到正反馈。训练几百回合后策略退化成一味躲罚停着不动或者原地转圈因为不撞就不会扣分。解决方向不是简单调大到达奖励而是把每一步的反馈做稠密。要让无人机时刻知道“这一步相比上一步是变好了还是变坏了”。最常见的是距离差分奖励r d_old - d_new靠近目标就加分远离目标就减分。这个信号几乎每个步都非零训练效率会明显提升。但稠密化会带来新问题无人机可能为了最大化每一步的距离差走出奇怪的弧线甚至故意绕远路因为绕远路的某些中间步距离差更大。所以稠密奖励不是越多越好要控制每一项的权重。4.2 稠密化奖励怎么搭距离差、危险区、动作惩罚三项合一一个能用的奖励函数至少包含三个部分距离差分奖励鼓励朝目标靠近碰撞/边界惩罚保证安全边界时间或动作惩罚抑制绕圈和原地打转在最小实现里公式可以写成R α * (d_t - d_{t1}) - β * collision - γ * time_penalty下面是一套从零起步的参考参数参数含义典型初始值敏感度α距离差分权重0.5太大易绕路太小无引导β碰撞惩罚10太小穿障碍太大无人机不敢动γ每步固定惩罚0.02太小会转圈太大不敢移动到达奖励终点额外奖励20与α互相影响α0.5时一步靠近一格最多能拿0.5左右而碰撞一次扣10相当于牺牲20步靠近优势才敢撞一次。多数情况下这个比例能压住无人机不碰障碍。γ0.02看起来很小但一个回合几十步累计起来相当于一个中等奖励能有效惩罚原地转圈。注意到达奖励和距离差分是耦合的。如果你把到达奖励改成50无人机可能倾向于直接冲向目标哪怕中途擦碰障碍物因为成功一次收益远大于几次碰撞的惩罚。这种情况下要同步把碰撞惩罚提高。4.3 奖励参数互相打架的三个案例案例一α1.0β10γ0无人机走了一个大弧线虽然绕路但每一步都在靠近目标累计距离差收益比直飞还高。这不是网络笨是奖励函数本身奖励了绕路。把α降回0.5同时增加每步时间惩罚γ让绕路的总收益低于直飞。案例二β50γ0.1α0.3无人机停在起点附近完全不动。因为每走一步都有0.1的惩罚周围障碍物又多碰撞扣50太狠网络学到的策略是“不动就不扣”。这是典型的保守策略。解决方法是给每步惩罚加一个上界或者给无人机一个初始推进力例如动作选择偏向直行。案例三到达奖励设成100碰撞惩罚只有5无人机开始各种撞墙因为撞几次只要成功到达一次就回本。这个问题的本质是风险收益失衡调整方向是提高碰撞惩罚而不是降低到达奖励。调参时一定不要只看最终成功率要记录每个回合的平均步数、总奖励、碰撞次数三个指标。总奖励上升但步数也大幅上升大概率是奖励函数奖励了绕路。碰撞次数降为零但成功率也低大概率是策略趋于保守。5. DQN三维避障训练避坑四个反复踩的收敛陷阱这一章是血泪经验。很多项目不是模型写错而是下面几个不起眼的细节把整个训练拖垮。5.1 现象loss一直下降但避障成功率不升反降第一次跑通时我看到loss从很大降到很小心里还挺高兴结果测试时无人机几乎每次都在终点附近乱转。原因马上暴露奖励函数里距离差分的占比太小网络优化的主要方向变成了“少扣分”而不是“飞得更远”。解决把奖励的每一项单独打点输出。训练时跑几条较短的采样轨迹打印每个时间步的distance_reward、collision_penalty、time_penalty。如果distance_reward平均值接近0说明距离信号被其他两项淹没了需要调大α。Loss下降只能说明网络在拟合当前目标不能说明策略在变好。5.2 现象探索率衰减设置不同收敛结果天差地别同一个环境epsilon_decay设成0.99比0.995差一大截。原因是0.99衰减太快不到300个回合探索率就降到0.05附近早期根本没摸清地图结构后面只能钻牛角尖。解决把手动降低探索率改成按到达次数衰减或者干脆用线性衰减前60%训练回合从1.0线性降到0.1后40%固定为0.1。这样前期充分探索后期稳定利用。不要迷信指数衰减它适合简单环境复杂三维避障里往往需要更长探索周期。5.3 现象目标网络同步太频繁训练直接发散不稳的时候我试过每10步同步一次目标网络结果Q值开始暴涨很快就变成NaN。原因是目标网络跟随在线网络太紧回归目标和当前预测之间几乎没延迟DQN变成了自转的飞轮任何一个过估计都会被迅速放大。解决目标网络同步间隔至少50步起步推荐100到200步。判断标准是如果Q值曲线一路向上不回头先检查是不是同步间隔太短了。另一个辅助手段是给在线网络梯度裁剪值设成1到10之间能防止极端Q值波动。5.4 现象训练时避障很好换一张地图就崩用固定的随机种子生成了训练地图避障成功率刷到95%把地图换掉马上掉回20%。原因是无人机把障碍物的具体坐标背下来了而不是学到通用的避障模式。解决训练时每个回合重新生成障碍物或者至少准备3到5张地图轮流切换。如果目标是部署到未知环境还要在状态里加入足够的局部传感器信息让全局位置的作用下降。全局位置信息越强越容易过拟合到地图布局。5.5 现象碰撞检测步长太大无人机“穿模”了网格环境下每个动作走一格碰撞检测只判断终点那一格是否被占用。当运动变成连续模型后如果仿真步长是0.5秒无人机可能在两个采样点之间穿过一个薄障碍物。表现是雷达明明扫到了墙但无人机还是一头扎进去。解决连续环境里不能只判断终点要做射线插值每0.1个步长间隔检查一次路径上是否有障碍物。网格环境虽然不会穿模但也要注意障碍物膨胀层给障碍物外围加一圈虚拟膨胀让航线尽量远离障碍物表面。6. 让避障航迹更稳的进阶动作Double DQN与航迹平滑6.1 Double DQN消除Q值过估计的那点改动普通DQN在计算目标值时直接取max天然会高估Q值噪声越大的地方高估越严重。Double DQN的思路是用在线网络选动作用目标网络评估这个动作的值把选动作和估值拆开。改动只有两行# 普通 DQN用目标网络选动作并估值 q_next target(s2_b).max(1, keepdimTrue).values # Double DQN在线网络选动作目标网络估值 next_action online(s2_b).argmax(1, keepdimTrue) q_next target(s2_b).gather(1, next_action) target_q r_b gamma * q_next * (1 - d_b)这样做的实际效果是打压那些被噪声放大的虚假高值Q训练后期更稳定。如果你发现无人机总在某个拐角突然激进加速多半就是Q值过估计在作祟。Double DQN不是万灵药但实现成本极低建议默认加上。6.2 航迹平滑把折线变成可飞航线DQN输出的航迹是一串离散点直接给飞控飞会很抖。落地时我习惯用B样条插值做后处理再检查平滑后的每一小段是否碰到障碍物。代码用SciPy就能做from scipy.interpolate import splprep, splev # path 是无人机飞行轨迹的三维坐标序列形状是 (N, 3) tck, u splprep([path[:, 0], path[:, 1], path[:, 2]], s1.0) smooth_points splev(np.linspace(0, 1, 100), tck) smooth_points np.vstack(smooth_points).T # 检查平滑后采样点是否穿障碍穿了就降低平滑强度 s 或回到原始点 for p in smooth_points: obs_idx tuple(np.round(p).astype(int)) if obstacles[obs_idx] 1: smooth_points path breaks参数控制平滑程度s越大曲线越光滑但也越容易偏离原始航迹。我一般的做法是先取s0.0完全贴近原始点再逐渐增大直到转折角小于无人机最大转弯角为止。做完之后再检查碰撞平滑绝不能牺牲安全性。这一路调下来我最深的教训是每次改参数前一定要把随机种子、奖励权重、网络结构记录清楚。DQN的收敛结果很大程度取决于随机性同一个参数换一个种子结果可能完全不同。把种子和参数写进保存路径隔几天再回来才分得清哪份结果是哪组参数跑的。希望这篇实战记录能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑