☰
DQN三维无人机避障航迹规划:原理、实现与调参实战
2026/10/14 6:10:41 网站建设 项目流程

简介:面向无人机避障与深度强化学习研究者的完整实现包,基于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-30+10左转并爬升
30-10直行下降
400直行平飞
50+10直行爬升
6+30-10右转并下降
7+300右转平飞
8+30+10右转并爬升

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_size=24, target=None): 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, size=3) 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.], dtype=float) 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, dtype=float) * 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, dtype=float) 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], dtype=float) # 边界惩罚,撞墙直接结束 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_dim=12, action_dim=6): 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, capacity=50000): self.memory = deque(maxlen=capacity) 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(episodes=1500): env = UAVEnv() online = DQN() target = DQN() target.load_state_dict(online.state_dict()) buf = ReplayBuffer() opt = torch.optim.Adam(online.parameters(), lr=1e-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, keepdim=True).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(f"ep {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_{t+1}) - β * 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, keepdim=True).values # Double DQN:在线网络选动作,目标网络估值 next_action = online(s2_b).argmax(1, keepdim=True) 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]], s=1.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 break

s参数控制平滑程度,s越大曲线越光滑,但也越容易偏离原始航迹。我一般的做法是先取s=0.0完全贴近原始点,再逐渐增大,直到转折角小于无人机最大转弯角为止。做完之后再检查碰撞,平滑绝不能牺牲安全性。

这一路调下来,我最深的教训是:每次改参数前一定要把随机种子、奖励权重、网络结构记录清楚。DQN的收敛结果很大程度取决于随机性,同一个参数换一个种子结果可能完全不同。把种子和参数写进保存路径,隔几天再回来才分得清哪份结果是哪组参数跑的。希望这篇实战记录能帮到你。

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

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

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

立即咨询