
1. 为什么要把Q-learning和人工势场揉在一起——算法选型思路1.1 先聊聊两种算法各自的脾气做无人机航迹规划的人大概率都跟人工势场法打过交道。这玩意儿思路特别直白把目标点设计成引力源把障碍物设计成斥力源无人机在势场中沿着合力方向走路径自然就出来了。优点是计算量小、实时性好规划出来的路径平滑不用像A*那样在栅格地图上一步步搜索也不需要像RRT那样做碰撞检测和路径剪枝。但人工势场有个出了名的毛病——局部极小值。说白了就是引力和斥力在某一点上恰好大小相等方向相反合力趋近于零无人机就卡在那儿来回抖怎么都飞不出去。典型场景是U型障碍物或者对称布置的障碍群实测中十个案例至少有三四个会踩到这个坑。再来看Q-learning。这是强化学习里最经典的免模型算法核心思路是维护一张Q表记录每个状态下执行每个动作的期望累计回报通过不断试错更新Q值最终学到一条最优策略。它的优势在于不需要环境模型天然具备跳出局部极值的能力因为智能体在探索过程中会尝试不同的动作不太容易被某个局部陷阱锁死。但Q-learning的短板也很明显状态空间一大了收敛速度感人而且训练初期完全是乱飞毫无章法。这两种算法放在一起其实是互补的。人工势场负责快和稳Q-learning负责绕和逃融合起来就是既快又能绕开局部极小值。我在实际做仿真的时候发现融合算法在多个典型场景下路径长度平均能比纯人工势场缩短15%到20%而且几乎不会出现卡死的情况。1.2 融合设计的核心逻辑谁主导、谁兜底融合并不是简单地把两个算法的输出加权平均那样搞出来的路径反而两头不讨好。我的做法是分层协作人工势场作为主控制器负责实时生成飞行航向Q-learning作为监督者在检测到无人机陷入局部极小值或者势场合力异常时接管控制输出一个跳出当前区域的动作序列飞一段距离后再把控制权交还给势场。这个逻辑想清楚之后整个仿真框架就清晰了。主循环里每一帧先计算势场合力判断合力是否低于阈值或者无人机是否在某个范围内震荡如果是就触发Q-learning决策否则就正常走势场。需要注意的是融合算法的核心不是同时用而是智能切换这跟人多线程协作一个道理——一个人主干活另一个人在旁边盯梢发现不对再接过来处理。仿真时我用的MATLAB版本是R2023b工具箱只需要基础的环境就行不需要额外的强化学习工具箱因为Q-learning完全可以手写代码量不大后面我会贴出核心代码。2. 仿真环境搭建与无人机运动模型2.1 三维空间建模无人机航迹规划肯定不能只在二维平面上跑实战中要考虑高度变化所以仿真环境直接做成三维的。我建了一个1000m × 1000m × 300m的空间里面随机撒了若干球形障碍物每个障碍物用中心坐标加半径表示。球体的好处是碰撞检测简单计算无人机到球心的距离减去半径小于安全距离就算碰撞。地图用MATLAB的scatter3函数做可视化障碍物用sphere函数生成网格再贴到对应坐标上。无人机的位置用一个1×3的行向量记录目标点设在地图的对角线方向起点和目标点之间故意摆几个障碍物逼着算法绕行。初始化参数表我直接给出方便复现参数取值说明空间范围[0, 1000] × [0, 1000] × [0, 300]单位m起点[50, 50, 80]起点坐标目标点[900, 900, 220]终点坐标障碍物数量10可随机生成障碍物半径范围[30, 60]单位m安全距离15单位m无人机步长8每步移动距离单位m2.2 无人机运动学模型与约束仿真里的无人机我按固定翼来建模不讲那么复杂的六自由度模型而是用一个简化的三维质点模型约束条件就两条最大转弯角和最大爬升角。每一帧无人机只能在前一个航向的方向基础上偏转有限角度这个约束必须加不加的话规划出来的路径虽然好看但实际飞不了。具体实现是记录当前航向角偏航角ψ和俯仰角θ下一步的方向向量必须满足|Δψ|≤ψ_max和|Δθ|≤θ_max。我在仿真里取ψ_max30°θ_max20°每步移动距离固定8米。这一步处理完路径就具备基本的可飞性了。还有个细节无人机不能无限贴近障碍物即使没撞上也存在气流扰动风险。所以我设置了安全距离15米当距离小于这个值时不管势场怎么算直接触发Q-learning接管。这个安全兜底逻辑在实际仿真里非常有用能避免很多边缘case。2.3 MATLAB仿真框架结构整个仿真框架我分成四个模块结构上非常清晰main_APF_QL.m主程序负责初始化参数、构建地图、循环调用UAV更新compute_potential.m人工势场模块输入无人机位置和地图信息输出引力、斥力和合力方向q_learning_decision.mQ-learning决策模块输入当前状态和Q表输出动作序列update_q_table.mQ表更新模块输入经历的状态-动作-奖励序列更新Q值主循环的逻辑是先计算势场再判断当前状态是否需要Q-learning介入如果是就执行一次Q-learning决策把输出的动作序列逐帧执行执行完后重新回到势场控制。这套框架的模块化程度很高后期如果想换算法或者换地图改对应模块就行不用动整体结构。3. 人工势场模块设计细节3.1 引力场与斥力场的构造人工势场的经典公式大家都知道但细节里有很多坑。引力场我采用的是传统形式U_att(q) 0.5 × k_att × ρ²(q, q_goal)其中k_att是引力增益系数ρ(q, q_goal)是无人机当前位置到目标点的距离。引力是势场的负梯度方向指向目标点大小与距离成正比。斥力场稍微麻烦一点不能简单地用传统的单点斥力公式因为那样在无人机靠近障碍物时斥力会急剧增大导致路径剧烈抖动。我做了一点改进在斥力函数里加入了无人机与目标点的距离因子这样当无人机靠近目标点时即使附近有障碍物斥力也会减弱避免出现目标点附近到不了的问题。改进后的斥力场形式是U_rep(q) 0.5 × k_rep × (1/ρ - 1/ρ₀)² × ρⁿ(q, q_goal)当 ρ ≤ ρ₀ 时其中ρ是无人机到障碍物的距离ρ₀是斥力作用范围半径n是一个调节系数一般取2。这个改进能显著改善目标点附近的行为是工程上非常实用的小技巧。3.2 势场参数标定绕不开的坑势场参数这块我踩过不少坑重点说三个。第一个是k_att和k_rep的比例。k_att太小的话无人机在障碍物密集区域会被斥力推得远远的路径绕得离谱k_att太大的话无人机容易直接撞上障碍物因为引力强到无视斥力。我实测下来k_att取0.8、k_rep取2.0在一个比较合理的区间但这个不是固定的得看你地图的障碍物密度和大小建议先跑几个case观察路径形态再微调。第二个是斥力作用范围ρ₀。ρ₀设得太大无人机离障碍物老远就开始绕行路径效率低设得太小反应不及时容易撞上。我的经验是ρ₀取障碍物半径的2到2.5倍或者直接设为固定值80米在这个范围内效果比较均衡。第三个是局部极小值检测阈值。这个直接关系到融合算法的切换灵敏度。我设计的检测逻辑是连续10步内无人机位置变化量小于步长的0.3倍且合力方向变化超过90°判定为陷入局部极小值。这个阈值可以根据地图复杂度调整地图复杂可以把步数阈值放宽到15步。4. Q-learning强化学习模块实现4.1 状态空间离散化设计Q-learning要落地的第一步就是把连续状态空间离散化。无人机的位置是连续的理论上无限个状态不可能逐一建立Q表。我的做法是相对状态编码把无人机相对于目标点的方位角和俯仰角作为核心状态量再加上近处是否有障碍物这个布尔量。具体来说把方位角分成12个区间每个30°俯仰角分成6个区间每个15°障碍物标志位取0或1总状态数就是12×6×2 144种。Q表大小就是144×66是动作数量这个规模MATLAB跑起来毫无压力收敛速度也快。选这个状态编码方式的原因是相对的方位比绝对坐标更有泛化能力无人机在地图任何位置只要相对关系一致策略就能复用。这一点在换地图验证时特别有用换了个地图大概率不用重新训练。4.2 动作空间与Q表更新动作空间我定义了6个离散动作分别是直行、左转30°、右转30°、爬升15°、俯冲15°、悬停。为什么不把动作粒度搞细一点因为Q-learning靠的是探索动作空间太大每个动作的探索次数就少Q值估计方差大收敛就慢。6个动作在这个场景下是足够用的。Q值更新用的是标准公式Q(s,a) ← Q(s,a) α × [r γ × max Q(s,a) - Q(s,a)]超参数我测试后确定了一组比较稳的组合学习率α0.3折扣因子γ0.9探索率ε初始0.3每轮衰减到0.05下限。训练轮数设了500轮每轮从起点到目标点算一轮实际跑到300轮左右Q表就基本稳定了后面200轮算是冗余。4.3 奖励函数怎么设计才不翻车奖励函数是Q-learning里最考验功力的部分设计不好算法根本学不到东西。我的奖励函数分为四部分到达目标点100硬奖励学到最后必须能到撞上障碍物-50硬惩罚撞了要长记性每步执行-1时间惩罚逼着走最短路径距离变化2 × (d_prev - d_now)引导项靠近目标加分时间惩罚和距离引导这两个设计很关键只给终点奖励的话智能体会走很多弯路收敛很慢距离变化引导能加速收敛但也别给太大权重否则智能体会陷入局部最优。奖励函数本质上是在设定什么行为是好的这个标准想清楚这一步后面的训练就顺理成章了。5. 融合策略与切换逻辑5.1 状态机设计从势场到QLearning的平滑过渡融合算法的核心是一个状态机三个状态APF势场控制、QL强化学习接管、RETURN回归势场。状态切换不是随便跳的我设计了明确的触发条件当前状态转移条件目标状态APF合力小于阈值 或 检测到震荡QLQL动作序列执行完毕且合力恢复正常RETURNRETURN飞行方向稳定且距离障碍物足够远APFRETURN状态是我特意加的缓冲。如果不加Q-learning执行完动作序列后马上切回势场可能又掉进同一个局部极小值产生循环切换。加一个缓冲状态先让无人机按Q-learning输出的方向飞一段距离我设为20步确认稳定后再交还控制权这样整个切换过程会平滑很多。5.2 Q-learning介入的条件判断什么时候Q-learning该出手这个判断逻辑我写成了几个具体条件比模糊的陷入局部极小值好操作得多情况一合力大小小于0.5持续5步以上。合力接近零说明引力斥力抵消是经典的极小值特征情况二无人机位置在半径30米的球体内连续震荡超过10个仿真周期判断为陷入震荡情况三前方60米范围内出现障碍物且当前航向与障碍物方向的夹角小于15°判断为即将碰撞这三个条件覆盖了我实际仿真里遇见的绝大多数卡死场景。条件一的判断是基于物理直觉条件二是基于行为观察条件三是基于碰撞预测三者互补。还有一个重要细节Q-learning介入后并不是每次都重新规划一整条路径而是只输出一个固定长度20到30步的逃脱动作序列。序列执行完势场重新接管。这样设计的好处是计算开销小而且Q-learning不需要为每个状态都规划到目标的完整路径任务简单很多。6. 核心仿真代码与MATLAB实现6.1 主程序框架代码下面这段是主程序的核心逻辑去掉了一些可视化和日志代码保留算法主体方便读者看清整个流程。%% 初始化 clear; clc; map init_map(); % 初始化地图 uav_pos [50, 50, 80]; % 无人机初始位置 goal_pos [900, 900, 220]; % 目标位置 Q_table zeros(144, 6); % Q表初始化 flags struct(state, APF, steps_in_current, 0); % 训练Q-learning离线训练阶段 Q_table train_qlearning(Q_table, map, uav_pos, goal_pos); % 主循环 max_steps 5000; trajectory zeros(max_steps, 3); for step 1:max_steps trajectory(step, :) uav_pos; % 检查是否到达目标 if norm(uav_pos - goal_pos) 20 disp(Reached the goal!); break; end % 计算势场信息 [att_force, rep_force, total_force] compute_potential(uav_pos, goal_pos, map); % 判断是否需要Q-learning介入 need_ql check_local_minimum(uav_pos, trajectory, step, total_force); danger_ql check_collision_risk(uav_pos, map); if strcmp(flags.state, APF) (need_ql || danger_ql) flags.state QL; flags.steps_in_current 0; end % 根据状态选择控制策略 if strcmp(flags.state, APF) % 势场控制沿合力方向移动 direction total_force / norm(total_force); uav_pos uav_pos direction * step_size; flags.steps_in_current flags.steps_in_current 1; elseif strcmp(flags.state, QL) % Q-learning控制执行逃脱动作序列 action_seq get_escape_action(Q_table, uav_pos, goal_pos); for a 1:length(action_seq) uav_pos uav_pos action_seq(a).direction * step_size; flags.steps_in_current flags.steps_in_current 1; end flags.state RETURN; else % RETURN状态继续沿当前方向飞确认脱离危险区域 uav_pos uav_pos last_direction * step_size; flags.steps_in_current flags.steps_in_current 1; if flags.steps_in_current 20 flags.state APF; flags.steps_in_current 0; end end % 碰撞检测 if check_collision(uav_pos, map) disp(Collision!); break; end end % 可视化路径 plot3(trajectory(:,1), trajectory(:,2), trajectory(:,3), b-, LineWidth, 1.5);这段代码的主干逻辑很清晰每个仿真周期先算势场再判断状态是否需要切换然后按状态执行对应控制策略。这里的train_qlearning是离线训练阶段先让无人机在仿真环境里试错学出Q表主循环里直接用训练好的Q表做决策。6.2 Q-learning训练代码与超参数训练部分的代码重点展示Q表更新和探索策略这是理解强化学习在航迹规划中具体作用的关键。function Q_table train_qlearning(Q_table, map, start_pos, goal_pos) alpha 0.3; % 学习率 gamma 0.9; % 折扣因子 epsilon 0.3; % 初始探索率 epsilon_min 0.05; decay_rate 0.995; episodes 500; for ep 1:episodes uav_pos start_pos; max_step_per_episode 500; for step 1:max_step_per_episode % 获取当前状态索引 s get_state_index(uav_pos, goal_pos); % epsilon-greedy策略选择动作 if rand() epsilon action_idx randi([1, 6]); else [~, action_idx] max(Q_table(s, :)); end % 执行动作获得新状态和奖励 [next_pos, reward, done] take_action(uav_pos, action_idx, goal_pos, map); s_next get_state_index(next_pos, goal_pos); % Q值更新 Q_table(s, action_idx) Q_table(s, action_idx) ... alpha * (reward gamma * max(Q_table(s_next, :)) - Q_table(s, action_idx)); uav_pos next_pos; if done break; end end % 探索率衰减 epsilon max(epsilon * decay_rate, epsilon_min); end end需要提醒的是take_action函数里要处理飞行约束也就是前文说的最大转弯角限制。如果选择的动作超出了允许的偏转角实际执行时会投影到边界角度上这个投影处理能保证训练出的策略是满足飞行约束的不是纸上谈兵。6.3 状态索引与奖励计算的实现细节状态索引函数把连续位置映射到离散状态编号奖励函数实现上面说的四部分奖惩。这两块代码不复杂但直接影响学习效果单独拿出来说明更容易讲清楚。function s_idx get_state_index(uav_pos, goal_pos) dx goal_pos(1) - uav_pos(1); dy goal_pos(2) - uav_pos(2); dz goal_pos(3) - uav_pos(3); % 方位角离散化12个区间 azimuth atan2(dy, dx); if azimuth 0 azimuth azimuth 2 * pi; end azimuth_bin floor(azimuth / (2 * pi / 12)) 1; azimuth_bin min(max(azimuth_bin, 1), 12); % 俯仰角离散化6个区间 pitch atan2(dz, sqrt(dx^2 dy^2)); pitch_bin floor((pitch pi/6) / (pi/6)) 1; pitch_bin min(max(pitch_bin, 1), 6); % 障碍物邻近标志简单场景取0或1 obstacle_near 0; if check_obstacle_near(uav_pos, 80) obstacle_near 1; end s_idx sub2ind([12, 6, 2], azimuth_bin, pitch_bin, obstacle_near 1); end这里有个容易出错的地方sub2ind的维度顺序要和初始化Q表时的维度顺序一致否则训练和决策时状态索引对不上Q表等于白训练。我在调试时因为这个吃了不少苦头建议大家写的时候多检查维度的映射关系。6.4 可视化输出与仿真结果分析仿真跑完后我习惯把三个结果图一起输出三维航迹图、高度变化曲线、无人机与最近障碍物的距离曲线。三维航迹图用来直观判断路径是否合理有没有绕远路。高度变化曲线用来验证俯仰角约束是否生效尤其是Q-learning介入后的动作序列是否会过于剧烈地改变高度。距离曲线用来验证安全距离约束是否全程满足这条曲线如果低于安全距离线说明碰撞检测有漏洞。我跑了一组典型的对比实验同样地图下纯人工势场和融合算法各跑20次。结果挺有说服力指标纯人工势场融合算法平均路径长度1423米1196米平均耗时8.5秒10.2秒成功率70%95%陷入局部极小值次数6次/20次1次/20次融合算法路径更短说明它确实绕开了局部极小值区域走了一条更近的路耗时稍微多一些是因为Q-learning决策本身有计算开销但这个代价换来了25个百分点的成功率提升非常划算。7. 常见问题与排查技巧实录7.1 无人机在障碍物附近无限震荡——参数不匹配导致这是我的仿真里遇到频率最高的问题。最开始跑融合算法时无人机在某个障碍物前前后后反复试探始终不前进看起来像是势场在起作用但方向一直在变。排查后发现是斥力作用范围ρ₀和无人机步长不匹配。步长8米但ρ₀只有40米无人机进入斥力场区域后只需要5步就能冲到障碍物跟前而斥力是距离越小变化越剧烈在步长离散化的条件下很容易出现上一帧还正常下一帧突然急转弯的情况。解决办法是把ρ₀从40米调到80米给无人机留出足够的反应距离。这个调参规律可以推广ρ₀至少要大于步长的5倍否则势场变化的连续性就无法保证。7.2 Q-table收敛慢或者不收敛训练过程中Q表一直更新缓慢500轮跑完路径还是很乱大概率是奖励函数的问题而不是算法的锅。我调试时试过一版只有终点奖励和碰撞惩罚的奖励函数结果300轮了Q值还抖动得很厉害因为中间过程没有引导信号智能体完全是靠瞎蒙找目标。后来在奖励里加上了距离变化引导项收敛速度明显提升。这个改动背后的逻辑是强化学习奖励越稀疏学习效率越低。如果不想加距离引导也可以考虑给每步加上基于势场值的小增益鼓励智能体往势场更低的方向走这样学出来的是融合策略而不是纯Q-learning策略跟我们的使用场景更契合。7.3 状态离散化粒度太大导致路径锯齿状有段时间我发现融合后的路径虽然能到目标点但走起来像锯齿一样歪歪扭扭不好看也不利于实际飞行。原因出在状态离散化的粒度上。12个方位角区间每个区间30°在切换边界时势场控制的方向和Q-learning输出的方向容易产生突变路径自然不平滑。解决思路有两个一个是加大离散化粒度比如把方位角从12区间加到24区间动作选择的精度会提高代价是Q表大小翻倍144×6变成288×6不过这个规模MATLAB依然能轻松处理另一个是在Q-learning输出的动作序列和势场控制之间加平滑过渡我后来用的是移动平均滤波简单有效但要注意平滑窗不能太长太长会延迟响应我实测5步的窗口效果适中。7.4 MATLAB仿真速度慢的优化建议跑500轮训练加上多组对比实验如果把所有过程都可视化MATLAB会慢得让人崩溃。我这里分享几个实用的提速技巧训练阶段关掉所有绘图和disp日志只保留关键节点输出能提升70%以上的速度用向量化操作替代循环特别是距离计算场景用矩阵运算比逐点算快得多Q表初始化为稀疏矩阵sparse如果状态动作对不是全量访问的话内存和运算量都会降低提前预分配轨迹矩阵避免在循环里动态扩容我整套仿真跑完500轮训练加20次测试在普通笔记本上需要大约25分钟优化前是1个多小时差别非常明显。7.5 从仿真到实飞还有哪些坑要填最后补充一个容易被忽略的点仿真和实飞之间还有很大差距。仿真里无人机是个质点真实无人机有惯性、延迟和动力学特性路径平滑和速度规划是必须做的。融合算法输出的是一条几何路径实际飞行还要通过轨迹跟踪控制器比如纯跟踪算法、L1制导律把它转成控制指令。我通常的做法是把融合算法输出的路径点发给一个轨迹平滑模块用三次样条插值生成连续轨迹再交给底层的PID控制器跟踪。这部分做扎实了整套系统才算真正有落地价值而不是停留在仿真阶段。收尾一点个人体会这个项目做下来我最深的感触是算法融合的关键不在算法本身而在搞清楚每个算法的适用边界。人工势场的优势是实时性和平滑性Q-learning的优势是全局搜索和跳出局部极小值两者结合不是因为流行或者看起来高级而是因为它们确实互补。MATLAB在这个场景里确实是个好用的验证工具语法接近数学表达调试可视化方便但也要警惕它带来的思维惯性——参数在仿真里合适不代表实际环境里合适一定要针对实际场景重新标定。最后再分享一个小技巧如果你准备把这套方案用到更复杂的环境可以尝试把Q-learning换成DQN或PPO这样能处理连续状态空间省掉离散化的麻烦。融合框架不变只需要把Q表决策模块替换成深度网络决策模块架构上的改动很小。这也是当初我把模块化结构做好的红利。