第 7 章 腿足全身 MPPI 与接触丰富操作¶
前六章我们把采样式 MPC 的"内核"打磨完整了:第 1 章给出路径积分的数学地基,第 2 章把它变成 GPU 上能实时跑的工程系统,第 3–4 章用六大变体和统一框架把"怎么采样、怎么加权、怎么更新"这几个旋钮拧了个遍,第 5 章用扩散退火突破了高维非凸,第 6 章把动力学换成学习的世界模型。但到目前为止,我们验证内核的舞台几乎都是低维的玩具——点质量、CartPole、单摆。本章把这套内核推上它**最有戏剧性的真实战场**:腿足机器人的全身控制,以及与之同源的接触丰富操作(contact-rich manipulation)。
为什么这是"最有戏剧性"的战场?因为这正是梯度式 MPC(你的前置课程:OCS2/Crocoddyl 的 iLQR/DDP)最吃力、而采样式 MPC 最能扬长避短的地方。一条腿落地的瞬间、一只手碰到箱子的瞬间,动力学会发生**不可微、甚至不连续**的跳变。梯度方法要对动力学求导,在这种跳变面前要么失效要么乱跳;而采样方法**根本不求导**,只把轨迹丢进仿真器看代价——接触的跳变对它毫无影响。这一章要回答的核心问题只有一个,但要彻底讲透:为什么"不可微的接触"对梯度法是噩梦、对采样法却几乎是免费的,以及如何把这个优势真正兑现到几十自由度的真实机器人上。
本章不重新发明算法。STORM 用的是你第 2–3 章学的 MPPI、MJPC 的 Predictive Sampling 你在第 4 章 §4.4 已经吃透、DIAL-MPC 的扩散退火第 5 章 §5.3 已经讲完。本章做的是**把这些内核搬到接触丰富的高维系统上**,并把搬运过程中冒出来的真问题——维度灾难怎么治、动作空间选力矩还是位置、和你已有的 OCS2 全身 MPC 怎么配合——一个个讲清楚。
📋 前置自测¶
答不出 ≥ 2 题,回对应章节补完再来:
- vanilla MPPI 一个控制周期里要对 \(K\) 条 rollout 做什么操作才得到要执行的控制?权重公式长什么样?(→ 第 2 章 §2.1)
- 为什么说采样式 MPC "不需要可微动力学"?它对动力学模型的唯一要求是什么?(→ 第 2 章 §2.4、第 1 章 §1.3)
- 第 5 章的扩散退火(DIAL-MPC)是靠改进"单步提议分布"还是靠"多步复合"来征服高维的?(→ 第 5 章 §5.1、§5.3)
- 第 4 章的 Predictive Sampling(MJPC 的默认 planner)和 MPPI 在权重函数上有什么区别?它为什么"意外地有效"?(→ 第 4 章 §4.4)
- 你的前置课程里,梯度式全身 MPC(如 OCS2 WBC)是怎么处理接触的?它需要预先知道什么?(→ 腿足 MPC 基础 / OCS2)
参考答案要点(自评用,不必逐字对照):
(1) 对每条 rollout 求其总代价 \(J_k\),按 \(w_k \propto \exp(-\frac{1}{\lambda}(J_k - \rho))\) 算权重(\(\rho\) 常取 \(\min_k J_k\) 做数值稳定),归一化后对扰动 \(\varepsilon_k\) 加权平均更新名义控制序列 \(U \leftarrow U + \sum_k w_k \varepsilon_k\),执行更新后序列的第一格 \(u_0\),下周期滚动重来。核心是"代价越低权重越大"的指数加权 + 滚动时域。
(2) 因为 MPPI 的更新公式里只出现代价的**求值**(把控制丢进动力学跑出轨迹、算代价),从不出现代价或动力学的**导数**。它对动力学模型的唯一要求是"能前向推进"(给定状态和控制,能算出下一个状态并打分)——是不是可微、是不是解析的,都不要求。这正是本章 §7.1 的地基。
(3) 靠**多步复合**(退火的多个尺度从粗到精串起来),而非把单步的提议分布做得多聪明。DIAL-MPC 用 bi-level 退火,外层在轨迹级、内层在采样级逐级缩小方差,每一级在前一级基础上精修——是"由粗到精的多步"征服高维,不是"单步一招制胜"。
(4) Predictive Sampling 不做指数加权,而是**贪心地只保留当前最优样本**(相当于 CEM 取精英数 = 1 的极端)。它"意外有效"是因为:配上好的仿真器(MuJoCo)和合理的代价,"采一批、谁最低用谁"这个最朴素的更新就足以让复杂行为涌现——说明很多任务的瓶颈不在优化器的精巧,而在仿真和代价(本章 §7.3 的核心论点)。
(5) 梯度式全身 MPC 靠**预设接触序列**(gait scheduler 规定何时哪只脚着地)来处理接触:在每个固定的接触模式内,动力学是光滑可微的,梯度法才能工作。它需要预先知道**接触序列/步态**——而这正是采样法不需要、能让其涌现的东西(本章 §7.1 反复对照的点)。
本章目标¶
学完本章后,你应该能够:
- **解释清楚**接触为什么让梯度式 MPC 失效(不可微/不连续 + 维度爆炸),以及采样式 MPC 为什么能绕开——并能向别人讲明白这不是"魔法"而是"把求导换成了仿真打分"。
- 读懂并复现 STORM 的关节空间张量化 MPPI 设计:批量 rollout、多目标代价缓冲(cost buffer)、关节空间 vs 任务空间的取舍。
- 理解 MJPC 在 humanoid standup / 四足行走上"好仿真器 + 极简采样器"的范式,会写 MuJoCo 任务的 residual 代价,会在 GUI 里对比 Predictive Sampling 与 iLQG。
- **掌握**全身采样 MPC 治维度灾难的三把钥匙——样条节点降维、扩散退火(复用第 5 章)、warm-start(复用第 2 章)——并知道它们各自治的是哪一种"灾难"。
- **做出**动作空间的工程判断:什么时候直接采力矩(如 DIAL-MPC)、什么时候采关节位置目标走 PD(如 CMU Whole-Body MPPI),以及这个选择如何决定 sim-to-real 的难易。
- **画出**采样式全身控制与梯度式 OCS2 WBC 的互补边界,理解"OCS2 稳态 + MPPI 敏捷 + RL 微调"这类混合架构为什么是当下趋势。
- 给你的累积项目 Mini-MPPI 接上一个接触任务,并新增"动作空间"维度,亲手看到 MPPI 在不枚举接触模式的情况下完成推箱子。
本章知识导航¶
本章围绕一条主线展开:接触 → 采样的天然优势 → 在高维真实机器人上兑现这个优势。七节的逻辑关系如下:
§7.1 为什么接触是梯度法的克星、采样的主场(全章地基)
│ 确立"采样不怕接触"这个核心论点
▼
§7.2 STORM:在机械臂上把 MPPI 张量化、多目标化(接触操作的入门范例)
│ 低维一点的接触场景,先把工程骨架建起来
▼
§7.3 MJPC 腿足范式:好仿真器 + 极简采样器就能让人形站起来
│ 证明"简单采样器 + 好仿真"在腿足上已足够
▼
§7.4 全身采样 MPC 的维度灾难与三把钥匙(核心难点)
│ 几十自由度怎么办——降维 + 退火 + warm-start
▼
§7.5 动作空间之争:力矩级 vs 位置目标(PD)(sim-to-real 的关键决策)
│ 同样是全身 MPPI,DIAL-MPC 采力矩、WBMPPI 采位置,为什么
▼
§7.6 Whole-Body MPPI vs OCS2 WBC:互补、混合与选型
│ 和你已有的梯度式全身 MPC 怎么配合
▼
§7.7 实战:给统一规划器接上接触任务与动作空间维度(累积项目)
读法建议:§7.1 是全章论点的地基,务必读透——后面所有内容都是它的展开和兑现。§7.4、§7.5 是本章最有研究价值、也最容易踩坑的两节(⭐⭐⭐⭐ / ⭐⭐⭐),值得慢读。如果你只想拿走一个洞察,那就是 §7.1 的"求导换成仿真打分"和 §7.5 的"位置目标比力矩更 sim-to-real 友好"。
前置知识桥接¶
本章重度复用前六章,先把要用到的核心点激活一遍(避免你翻回去):
- 回顾第 2 章 §2.1:vanilla MPPI 每个控制周期采 \(K\) 条控制扰动序列 \(\{U+\varepsilon_k\}\),前向 rollout 得每条代价 \(J_k\),再按 \(w_k=\frac{1}{\eta}\exp(-\frac{1}{\lambda}(J_k-\rho))\) 加权平均扰动更新均值序列 \(U\leftarrow U+\sum_k w_k\varepsilon_k\),只执行第一格 \(u_0\),下周期 warm-start 平移重来。整个过程只需要前向 rollout 算代价,不求任何导数——这是本章一切的出发点。
- 回顾第 2 章 §2.4:采样式 MPC 对动力学的唯一要求是"能前向 rollout 出一条轨迹并算代价",模型可以是黑箱仿真器、神经网络、甚至带不连续 indicator 的代价。梯度式 MPC(iLQR/DDP)则要求 \(\partial f/\partial x\)、\(\partial f/\partial u\) 存在且良态。
- 回顾第 4 章 §4.4:Predictive Sampling 是 MJPC 的默认 planner,把 MPPI 的指数加权替换成"只保留当前最优样本"的极简更新(它甚至比精英平均还粗暴),却在很多任务上意外地够用——本章 §7.3 会看到它把人形机器人扶起来。
- 回顾第 5 章 §5.3:DIAL-MPC 用"多级退火 MPPI"在全阶力矩空间上工作——高温大方差先看全局、低温小方差再精修,配合 warm-start 和 bi-level 退火,在真实 Go2 上以 50 Hz 完成带载跳跃。本章把它放进"全身采样 MPC"的大家庭里和别的方法对照。
- 回顾你的前置课程(OCS2 WBC):梯度式全身 MPC 为了处理接触,通常需要一个 gait scheduler(步态调度器) 预先规定接触序列(先迈哪只脚、何时落地),在每个接触模式内用解析动力学(Pinocchio RNEA/ABA)求导、解 QP。本章要讲的采样式方法**不需要这个预设序列**。
如果跳过本章会怎样¶
举两个具体场景说明跳过本章的代价:
场景一:你想让四足机器人爬上一个和它差不多高的台子。 你打开 OCS2,发现要先给 gait scheduler 编排一套"前腿搭上、重心前移、后腿蹬起"的接触序列,每换一个台子高度就要重编一次,调到崩溃。学过本章你会知道:用全身 MPPI(§7.4–§7.6),把"到达台子顶部"写进代价,接触序列让它自己涌现——CMU 的 Whole-Body MPPI 正是这么让 Go1 爬上自身高度的箱子的。
场景二:你把仿真里调好的全身力矩 MPPI 部署到真机,机器人一上电就剧烈抖动甚至摔倒。 你百思不得其解——仿真里明明很稳。学过本章 §7.5 你会立刻怀疑动作空间:直接采力矩对模型误差极其敏感,真机的电机动力学、延迟、摩擦和仿真对不上,力矩级控制就崩了;换成采关节位置目标走底层 PD,鲁棒性会高一个量级。这个判断能帮你少走几周弯路。
预计阅读时间¶
| 模式 | 时间 | 适合人群 |
|---|---|---|
| 精读(含动手) | 14–18 小时 | 想真正掌握全身采样 MPC、未来要做腿足/操作的人 |
| 速读(跳代码与推导细节) | 5–7 小时 | 想建立全局图景、知道选型边界的人 |
| 速查(只读小结 + 速查表 + §7.5/§7.6) | 1.5 小时 | 已有腿足 MPC 经验、只想补采样视角的人 |
本章配套两个可在纯 Python/NumPy 里跑的 demo(§7.1 的接触地形对比、§7.7 的推箱子),不依赖 GPU 或 MuJoCo 也能看到核心现象;想完整体验全身控制,按 §7.3 / 延伸阅读跑 MJPC GUI 与 DIAL-MPC 官方仓库。
本章学习路径建议¶
如果时间紧张,按这个优先级:先读 §7.1(地基,必读)→ §7.5(动作空间,最实用的工程判断)→ §7.6(和你已有的 OCS2 怎么配)。这三节读完,你已经能在工作中做出"用不用采样式全身 MPC、用力矩还是位置、和梯度法怎么分工"的关键决策。有余力再补 §7.2/§7.3(两个具体系统的范例)和 §7.4(维度灾难的治理细节)。§7.7 的累积项目建议无论如何动手跑一遍——亲眼看到 MPPI 不枚举接触模式就把箱子推到位,胜过读十遍"采样不怕接触"。
科研发展脉络¶
| 年份 | 工作 | Venue | 核心贡献 | 在本章的位置 |
|---|---|---|---|---|
| 2021 | STORM(Bhardwaj 等,NVIDIA/UW) | CoRL 2021(Oral) | 关节空间张量化 MPPI,多目标代价缓冲,机械臂实时反应式控制(每步 <0.02 s) | §7.2 |
| 2022 | Predictive Sampling / MJPC(Howell 等,Stanford/DeepMind) | arXiv 2212.00541 | 开源交互式采样 MPC 框架,极简采样器在 humanoid/四足上意外地强(多核 CPU 并行) | §7.3(算法见第 4 章 §4.4) |
| 2024–25 | Whole-Body MPPI(Alvarez-Padilla 等,CMU Manchester 组) | ICRA 2025 | 首个在真实四足(Go1)上部署的全身采样 MPC——采关节位置目标,样条节点降维 | §7.4、§7.5、§7.6 |
| 2025 | DIAL-MPC(Xue 等,CMU LeCAR) | ICRA 2025(最佳论文入围) | 扩散退火全身**力矩**级 MPPI,真实 Go2 带载跳跃,免训练 | 第 5 章 §5.3,本章 §7.5 对照 |
一句话脉络:STORM(2021)证明 GPU 让接触操作的 MPPI 实时可行 → MJPC(2022)证明"好仿真 + 极简采样器"在腿足上已够用 → Whole-Body MPPI 与 DIAL-MPC(2025)把它推上真实腿足全身控制,并在"采位置 vs 采力矩"上分出两条路线。 注意这条线和第 3 章 MPPI 复兴、第 5 章扩散退火讲的是同一个底层故事——采样式方法的崛起是被 GPU/高效并行仿真催生的,只是这一章把舞台换成了维度最高、接触最复杂的腿足全身。
把这条脉络讲得再细一点,你会看到一个清晰的"问题接力"——每一代工作都站在前一代的肩上、解决前一代留下的下一个瓶颈。STORM 解决的是"实时性":它证明了借助 GPU 张量化,接触丰富的 MPPI 可以快到几十赫兹、能驱动真实机械臂做反应式操作——回答了"采样式接触控制能不能实时"。MJPC 解决的是"复杂度认知":在实时已不成问题后,它进一步证明采样器本身可以极简(Predictive Sampling),复杂行为靠好仿真 + 合理代价涌现,把人们的注意力从"设计精巧优化器"扭转到"仿真和代价才是瓶颈"——回答了"采样式控制到底难在哪"。Whole-Body MPPI 解决的是"上真机的高维全身":它把采样 MPC 从机械臂/仿真推到真实四足的全身控制,用三把钥匙治维度灾难、用位置目标保 sim-to-real,第一次让"真机上的全身采样控制"成为现实——回答了"高维全身能不能真机落地"。DIAL-MPC 解决的是"力矩级的表现力上限":它走另一条路,用扩散退火驾驭全阶力矩级控制,在真实 Go2 上做出带载跳跃这类需要极限力矩表现的动作——回答了"采样式控制的动态表现力能推到多高"。
这个接力揭示了一条更深的规律:一个方法方向的成熟,往往不是一蹴而就,而是沿着"能不能做(实时)→ 难在哪(认知)→ 能不能落地(真机高维)→ 能做到多好(表现力上限)"逐级推进的。腿足全身采样控制目前正处在"真机落地"刚突破、"表现力上限"和"sim-to-real 鲁棒性"还在攻坚的阶段——这也正是章末"前沿与开放问题"要展开的地方。理解了这条接力,你读这个方向的任何新论文,都能立刻判断它在接力的哪一棒、想往前推哪个瓶颈,而不是孤立地看一个个名字。
§7.1 接触为什么是梯度法的克星、采样的主场 ⭐⭐⭐¶
动机¶
先把"接触"这件事在腿足和操作里的分量说清楚。
腿足机器人**靠接触移动**。它不像轮式机器人有持续的轮地接触,也不像无人机靠连续的气动力——它前进的每一步,都是"脚落地产生支撑力 → 蹬地 → 抬腿 → 再落地"的循环。没有接触,腿足机器人寸步难行。同样,机械臂做的大量任务也是接触主导的:抓取要接触、推动要接触、装配要接触、擦桌子要接触。接触不是这些系统的"边界情况",而是它们功能的核心。
可偏偏,接触在数学上是出了名的难处理。脚一落地,地面反作用力**瞬间从零跳到几百牛**;脚一离地,又**瞬间归零**。手指碰到物体的刹那,接触力凭空出现。这种"通断"让系统的动力学方程 \(\dot{x}=f(x,u)\) 在接触切换的瞬间发生剧变——用数学语言说,\(f\) 对状态和控制的导数 \(\partial f/\partial x\)、\(\partial f/\partial u\) 在接触切换点**不连续,甚至不存在**。
这一节要回答的就是:这个"不可微的接触",为什么对你前置课程里学的梯度式 MPC(iLQR/DDP/SQP)是致命的,而对前六章学的采样式 MPC 却几乎免费。把这件事想透,你就理解了整章——后面 STORM、MJPC、Whole-Body MPPI、DIAL-MPC 全都是同一个论点在不同系统上的兑现。
这一节和第 5 章 §5.3 的"深一层:为什么全阶力矩控制对传统方法这么难"有交集,但视角不同:那里是为 DIAL-MPC 一个具体方法铺背景,这里是把它上升为**整章的核心论点**,并补上机械臂操作的视角。两节可以互相印证。
如果不这样做会怎样¶
不理解"接触为什么难",你会犯两类相反的错误,且都很费时间。
第一类:高估梯度法,硬上 iLQR 做接触任务,然后被各种数值病症折磨。 你会发现 backward sweep 时不时报 NaN、牛顿步乱跳、收敛到原地踏步的烂局部最优,却不知道根因是接触把导数搞坏了。你会怪自己代码写错、参数没调好,反复折腾,而真正的问题是**方法和问题不匹配**——梯度法的前提(动力学可微)在接触面前根本不成立。
第二类:低估采样法,以为它处理接触也要像梯度法那样"枚举接触模式"。 于是你给 MPPI 也配一个 gait scheduler、也预设接触序列,把采样法最大的优势(不需要这些)白白扔掉,还引入了不必要的复杂度和不灵活性。
理解了这一节,你就能在面对一个接触任务时立刻做出正确判断:这是采样法的主场,直接前向 rollout 打分,不要碰导数、不要预设接触序列。
历史:腿足控制为什么长期"绕着接触走"¶
腿足机器人控制有几十年历史,但在 2020 年前后采样式方法成熟之前,主流做法几乎都在**想方设法绕开接触的不可微**。理解这段历史,你才知道采样法解决的到底是什么痛点。
最经典的绕法是**降阶 + 预设接触**。既然全阶动力学带接触太难,那就:(1) 用简化模型——质心动力学(centroidal dynamics)、线性倒立摆(LIP)、单刚体模型(SRBD)——把几十自由度压成几个,部分回避接触复杂性;(2) 用一个 gait scheduler 人为规定接触序列——什么时刻哪只脚着地、哪只脚摆动,把"接触切换该在何时发生"这个最难优化的离散决策**变成预先给定的输入**;(3) 在每个固定的接触模式内,动力学是光滑的,于是可以求导、解 QP/NLP,再交给底层 WBC(whole-body control,全身控制)跟踪。OCS2、Crocoddyl 走的都是这条路,你的前置课程学的也是它。
这条路非常成功——今天波士顿动力、宇树的稳态行走背后都有它的影子。但它的代价是:步态被预先编排死了(机器人只能做你排好的动作)、每个新任务/新地形要重新设计降阶模型和接触序列、对于"接触序列本身就该被优化"的任务(比如怎么爬上一个不规则台子、怎么用身体翻越障碍)它力不从心。
另一条绕法是**接触平滑(contact smoothing)**:把刚性的"通断"接触换成一个光滑的弹簧-阻尼软接触模型,让动力学处处可微,于是梯度法又能用了。但这引入了模型误差——软接触和真实刚性接触的行为有差距,且梯度变成了"有偏的"(在真实接触点附近,光滑模型的梯度并不指向真实的下降方向)。
采样式方法的出现,让人第一次可以**既不降阶、也不预设接触序列、也不平滑**,直接在全阶带接触动力学上工作。它怎么做到的?下面进入理论。
理论:梯度法卡在哪、采样法为什么不卡¶
我们把两类方法面对接触时的处境并排拆解。
梯度式 MPC 的处境:它必须穿过接触求导。 iLQR/DDP 的核心是 backward sweep——从终端往回,用动力学的雅可比 \(A_t=\partial f/\partial x\)、\(B_t=\partial f/\partial u\) 递推值函数的二次近似(Riccati 类递推)。SQP 也类似,要用 \(A_t\)、\(B_t\) 线性化约束。这些方法的每一步都**显式依赖动力学的导数**。
现在让接触发生。考虑一个最简单的画面:一个球从空中落向地面。在接触前,动力学是自由落体 \(\ddot{z}=-g\);接触瞬间,地面给一个巨大的法向力 \(\ddot{z}=-g+\frac{1}{m}F_N\),\(F_N\) 从 0 跳到很大。状态 \(z\)(高度)对时间的二阶导在接触点不连续,那么 \(\partial f/\partial z\) 在接触点附近要么不存在、要么是一个巨大的尖峰。把这样的雅可比喂进 Riccati 递推,会发生什么?
- 如果导数**不存在**(理想刚性接触),递推无法进行——算法卡死或要靠数值差分硬凑一个导数,而差分出来的导数在不连续点毫无意义。
- 如果用软接触让导数**存在但极大**,那么 Riccati 递推里会出现病态的大数,数值上不稳定,牛顿步可能跳到离谱的地方。
- 即便侥幸算出来,这个导数也只反映"接触切换点这一个瞬间"的局部信息,对"该不该在这个时刻发生接触"这种**离散的、组合性的决策**毫无指导——而这恰恰是腿足规划最关键的决策。
这就是为什么梯度法做接触任务,要么报错、要么乱跳、要么陷在烂局部最优。它的整个机器(求导 + 牛顿步)建立在"动力学光滑"的假设上,而接触违反了这个假设。
采样式 MPC 的处境:它根本不求导,只前向 rollout 打分。 回顾第 2 章 §2.1 的 MPPI 主循环——它对每条扰动控制序列 \(U+\varepsilon_k\) 做的事是:丢进动力学/仿真器,前向积分**出一条状态轨迹,沿轨迹累加代价得 \(J_k\)。整个过程**只用到 \(f\) 的前向求值 \(x_{t+1}=f(x_t,u_t)\),从不碰 \(\partial f/\partial x\)。
那接触在这个过程里扮演什么角色?接触发生在仿真器内部——你用 MuJoCo 做 rollout,MuJoCo 自己会检测碰撞、计算接触力、积分出落地后的轨迹。对 MPPI 而言,仿真器是个**黑箱**:输入控制序列,输出轨迹和代价。接触切换那一刻动力学有没有不连续?MPPI 看不见也不关心——它只看到"这条控制序列最后落得怎么样、代价是多少"。不可微的接触,对一个不求导、只看结果的算法来说,根本不构成障碍。
把这两个处境对照,本章的核心论点就立住了:
本质洞察:采样式 MPC 处理接触的秘诀,是把"穿过接触求导"(数学上不可能或病态)这个难题,替换成了"用仿真器穿过接触、再给结果打分"(仿真器本来就会做的事)。梯度法要回答"在接触点动力学的导数是多少"——这个问题没有好答案;采样法只问"我试这 \(K\) 条动作,哪条仿真出来代价最低"——这个问题永远有答案,因为仿真器总能给你一条轨迹和一个数。难点没有消失,它被转移给了仿真器,而仿真器处理接触是成熟可靠的。这就是为什么"接触丰富"几乎是采样式方法的专属主场。
这个洞察还顺带解释了一件事:为什么采样式腿足控制不需要 gait scheduler。 梯度法需要预设接触序列,是因为"接触何时发生"是个离散决策,梯度无法优化离散变量。而采样法不优化离散变量——它直接采**连续的控制**(力矩或位置目标),让仿真器根据这些控制自然地产生接触。哪只脚先落地、何时蹬地,这些不是被规定的,而是从"哪条控制序列代价最低"里**涌现**出来的。换句话说,接触序列从"输入"变成了"输出"。
深一层:为什么"枚举接触模式"这条路走不通¶
上面说梯度法因为"无法优化离散的接触决策"才需要预设接触序列。但你可能会想:那为什么不**枚举**所有可能的接触模式、对每种分别用梯度法优化、再挑最好的?这样不就既保留梯度法的精度、又不用人为预设了吗?这一小节算一笔账,说明这条"枚举"的路为什么在腿足上走不通——理解了它,你才真正明白采样法绕开的是一座多大的山。
先定义"接触模式"。在某个时刻,机器人的每个潜在接触点(四足的 4 个脚、人形的 2 脚 2 手等)都处于"接触"或"不接触"两种状态之一。\(N\) 个接触点,一个时刻就有 \(2^N\) 种接触模式组合。四足 \(N=4\),一个时刻 \(2^4=16\) 种模式;人形若算 4 个接触点也是 16 种,算上脚的多个接触面会更多。
关键在于,控制是在一个 \(H\) 步的时域上规划的,而**每一步的接触模式都可能不同**。于是一条完整的接触模式序列,有 \(\underbrace{16\times 16\times\cdots\times 16}_{H}=16^H\) 种可能。代入一个不算长的时域 \(H=20\):
这是个天文数字——比地球上的沙粒还多。要"枚举所有接触模式序列、对每种跑一遍梯度优化、再挑最好",等于要跑 \(10^{24}\) 次梯度优化,完全不可能。这就是为什么梯度法在接触上只有两条现实的路:要么**人为预设**一条接触模式序列(gait scheduler,把 \(16^H\) 种可能性砍成人指定的 1 种),要么用**混合整数优化**(MINLP,把接触模式当离散变量和连续控制一起优化)——后者理论上更优,但混合整数优化本身是 NP 难的,在实时控制的时间预算(毫秒级)内根本解不动复杂问题。
本质洞察:接触的根本困难,是它在"连续的控制"之外,叠加了一层**离散的、组合爆炸的接触模式选择**。梯度法擅长连续优化,但对这层离散组合无能为力——枚举是 \(16^H\) 的天文数字,混合整数优化是 NP 难,预设则牺牲了灵活性。采样法的过人之处,在于它**根本不显式处理这层离散结构**:它只采连续控制、丢进仿真器,接触模式作为仿真的"副产品"自然确定下来——某条控制序列让脚在某些时刻碰地、某些时刻离地,这个接触模式序列是 rollout 出来的,不是优化变量。于是那个 \(16^H\) 的离散搜索空间,被采样法"溶解"进了连续控制的采样里,从未被显式面对。这是比"不求导"更深一层的洞察:采样法不仅绕开了接触的不可微(连续层面),还绕开了接触模式的组合爆炸(离散层面)——两座山一起绕过去了。这也从根本上解释了为什么"让接触涌现"不只是省事,而是**把一个组合不可解的问题,变成了一个连续可采样的问题**。
把这笔账记住,你对"采样法不需要 gait scheduler"会有比"省了道工序"深得多的理解——它省掉的是一个 \(10^{24}\) 量级的、梯度法根本啃不动的离散搜索。当然,代价仍是 §7.1 易错点反复强调的:采样法把这一切转移给了仿真器和连续采样,于是仿真器质量(要能正确算出各种接触)和维度灾难(连续控制空间仍然高维,§7.4)成了它要付的账。天下没有免费的午餐,但把"组合不可解"换成"高维但可采样",是一笔非常划算的交易。
多视角:两种"在崎岖地形上找路"的方式¶
给这个对照配一个类比,帮你记牢——但记住类比都有边界,我会标出像与不像。
想象你要在一栋有楼梯和墙的建筑里从一楼走到目标房间,但你蒙着眼,只能靠某种方式探路。
梯度法像"摸着脚下地板的坡度走"。 你感受当前脚下地板往哪边倾斜,朝下坡方向挪一小步,再感受、再挪。在平整的斜坡上这很高效。但一遇到楼梯(地板高度突变 = 动力学不连续)你就懵了——脚下要么是平的(坡度为零,不知道往哪走)要么是悬空的(坡度无穷大,一脚踏空)。遇到墙(硬约束突变)更是直接撞上去。
采样法像"往四面八方扔一把会报位置的探测球"。 你一次扔出 \(K\) 个球到周围不同方向,每个球落定后报告"我落在哪、这地方离目标多远"。你看哪个方向的球落得最好,就往那个方向的加权平均挪。楼梯?球自己会滚上或滚下台阶,落定后照样报位置,你完全不用知道台阶的"坡度"。墙?撞墙的球报一个很差的位置(高代价),你自然就不往那边走。
像的地方:两者都是迭代式地"探一下、挪一步"。不像的地方(边界):梯度法用的是**一阶局部信息**(当前点的导数),所以怕不连续、怕突变;采样法用的是**零阶全局信息**(一批点的函数值),所以不怕不连续,但代价是要扔很多球(样本效率低,这是它的软肋,见 §7.4)。不要把类比延伸成"采样法处处优于梯度法"——在光滑、低维、要求高精度的问题上,"摸坡度"比"扔球"高效得多(这正是 §7.6 选型的核心)。
易错点:采样不怕接触 ≠ 接触任务就免费了¶
讲到这里,要主动浇一盆冷水,免得你产生"用了采样法接触任务就迎刃而解"的错觉。这是初学者读完上面会立刻产生、却很危险的误解。
采样法绕开的只是"接触的不可微"这一个困难,没有绕开别的困难。 具体说,全身接触任务还有两个硬骨头采样法并不能免费解决:
第一,维度灾难。腿足全身几十个自由度,第 4 章 §4.2/§4.6 早就警告过:高维下随机样本几乎都落在远离均值的薄壳上,要"撒满"空间所需样本数随维度指数爆炸。所以单纯把 vanilla MPPI 丢到全身上,会因为样本不够而找不到好轨迹。这就是 §7.4 要讲三把钥匙(降维、退火、warm-start)的原因——它们治的就是这个,而不是接触本身。
第二,仿真器质量。采样法把难点转移给了仿真器,那仿真器就成了瓶颈:仿真器的接触模型不准(接触力、摩擦、穿透处理),rollout 算出的代价就是错的,优化出来的控制在真机上就不work(sim-to-real gap)。MJPC 论文那句"你只能控制 MuJoCo 能仿真的东西"(you can only control what MuJoCo can simulate)就是这个意思。所以采样式全身控制对仿真器精度极其敏感(§7.5 会接着讲这点如何影响动作空间选择)。
把这两条记住:采样法让你"能碰"接触任务,但"碰得好"还需要治维度灾难、还需要好仿真器。 本章后面的篇幅,很大程度上就是在讲怎么"碰得好"。
一个能跑的对照:光滑地形 vs 带台阶地形¶
最后用一个最小 demo 把上面的论点落到代码上。我们不真的搭腿足仿真(那要 MuJoCo),而是用一个一维点质量在"代价地形"上找最低点,对比两种地形:一个光滑碗状地形,一个带"台阶"(不连续跳变)的地形。我们看 MPPI 这种零阶采样法在两种地形上是否一视同仁。
import numpy as np
rng = np.random.default_rng(0)
# 两种"地形":把它想象成代价随位置 x 的变化
def cost_smooth(x):
"""光滑碗状:处处可微,梯度法的主场。"""
return (x - 3.0) ** 2
def cost_stair(x):
"""带台阶:在 x=1 和 x=2 处代价不连续跳变(模拟接触切换)。
梯度法在跳变点导数不存在;采样法只看函数值,不受影响。"""
base = (x - 3.0) ** 2
step = 4.0 * (x < 1.0) + 2.0 * ((x >= 1.0) & (x < 2.0)) # 不连续的"墙/台阶"
return base + step
def mppi_1d_minimize(cost_fn, x0=-2.0, K=200, iters=20, lam=1.0, sigma=0.8):
"""用 MPPI 的"采样—加权—更新"思想,在 1D 上最小化 cost_fn。
注意:全程只调用 cost_fn 求值,从不对它求导——这正是采样法的本质。"""
x = x0
for _ in range(iters):
eps = rng.normal(0.0, sigma, size=K) # 采 K 个扰动
xs = x + eps # K 个候选位置
J = cost_fn(xs) # 只前向求值,不求导
rho = J.min() # 减最小值,数值稳定(见第2章§2.1)
w = np.exp(-(J - rho) / lam)
w /= w.sum()
x = x + np.sum(w * eps) # 加权更新
return x, cost_fn(np.array([x]))[0]
for name, fn in [("光滑地形", cost_smooth), ("带台阶地形", cost_stair)]:
x_star, J_star = mppi_1d_minimize(fn)
print(f"{name}: 收敛到 x={x_star:.3f}, 代价={J_star:.4f} (真最优 x=3.0)")
# 典型输出:
# 光滑地形: 收敛到 x≈2.98, 代价≈0.000
# 带台阶地形: 收敛到 x≈2.98, 代价≈0.000
# —— 两种地形 MPPI 表现几乎一样,因为它只看代价值、不在乎可不可微
这个 demo 朴素到近乎平凡,但它精确演示了本节的论点:MPPI 在"带台阶"(不连续)地形上和在光滑地形上表现几乎一样好,因为它的更新公式里只出现 cost_fn 的**求值**(J = cost_fn(xs)),从不出现它的导数。把这里的"台阶"换成真实腿足里的"接触切换",把"1D 位置"换成"几十维控制序列",把 cost_fn 换成 MuJoCo rollout,你就得到了全身采样 MPC 的核心——而中间的逻辑一模一样。
作为对照,如果你写一个梯度下降 x = x - lr * grad(cost_fn)(x) 去跑带台阶地形,它在台阶处的数值梯度要么是 0(被卡住)要么是巨大尖峰(跳飞),根本到不了最优。你可以把这个对照实现作为练习(见本节练习 2)。
本质洞察(与上一个洞察互补,从代码角度再说一遍):一个优化算法怕不怕"不可微",本质上取决于它的更新公式里出现的是目标函数的**导数**还是**值**。出现导数的(梯度下降、iLQR、SQP)就怕不可微;只出现值的(MPPI、CEM、Predictive Sampling)就不怕。这是个一眼就能判断的标准——以后看到任何新优化器,先问"它的更新用导数还是用值",立刻知道它能不能碰接触/黑箱/不连续代价。
把对照做实:梯度下降在"墙"前怎么栽¶
光说"梯度下降在台阶处会卡住或跳飞"不够直观,我们把它跑出来。为了让失败现象**稳定可复现**(而非碰运气),我们把上面的台阶地形推到极端——做一道"平坦高墙":墙这边代价完全平坦(处处梯度为零),最优解被挡在墙的另一边。这个地形有清晰的物理对应:平坦区 = "推杆还没接触到箱子,怎么小幅扰动都不改变代价,梯度提供不了任何方向信息";墙的另一边 = "接触上了,代价开始随动作变化"。梯度用有限差分近似(真实代价常无解析导数,你也只能这么估):
def cost_wall(x):
"""平坦高墙地形:x<1 是完全平坦的高台(代价恒=5, 梯度处处为0),
最优 x=3 被挡在墙后(x>=1 是光滑碗 (x-3)^2)。对应'没接触上→梯度无信息'。"""
x = np.asarray(x, dtype=float)
return np.where(x < 1.0, 5.0, (x - 3.0) ** 2)
def grad_descent_1d(cost_fn, x0=-2.0, lr=0.05, iters=300, fd_eps=1e-4):
"""有限差分梯度下降。平坦区差分梯度≈0,于是原地卡死、过不了墙。"""
x = x0
for _ in range(iters):
# 有限差分估计导数:(f(x+h)-f(x-h))/(2h)
g = (cost_fn(np.array([x + fd_eps])) - cost_fn(np.array([x - fd_eps]))) / (2 * fd_eps)
x = x - lr * g[0]
return x, float(cost_fn(np.array([x]))[0])
# 都从 x0=-2(墙这边的平坦区)出发,真最优 x=3 在墙后
x_mppi, J_mppi = mppi_1d_minimize(cost_wall, x0=-2.0, sigma=1.2, iters=30)
x_gd, J_gd = grad_descent_1d(cost_wall, x0=-2.0)
print(f"MPPI : x={x_mppi:.3f}, 代价={J_mppi:.4f}")
print(f"梯度下降: x={x_gd:.3f}, 代价={J_gd:.4f}")
# 典型输出(跨随机种子稳定复现):
# MPPI : x≈3.01, 代价≈0.000 —— 样本越过墙, 到达最优
# 梯度下降: x=-2.000, 代价=5.000 —— 平坦区梯度为0, 原地卡死, 永远过不了墙
跑出来的现象一目了然:梯度下降从 \(x=-2\) 出发,脚下是完全平坦的高台,有限差分看到的梯度处处为零——它不知道该往哪走,于是原地不动,永远卡在代价 5 的次优区,到不了墙后的最优 \(x=3\)。而 MPPI 以 \(\sigma=1.2\) 的方差把样本撒出去,总有一批样本**直接越过墙**落到墙后的低代价区,加权更新就把均值往墙后拽,轻松到达 \(x\approx 3\)。
这就是"局部一阶信息(梯度)"和"全局零阶信息(一批样本的值)"的本质差别在最小例子上的显形:梯度只知道脚下一点点的坡向,脚下平坦(或被墙挡住)就抓瞎;采样撒出一片探针,墙那边有什么它一眼就看见。把这道"墙"想象成腿足里"脚要不要跨过一个台阶、手要不要够到还没接触的物体"——在接触建立之前,代价对动作往往不敏感(没碰到,怎么动都一样),梯度给不出方向,正如这道平坦的墙。这就是为什么梯度法需要人预先告诉它"在这一步去接触"(gait scheduler 预设接触序列),而采样法自己就能发现"越过墙/接触上代价更低"。
这里也顺带演示了梯度法在不可微/不敏感地形上的**两种**典型失败(本节练习 2 让你都跑一遍):一种是上面这种"平坦/被挡→梯度≈0→卡死",另一种是"在陡峭不连续点→有限差分梯度爆炸→大步跳飞到不可预测的地方"。后者的危险在于**不可预测**:跳飞有时恰好落到好的一侧(侥幸成功),有时落到更糟的地方(彻底失败),完全取决于步长和地形的偶然配合——一个会"偶尔靠运气成功"的优化器,在真机上是不能信任的。
深一层:接触平滑为什么不是"免费的午餐"¶
§7.1 历史部分提过一条"绕法"——接触平滑(用光滑软接触换掉刚性接触,让梯度法又能求导)。它听起来很美:既保留梯度法的高效,又解决了不可微。为什么本章还要费劲讲采样法,而不是都用接触平滑?这一小节把这件事讲透,它能让你更深地理解采样法的价值边界。
接触平滑的核心操作,是把"通断"的刚性接触力换成一个随穿透深度光滑变化的软接触力。比如刚性接触是"穿透为零时力为零、一旦穿透力瞬间很大"(阶跃),平滑后变成"力随穿透深度连续、可微地增大"(比如线性弹簧 \(F = k\cdot\text{penetration}\),甚至更光滑的形式)。这样动力学处处可微,iLQR/DDP 又能跑了。
问题出在两个层面。
第一层:平滑引入了模型与真实的偏差。 软接触和真实刚性接触的物理行为不一样——落地的反弹、能量耗散、接触建立和断开的时机都有差。你用软接触模型优化出的"最优控制",是"软接触世界"的最优,搬到真机的刚性接触世界就未必最优、甚至不可行。这是个 sim-to-real 层面的偏差。
第二层(更微妙):平滑后的梯度是"有偏的向导"。 这一层是关键,也最容易被忽略。即便你接受软接触的物理偏差,平滑后的梯度本身也可能把优化**带错方向**。直觉是这样:在真实接触点附近,平滑模型为了"可微",把一个本该陡峭的变化抹成了缓坡,于是梯度的大小和方向都被这个人为的缓坡扭曲了——它指向的"下降方向"是软接触世界里的下降方向,未必是你真正想要的方向。平滑参数(软接触有多软)越大,可微性越好但偏差越大;越小,越接近真实但梯度越病态(接近刚性时又回到大尖峰)。你被夹在"可微但有偏"和"接近真实但病态"之间,没有一个能同时满足两者的甜点。
本质洞察:接触平滑试图把采样法的优势(能处理接触)嫁接到梯度法上,但它付出的代价是"用一个被扭曲的、有偏的问题替换了真问题"。采样法的高明之处,恰恰是它**根本不需要这个替换**——它直接在真实的、不可微的接触上工作,因为它不求导、不需要可微性。换句话说,接触平滑是"为了能用梯度法,把问题改造成梯度法能啃的样子";采样法是"不改造问题,换一个不挑剔问题的方法"。这两条路的哲学差异很深:一个削足适履(改问题去适配方法),一个量体裁衣(换方法去适配问题)。这也回答了本小节开头的问题——为什么不都用接触平滑:因为它解决不可微靠的是"假装可微",而这个假装是有代价的(物理偏差 + 梯度偏差),在接触主导、要求真机可行的任务上,这个代价往往比采样法的代价(样本效率低,靠§7.4治)更难承受。
不过也别把接触平滑一棍子打死。在接触是"配角"、动力学主体光滑、且你确实需要梯度法的高精度/硬约束的场景(比如抓取中轻微的指尖接触、行走中已知接触序列的微调),适度的接触平滑 + 梯度法是有效的——这又回到 §7.6 的互补思想:没有放之四海皆准的方法,关键是接触在你的问题里是主角还是配角。接触是主角(爬越、推、非结构化交互)→ 采样法的主场;接触是配角(已知步态的精修)→ 平滑 + 梯度法可行。
系统分类:接触不止一种,难点也不一样¶
前面笼统地说"接触让动力学不可微",但"接触"其实是一类现象的统称,细分起来有几种类型,难度和处理方式各不相同。建立这个分类,能让你拿到一个具体任务时,更精确地判断"这里的接触到底难在哪、采样法的优势有多大"。
按接触的物理特性,腿足/操作里常见的接触可粗分为三类:
类型一:通断接触(making/breaking contact)。 最基本、也最典型的不可微来源——接触从"无"到"有"或从"有"到"无"的那个瞬间。脚落地、脚离地、手碰到物体、手松开,都是通断。它的不可微体现在"接触力在接触建立的瞬间从零跳到非零"(§7.1 demo 里那个 pen 切换正是它的最小化身)。这是采样法相对梯度法优势最大的地方——通断是纯粹的离散切换,梯度法最头疼,采样法完全无感。
类型二:滑动/黏滞接触(sliding/sticking, 摩擦)。 接触已经建立,但接触面可能"黏住"(静摩擦,无相对滑动)或"滑动"(动摩擦)。这两种状态的切换(黏↔滑)也是不连续的——摩擦力在黏滑切换处不光滑(库仑摩擦模型在零相对速度处有个不可微的尖角)。推箱子、脚在地面蹬地都涉及黏滑。这类接触的不可微比通断更微妙,但采样法同样不受影响(它只 rollout,仿真器算黏滑、它打分)。
类型三:刚性冲击(rigid impact)。 高速接触的瞬间(如跳跃落地、快速踏步),接触力在极短时间内达到很大值(理想刚性下是冲量/狄拉克函数)。它不仅不可微,数值上还很"硬"(stiff),对仿真器的接触求解器是个考验。这类接触对仿真保真度要求最高——冲击的能量耗散、反弹建模稍有偏差,sim-to-real 就差很多(呼应 §7.1 易错点和章末前沿的"仿真器瓶颈")。
本质洞察:这三类接触有个共同点——它们都在某个"状态切换"处不可微(通断在"接触有无"切换、黏滑在"摩擦状态"切换、冲击在"速度突变"处),而这正是梯度法的统一软肋、采样法的统一主场。但它们对**仿真器**的要求递增:通断只要仿真器能正确判断接触有无;黏滑要仿真器有合理的摩擦模型;刚性冲击要仿真器能稳定、准确地解高刚度接触。这揭示了一个对采样法尤其重要的判断——接触越"硬"(越往冲击端走),采样法"绕开不可微"的优势越不打折扣(它照样只 rollout 打分),但它对仿真器保真度的依赖越重(§7.1 易错点)。所以评估一个接触任务对采样法的难度,不只看"有没有接触",更要看"接触是哪种类型、仿真器能不能把这种接触算准"。一个全是刚性冲击的任务(如高台跳下落地),采样法在"绕开不可微"上毫无压力,但成败几乎全押在"仿真器能不能把落地冲击算准"上——这就把难点精确地定位到了仿真器,而非优化器。
带着这个分类回看本章的系统:MJPC/STORM 处理的多是通断 + 中等黏滑(站立、行走、操作),仿真器要求适中;DIAL-MPC 的带载跳跃涉及刚性冲击(落地),所以它对仿真保真度和退火都要求更高(§7.5/第 5 章)。下次你读一篇腿足采样控制的论文,先判断它的任务主要是哪类接触,你就能预判它的难点在优化还是在仿真,以及它的成果有多依赖仿真器质量。
⚠️ 常见陷阱¶
陷阱 1:以为"MPPI 不需要接触模型"——把接触当成可以无视的东西 ⚠️ 概念误区 - 错误想法:"既然 MPPI 不求导、不枚举接触模式,那我连接触都不用管,它自己会处理。" - 现象/后果:你用一个**没有接触的**简化模型(比如只有自由飞行动力学)做 rollout,结果机器人"穿过"地面、脚陷进地里,优化出的轨迹在真机上完全不可行。 - 根本原因:混淆了"不需要对接触求导"和"不需要仿真接触"。MPPI 确实不求接触的导数,但它**必须用一个会算接触的仿真器做 rollout**——接触力、碰撞、摩擦得真实地出现在前向积分里,否则 rollout 出的轨迹是假的。难点是被转移给仿真器了,不是被消除了。 - 正确做法:用一个带完整接触动力学的仿真器(MuJoCo、Isaac、Drake 等)做 rollout。确认仿真里脚和地面、手和物体的接触是被正确建模的。自检:把一条 rollout 的轨迹可视化,看脚有没有正确踩在地面上、有没有穿透。
陷阱 2:仍然给采样式腿足控制配一个 gait scheduler ⚠️ 思维陷阱 - 错误想法:"腿足控制嘛,肯定要先排好步态序列,MPPI 也不例外。"(被梯度法的范式惯性带偏) - 现象/后果:你预设了接触序列喂给 MPPI,机器人只能做你排好的步态,丧失了采样法本可以涌现新步态、应对非结构化地形的最大优势;还引入了步态切换调参的额外负担。 - 根本原因:没理解采样法是直接采连续控制、让接触从代价里涌现的(本节理论部分)。预设接触序列是梯度法因"不能优化离散变量"而不得已的妥协,采样法不受这个限制。 - 正确做法:让 MPPI 直接采控制(力矩或位置目标),把任务目标(到达某处、维持平衡)写进代价,接触序列交给优化自然产生。只有在你**确实想约束某种特定步态**(比如要求对称小跑)时,才通过代价软引导,而不是硬编排序列。
陷阱 3:用平滑/惩罚接触模型做 rollout,却期望真机表现一致 ⚠️ 编程陷阱 - 错误想法:"刚性接触不好仿,我用软接触(大刚度弹簧)让仿真更稳,反正 MPPI 不求导,软硬接触对它都一样。" - 现象/后果:仿真里很顺,真机上机器人落地"软绵绵"或反复反弹,sim-to-real 差距大;尤其是依赖精确接触时机的动作(跳跃落地、精确踩点)会失败。 - 根本原因:软接触改变了接触的**物理行为**(落地的反弹、能量耗散、接触时机),rollout 出的轨迹对应的是"软接触世界"的最优,和真机的刚性接触世界不一致。MPPI 不求导不代表它对仿真器的**物理保真度**不敏感——恰恰相反,它把全部信任都押在了仿真器上。 - 正确做法:尽量用接近真机的接触参数(合理的刚度、阻尼、摩擦系数、穿透容差)。做一点 sim-to-real 校准(system identification)。优先选择对接触参数误差更鲁棒的动作空间(§7.5 会讲:位置目标 + PD 比直接力矩鲁棒得多)。
练习¶
- (概念) 用一句话向一个只学过 iLQR 的同事解释"为什么 MPPI 能做接触任务而 iLQR 不能"。要求不超过两句,且必须点出"求导 vs 求值"这个关键。
- (动手·对照实验) 把本节 demo 里的 MPPI 换成普通梯度下降
x ← x - lr * ∂cost/∂x(用有限差分近似导数),在"带台阶地形"上跑,记录它卡在哪里、为什么。再把步长lr调大调小,观察它在台阶处的行为(卡住 / 跳飞)。写一段话总结:梯度法在不连续点的两种典型失败模式各是什么。 - (设计·开放) 假设你要让四足机器人完成"从趴下到站起"。如果用梯度式 OCS2,你需要预设哪些接触相关的东西?如果用采样式 MPPI,你需要写哪些代价项、不需要预设什么?把两种方案的"需要人工指定的东西"列个对照表。
- (综合·联系第 5 章) 第 5 章 §5.3 说 DIAL-MPC 能做全阶力矩控制,靠的是"免求导(采样)+ 免陷局部(退火)"两个叠加。结合本节,请说明:这两个"免"分别对应本章的哪两个困难(接触不可微 / 高维非凸)?只有采样、没有退火会怎样?只有退火、用梯度会怎样?
§7.2 STORM:张量化关节空间 MPPI 与多目标代价缓冲 ⭐⭐⭐¶
动机¶
§7.1 确立了"采样不怕接触"的论点。现在我们看第一个把它兑现到真实机器人上的系统——STORM,一个跑在机械臂上的反应式(reactive)采样 MPC。选 STORM 作为入门范例,是因为机械臂比腿足全身维度低一点(7 自由度 vs 几十)、接触相对简单(主要是避碰和可选的接触操作),适合先把工程骨架建立起来,再上腿足的硬骨头。
STORM 要解决的具体问题是**反应式操作**:让机械臂实时跟踪一个移动的目标(比如人手里晃动的物体),同时避开障碍、不撞自己、不超关节限位、不进奇异位形。"实时 + 反应式"意味着不能离线规划好一条轨迹再执行——目标在动、环境在变,必须每个控制周期重新规划,这正是 MPC 的设定,而且是采样式 MPC 擅长的设定(黑箱、约束复杂、要快)。
如果不这样做会怎样:任务空间 MPC 的盲区¶
在 STORM 之前,机械臂的实时 MPC 常见做法是**任务空间(task-space)MPC + 底层操作空间控制器(OSC)**:MPC 在末端执行器的 6 维任务空间里规划(位置/姿态),算出末端该往哪动,再交给一个底层 OSC 把任务空间指令转成关节力矩。
这条路有个根本盲区:任务空间里看不到关节空间的约束。 关节限位(每个关节的角度范围)、奇异位形(雅可比退化、末端某些方向失去可控性)、连杆自碰撞(机械臂自己的两段连杆撞上)——这些全是**关节空间**的概念。MPC 在任务空间里规划,对这些约束是"盲"的,只能寄希望于底层 OSC 去兜底,但 OSC 是个局部控制器,没有预测能力,常常等约束快违反了才反应,已经晚了。
STORM 的选择是**直接在关节空间做 MPC**——决策变量是关节力矩/速度序列,rollout 在关节空间进行,于是关节限位、奇异、自碰撞这些约束可以**直接写进代价**、在规划时就被前瞻性地避开。代价是关节空间维度更高、动力学更复杂,但采样式方法(不求导、GPU 并行)恰好能扛住这个代价——这又是 §7.1 论点的体现。
历史¶
STORM 的全称是 Stochastic Tensor Optimization for Robot Motion(机器人运动的随机张量优化),出自 NVIDIA 与华盛顿大学(Bhardwaj、Sundaralingam、Mousavian、Ratliff、Fox、Ramos、Boots),发表于 CoRL 2021(Oral)。它是把第 2 章那套 GPU MPPI 思想,用一种**和 MPPI-Generic 不同的工程哲学**实现出来的代表——值得和第 2 章 §2.3 对照着理解。
理论与设计:三个值得学的工程决策¶
STORM 的算法内核就是你第 2–3 章学的 MPPI(指数加权 + 相关噪声做平滑,平滑思想见第 3 章 §3.2),没有新数学。它的价值在三个工程决策上。
决策一:用 PyTorch 张量运算做批量 rollout,而非手写 CUDA kernel。 这是 STORM 和第 2 章 MPPI-Generic 最大的哲学分歧。回顾第 2 章 §2.2–§2.3:MPPI-Generic 为了极致性能,手写 CUDA kernel,精细控制线程映射、shared memory、访存合并。STORM 反其道而行——它把 \(K\) 条 rollout 表达成**批量张量操作**:状态张量形状 \((K, H, n_x)\)、控制张量 \((K, H, n_u)\),动力学步进、代价计算全用 PyTorch 的批量算子(如 torch.bmm 批量矩阵乘)写成。GPU 加速"免费"得来——PyTorch 自己会把张量操作派发到 GPU,你一行 CUDA 都不用写。
这两种哲学的取舍很清晰:
| 维度 | 手写 CUDA(MPPI-Generic,第 2 章) | 张量化(STORM 本节) |
|---|---|---|
| 开发难度 | 高——要懂 CUDA 内存层级、线程映射 | 低——会 PyTorch 即可 |
| 性能上限 | 高——可榨干硬件 | 中——受框架开销限制 |
| 灵活性/可改 | 改 kernel 成本高 | 改一行张量操作即可换动力学/代价 |
| 适合谁 | 要部署、追求极致延迟 | 要快速原型、做研究、调代价 |
本质洞察:STORM 和 MPPI-Generic 的分歧,本质是"开发效率 vs 运行效率"在采样式 MPC 工程里的一次具体权衡。它呼应第 2 章 §2.3 反复讲的"为什么要一套抽象"——只不过 STORM 选择了用**深度学习框架的张量抽象**当那层抽象,代价是放弃一部分峰值性能、换来研究迭代速度。这个权衡没有标准答案:做研究、频繁改代价函数时,张量化让你几分钟换一版;做产品、要压榨每一毫秒延迟时,手写 CUDA 才值得。学会按"我现在是在迭代研究还是在压榨部署"来选,比记住哪个库更快有用。
决策二:多目标代价缓冲(cost buffer)——把"很多个想要"加成一个数。 反应式操作要同时满足好几个目标:到达目标、避碰、不超限位、远离奇异、动作平滑。STORM 把每个目标写成一个独立的代价项,每项都是对 \((K, H)\) 批量轨迹的一次张量运算,最后加权求和成每条轨迹的总代价 \(J_k\)。这就是"代价缓冲"——一组可插拔、可叠加的代价模块。
import torch
import torch.nn.functional as F
class StormCostBuffer:
"""STORM 风格的多目标代价:每项都是 (K,H) 批量张量运算,最后加权求和。
形状约定: state_batch (K,H,nx), action_batch (K,H,nu)。
注意整段没有任何对动力学/代价的求导——纯前向求值(§7.1 的核心)。"""
def __init__(self, goal, weights):
self.goal = goal # 目标末端位置 (3,)
self.w = weights # 各项权重 dict
def __call__(self, state_batch, action_batch):
K, H = state_batch.shape[:2]
cost = torch.zeros(K, device=state_batch.device)
# 1) 到达代价:末端执行器到目标的距离(沿时域累加)
ee_pos = forward_kinematics(state_batch[..., :7]) # (K,H,3) 正运动学
cost += self.w['reach'] * ((ee_pos - self.goal) ** 2).sum(dim=(-1, -2))
# 2) 避碰代价:signed distance 为负(穿入)时罚, margin 是安全裕度
sdf = query_sdf(ee_pos) # (K,H) 到最近障碍的有符号距离
cost += self.w['coll'] * F.relu(self.margin - sdf).sum(dim=-1)
# 3) 关节限位代价:超出 [q_lo,q_hi] 才罚(任务空间 MPC 看不见这一项, §动机)
q = state_batch[..., :7] # (K,H,7) 关节角
over = F.relu(q - self.q_hi) + F.relu(self.q_lo - q)
cost += self.w['limit'] * (over ** 2).sum(dim=(-1, -2))
# 4) 奇异/可操作度代价:可操作度越低越危险(雅可比行列式的代理)
manip = manipulability(state_batch[..., :7]) # (K,H) 越大越好
cost += self.w['sing'] * F.relu(self.manip_min - manip).sum(dim=-1)
# 5) 平滑代价:相邻控制差分, 抑制抖动(思想同第3章§3.2)
du = action_batch[:, 1:] - action_batch[:, :-1]
cost += self.w['smooth'] * (du ** 2).sum(dim=(-1, -2))
return cost # (K,)
这段代码体现了两件事。其一,每一项都是纯张量求值——relu、平方、求和,没有任何 .backward() 或梯度,完美符合 §7.1"只求值不求导"。其二,关节限位项(第 3 项)和奇异项(第 4 项)正是任务空间 MPC 看不见、而关节空间 MPC 能直接写进去的约束——这把 §动机里的论点落到了具体代码上。
这个"多目标代价缓冲"你其实已经见过它的近亲:第 4 章 §4.5 的 Nav2 MPPI 用 critic 架构**把导航的多个目标(路径跟随、避障、朝向、限速)拆成可插拔的 critic 插件,逐个算分再加权。STORM 的 cost buffer 和 Nav2 的 critic 架构是同一个工程智慧——**把复杂的"我想要很多东西"分解成一组独立、可叠加、可单独调权重的模块——只是一个用 C++ pluginlib(生产级,第 4 章)、一个用 PyTorch 张量(研究级,本节)。
深一层:cost buffer 的模块化为什么是"好工程"¶
cost buffer / critic 这种"把代价拆成独立模块"的设计,看起来只是代码组织问题,但它背后有扎实的软件工程价值,值得点破——理解它,你写任何采样 MPC 的代价时都会自觉地往这个方向走,而不是把所有目标揉进一个庞大的代价函数。
把多目标代价模块化(而非写成一个大函数),换来三个具体好处:
其一,可单独测试。 每个代价项是独立模块,你可以单独喂给它一批已知轨迹、验证它的输出符合预期(比如给避碰项一条明显穿障的轨迹,确认它返回高代价)。如果代价揉成一个大函数,某项写错了,你只能看到"总代价不对",很难定位是哪一项的锅。模块化让 bug 可隔离、可单测——这是工程可靠性的基础。
其二,可单独调权重、可增删。 调参时你常要问"如果更在乎避碰会怎样",模块化让你只动 w['coll'] 一个数、其余不变,干净地做消融(§7.2 陷阱2 调权重的前提就是各项解耦)。要加新目标(如"末端朝向"),加一个模块即可,不碰已有代码;要去掉某目标,删一个模块。这种"开闭原则"(对扩展开放、对修改封闭)让代价能随任务演进而平滑增删。
其三,可跨任务复用。 很多代价项是任务无关的——"关节限位""平滑""避碰"几乎所有机械臂任务都要。模块化让这些通用项写一次、到处复用,新任务只需写它特有的目标项。Nav2 的 critic 插件正是这么用的:一套 critic 库,不同导航任务复用 + 组合。
本质洞察:cost buffer 的模块化,是把**软件工程的"高内聚、低耦合"原则用到了代价设计上**——每个代价项内聚地负责一个目标(高内聚),项与项之间通过"各算各的分、最后加权求和"解耦(低耦合)。这个解耦不是为了好看,而是直接换来可测试、可调、可复用三大工程收益。它和第 2 章 §2.3 MPPI-Generic 把 Dynamics/Cost/Sampling/Controller 解耦成四件是同一种智慧的不同应用——把一个复杂系统拆成职责单一、接口清晰的模块,是让它可维护、可演进、可协作的根本。这条原则超越采样 MPC,是所有复杂软件的通则;但在采样 MPC 里它格外适用,因为"加权求和"这个天然的组合方式,让代价项的解耦几乎零成本。所以你写采样 MPC 的代价时,默认就该往模块化走——这不是过度设计,而是会持续回报的好习惯。
决策三:实时性。 STORM 论文报告在单 GPU 上每个控制周期计算下一个控制指令耗时不到 0.02 秒(约几十赫兹量级),足以支撑机械臂的反应式控制。这个速度的来源正是张量化批量 rollout——几千条 7 自由度轨迹的前向积分被 GPU 一次并行算完。
把张量化讲实:批量维 K 怎么并行、时域 H 怎么串行¶
张量化 rollout 是 STORM 的命脉,也是最容易写错(写成假并行)的地方(见下面陷阱1)。用一个最小的批量 rollout 骨架把"哪个维度并行、哪个维度串行"讲死,你照着这个形状写就不会错:
import torch
def batched_rollout(x0, U, step_fn):
"""张量化批量 rollout 的正确骨架。
x0: (K, nx) 初始状态(K 条轨迹);U: (K, H, nu) 控制序列。
返回: 状态轨迹 (K, H, nx)。
要点:时域 H 必须串行(物理因果,x_{t+1} 依赖 x_t);
但每个时间步内对 K 条轨迹的计算是一次批量张量操作(并行)。"""
K, H, nu = U.shape
x = x0 # (K, nx) —— K 这一维是批量, 将被并行
traj = []
for t in range(H): # 唯一的 Python 循环: 沿时域 H 串行
u_t = U[:, t, :] # (K, nu) 取所有轨迹在 t 步的控制
x = step_fn(x, u_t) # (K, nx) 一次性推进所有 K 条! 批量并行
traj.append(x)
return torch.stack(traj, dim=1) # (K, H, nx)
# 反例(错误!): 连 K 都用 Python 循环, 完全没有并行, PyTorch 开销反而拖慢
def batched_rollout_WRONG(x0, U, step_fn):
K, H, nu = U.shape
trajs = []
for k in range(K): # ✗ 对每条轨迹单独循环 —— 致命错误
x = x0[k]; one = []
for t in range(H):
x = step_fn(x, U[k, t]) # 一次只算一条, GPU 几乎闲置
one.append(x)
trajs.append(torch.stack(one))
return torch.stack(trajs)
对照这两个版本,张量化的精髓一目了然:正确版只有一层 for t in range(H) 循环(时域,物理上必须串行),循环体内 step_fn(x, u_t) 一次性处理全部 \(K\) 条轨迹(批量维并行);错误版多了一层 for k in range(K),把本该并行的 \(K\) 条轨迹拆成一条条串行算,GPU 的并行能力完全用不上,PyTorch 的调度开销反而让它比 NumPy 还慢。
为什么 \(H\) 不能也并行掉?因为 rollout 是物理仿真,\(x_{t+1}=f(x_t,u_t)\) 有严格的因果依赖——你必须先算出 \(t\) 时刻的状态,才能算 \(t+1\) 时刻。这个串行是物理决定的,绕不开。而 \(K\) 条轨迹之间**没有任何依赖**(每条是独立的"如果这么控制会怎样"的假设),所以可以、也必须并行。这正是第 2 章 §2.2 讲 CUDA 线程映射时"\(K\) 个线程各算一条轨迹"在张量框架里的等价表达——CUDA 里是 \(K\) 个线程并行,张量化里是张量第 0 维(批量维)并行,底层硬件做的是同一件事。
本质洞察:所有并行采样 MPC(不管用 CUDA 还是张量框架)的可并行性都来自同一个结构事实——\(K\) 条候选轨迹彼此独立(可并行),但每条轨迹内部沿时域有因果依赖(须串行)。理解了这一点,你无论用什么工具实现 rollout,都知道该把并行用在哪一维(批量维 \(K\))、哪一维只能认命串行(时域 \(H\))。写不出加速的人,十有八九是把这两维搞反了——要么没并行 \(K\)(错误版),要么妄图并行 \(H\)(违反因果)。
深一层:关节空间采样的隐藏成本——每条 rollout 都要算正运动学¶
§7.2 的动机讲了关节空间 MPC 的好处(能直接处理关节限位、奇异、自碰撞)。但关节空间不是免费的,它有一个容易被忽略的成本,值得点破——代价计算里要反复算正运动学(FK),而且是在巨大的批量上算。理解这个成本,你才能合理估算关节空间采样 MPC 的算力需求、也才明白为什么张量化(§7.2 决策一)在这里如此关键。
问题出在哪?关节空间里,状态/控制是关节角 \(q\),但很多代价是定义在**任务空间**的——"末端执行器离目标多远"(到达代价)、"末端会不会撞到障碍"(避碰代价)。要算这些代价,你必须先把关节角 \(q\) 通过正运动学 \(\text{FK}(q)\) 映射到末端位置/姿态。回看 §7.2 决策二的 cost buffer 代码,第一行就是 ee_pos = forward_kinematics(state_batch[..., :7])——这个 FK 不是算一次,而是要对**整个 \((K, H)\) 批量**算:\(K\) 条轨迹 × \(H\) 个时间步,每个都要做一次 FK。\(K=1000\)、\(H=30\) 就是 3 万次 FK,每个控制周期都要重算。
这就是关节空间相对任务空间的"账":任务空间 MPC 直接在末端位置上规划,代价不需要 FK(但代价是看不见关节约束,§7.2 动机);关节空间 MPC 能看见关节约束,但每次算任务空间代价都得过一遍 FK。关节空间用"额外的 FK 计算"换来了"能直接表达关节约束"。
本质洞察:这个 FK 成本恰恰解释了为什么张量化(§7.2 决策一)对关节空间 MPC 是天作之合——FK 是一串矩阵乘法(每个关节一个变换矩阵,链式相乘),而矩阵乘正是 GPU/张量框架最擅长、最容易批量并行的运算。把 \((K,H)\) 个 FK 写成批量张量操作(
torch.bmm之类),GPU 一次并行算完 3 万次 FK,几乎不费时间。换句话说,关节空间采样 MPC 的"隐藏成本"(大量 FK),恰好被张量化的"核心优势"(批量矩阵乘并行)抵消了。这不是巧合,而是 STORM 选择"关节空间 + 张量化"这套组合的内在逻辑:关节空间带来 FK 负担,张量化正好高效消化这个负担,于是你既拿到了关节空间的约束表达能力、又没真正付出 FK 的时间代价。理解了这层配合,你就明白 STORM 的两个决策(关节空间 + 张量化)不是各自独立的选择,而是相互成就的一对。
实践含义:用关节空间采样 MPC 时,别忘了把 FK(以及任何任务空间代价的计算)也写成批量张量操作,和 rollout 一起在 GPU 上批量算——如果你把 FK 写成对每条轨迹的 Python 循环(像 §7.2 张量化反例那样),FK 就会成为新的瓶颈,把关节空间的好处吃掉。FK 的批量化和 rollout 的批量化同等重要。
与 MoveIt STOMP / CHOMP 的对照:采样式轨迹优化的代际差¶
你的前置课程里可能接触过 MoveIt 的运动规划,里面有 STOMP(Stochastic Trajectory Optimization for Motion Planning)和 CHOMP。它们和 STORM 是什么关系?
STOMP 本身也是采样式轨迹优化——它对一条轨迹加随机扰动、按代价加权更新,思想和 MPPI 同源(都源自路径积分类方法,见第 1 章脉络)。但 STOMP 是**离线规划器**(一次规划好整条轨迹)、跑在 CPU 上、用**固定的高斯噪声**。STORM 把这套思想升级成:在线 MPC(每周期重规划,反应式)、跑在 GPU 上(张量化批量 rollout)、用**相关噪声做平滑**。
| 维度 | STOMP/CHOMP(MoveIt,离线规划) | STORM(在线 MPC) |
|---|---|---|
| 时机 | 离线一次规划整条轨迹 | 在线每周期重规划(反应式) |
| 平台 | CPU | GPU(张量化) |
| 噪声 | 固定高斯 | 相关噪声平滑(第3章思想) |
| 对动态环境 | 差(规划完就固定) | 好(目标/障碍可实时变) |
| 典型用途 | 静态场景的取放路径 | 跟踪移动目标、动态避障 |
不要把"STORM 比 STOMP 新"理解成"STORM 全面取代 STOMP"。离线规划在很多结构化、静态的取放任务里又快又稳,没必要上 GPU MPC。STORM 的价值是在**动态、反应式**场景——这又回到第 2 章 §2.4 和后面 §7.6 反复强调的选型思维:方法没有绝对优劣,只有匹配不匹配场景。
⚠️ 常见陷阱¶
陷阱 1:张量化就等于"自动快",不管批量形状 ⚠️ 编程陷阱
- 错误想法:"用了 PyTorch、丢到 GPU 上,rollout 自然就并行快了。"
- 现象/后果:你的 rollout 在时域 \(H\) 上写了 Python for t in range(H) 循环、每步只处理一个时间片,GPU 利用率极低,比预期慢 10 倍以上。
- 根本原因:张量化的加速来自**让 \(K\) 这个批量维一次性并行**。时域 \(H\) 上的递推(\(x_{t+1}=f(x_t,u_t)\))有数据依赖、必须串行,但每一步内部对 \(K\) 条轨迹的计算必须是批量张量操作。如果你连 \(K\) 维都用 Python 循环展开,就完全没有并行,PyTorch 的开销反而拖慢你。
- 正确做法:rollout 写成"时域 \(H\) 串行循环,每步对 \((K, \cdot)\) 做批量张量运算"。检查方法:rollout 里应该只有一层关于 \(t\) 的 Python 循环,循环体内全是 batched tensor 操作,不应出现任何关于 \(k\)(样本)的 Python 循环。
- 联系第 2 章 §2.2:这其实就是"线程映射——谁算哪条轨迹"的张量化版本。CUDA 里是 \(K\) 个线程并行;张量化里是张量的第 0 维(批量维)并行。本质一样。
陷阱 2:多目标代价权重不做量纲归一,导致某一项"吃掉"全部 ⚠️ 编程陷阱 - 错误想法:"把各项代价加起来就行,权重凭感觉设。" - 现象/后果:到达代价的量级是米²(1)、避碰代价的量级是米(0.01)、关节限位代价量级可能是弧度²(~10),不归一就加,结果某一项(比如限位)的数值大到压倒一切,机器人只顾不超限位、完全不往目标走。 - 根本原因:不同代价项的物理量纲和典型数值范围差异巨大,直接相加等于隐式地给了大数值项极高的权重。 - 正确做法:先把每项代价归一化到可比的量级(除以各自的典型尺度),再用权重调相对重要性。调参时一项一项加:先只开到达,确认能到目标;再加避碰,确认会绕障;逐项叠加并观察。自检:打印每条 rollout 各代价分项的数值范围,确认没有某项数量级碾压其他项(呼应第 5 章故障排查里"代价项失衡"的思路)。
陷阱 3:在关节空间规划,却忘了把 forward kinematics 也放进 rollout ⚠️ 概念误区
- 错误想法:"我在关节空间采控制、rollout 关节轨迹就够了,避碰代价直接在关节上算。"
- 现象/后果:避碰、到达这些代价其实关心的是**末端/连杆在笛卡尔空间的位置**,但你只有关节轨迹,于是要么算错、要么漏算,机器人撞上障碍。
- 根本原因:关节空间规划不代表所有代价都在关节空间算。到达目标、避碰是笛卡尔空间的,必须先用正运动学(FK)把关节轨迹映射到笛卡尔空间,再算这些代价。
- 正确做法:在 rollout 里(或代价计算里)显式调用 batched forward kinematics 把 \((K,H)\) 的关节轨迹转成末端/连杆的笛卡尔轨迹(见上面代码第 1 项的 forward_kinematics),再在笛卡尔空间算到达和避碰。自检:确认代价函数里对每个"笛卡尔空间目标"都有对应的 FK 调用。
陷阱 4:把 STORM 的成功简单归因于"上了 GPU",于是只想着堆算力 ⚠️ 思维陷阱 - 错误想法:"STORM 强是因为它用 GPU 把采样并行了,那我要复现/超越它,核心就是搞更强的显卡、采更多样本。" - 现象/后果:你配了更强的 GPU、把样本数翻了几倍,性能却没有相应提升——因为你忽略了 STORM 真正的两个关键贡献:在关节空间建模(才能直接表达关节限位/奇异/自碰约束,§动机)和**多目标代价缓冲**(才能把避碰/平滑/限位等组织好,决策二)。光堆算力,问题表述本身没改好,提升有限。 - 根本原因:把"工程加速手段"(GPU 张量化)误当成"方法的本质贡献"。GPU 让 STORM 跑得**快**(实时可行),但让它**好用**的是问题表述——在哪个空间规划、代价怎么组织。这正是本章反复出现的母题:采样 MPC 的成败更多取决于"问题怎么表述、仿真和代价对不对",而非优化器/算力本身(§7.3 会把这点讲透)。 - 正确做法:评估或改进一个采样 MPC 系统时,先问"它的问题表述对不对"(规划空间、代价设计、动作空间),再问"算力够不够"。提升性能的杠杆顺序应该是:先把问题表述和代价调对,再考虑加算力/换更聪明的采样。自检:如果加倍样本数带来的提升很小,说明瓶颈不在采样量,而在问题表述或仿真/代价——别再堆算力,回头查表述。
练习¶
- (实现) 给上面的
StormCostBuffer加一道"姿态对齐"代价——要求末端执行器的朝向对准目标朝向(用旋转矩阵或四元数的距离)。写出这一项的批量张量实现,注意它也要是纯求值、批量化的。 - (调试·联系陷阱 2) 给你一组失衡的权重,机器人只顾避碰不到目标。描述你的诊断流程:怎么定位是哪一项在"吃掉"代价、怎么逐项重新平衡。
- (对比·设计) 同样是"多目标 + 可插拔",STORM 用 PyTorch cost buffer、Nav2(第 4 章 §4.5)用 C++ critic 插件。如果你要做一个"研究阶段快速试代价、成熟后部署到 ROS2"的项目,你会怎么设计这两个阶段的代价架构、怎么从前者迁移到后者?
- (综合·联系第 2 章) STORM(张量化)和 MPPI-Generic(手写 CUDA,第 2 章 §2.3)各自适合什么场景?假设你要在 Jetson(嵌入式 GPU,算力有限)上部署一个延迟敏感的反应式控制器,你倾向哪个?为什么?
§7.3 MJPC 的腿足范式:好仿真器 + 极简采样器 ⭐⭐¶
动机¶
§7.2 的 STORM 还用了相关噪声、多目标代价等一系列工程设计。现在我们走向另一个极端,问一个挑衅性的问题:采样器可以简单到什么程度,还能控制一个人形机器人?
MJPC(MuJoCo MPC)给出的答案近乎反直觉地极端:用**最简单的采样器**(Predictive Sampling,你在第 4 章 §4.4 已经吃透——它甚至比精英平均还粗暴,只保留当前最优样本)+ 一个好仿真器(MuJoCo)+ 一个设计得当的代价,就足以让人形机器人从地上站起来、让四足机器人走起来——不需要 gait scheduler、不需要 WBC、不需要 RL 训练。这个结果对整个腿足控制领域是个不小的冲击,也是 §7.1 论点最有说服力的证据之一。
如果不这样做会怎样:被"必须复杂"的成见困住¶
在 MJPC 之前(也包括现在很多人的默认认知),让人形机器人站起来/走路被认为是个**需要重武器**的任务:要么一套精心设计的全身控制器(WBC + gait scheduler + 状态机),要么一个用海量仿真数据训出来的 RL 策略。这个成见有道理——人形机器人高维、欠驱动、强非线性、接触复杂,直觉上确实该很难。
如果你被这个成见困住,面对"让机器人站起来"你会直接去搭 WBC 或训 RL,投入数周甚至数月。而 MJPC 告诉你:先别急着上重武器,试试"好仿真器 + 极简采样器 + 几项代价"——很多任务它已经够用,而且当天就能看到机器人动起来,迭代极快。即便最终要上 WBC/RL,先用 MJPC 快速验证任务可行性、调代价、建立直觉,也能省下大量弯路。理解这一点,能帮你避免"用牛刀杀鸡"的过度工程。
历史¶
MJPC 出自 Howell、Gileadi、Tunyasuvunakool、Zakka、Erez、Tassa(Stanford / DeepMind),2022 年开源(arXiv:2212.00541),是一个**开源、交互式**的实时预测控制框架。它支持三个 shooting-based planner:两个基于导数的——iLQG 和 Gradient Descent,一个无导数的——Predictive Sampling。
论文有句很坦诚的话值得记住:Predictive Sampling 当初是作为一个"基础对照(elementary baseline)"设计的,主要为了教学价值,却意外地和那些更成熟的算法(iLQG)打得有来有回。 这正是第 4 章 §4.4 的核心结论"极简采样器为何意外地有效"在腿足舞台上的再次印证。论文还明确说自己"不提出算法创新,而是优先做高性能实现、简洁代码、和让基于模型的方法更易用"——它的贡献是**工程与可及性**,不是新算法。
理论与设计:把 §4.4 的内核搬到腿足上¶
Predictive Sampling 的算法本身第 4 章 §4.4 已经讲透,这里不重复(回顾:它每周期采若干条控制序列,只保留代价最低的那条作为新名义序列,下周期在它附近继续采——是 CEM 取精英数 = 1 的极端,或者说"贪心采样")。本节聚焦它在腿足上的**应用层设计**,三个要点。
要点一:代价用"残差(residual)+ 范数 + 权重"组织。 MuJoCo MPC 的代价不是直接写一个标量函数,而是定义一组**残差** \(r_i\)(每个残差是"当前状态离目标状态的偏差",比如头部高度偏离站立高度的量),每个残差配一个范数(如二次范数)和权重,总代价是各项加权范数之和。以人形站立任务(mjpc/tasks/humanoid/stand/)为例,核心残差只有三项:
<!-- 人形站立任务的代价残差(示意, 真实 task.xml 结构) -->
<!-- 残差1: 头部高度——离站立目标高度越远代价越高(驱动它站起来) -->
<residual name="head_height" .../>
<!-- 残差2: 平衡——躯干姿态/角速度偏离竖直越多代价越高(别摔) -->
<residual name="balance" .../>
<!-- 残差3: 控制惩罚——抑制过大的控制(省力、防剧烈动作) -->
<residual name="control" .../>
仅仅这三项——站立高度、平衡、控制惩罚——Predictive Sampling 就能让一个几十自由度的人形从地面站起来。没有人去编排"先撑手、再屈膝、再起身"的动作序列,这套站起来的复杂协调动作,是从"让头部到达站立高度且别摔"这个简单代价里**涌现**出来的。这正是 §7.1 讲的"接触序列/复杂协调从代价里涌现,无需预设"的最直观例证。四足行走任务(mjpc/tasks/quadruped/)也是同样简洁的设计——给个前进速度目标 + 平衡 + 控制惩罚,步态自己涌现。
到底有多简单?把 Predictive Sampling 控制人形站立的整个回路写成伪代码,你会惊讶于它的朴素(算法细节第 4 章 §4.4 已讲,这里只看它应用到腿足时的骨架):
算法: Predictive Sampling 控制人形站立(应用骨架)
输入: 当前状态 x₀, 上周期名义控制曲线 θ_prev(样条节点参数), 样本数 K
输出: 当前要执行的控制 u₀
θ ← shift(θ_prev) # warm-start: 上周期解平移做初值(§7.4 钥匙③)
best_θ, best_J ← θ, rollout_cost(x₀, θ) # 先评估名义曲线自己
for k = 1..K (并行): # 在名义曲线附近采 K 条扰动曲线
θ_k = θ + ε_k, ε_k ~ N(0, Σ) # 扰动样条节点
U_k = spline_interp(θ_k) # 节点插值成完整控制序列(§要点二降维)
J_k = rollout_MuJoCo(x₀, U_k) # MuJoCo 前向仿真算代价(三项残差之和)
if J_k < best_J: # ★ 唯一的"更新": 只保留迄今最优的那条
best_θ, best_J ← θ_k, J_k
θ ← best_θ # 名义曲线 = 最优样本(无加权、无精英平均)
execute(u₀ = spline_interp(θ)[0]) # 只执行第一格, 下周期重来(滚动时域)
看那个加 ★ 的行——这就是 Predictive Sampling 的全部"智慧":采一批、谁最低就用谁。没有 MPPI 的指数加权(第 2 章),没有 CEM 的精英平均(第 4 章),就是个"贪心地保留当前最优样本"的操作(第 4 章 §4.4 说它是 CEM 取精英数 = 1 的极端)。可就是这么个朴素到几乎不像"算法"的东西,配上 MuJoCo 这个好仿真器和三项残差代价,让几十自由度的人形从地上站了起来。这把 §7.3 的核心论点钉死在了代码上——复杂的不是采样器(它简单到一行更新),复杂的协调行为来自"好仿真 + 合理代价 + 让动作涌现"。
要点二:控制用样条参数化降维。 MJPC 不对时域里每一步都独立采一个控制,而是用**时间索引的样条**(支持零阶/线性/三次插值)表示整条控制曲线——只在少数样条节点上采值,节点之间插值。这把决策维度从"\(H\) 步 × \(n_u\) 维"降到"少数节点 × \(n_u\) 维",是高维控制里的关键降维技巧。你应该觉得眼熟——第 5 章 §5.3 的 DIAL-MPC 用 Hnode=5 参数化 25 步轨迹,是同一个技巧;§7.4 会把这件事讲成"治维度灾难的第一把钥匙"。
要点三:跑在多核 CPU 上,靠线程池并行 rollout——不是 GPU。 这是一个容易被想当然搞错、但很重要的事实:MJPC 的并行**用的是多核 CPU**(论文在多核 CPU 工作站上运行),通过异步线程池把 \(K\) 条 rollout 分到各 CPU 核上并行跑 MuJoCo 仿真,不依赖 GPU。这和第 2 章 MPPI-Generic、§7.2 STORM 的 GPU 路线是不同的工程选择。原因是 MuJoCo 的单条仿真本身在 CPU 上就很快,且接触动力学的串行性使得单条 rollout 不容易在 GPU 单线程上加速;MJPC 选择"用 CPU 多核并行许多条 MuJoCo"而非"在 GPU 上并行"。
这个事实有个直接的实践含义:你不一定需要 GPU 才能玩采样式腿足控制。 一台多核工作站就能跑 MJPC,对没有强力显卡的学习者非常友好(本章计算需求里,§7.3 标注的就是"多核 CPU 即可")。当然,要上更大规模并行(如 DIAL-MPC 那种每级 2048 条全阶 rollout × 多级退火),GPU 并行物理仿真(MuJoCo MJX、Brax)才划算——但那是 §7.4 之后的事。
要点四:交互式 GUI——能实时看采样轨迹、施加外力、切换 planner。 MJPC 提供一个交互界面:你能看到当前名义轨迹和采样出的候选轨迹的可视化,能暂停/减速仿真、用鼠标对机器人施加外力扰动看它怎么恢复,还能**实时切换 planner**(Predictive Sampling ↔ iLQG ↔ Gradient Descent)直接对比效果。这个 GUI 是极好的学习工具——本章练习就让你在 GUI 里跑人形站立、对比采样和 iLQG(见练习与延伸阅读)。
深一层:residual 代价怎么写好——MJPC 范式里最该练的功¶
MJPC 把"采样器"做到了极简(Predictive Sampling 一行更新),代价就是它把成败的重担压到了**代价设计**上(§7.3 本质洞察:瓶颈在环境侧)。所以在 MJPC 范式里,真正该练的功不是调采样器,而是**怎么把任务写成好的 residual**。这一小节给三个实战要点,帮你避开 residual 设计里最常见的坑。
要点一:residual 要写成"偏差量",而非"目标值"。 MJPC 的 residual \(r_i\) 的设计哲学是"当前状态离期望有多远",总代价是各 residual 加权范数之和 \(\sum_i w_i \|r_i\|\)。所以每个 residual 应该写成一个**在达到目标时为零**的偏差量(如"头部高度 − 站立高度"、"质心水平速度 − 目标速度"),而不是直接写一个标量目标。这样的好处是:范数 \(\|r_i\|\) 自然在目标处取最小,且各项的"达标"含义统一(都是趋零),权重才好平衡。新手常犯的错是把 residual 写成不规整的标量拼凑,导致"零点"不明确、权重难调。
要点二:量纲必须先归一化,再谈权重(呼应 §7.2 陷阱2)。 不同 residual 的物理量纲天差地别——高度是米(量级 ~1),速度是米/秒(量级 ~1),姿态误差可能是弧度(量级 ~0.1),力矩惩罚可能是牛米平方(量级 ~100)。若不归一化直接加权求和,量级大的项会**隐式霸占**总代价,机器人只顾那一项。正确做法是先把每个 residual 除以它的"典型尺度"(让各项归一到可比量级,如都在 0~1 量级),**再**用权重 \(w_i\) 表达"我有多在乎这一项"。记住:权重应该表达偏好,而不该被迫去抵消量纲差异——量纲的事归一化解决,权重只管偏好。
要点三:分清"shaping 引导"和"硬目标",别用 residual 硬编码过程。 residual 应该描述**你想要的结果**(站立、前进、平衡),而不该硬编码**达成结果的过程**(先抬左脚、再蹬右腿)。后者就退回成了"预设动作序列",扼杀了 §7.1 讲的涌现优势(§7.7 陷阱3 也警告过)。如果某个动作迟迟涌现不出来,正确的做法是加**温和的 shaping residual**(如"鼓励质心朝目标方向移动"这种方向性引导),而不是硬写"第 k 步必须处于某姿态"。shaping 是给优化指个大方向,硬编码是替优化把活干了——前者保留涌现,后者杀死涌现。
本质洞察:MJPC 范式把"控制的智慧"从优化器转移到了代价设计,所以**在这个范式里,写代价的能力就是控制的能力**。一个会写 residual 的人,能用最朴素的 Predictive Sampling 让机器人做出复杂行为;一个不会写的人,给他再花哨的优化器也调不出想要的动作。这颠覆了"算法越复杂越强"的直觉——在采样式控制(尤其 MJPC 这种极简采样器)里,代价设计才是那门最该精进、回报最高的手艺。这也是为什么本章反复强调"瓶颈在环境侧":环境侧(仿真 + 代价)才是你该投入精力的地方,而代价设计是其中你最能掌控、最能通过练习提升的部分。
把这三个要点记住,下次你在 MJPC(或任何采样式控制)里调任务,就有了章法:先把目标写成趋零的 residual,归一化各项量纲,用权重表达偏好,动作涌现不出来时加温和 shaping 而非硬编码过程。这套方法论比任何"调采样器"的技巧都更决定你能不能让机器人做出想要的行为。
多视角:MJPC 的在线优化 vs RL 的离线策略¶
让人形站起来,MJPC(在线采样优化)和 RL(离线训练策略)都能做到,但路径截然不同——这个对照能帮你看清两类方法的本质区别(也呼应第 6 章 §6 把 TD-MPC 放进 model↔data 光谱的视角)。
RL 在离线阶段用海量仿真数据训练一个神经网络策略 \(\pi(a|s)\),把"什么状态该做什么动作"**摊销(amortize)**进网络权重;部署时只需前向推理一次网络,极快,但训练贵、且策略对训练分布外的状态可能失效。
MJPC 不训练任何东西,每个控制周期**在线重新优化**——当场采一批轨迹、选最优、执行第一步、下周期重来。它不需要训练数据、不需要等训练收敛、换任务只换代价;代价是每步都要花算力做优化(虽然 Predictive Sampling 很轻),且没有把经验沉淀成可复用的策略。
| 维度 | RL 策略(离线摊销) | MJPC(在线优化) |
|---|---|---|
| 离线开销 | 大(训练数周) | 无 |
| 在线开销 | 极小(一次网络前向) | 中(每步采样优化) |
| 换任务 | 常需重训 | 改代价即可 |
| 对 OOD 状态 | 可能失效 | 当场优化, 较鲁棒 |
| 沉淀经验 | 有(策略网络) | 无(每步从头) |
像的地方:两者最终都能产生让机器人站起来的控制。不像的地方(边界):一个把"思考"放在离线、用网络记住答案;一个把"思考"放在在线、每步现算。这也预示了第 6 章 TD-MPC 和后续混合架构(§7.6)的思路——把两者结合:用 RL 学个策略当 MPPI 的先验/初值(amortize 一部分),再用在线采样优化兜底(保鲁棒)。MJPC 自己是纯在线的一端,理解它,你才能体会为什么"在线优化 + 学习先验"的混合会是趋势。
本质洞察:MJPC 最深的启示不在于"采样器可以很简单",而在于**它把"控制一个复杂系统"的瓶颈,从"优化器够不够聪明"重新定位到了"仿真器够不够好、代价写得对不对"。** Predictive Sampling 这么笨的采样器都能让人形站起来,说明在很多任务上,优化算法的复杂度并不是关键瓶颈——好的物理仿真(MuJoCo)和合理的代价设计才是。这和第 4 章 §4.4 Predictive Sampling 的结论、第 5 章 §5.3 "突破来自工程时机(GPU 并行仿真)而非单步算法"是同一个母题的不同侧面:采样式控制的成败,越来越取决于仿真和代价这两个"环境侧"因素,而非优化器本身的精巧。 这个认知会改变你的精力分配——遇到任务不work,先去查仿真保真度和代价设计,而不是急着换一个更花哨的优化器。
MJPC 的三个 planner:一个微缩的"梯度 vs 采样"选型现场¶
MJPC 不只提供 Predictive Sampling,它内置了三个可实时切换的 planner,正好覆盖了"梯度↔采样"谱上的三个点——这其实是 §7.6 选型思想在一个框架内部的微缩版,值得拿出来对照,它能帮你在动手用 MJPC 时知道何时切到哪个。
三个 planner 是:
Predictive Sampling(无导数采样):本节主角,采一批控制、谁代价低用谁。不需要导数,所以能碰接触、碰任何 MuJoCo 能仿真的东西,鲁棒、好理解;代价是样本效率不如梯度法,精度受样本数限。适合接触丰富、不可微、或你想快速验证可行性的任务。
Gradient Descent(一阶梯度):对代价用(数值/解析)梯度做一阶下降。用导数,所以在光滑问题上比纯采样高效;但遇到接触等不可微就会受 §7.1 讲的那些罪(梯度病态/失效)。适合动力学光滑、要比采样更高效收敛的任务。
iLQG(二阶,迭代 LQR 的高斯变体):用动力学的一阶/二阶信息做类牛顿迭代,是三者里**收敛最快、精度最高**的——在光滑问题上几步就到很好的解;但它最依赖可微性和良好的动力学线性化,接触一多就最先失灵,也最复杂。适合光滑、要高精度高效率的任务。
本质洞察:MJPC 把这三个 planner 放进同一个框架、还支持实时切换,本身就是一个深刻的设计表态——它承认没有一个 planner 通吃,让你按任务在"采样(鲁棒、能碰接触、效率低)↔ 梯度(高效、高精度、怕不可微)"谱上自己选。这正是 §7.6 互补选型在单个工具内部的体现:Predictive Sampling 在谱的采样端、iLQG 在梯度端、Gradient Descent 在中间。你在 MJPC 的 GUI 里实时切换这三个、看同一个任务下它们的表现差异,其实就是在亲手做 §7.1 和 §7.6 的实验——光滑任务上 iLQG 又快又准、采样显得笨;一加接触/不可微,iLQG 立刻露怯、采样稳稳работает。这是理解"梯度 vs 采样"最好的互动实验场,比读任何文字都直观。所以用 MJPC 时,别只用默认的 Predictive Sampling——主动在三个 planner 间切换对比,你对本章核心分野的理解会深一个层次。
实践建议:在 MJPC 里上手一个新任务,可以先用 iLQG 试(光滑任务它最快),不行(接触多、不收敛)再切 Predictive Sampling(鲁棒兜底)——这个"先试梯度、不行退采样"的顺序,本身就是 §7.6 选型框架的一次实战演练。
深一层:MJPC 为什么跑在 CPU 上,而不是 GPU¶
这是个常让人困惑的设计选择——本章前面(和陷阱1)反复强调"MJPC 跑多核 CPU,不是 GPU",但同样是采样式控制,STORM(§7.2)和 DIAL-MPC(第 5 章)都用 GPU。为什么 MJPC 偏偏选 CPU?理解这个选择背后的权衡,能让你以后自己判断"一个采样 MPC 该上 CPU 还是 GPU"。
根子在于 rollout 的计算特性,而 rollout 的特性又取决于动力学里**接触求解**这一步。回想 §7.1:采样法的 rollout 要让仿真器"穿过接触"。MuJoCo 算接触不是简单的前向公式,而是**每个时间步解一个凸优化问题**(求解满足接触约束、摩擦锥的接触力)——这是个带迭代的、逻辑分支较多的过程。这种计算和 GPU 擅长的"大规模、整齐划一的浮点运算"不太合拍:GPU 的威力在于成千上万个线程做**完全相同**的简单操作(SIMT),而接触求解的迭代次数、分支走向因状态而异,线程之间不整齐,GPU 的并行效率会大打折扣(线程发散)。
相比之下,多核 CPU 处理这种"每条 rollout 内部有复杂分支逻辑、但 rollout 之间相互独立"的负载反而更自如:开 \(K\) 个线程,每个核认领一条 rollout,各自跑自己的接触求解,互不干扰。MJPC 论文在多核 CPU 工作站上运行,几百个样本分到各核上,每个核串行跑几条——对几十赫兹的控制频率,这个并行度够用了。
把几个采样 MPC 的平台选择放在一起,权衡就清楚了:
| 系统 | 平台 | 为什么这样选 |
|---|---|---|
| MJPC | 多核 CPU | rollout 含复杂接触求解(分支多),CPU 处理不规则负载更自如;样本数适中(几百),几十核够用;易调试、易部署 |
| STORM | GPU(张量化) | 机械臂动力学相对规整、接触少,rollout 适合张量化批量;要几千样本,GPU 吞吐高 |
| DIAL-MPC | GPU(MJX/Brax) | 用 GPU 版物理(MJX),几千样本×多退火级,需要 GPU 的大规模吞吐;接受为此调适接触仿真 |
本质洞察:采样 MPC 选 CPU 还是 GPU,核心看 rollout 的计算"长什么样"——rollout 规整(少分支、整齐的浮点运算、超多样本)就适合 GPU 的吞吐;rollout 不规整(接触求解这类多分支、迭代不定的逻辑,样本数适中)就适合 CPU 的灵活。这不是"GPU 一定比 CPU 强"的简单问题,而是"负载特性和硬件特性是否匹配"。MJPC 选 CPU 不是落后,而是清醒地认识到它的 rollout(重接触求解)和 CPU 更配,外加 CPU 方案开发调试部署都更省事。这条判断(按负载特性选硬件,而非迷信 GPU)在你自己搭采样 MPC 时极其实用——先看你的 rollout 重不重接触、要多少样本,再决定硬件,别一上来就假定"必须上 GPU"。
顺带说一句,这也呼应了 §7.6 即将讲的选型思维:没有放之四海皆准的"最优"选择,关键是让方案的特性匹配问题的特性。硬件选择如此,方法选择(采样 vs 梯度)亦如此。
易错点:MJPC 的惊艳大多在仿真里¶
要主动点破一个容易被宣传效果带偏的点。MJPC 让人形站起来、四足走起来的演示确实惊艳,但这些**大多是在 MuJoCo 仿真里**完成的。MJPC 论文自己明确指出一条根本限制:"你只能控制 MuJoCo 能仿真的东西"(you can only control what MuJoCo can simulate),而且这个限制比一般的预测控制更强——因为采样法把全部信任押在了仿真器上(§7.1 易错点已埋下这个伏笔)。
这意味着:MJPC 的范式在**仿真任务、或仿真和真机差距小的任务**上特别强;但要把它的成果搬到真机,仍要面对 sim-to-real gap(仿真的接触、摩擦、电机模型和真机的差异)。MJPC 本身**不解决 sim-to-real**——它是个"在给定仿真器里做实时控制"的框架。真机部署需要额外的仿真校准、或选择对模型误差更鲁棒的动作空间(§7.5)。别看了演示就以为"采样式控制已经解决了人形机器人"——它解决的是"在好仿真里实时控制",真机这一步还有专门的功夫要下。
⚠️ 常见陷阱¶
陷阱 1:以为 MJPC 用 GPU,照着配显卡却发现用不上 ⚠️ 概念误区 - 错误想法:"采样式控制要并行几千条 rollout,肯定要 GPU,MJPC 也不例外。" - 现象/后果:你花钱配了显卡跑 MJPC,发现 GPU 占用是 0、瓶颈全在 CPU;或者在一台单核机器上跑 MJPC 慢得没法用,却怪 MJPC 不行。 - 根本原因:MJPC 的并行是**多核 CPU 线程池**,不是 GPU(本节要点三)。它的性能取决于 CPU 核数,不是显卡。 - 正确做法:跑 MJPC 优先准备**多核 CPU**(核越多并行的 rollout 越多)。想要 GPU 大规模并行物理仿真,得用 MuJoCo MJX / Brax 那条路线(如 DIAL-MPC),那是另一套技术栈。自检:跑 MJPC 时看 CPU 各核占用应该都被吃满,GPU 应该闲着。
陷阱 2:把仿真里的成功直接当成真机能行 ⚠️ 思维陷阱 - 错误想法:"MJPC 在仿真里让人形站起来了,那部署到真机也差不多能站。" - 现象/后果:真机上机器人动作变形、抖动、摔倒,和仿真表现差很远,浪费大量真机调试时间。 - 根本原因:忽略了 sim-to-real gap(本节易错点)。仿真的接触/摩擦/电机模型与真机有差异,采样法又把全部信任押在仿真上,差异会被直接传导到真机行为。 - 正确做法:把"仿真成功"当成"任务可行性验证 + 代价设计验证",而非"真机就绪"。真机前做仿真校准(辨识真机的摩擦、惯量、延迟),并优先用对模型误差鲁棒的动作空间(§7.5 的位置目标 + PD)。把真机部署当成一个独立的、需要专门下功夫的阶段。
陷阱 3:残差代价写得太多太细,反而调不出来 ⚠️ 编程陷阱 - 错误想法:"要让机器人动作漂亮,得把每个细节都写进代价——这个关节别太弯、那个脚尖要绷直、重心要在某条线上……" - 现象/后果:代价项一多,各项权重互相牵制、调参维度爆炸,机器人动作僵硬或根本优化不动;而且过度约束扼杀了"涌现"——你把本该让优化自己发现的协调,硬塞成了一堆约束。 - 根本原因:违背了 MJPC 范式的精髓——少而精的代价 + 让复杂行为涌现。人形站立只用三项代价就够,不是偶然,是因为过多约束反而限制了优化的自由度。 - 正确做法:从最少的核心代价项起步(任务目标 + 平衡/安全 + 控制惩罚),跑起来看涌现的行为,只在确实需要修正某个具体问题时才**谨慎地**加一项。每加一项都问:"这是必须的吗?能不能让优化自己发现?"自检:如果你的代价项超过 5–6 项还在加,停下来反思是不是过度设计了。
练习¶
- (动手·必做) 安装 MJPC,在 GUI 里运行人形站立(humanoid stand)任务。先用 Predictive Sampling 跑,观察它怎么站起来;再切换到 iLQG,对比两者的行为、收敛速度、稳定性。写一段观察:在这个任务上,简单采样器和 iLQG 谁更好用?哪些情况下采样器反而更稳?
- (动手·代价设计) 在 MJPC 的某个腿足任务里,试着删掉一项代价(比如平衡项),观察机器人行为怎么退化;再试着改一项代价的权重,观察行为变化。总结:哪一项代价对这个任务最关键、为什么。
- (概念·联系第 4 章) Predictive Sampling 是 CEM 取精英数 = 1 的极端(第 4 章 §4.4)。请解释:为什么这么"贪心"的采样器在 MJPC 的腿足任务上够用?它在什么样的任务上会暴露出"太贪心"的缺点(提示:多模态、需要全局探索的任务,联系第 3 章 §3.4 SVG-MPPI)?
- (综合·开放) MJPC 的核心论点是"瓶颈在仿真和代价,不在优化器"。你同意吗?给出一个你认为"优化器复杂度确实是瓶颈"的反例任务,并说明为什么在那个任务上 Predictive Sampling 会不够、需要更聪明的采样器(如退火、Stein 引导)。
§7.4 全身采样 MPC 的维度灾难与三把钥匙 ⭐⭐⭐⭐¶
动机¶
前两节的舞台还相对温和——STORM 是 7 自由度机械臂,MJPC 的惊艳大多在仿真。现在我们走上真正的硬骨头:真实腿足机器人的全身控制。一只四足(如 Unitree Go1/Go2)有 12 个驱动关节,加上浮动基座共 18 个自由度;人形机器人更是 20–30+ 自由度。如果要在 \(H\) 步的时域上为每个关节、每一步都决定一个控制,决策变量是 \(H\times n_u\) 维——动辄几百维。
§7.1 已经埋下伏笔、§7.1 的易错点也明说了:采样法绕开了"接触不可微",但**没有绕开维度灾难**。这一节就专门治维度灾难——它是横在"采样法能碰全身控制"和"采样法能把全身控制做好"之间的最后一道大坎。好消息是,治它的三把钥匙你都见过零件(样条降维在 §7.3 露过脸、退火是第 5 章主线、warm-start 是第 2 章基本功),本节把它们组装成一套完整的"全身采样 MPC 生存工具包",并讲清每把钥匙各治哪一种"灾难"。
如果不这样做会怎样:把 vanilla MPPI 直接丢到全身上¶
假设你不知道这三把钥匙,直接拿第 2 章的 vanilla MPPI——对 \(H\) 步 × 几十个关节的每一维独立采高斯噪声——丢到四足全身上。会发生什么?
回顾第 4 章 §4.2/§4.6 量化过的维度灾难:在 \(D\) 维空间里,独立高斯样本几乎全部落在距均值 \(\sqrt{D}\sigma\) 的**薄壳**上,要在高维里"撒满"有意义的区域,所需样本数随 \(D\) 指数增长。在几百维的控制空间里,你那几千条样本就像往一个巨大的高维球里扔几粒沙子——绝大多数样本都是质量很差的随机抖动,碰巧撞上好轨迹的概率微乎其微。
具体后果:要么机器人原地抽搐(采到的全是高频噪声般的烂控制,没有一条像样的协调动作)、要么干脆找不到能站稳的轨迹直接摔倒。你会以为"采样式全身控制根本不可行"——而这正是 2024 年前的普遍看法,直到 Whole-Body MPPI 和 DIAL-MPC 用这三把钥匙证明它可行。所以这一节治的不是接触,而是"维度"这个采样法的固有软肋。 分清这两件事很重要:接触是采样法的主场(免费赢),维度是采样法的软肋(要靠技巧治)。
理论:三把钥匙,治三种不同的"灾难"¶
很多人把"治维度灾难"笼统当成一件事,其实高维带来的困难有不同的侧面,三把钥匙恰好各打一个。先建立这个分工框架,再逐个拆解。
| 钥匙 | 它降低/利用的是 | 它治的"灾难"侧面 | 出处 |
|---|---|---|---|
| ① 样条节点降维 | 决策变量的**维度本身**(沿时域轴) | 维度太高,空间太大 | §7.3、DIAL-MPC Hnode、WBMPPI knot |
| ② 扩散退火 | 高维空间里的**搜索效率** | 一次撒不满高维空间 | 第 5 章 §5.1–§5.3 |
| ③ warm-start | 相邻控制周期的**时间冗余** | 每周期从零搜太浪费 | 第 2 章 §2.1、第 5 章 §5.3 |
钥匙①:样条节点降维——直接把维度本身降下来。 最直接的对策:不要为时域里每一步都独立决定一个控制,而是用**少数几个样条节点**(knot points)参数化整条控制曲线,节点之间用插值(零阶/线性/三次样条)填充。比如把 \(H=30\) 步的控制曲线用 \(5\) 个三次样条节点表示——决策维度从 \(30\times n_u\) 骤降到 \(5\times n_u\),降了 6 倍。
为什么这合理而不只是"偷懒降维"?因为**好的控制曲线本来就是光滑的**——你不会希望机器人的关节力矩在相邻两步之间剧烈跳变(那既不物理、又会抖动,回顾第 3 章 §3.2 抖动的危害)。既然最优控制曲线天然光滑,用少数样条节点就足以表达它,那些被插值掉的高频自由度本来就是你不想要的。这是 §7.3 提过的技巧(MJPC 的样条控制、DIAL-MPC 的 Hnode=5),CMU Whole-Body MPPI 论文也明确把"用样条节点缩小搜索空间"列为它能在真机上工作的关键设计之一。
钥匙②:扩散退火——让高维里的搜索变高效。 即便降了维,几十维仍然不低。退火(第 5 章主线)解决的是"在这个仍然不低的维度里,怎么用有限样本高效搜索"。回顾第 5 章 §5.3:把"一次在高维里撒满"这个不可能任务,拆成"多级由粗到精"——高温大方差的级负责全局覆盖、跨过非凸地形的局部极小,低温小方差的级负责局部精修。DIAL-MPC 正是靠它(每级 2048 样本 × 4 级退火)在全阶力矩空间上工作的。这把钥匙我不重复推导(第 5 章 §5.1–§5.3 讲得很透),只强调它在"治维度灾难"框架里的角色:它不降低维度,而是提高在给定维度里搜索的效率。
钥匙③:warm-start——利用时间冗余,不每周期从零搜。 回顾第 2 章 §2.1 和第 5 章 §5.3 的"深一层":滚动时域 MPC 相邻周期的最优解高度相似(差别主要是"整体往前挪一格"),所以下周期用上周期解平移做初值(shift),只需在一个已经很接近最优的起点附近微调。这把钥匙治的是**时间维度上的冗余**——你不是在每个控制周期都面对一个全新的高维搜索,而是站在上周期搜索成果的肩膀上。第 5 章那个漂亮的统一视角说得好:整个滚动时域 MPC 是一条跨越无数周期、永不停止的退火长链,warm-start 把上一段的成果传给下一段。
把三把钥匙的分工连起来看:
本质洞察:维度灾难不是一个敌人,而是三个——维度本身太高、高维里一次搜不动、每周期重搜太浪费。全身采样 MPC 的可行,不是靠任何单一招式(尤其不是靠"多撒样本"这个最笨的办法),而是**同时从三个正交的方向夹击**:样条降维砍掉冗余维度(治"维度本身")、退火提高搜索效率(治"搜不动")、warm-start 复用历史(治"重搜浪费")。三者缺一不可——只降维不退火,几十维仍会陷局部;只退火不降维,几百维退火也吃力;不 warm-start,每周期从头退火在实时回路里跑不完。这也解释了为什么 Whole-Body MPPI 和 DIAL-MPC 都同时用了这三招(外加好的 GPU/CPU 并行仿真)。学会把"维度灾难"拆成这三个可分别施策的子问题,比记住任何单一技巧都更有威力——以后遇到任何高维采样问题,你都可以逐一问:能降维吗?能退火吗?能复用历史吗?
一个能跑的对照:降维如何用同样的预算赢¶
用一个最小 demo 演示钥匙①的威力——同样的采样预算下,全维 vanilla MPPI 找不到好解,而样条节点降维能找到。我们设一个需要"绕路"的高维控制任务(非凸,逼着搜索全局),不依赖任何仿真器。把时域取到 60 步,让决策维度高到足以让维度灾难真正显现。先定义这个带"时间窗口障碍"的任务:
import numpy as np
H, dt = 60, 2.0 / 60 # 时域 60 步(决策维度高到让维度灾难真正显现)
x0, target = 0.0, 1.0 # 1D 点质量: 从 0 走到 1
def rollout_cost(U): # U: (..., H) 速度序列, 支持批量
x = np.full(U.shape[:-1], x0); cost = np.zeros(U.shape[:-1])
for t in range(H):
x = x + dt * U[..., t]
if 18 <= t <= 30: # 障碍窗口: 落在[0.3,0.7]区间重罚(非凸, 逼着绕路)
cost += 50.0 * ((x > 0.3) & (x < 0.7))
cost += 100.0 * (x - target) ** 2 # 终端到达
cost += 0.05 * (U ** 2).sum(-1) # 控制能量
return cost
任务定好后,关键是对照两种采样方式:一种对 60 步每一维独立采样(全维,60 维决策),一种只对 8 个样条节点采样、再插值成 60 步(降维,8 维决策)。两者用**完全相同**的样本数和迭代数,唯一的差别就是决策维度。为避免单个随机种子的偶然性,我们统计 20 个种子的成功率(末代价 < 5 视为成功绕过障碍并到达):
def mppi_full(K, iters, seed, lam=1.0, sigma=1.5):
"""全维 MPPI: 对 60 维每一维独立采样。"""
rng = np.random.default_rng(seed); U = np.zeros(H)
for _ in range(iters):
eps = rng.normal(0, sigma, size=(K, H))
J = rollout_cost(U + eps)
w = np.exp(-(J - J.min()) / lam); w /= w.sum()
U = U + (w[:, None] * eps).sum(0)
return float(rollout_cost(U[None])[0])
def mppi_knot(K, iters, seed, n_knot=8, lam=1.0, sigma=1.5):
"""样条节点降维 MPPI: 只对 8 个节点采样, 插值到 60 步。"""
rng = np.random.default_rng(seed)
knot_t = np.linspace(0, H - 1, n_knot); full_t = np.arange(H)
theta = np.zeros(n_knot) # 决策变量只有 8 维!
for _ in range(iters):
eps = rng.normal(0, sigma, size=(K, n_knot))
U = np.array([np.interp(full_t, knot_t, th) for th in (theta + eps)])
J = rollout_cost(U)
w = np.exp(-(J - J.min()) / lam); w /= w.sum()
theta = theta + (w[:, None] * eps).sum(0)
return float(rollout_cost(np.interp(full_t, knot_t, theta)[None])[0])
K, iters, trials = 64, 8, 20 # 同样的偏小预算
sr = lambda a: np.mean([c < 5 for c in a])
full = [mppi_full(K, iters, s) for s in range(trials)]
knot = [mppi_knot(K, iters, s) for s in range(trials)]
print(f"全维 MPPI (60维): 成功率 {sr(full):.0%}, 代价中位数 {np.median(full):.1f}")
print(f"节点降维 MPPI (8维): 成功率 {sr(knot):.0%}, 代价中位数 {np.median(knot):.1f}")
# 典型输出:
# 全维 MPPI (60维): 成功率 0%, 代价中位数 8.6 (几乎从不绕过障碍)
# 节点降维 MPPI (8维): 成功率 75%, 代价中位数 4.8 (多数时候绕过并到达)
这个 demo 演示了钥匙①的核心:同样 \(K=64\)、\(8\) 次迭代的预算下,把决策维度从 60 降到 8,成功率从 0% 跳到 75%。 全维 MPPI 的 64 个样本撒在 60 维空间里太稀,怎么迭代都找不到那条"先压低、绕过障碍窗口、再冲向目标"的窄路,20 个种子全军覆没;节点降维后只在 8 维里搜,同样的样本密度高出几个数量级,多数种子都能找到绕过障碍的解。注意降维也不是 100% 成功(这正是 §7.4 后面"钥匙叠加"要用退火进一步补上的缺口)。把这里的"1D 点质量 60 步"换成"四足 12 关节 × 数十步",把 rollout_cost 换成 MuJoCo 全身 rollout,逻辑完全一样——这就是 Whole-Body MPPI 和 DIAL-MPC 在真机上用样条节点降维的微缩版。
你可以把钥匙②(退火)叠加上去当练习:给 mppi_knot 的 sigma 加一个逐迭代衰减的调度(高→低),并复用第 5 章的多级退火思路,会看到在更难的障碍配置下成功率进一步提升。三把钥匙是可以、也应该叠加使用的。
深一层:降维为什么不会"把好解排除在外"¶
看到样条降维(只用少数几个节点参数化几十步控制),很多人会本能地担心一件事:这么粗的参数化,会不会把最优解给排除掉? 万一最优控制需要几十步各不相同的精细值,少数几个节点根本表达不出来,那降维不就是"为了省搜索、牺牲了最优性"吗?这个担心听起来合理,但其实多虑了——这一小节讲清为什么降维在绝大多数情况下不丢好解,让你用得安心。
关键在于一个事实:好的控制曲线本来就是"光滑的、低频的",而光滑低频的曲线恰好是少数样条节点能很好逼近的。 想想真实机器人的控制——让腿平稳迈步、让手臂顺畅伸展,需要的是连续、渐变的控制信号,而不是每一步都剧烈跳变的高频信号。一条光滑的控制曲线,它的"信息量"本来就不需要几十个独立自由度来承载,少数几个样条节点 + 插值就能把它的形状描出来(就像一条平滑曲线用几个控制点的贝塞尔/样条就能画准)。所以这个低维样条空间里,其实**装得下所有光滑的好解**。
那降维到底排除了什么?它排除的是**高频抖动的控制**——那些每步剧烈跳变、锯齿状的控制曲线。而这些恰恰**不是好解**:高频抖动的控制在真机上要么执行不出来(电机带宽有限)、要么激发振动和冲击、要么浪费能量(§7.4 易错点和有色噪声那段都提过)。换句话说,降维排除掉的,正是你本来就不想要的那部分控制空间。它不是"把好解连同坏解一起砍了",而是"精准地砍掉了坏解所在的高频区域,保留了好解所在的光滑区域"。
本质洞察:样条降维之所以"几乎免费"(大幅缩小搜索空间却不丢好解),是因为它利用了一个关于解的**先验结构**——好控制是光滑的。降维不是盲目地砍维度,而是**沿着"光滑 vs 抖动"这个方向,把搜索限制在"光滑控制"这个低维子空间里**,而最优解本就(几乎总是)落在这个子空间内。这呼应了"第四把钥匙"那段的总原则——用结构换搜索:这里用的结构是"好控制的光滑性",用它把几十维的全控制空间换成几维的光滑控制子空间。所以降维的"安全性"不是运气,而是有依据的——只要你的任务的最优控制确实是光滑的(绝大多数机器人任务如此),降维就不会丢掉它。唯一的例外,是那些最优控制真的需要高频成分的任务(如需要瞬间脉冲式发力的极限动作),这时降维(节点太少)才会丢解——而这正是 §7.4 易错点"降维过头"警告的情形,处理办法是增加节点或在需要高频的时段加密节点。
所以用样条降维时的心法是:相信"好控制是光滑的"这个先验,放心降维;只在确有证据表明任务需要高频控制时,才回头增加节点。 别因为"怕丢最优解"就不敢降维——那等于为了一个几乎不会发生的担心,放弃了治维度灾难最有力的一把钥匙。
钥匙叠加:降维 + 退火,成功率从 75% 到 100%¶
上面只用了钥匙①(降维)。现在把钥匙②(退火)叠上去,在一个**更难的双障碍配置**(控制曲线要先躲过一个窗口、再躲过另一个,非凸更强)下,看两把钥匙叠加的效果。退火就是让采样方差随迭代由大到小——前期大方差全局探索两个窗口之间的通路,后期小方差精修:
def rollout_cost_2win(U):
"""前面 rollout_cost 的双障碍版:把单个障碍窗口改成两个相邻窗口(更非凸)。"""
x = np.full(U.shape[:-1], x0); cost = np.zeros(U.shape[:-1])
for t in range(H):
x = x + dt * U[..., t]
if 8 <= t <= 12: cost += 50.0 * ((x > 0.25) & (x < 0.55)) # 第一道窗口
if 18 <= t <= 22: cost += 50.0 * ((x > 0.55) & (x < 0.85)) # 第二道窗口
cost += 100.0 * (x - target) ** 2 + 0.1 * (U ** 2).sum(-1)
return cost
def mppi_knot_anneal(K, iters, n_knot=5, lam=1.0, sigma=1.0, anneal=None):
"""在 mppi_knot(钥匙①降维)基础上叠加钥匙②退火: anneal 是逐段方差缩放。"""
knot_t = np.linspace(0, H - 1, n_knot); full_t = np.arange(H)
theta = np.zeros(n_knot)
for it in range(iters):
# 退火: 按迭代进度取当前级的方差缩放(高→低), anneal=None 则退化为不退火
scale = anneal[min(it * len(anneal) // iters, len(anneal) - 1)] if anneal else 1.0
eps = rng.normal(0, sigma * scale, size=(K, n_knot))
U = np.array([np.interp(full_t, knot_t, th) for th in (theta + eps)])
J = rollout_cost_2win(U) # 双障碍窗口的更难配置
w = np.exp(-(J - J.min()) / lam); w /= w.sum()
theta = theta + (w[:, None] * eps).sum(0)
return float(rollout_cost_2win(np.interp(full_t, knot_t, theta)[None])[0])
# 在双障碍难配置上, 统计 20 个随机种子的成功率(末代价<5 算成功)
def success_rate(anneal, trials=20):
succ = 0
for s in range(trials):
global rng; rng = np.random.default_rng(s)
if mppi_knot_anneal(64, 30, sigma=1.0, anneal=anneal) < 5.0:
succ += 1
return succ / trials
print(f"只降维(无退火) 成功率: {success_rate(None):.0%}")
print(f"降维 + 退火 成功率: {success_rate([3.0, 1.5, 0.6, 0.3]):.0%}")
# 典型输出:
# 只降维(无退火) 成功率: 75%
# 降维 + 退火 成功率: 100%
结果很清楚:在双障碍这个更非凸的配置上,只用钥匙①(降维)的成功率是 75%——四分之一的种子会陷在某个障碍窗口里出不来;叠加钥匙②(退火)后成功率升到 100%——大方差的早期级帮搜索跨过局部陷阱、看清两窗口之间的通路,小方差的后期级再精修到终点。这就是 §7.4 理论部分讲的"三把钥匙各治一个子问题、叠加才完整"在数字上的印证:降维治"维度本身"(让 64 个样本在 5 维里够密),退火治"搜不动"(让搜索跨过非凸的局部陷阱),缺了退火,再低的维度也会被强非凸卡住四分之一的概率。把这个双障碍想象成腿足里"先迈过一道坎、再跨过一个沟"的连续接触决策,你就懂了为什么 DIAL-MPC、Whole-Body MPPI 这些做高难连续动作的方法都要把降维和退火一起上。
多视角:降维像"调低你要搜索的分辨率"¶
给样条降维配个直觉:它就像**调低你要搜索的图像分辨率**。假设你要在一张图里找一个特定的大致形状(好的控制曲线),全维搜索相当于在 4K 高清图(每像素都是自由度)里逐像素试——像素太多,组合爆炸。样条降维相当于先在缩略图(几个节点)里找到大致形状,因为你要找的形状本来就是"大块的、光滑的",缩略图的分辨率足够表达它,而搜索空间小了好几个量级。
像的地方:都是用"较低分辨率/较少自由度"表达一个本来就不需要高分辨率的目标,从而缩小搜索空间。不像的地方(边界):图像缩略是固定的下采样,而样条节点是可以选位置、选插值阶数的(甚至可以非均匀放节点,把节点密集放在你预期控制变化快的时段)。另外,如果你要找的形状**确实有高频细节**(比如某个需要瞬间脉冲的控制),缩略图就丢了它——对应到控制上,就是样条节点太少会丢掉必需的高频控制(见下面易错点)。所以"调低分辨率"是有代价的,关键是把分辨率调到"刚好够表达你要的控制、又不浪费在噪声上"。
深一层:warm-start 在接触切换处的微妙失效¶
三把钥匙里,钥匙③(warm-start)看起来最无害——"复用上周期的解当这周期的初值",几乎是白捡的加速。但在接触任务里,它藏着一个容易被忽略的陷阱,理解它能帮你避开真实全身控制里一类难查的 bug。
warm-start 之所以有效,依赖一个隐含假设:相邻控制周期的最优解是接近的(所以上周期的解平移一下就是这周期的好初值)。在光滑的、接触状态稳定的时段,这个假设成立——机器人正在稳定地迈步或站立,控制曲线缓慢变化,warm-start 提供的初值很好。
但在**接触状态切换的瞬间**(脚刚落地、刚离地、手刚碰到物体),这个假设可能突然失效。原因是:上周期的最优解是在"旧接触状态"下算出来的(比如脚还悬空,最优控制是"继续摆腿");而这周期接触状态变了(脚落地了),最优控制可能截然不同(变成"用这条腿支撑发力")。这时把旧解平移过来当初值,给的可能是一个**针对旧接触状态的、现在已经不合适甚至有害的**起点——采样从这个坏初值附近撒,可能要花好几个周期才能"爬"到新接触状态对应的好解,期间控制质量下降,表现为接触切换瞬间的动作卡顿或抖动。
本质洞察:warm-start 的本质是"赌相邻周期的解连续",而接触切换正是控制解可能**不连续**的地方——这个赌在接触切换处可能输。这揭示了一个更普遍的道理:任何依赖"时间连续性"的加速技巧(warm-start、热启动、增量求解),在系统状态发生离散切换的地方都要格外小心。 接触切换是腿足系统里最典型的离散切换点,所以它是 warm-start 假设最容易破裂的地方。这也和 §7.6 混合架构里"控制器切换瞬间要重置 warm-start"(
_handoff)是同一类问题——切换点是连续性假设的天敌。
实践中怎么处理?几种思路:一是**检测到接触切换时,适当加宽该周期的采样方差**(不完全信任 warm-start 初值,给采样更大的探索范围去找新接触状态的解);二是**接触切换时部分重置 warm-start**(保留高层意图、重置接触相关的控制分量);三是接受它、靠**高频重规划**硬扛(重规划够快的话,几个周期就能爬到新解,卡顿不明显)——这也是为什么实时性(高重规划频率)对接触丰富任务尤其重要。多数系统用第三种(够快就行),但知道前两种能帮你在卡顿明显时对症下药。重点是:别把 warm-start 当成无脑白捡的加速,要知道它在接触切换处会打折扣,这样遇到"接触切换瞬间抖动"的现象,你能立刻想到 warm-start 这个嫌疑点,而不是漫无目的地乱调。
有没有"第四把钥匙"?——三把之外的进阶手段及其定位¶
讲完三把钥匙,一个自然的问题是:治维度灾难就这三把吗?还有没有别的?答案是:有一些**进阶手段**能进一步提升高维采样的效率,但它们和三把钥匙的地位不同——三把钥匙是真实全身控制几乎**必需**的(缺了对应子问题就栽),而这些进阶手段是**锦上添花**(用了更好,不用也能 work)。讲清这个区别,能让你既不忽视进阶手段,也不把它们误当成和三把钥匙同等的必需品。
几个常见的进阶手段:
自适应协方差(第 4 章 CoVO-MPC 等)。 三把钥匙里的采样方差是固定或按退火调度的;更进阶的做法是让协方差**自适应**——根据当前的代价地形(比如用样本的二阶统计量估计局部曲率)动态调整每个维度的采样方差,在"敏感方向"采得细、"不敏感方向"采得粗。这能进一步提升样本效率,但实现更复杂、也更容易不稳定,属于"调好了有提升、调不好反添乱"的进阶选项。
有色噪声 / 平滑采样(第 3 章 §3.2)。 默认的高斯白噪声采样会产生抖动的控制曲线(高频成分多),在高维全身上既浪费样本(很多抖动样本一看就不可行)、又可能激发不想要的高频动作。用时间相关的有色噪声(或在采样后做平滑)让采样出的控制曲线天生更光滑,等于把采样预算集中在"光滑的、更可能可行的"控制子空间里。这和样条降维(钥匙①)的目标相近(都偏向光滑控制),常和它配合用。
学习提议分布 / learned prior(§7.6 思路三、第 6 章)。 最强的进阶方向:用 RL/扩散策略学一个"好动作大概长什么样"的先验,当采样的提议分布或初值,把一部分"该往哪搜"的知识从在线摊销到离线。这能极大提升高维采样的效率(不再从零盲搜),但代价是要训练、要数据,且引入了 sim-to-real 的策略迁移问题。
本质洞察:把三把钥匙和这些进阶手段放一起看,能提炼出一个**对抗维度灾难的总原则——用"结构"换"搜索"。三把钥匙各自利用了一种结构:样条降维利用"好控制是光滑的"(结构①,砍维度)、退火利用"由粗到精能跨非凸"(结构②,导搜索)、warm-start 利用"相邻周期解连续"(结构③,省重搜)。进阶手段则利用更多结构:自适应协方差利用"代价地形的局部曲率"、有色噪声利用"控制的时间平滑性"、learned prior 利用"数据里的先验知识"。**所有这些手段,本质都是在问:这个高维问题里有什么结构可以利用,从而不必在整个高维空间里盲目搜索? 维度灾难的根源是"高维空间太大、盲搜搜不动",而一切解药都是"找到结构、用结构缩小实际要搜的空间"。理解了这个总原则,你遇到任何高维采样问题,都会本能地先问"这里有什么结构能利用",而不是无脑加样本——这比记住具体哪三把钥匙更根本,因为它告诉你**为什么**这些钥匙有效、以及**怎么找**新的钥匙。
所以回答开头的问题:三把钥匙是必需的地基,进阶手段是在地基上按需添加的增益。实践建议是——先用三把钥匙把基础打牢(它们简单、可靠、几乎总有正回报),确认还需要更高效率时,再按"我的问题有什么额外结构可利用"去选合适的进阶手段(控制不够光滑→有色噪声;有数据和训练能力→learned prior;地形各向异性强→自适应协方差)。别一上来就堆进阶手段,那既增加复杂度、又容易不稳定。
易错点:三把钥匙各有各的"调过头"¶
主动点破三把钥匙各自的失败模式,免得你机械套用:
降维过头——节点太少,表达不了必需的控制。 样条节点降维省了搜索空间,但如果任务确实需要快速变化的控制(比如落地瞬间的力矩脉冲、快速踏步),而你的节点太稀疏(插值太"平滑"),那条必需的控制曲线**根本无法被表达**——无论你怎么搜,样条都拟合不出那个尖峰。表现是机器人做不出需要快速响应的动作。对策:节点数要和任务的控制带宽匹配,需要快速响应的任务多放节点(或非均匀放)。这正是上面"调低分辨率"类比的边界。
退火调度方向/末级方差错——回顾第 5 章 §5.5 的四种错误写法。 退火的坑第 5 章 §5.5 已经详列(调度方向写反 → 由精到粗,退火失效甚至帮倒忙;末级方差太大 → 精修不到位,终点飘)。这里只提醒:把退火叠到全身控制上时,这些坑照样存在,去第 5 章 §5.5 复习。
warm-start 的末尾格污染——回顾第 5 章 §5.3 "warm-start 为什么是退火的天然搭档"。 平移后新生的末尾格是唯一没被充分退火打磨的部分,处理不当会随滚动时域逐步污染整条轨迹。第 5 章那一节讲透了,这里不重复。
把这三个"调过头"记住:三把钥匙是工具不是咒语,每把都有它的最佳剂量和方向。
⚠️ 常见陷阱¶
陷阱 1:治维度灾难的第一反应是"多撒样本" ⚠️ 思维陷阱 - 错误想法:"样本不够导致高维找不到好解,那我把 \(K\) 从 1024 加到 16384 不就行了?" - 现象/后果:样本数加 16 倍,计算量也加 16 倍(实时性崩),而效果提升却很有限——因为维度灾难是指数的,线性加样本追不上指数增长的空间体积。你很快会发现"加样本"这条路在高维上性价比极低。 - 根本原因:维度灾难的本质是空间体积随维度指数膨胀,靠线性增加样本数对抗指数膨胀是徒劳的(第 4 章 §4.2 量化过)。真正有效的是降低维度(钥匙①)和提高搜索效率(钥匙②),而非堆样本。 - 正确做法:先降维(样条节点)、再退火(提高搜索效率)、再 warm-start(复用历史),把样本花在刀刃上。只有在这三招都用足、确认瓶颈确实是样本密度时,才考虑适度加样本。自检:如果你在靠加样本硬扛,停下来问"我降维了吗?退火了吗?"
陷阱 2:样条节点既想少(省搜索)又想多(保表达),纠结不决 ⚠️ 概念误区 - 错误想法:"节点少了表达不够、多了又没省到搜索空间,到底取多少?" - 现象/后果:要么节点太多没省到维度、要么太少做不出动作,反复试错。 - 根本原因:没抓住"节点数应匹配任务的控制带宽"这个原则——它不是个玄学数字,而是由任务需要多快的控制变化决定的。 - 正确做法:从任务的物理时间尺度反推。稳态行走这类控制变化平缓的任务,节点可以很少(如 DIAL-MPC 的 5 个);需要快速响应的敏捷动作,节点要多或非均匀加密。先从少量节点起步,如果发现某类必需动作做不出来(如快速踏步),再加节点。把节点数当成一个和任务带宽挂钩的、可解释的设计量,而非凭感觉的超参。
陷阱 3:以为三把钥匙是"可选优化",只用一两把 ⚠️ 编程陷阱 - 错误想法:"我先只用 warm-start 和加样本,退火和降维以后再说。" - 现象/后果:在低维玩具上似乎还行,一上真实全身就跑不动或效果差,却不知道是少用了钥匙。 - 根本原因:三把钥匙治的是三个不同的子问题(本节理论的分工表),在真实全身(高维 + 实时 + 强非凸)上,三个子问题同时存在,缺任何一把都会在对应的子问题上栽跟头。 - 正确做法:做真实全身采样 MPC 时,三把钥匙作为**标配**一起上(这也是 WBMPPI 和 DIAL-MPC 的共同选择)。在低维玩具上可以省略某些(如 §7.7 的累积项目 demo 维度低,不必全用),但一旦上真实全身,把三把钥匙当成默认配置。自检:检查你的全身 MPPI 实现是否同时具备样条参数化、退火调度、warm-start 三件套。
练习¶
- (动手·叠加钥匙②) 在本节 demo 的
mppi_knot上加退火:让sigma随迭代从大到小衰减(如每 5 次迭代乘 0.7)。在更难的障碍配置(窗口更宽、或多个窗口)下,对比"降维"和"降维+退火"的成功率。验证两把钥匙叠加是否优于单用。 - (概念·分工) 用你自己的话填这张表:维度灾难的三个子问题各是什么、三把钥匙各治哪个、为什么不能用一把钥匙治所有。要求能向同事讲清"为什么 DIAL-MPC 三招都用"。
- (设计·节点数) 给两个任务——(a) 四足在平地稳态小跑,(b) 四足从静止快速起跳越过一道沟。分别估计你会给样条控制设多少节点、为什么。哪个任务节点该多?哪个该非均匀加密、加密在哪个时段?
- (综合·联系第 4、5 章) 第 4 章 §4.2 的 iCEM 也用了"有色噪声 + 复用上次精英"来对抗维度和浪费。请对比:iCEM 的这两招分别对应本节哪把钥匙?iCEM 缺了哪把(提示:退火)?如果给 iCEM 加上退火,它会变成什么样的方法(联系第 5 章的统一视角)?
§7.5 动作空间之争:力矩级 vs 位置目标(PD)与 sim-to-real ⭐⭐⭐¶
动机¶
§7.4 治好了维度灾难,机器人在仿真里能动了。但当你要把它搬到真机,会撞上一个看似细枝末节、实则决定成败的设计选择:你采样的"控制",到底是什么?
这个问题听起来奇怪——控制不就是控制吗?但对腿足机器人,"控制"至少有两种截然不同的含义:
- 直接采关节力矩 \(\tau\):MPPI 采的扰动序列就是关节力矩,rollout 时直接把力矩喂给动力学,执行时直接把 \(u_0\)(一组力矩)发给电机。DIAL-MPC(第 5 章 §5.3)走的就是这条"全阶力矩级"路线。
- 采关节位置目标 \(q_\text{des}\),走底层 PD:MPPI 采的是每个关节的**目标角度**序列,执行时把目标角度 \(q_\text{des}\) 发给一个**底层 PD 控制器**,由 PD 实时算出力矩 \(\tau = K_p(q_\text{des}-q) - K_d\dot q\) 发给电机。CMU 的 Whole-Body MPPI 走的就是这条路线。
这两个选择看起来只是"采什么变量"的小差别,但它**直接决定了 sim-to-real 的难易、动作的表现力、以及对仿真器精度的依赖**。这一节把这个选择讲透——它是本章最实用的工程判断之一,也是 §7.1 易错点"采样法把全部信任押在仿真器上"那个伏笔的正式兑现。
如果不这样做会怎样:在真机上直接采力矩然后摔得不明不白¶
设想你不懂这个区别,照着 DIAL-MPC 的"力矩级"在自己的机器人上实现了全身 MPPI,仿真里调得很稳,信心满满地部署到真机——结果机器人一上电就剧烈抖动、动作变形,甚至直接摔倒。你反复检查代码、调参数、加滤波,仿真里却怎么都复现不出这个问题。
这是无数人踩过的坑。根因往往就是动作空间:直接采力矩,对模型误差极其敏感。 真机的电机有自己的动力学(电流环、饱和、延迟)、关节有摩擦和间隙、连杆惯量和仿真有出入——这些误差在"力矩直接驱动"下会被原封不动地传导成轨迹误差,因为力矩到运动之间没有任何反馈来纠偏。仿真里没有这些误差所以很稳,真机一上全暴露。
如果你懂这一节,面对真机抖动你会**第一时间怀疑动作空间**,把直接采力矩换成采位置目标 + 底层 PD——PD 环提供了一个实时反馈,能吸收相当一部分模型误差,鲁棒性立刻高一个量级。这个判断能帮你少走几周弯路,也是 CMU Whole-Body MPPI 能用"非常简单的控制策略"就在真机上 work 的关键。
历史与理论:两条路线的取舍¶
两条路线背后是同一个更深的问题:控制的负担,多少交给在线优化、多少交给一个固定的底层环?
力矩级(DIAL-MPC 路线):把全部控制负担交给在线优化。 MPPI 直接决定每个关节每一刻的力矩,没有任何底层环帮忙——机器人怎么动,100% 由 MPPI 当场算出的力矩决定。这条路的优点是**表现力最强、权限最高**:力矩是机器人能施加的最底层量,直接控制它意味着 MPPI 能榨干全阶动力学的每一分能力,做出最敏捷、最 dynamic 的动作(如 DIAL-MPC 的精确跳跃、利用全身惯量的翻越)。缺点是**对模型误差最敏感、sim-to-real 最难**:没有底层反馈纠偏,仿真和真机的任何动力学差异都直接体现为行为差异。DIAL-MPC 能在真机 Go2 上 work,靠的是退火带来的高质量解 + 相当下功夫的仿真校准 + GPU 大规模并行——这是个需要重投入才能驯服的路线。
位置目标 + PD(CMU Whole-Body MPPI 路线):把稳定的负担交给固定的 PD 环。 MPPI 只决定每个关节的**目标角度**(在样条节点上采,§7.4 的降维),具体的力矩由一个固定增益的底层 PD 控制器实时计算。这条路的优点是**sim-to-real 鲁棒得多**:PD 环是一个简单、可靠的反馈控制器,它会持续地把关节往目标角度拉,自动吸收一部分模型误差(电机延迟、摩擦、惯量误差导致关节偏离目标时,PD 会自动补偿)。所以即便仿真和真机有差异,位置目标这个"指令"比力矩这个"指令"更**不变(invariant)——"把膝关节转到 30 度"这个目标在仿真和真机上含义一致,而"给膝关节施加 5 牛米"在仿真和真机上产生的实际运动可能差很多。代价是**表现力略受限:PD 增益限制了控制带宽,某些需要精确利用全力矩的极限动作可能做不出来;而且 PD 增益本身成了需要调的超参。
CMU Whole-Body MPPI 论文把"采关节位置目标"作为它能在真机上用"非常简单的控制策略"就成功的关键设计——它在 Go1 上完成了越野行走、爬上自身高度的箱子、推箱子到目标,全部用位置目标 + PD,没有复杂的力矩级优化。这印证了:对大多数任务,把稳定交给 PD、让 MPPI 只管设定目标,是 sim-to-real 友好得多的选择。
把两条路线并排对比:
| 维度 | 力矩级(DIAL-MPC) | 位置目标 + PD(WBMPPI) |
|---|---|---|
| MPPI 采的是 | 关节力矩 \(\tau\) | 关节目标角度 \(q_\text{des}\) |
| 执行时 | 力矩直接发给电机 | 目标角度 → 底层 PD → 力矩 |
| 谁负责稳定 | 全靠在线 MPPI | 固定的 PD 环 |
| 表现力/权限 | 最高(榨干全阶动力学) | 略受限(PD 带宽) |
| 对模型误差 | 极敏感 | 较鲁棒(PD 吸收误差) |
| sim-to-real | 难(需精确仿真+校准) | 友好得多 |
| 对仿真器精度 | 依赖极高 | 依赖较低 |
| 额外超参 | 退火调度等 | PD 增益 \(K_p,K_d\) |
| 适合 | 极限敏捷动作、研究 | 大多数行走/操作、真机落地 |
本质洞察:动作空间的选择,本质是在决定**"把控制问题的哪一部分交给在线优化、哪一部分交给一个固定的、鲁棒的底层环"。力矩级把一切都交给在线优化——表现力拉满,但脆弱(全部模型误差直接传导);位置目标 + PD 把"稳定"这个最需要鲁棒性的部分外包给固定的 PD 环,只让 MPPI 解"去哪"这个相对高层的问题——牺牲一点表现力,换来巨大的鲁棒性。这和 §7.3 的"在线优化 vs 离线策略"、§7.6 即将讲的"混合架构"是**同一个母题:在"全部在线现算(灵活但脆弱/贵)"和"部分交给固定/预先准备好的东西(省事且鲁棒,但受限)"之间划线。这条线划在哪,是机器人控制里反复出现的核心权衡。学会问"这个控制负担,该在线解还是交给一个固定底层环",比记住任何具体方法都更长远。
深一层:为什么位置目标对模型误差"天生不变"¶
上面说位置目标 + PD 鲁棒、力矩级脆弱,但只给了"PD 反馈吸收误差"这个直觉。这一小节用一点简单的数学把"为什么"讲到底——理解了它,你就知道这个鲁棒性不是调出来的,而是**结构性的、天生的**。
考虑一个最简单的单关节,名义模型惯量为 \(m\),真机惯量是 \(m'=m(1+\delta)\)(有 \(\delta\) 的建模误差)。看两种动作空间下,同样的"指令"在名义和真机上产生的运动差多少。
力矩级:误差直接传导。 MPPI 在名义模型上算出一个力矩 \(\tau\),它在名义模型上产生加速度 \(\ddot q = \tau/m\)。但真机执行同一个 \(\tau\),产生的加速度是 \(\ddot q' = \tau/m' = \tau/(m(1+\delta)) = \ddot q /(1+\delta)\)。也就是说,惯量误差 \(\delta\) 原封不动地变成了加速度误差——名义算的轨迹和真机跑出的轨迹直接差一个 \((1+\delta)\) 因子,且这个误差会沿时域积分、累积放大。没有任何机制纠正它。
位置目标 + PD:误差被反馈"吃掉"在稳态里。 MPPI 在名义模型上算出一个位置目标 \(q_\text{des}\)。执行时,底层 PD 持续施加力矩 \(\tau = K_p(q_\text{des}-q) - K_d\dot q\)。关键在于:PD 是一个闭环,它的稳态(关节稳定下来时)由"目标"决定,而几乎与惯量无关。 稳态时 \(\dot q = \ddot q = 0\),代入 PD 律得 \(K_p(q_\text{des}-q_\text{ss})=0\),于是稳态位置 \(q_\text{ss}=q_\text{des}\)——无论惯量是 \(m\) 还是 \(m'\),关节最终都停在同一个 \(q_\text{des}\)。惯量误差 \(\delta\) 只影响"多快到达 \(q_\text{des}\)"(瞬态),不影响"最终停在哪"(稳态)。也就是说,"把关节转到 30 度"这个位置目标,在名义和真机上含义一致(都停在 30 度);而"给关节施加 5 牛米"这个力矩指令,在名义和真机上产生的运动差一个 \((1+\delta)\)。
本质洞察:位置目标的 sim-to-real 鲁棒性,根源在于**它把指令编码在"稳态由反馈律唯一确定"的量上**。PD 闭环的稳态位置等于目标位置,这个等式不含惯量、不含大部分模型参数——所以位置目标这个指令对模型误差"天生不变"。力矩则相反,它编码在"直接乘以模型参数才变成运动"的量上,模型参数一错,运动就错。这解释了一个更深的道理:选动作空间,本质是选一个"对模型误差不敏感"的指令表示。 位置目标之所以好,不是因为 PD 有什么魔力,而是因为"目标位置"这个量恰好被闭环的稳态条件锚定住了,与模型参数解耦。这也是为什么 §7.7 的开环回放实验里,加了外扰后位置目标几乎不动、力矩满盘皆输——你现在从数学上看到了那个实验背后的原因。
当然,"稳态不变"不等于"瞬态也不变"——惯量误差仍会影响到达目标的快慢、超调、振荡(这正是 PD 增益要调的原因,见下面易错点)。但对大多数任务,"最终停在对的地方"(稳态正确)比"过程分毫不差"(瞬态完美)重要得多,而位置目标恰好保证了前者。这就是它 sim-to-real 友好的数学本质。也要注意这个分析的边界:它在"动作变化不太快、能近似看稳态"时最准;对要榨干瞬态全力矩的极限动作(如爆发跳跃),瞬态误差就不能忽略了——那正是力矩级路线(牺牲鲁棒换瞬态表现力)有时不得不上场的场景。
一个熟悉的印证:RL 腿足为什么也几乎都输出位置目标¶
这个洞察你其实在 RL 那边见过——这是个很好的交叉印证(呼应你的前置 RL 课程)。
你训过的腿足 RL 策略(PPO/SAC + IsaacLab),它的动作输出几乎总是**关节位置目标(或目标偏移),再走底层 PD,而**很少直接输出力矩。为什么?答案和本节完全一样:位置目标 + PD 的 sim-to-real 鲁棒性远高于直接力矩。 RL 在仿真里训练,要迁移到真机,位置目标这个动作空间对 sim-to-real gap 容忍度高得多——这是腿足 RL 领域多年踩坑总结出的共识(早期直接输出力矩的策略 sim-to-real 极难,后来基本都转向位置目标 + PD)。
所以你会看到一个漂亮的呼应:采样式全身控制(WBMPPI)和学习式全身控制(RL)在动作空间上殊途同归——都倾向位置目标 + PD,都是出于同一个 sim-to-real 理由。 这不是巧合,而是因为这个理由是物理层面的(PD 反馈吸收模型误差),与你用采样还是用学习来产生目标无关。
不要把"位置目标更好"绝对化。在追求极限性能、且有能力下功夫校准仿真的场景(如 DIAL-MPC 的精确跳跃、某些需要精确力控的操作),直接力矩的表现力是位置目标给不了的。RL 里也有用力矩控制做出极致 dynamic 动作的工作。关键还是匹配——大多数任务用位置目标(鲁棒省事),极限任务且有校准能力时考虑力矩。
深一层:PD 增益是个"隐藏超参",怎么选¶
选了位置目标 + PD,事情还没完——你引入了两个新的、容易被当成"随便设个值就行"的超参:比例增益 \(K_p\) 和微分增益 \(K_d\)。下面的陷阱会专门讲"别随便设",这一小节先把"该怎么设"的直觉给你,让你知道这两个旋钮各拧向哪个方向、会换来什么、付出什么。
先理解两个增益各管什么。\(K_p\) 管"拉力的强度"——关节偏离目标多远,就用正比于偏差的力矩往回拉;\(K_d\) 管"阻尼"——正比于关节速度施加反向力矩,抑制振荡。它们共同决定 PD 环把关节"拉到目标位置"的动态品质。
往两个方向拧,各有代价:
\(K_p\) 太小:拉力不足,关节跟不上 MPPI 给的目标,跟踪滞后、迟钝——表现为机器人动作"软绵绵"、响应慢,需要快速发力的动作做不出来(呼应 §7.4 故障场景三)。更糟的是,rollout 里 MPPI 以为"目标会被准确跟踪",实际跟踪不到位,规划和执行就出现偏差。
\(K_p\) 太大:拉力过猛,关节刚性过强。两个后果:一是容易**振荡**(拉过头又弹回,尤其 \(K_d\) 阻尼不足时),二是**接触冲击大**——脚以很高的刚性砸向地面,接触力尖峰大,既伤硬件又可能让仿真的接触求解不稳定,sim-to-real 也更难(真机的接触冲击和仿真差异在高刚性下被放大)。
\(K_d\) 则是在 \(K_p\) 选定后用来"压住振荡"的:\(K_d\) 太小,欠阻尼、振荡;\(K_d\) 太大,过阻尼、迟钝(又回到响应慢)。\(K_p\) 和 \(K_d\) 要配合,常见的直觉是让系统接近"临界阻尼"(既不振荡又不迟钝的甜点附近)。
本质洞察:PD 增益的选择,本质是在**"跟踪精度/响应速度"和"柔顺性/接触友好/鲁棒性"之间权衡**。高 \(K_p\) 给你精确快速的跟踪,但牺牲柔顺(接触冲击大、易振荡、对仿真误差更敏感);低 \(K_p\) 给你柔顺和鲁棒,但牺牲跟踪精度和响应速度。这个权衡和 §7.5 主轴"力矩级 vs 位置目标"的权衡是**同构**的——其实是同一个"表现力 vs 鲁棒"权衡在更细一层的重现:选位置目标是大方向上选了鲁棒,而 PD 增益的高低,是在位置目标这个大方向内部再做一次"偏表现力(高增益)还是偏鲁棒(低增益)"的微调。所以 PD 增益绝不是"随便设"的实现细节,它和动作空间选择是同一类决策,只是粒度更细。
实践上的调法(一个稳妥的顺序):先在**无接触**的简单动作(如空中摆腿)上调 \(K_p/K_d\),让关节能平稳准确地跟踪目标(不振荡、不迟钝);再加入**接触**任务,观察接触冲击是否过大、是否需要适当降一点 \(K_p\) 换柔顺;全程**保证 rollout 仿真用的增益和真机/执行用的增益严格一致**(否则规划的是"假 PD"的行为,§7.5 易错点会再强调)。一个常见的起点是参考你用的机器人平台(如 Unitree 系列)官方或社区给出的增益范围,再按任务微调,而不是凭空拍一个值。记住:当机器人出现"动作迟钝"或"接触时抖/硬"的现象,PD 增益应该是你排查的前几个嫌疑点之一。
深一层:二分之外的灰度——前馈力矩 + 反馈(计算力矩控制)¶
本节为了讲清核心权衡,把动作空间简化成了"力矩级 vs 位置目标"的二分。但诚实地说,真实世界的动作空间是有**灰度**的,二分之间还有一条经典的中间道路值得你知道——计算力矩控制(Computed Torque Control, CTC),也叫前馈 + 反馈。理解它,你对动作空间的认识会从"二选一"升级成"一条连续谱"。
CTC 的思路是把发给关节的力矩拆成两部分:\(\tau = \underbrace{\tau_\text{ff}}_{\text{前馈}} + \underbrace{K_p(q_\text{des}-q) - K_d\dot q}_{\text{反馈}}\)。前馈项 \(\tau_\text{ff}\) 由**逆动力学模型**算出——"要让关节按计划运动,名义上需要多大力矩"(用机器人动力学方程 \(\tau = M(q)\ddot q_\text{des} + C(q,\dot q)\dot q + g(q)\) 算);反馈项就是熟悉的 PD,负责纠正前馈算不准的部分(模型误差、扰动)。
为什么这是"灰度"?因为它把力矩级和位置目标的优点**揉在了一起**:前馈项提供了力矩级的**表现力**(主动补偿重力、惯量、科氏力,让运动更精确、更有"力感",能做更动态的动作),反馈项提供了位置目标 + PD 的**鲁棒性**(PD 兜住前馈的模型误差和扰动)。它在"纯力矩(全靠在线优化算力矩,无反馈兜底)"和"纯位置目标 + PD(无前馈,全靠反馈把关节拽过去)"之间,占据了一个中间位置。
本质洞察:把三种动作空间排在一条谱上,你会看到一个漂亮的连续过渡——纯位置目标 + PD(无前馈)→ 计算力矩 CTC(前馈 + 反馈)→ 纯力矩级(无反馈兜底),从左到右,"交给在线优化/模型的部分"越来越多、"交给固定反馈环兜底的部分"越来越少,表现力递增、鲁棒性递减。这正是 §7.5 那个"表现力 vs 鲁棒"主轴的连续化——动作空间不是离散的两个点,而是这条谱上的一个连续选择,你可以根据任务对表现力和鲁棒的需求,在谱上选一个合适的位置。本章主推的"位置目标 + PD"在谱的鲁棒端,DIAL-MPC 的"力矩级"在表现力端,CTC 在中间——理解了这条谱,你就不会再把动作空间当成非此即彼的二选一,而能在表现力和鲁棒之间做连续的、精细的权衡。
那为什么本章主线还是讲二分、主推位置目标?因为 CTC 的前馈项**依赖一个足够准的逆动力学模型**——模型不准,前馈就帮倒忙(算错的前馈力矩反而要靠反馈去纠正,得不偿失),而真机的精确逆动力学模型恰恰难得(这又回到 sim-to-real 的老问题)。所以对大多数 sim-to-real 场景,"放弃前馈、纯靠位置目标 + PD"是更省事更稳的起点(不需要准模型);当你有了较好的模型、又需要更高表现力时,再往谱的中间(CTC)甚至力矩端走。这也再次呼应 §7.6 的选型精神:没有一个点是绝对最优的,关键是按你的模型质量和任务需求,在这条谱上选对位置。
把动作空间落到硬件:电机的控制模式¶
讲了这么多动作空间的抽象选择,值得花一小节把它连到**真实硬件**上——你选的"动作空间",最终要通过电机驱动器(motor driver)的某种控制模式落地。理解这一层,你才知道"位置目标 + PD"在真机上到底发生了什么、那个 PD 环跑在哪里。
现代机器人关节的驱动器(如 Unitree 等用的电机)通常支持三种基本控制模式,恰好对应动作空间谱上的三个区域:
位置模式(position mode):你给驱动器发一个目标角度,驱动器内部用自己的 PD(或更复杂的)环把电机转到该角度。这正对应"位置目标 + PD"动作空间——而且关键在于,这个 PD 环常常跑在驱动器固件里、频率极高(可达 kHz 量级),远高于上层 MPC 的重规划频率。这就是 §7.7 那个"开环回放"实验的物理对应:MPC 几十赫兹给目标,驱动器 kHz 的 PD 在两次目标更新之间顶住扰动。
力矩模式(torque mode):你直接给驱动器发目标力矩(电流),驱动器只管把电机的输出力矩调到该值,不做位置反馈。这对应"力矩级"动作空间——所有的稳定都得靠上层在线优化,驱动器不帮你兜底。这也是为什么力矩级对上层算法和模型误差那么敏感(§7.5):没有了那个高频 PD 兜底环。
混合模式(很多驱动器支持):发一组 \((q_\text{des}, \dot q_\text{des}, K_p, K_d, \tau_\text{ff})\),驱动器执行 \(\tau = K_p(q_\text{des}-q)+K_d(\dot q_\text{des}-\dot q)+\tau_\text{ff}\)。这正是 CTC(前馈 + 反馈)的硬件实现——你既给位置目标和增益(反馈部分),又给前馈力矩,驱动器在固件里合成。
本质洞察:动作空间的选择,落到硬件就是**选驱动器的哪种控制模式、把那个高频反馈环放在哪一层**。选位置模式 = 把 PD 兜底环放在驱动器固件(高频、鲁棒、不用你操心);选力矩模式 = 不要驱动器的反馈环、所有控制都由上层在线算(灵活但脆);选混合模式 = 既用驱动器的反馈环、又叠上你算的前馈(CTC)。这把 §7.5 一直在讲的"哪部分交给固定反馈、哪部分交给在线优化"这个抽象母题,落成了一个非常具体的工程问题——那个固定反馈环,物理上就跑在电机驱动器里。理解了这一层,"位置目标 + PD 更鲁棒"就不再是一句抽象的话,而是"我把那个 kHz 的 PD 兜底环放进了驱动器固件,让它在我 MPC 来不及更新时顶住扰动"这个具体的、看得见摸得着的工程决策。
实践含义很直接:用位置目标 + PD 时,你要把 MPPI 算出的目标角度发给驱动器的位置模式(或混合模式),并把 PD 增益设进驱动器;要做 sim-to-real,rollout 里仿真的 PD 必须和驱动器固件里实际跑的 PD 增益、频率尽量一致(§7.5 易错点的硬件版)。选驱动器和配置它的控制模式,是把抽象动作空间变成真机行为的最后一公里。
易错点:位置目标不是"免费的稳"¶
主动点破:位置目标 + PD 虽然鲁棒,但不是"接上 PD 就稳了"那么简单,有两个容易忽略的点。
第一,PD 增益是新引入的、必须调的超参,且影响重大。 \(K_p\) 太小,关节跟不上目标(软绵绵、跟踪差);\(K_p\) 太大,系统刚性过强、易振荡、且在接触时产生过大冲击力(甚至打坏减速器)。\(K_d\) 控制阻尼,太小振荡、太大迟钝。这些增益要和机器人的惯量、任务的动态性匹配。你省去了"在线算力矩"的负担,但接过了"调 PD 增益"的负担——只是后者通常更好调、更直观。
第二,rollout 里必须仿真同一个 PD 环,否则训练和执行不一致。 一个隐蔽但致命的坑:如果你执行时用位置目标 + PD,但 rollout 时却直接把位置目标当力矩用(或用了不同的 PD 增益),那 MPPI 优化的是"假世界"的目标,和真实执行不匹配。rollout 必须忠实地包含底层 PD 环(用和真机相同的 \(K_p,K_d\)),让 MPPI 看到的 rollout 行为和执行时一致。这一点和 §7.1 陷阱 3(rollout 模型要和真实一致)一脉相承。
⚠️ 常见陷阱¶
陷阱 1:默认用直接力矩,因为"力矩才是真正的控制" ⚠️ 思维陷阱 - 错误想法:"力矩是最底层的物理量,直接控制力矩最'正宗'、最强大,位置目标是'偷懒'。" - 现象/后果:在真机上被 sim-to-real gap 折磨——抖动、变形、摔倒,调试数周仍不稳。 - 根本原因:把"表现力强"误当成"应该默认用"。力矩级表现力确实最强,但它对模型误差极敏感,sim-to-real 最难(本节理论)。"正宗"不等于"适合你的场景"。 - 正确做法:默认从位置目标 + PD 起步(鲁棒、省事、大多数任务够用),把直接力矩留给"确认任务需要极限表现力、且你有能力校准仿真"的场景。先用位置目标快速在真机上 work,建立 baseline,再判断是否真的需要上力矩级。
陷阱 2:仿真用位置目标 + PD,真机却忘了配同样的 PD(或反之) ⚠️ 编程陷阱 - 错误想法:"仿真里我用位置目标采样优化,真机直接把优化出的目标发下去就行。" - 现象/后果:真机行为和仿真对不上——可能因为真机的 PD 增益和仿真不同,或真机根本没配 PD 而把位置目标错当力矩。 - 根本原因:动作空间的语义必须在 rollout(仿真)和执行(真机)两端严格一致。位置目标只有配上**相同的** PD 环才有意义;两端的 PD 增益不一致,等于优化的和执行的是两个不同系统。 - 正确做法:确保 rollout 仿真里的底层 PD 增益和真机的 PD 增益完全一致;执行时位置目标确实经过 PD 转成力矩。自检:在仿真和真机上发同一组位置目标序列,对比关节实际轨迹应该高度一致(差异主要来自未建模的真机因素,而非 PD 配置不同)。
陷阱 3:把 PD 增益当成"设一次就不管"的常数 ⚠️ 概念误区 - 错误想法:"PD 增益随便设个差不多的值,反正主要靠 MPPI 优化。" - 现象/后果:增益不当导致跟踪差(太软)或振荡/接触冲击过大(太硬),而你却以为是 MPPI 或代价的问题,南辕北辙地调错地方。 - 根本原因:位置目标路线下,PD 增益直接决定了"目标→实际运动"的转换质量,它和 MPPI 是协同工作的,不是无关紧要的常数。增益不当时,再好的目标也执行不好。 - 正确做法:把 PD 增益当成和惯量、任务动态性匹配的重要参数来调。可以先用经验值(参考同类机器人)起步,再根据跟踪表现和接触行为微调:跟不上就加 \(K_p\)、振荡就加 \(K_d\) 或减 \(K_p\)、接触冲击大就适当减 \(K_p\)。自检:单独测试 PD 跟踪——发一个简单的目标轨迹,看关节能否平稳准确跟上,确认 PD 本身工作良好后再上 MPPI。
练习¶
- (概念·必答) 用两句话向同事解释"为什么直接采力矩的全身 MPPI 比采位置目标的更难 sim-to-real"。必须点出"PD 环吸收模型误差"这个关键。
- (动手·对照) 在一个简单仿真(如倒立摆或单关节)里,实现两种 MPPI:一种采力矩、一种采位置目标 + PD。然后给仿真动力学引入误差(如把质量改大 20%、加电机延迟),对比两种动作空间下控制性能下降的程度。验证位置目标是否更鲁棒。
- (设计·选型) 给三个任务选动作空间并说明理由:(a) 四足在崎岖地形稳健行走、(b) 四足从静止做一个需要榨干全力矩的高难度后空翻、(c) 机械臂做精细力控装配。哪些用位置目标?哪个可能需要力矩级?为什么?
- (综合·联系 RL) 你的 RL 腿足策略输出位置目标 + PD。现在你要用采样式 MPPI 替换或补充这个 RL 策略。如果保持相同的动作空间(位置目标 + PD),对你有什么好处(提示:rollout 仿真、sim-to-real 经验可复用)?这是否提示了一种"RL 和 MPPI 共享动作空间"的混合架构(预告 §7.6)?
§7.6 Whole-Body MPPI vs OCS2 WBC:互补、混合与选型 ⭐⭐⭐¶
动机¶
到这里,你手里有了两套全身控制工具:一套是前置课程学的**梯度式全身 MPC**(OCS2 WBC,用 Pinocchio 解析动力学 + QP + gait scheduler),一套是本章学的**采样式全身控制**(Whole-Body MPPI / DIAL-MPC,用 MuJoCo 黑箱 rollout + 退火,接触涌现)。一个自然又重要的问题摆在面前:这两套该怎么选?是竞争关系(采样取代梯度)还是互补关系? 这一节给出答案——它们高度互补,而且当下最有前景的不是二选一,而是把它们(以及 RL)叠成一个栈。这也是把本章前面所有线索(§7.3 在线 vs 离线、§7.5 动作空间外包)收束成一个统一图景的地方。
如果不这样做会怎样:陷入"非此即彼"的误判¶
不理解两者的互补,你容易掉进两个对称的坑。
一个坑是**采样万能论**:"采样式不需要 gait scheduler、能涌现步态、还不怕接触,那是不是该把 OCS2 全扔了,一切都用 MPPI?" 后果是你在稳态行走这种 OCS2 又快又稳又能保证硬约束的任务上,硬用采样法——结果精度不如 OCS2、还吃掉大量算力、硬约束(摩擦锥、关节限位)也保证不了。
另一个坑是**梯度守旧论**:"采样法 sim-to-real 难、精度不如梯度法、还要 GPU,不靠谱,继续用 OCS2 就行。" 后果是遇到爬不规则台子、从奇怪姿态恢复、需要涌现新步态的非结构化任务时,你被 gait scheduler 卡死,做不出来,而这些恰恰是采样法的主场。
理解了互补边界,你面对一个具体任务能立刻判断:稳态行走交给 OCS2、非结构化敏捷动作交给 MPPI、要 sim-to-real 鲁棒再叠 RL——各用其长。
理论:互补边界——各自强在哪、弱在哪¶
先把两者的对比摆全(这是本章骨架表的展开版),然后讲清"为什么"互补。
| 维度 | OCS2 WBC(梯度式) | Whole-Body MPPI(采样式) |
|---|---|---|
| 动力学模型 | Pinocchio RNEA/ABA(解析、可求导) | MuJoCo 前向仿真(黑箱,§7.1) |
| 求解方式 | 求导 + 牛顿步 + 解 QP | 前向 rollout + 加权(§7.1,不求导) |
| 接触处理 | 预设接触模式序列(gait scheduler) | 不预设——接触从代价涌现(§7.1) |
| 约束 | 硬约束(QP):关节限位、摩擦锥 | 软约束:代价/barrier 罚 |
| 步态 | 需要 gait scheduler 预设 | 可从代价涌现(§7.3) |
| 计算平台 | CPU 实时(解析动力学快) | GPU/多核 CPU 并行 rollout(§7.3) |
| 维度处理 | 解析梯度天然处理高维 | 需三把钥匙治维度灾难(§7.4) |
| sim-to-real | 需精确解析模型 | 需精确仿真器(§7.1 易错点) |
| 精度 | 高(局部二阶收敛) | 中(采样精度受样本数限) |
| 最适场景 | 已知步态的**稳态行走** | 非结构化运动(跳/跨/翻/恢复) |
为什么是互补而非竞争?因为两者的强弱**恰好错开**,根源在 §7.1:
OCS2(梯度)强在"光滑、结构化、要精度和硬约束"的场景。 当步态已知(稳态行走)、动力学在每个接触模式内光滑、你需要高精度和硬约束保证(摩擦锥不能违反、关节不能超限),梯度法的局部二阶收敛又快又准,QP 还能严格保证硬约束。这是它几十年积累的主场,采样法在这里反而吃亏——精度不如它、硬约束保证不了、还更费算力。
MPPI(采样)强在"非结构化、接触序列该被发现、动力学非光滑/黑箱"的场景。 当任务需要发现新的接触序列(爬不规则台子、从摔倒姿态恢复、用身体翻越)、动力学因接触而非光滑、或你只有一个黑箱仿真器,梯度法的整套机器(求导 + 预设接触序列)就失灵了(§7.1),而采样法不求导、让接触涌现,正好补上。
本质洞察:OCS2 和 MPPI 的互补,本质是 §7.1 那个"求导 vs 求值"分野在系统层面的投影。梯度法用一阶局部信息——所以在光滑结构化问题上又快又准,在非光滑非结构化问题上失灵;采样法用零阶全局信息——所以不怕非光滑、能全局发现,但精度和效率不如梯度法。两者的强弱边界,就是"光滑结构化 ↔ 非光滑非结构化"这条线。没有哪一方更优,它们是同一枚硬币的两面,分居这条线的两端。 这就是为什么成熟的系统不二选一,而是按任务在这条线上的位置分工——这也是整个机器人控制(不止腿足)反复出现的格局:局部精确的方法和全局探索的方法各司其职。
从控制史看:混合/分层不是权宜之计,而是常态¶
你可能会觉得"OCS2 + MPPI + RL 混合"是因为现在每个方法都不完美、临时拼凑的权宜之计——等某个方法成熟了就能统一。但拉开历史看,机器人控制(乃至整个机器人系统)几乎从未被单一方法统一过,分层/混合一直是常态而非例外。理解这一点,能让你对"混合是趋势"有更深的信心,而不是把它当成过渡阶段。
回看机器人控制的历史脉络,你会发现一个反复出现的格局——不同层次、不同特性的方法各司其职。经典的运动控制就是分层的:高层做任务/路径规划(离散、组合、慢),中层做轨迹优化(连续、光滑、中速),底层做伺服反馈(高频、稳定、快)。强化学习兴起后,也没有取代这套分层,而是嵌入其中——RL 常被用在某一层(如学一个底层运动策略、或学一个高层决策),与传统方法的其他层共存。即便是端到端学习最激进的设想,落到真实部署时,也往往保留某种分层(如学习的策略 + 传统的安全层/底层稳定环)。
为什么是这样?因为**真实机器人任务跨越了太宽的"时间尺度"和"问题特性",没有任何单一方法能在所有尺度、所有特性上都最优**。毫秒级的关节稳定和秒级的任务规划,要求的方法特性截然不同(前者要快要稳、后者要会推理);光滑的稳态行走和非结构化的跌倒恢复,适合的方法也截然不同(§7.6 互补边界)。指望一个方法通吃,等于指望它在所有矛盾的需求上都最优——这违背"没有免费午餐"的基本规律。
本质洞察:混合/分层是常态,根源在于**真实机器人问题本身是异质的——它在不同时间尺度、不同结构化程度上,提出了相互矛盾的方法需求**。一个方法要快(底层稳定)就难兼顾会推理(高层规划);要精确(光滑结构化)就难兼顾会探索(非结构化发现)。既然需求是异质且矛盾的,最优的系统结构就必然是异质的——让各擅其长的方法各管一摊。所以本章讲的"OCS2 稳态 + MPPI 敏捷 + RL 微调"不是某个时期的临时方案,而是这个深层规律在腿足全身控制上的具体体现。随着方法进步,混合的"配方"会变(谁管哪层、边界划在哪),但"混合"本身不会消失——因为问题的异质性不会消失。认识到这一点,你设计任何机器人系统时,第一反应就该是"这个任务跨越了哪些尺度/特性,该怎么分层、各层用什么",而不是"哪个最强的单一方法能搞定一切"。后者几乎总是错的问法。
这也给本章的选型框架(§7.6 即将讲)一个更高的立足点:选型不只是"这个任务用采样还是梯度"的单点决策,而是"这个任务该怎么分解成几层/几块、每块交给最适合的方法"的架构决策。把这个历史视角带上,你看任何成熟机器人系统的架构,都能看出它是怎么把异质的需求拆给异质的方法的——这是比"哪个方法最强"深得多的系统观。
混合架构:把它们叠成一个栈¶
理解了互补,"混合"就是自然的下一步。当下几种有前景的混合思路:
思路一:分层——MPPI 发现、OCS2/WBC 精修。 用采样式 MPPI 在高层发现一个粗的、可行的全身运动计划(包括接触序列),再用 OCS2/WBC 在低层精确跟踪这个计划、并施加硬约束。这把"发现接触序列"(MPPI 强)和"精确执行 + 硬约束"(OCS2 强)各放在它擅长的层。
思路二:分场景——OCS2 稳态 + MPPI 敏捷 + RL 微调。 这是本章骨架点出的趋势,也最可能成为 2026–2028 的主流形态:稳态行走用 OCS2(成熟、高效、稳)做 baseline;遇到非结构化敏捷动作(跳、跨、翻、恢复)切换到 MPPI/DIAL-MPC;最后用 RL 做 sim-to-real 微调,把仿真里的策略磨到真机能用。三者按场景分工,一个统一的高层状态机决定何时用哪个。
思路三:RL 当 MPPI 的 learned prior——把在线和离线缝合。 这是 §7.3"在线 vs 离线"和第 6 章 TD-MPC 思路的延续:用 RL/扩散策略学一个"好动作大概长什么样"的先验,当 MPPI 的提议分布或初值(把一部分思考摊销到离线),再用在线采样 MPPI 兜底保鲁棒。第 5 章 §5.6 的"扩散先验 + 采样 MPC"正是这条路的一个实例。而 §7.5 揭示的"RL 和 MPPI 都倾向位置目标 + PD 这个共享动作空间",让这种混合在工程上格外顺滑——两者动作空间一致,先验直接可用。
把这三条线索连起来看,本章其实一直在讲同一件事的不同侧面——控制系统的设计,是在反复决定"哪部分交给在线现算、哪部分交给预先准备好的东西":§7.3 是"在线优化 vs 离线策略",§7.5 是"在线解力矩 vs 交给固定 PD 环",§7.6 是"在线采样发现 vs 预设步态/解析求解 + 学习先验"。三者是同一个划线问题在不同层面的体现。理解了这个母题,你看任何机器人控制架构,都能一眼看出它把这条线划在了哪、为什么。
把混合讲实:一个分场景调度的骨架¶
思路二(OCS2 稳态 + MPPI 敏捷)说起来简单,落到代码上最关键、也最容易出 bug 的是**切换逻辑**——何时从一个控制器切到另一个、切换瞬间怎么不抖。给一个高层调度的伪代码骨架,把这件事讲实(注意它只是骨架,真实系统的切换条件和平滑处理要复杂得多):
class HybridLocomotionController:
"""分场景混合:稳态用 OCS2(梯度), 非结构化用 MPPI(采样)。
两者共享同一个动作空间(位置目标+PD, §7.5)——这是平滑切换的前提。"""
def __init__(self, ocs2_ctrl, mppi_ctrl):
self.ocs2 = ocs2_ctrl # 梯度式: 已知步态稳态行走(§7.6 主场)
self.mppi = mppi_ctrl # 采样式: 非结构化敏捷动作(§7.1 主场)
self.mode = 'ocs2' # 默认走稳态(成熟、高效、稳)
def select_mode(self, state, terrain):
"""决定该用哪个控制器。核心: 按'是否结构化/接触序列是否已知'切换(§7.6)。"""
if terrain.is_structured and not state.is_recovering:
return 'ocs2' # 平地/已知地形 + 姿态正常 → 梯度法
else:
return 'mppi' # 不规则障碍/需翻越/从摔倒恢复 → 采样法
def control(self, state, terrain):
new_mode = self.select_mode(state, terrain)
if new_mode != self.mode: # 发生模式切换
self._handoff(self.mode, new_mode, state) # 关键: 平滑交接, 防抖
self.mode = new_mode
ctrl = self.ocs2 if self.mode == 'ocs2' else self.mppi
return ctrl.compute(state, terrain) # 输出位置目标(两者同一动作空间)
def _handoff(self, old, new, state):
"""切换瞬间的平滑交接——混合架构最易出 bug 处(§7.6 陷阱2)。
要点: 用当前真实关节状态初始化新控制器的 warm-start, 让新控制器
从'当前实际在哪'接着规划, 而非从零开始(否则切换瞬间目标跳变→抖动)。"""
if new == 'mppi':
self.mppi.U[:] = 0.0
self.mppi.U[0] = state.q # 用当前关节角做位置目标初值
else:
self.ocs2.warm_start_from(state) # OCS2 也从当前状态接管
这个骨架点出混合架构成败的三个关键。其一,切换条件(select_mode)回到 §7.6 的核心——按"地形是否结构化、接触序列是否已知、是否在做恢复类非结构化动作"决定谁上场,这正是互补边界(光滑结构化 ↔ 非光滑非结构化)的代码化。其二,共享动作空间——两个控制器都输出位置目标 + PD(§7.5),这不是巧合而是设计前提:动作空间一致,切换才不会因为"一个发力矩、一个发位置"而产生语义断裂。其三,也是最容易栽的——平滑交接(_handoff),切换瞬间必须用当前真实关节状态初始化新控制器(warm-start),让它从"现在实际在哪"接着规划,否则新控制器从零规划出的第一个目标可能和当前姿态差很远,位置目标一跳变,PD 就猛地拉一下,机器人当场抖动甚至摔倒。
本质洞察:混合架构的难点几乎全在"接缝"处,而非各个控制器本身。OCS2 和 MPPI 各自都成熟,把它们拼起来时,真正的工程量在切换条件设计、动作空间对齐、切换瞬间的状态连续性这三件"接缝活"上。这呼应 §7.6 陷阱2"混合不是免费午餐"——收益是真的,但成本集中在接缝。共享动作空间(§7.5)之所以反复被强调,正是因为它一举简化了"接缝活"里最麻烦的对齐问题:两个控制器说同一种"控制语言",交接自然顺滑。这也是为什么本章把"动作空间"提升为累积项目的一个独立维度——它不只影响单个控制器的 sim-to-real,还决定了多个控制器能否优雅地拼装。
选型:一个可操作的判断框架¶
把选型浓缩成几个问题,按顺序问,帮你在实践中快速定位:
- 步态/接触序列是已知且固定的吗? 是(稳态行走)→ 倾向 OCS2(梯度,高效精确)。否(要发现/涌现)→ 倾向 MPPI(采样,§7.1)。
- 需要严格保证硬约束(摩擦锥、关节限位绝不违反)吗? 是 → 倾向 OCS2(QP 硬约束)或在 MPPI 之上叠一层 WBC 精修(思路一)。否(软约束够用)→ MPPI 可行。
- 动力学在任务全程光滑可微吗? 是 → 梯度法可用且高效。否(接触主导、非光滑)→ 采样法的主场(§7.1)。
- 要 sim-to-real 上真机吗?对模型误差敏感吗? 是 → 注意动作空间(§7.5 优先位置目标 + PD),且考虑叠 RL 微调(思路二)。
- 有 GPU/多核算力做大规模并行 rollout 吗? 有 → 全身 MPPI/DIAL 可行。没有 → MJPC(多核 CPU,§7.3)或退回 OCS2。
这个框架不是要你死板套用,而是帮你建立"按任务特性在互补边界上定位"的直觉。多数真实系统的答案不是单选,而是"主体用 A、某些环节用 B"的混合。
多视角:探索者与精算师¶
给互补配个角色类比。把 MPPI 想成**探索者**——它派出大量侦察兵(样本)到处试,擅长在陌生地形(非结构化、非光滑)里发现一条能走通的路,但走得不够精细。把 OCS2 想成**精算师**——它在熟悉的地形(光滑、已知步态)里用精确的计算(梯度、QP)走出最优且严格合规(硬约束)的路线,但一到陌生地形(接触切换、要发现新路)就算不下去。
像的地方:一个团队既需要探索者开路、也需要精算师精修,二者配合才能又快又稳地征服复杂地形。不像的地方(边界):这个类比可能让你以为"总是先探索再精算",但实际分工更灵活——稳态行走全程是精算师的活、不需要探索者;非结构化恢复可能全程是探索者的活。混合不是固定流水线,而是按地形动态调度谁上场(思路二的状态机)。
深一层:混合架构最隐蔽的坑——模式抖动¶
混合架构的"接缝活"(§7.6 混合骨架小节)里,有一类坑比"切换瞬间抖动"更隐蔽、更难查,值得单独点破,因为它是所有"按条件在多个模式间切换"的系统的通病——模式抖动(mode chattering):系统在两个控制模式之间**反复快速横跳**,而不是稳定地停在某一个模式上。
它怎么发生的?设想你的切换条件是"地形是否结构化"(决定用 OCS2 还是 MPPI)。如果机器人正好走在结构化和非结构化的**边界附近**,传感器/状态估计的微小噪声就可能让"是否结构化"的判断在每个控制周期反复翻转:这一周期判定为结构化→切到 OCS2,下一周期噪声让它判定为非结构化→切到 MPPI,再下一周期又翻回来……系统在 OCS2 和 MPPI 之间每周期横跳。后果很糟:每次切换都有交接开销和不连续(§7.6 混合骨架的 _handoff),高频反复切换让这些不连续累积成持续的抖动,机器人动作支离破碎,甚至失稳——而且这种失稳很难查,因为每个模式单独都正常,问题出在"切换本身太频繁"。
本质洞察:模式抖动的根源,是**用一个"硬阈值"去切分一个本质上连续/带噪声的判断量**。"是否结构化"不是非黑即白的,而是有灰度的;用一条硬线(阈值)去切,落在线附近的状态就会因噪声反复跨线。这是控制、决策、乃至一切"基于阈值切换"的系统的普遍陷阱——不止混合机器人控制,温控、调度、金融策略都有同构的问题。认识到"硬阈值 + 带噪连续量 = 抖动风险",你就能在设计任何切换逻辑时提前预防。
怎么治?几个标准手段(都是"给切换加惰性"的思路):其一,迟滞(hysteresis)——切过去和切回来用不同的阈值(比如"结构化度 > 0.7 才切到 OCS2,< 0.3 才切回 MPPI",中间的 0.3~0.7 保持当前模式),制造一个"缓冲带",让噪声在带内晃动时不触发切换。其二,最小驻留时间(dwell time)——规定切换后必须在新模式停留至少 \(N\) 个周期才允许再切,从时间上禁止高频横跳。其三,对判断量本身做平滑/滤波——别用瞬时的带噪状态做判断,用滤波后的、平稳的量。实践中常把迟滞和最小驻留时间一起用。
把这个坑记住,它能帮你避开混合架构里一类"每个部件都对、合起来却抖"的诡异故障——遇到混合系统莫名抖动,先查切换日志看是不是在模式间高频横跳,是的话加迟滞和驻留时间,往往药到病除。这也再次印证 §7.6 的主旨:混合的难点在接缝,而模式抖动是接缝上最隐蔽的一道。
⚠️ 常见陷阱¶
陷阱 1:认为采样式全身 MPC 会取代 OCS2 ⚠️ 思维陷阱 - 错误想法:"WBMPPI/DIAL-MPC 不要 gait scheduler、接触自动涌现,这么先进,OCS2 该被淘汰了。" - 现象/后果:在稳态行走上硬用采样法,精度、效率、硬约束保证都不如 OCS2,做了费力不讨好的"技术降级"。 - 根本原因:没理解互补边界(本节理论)。采样法在非结构化任务上强,但在稳态行走这个 OCS2 的主场上,梯度法的精度/效率/硬约束是采样法给不了的。 - 正确做法:把采样法看成**扩展了能力边界**(让你能做以前做不了的非结构化任务),而非**取代**已有的 OCS2。稳态行走继续用 OCS2,把采样法用在它的主场。理想是混合架构(思路二),各用其长。
陷阱 2:以为混合架构是"免费午餐",叠起来就行 ⚠️ 概念误区 - 错误想法:"那我把 OCS2、MPPI、RL 全叠上,取各家之长,肯定最强。" - 现象/后果:系统复杂度爆炸——三套方法的接口、状态表示、动作空间要对齐,模式切换(何时从 OCS2 切到 MPPI)的逻辑容易出 bug,切换瞬间的不连续可能导致机器人抖动甚至摔倒。 - 根本原因:混合的收益是真的,但集成成本也是真的。多套方法协同需要解决接口对齐、模式切换平滑、状态一致性等一系列工程难题,不是简单堆叠。 - 正确做法:混合要**渐进式**地上——先用单一方法(如 OCS2 或 MPPI)把基础任务跑通,确认确有单一方法覆盖不了的场景,再针对性地引入第二种方法,并重点设计好两者的接口和切换逻辑(共享动作空间能极大简化对齐,见 §7.5/思路三)。不要一上来就追求"三合一"。自检:每引入一种方法,问"它解决了哪个单一方法解决不了的具体问题?切换逻辑稳吗?"
陷阱 3:选型时只看"哪个方法新/酷",不看任务特性 ⚠️ 思维陷阱 - 错误想法:"采样式 + 扩散退火听起来最前沿,就用它。" - 现象/后果:用错了场景——比如给一个标准稳态行走任务上 DIAL-MPC,结果远不如直接用成熟的 OCS2,白白增加复杂度和算力。 - 根本原因:方法选型应该由**任务特性**驱动(本节选型框架的五个问题),而非由"哪个方法更时髦"驱动。 - 正确做法:拿到任务先过一遍选型框架的五个问题(步态是否已知、是否要硬约束、是否光滑、是否上真机、有无算力),让任务特性把你导向合适的方法。前沿不等于通用,§7.6 的整个论点就是"按任务在互补边界上定位"。
陷阱 4:实现模式切换时直接替换控制器对象,丢掉了内部状态 ⚠️ 编程陷阱
- 错误做法:混合架构里从 OCS2 切到 MPPI(或反向)时,简单地 self.active = self.mppi(直接换对象/换指针),就开始用新控制器算下一步。
- 现象/后果:新控制器的内部状态(MPPI 的名义控制序列 U/warm-start、OCS2 的求解器热启动状态、任何积分器/滤波器状态)都是**上次它被用时的陈旧值**或全零初值。于是切换后的第一个输出基于错误的内部状态算出,与当前真实姿态不匹配——机器人在切换瞬间抖动、跳变,甚至失稳(这正是 §混合骨架 _handoff 和模式抖动那段在防的事)。
- 根本原因:控制器不是无状态函数,它带着随时间演化的内部状态(warm-start、积分项等)。直接替换对象只换了"用哪个算法",没有把"当前系统实际在哪"这个信息交接给新控制器,导致新控制器从一个与现实脱节的内部状态起步。
- 正确做法:切换时显式做**状态交接**——用当前真实关节状态/速度重新初始化新控制器的 warm-start(如 self.mppi.U[:]=0; self.mppi.U[0]=state.q),重置或迁移必要的内部状态,让新控制器从"现在实际在哪"接着算(参见 §混合骨架的 _handoff)。自检:在切换的那一帧打印新控制器的首个输出和当前实际状态,确认两者一致、没有突变;用日志记录切换时刻,确认切换不是高频反复发生(否则先治模式抖动)。
练习¶
- (选型·必做) 用本节的五问框架,为下面四个任务各选方法(OCS2 / MPPI / 混合)并说明理由:(a) 仓库里四足机器人沿固定路线巡逻(平地、已知步态);(b) 四足从侧翻姿态自主翻正站起;(c) 四足驮货爬上一段不规则楼梯;(d) 人形在拥挤人群中稳步行走且绝不允许关节超限。
- (架构·设计) 设计一个"OCS2 稳态 + MPPI 敏捷"的混合系统的高层状态机:什么信号触发从 OCS2 切到 MPPI?切回时怎么保证平滑(避免切换瞬间抖动)?两者的动作空间怎么对齐(联系 §7.5)?
- (概念·联系全章) §7.3(在线 vs 离线)、§7.5(在线解力矩 vs 交给 PD)、§7.6(在线采样 vs 预设步态/学习先验)被本章说成"同一个划线母题"。请用你自己的话阐述这个母题,并各举一个例子说明这条"线"在三节里分别划在哪。
- (开放·联系第 6 章) 思路三"RL 当 MPPI 的 learned prior"和第 6 章 TD-MPC(学习世界模型 + 规划)是什么关系?如果你用 TD-MPC 学到的策略当 MPPI 的先验、用 TD-MPC 学到的世界模型当 MPPI 的 rollout 模型,你会得到一个什么样的系统?它在 §7.6 的互补图景里处于什么位置?
§7.7 实战:给统一规划器接上接触任务与动作空间维度 ⭐⭐⭐¶
动机¶
本章前六节讲了很多论点和系统,现在用你的累积项目把它们落到能跑的代码上。回顾累积项目的进展:从第 2 章的 vanilla MPPI 起步,第 4 章加了 weight/noise/cov 三个正交维度,第 5 章加了 schedule(退火)维度,第 6 章加了 model_source/terminal(动力学来源/终端处理)。本章给它加上一个全新的、本章专属的维度:action_space(动作空间——力矩级还是位置目标),并第一次让它在一个**接触任务**上工作。
这一节要亲手验证本章两个最核心的论点:(1) MPPI 在接触任务上不需要预设"何时接触",接触序列从代价里自然涌现(§7.1);(2) 动作空间选位置目标 + PD 比直接采力,对模型误差/外扰鲁棒得多(§7.5)。 我们用一个最小的 1D 推箱子任务,纯 NumPy 实现,不依赖 MuJoCo/GPU 也能跑出这两个现象。
任务设定¶
一个推杆(pusher,点)在一条线上,从左侧推动一个箱子(box)到目标位置。关键的非光滑性在于:箱子只有在被推杆触到(距离小于接触半径)时才移动,没触到就纹丝不动。 这个"通断"接触正是 §7.1 讲的不可微现象的最小化身。推杆怎么接近箱子、何时开始推、推多久——这些**都不预设**,全靠 MPPI 最小化"箱子离目标"的代价时自然产生。
环境支持两种动作空间(本章新增的维度):force(控制就是施加在推杆上的力/速度指令,开环)和 pd_target(控制是推杆的目标位置,由底层 PD 闭环跟踪)。注意一个工程细节——rollout 和真实执行复用同一个 step 函数(呼应 §7.5 易错点"rollout 必须仿真同一个 PD 环"),只是真实执行时可以注入模型误差/外扰,规划时不注入。
代码一:接触环境(含两种动作空间)¶
import numpy as np
rng = np.random.default_rng(0)
class ContactPusher1D:
"""1D 推箱子环境。action_space 是本章给累积项目新增的维度。"""
def __init__(self, action_space='pd_target', r_contact=0.15,
push_gain=0.8, Kp=18.0, dt=0.1):
self.action_space = action_space # 'force' / 'pd_target' —— 本章新增维度
self.r_contact = r_contact; self.push_gain = push_gain
self.Kp = Kp; self.dt = dt # Kp: 底层 PD 增益(pd_target 用)
def step(self, p, b, u, gain=1.0, disturb=0.0):
"""单步推进。批量(K,)和标量都适用。rollout 与执行复用同一函数。
gain/disturb 在'执行'时注入模型误差/外扰(规划时 gain=1,disturb=0)。"""
if self.action_space == 'force': # 开环:控制即力/速度指令
p = p + self.dt * (gain * u) + self.dt * disturb
else: # 闭环:控制是位置目标,PD 跟踪
p = p + self.dt * (gain * self.Kp * (u - p)) + self.dt * disturb
# 非光滑接触:推杆从左侧触到箱子(b-p<r)才右推;没触到箱子不动
pen = np.clip(p - (b - self.r_contact), 0.0, None) # 穿透量(0/1切换,不可微!)
b = b + self.push_gain * pen
return p, b
def rollout(self, p0, b0, target, U):
"""批量 rollout。U:(K,H)。复用 step 逐步前向积分,返回每条代价 (K,)。
全程只前向求值、不求导(§7.1);接触何时发生不预设,由优化发现。"""
K, H = U.shape
p = np.full(K, p0, float); b = np.full(K, b0, float); cost = np.zeros(K)
for t in range(H):
p, b = self.step(p, b, U[:, t], gain=1.0) # 规划用名义模型
cost += (b - target) ** 2 # 箱子到目标(每步)
cost += 0.05 * np.clip(b - p, 0, None) # 轻shaping:鼓励追上箱子
cost += 0.01 * U[:, t] ** 2 # 控制能量
cost += 30.0 * (b - target) ** 2 # 终端重罚
return cost
注意 step 里那行 pen = np.clip(...)——它就是非光滑接触:pen 在推杆触到箱子的瞬间从 0 变正,是个不可微的切换。但 rollout 全程只调用 step 做前向积分、累加代价,从不对这个切换求导——这正是 §7.1 的核心在代码里的体现。
代码二:统一采样规划器(累积项目)¶
class UnifiedSamplingPlanner:
"""累积项目:前几章的 weight/noise/cov/schedule/model_source/terminal,
本章新增的 action_space 维度体现在 env 里。这里给接触任务的精简实例。"""
def __init__(self, env, H=20, K=300, lam=0.2, sigma=0.6, weight='mppi'):
self.env, self.H, self.K = env, H, K
self.lam, self.sigma, self.weight = lam, sigma, weight
self.U = np.zeros(H) # 名义控制序列
def _update(self, p, b, target):
"""单步:采样—rollout—加权更新(复用前几章的 _update 核心逻辑)。"""
eps = rng.normal(0.0, self.sigma, size=(self.K, self.H))
J = self.env.rollout(p, b, target, self.U[None] + eps)
if self.weight == 'mppi': # 第4章:指数加权
w = np.exp(-(J - J.min()) / self.lam); w /= w.sum()
self.U = self.U + (w[:, None] * eps).sum(0)
else: # 'cem':精英平均
elite = np.argsort(J)[:max(1, self.K // 10)]
self.U = (self.U[None] + eps)[elite].mean(0)
return self.U[0]
def control(self, p, b, target):
u0 = self._update(p, b, target)
self.U = np.roll(self.U, -1); self.U[-1] = 0.0 # warm-start 平移(§7.4 钥匙③)
return u0
这个规划器对动作空间是**无感的**——它只管采控制序列、rollout、加权更新;控制到底被解释成"力"还是"位置目标",是 env.step 的事。这种解耦正是好设计:动作空间维度被封装在环境里,规划器内核保持不变。 这呼应第 2 章 §2.3 MPPI-Generic 的解耦哲学(Dynamics/Cost/Sampling/Controller 四件分离)——动作空间属于 Dynamics 那一侧,换它不影响 Controller。
代码三:闭环执行 + 鲁棒性消融¶
def run_closed_loop(action_space, steps=50, seed=0):
"""闭环执行(每步重规划):MPPI 把箱子推到目标,记录执行的控制序列。"""
global rng; rng = np.random.default_rng(seed)
env = ContactPusher1D(action_space=action_space)
planner = UnifiedSamplingPlanner(env)
p, b, target = 0.0, 0.6, 1.5; U_exec = []
for _ in range(steps):
u = planner.control(p, b, target) # 在线规划
U_exec.append(u)
p, b = env.step(p, b, u, gain=1.0) # 真实执行(复用同一 step)
return env, np.array(U_exec), abs(b - target)
def open_loop_replay(env, U_exec, disturb):
"""开环回放(不重规划)在带外扰世界里执行已规划序列,看动作空间鲁棒性(§7.5)。"""
p, b, target = 0.0, 0.6, 1.5
for u in U_exec:
p, b = env.step(p, b, u, gain=1.0, disturb=disturb)
return abs(b - target)
# === 演示一:闭环重规划,接触序列自然涌现 ===
plans = {}
for asp in ['force', 'pd_target']:
env, U_exec, err = run_closed_loop(asp); plans[asp] = (env, U_exec)
print(f"动作空间={asp:9s}: 箱子从离目标 0.90 → 最终 {err:.3f}")
# === 演示二:开环回放 + 持续外扰,看鲁棒性(§7.5) ===
for asp in ['force', 'pd_target']:
env, U_exec = plans[asp]
errs = [open_loop_replay(env, U_exec, d) for d in (0.0, -0.4, -0.8)]
print(f"动作空间={asp:9s}: 无扰{errs[0]:.3f} | 中扰{errs[1]:.3f} | 强扰{errs[2]:.3f}")
跑出来的两个现象¶
实际运行的输出(你照抄就能复现):
=== 演示一:闭环重规划,接触序列自然涌现(不枚举接触模式) ===
动作空间=force : 箱子从离目标 0.90 → 最终 0.037 (成功推到目标)
动作空间=pd_target: 箱子从离目标 0.90 → 最终 0.075 (成功推到目标)
=== 演示二:开环回放 + 持续外扰,看鲁棒性(§7.5) ===
动作空间=force : 无扰0.037 | 中扰0.463 | 强扰0.837
动作空间=pd_target: 无扰0.075 | 中扰0.098 | 强扰0.121
现象一(验证 §7.1):接触序列涌现,无需枚举。 两种动作空间下,MPPI 都把箱子从离目标 0.90 推到不足 0.08——成功完成推箱子。而我们**从没告诉它"先接近箱子、在第几步开始推、推到哪停"**——代码里没有任何接触模式、接触序列、接触时机的预设。它只是在最小化"箱子离目标"的代价,"接近—接触—持续推"这套协调就自然产生了。把这个 1D 任务换成 Go1 推一个真实箱子(CMU Whole-Body MPPI 做的),逻辑完全一样。这就是 §7.1"接触从代价涌现"最朴素的亲手验证。
现象二(验证 §7.5):位置目标 + PD 对外扰鲁棒,开环力不行。 演示二把规划好的控制序列拿去**开环回放**(不重规划)在一个有持续外扰的世界里执行——外扰模拟未建模的干扰、sim-to-real 的模型误差。结果一目了然:随着扰动从无到强,force(开环力)的误差从 0.037 一路飙到 0.837(箱子几乎没推动多少),而 pd_target(位置目标走 PD)的误差只从 0.075 微增到 0.121(几乎不受影响)。原因正是 §7.5 讲的:PD 是闭环反馈,看到推杆被外扰推偏就自动顶回目标位置;而开环的力指令对扰动毫无招架之力,误差直接累积。 这就是为什么真机上位置目标 + PD 远比直接采力鲁棒——你刚刚用 12 行的开环回放亲眼看到了。
一个诚实的说明:演示一里两种动作空间表现接近,是因为闭环 MPC 每步都重规划、看到真实箱子位置,这个外层反馈本身就补偿了大部分误差,掩盖了动作空间的差异。只有在演示二的**开环回放**(去掉重规划这层反馈)下,动作空间自身的鲁棒性差异才暴露出来。这对应真机的现实结构:底层 PD 跑在很高频率(如 1 kHz),而 MPC 重规划频率低得多(如 50 Hz)——在两次重规划之间的那段"开环"时间里,正是 PD(而非来不及更新的 MPC)在顶住扰动。这个 demo 用"开环回放"把那段时间放大给你看。
本质洞察:这个小 demo 把本章的两条主线浓缩进了几十行代码——采样让你免费拿下接触(现象一:不可微的
pen切换,MPPI 视而不见),而动作空间决定你能否把它鲁棒地落到真机(现象二:PD 反馈顶住扰动)。 前者是采样式方法的天赋(§7.1),后者是把天赋兑现到现实的工程关键(§7.5)。一个解决"能不能做",一个解决"做了能不能用"。把这两件事想透,你就抓住了从"仿真里能跑的全身 MPPI"到"真机上能用的全身 MPPI"之间最关键的两步。
把这个 demo 往真实全身控制推¶
这个 1D demo 是真实全身采样 MPC 的"骨架最小版"。要把它推向真实四足,需要补上本章讲过的几件事,这也是一张很好的"从 demo 到真机"的路线图:
- 换 rollout 模型:把
step里的 1D 接触换成 MuJoCo 全身仿真(§7.1/§7.3),让脚地接触、自碰撞被真实建模。 - 上三把钥匙(§7.4):几十维全身控制必须加样条节点降维、退火、warm-start(demo 里只用了 warm-start,因为 1D 不需要前两把)。
- 选对动作空间(§7.5):真机优先
pd_target+ PD(demo 已演示其鲁棒性),除非要极限敏捷动作才考虑力矩级。 - 考虑混合(§7.6):稳态部分可交给 OCS2,把采样 MPPI 用在非结构化敏捷动作上。
每一步都在本章对应小节讲过——这个 demo 是把它们串起来的线头。
⚠️ 常见陷阱¶
陷阱 1:rollout 用一种动作空间,执行用另一种 ⚠️ 编程陷阱
- 错误想法:"规划时怎么方便怎么来,执行时再转成真机要的格式。"
- 现象/后果:规划优化的是"假世界"的控制,和真实执行不匹配,行为对不上(§7.5 陷阱 2 的具体化)。比如 rollout 把控制当力、执行当位置目标,箱子根本推不到。
- 根本原因:动作空间的语义必须在 rollout 和执行两端严格一致。本 demo 用同一个 env.step 保证这点——这不是偷懒,是正确性的保证。
- 正确做法:让 rollout 和执行共享同一份动力学/动作空间代码(如本 demo 的 step)。两端的 action_space、PD 增益等必须完全一致。自检:能不能用同一个函数同时服务 rollout 和执行?如果不能,警惕两端不一致。
陷阱 2:以为闭环 demo 表现好就代表动作空间选对了 ⚠️ 思维陷阱 - 错误想法:"演示一里 force 和 pd_target 都把箱子推到了,那动作空间无所谓嘛。" - 现象/后果:据此在真机上随便选了 force(开环力),结果一上真机被模型误差/扰动打回原形(演示二的强扰场景)。 - 根本原因:闭环每步重规划的外层反馈掩盖了动作空间的鲁棒性差异(本节诚实说明)。在仿真、高频重规划、无扰动的理想条件下,动作空间的差异确实小;但真机有模型误差、有扰动、PD 和 MPC 频率不同,差异会放大。 - 正确做法:评估动作空间鲁棒性,要在**接近真机的条件**下测——加模型误差、加扰动、用真实的 MPC/PD 频率比,或像本 demo 用开环回放放大差异。别只看理想闭环 demo 的表现就下结论。
陷阱 3:shaping 代价写成"硬指定接触时机",扼杀涌现 ⚠️ 概念误区
- 错误想法:"为了让它快点推,我直接在代价里写'第 5 步必须接触箱子'。"
- 现象/后果:这就退回成了"预设接触序列"(§7.1 陷阱 2),丢掉了采样法让接触涌现的最大优势,还可能因为硬指定的时机不合适而让任务更难。
- 根本原因:混淆了"温和的 shaping"(鼓励靠近箱子,不指定时机)和"硬指定接触时机"。本 demo 的 0.05 * clip(b-p,0,None) 只是鼓励推杆别离箱子太远,没规定何时接触——接触时机仍由优化自由决定。
- 正确做法:shaping 只提供温和的、方向性的引导(如鼓励靠近目标物),把"何时、如何接触"留给优化涌现。如果你发现自己在代价里写"第几步该接触""按什么顺序接触",停下来——你正在把采样法退化成需要预设序列的梯度法。
练习¶
- (动手·必做) 把这个 demo 跑起来,复现两个现象。然后改变箱子初始位置、目标位置、接触半径
r_contact,观察 MPPI 是否仍能让接触涌现、把箱子推到目标。记录:什么情况下会失败(提示:箱子离目标太远、接触半径太小导致接触太稀疏)? - (动手·扩展动作空间) 给
ContactPusher1D加第三种动作空间'pd_target_delay'——位置目标 + PD,但 PD 有一拍执行延迟(真机常见)。在演示二里对比它和无延迟pd_target的鲁棒性。延迟让 PD 的优势打折扣了吗? - (动手·叠钥匙) 把任务改成 2D(推杆和箱子在平面上,箱子要推到一个二维目标),决策维度变高。先用 vanilla(全维采样)跑,再加样条节点降维(§7.4 钥匙①),对比成功率。验证维度一高,钥匙①就开始起作用。
- (综合·联系全章) 这个 demo 的累积项目现在有
action_space维度。请说明:怎么把第 5 章的schedule(退火)维度也接到这个接触任务上(在_update里加多级方差缩放)?什么样的接触任务(提示:更非凸、局部最优更多)会让退火真正派上用场,而当前这个简单推箱子用不上退火?
把四个系统拉通看:同一个采样内核,为什么做出不同的工程选择¶
学完全章四个系统(STORM、MJPC、Whole-Body MPPI、DIAL-MPC),值得把它们沿几条关键设计轴拉通对比一次。这不是再列一遍事实(那是后面"版本信息速查"的活),而是回答一个更有洞察的问题:它们用的是同一个采样内核(采一批控制、rollout、按代价更新),为什么在工程选择上分化得这么明显? 看懂这个分化的逻辑,你就掌握了"给定一个新任务,该怎么配置采样 MPC"的判断力。
先把分化摆在一张表上,沿本章建立的几条轴:
| 设计轴 | STORM | MJPC | Whole-Body MPPI | DIAL-MPC |
|---|---|---|---|---|
| 动作空间(§7.5) | 关节速度/力矩 | 力矩(经样条) | 关节**位置目标**+PD | **力矩**级(全阶) |
| 计算平台(§7.3深一层) | GPU(张量化) | 多核CPU | GPU(MuJoCo并行) | GPU(MJX/Brax) |
| 降维(§7.4钥匙①) | 时域较短 | 样条节点 | 样条节点 | 样条节点(Hnode=5) |
| 退火(§7.4钥匙②) | 否(单尺度) | 否(单尺度) | 否(单尺度) | 是(扩散退火) |
| 加权(第2/4章) | MPPI指数加权 | 贪心取最优(PS) | MPPI指数加权 | MPPI+退火 |
| 真机验证 | 机械臂 | 多在仿真 | 真实Go1 | 真实Go2 |
| 本质定位 | 反应式操作的GPU张量化MPPI | 极简采样器+好仿真的范式证明 | 位置目标采样控制上真机 | 力矩级退火榨干表现力 |
从这张表里能读出几条贯穿性的规律——这才是拉通看的价值:
规律一:动作空间的选择,和"要不要上真机做高难动作"强相关。 上了真机做行走/操作的(WBMPPI 的 Go1),选位置目标 + PD(鲁棒优先,§7.5);要榨干力矩表现力做极限动作的(DIAL-MPC 的 Go2 带载跳跃),才选力矩级,并为此付出退火 + 仿真校准的代价。这印证了 §7.5 的核心判断:动作空间不是风格偏好,而是由"鲁棒 vs 表现力"的任务需求决定的。
规律二:退火是"非凸难度"的函数,不是标配。 四个系统里只有 DIAL-MPC 用了退火——因为它做的全身力矩级敏捷动作(带载跳跃)是最非凸、局部最优最多的任务(力矩级的搜索空间比位置目标更崎岖)。其余三个在各自任务上单尺度采样就够。这印证 §7.4:钥匙②(退火)治的是"搜不动"这个子问题,任务够非凸才需要它——别无脑给所有任务上退火。
规律三:平台选择跟着 rollout 特性走,不迷信 GPU。 MJPC 独选 CPU(rollout 重接触求解、分支多、样本适中),其余选 GPU(rollout 较规整或要超多样本)。这印证 §7.3 深一层:按 rollout 的计算特性选硬件,而非假定 GPU 总是更好。
规律四:降维几乎是全身控制的标配,加权方式反而可以很随意。 四个系统除 STORM(时域较短,降维需求弱)外都用样条节点降维——印证 §7.4 钥匙①是高维控制绕不开的。而加权方式从"贪心取最优"(MJPC 的 Predictive Sampling)到 MPPI 指数加权都有,差别没那么关键——印证 §7.3 的本质洞察:采样器的精巧程度不是瓶颈,降维、动作空间、仿真质量这些"结构性选择"才是。
本质洞察:四个系统的分化告诉你一件很重要的事——采样式全身控制的工程配置,不是"哪个最先进就用哪个",而是由任务的几个特性(要不要上真机、动作多敏捷、有多非凸、rollout 多重、要多少样本)共同决定的一组耦合选择。 动作空间、退火、平台、降维,这几个旋钮不是独立调的,而是围绕"任务需要什么"协同确定的:要真机鲁棒 → 位置目标 → 搜索空间较平缓 → 可能不需要退火 → 样本需求适中 → CPU 或中等 GPU 够用(WBMPPI 一路);要极限力矩表现 → 力矩级 → 搜索空间崎岖 → 需要退火 → 多级大批量样本 → 必须大 GPU(DIAL-MPC 一路)。看懂这种"成套配置"的耦合逻辑,比记住任何单个系统都有用——下次你拿到一个新的腿足任务,能顺着"任务特性 → 一套协调的配置选择"推下去,而不是盲目套用某篇论文的配置。
这也正是 §7.6 选型框架的延伸:选型不止是"采样还是梯度"的二选一,在确定用采样之后,还有动作空间、退火、平台、降维这一整套"怎么配"的决策——而这套决策有迹可循,就是上面四条规律。把这套判断力带走,你面对这个高产方向的任何新系统,都能快速看懂它"为什么这么配",也能为自己的任务"配一套合适的"。
本章前沿与开放问题¶
本章覆盖的方法(STORM、MJPC、Whole-Body MPPI、DIAL-MPC)大多是 2021–2025 的新工作,这个方向仍在快速演进。几个值得关注的前沿与尚未解决的开放问题:
开放问题一:全身力矩 MPPI 的 sim-to-real。 这是本章骨架点名的核心开放问题。DIAL-MPC 虽然在真实 Go2 上跑通了全阶力矩级控制,但力矩级路线的 sim-to-real 仍然很脆弱(§7.5)——真实电机有电流环动力学、力矩饱和、传动延迟、温度漂移,关节有摩擦和间隙,这些在仿真里很难精确建模,而力矩级控制对它们极其敏感。如何系统地缩小这个差距——是靠更精确的电机/接触辨识、靠在线自适应、还是靠在力矩级之上叠一层鲁棒底层环——目前没有公认的最佳答案。位置目标 + PD 用"牺牲一点表现力换鲁棒"绕开了它,但对需要极限力矩表现的任务,这个开放问题仍横在那里。
开放问题二:仿真器保真度这个根本瓶颈。 §7.3 反复强调"采样法把全部信任押在仿真器上",MJPC 那句"你只能控制 MuJoCo 能仿真的东西"是悬在所有采样式全身控制头上的达摩克利斯之剑。接触尤其是软肋——刚性接触、摩擦、多点接触、形变接触的仿真都还不完美。前沿方向包括:可微仿真与采样的结合、用真机数据校准/学习接触模型、以及更快更准的接触求解器。仿真器进步一分,采样式全身控制的真机能力就涨一分——这个方向的天花板很大程度上由仿真器决定。
前沿一:learned prior + 采样 MPC。 延续第 5 章 §5.6 和本章 §7.6 思路三:纯免训练的全身 MPPI(DIAL-MPC)写不出"自然的运动风格"这类隐式偏好,也无法利用数据里的先验知识。把训练好的扩散策略/RL 策略当作采样 MPC 的 learned prior(提供"好动作大概长什么样"),再用在线采样保证可行性,是把数据先验和在线优化结合的活跃方向。§7.5 揭示的"采样和 RL 共享位置目标 + PD 动作空间"让这种结合在工程上格外顺滑。
前沿二:向人形和更高维扩展。 本章的真机成果(WBMPPI 的 Go1、DIAL-MPC 的 Go2)主要在四足上。人形机器人自由度更高(20–30+)、更欠驱动、平衡更难,把全身采样 MPC 扩展到人形(且在真机上鲁棒工作)是当前最受关注的前沿之一——三把钥匙(§7.4)够不够用、需不需要新的降维/搜索机制,仍是开放的。
前沿三:算力民主化。 GPU 并行物理仿真(MuJoCo MJX、Brax)和多核 CPU 并行(MJPC)正在让全身采样 MPC 从"需要大集群"变成"一台工作站甚至嵌入式平台就能跑"。随着硬件和并行仿真的进步,曾经只有大实验室能做的全身采样控制正在普及——这会催生更多应用和迭代(呼应第 3 章/第 5 章"采样式方法的复兴由算力催生"的母题)。一个值得注意的新动向是"用更少的样本做到同样的事":2025 年前后出现的一些工作(如 arXiv:2511.19204 的 reference-free 采样 MPC)把三次样条参数化和扩散启发的噪声退火结合起来,在普通 CPU 上仅用几十条 rollout(四足约 20–30 条、人形约 60–70 条)就实现了实时无参考运动控制——相比 DIAL-MPC 等依赖数千条 GPU rollout 的路线,这是"算力民主化"的另一条腿:不是靠更强的硬件,而是靠更高的样本效率,把全身采样控制推向人人可跑。这条线和§7.4 三把钥匙、前沿一的 learned prior 一脉相承——都是在问"怎么用更少的搜索办成事"。
前沿三点五:向人形的早期突破。 紧接前沿二,上面提到的 reference-free 工作还有一个值得单独点出的意义:它在同一套框架下既驱动了 Go2 四足、也驱动了 G1 人形(且无需为两者分别设计步态或参考轨迹)。这是"全身采样 MPC 向人形扩展"的一个早期正面信号——说明样条降维 + 退火这套钥匙(§7.4)配上足够的样本效率,原则上可以跨越四足到人形的维度跃升,而不必为人形重新发明一套机制。但要强调这仍是早期工作、且人形真机的鲁棒部署比四足难得多,前沿二的开放性并未因此消除——它只是有了第一个鼓舞人心的脚印。
前沿四:与感知闭环。 本章的 demo 和很多工作都假设状态已知(箱子在哪、地形什么样)。真实的接触丰富操作(loco-manipulation)需要从感知(视觉/触觉)实时估计物体和接触状态,再喂给采样 MPC。感知与采样控制的紧耦合(如 STORM 用学习的碰撞检测从原始传感器数据避障)是把这些方法推向非结构化真实环境的关键一环。
理论空白:采样式全身控制的保证。 相比梯度式 MPC 有较成熟的稳定性/收敛分析,采样式全身控制目前主要靠经验——多少样本够、退火怎么调能保证收敛、什么条件下不会陷入危险的局部解,这些大多是经验法则而非理论保证。给接触丰富的高维采样 MPC 建立更扎实的理论(哪怕是概率性的保证),是一个有价值但困难的开放方向。
这些问题没有标准答案,正是它们让这个方向充满机会。用本章建立的框架(§7.1 求值vs求导、§7.4 三把钥匙、§7.5 动作空间、§7.6 互补选型)去读这个方向的新论文,你能快速判断每篇工作在推动哪个边界、绕开了哪个开放问题、又留下了哪个没解决。
本章常见误解汇总¶
把全章散落的误区集中成一张表,便于回头速查(每条都在正文有详细展开,标注了出处):
| 误解 | 真相 | 出处 |
|---|---|---|
| MPPI 不求导,所以接触任务对它免费、连接触都不用管 | 它不求接触的导数,但**必须用会算接触的仿真器做 rollout**;难点转移给仿真器,没消失 | §7.1 陷阱1 |
| 采样式腿足控制也要先排好步态序列(gait scheduler) | 不需要——采样法直接采连续控制,接触序列从代价**涌现**;预设序列是梯度法的妥协 | §7.1 陷阱2 |
| 用平滑/软接触模型 rollout,反正 MPPI 不求导都一样 | MPPI 不求导但对仿真的**物理保真度**极敏感;软接触改变物理行为,sim-to-real 差 | §7.1 陷阱3 |
| 张量化(PyTorch)就自动比手写 CUDA 快 | 不一定——张量化加速来自**批量维 K 的并行**;峰值性能仍不如精心手写的 CUDA | §7.2、第2章§2.3 |
| MJPC 用 GPU 并行 rollout | MJPC 用**多核 CPU 线程池**,不是 GPU;性能取决于 CPU 核数 | §7.3 陷阱1 |
| MJPC 在仿真里让人形站起来了,真机也差不多能行 | 那些惊艳成果**大多在仿真**;"只能控制 MuJoCo 能仿真的东西",真机要另下功夫 | §7.3 易错点/陷阱2 |
| 治维度灾难就加样本 | 维度灾难是**指数**的,线性加样本追不上;要降维+退火+warm-start | §7.4 陷阱1 |
| 三把钥匙是可选优化,用一两把就行 | 真实全身上三把治三个不同子问题,缺一会在对应子问题栽跟头 | §7.4 陷阱3 |
| 力矩才是"正宗"的控制,应该默认用 | 力矩表现力最强但**对模型误差极敏感**、sim-to-real 最难;默认应从位置目标+PD 起步 | §7.5 陷阱1 |
| 位置目标 + PD 接上就稳了,PD 增益随便设 | PD 增益是**重要超参**,影响跟踪和接触冲击;且 rollout 必须仿真同一个 PD 环 | §7.5 易错点/陷阱3 |
| 采样式全身 MPC 会取代 OCS2 | 两者**互补**——稳态行走 OCS2 更优(精度/硬约束/效率),采样法强在非结构化 | §7.6 陷阱1 |
| 把 OCS2+MPPI+RL 全叠上就最强 | 混合收益真、集成成本也真;要渐进式上、重点设计接口与切换 | §7.6 陷阱2 |
| 闭环 demo 里 force 和 pd_target 都行,动作空间无所谓 | 闭环重规划的外层反馈**掩盖了**差异;真机的模型误差/扰动会放大它 | §7.7 陷阱2 |
本章小结¶
术语速查表¶
| 术语 | 英文 | 含义 | 出处 |
|---|---|---|---|
| 接触模式枚举 | contact mode enumeration | 预先列出所有可能的接触状态组合分别优化;梯度法处理接触的传统手段,组合爆炸 | §7.1 |
| 步态调度器 | gait scheduler | 预先规定接触序列(何时哪只脚着地)的模块;OCS2 等梯度式全身 MPC 需要它 | §7.1、§7.6 |
| 接触平滑 | contact smoothing | 把刚性接触换成光滑软接触模型使动力学可微;让梯度法能用,但引入模型误差 | §7.1 |
| 张量化 | tensorization | 用批量张量运算(而非手写 CUDA)实现并行 rollout;STORM 的工程路线 | §7.2 |
| 代价缓冲 | cost buffer | 一组可插拔、可叠加、各自带权重的代价模块;STORM 组织多目标的方式 | §7.2 |
| 关节空间 MPC | joint-space MPC | 在关节空间(而非任务空间)规划,能直接处理关节限位/奇异/自碰撞 | §7.2 |
| 残差 | residual | MuJoCo MPC 里"当前状态离目标的偏差",加权范数求和成代价 | §7.3 |
| 样条节点降维 | spline knot reduction | 用少数样条节点参数化控制曲线,降低决策维度;治维度灾难的钥匙① | §7.4 |
| 维度灾难 | curse of dimensionality | 高维下样本落在薄壳、撒满空间需指数级样本;采样法的固有软肋 | §7.4 |
| 动作空间 | action space | MPPI 采的"控制"的具体含义:力矩级 / 位置目标(PD) | §7.5 |
| 力矩级控制 | torque-level control | 直接采关节力矩;表现力最强、sim-to-real 最难(DIAL-MPC) | §7.5 |
| 位置目标 + PD | position target + PD | 采关节目标角度走底层 PD;sim-to-real 鲁棒(WBMPPI、RL 腿足) | §7.5 |
| 全身控制 | whole-body control (WBC) | 在完整刚体动力学上的全身控制;OCS2 WBC 是梯度式代表 | §7.6 |
| 混合架构 | hybrid architecture | OCS2 稳态 + MPPI 敏捷 + RL 微调等的组合 | §7.6 |
| 开环回放 | open-loop replay | 不重规划地执行已规划序列;用来暴露动作空间自身的鲁棒性差异 | §7.7 |
知识点总表¶
| # | 知识点 | 一句话 | 出处 |
|---|---|---|---|
| 1 | 接触为什么难 | 接触使动力学不可微/不连续,梯度法的求导前提失效 | §7.1 |
| 2 | 采样为什么不怕 | 采样只前向 rollout 求值、不求导,不可微对它透明 | §7.1 |
| 3 | 求值 vs 求导判据 | 更新公式里出现导数的怕不可微,只出现值的不怕——一眼判断 | §7.1 |
| 4 | 接触序列涌现 | 采样直接采连续控制,接触从代价涌现,无需 gait scheduler | §7.1 |
| 5 | 采样不解决的 | 接触免费,但维度灾难和仿真器质量仍是硬骨头 | §7.1、§7.4 |
| 6 | STORM 张量化 | 用 PyTorch 批量张量做 rollout,GPU 加速"免费"得来 | §7.2 |
| 7 | 关节空间 MPC | 在关节空间规划,直接处理任务空间看不见的关节约束 | §7.2 |
| 8 | 多目标代价缓冲 | 可插拔代价模块加权求和;同 Nav2 critic 架构的智慧 | §7.2 |
| 9 | MJPC 极简范式 | 好仿真器 + 极简采样器 + 少而精的代价 = 人形站起来 | §7.3 |
| 10 | MJPC 跑 CPU | 多核 CPU 线程池并行,不是 GPU | §7.3 |
| 11 | 瓶颈在环境侧 | 采样控制成败越来越取决于仿真和代价,而非优化器精巧 | §7.3 |
| 12 | 维度灾难三子问题 | 维度本身高 / 一次搜不动 / 每周期重搜浪费 | §7.4 |
| 13 | 三把钥匙 | 样条降维 / 退火 / warm-start,各治一个子问题 | §7.4 |
| 14 | 不靠加样本 | 线性加样本追不上指数膨胀,要降维+提效率 | §7.4 |
| 15 | 动作空间是关键决策 | 力矩级 vs 位置目标决定 sim-to-real 难易 | §7.5 |
| 16 | 力矩级表现力强但脆 | 全靠在线优化,模型误差直接传导 | §7.5 |
| 17 | 位置目标 + PD 鲁棒 | PD 闭环吸收模型误差/扰动,RL 腿足也这么选 | §7.5 |
| 18 | 划线母题 | 哪部分在线现算、哪部分交给固定/预备好的东西 | §7.3/§7.5/§7.6 |
| 19 | 梯度采样互补 | OCS2 强在光滑结构化稳态,MPPI 强在非结构化非光滑 | §7.6 |
| 20 | 混合是趋势 | OCS2 稳态 + MPPI 敏捷 + RL 微调,按场景分工 | §7.6 |
| 21 | 选型五问 | 步态已知?要硬约束?光滑?上真机?有算力? | §7.6 |
| 22 | 累积项目加动作空间 | 给 Mini-MPPI 加 action_space 维度 + 接触任务 | §7.7 |
全章一句话速记¶
接触让梯度法失效、却是采样法的免费主场(因为采样只求值不求导);要把这优势从仿真兑现到几十自由度的真机,靠样条降维+退火+warm-start 治维度灾难、靠位置目标+PD 治 sim-to-real、靠与 OCS2/RL 的互补混合各取所长。
如果只能记一句,就记这句——它串起了本章从"为什么采样适合接触"到"怎么把它落到真实腿足"的完整逻辑。
学完自检¶
读完本章,检查你能否回答(不能就回对应小节):
- 为什么接触让 iLQR/DDP 失效,而 MPPI 不受影响?用"求值 vs 求导"一句话说清。(§7.1)
- 采样法绕开了接触的什么困难、没绕开什么困难?(§7.1、§7.4)
- STORM 为什么在关节空间而非任务空间做 MPC?张量化和手写 CUDA 各适合什么场景?(§7.2)
- MJPC 跑在什么硬件上?它的核心论点"瓶颈在哪"是什么?(§7.3)
- 治维度灾难的三把钥匙各是什么、各治哪个子问题?为什么不能只靠加样本?(§7.4)
- 力矩级和位置目标 + PD 的根本区别是什么?为什么后者 sim-to-real 更友好?(§7.5)
- 采样式全身 MPC 和 OCS2 WBC 各强在哪?选型该问哪几个问题?(§7.6)
- 本章给累积项目加了什么维度?推箱子 demo 验证了哪两个论点?(§7.7)
承上启下¶
承上:本章是前六章采样内核在"接触丰富 + 高维真实机器人"上的总兑现。它直接用到——第 2 章的 MPPI 主循环与 warm-start、第 3 章的平滑思想、第 4 章的 CEM/Predictive Sampling 与多目标 critic、第 5 章的扩散退火、第 6 章的在线-离线视角。可以说本章是前六章工具的"实战大检阅"。
启下:本章建立的几个判断力——"按任务在互补边界上选方法"(§7.6)、"按 sim-to-real 需求选动作空间"(§7.5)、"瓶颈常在环境侧而非优化器"(§7.3)——会直接迁移到后续的其他应用方向(导航、自动驾驶、无人机)。那些方向同样要在"梯度 vs 采样""在线 vs 离线""精确模型 vs 黑箱仿真"之间权衡,本章练就的选型思维是通用的。
本章的累积项目¶
前几章你一直在搭一个"统一采样优化器"UnifiedSamplingPlanner:第 2 章给它基础 MPPI 内核,第 4 章加上 weight(加权方式:mppi/cem)、noise(噪声类型:gaussian/colored)、cov(协方差:fixed/adaptive)三个正交维度,第 5 章加上 schedule(退火调度)维度,第 6 章加上 model_source(动力学从哪来:true/learned)和 terminal(终端怎么处理:truncate/value)两个维度。本章给它加上一个全新的、本章专属的维度:action_space(动作空间——你采的"控制"到底是力矩还是位置目标),并第一次让它在一个**接触任务**上工作。
具体地,把优化器/环境的接口扩展成支持两种动作空间:
class UnifiedSamplingPlanner:
def __init__(self, weight='mppi', noise='gaussian', cov='fixed', schedule=None,
model_source='true', terminal='truncate', action_space='pd_target'):
# 前几章的维度
self.weight = weight # 'mppi' / 'cem' —— 第4章
self.noise = noise # 'gaussian' / 'colored' —— 第4章
self.cov = cov # 'fixed' / 'adaptive' —— 第4章
self.schedule = schedule # None / 退火调度 —— 第5章
self.model_source = model_source # 'true' / 'learned' —— 第6章
self.terminal = terminal # 'truncate' / 'value' —— 第6章
# 本章新增的维度
self.action_space = action_space # 'torque'(力矩级) / 'pd_target'(位置目标+PD) —— 本章
为什么 action_space 值得作为一个独立维度加进来?因为它和前面所有维度**正交**,却独立地决定了一个前面任何维度都管不到的东西——sim-to-real 的难易(§7.5)。weight/noise/cov/schedule 调的是"怎么采样、怎么搜索",model_source/terminal 调的是"用什么模型、怎么收尾",而 action_space 调的是"你输出的控制怎么被机器人执行"。同一套采样搜索(比如 mppi + colored + 退火),配 action_space='torque' 就是 DIAL-MPC 那条表现力强但脆弱的路线,配 action_space='pd_target' 就是 Whole-Body MPPI 那条鲁棒友好的路线。一个维度的切换,决定了你的规划器是"仿真里惊艳、真机上抖动"还是"真机上稳健可用"。
把这个维度和前面的维度组合,你的统一优化器现在能表达的方法谱系又宽了一截:
action_space='torque', schedule=anneal, weight='mppi':DIAL-MPC 式全阶力矩退火 MPPI(§7.5、第 5 章 §5.3)action_space='pd_target', weight='mppi':CMU Whole-Body MPPI 式位置目标采样控制(§7.5、§7.6)action_space='pd_target', model_source='learned':在学习的模型上规划、输出位置目标——一种 sim-to-real 友好的 model-based 路线(§7.6 思路三的雏形)
这个累积项目还能怎么往下走? §7.7 的 demo 是 1D 的,三把钥匙里只用到了 warm-start。一个自然的延伸是把它推向更高维、补齐另外两把钥匙:给 _update 加样条节点参数化(钥匙①),让决策变量从"每步一个控制"变成"少数节点 + 插值";再把第 5 章的 schedule 维度真正接到接触任务上(钥匙②),在更非凸的接触配置(如要绕过障碍再推箱子)里看退火发挥作用。如果你想在课程之外继续打磨,可以把 env.step 从 1D 接触换成一个真实的 MuJoCo 全身 rollout——那一步迈出去,你的统一规划器就从"玩具接触 demo"成长为"能驱动真实腿足的全身采样 MPC"。这条从 demo 到真机的路线图,§7.7 末尾已经给你画好了。
延伸阅读¶
核心论文(按阅读顺序):
- STORM(Bhardwaj 等,CoRL 2021 Oral,arXiv:2104.13542):本章 §7.2 的源头。重点读它为什么坚持在**关节空间**做 MPC(而非任务空间 + OSC)、cost buffer 怎么组织多目标、以及张量化批量 rollout 的实现。配合
NVlabs/storm代码看更直观——它是理解"用深度学习框架而非手写 CUDA 做采样 MPC"的最佳范例。 - Predictive Sampling / MJPC(Howell 等,arXiv:2212.00541):本章 §7.3 的源头,也是第 4 章 §4.4 的延续。重点读它的极简采样器为什么"意外地有效"、任务的 residual 代价怎么写、以及它坦诚承认的局限("只能控制 MuJoCo 能仿真的东西")。强烈建议跑它的交互式 GUI——亲手对比 Predictive Sampling 和 iLQG,看人形怎么站起来,比读十遍论文管用。
- Real-Time Whole-Body Control with MPPI(Alvarez-Padilla 等,CMU,ICRA 2025,arXiv:2409.10469):本章 §7.4–§7.6 的主角。重点读它的三个关键设计——样条节点降维、采**关节位置目标**(而非力矩)、用 MuJoCo 并行 rollout——以及它在真实 Go1 上的越野行走、爬箱、推箱实验。它是"采样式全身控制能上真机"的里程碑,也是位置目标路线的代表。项目页
whole-body-mppi.github.io有视频。 - DIAL-MPC(Xue 等,CMU,ICRA 2025):第 5 章 §5.3 已详讲,本章 §7.5 作为力矩级路线的对照。和 Whole-Body MPPI 对照读最有收获——同样是全身采样 MPC,一个采力矩(DIAL-MPC,表现力强、需退火和精确仿真)、一个采位置目标(WBMPPI,鲁棒、简单),完美诠释 §7.5 的动作空间之争。
背景与对照工作:
- OCS2 / Crocoddyl(你的前置课程):梯度式全身 MPC 的代表。本章 §7.6 反复和它对照——理解它的 gait scheduler、Pinocchio 解析动力学、QP 硬约束,才能体会采样法补上了什么、又在哪些地方不如它。
- MoveIt STOMP / CHOMP:采样式/优化式轨迹规划的前辈(§7.2 对照)。理解它们(离线、CPU、固定噪声)能看清 STORM 把这套思想升级成在线 GPU MPC 的进步在哪。
- 腿足 RL(IsaacLab + PPO/SAC 的 locomotion 工作):你的 RL 背景。§7.5 揭示了采样式全身控制和 RL 在动作空间上殊途同归(都倾向位置目标 + PD)——把这条线索和你训过的腿足策略对照,理解 sim-to-real 这个共同约束。
代码:
NVlabs/storm:STORM 官方实现,PyTorch。按 §7.2 的设计读:先看 cost buffer 怎么拼多目标,再看批量 rollout 的张量操作。google-deepmind/mujoco_mpc:MJPC 官方实现 + 交互式 GUI。先跑 GUI 体验,再按 §7.3 读mjpc/tasks/下的任务 residual 定义。- DIAL-MPC 官方仓库(JAX/Brax + MuJoCo MJX):第 5 章延伸阅读已列。想看力矩级 + GPU 大规模并行退火的完整实现,读它。
一条务实的学习路线:先跑通本章 §7.1 的接触地形 demo 和 §7.7 的推箱子 demo(纯 NumPy,吃透"采样不怕接触"和"位置目标更鲁棒"两个核心现象)→ 装 MJPC 跑 GUI(亲眼看采样式控制让人形站起来,并对比采样 vs iLQG)→ 读 Whole-Body MPPI 论文 + 项目页视频(理解真机上的关键设计)→ 若有 GPU 再碰 DIAL-MPC(力矩级 + 退火的硬核版)。如果时间有限,哪怕只完成前两步(两个 demo + MJPC GUI),你对本章核心的理解也会甩开"只读过论文"的人一大截——因为"接触从代价涌现""位置目标比力矩鲁棒"这两件事,看一眼 demo 和 GUI 里机器人的行为,胜过读十遍文字。
怎么在这个高产方向里筛选新论文(元建议):腿足全身采样控制是 2024 年后突然变热的方向,每个顶会都有新工作。用本章的框架快速筛选:(a) 先看它在**哪个动作空间**工作(力矩还是位置目标,§7.5)、用**什么仿真器**做 rollout、上没上**真机**——这三点立刻给它定位;(b) 看它用了**三把钥匙**的哪几把、有没有新的降维/搜索机制(§7.4);(c) 看它和梯度式/RL 是**竞争还是混合**(§7.6),混合的话接口怎么设计;(d) 优先读有真机实验、有开源代码的工作——这个方向"仿真里能跑"和"真机上能用"差距巨大(§7.3 易错点),真机结果含金量高得多。在论文产量远超阅读能力的领域,"会用框架定位"比"读得多"更决定成长速度。
本章与后续章节的关系¶
本章在整个采样式 MPC 体系里的位置,以及它和前后章节的关系:
与前序章节(第 2–6 章):本章是采样内核的"实战总兑现"。第 2 章建立内核(MPPI 主循环、warm-start)、第 3 章给变体和平滑、第 4 章给统一框架(weight/noise/cov)和极简采样器(Predictive Sampling)、第 5 章给扩散退火、第 6 章给学习模型——这些工具在本章被一次性地、综合地用到接触丰富的高维真实机器人上。本章几乎每一节都在调用前面的某个零件:§7.1 用第 2 章的"只 rollout 不求导"、§7.2 用第 2 章的解耦哲学和第 4 章的 critic 架构、§7.3 用第 4 章的 Predictive Sampling、§7.4 用第 5 章的退火和第 2 章的 warm-start、§7.6 用第 6 章的在线-离线视角。可以说本章是前六章的"集大成应用"。
与你的前置课程(梯度式 MPC / RL):本章是采样式方法和你已有两套工具的交汇点。它和梯度式全身 MPC(OCS2 WBC)的对照贯穿 §7.6——两者沿"光滑结构化 ↔ 非光滑非结构化"这条线互补;它和 RL 的呼应出现在 §7.5(动作空间殊途同归)和 §7.6(RL 当 learned prior、做 sim-to-real 微调)。学完本章,你手里的"采样 MPC""梯度 MPC""RL"三套工具不再是孤立的,而是被"按任务特性在互补边界上分工/混合"的判断力串了起来。
与后续应用章节(导航/自驾/无人机等):后续章节会把本系列的采样工具落到更多机器人领域。本章建立的几个判断力是通用的、会直接迁移:§7.6 的"按任务在互补边界上选方法"、§7.5 的"按 sim-to-real 需求选动作空间/接口"、§7.3 的"瓶颈常在环境侧(仿真/代价)而非优化器"。腿足全身是接触最复杂、维度最高的应用,啃下它之后,导航/自驾/无人机这些接触相对简单的领域,你会觉得采样 MPC 的应用思路顺理成章——它们是本章方法论在更温和场景下的变奏。
与具身智能的大图景:本章把你接到了当下机器人最前沿的一个交汇点。腿足全身采样控制(WBMPPI、DIAL-MPC)、它与 RL/扩散策略的混合(learned prior)、它对高保真仿真的依赖(MuJoCo/Isaac)——这些正是"具身智能"这个大方向里"控制与学习如何结合"的核心议题。理解了本章,你再看具身大模型、sim-to-real、loco-manipulation 等工作,都能用"采样发现 + 学习先验 + 在线优化兜底"的框架去定位它们。
把这些关系连起来看,本章既是前六章采样工具的实战检阅,又是采样法与梯度法/RL 的交汇点,还是通往腿足/操作/具身智能前沿的一扇门。学完它,你不只是多会了几个全身控制方法,而是拿到了一套"在接触丰富的高维真实机器人上,如何在多种方法间权衡选型"的判断力——这套判断力,比任何单一方法都更经得起这个快速演进的领域的考验。
再放到更大的图景里看一眼:腿足机器人正从"实验室里编排好步态的稳态行走"走向"真实世界里应对非结构化地形、与环境丰富交互"的通用移动操作。这个跃迁的技术底座,恰恰是本章讲的——能处理不可微接触、能在高维上实时搜索、能从仿真鲁棒地迁移到真机的控制方法。采样式全身控制不是这个故事的全部(它要和梯度法、RL 协同),但它补上了一块以前缺失的关键拼图:让机器人能做那些"接触序列本该被发现而非预先编排"的动作——爬上意外的障碍、从摔倒中爬起、用整个身体完成一个之前没人教过的动作。你在本章建立的能力,正站在这个跃迁的技术前沿上。
故障排查手册¶
把本章方法在实践中最常见的故障,按"症状 → 可能原因 → 排查与对策"整理成手册。遇到问题先按症状定位。
场景一:仿真里全身 MPPI 很稳,一上真机就剧烈抖动/摔倒¶
症状:仿真中机器人动作流畅稳定,部署到真机后上电即抖动、动作变形、甚至直接摔倒,且仿真里无论如何复现不出。
可能原因与排查: 1. 动作空间是直接力矩(最常见根因)。力矩级对模型误差极敏感(§7.5),真机的电机动力学、延迟、摩擦和仿真对不上,误差直接传导成抖动。排查:确认你采的是力矩还是位置目标。 2. rollout 仿真和真机的物理参数差太多(sim-to-real gap,§7.1 易错点)。接触刚度、摩擦、惯量、延迟不一致。 3. PD 增益不当(若用位置目标)。\(K_p\) 太大导致刚性过强振荡(§7.5 陷阱3)。
对策(按优先级):先把动作空间从力矩换成**位置目标 + PD**(§7.5)——这一步通常能立刻把鲁棒性提一个量级;再做仿真校准(辨识真机的摩擦/惯量/延迟,把 rollout 仿真调到接近真机);若用位置目标,单独测 PD 跟踪、把增益调到平稳准确。**不要**第一反应去加滤波或调 MPPI 参数——根因多半在动作空间和 sim-to-real,不在优化器。
场景二:全身 MPPI 在高维上找不到好轨迹,机器人原地抽搐或不动¶
症状:把 MPPI 上到几十自由度的全身后,采样出的全是高频抖动般的烂控制,机器人原地抽搐、找不到能站稳/前进的协调动作。
可能原因与排查: 1. 没做样条节点降维(钥匙①缺失,§7.4)。直接对 \(H\) 步 × 几十关节每维独立采样,决策几百维,样本撒不满。排查:你的决策维度是多少?有没有用样条节点参数化? 2. 没退火(钥匙②缺失)。单尺度采样在高维非凸里陷局部。 3. 样本数虽多但仍不够——但注意这通常**不是**主因(§7.4 陷阱1:加样本追不上指数膨胀)。
对策:按三把钥匙的顺序上——先加**样条节点降维**(把决策维度从几百降到几十,最立竿见影);再加**退火**(多级方差由大到小,§7.4 钥匙② / 第 5 章);确保有 warm-start(钥匙③)。三把钥匙是真实全身的标配,缺一不可。**不要**只靠堆样本硬扛——性价比极低。
场景三:机器人能动但动作僵硬、做不出需要快速响应的动作¶
症状:机器人能完成平缓动作(慢走、站立),但一遇到需要快速变化控制的动作(快速踏步、起跳、落地缓冲)就做不出来,动作显得"反应迟钝"。
可能原因与排查: 1. 样条节点太少(降维过头)(§7.4 易错点)。节点太稀疏,插值太平滑,必需的快速控制曲线无法被表达。排查:你的样条节点数是多少?任务需要多快的控制变化? 2. PD 增益太小(若用位置目标)。\(K_p\) 不足,关节跟不上目标(§7.5 陷阱3)。 3. 规划时域 \(H\) 太短,看不到需要提前准备的快速动作。
对策:增加样条节点数,或在需要快速响应的时段**非均匀加密**节点(§7.4 练习3);节点数应匹配任务的控制带宽,而非凭感觉。若用位置目标,适当提高 \(K_p\) 让关节跟得上。检查 \(H\) 是否覆盖了动作所需的预见时间。
场景四:多目标代价里某一项"吃掉"全部,机器人只顾一个目标¶
症状:机器人只顾某个目标(如只顾避碰/只顾不超限位),完全不做主任务(如不往目标走)。
可能原因与排查: 1. 代价项量纲不归一(§7.2 陷阱2)。不同代价项数值范围差几个数量级,大数值项隐式获得极高权重。排查:打印每条 rollout 各代价分项的数值范围,看是否某项数量级碾压其他。 2. 权重设置失衡。
对策:先把每项代价归一化到可比量级(除以各自典型尺度),再调相对权重;调参时一项一项加(先只开主任务确认能完成,再逐项叠加并观察)。这和 §7.2 陷阱2、第 5 章故障排查"代价项失衡"是同一套方法。
场景五:rollout 出的轨迹"穿过"地面/物体,优化结果在真机不可行¶
症状:可视化 rollout 轨迹,发现脚陷进地里、推杆穿过箱子;优化出的控制在真机上完全不可行。
可能原因与排查: 1. rollout 用的模型没有(正确的)接触(§7.1 陷阱1)。用了无接触的简化模型,或接触建模错误。排查:rollout 用的是什么仿真器?接触是否被正确建模?把一条 rollout 轨迹可视化,看脚/手有没有正确踩在表面上、有没有穿透。 2. 接触参数不当(穿透容差太大、刚度太低)。
对策:用带完整、正确接触动力学的仿真器做 rollout(MuJoCo/Isaac/Drake);检查接触参数(刚度、穿透容差、摩擦)合理。记住 §7.1 的核心:采样法不求接触的导数,但**必须用会正确算接触的仿真器**——难点转移给仿真器,没消失。
场景六:开环回放/低频重规划下机器人发散,闭环高频下却正常¶
症状:MPC 高频重规划时机器人正常,但在两次重规划之间的"开环"时段(或人为做开环回放测试)机器人明显发散。
可能原因与排查: 1. 动作空间是开环力矩,缺乏底层反馈(§7.7 现象二)。两次 MPC 更新之间没有反馈顶住扰动,误差累积。排查:底层跑的是什么?有没有 PD 这类高频反馈环? 2. MPC 重规划频率太低,开环时段太长。
对策:用**位置目标 + 底层高频 PD**(§7.5/§7.7)——PD 在 MPC 两次更新之间的开环时段顶住扰动,这正是真机的标准结构(PD 跑 1 kHz、MPC 跑 50 Hz)。若必须用力矩级,要么提高 MPC 频率缩短开环时段,要么叠一个鲁棒底层环。
概念与 API 速查表¶
核心概念速查¶
| 概念 | 一句话定义 | 关键点 |
|---|---|---|
| 接触不可微 | 接触切换时动力学导数不连续/不存在 | 梯度法的克星(§7.1) |
| 求值 vs 求导判据 | 更新公式用导数则怕不可微,用值则不怕 | 一眼判断能否碰接触(§7.1) |
| 接触序列涌现 | 接触从代价最小化中自然产生,无需预设 | 采样法不要 gait scheduler(§7.1) |
| 张量化 rollout | 用批量张量(K维并行)而非手写CUDA | 开发快、峰值性能略低(§7.2) |
| 代价缓冲 / critic | 可插拔、加权求和的多目标代价模块 | STORM=PyTorch, Nav2=C++插件(§7.2) |
| 关节空间 MPC | 在关节空间规划,能管关节限位/奇异/自碰 | 任务空间MPC的盲区(§7.2) |
| 极简采样器 | Predictive Sampling,CEM精英=1的极端 | 好仿真+它就够(§7.3) |
| 三把钥匙 | 样条降维 / 退火 / warm-start | 治维度灾难三子问题(§7.4) |
| 动作空间 | 采的"控制"=力矩 or 位置目标 | 决定sim-to-real难易(§7.5) |
| 划线母题 | 哪部分在线现算、哪部分交给固定/预备件 | 贯穿§7.3/§7.5/§7.6 |
| 互补边界 | 光滑结构化↔非光滑非结构化 | OCS2与MPPI分居两端(§7.6) |
| 开环回放 | 不重规划地执行已规划序列 | 暴露动作空间鲁棒性差异(§7.7) |
方法-场景速查¶
| 方法 | 动作空间 | 平台 | 真机验证 | 最适场景 |
|---|---|---|---|---|
| STORM | 关节速度/力矩 | GPU(张量化) | 机械臂(Franka/UR5) | 反应式操作、动态避障 |
| MJPC (Predictive Sampling) | 力矩(经样条) | 多核CPU | 多在仿真 | 任务可行性验证、教学、代价调试 |
| Whole-Body MPPI (CMU) | 关节**位置目标**+PD | GPU(MuJoCo并行) | 真实Go1 | 非结构化行走、爬箱、推箱(sim-to-real友好) |
| DIAL-MPC | **力矩**级(全阶) | GPU(MJX/Brax) | 真实Go2 | 极限敏捷动作、精确跳跃(需退火+校准) |
| OCS2 WBC (梯度式,前置) | 力矩(经WBC) | CPU | 广泛 | 已知步态的稳态行走(高精度+硬约束) |
选型五问速查(§7.6)¶
| 问题 | 是 → | 否 → |
|---|---|---|
| 1. 步态/接触序列已知固定? | OCS2(梯度) | MPPI(采样) |
| 2. 需严格保证硬约束? | OCS2 / MPPI上叠WBC | MPPI可行 |
| 3. 动力学全程光滑可微? | 梯度法高效 | 采样法主场 |
| 4. 上真机、对模型误差敏感? | 位置目标+PD,考虑叠RL | 力矩级可考虑 |
| 5. 有GPU/多核做并行rollout? | 全身MPPI/DIAL可行 | MJPC(CPU)或退回OCS2 |
关键 API 速记(累积项目)¶
| 维度 | 取值 | 含义 | 引入章节 |
|---|---|---|---|
weight |
mppi / cem | 加权方式 | 第4章 |
noise |
gaussian / colored | 噪声类型 | 第4章 |
cov |
fixed / adaptive | 协方差 | 第4章 |
schedule |
None / 退火调度 | 多级方差缩放 | 第5章 |
model_source |
true / learned | 动力学来源 | 第6章 |
terminal |
truncate / value | 终端处理 | 第6章 |
action_space |
torque / pd_target | 动作空间 | 本章 |
研究与实践建议¶
如果你是研究者¶
找问题的方向:本章末"前沿与开放问题"列的几个方向都值得深挖,尤其是——力矩级 sim-to-real(如何让全阶力矩 MPPI 在真机上像位置目标一样鲁棒)、采样式全身控制的理论保证(收敛性、样本复杂度、安全性的概率保证)、向人形扩展(更高维下三把钥匙够不够)。这些是真问题、有真机会。
做实验的纪律:这个方向"仿真里能跑"和"真机上能用"差距巨大(§7.3 易错点)。做研究务必——(a) 报告真机结果,或至少在带模型误差/扰动的仿真里评估(别只报理想仿真);(b) 明确说清动作空间(力矩还是位置目标,§7.5)和仿真器,这决定了结果的可比性和可复现性;(c) 做消融,分清你的提升来自哪——是新算法,还是仅仅因为换了更好的仿真器/动作空间(很多"提升"其实是后者,§7.3 本质洞察)。
站在巨人肩上:本章的方法都开源(STORM、MJPC、WBMPPI、DIAL-MPC)。先复现一个基线、在它上面改,比从零造轮子高效得多。用本章的框架(§7.4 三把钥匙、§7.5 动作空间、§7.6 互补)给你要改进的工作精确定位,找到它在哪个维度上有提升空间。
警惕"换皮":这个高产方向里有不少"换个名字的 MPPI + 退火 + 样条降维"。用本章概念框架(动作空间?仿真器?三把钥匙的哪几把?和梯度/RL 竞争还是混合?)几分钟就能判断一篇新工作是真有新意还是换了层包装——把精力花在真有新意的工作上。
如果你是工程师¶
从最简起步:要在真机上做全身采样控制,先从位置目标 + PD 起步(§7.5),别一上来追力矩级。位置目标鲁棒、好调、大多数任务够用;确认任务确实需要极限力矩表现、且你有能力校准仿真,再考虑力矩级。同理,先用 MJPC(多核 CPU,§7.3)快速验证任务可行性、调代价、建直觉,再决定要不要上更重的 GPU 退火方案。
别重复造轮子:MJPC 提供了开箱即用的交互式框架和一堆任务模板,STORM/WBMPPI 提供了可参考的实现。先用现成的跑通你的任务,再针对性地改。
混合要渐进:如果要上 OCS2 + MPPI + RL 的混合(§7.6),别一步到位。先用单一方法把基础跑通,确认确有单一方法覆盖不了的场景,再针对性引入第二种方法,重点设计接口和切换逻辑(共享动作空间能极大简化对齐,§7.5/§7.6)。
仿真校准是关键投入:采样法把信任押在仿真器上(§7.1),所以在仿真校准(system identification——辨识真机的摩擦、惯量、延迟、接触参数)上的投入回报极高。这件事不性感但极其重要——很多"算法不 work"其实是"仿真和真机对不上"。
调参顺序:遇到问题别乱调。按本章故障排查手册的顺序——先确认动作空间和仿真保真度(最常见根因),再看三把钥匙是否齐全,再调代价权重(先归一化再调相对值),最后才动 MPPI 的 \(\lambda\)/\(K\)/\(\sigma\) 这些内核参数。
通用建议¶
无论研究还是工程,本章最该带走的不是某个具体方法,而是三个判断力:(1) 看到一个控制问题,先用"求值 vs 求导"判断它是不是采样法的主场(§7.1);(2) 看到一个全身采样方案,先问它的动作空间和三把钥匙(§7.4/§7.5);(3) 做方法选型,先用五问框架在互补边界上定位、考虑混合(§7.6)。 这三个判断力是通用的、可迁移的,会比任何单一方法活得更久。
动手前的实践 Checklist¶
故障排查手册帮你诊断**已经出问题**的系统,而这份 checklist 是**预防性**的——在你动手搭一个采样式接触控制系统之前过一遍,能帮你避开本章反复警告的那些坑。按从立项到部署的顺序排列:
立项与选型阶段: - [ ] 用"求值 vs 求导"判断过这个任务该用采样还是梯度了吗?接触是主角还是配角(§7.1、§7.6)? - [ ] 过了一遍 §7.6 选型五问(步态已知?要硬约束?光滑?上真机?有算力?)了吗? - [ ] 考虑过混合架构吗(稳态部分是否更适合交给梯度法/OCS2,§7.6)? - [ ] 任务的接触主要是哪种类型(通断/黏滑/刚性冲击),仿真器能不能把它算准(§7.1 系统分类)?
搭建内核阶段: - [ ] rollout 用的仿真器有正确的接触动力学吗(别用无接触/错接触的简化模型,§7.1 易错点)? - [ ] 高维全身上了三把钥匙吗——样条降维、退火、warm-start,缺一个就会在对应子问题栽(§7.4)? - [ ] 决策维度降到合理范围了吗?样条节点数够表达任务所需的控制变化、又不过多(§7.4 易错点)? - [ ] rollout 的批量维 \(K\) 真的并行了吗(别写成对每条轨迹的 Python 循环,§7.2)?关节空间的话 FK 也批量化了吗(§7.2 FK 那段)?
动作空间与 sim-to-real 阶段: - [ ] 动作空间从位置目标 + PD 起步了吗(除非确有极限力矩需求,§7.5)? - [ ] rollout 仿真的 PD 环和真机/驱动器实际跑的 PD(增益、频率)一致吗(§7.5 易错点、电机模式那段)? - [ ] PD 增益单独调过吗(先无接触调平稳跟踪,再加接触看冲击,§7.5 PD 增益那段)? - [ ] 做过接近真机条件的鲁棒性评估吗(加模型误差/扰动,或开环回放,别只看理想闭环 demo,§7.7)?
代价设计阶段: - [ ] 各代价项的量纲归一化了吗(先归一化,再用权重表达偏好,§7.2/§7.3)? - [ ] 代价写成"趋零的偏差量"了吗?是模块化、可单独测试/调权重的吗(§7.2 cost buffer、§7.3 residual)? - [ ] 有没有用代价硬编码接触时机/动作序列(那会扼杀涌现,应该用温和 shaping,§7.7 陷阱3、§7.3 residual)?
混合与切换阶段(若用混合架构): - [ ] 多个控制器共享同一动作空间吗(简化对齐,§7.6 混合骨架)? - [ ] 切换瞬间用当前真实状态重置 warm-start 了吗(防切换抖动,§7.6 混合骨架)? - [ ] 切换条件加了迟滞/最小驻留时间吗(防模式抖动,§7.6 模式抖动那段)?
本质洞察:这份 checklist 的每一条,背后都是本章某处用"现象 → 原因 → 对策"讲过的一个坑。把它过一遍,等于在动手前就把本章的经验"预装"进你的系统设计,而不是等踩了坑再回来查故障排查手册。好的工程师不是不犯错,而是把别人踩过的坑变成自己的检查清单——这份 checklist 就是把本章踩坑经验清单化的产物。建议你真的把它存下来,下次搭采样式接触控制系统时对照着走一遍。
版本信息速查¶
本章涉及的工作、平台和它们的关键信息(截至 2025 年,部分信息可能随版本演进,请以官方仓库为准):
| 项目 | 出处/年份 | 平台/依赖 | 动作空间 | 代码 | 关键事实 |
|---|---|---|---|---|---|
| STORM | Bhardwaj 等, CoRL 2021 Oral, arXiv:2104.13542 | PyTorch, 单GPU | 关节速度/力矩 | NVlabs/storm |
关节空间MPPI;张量化批量rollout;每步<0.02s(~50Hz);Franka/UR5真机 |
| MJPC / Predictive Sampling | Howell 等, arXiv:2212.00541, 2022 | C++, 多核CPU, MuJoCo | 力矩(经样条) | google-deepmind/mujoco_mpc |
三planner(iLQG/GD/Predictive Sampling);交互GUI;多核CPU非GPU;极简采样器意外有效 |
| Whole-Body MPPI | Alvarez-Padilla 等, CMU, ICRA 2025, arXiv:2409.10469 | MuJoCo并行, GPU | 关节**位置目标**+PD | whole-body-mppi.github.io |
首个真机四足(Go1)全身采样MPC;样条节点降维;越野/爬箱/推箱 |
| DIAL-MPC | Xue 等, CMU, ICRA 2025 | JAX/Brax+MuJoCo MJX, GPU | **力矩**级(全阶) | 见第5章延伸阅读 | 扩散退火全身力矩MPPI;真实Go2带载跳跃50Hz;免训练;最佳论文入围 |
| OCS2 | (前置课程) | C++, Pinocchio, CPU | 力矩(经WBC) | leggedrobotics/ocs2 |
梯度式全身MPC;gait scheduler;解析动力学+QP硬约束 |
易混淆点提醒(本章特别核实过的事实): - MJPC 跑在**多核 CPU**(论文在多核 CPU 工作站上运行),不是 GPU——这点常被想当然搞错(§7.3 陷阱1)。 - CMU Whole-Body MPPI 采**关节位置目标**(走 PD),不是直接力矩——和 DIAL-MPC 的力矩级形成 §7.5 的核心对照。 - Predictive Sampling 的**算法**在第 4 章 §4.4 讲,本章 §7.3 讲它在腿足上的**应用**——别把两处混为一谈。
跨章综合应用¶
本章是采样内核的实战检阅,正好用一个贯穿性的综合场景,把第 2 章到第 7 章的工具串起来用一遍。
场景:你要为一个四足机器人做一套控制系统,任务谱系从"平地巡逻"到"爬越不规则障碍"到"驮货推门"。请综合运用整个采样式 MPC 工具箱设计方案。
第一步——定位任务,分配方法(§7.6)。先用五问框架把任务谱系分类:平地巡逻(步态已知、要稳要省)→ 交给梯度式 OCS2;爬越不规则障碍、从摔倒恢复(接触序列要发现、非结构化)→ 交给采样式全身 MPPI;最终都要上真机 → 用 RL 做 sim-to-real 微调。这就是 §7.6 的"OCS2 稳态 + MPPI 敏捷 + RL 微调"混合架构。
第二步——为采样部分搭内核(第 2–4 章)。采样式全身 MPPI 的内核用第 2 章的 MPPI 主循环(warm-start、滚动时域),用第 3 章的相关噪声做平滑(避免抖动力矩),用第 4 章的统一框架选 weight(mppi 加权)。多目标代价(到达、平衡、避碰、不超限)用第 4 章 §4.5 / §7.2 的 critic / cost buffer 架构组织,逐项归一化后加权。
第三步——治维度灾难(§7.4 + 第 5 章)。几十自由度的全身,三把钥匙全上:样条节点降维(钥匙①,决策维度从几百降到几十)、扩散退火(钥匙②,复用第 5 章 §5.3 的多级退火,高温探索低温精修)、warm-start(钥匙③,第 2 章)。
第四步——选动作空间,保 sim-to-real(§7.5)。真机部署,动作空间选位置目标 + PD(鲁棒、和 RL 微调共享动作空间),除非某个极限敏捷动作(如跨越大沟的起跳)确实需要力矩级表现力,才对那个动作单独用 DIAL-MPC 式力矩退火。
第五步——考虑学习增强(第 6 章 + §7.6 思路三)。若要进一步提升,可以把 RL/扩散策略学到的先验当 MPPI 的 learned prior(§7.6 思路三),或用第 6 章 TD-MPC 学一个世界模型在其上规划(适合感知复杂、模型难精确建的子任务)。
第六步——调试纪律(本章故障排查 + 各章陷阱)。遇到问题按故障排查手册顺序定位:真机抖动先查动作空间和仿真保真度(§7.7/§7.1),高维找不到解先查三把钥匙是否齐全(§7.4),代价失衡先归一化(§7.2)。
这个方案几乎调用了整个系列的每一件工具——这正是本章作为"实战检阅"的意义:你学的不是孤立的方法,而是一套能协同解决真实复杂问题的工具箱。
跨章综合思考题¶
这些题需要你跨越多章、综合本章和前序内容才能答好,适合检验你是否真正融会贯通:
-
(§7.1 + 第 1–2 章) 第 1 章讲路径积分控制的理论根基是"用采样估计期望、不需要梯度",第 2 章把它工程化。本章 §7.1 说这正是采样法能碰接触的根本原因。请把这条线串起来:从路径积分的数学本质(第 1 章),到"只 rollout 不求导"的工程实现(第 2 章),到"不可微接触对它透明"(本章 §7.1)——这是同一件事在三个层面的体现吗?请阐述这个"一以贯之"的逻辑。
-
(§7.4 + 第 4 章 + 第 5 章) 治维度灾难的三把钥匙里,"退火"(钥匙②)来自第 5 章、和第 4 章 iCEM 的"复用精英 + 有色噪声"有何异同?如果把 iCEM(第 4 章)、扩散退火(第 5 章)、本章三把钥匙放在一起看,它们对抗维度灾难的思路能不能统一成一个更高层的原则?请尝试提炼这个原则(提示:都在"用结构/历史/由粗到精来省搜索")。
-
(§7.5 + §7.6 + 第 6 章) §7.5 说采样式全身控制和 RL 在动作空间上殊途同归(都倾向位置目标 + PD),§7.6 说 RL 可以当 MPPI 的 learned prior,第 6 章的 TD-MPC 把学习模型和规划结合。把这三条线索综合:设计一个"TD-MPC 世界模型 + RL 策略先验 + 采样 MPPI 在线优化 + 位置目标动作空间"的完整 loco-manipulation 系统。它的每个部件分别解决什么问题?这个系统在"模型驱动 ↔ 数据驱动"的光谱上处于什么位置?
-
(§7.6 + 全系列) 本章反复出现一个"划线母题"——哪部分控制交给在线现算、哪部分交给固定/预先准备好的东西(§7.3/§7.5/§7.6)。请回顾整个采样式 MPC 系列(第 2–7 章),找出这个母题在每一章的体现(提示:第 2 章滚动时域只执行第一步、第 4 章 iCEM 复用精英、第 5 章 warm-start 当退火第 0 级、第 6 章学习价值摊销长期回报……)。这个母题是不是整个系列最深的一条暗线?请阐述你的理解。
-
(开放·面向未来) 假设五年后,可微仿真和神经接触模型都成熟了——接触变得"可微且仿真精确"。这会动摇本章 §7.1 的核心论点("采样适合接触因为它不求导")吗?梯度法会不会因此重新夺回接触控制的主场?还是说采样法仍有不可替代的价值(提示:全局探索、黑箱兼容、不需要可微)?请结合本章对"求值 vs 求导"的分析,论证你的判断。
这些题没有标准答案,重在你能否调动整个系列的知识、用本章建立的框架去综合思考。能把第 4 题(划线母题贯穿全系列)讲透,说明你已经不只是学会了若干方法,而是看见了贯穿它们的那条思想暗线——那才是这个系列最想留给你的东西。
本章给你的可迁移判断力¶
学完本章,最值得带走的不是某个具体方法(方法会过时、会被新工作替代),而是几个**可迁移的判断力**——它们超越腿足、超越采样 MPC,在你以后遇到的各种控制乃至更广的工程问题里都能用。把它们单独拎出来,配上"什么时候用、怎么用",作为本章最该内化的思维工具清单。
判断力一:用"求值 vs 求导"快速判断方法适用性。 拿到一个优化/控制问题,先问"我打算用的方法,它的更新靠目标函数的**值**还是**导数**?"——靠导数的(梯度下降、iLQR、SQP)怕不可微、怕黑箱、怕不连续;靠值的(MPPI、CEM、进化算法)不怕。用它你能一眼判断:这个问题(有接触?有黑箱仿真?代价不连续?)该用哪类方法。运用场景:任何时候你纠结"这问题能不能用梯度法",先做这个判断(§7.1)。
判断力二:用"结构换搜索"对抗高维。 遇到高维搜索/优化问题,别本能地加样本/加算力,先问"这个问题里有什么**结构**可以利用,从而不必在整个高维空间盲搜?"——好解光滑(→降维)、由粗到精能跨非凸(→退火/多尺度)、相邻问题解连续(→warm-start)、有数据先验(→learned prior)。运用场景:任何高维采样、优化、规划问题卡在"维度太高搜不动"时(§7.4、第四把钥匙那段)。
判断力三:在"表现力 vs 鲁棒"谱上选接口。 设计任何"上层决策 + 底层执行"的系统,都要决定"哪部分交给在线现算(表现力强但脆)、哪部分交给固定的鲁棒底层环(省事鲁棒但受限)"——这是一条连续谱,不是二选一。运用场景:选动作空间(力矩↔位置目标)、选控制接口、设计任何分层系统时(§7.5、电机模式那段)。
判断力四:先怀疑"环境侧",再怀疑"算法侧"。 系统不 work 时,别第一时间去换更复杂的算法,先查"环境侧"——仿真保真度够不够、代价/目标写得对不对。很多"算法不行"其实是"仿真和真机对不上"或"代价没写对"。运用场景:任何基于模型/仿真的方法(采样 MPC、model-based RL)出问题时的排查顺序(§7.3)。
判断力五:用互补边界做选型,默认考虑混合。 面对方法选择,别问"哪个方法最强",问"这个任务跨越了哪些特性/尺度,该怎么分解、每块交给最适合的方法"——成熟系统几乎都是混合/分层的,因为真实问题是异质的。运用场景:任何方法选型、系统架构决策时(§7.6、控制史那段)。
判断力六:警惕"硬阈值 + 带噪连续量 = 抖动"。 设计任何基于阈值的切换逻辑(模式切换、状态机),都要警惕:被切分的量若本质连续且带噪,落在阈值附近就会反复横跳。用迟滞、最小驻留时间、滤波预防。运用场景:混合架构的模式切换、任何条件触发的离散决策(§7.6 模式抖动那段)。
本质洞察:这六个判断力有一个共同点——它们都不是关于"某个方法怎么用"的具体知识,而是关于"怎么思考一类问题"的元知识。具体方法是"鱼",判断力是"渔"。本章讲了 STORM、MJPC、WBMPPI、DIAL-MPC 这些具体的"鱼",但真正希望你带走的是这六条"渔"——因为五年后这些具体方法可能都被更新的工作替代了,但"用求值vs求导判断适用性""用结构换搜索""在表现力鲁棒谱上选接口""先怀疑环境侧""按互补边界选型并考虑混合""警惕阈值抖动"这些思维方式,会一直有效。如果你只能从本章带走一页纸,就带走这六条——它们是本章方法论的精华,也是你面对这个快速演进领域时最经得起时间考验的装备。
收束语¶
这一章,我们把前六章打磨的采样内核,推上了它最有戏剧性的战场——接触丰富、维度爆炸的真实腿足机器人。
回头看这一路:§7.1 立起了全章的地基,一个朴素却深刻的论点——接触让梯度法失效,恰恰是因为梯度法要求导;而采样法只求值不求导,于是不可微的接触对它几乎透明。这不是魔法,是"把穿过接触求导这个不可能任务,换成用仿真器穿过接触再打分"的视角转换。这个论点之后被一遍遍兑现:STORM 在机械臂上、MJPC 在人形上、Whole-Body MPPI 和 DIAL-MPC 在真实四足上。
但本章也始终诚实地提醒你:采样让你"能碰"接触,不等于"碰得好"。维度灾难要靠三把钥匙治(§7.4),sim-to-real 要靠动作空间选对(§7.5),而面对一个具体任务,采样法也未必是最优解——它和梯度式 OCS2 沿"光滑结构化 ↔ 非光滑非结构化"互补,成熟的系统往往是两者加 RL 的混合(§7.6)。这份"知道一个方法强在哪、也知道它弱在哪、还知道何时该用别的"的清醒,比任何单一方法都珍贵。
如果这一章只在你心里留下一样东西,希望是那条反复出现的暗线——控制系统设计,本质是在反复决定"哪部分交给在线现算、哪部分交给预先准备好的东西"。在线优化 vs 离线策略(§7.3)、在线解力矩 vs 交给固定 PD(§7.5)、在线采样发现 vs 预设步态 + 学习先验(§7.6),都是这条线在不同层面的划法。看懂了这条线,你看任何机器人控制架构,都能一眼看出它把这条线划在了哪、为什么。
更大的图景里,腿足机器人正从"编排好步态的稳态行走"走向"真实世界里应对非结构化地形、与环境丰富交互"的通用移动操作。采样式全身控制补上了一块关键拼图——让机器人能做那些"接触序列本该被发现而非预先编排"的动作:爬上意外的障碍、从摔倒中爬起、用整个身体完成一个没人教过的动作。你在本章建立的能力,正站在这个跃迁的前沿。
下一程,我们会把这套采样工具箱带到更多机器人领域。但无论场景怎么变,本章练就的判断力——用"求值 vs 求导"识别采样的主场、用三把钥匙和动作空间把它落到真机、用互补边界做选型——都会一路陪着你。带上它,继续走。
最后留一句话给将来的你:当你某天面对一个真实机器人、一个棘手的接触任务、一堆相互矛盾的方法选择时,回想本章——它给你的不是一个标准答案,而是一套**提问的方式**。会提对的问题(这是采样的主场吗?维度灾难的哪个子问题在咬我?该把控制负担划在哪一层?这个任务该怎么拆给互补的方法?),比记住任何一个答案都重要。因为答案会随着领域演进而过时,而好的问题,会一直带你找到当下最好的答案。这,才是本章真正想交到你手里的东西。