做四旋翼避障规划,最头疼的不是算法选型,而是“模型不准”。同一个控制器,仿真里飞得好好的,换一台稍微重一点、风阻大一点的机架,立刻开始抖、偏、甚至撞上去。我最近花了几周时间把一个基于Learning MPC的四旋翼避障路径规划方案从零跑通,从动力学建模、数据采集、残差学习到仿真验证走了一遍完整流程。这篇文章把整个探索过程、关键原理和排查经验做个梳理,给正在折腾无人机规划或Learning-based控制的朋友做个参考。
Learning MPC,说白了就是“物理模型打底、数据补偏差”的模型预测控制。核心价值在于:MPC本来依赖精确模型做滚动优化,四旋翼这种强非线性、欠驱动、气动参数随载荷和风速变化的对象很难拿到精确模型,那就用数据学一个残差项补上模型误差,让预测更准、控制更稳,避障时也能在约束边界附近大胆一点而不至于失控。适合谁看?如果你已经了解传统MPC的基本套路,又想知道“学习”这块到底怎么接进去、怎么落地调参,那这篇会比较对路。
1. Learning MPC方案怎么设计?先拆清楚要解决什么问题
1.1 为什么选MPC而不是几何避障或采样规划
四旋翼的避障路径规划,主流路线其实不少。A*、RRT*这类采样规划擅长全局路径搜索,但做的是“离线/低频重规划”,环境变了反应不够快;纯几何感知方法(比如DWA、VFH)速度快,但只看眼前几步,很容易在复杂障碍物场里陷入震荡;人工势场法实现极简,孱孱的局部极小值问题也让它在工程里用得很少。
MPC的思路是“滚动优化”:每一步都基于当前状态和预测模型,向前看一个时间窗口,在满足动力学约束、避障约束、输入限幅的前提下求解最优控制序列,但只执行第一步,然后下一时刻重新来。这个“边走边算”的方式天然适合动态避障。更重要的是,MPC把“规划”和“跟踪”统一了——它不输出一个路径点让底层去追,而是直接给出控制输入,中间少一层误差传递。
代价也很明显:每一拍都要解一个优化问题,计算量大、对模型敏感。这就是引入Learning的动因。与其费劲做系统辨识去逼近物理真值,不如用数据把这部分误差“吃掉”。
1.2 Learning从哪里入手?我最终选了残差模型路线
Learning MPC大致可以分成三类做法:
- 学模型残差:保留已知的物理模型,用GP(高斯过程)或神经网络去学习“名义模型预测值和实际状态差异”,然后把这个差异加到MPC的预测里。
- 学代价/策略:用强化学习或模仿学习直接学MPC的代价函数或终端价值函数,改进优化目标。
- 学整个动态模型:端到端用神经网络做状态转移预测,舍弃物理模型。
端到端很诱人,但在实际中很难保证安全——纯数据模型在小概率状态空间里预测可能完全跑飞,四旋翼这种对象炸机成本太高,我打心底不太敢直接上。学代价函数的方案训练曲线长、reward design费劲。最终我采用的是最扎实的残差学习路线:名义模型负责大部分动力学刻画,数据只负责补偿那个“说不清道不明的偏差”,比如桨叶气动阻力、传感器延迟、重心偏移引入的加速度误差等。这个方案有几个实际好处:学习负担小,样本效率高;模型退化时能回退到物理模型;误差项有明确的物理含义,调起来直觉性强。
1.3 系统整体架构:感知、状态估计、MPC、底层控制怎么分工
完整系统的层级是这样的:感知层从深度相机或激光雷达得到点云/深度图,经过障碍物检测模块输出障碍物位置与速度(或直接占据栅格);状态估计层用IMU融合光流或视觉里程计得到位置、速度、姿态;Learning MPC作为位置外环,接收目标点、障碍物信息和当前状态,输出期望加速度(或姿态指令);最后交给底层姿态控制器去执行。
关键点在于:MPC不需要直接输出四个电机转速,那样维度太高还是强耦合,求解特别费劲。只输出期望加速度,再通过四旋翼的力学关系反解出期望滚转、俯仰角和总推力,底层姿态环跟踪角度指令。这种分层设计在工程里非常普遍,也大大降低了MPC动态模型的复杂度——位置环模型可以简化成质量点+加速度约束,省掉很多麻烦的力-力矩耦合项。
2. 关键原理与模型细节:做好这四个点,Learning MPC就立住了一半
2.1 四旋翼简化动力学模型怎么建才够用
做位置外环MPC,我的状态取位置p、速度v、姿态四元数q(或者简化为欧拉角)、角速度ω。实际写代码时经常更进一步简化:既然内环能快速跟踪姿态指令,外环MPC只关心位置和速度,用期望加速度作为输入u。
名义模型写成:
x_{k+1} = f_nominal(x_k, u_k) + d(x_k, u_k)
其中f_nominal就是离散化的二重积分模型,比如 p_{k+1} = p_k + v_k Δt,v_{k+1} = v_k + u_k Δt,再加上重力补偿项。u_k是期望加速度向量。这个模型简单到令人怀疑,但它作为“骨架”完全够用——真正能飞的奥妙在于残差项d(x_k, u_k)会把模型压向真值。
残差项怎么学呢?数据采集时让飞行器执行随机探针动作(随机加了噪声的加速度指令序列),记录当前状态u_k和下一时刻状态x_{k+1},残差就是实际增量减去名义模型算出的增量。把这组(x,u)作为输入、残差作为标签喂给学习模型就行。
2.2 避障约束怎么表达才不容易“卡死”
避障约束在MPC里有两种经典表达。
第一种是硬约束:把障碍物当成一个不等式约束。比如半径为r的圆柱障碍,位置是p_obs,要求飞行器在某一高度层满足 ||p - p_obs|| >= d_safe + r。这个约束是非凸的(可行域是个“挖掉圆形”的区域),直接塞进凸优化问题里会破坏问题的凸性,常见做法是在工作点附近线性化。但线性化避障约束有个隐患:预测轨迹一旦穿越障碍附近,线性化的方向可能让优化器觉得“无路可走”,QP直接跑无解。
第二种是软约束/势场惩罚:把避障项写进代价函数,距离越近惩罚越大,比如 J_avoid = Σ K_obs / (||p_k - p_obs||^2 + ε)。这成一个NLP(非线性规划)问题,求解慢,但不会像硬约束那样轻易无解。实际中我更喜欢把“走廊”思路结合进来:先用上层全局规划(比如RRT*生成粗路径)得到一串航点,在相邻航点之间生成凸的安全走廊,MPC只在当前走廊的凸约束里优化。这样避障约束在局部就是凸的,QP求解又快又稳,还有全局引导避免局部极小。
一个很容易踩的坑:目标点被障碍物完全挡住时,硬约束避障会让MPC在局部捉迷藏,因为它只能看到局部代价。我最后用的是“代价函数里加走廊中心线吸引力”的做法,让飞行器先沿走廊走,而不是直愣愣地望着目标点冲。
2.3 残差学习用GP还是神经网络,取舍依据是什么
我把两种方案都试了,各有战场。
高斯过程回归(GPR)适合低维输入,比如状态+输入总共10维以下。天然自带不确定性度量(预测方差),这个不确定性可以直接喂给MPC做机会约束或者降低置信度。但GPR的推理和训练复杂度随数据点增加快速上升,超过一两千个样本就明显变慢,在线更新更是折腾。神经网络(比如三到五层MLP)在输入维度高、样本量大时表现更好,推理快,适合实时在线更新,但没有天然的不确定性估计。
我的最终方案:离线预训练一个小MLP(两层128节点),在线用最近滑窗数据做增量更新,每隔两三秒微调几下。不确定性的问题通过“限制在线更新力度”解决——防止分布外数据把模型带偏。如果你的输入维度不高、对安全等级要求高,GP依然是好选择。
2.4 代价函数设计与约束松弛:走向实用的关键一步
MPC的代价函数我写成四部分:跟踪代价(当前位置/速度与参考轨迹的差距)、输入代价(控制量大小)、输入变化率代价(平滑性)、避障/走廊代价。调权重时有个老经验:跟踪代价的权重不必开太大,否则飞行器会在参考轨迹附近剧烈修正,姿态变得很神经质。输入变化率代价对平滑性帮助极大,实测发现它还能明显减少底层姿态环的震荡。
约束方面,状态约束(比如最大飞行速度、加速度限幅)建议保留硬约束,这些是物理极限,松弛了会危险。避障约束则务必做成软约束——引入一个松弛变量,在QP里加对应的惩罚项。这样就算预测轨迹离障碍很近导致约束冲突,优化器也不会直接罢工,而是给出一个“尽量避障”的次优解。这里面的哲学是:把不可侵犯的物理限制设为硬约束,把“希望满足”的任务要求做成软约束,系统会皮实很多。
3. 实操过程:从数据采集到MPC部署,一步步跑通
3.1 仿真环境选型:我用的是什么思路
第一步是挑仿真平台。通常有两条路:元器件级仿真(像PX4软件在环那类)或者研究型四旋翼专用框架。对于Learning MPC这种需要快速改动力学模型、频繁读取状态真值的任务,我建议选轻量级的研究型框架,而不是重型全物理仿真——全物理仿真虽然真实,但编译慢、跑得慢,一个回合动辄几十秒,调参效率太低。
选型原则有三条:能否方便地给系统加随机扰动(风场、桨叶效率衰减);能否自定义名义模型和残差模块;状态真值能否无损导出。我最后在Gazebo里搭了一个带简单风场的四旋翼模型,视觉用深度相机给障碍物坐标处理供位置误差。仿真不是越真实越好,关键是能复现你想研究的“模型误差”。
3.2 激励信号怎么设计才能收集到“有用”的数据
数据采集是整个Learning环节的灵魂。如果只是让四旋翼飞几个定点航线,采集到的数据范围太窄,学出来的残差模型只对那几个工作点有效,换个地方预测立刻崩。
我的做法是设计探针航线:在安全空域内,让飞行器随机飞往一系列航点,同时在加速度指令上叠加高斯噪声或啁啾信号(频率由低到高的正弦扫频),保证位置、速度、加速度空间被尽量均匀覆盖。这里有个细节:噪声幅度不能太大,否则数据全是大机动状态,和正常巡航状态差异太大;也不能太小,否则残差完全淹没在噪声里。
采集完成后,每条样本的构造是这样的:输入特征 [状态x_k, 指令u_k],标签 [x_{k+1} - f_nominal(x_k, u_k)]。我在采集时同时记录时间戳,方便后期做数据对齐和滤波。要注意的是名义模型里的Δt必须和实际控制周期严格一致,不然残差里会莫名其妙混进一个“离散步长误差”,后期会很难排查。
3.3 模型训练和在线更新:核心代码长什么样
训练残差模型的流程我用伪代码把思路摆出来:
# 数据准备:从采集日志中构建训练集 samples = [] for log in flight_logs: x_k = log.state_k # 状态: [pos(3), vel(3)] u_k = log.acc_cmd_k # 期望加速度指令 x_next = log.state_k1 # 实际下一时刻状态 # 名义预测 x_nominal = nominal_step(x_k, u_k, dt) # 残差标签 residual = x_next - x_nominal samples.append((x_k, u_k, residual)) # 归一化 scaler = StandardScaler().fit(all_inputs) # 定义MLP残差模型 residual_net = MLP( input_dim=6 + 3, # 状态 + 输入 hidden_dims=[128, 128], output_dim=3, # 位置残差增量(或速度残差) ) # 训练 for epoch in range(200): for batch in dataloader(samples, batch_size=256): loss = mse(residual_net(batch.x), batch.y) sgd_step(loss) # 在线更新:使用最近N条样本做小学习率微调 online_buffer = deque(maxlen=500) # 每收到新样本就入队,每隔3秒触发微调: finetune_step(online_buffer[:256], lr=1e-5)在线更新时有三个坑必须注意:首先数据要用最新状态的滑窗,老的样本在环境变化后可能已经失效;其次学习率要比离线小一个数量级以上,防止单次异常数据把模型打飞;最后更新时不要阻塞MPC求解线程,用异步更新——哪怕模型是滞后几十毫秒的,也远比卡断控制周期致命。
3.4 MPC求解配置与实际参数选择
MPC优化问题一个周期内必须解完。我用的配置如下:
- 预测时域N=20步,控制时域M=6步,步长dt=0.05s,总预测周期1秒;
- 控制频率20Hz(50ms一个周期);
- 优化问题类型:凸二次规划(QP),用OSQP求解,硬约束和代价都是二次型;
- 残差模型推理放在预测模型里面,每一步状态转移都要调一次网络前向。
这个配置下单周期在普通桌面CPU上是3-8ms,留在很大的余量。如果预测时域拉长到30步以上,或者换上非线性避障代价,求解时间可能到20-30ms,那就得小心了——20Hz的控制周期已经快被吃满了。
一个容易被忽视的参数是“控制时域”。如果把控制时域设成和预测时域一样长,优化变量会暴增,求解时间非线性上升。实际把M设小之后,后半段预测自动用维持零输入的方式递推,求解速度快了,控制效果差别很小。
权重初值我一般用这个顺序试起来:先全设0,只留跟踪代价和输入代价;飞稳之后加输入变化率代价,解决抖动问题;最后加走廊惩罚。每次只改一个权重量级,一次改太多就分不清哪个改动起了作用。
3.5 参数调优心得:一组能直接上手的参考值
这里给一组我自己跑通用的起点参数:状态权重Q_p=4(位置)、Q_v=2(速度),输入权重R=0.1,输入变化率惩罚Rdot=0.3,走廊惩罚W_corridor=50。最大速度设定2.5m/s,最大加速度6m/s²,安全距离0.3m(外加障碍半径)。这些值不是最优解,但能让系统在你面前先飞起来,之后再根据实际表现做局部调整。
风场实测中,我的做法是在仿真环境里加一个0.5-2m/s的时变风场,用Learning MPC跟传统MPC对比跟踪误差。结果很有意思:没学残差时,跟踪误差稳态在0.3m左右,加风后偶尔冲到0.6m;开启残差学习后,稳态误差压到0.12m以内,大扰动后恢复时间缩短将近一半。这种对比实验对验证“学习到底有没有用”至关重要,建议你也记录同样的量化指标。
4. 常见问题与排查技巧实录:照着这个清单避坑
4.1 求解器偶发无解或超时
这是最典型的拦路虎。现象是MPC偶发返回求解失败,导致控制输出保持上一拍或者置零,飞行器猛地一抽。深挖原因一般有三类:避障约束线性化方向出错导致可行域为空;约束过紧没有回旋余地;预测时域太长导致数值病态。
排查顺序我一般是这样:先把避障约束改成软约束,看无解频率是否骤降;然后在求解前检查所有约束的松紧程度,安全距离设到物理要求边缘往往是最不可取的;最后缩短预测时域验证数值问题。如果QP本身数值问题严重,检查一下状态向量和权重矩阵是否做了归一化——某些状态的量级差出两三个数量级,QP的KKT系统条件数会非常难看。
4.2 学了残差之后反而更差:先怀疑“分布不匹配”
这是Learning方案最容易被劝退的地方:离线阶段训练MSE很低,一接进MPC闭环就发散。我排查时发现大概率是训练数据分布和在线运行时状态分布不匹配——离线采集时飞得慢、稳、低机动,在线运行却要高速避障大机动,残差模型在训练数据覆盖范围外的预测根本不可靠。
解决思路有两个方向。第一,让训练数据覆盖运转包线:采集时主动把大机动、高速段都飞一遍,宁可多一些看着“危险”的数据也不要只采巡航段。第二,让残差模型对“没见过的地方”保持保守:神经网络可以用MC Dropout或集成方法估计不确定性,把不确定大的预测残差压小,避免盲目自信。保守性优先,然后逐步放开,这是Learning controller落地的通用原则。
4.3 控制量高频抖动,飞行器像打摆子
高频抖动通常是两个原因:要么代价函数里缺输入变化率惩罚,控制器每拍都在“猛打方向”;要么残差模型在相邻采样点之间预测差异很大,给MPC喂了一个“毛刺”的动态。后者在神经网络模型上很常见——网络太复杂、学到的函数不平滑。
我的处理顺序:先加输入变化率惩罚权重,见效最快;再把网络结构精简一点,或者检查输入特征里有没有量纲差异大的维度,及时归一化;最后用控制输出低通滤波兜底,但不要过度滤波,否则相位滞后会让内环不稳定。从根源上,我推荐在残差模型的损失函数里加一项输出平滑正则(对输入求梯度惩罚),效果比事后滤波干净得多。
4.4 在线更新导致MPC推理卡顿
在线更新跑在同一个进程里,会导致控制周期抖动,甚至错过发送指令的硬实时窗口。这个坑我踩完后用两个手段彻底解决了:一是把在线更新放到独立线程,更新期间只动模型参数的一个“影子副本”,更新完毕原子替换;二是给在线更新设置频率上限,每3秒只做一次微调,因为从系统辨识角度看,环境变化通常没有快到需要每个控制周期都更新。
另外必须保证数据采样的同步性。我遇到过数据时间戳对不齐导致残差标签里加了额外的时间延迟,学出来的是“带延迟的假模型”。排查这个问题的办法很粗暴:在仿真里关掉学习,手动加一个已知的恒定残差(比如反向加速度偏置),看MPC预测模型能不能准确复现。能复现,说明数据链路没问题;复现不出来,就去检查时间戳对齐和误差函数。
4.5 避障路径绕远路甚至跳变
单纯靠当前帧障碍物信息做MPC,很容易在多个障碍物密集的时候绕远路,因为MPC只看未来1秒,根本没有全局观念。跳变则通常是因为障碍物检测频率低,障碍物坐标突然更新导致避障约束位置突变。
这两个问题我都用混合规划架构解决:上层跑一个频率较低的全局规划器(比如每秒5次,RRT*在占据栅格图上找粗路径),把粗路径裁成若干航点,航点间生成凸安全走廊;下层MPC每拍在走廊约束内做精细轨迹优化。实测下来既能保证全局方向正确,又能利用MPC的预测能力做顺滑的局部避障。
5. 后续扩展:还有哪些方向值得继续做
残差学习框架跑通后,有很多现成的扩展方向。一个是把GP或NN的不确定性估计直接引入MPC代价,变成“风险感知的避障”——预测方差大的地方主动放慢速度、加大安全距离,这在人群密集或风况复杂的场景价值巨大。另一个是把避障走廊的生成也学习化,用图神经网络从点云直接预测安全走廊,省掉显式障碍物检测这一步。还有就是把残差模型从标量状态推广到“多模态混合”,比如通过在线聚类识别不同负载/不同风速环境,每个环境一个残差模型,切换时用置信度加权融合。
从我自己的体会来说,Learning MPC最舒服的地方在于它把“基于模型的安全保障”和“数据驱动的前瞻性”结合在了同一个框架里。你不需要完全信任黑盒模型,也不需要非得手搓一套精确物理模型出来。物理模型兜底、数据模型精调,这套思路在无人机之外,四足机器人、机械臂、自动驾驶横向控制等场景也能平移到。
最后再分享一个实操小技巧:无论怎么调参,每次改动前都建议保存一份场景配置文件,飞行数据也全程打标签存档。你会发现Learning这种带随机性的系统,跑十次可能有八次效果好两次翻车,没有完整留档,排查问题会变成大海捞针。版本化你的数据、模型和参数,是这个方向上性价比最高的投资。