基于DQN的无人艇避障控制:MATLAB实现与避坑指南
简介:这份资源面向无人艇(USV)控制、深度强化学习入门与课程设计人群,聚焦基于DQN的避障控制问题。包内提供无人艇与巡逻艇的运动学建模脚本、DQN训练主流程、经验回放与目标Q值计算、奖励函数设计、ε-greedy策略及模糊集合等模块,可用于复现避障仿真实验、理解强化学习在欠驱动水面航行器上的落地方式。资源共12个文件,以11个m脚本和1个mat数据文件为主,m文件承担建模、训练与仿真逻辑,mat文件保存训练过程或环境数据,压缩包约24.47MB,结构紧凑便于按模块查阅。目前已有6280人学习下载,适合希望从零搭建DQN避障仿真、对照调试奖励与收敛问题的读者参考,也可作为相关课题的代码基线。
1. 从一条翻船视频说起:DQN 到底能不能管住无人艇
刷到过一条无人艇湖试视频:船在开阔水面跑得好好的,一进桥墩附近的回流区就开始画龙,左满舵右满舵来回摆,最后擦着桥墩蹭过去。评论区一堆人说"PID 没调好",但做过水面无人艇(USV)的人心里清楚,这大概率不是 PID 的问题——是环境扰动的时变性和控制策略的固定性对不上。风、浪、流、浅水效应、岸壁效应,这些在传统控制里要么被当成有界扰动压住,要么被前馈补偿掉,可一旦工况切换,参数就得重调。这正是深度强化学习 DQN 切入无人艇避障控制的现实动机:让策略自己从交互里学,而不是靠人把每种工况都标定一遍。
这篇笔记讲的是基于深度强化学习 DQN 的无人艇避障控制,落地平台是 MATLAB。适合两类人:一类是手里有无人艇模型或想搭一个、准备用强化学习做避障的在校生和工程师;另一类是已经跑过 PID、A*、人工势场,想看看 DQN 到底能不能接上、值不值得投入的人。我会把状态设计、动作离散化、奖励函数、训练循环、MATLAB 里的实现细节和踩过的坑都摊开讲,能照着复现,也能看清边界。
2. 为什么是 DQN 而不是 PID 或势场法:选型理由与问题建模
2.1 避障控制的三条技术路线对比
在动手写代码之前,先把选型这件事说清楚,否则后面调参全是玄学。无人艇避障控制常见三条路线:传统控制(PID + 制导律)、几何/势场法(人工势场、A*、RRT)、学习类方法(强化学习、模仿学习)。它们不是替代关系,而是适用边界不同。
| 路线 | 核心假设 | 优势 | 明显短板 |
|---|---|---|---|
| PID + 制导 | 扰动有界、模型近似线性 | 实现快、可解释、实时性好 | 工况切换要重调参,动态障碍难处理 |
| 人工势场 / A* | 环境已知或可感知 | 路径清晰、易调试 | 局部极小值、动态障碍抖动、势场参数敏感 |
| DQN 类强化学习 | 可交互采样、奖励可设计 | 能学非线性策略、适应动态障碍 | 样本效率低、训练不稳、奖励难设计 |
DQN 的价值在于它把"避障"从"设计规则"变成"设计奖励 + 让策略自己找路"。代价是训练成本高、收敛看运气。所以我的判断是:静态已知环境用 A* 就够了,别上 DQN;只有动态障碍多、工况变化频繁、又拿不到精确模型时,DQN 才值得投入。
2.2 把避障问题写成 MDP:状态、动作、奖励三件套
DQN 求解的是马尔可夫决策过程,落地第一步是把无人艇避障写成 MDP 四元组(S, A, P, R)。这一步设计错了,后面训练再久也白搭。
状态 S 的设计要包含三类信息:自身运动状态、障碍相对位置、任务目标方向。我一般用这样的状态向量:
% 状态向量构造:state = [v, omega, d_obs, theta_obs, d_goal, theta_goal] % v 当前航速 (m/s) % omega 当前艏摇角速度 (rad/s) % d_obs 最近障碍距离 (m),归一化到 [0,1] % theta_obs 障碍相对方位角 (rad),归一化到 [-1,1] % d_goal 目标点距离 (m),归一化 % theta_goal 目标相对方位角 (rad),归一化 function s = buildState(usv, obs, goal) s = zeros(1,6); s(1) = usv.v / usv.v_max; s(2) = usv.omega / usv.omega_max; [d_o, th_o] = nearestObstacle(usv, obs); s(3) = min(d_o / usv.sensor_range, 1); s(4) = th_o / pi; d_g = norm(goal - usv.pos); th_g = wrapToPi(atan2(goal(2)-usv.pos(2), goal(1)-usv.pos(1)) - usv.psi); s(5) = min(d_g / usv.map_diag, 1); s(6) = th_g / pi; end逻辑说明:状态里同时放"障碍"和"目标"两组极坐标信息,是为了让网络能同时权衡"躲"和"去"。参数上,所有量都做了归一化,因为 DQN 的输入层对量纲敏感,不归一化会导致某些维度梯度主导,训练直接跑偏。sensor_range是感知半径,map_diag是地图对角线长度,这两个参数决定了归一化尺度,换地图要重设。
动作 A 用离散化,这是 DQN 的硬约束——它只能处理离散动作空间。常见做法是把连续舵角离散成 5 档或 7 档:
% 动作空间:5 档舵角 + 固定推力 action_set = [-30, -15, 0, 15, 30] * pi/180; % 舵角 (rad) % 索引 1~5 对应上述舵角,推力保持恒定或按航速闭环奖励 R 是最容易翻车的地方。稀疏奖励(只有到达目标才给正奖励)会导致训练初期几乎学不到东西;奖励太密又会让策略钻空子。我一般用分段塑形奖励:
function r = computeReward(usv, obs, goal, prev_dist) d_goal = norm(goal - usv.pos); [d_obs, ~] = nearestObstacle(usv, obs); r = 0; % 1) 靠近目标给正奖励(势能塑形) r = r + 1.0 * (prev_dist - d_goal); % 2) 碰撞重罚 if d_obs < usv.safe_radius r = r - 100; end % 3) 离障碍太近给连续惩罚,鼓励保持安全距离 if d_obs < usv.warn_radius r = r - 0.5 * (usv.warn_radius - d_obs); end % 4) 到达目标 if d_goal < usv.goal_radius r = r + 200; end % 5) 时间惩罚,逼它别磨蹭 r = r - 0.01; end参数说明:safe_radius是碰撞判定半径,warn_radius是预警半径(一般取 safe_radius 的 2~3 倍),goal_radius是到达判定半径。势能塑形项(prev_dist - d_goal)是关键,它把稀疏的"到达"信号变成每步都有反馈的稠密信号,是 DQN 能训起来的前提。但系数不能太大,否则策略会为了靠近目标而贴着障碍走,碰撞惩罚压不住。
2.3 网络结构与超参数的初始设定
MATLAB 里可以用 Deep Learning Toolbox 或 Reinforcement Learning Toolbox 搭 DQN。网络不用太深,两层全连接足够:
% DQN 网络:输入 6 维状态,输出 5 个动作的 Q 值 layers = [ featureInputLayer(6, 'Normalization', 'none') fullyConnectedLayer(64) reluLayer fullyConnectedLayer(64) reluLayer fullyConnectedLayer(5) % 输出维度 = 动作数 ];超参数初始值我一般这样设:经验回放池 10000,批大小 64,折扣因子 0.95,学习率 1e-3,目标网络更新间隔 200 步,ε 从 1.0 线性衰减到 0.05。折扣因子取 0.95 而不是 0.99,是因为避障任务里远期奖励不确定性大,折扣太接近 1 会让 Q 值发散。这些值不是最优,是能跑起来的起点,后面按训练曲线调。
3. 在 MATLAB 里把 DQN 避障跑起来:环境、训练与验证
3.1 搭一个最小可用的无人艇运动学环境
训练之前得有个环境。MATLAB 里最省事的做法是写一个继承自rl.env.MATLABEnvironment的类,或者干脆用函数式循环。我倾向后者,调试直观。无人艇用三自由度运动学模型(简化版,忽略横摇纵摇):
function [next_state, reward, done] = stepEnv(state, action_idx, obs, goal, usv) % 动作映射 delta = usv.action_set(action_idx); % 三自由度运动学更新(简化) usv.psi = usv.psi + usv.v * tan(delta) / usv.L * usv.dt; usv.pos = usv.pos + usv.v * [cos(usv.psi), sin(usv.psi)] * usv.dt; % 边界约束 usv.pos = max(min(usv.pos, usv.map_max), usv.map_min); % 计算奖励与终止 prev_dist = norm(goal - state(1:2)); % 注意:这里用上一状态位置 [d_obs, ~] = nearestObstacle(usv, obs); done = false; if d_obs < usv.safe_radius done = true; % 碰撞终止 end if norm(goal - usv.pos) < usv.goal_radius done = true; % 到达终止 end next_state = buildState(usv, obs, goal); reward = computeReward(usv, obs, goal, prev_dist); end逻辑说明:usv.dt是仿真步长,一般取 0.1~0.2 s,太小训练慢,太大运动学失真。usv.L是艇长,用于舵角到艏摇的转换。这里用的是简化运动学,真实艇还要考虑惯性、阻尼、推力响应,但作为 DQN 训练环境,简化模型足够验证策略逻辑,等策略收敛再上高保真模型做迁移。
3.2 训练主循环:经验回放与目标网络怎么接
DQN 的两个核心机制是经验回放和目标网络。MATLAB 里手写训练循环比用工具箱更可控:
% 初始化 replay_buffer = struct('s', {}, 'a', {}, 'r', {}, 's_next', {}, 'done', {}); buffer_cap = 10000; target_net = dqn_net; % 目标网络,初始与主网络相同 gamma = 0.95; epsilon = 1.0; eps_decay = 0.995; eps_min = 0.05; for episode = 1:2000 state = resetEnv(); total_r = 0; for t = 1:500 % ε-贪婪选动作 if rand < epsilon a = randi(5); else q = predict(dqn_net, state); [~, a] = max(q); end [next_state, r, done] = stepEnv(state, a, obs, goal, usv); % 存入回放池 replay_buffer(end+1) = struct('s',state,'a',a,'r',r, ... 's_next',next_state,'done',done); if numel(replay_buffer) > buffer_cap replay_buffer(1) = []; end % 采样训练 if numel(replay_buffer) >= 64 batch = replay_buffer(randperm(numel(replay_buffer), 64)); % 构造目标 Q 值 for i = 1:64 if batch(i).done y(i) = batch(i).r; else q_next = predict(target_net, batch(i).s_next); y(i) = batch(i).r + gamma * max(q_next); end end % 反向传播更新主网络(此处用 trainNetwork 或自定义梯度) % ... end state = next_state; total_r = total_r + r; if done, break; end end % ε 衰减 epsilon = max(epsilon * eps_decay, eps_min); % 目标网络软更新或硬更新 if mod(episode, 20) == 0 target_net = dqn_net; % 硬更新 end fprintf('Episode %d, Reward %.2f, Eps %.3f\n', episode, total_r, epsilon); end逻辑说明:y(i)是 DQN 的目标值,done为真时没有后续状态,目标就是即时奖励,这是终止状态处理的关键,漏了这一步 Q 值会系统性偏高。目标网络每 20 个 episode 硬更新一次,也可以用软更新(target = tau*main + (1-tau)*target),软更新更稳但慢。epsilon衰减到 0.05 后保持,保证训练后期还有探索。
3.3 训练收敛的判据与验证方法
怎么知道训练好了?别只看 reward 曲线。我一般看三个指标:平均回报的滑动平均是否稳定上升、碰撞率是否降到 5% 以下、到达率是否稳定在 90% 以上。验证时固定随机种子跑 100 个 episode,统计这三个数。如果 reward 涨但碰撞率不降,说明奖励设计有问题,策略在钻空子。
% 验证:固定策略,跑 100 个 episode collisions = 0; arrivals = 0; for ep = 1:100 state = resetEnv(); for t = 1:500 q = predict(dqn_net, state); [~, a] = max(q); % 验证时不用 ε-贪婪 [state, ~, done] = stepEnv(state, a, obs, goal, usv); if done if norm(goal - usv.pos) < usv.goal_radius arrivals = arrivals + 1; else collisions = collisions + 1; end break; end end end fprintf('到达率 %.1f%%, 碰撞率 %.1f%%\n', arrivals, collisions);4. 避坑与排查:DQN 避障训练里最常见的 5 个翻车现场
4.1 现象:reward 一直涨,但艇在原地打转
原因:奖励里的时间惩罚太小,或者势能塑形项系数过大,策略发现"原地小幅摆动"也能持续获得靠近目标的微小正奖励,于是学会了磨洋工。这是典型的奖励钻空子。
解决:加大时间惩罚(从 -0.01 提到 -0.05),同时给势能塑形项加一个"只有实际位移超过阈值才给奖励"的门控。我一般加if norm(usv.pos - prev_pos) > 0.01才计入塑形奖励。
4.2 现象:训练前期 Q 值爆炸,输出 NaN
原因:学习率太大,或者奖励量级没归一化。碰撞惩罚 -100 和势能奖励 ±1 差两个数量级,梯度被大奖励主导,Q 值发散。
解决:把奖励整体缩放到 [-1, 1] 区间,碰撞惩罚改成 -1,到达 +1,势能塑形按比例缩到 ±0.1。学习率从 1e-3 降到 1e-4 试。血泪经验:奖励量级不统一,是 DQN 训练不稳的头号原因。
4.3 现象:训练几百 episode 后策略突然退化,碰撞率反弹
原因:经验回放池里早期的高奖励样本被反复采样,网络过拟合到旧策略,加上目标网络更新太频繁,Q 值震荡。
解决:目标网络更新间隔从 20 拉到 50,或者改用软更新 tau=0.01。回放池容量加大到 50000,让样本分布更均匀。另外检查 ε 是不是衰减太快,探索不足会导致策略早熟。
4.4 现象:仿真里跑得好,换一张地图就撞
原因:状态归一化用了固定尺度(比如 map_diag),换地图后归一化尺度不匹配,网络看到的输入分布变了。这是过拟合到训练地图的典型表现。
解决:归一化尺度改成动态计算,或者用相对量(障碍距离除以感知半径,而不是除以地图尺寸)。更彻底的做法是训练时随机化地图尺寸和障碍布局,做域随机化。
4.5 现象:训练慢到怀疑人生,一个 episode 要几十秒
原因:MATLAB 里用predict逐样本前向传播,没有批处理;或者环境 step 里做了大量冗余计算(比如每步都重算所有障碍距离)。
解决:把状态批量构造,用predict的批处理接口;障碍距离用 KD-tree 或预计算的空间索引,别每步遍历。实测把逐样本改成批处理后,训练速度能快 3~5 倍。另外dt别设太小,0.1 s 够用。
5. 让 DQN 避障真正可用的几个进阶技巧
训练能收敛只是及格线,要让这套东西在真实无人艇上有意义,还得处理几个工程问题。第一个是动作空间的设计。纯离散舵角在低速时控制粒度太粗,我一般用"离散舵角 + 连续推力"的混合动作空间,或者用 DQN 输出舵角档位、再用一个 PID 内环跟踪推力。这样既保留 DQN 的离散优势,又不牺牲低速操控性。
第二个是奖励的课程学习。一上来就在密集障碍里训练,策略几乎不可能探索到目标。我的做法是分三阶段:第一阶段空旷水域,只学"朝目标走";第二阶段加少量静态障碍,学"绕开";第三阶段加动态障碍和随机流场,学"适应"。每阶段训练到到达率稳定再进下一阶段。这个技巧能把训练成功率从三成提到八成以上。
第三个是仿真到实艇的迁移。仿真里的运动学模型和真实艇差得远,直接迁移大概率翻车。常见做法是:在仿真里加域随机化(随机化艇的阻尼系数、推力响应时间、传感器噪声),让策略见过足够多的"变体",再上真艇时用少量在线微调。MATLAB 里可以把这些随机参数做成环境 reset 时采样的变量。
| 进阶技巧 | 解决的问题 | 关键参数 |
|---|---|---|
| 混合动作空间 | 低速控制粒度粗 | 舵角档位数、推力 PID 增益 |
| 课程学习 | 密集障碍下探索失败 | 阶段数、每阶段收敛阈值 |
| 域随机化 | 仿真到实艇迁移差 | 阻尼/推力/噪声的随机范围 |
最后一个技巧是验证别只看成功率。我习惯在验证时记录"最小障碍距离"的分布,而不只是碰撞与否。因为一个策略可能到达率很高,但每次都是擦着障碍过去,这种策略上真艇就是定时炸弹。最小障碍距离的中位数应该稳定在安全半径的 1.5 倍以上,才算真正可用。
说个我自己的教训:早期做这套东西时,我盯着 reward 曲线调了两周,越调越乱,后来才发现问题根本不在超参数,而在状态里漏了"障碍相对方位角"这一维,网络根本分不清障碍在左还是在右。从那以后我养成的习惯是:训练不收敛,先回去检查状态和奖励的设计,别急着调学习率。状态和奖励对了,DQN 自己会找到路;设计错了,调参就是给自己找安慰。希望帮到你。
本文还有配套的精品资源,点击获取