第 S5 章:可微分 MPC 与学习-控制融合¶
本章定位:可微分 MPC 不是把 MPC 代码包进神经网络那么简单,而是把“最优控制求解器”看成一个可求导的隐式函数层。 它连接了三条线:第一条是经典 MPC 的有限时域最优控制问题;第二条是可微分仿真的状态转移梯度;第三条是学习系统中的外层损失与参数更新。 学完本章后,你应能判断梯度应该穿过动力学、穿过求解器、穿过代价参数,还是根本不应该穿过某个非光滑边界。
S5.0 前置自测¶
答不出 2 题以上时,建议先回到 S4 可微分仿真理论、足式/70_腿足简化模型理论、足式/110_OCS2完整栈与双线程MPC、复合/30_多模态MPC 和数学中的非线性优化章节补齐背景。
| # | 问题 | 预期关键词 |
|---|---|---|
| 1 | 写出一个离散时间 MPC 问题的标准形式,包含动力学约束、路径约束和终端代价。 | \(x_{k+1}=f(x_k,u_k,\theta)\),\(\ell\),\(\ell_f\),\(g\le 0\),\(h=0\) |
| 2 | iLQR/DDP 的 backward pass 本质上在求解什么? | 二次近似,Riccati 递推,局部 LQR,前馈项和反馈增益 |
| 3 | SQP 如何把非线性规划变成一串 QP? | 线性化约束,二次近似拉格朗日函数,信赖域/线搜索 |
| 4 | KKT 条件包含哪几类条件? | stationarity、原始可行、对偶可行、互补松弛 |
| 5 | 可微分仿真中的接触梯度为什么容易有偏? | 非光滑、活动集切换、硬接触、事件时间不连续 |
**自测的目的**不是筛人,而是让后面的符号有落点。
本章会反复使用这些符号。
如果符号含义不清,读到隐式函数定理时会觉得只是矩阵求逆。
如果 MPC 结构不清,读到可微层时会误以为 loss.backward() 自动解决了一切。
S5.0.1 本章目标¶
学完本章后,你应能完成五类判断。
| 能力 | 具体表现 |
|---|---|
| 建模判断 | 能把可微分仿真器、MPC 求解器和学习损失连接成一条清晰的计算图 |
| 数学判断 | 能从 KKT 条件推导 \(\partial z^\star/\partial\theta\),理解隐式求导与 unroll 求导的差别 |
| 算法判断 | 能区分 shooting、collocation、iLQR、DDP、SQP 和 IPM 的求导接口 |
| 工程判断 | 能说明代价权重、残差模型、物理参数、终端价值函数分别如何被学习 |
| 安全判断 | 能识别不适合端到端求导的场景,保留 MPC 的稳定性、约束和调试边界 |
本章不会把任何一个框架神化。
mpc.pytorch、Theseus、acados、leap-c、MJX、OCS2、Crocoddyl 都只是不同层面的工具。
真正重要的是看懂它们在计算图中的位置。
S5.0.2 知识地图¶
可微分 MPC 的知识树可以按“谁对谁求导”展开。
可微分 MPC
├── 对动力学求导
│ ├── 解析导数:Pinocchio / CppAD / CasADi
│ ├── 自动微分:JAX / PyTorch / CppAD
│ └── 可微分仿真:MJX / Brax / Drake AutoDiffXd
├── 对轨迹优化求导
│ ├── unroll solver:把迭代过程展开成计算图
│ ├── implicit differentiation:对最优性条件求导
│ └── hybrid:短展开 + 隐式修正 / 截断反传
├── 对 MPC 参数求导
│ ├── 代价权重 Q/R
│ ├── 参考轨迹和终端代价
│ ├── 残差动力学模型
│ └── 物理参数:质量、摩擦、阻尼、延迟
└── 对外层任务求导
├── 模仿学习:专家动作误差
├── 强化学习:critic / advantage / return
├── 系统辨识:轨迹预测误差
└── 安全学习:约束裕度和风险指标
这个结构能避免一个常见误解。
可微分 MPC 不是“所有东西都可微”。
它只是在合适的边界上提供梯度。
有些边界应该光滑化。
有些边界应该保留为硬约束。
有些边界应该停止梯度。
本质洞察:可微分 MPC 的本质不是让控制器变成神经网络,而是把“求解一个带物理约束的优化问题”变成可被外层学习算法调用的结构化算子。 神经网络负责学习难以手写的部分,MPC 负责守住物理、约束和可解释性。
S5.0.3 科研脉络与概念辨析¶
| 年份 | 工作 | 关键意义 | 本章使用方式 |
|---|---|---|---|
| 2018 | Amos 等,Differentiable MPC | box-constrained iLQR 的 KKT 隐式求导 | 建立可微 MPC 的核心数学 |
| 2022 | Theseus | 可微非线性最小二乘层 | 说明优化层不只服务控制,也服务 SLAM/操作 |
| 2022 | TD-MPC | 学到世界模型 + MPPI | 澄清“名字里有 MPC”不等于对求解器求导 |
| 2024 | TD-MPC2 | 可扩展 model-based RL | 对比采样规划与可微优化 |
| 2025 | Differentiable NMPC / acados 灵敏度 | 通用约束 NMPC 的隐式灵敏度 | 扩展到 SQP/IPM/NLP |
| 2025 | leap-c | acados NMPC 作为 PyTorch 层 | 工程接口示例 |
| 2025 | Actor-Critic MPC | MPC 作为 actor,RL 学 MPC 参数 | 学习-控制融合案例 |
需要先澄清三组概念。
| 容易混淆的说法 | 正确区分 |
|---|---|
| “MPC 在神经网络训练里,所以就是可微 MPC” | 只有梯度穿过 MPC 的最优解映射时,才是本章意义上的可微 MPC |
| “TD-MPC2 很强,所以它是可微 MPC” | TD-MPC2 主要是世界模型和采样式规划,不是对 QP/NLP 求解器做隐式求导 |
| “可微分仿真已经有梯度,所以不需要 MPC 求导” | 仿真梯度描述 \(x_{t+1}\) 对参数的敏感性;MPC 求导描述 \(u^\star\) 对参数的敏感性 |
这三组区分在工程中很关键。
如果目标是学习世界模型,TD-MPC2 可能比可微 MPC 更适合。
如果目标是保持硬约束并学习权重,leap-c/acados 这类可微 NMPC 更贴近需求。
如果目标是大量接触 locomotion 策略训练,PPO/SHAC/AHAC 可能比把整个 MPC 端到端求导更稳。
S5.1 从普通 MPC 到可微 MPC ⭐⭐⭐¶
这一节解决一个基本问题:普通 MPC 已经会在线优化控制,为什么还要让它可微?
S5.1.1 普通 MPC 的输入输出¶
离散时间 MPC 通常写成:
这里 \(\hat{x}\) 是当前估计状态。
\(\theta\) 是一组外部参数。
它可以包含代价权重。
它可以包含动力学参数。
它可以包含参考轨迹。
它也可以包含神经网络输出的残差模型参数。
MPC 求解器返回最优轨迹:
部署时只执行第一步控制:
这已经是一个策略。
区别在于这个策略不是一个显式神经网络,而是一个优化问题的解。
S5.1.2 外层学习问题¶
学习系统关心的不是单次 MPC 的局部代价。
它关心外层损失:
其中 \(\tau(\theta)\) 是把 MPC 放入闭环、仿真或真实数据后得到的轨迹。
常见外层损失如下。
| 外层目标 | 损失形式 | 学习对象 |
|---|---|---|
| 模仿学习 | \(\sum_t\|u_t^\star(\theta)-u_t^{expert}\|^2\) | 权重、参考生成器、encoder |
| 系统辨识 | \(\sum_t\|x_t^{pred}(\theta)-x_t^{real}\|^2\) | 质量、摩擦、阻尼、延迟 |
| 任务性能 | \(-\sum_t r(x_t,u_t)\) | 代价权重、终端价值、残差模型 |
| 安全裕度 | \(\sum_t \operatorname{softplus}(-m_t)\) | barrier 参数、安全距离、风险权重 |
如果 MPC 不可微,外层只能使用零阶方法。
例如网格搜索权重。
例如 CMA-ES。
例如 PPO 把 MPC 参数当作动作。
这些方法能工作,但样本效率低。
如果 MPC 可微,可以直接计算:
这里的难点全部集中在第二项。
S5.1.3 为什么不能直接把求解器循环全部展开¶
最直接的思路是把求解器的每一步迭代都放进计算图。
例如 SQP 迭代:
如果展开 \(T\) 次迭代,反向传播会穿过 \(T\) 个求解步骤。
这种方法叫 unrolling。
它的优点是实现概念直接。
它的缺点也明显。
| 问题 | 表现 |
|---|---|
| 内存开销 | 需要保存每次迭代的中间变量 |
| 迭代数敏感 | 训练时求解器迭代次数变化会改变计算图长度 |
| 梯度不稳定 | 线搜索、clamp、活动集切换会制造非光滑点 |
| 误差来源混杂 | 梯度同时包含“最优解敏感性”和“算法迭代路径敏感性” |
隐式求导走另一条路。
它不关心求解器怎么走到最优点。
它只关心最优点满足什么方程。
如果最优点满足 \(F(z^\star,\theta)=0\),就对这个方程求导。
这就像用地图和终点坐标计算终点敏感性,而不是回放司机每一次打方向盘。
S5.1.4 可微 MPC 的三层接口¶
工程实现时,可以把可微 MPC 分成三层接口。
外层学习器
输入:观测 obs、任务目标 goal、历史轨迹
输出:MPC 参数 theta
│
▼
可微 MPC 层
输入:当前状态 x0、参数 theta
输出:最优控制 u0*、轨迹 z*
│
▼
可微 / 不可微环境
输入:u0*
输出:下一状态、任务损失、约束裕度
每一层都可以选择是否求导。
| 梯度路径 | 适用场景 | 风险 |
|---|---|---|
| 只穿过 MPC 层 | 学权重、学残差、学终端代价 | 外层环境不可微时需要 surrogate loss |
| 穿过 MPC + 可微仿真 | 系统辨识、短 horizon 策略优化 | 接触梯度偏差、长链梯度爆炸 |
| 不穿过 MPC,只蒸馏其输出 | 需要极快部署 | 失去硬约束保证 |
| 不穿过仿真,只用真实 rollout | 安全关键系统 | 需要低样本、保守更新 |
可微 MPC 的工程价值恰恰在于它允许这些边界清晰存在。
不是所有路径都必须打开。
S5.1.5 代码示例:把 MPC 看成 PyTorch 层的外形¶
下面代码不是某个库的完整实现。
它展示的是可微 MPC 层在学习系统中的接口形状。
import torch
from torch import nn
class MPCParameterEncoder(nn.Module):
"""从观测中预测 MPC 参数。
这里的输出不是关节动作,而是结构化控制器的参数。
这样做能保留 MPC 的约束和可解释性。
"""
def __init__(self, obs_dim: int, param_dim: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, param_dim),
)
def forward(self, obs: torch.Tensor) -> torch.Tensor:
# 输出 theta,例如 Q/R 权重、残差模型系数、终端代价参数。
raw = self.net(obs)
# 权重必须为正,softplus 比 exp 更不容易造成数值爆炸。
theta = torch.nn.functional.softplus(raw) + 1e-4
return theta
class DifferentiableMPCLayer(nn.Module):
"""可微 MPC 层的接口示意。
真正的实现会在 forward 中调用 iLQR/SQP/acados/Theseus。
backward 可以来自隐式求导,而不是展开所有求解迭代。
"""
def forward(self, x0: torch.Tensor, theta: torch.Tensor) -> torch.Tensor:
# 这里假设底层求解器返回最优第一步控制 u0_star。
# 在真实工程中,这个函数通常由自定义 autograd.Function 包装。
u0_star = solve_mpc_and_return_first_control(x0, theta)
return u0_star
def imitation_training_step(encoder, mpc_layer, batch, optimizer):
"""用专家动作训练 MPC 参数生成器。"""
obs = batch["obs"]
x0 = batch["state"]
expert_u = batch["expert_action"]
theta = encoder(obs)
u = mpc_layer(x0, theta)
# 外层损失不需要等于 MPC 内部代价。
# 这里用专家动作误差训练 theta,让 MPC 变得更像专家。
loss = torch.mean((u - expert_u) ** 2)
optimizer.zero_grad()
loss.backward()
optimizer.step()
return float(loss.detach())
这段代码有两个重要细节。
第一,神经网络输出的是 \(\theta\),不是直接输出 \(u\)。
第二,loss.backward() 的关键不是 PyTorch 能否求导,而是 mpc_layer 是否提供了正确的 backward。
如果 backward 只是展开 5 次不收敛的求解迭代,梯度可能是在学习求解器的错误。
如果 backward 来自 KKT 隐式求导,梯度更接近最优策略本身的敏感性。
S5.1.6 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 把 MPC 内部代价当作外层 loss | 权重学到退化值,控制器只会最小化自己定义的问题 | 内外层目标不是同一个对象 | 明确外层任务损失,MPC 内部代价只是结构化策略 |
| 神经网络直接输出无约束权重 | 训练中出现负权重,Hessian 变不定,求解器崩溃 | MPC 代价需要正定或半正定结构 | 用 softplus、Cholesky 或对角正参数化 |
| 对未收敛的求解器做隐式求导 | backward 结果与有限差分不一致 | KKT 条件没有满足,隐式方程不成立 | 检查 KKT 残差,必要时使用展开求导或停止更新 |
| 盲目打开仿真反传 | 短期 loss 下降,实机更差 | 接触梯度有偏,学习利用仿真光滑化漏洞 | 限制 horizon,加入真实数据校验和安全约束 |
S5.1.7 练习¶
- 概念题:写出一个外层损失,它不等于 MPC 内部代价,但可以通过 MPC 参数改善任务表现。
- 推导题:假设 \(u^\star(\theta)=\arg\min_u \frac{1}{2}u^THu+\theta^Tu\),手推 \(\frac{du^\star}{d\theta}\)。
- 编程题:实现一个 PyTorch encoder 输出正的 \(Q/R\) 权重,打印训练前后权重变化,并解释哪些权重变大是合理的。
S5.2 MPC 与可微分仿真的接口 ⭐⭐⭐¶
这一节解决的问题是:MPC 求解的是预测模型,可微分仿真也提供预测模型,两者怎样连接而不互相混淆?
S5.2.1 三种动力学来源¶
MPC 需要动力学:
动力学可以来自三种来源。
| 来源 | 例子 | 梯度来源 | 优点 | 风险 |
|---|---|---|---|---|
| 解析模型 | LIPM、SRBD、Centroidal、四旋翼模型 | 手写导数、CppAD、CasADi | 快、可解释、适合实时 | 模型误差 |
| 可微分仿真 | MJX、Brax、Drake AutoDiffXd、Genesis | AD 穿过仿真步 | 与仿真一致,适合辨识 | 接触梯度不可靠 |
| 学习模型 | MLP、latent dynamics、residual model | PyTorch/JAX AD | 捕获难建模效应 | 分布外风险、约束弱 |
这三种不是互斥。
最常见的结构是物理模型加残差:
\(p\) 是物理参数。
\(r_\phi\) 是神经网络残差。
MPC 使用这个组合模型预测。
外层训练可以同时更新 \(p\) 和 \(\phi\)。
S5.2.2 可微分仿真作为 rollout 环境¶
第一种接口是让 MPC 仍使用简化模型,但外层 loss 通过可微分仿真评估。
这种结构适合系统辨识和短 horizon 训练。
MPC 的模型可以是 SRBD。
仿真器可以是 MJX 的高保真模型。
如果只评估一次 MPC 求出的开环控制序列,外层梯度会包含两部分。
这个公式解释了“可微分仿真 + 可微 MPC”的接口。
可微分仿真告诉你控制序列如何影响轨迹。
可微 MPC 告诉你参数如何影响控制序列。
二者通过链式法则相乘。
这条式子是开环版本。闭环 MPC 更常见:每个时刻都重新求解,控制律是
此时状态灵敏度要按递推计算:
其中 \(F_x,F_u\) 来自仿真/动力学模型,\(\pi_x,\pi_\theta\) 来自 MPC 解对状态和参数的灵敏度。缺少 \(\pi_x\) 时,只能解释“固定一条开环控制序列”的梯度,不能解释闭环 MPC 策略的梯度。
S5.2.3 可微分仿真作为 MPC 内部模型¶
第二种接口是把可微分仿真步直接放进 MPC 的动力学约束。
这看起来很诱人,因为模型与仿真完全一致。
但实时 MPC 往往不会这样做。
原因有三点。
| 原因 | 具体影响 |
|---|---|
| 维度太高 | 仿真状态包含完整广义坐标、速度、接触求解器状态,SQP 线性化成本高 |
| 非光滑太多 | 接触切换、摩擦锥、求解器迭代都可能破坏局部二次模型 |
| 实时性不足 | MPC 需要每个节点多次评估动力学和导数,高保真仿真代价太高 |
因此,工程上更常见的是:
这与足式/70_腿足简化模型理论中的分层思想一致。
回顾该章:LIPM、SRBD、Centroidal 不是为了替代全身动力学,而是为了在 MPC 层保留最关键的低维物理。
本章复用同一个思想,只是把“低维物理模型”放进可微学习循环。
S5.2.4 双模型结构:优化模型与评估模型¶
一个成熟系统通常同时维护两个模型。
| 模型 | 作用 | 典型要求 |
|---|---|---|
| 优化模型 | MPC 内部预测,生成控制 | 低维、光滑、导数稳定、可实时 |
| 评估模型 | 可微仿真或真实系统,计算外层损失 | 高保真、覆盖真实误差、可验证 |
两者不一致不是缺陷。
它是实时控制的基本取舍。
如果优化模型过于高保真,MPC 算不动。
如果评估模型过于简化,学到的参数会过拟合简化误差。
可微 MPC 的目标不是消除这两个模型之间的差异。
它的目标是让这个差异可以通过数据、梯度和约束被系统性校正。
S5.2.5 接口代码:简化 MPC + 可微仿真评估¶
下面用 JAX 风格伪代码展示接口。
代码重点是梯度路径。
import jax
import jax.numpy as jnp
def srbd_mpc_solve(x0, theta):
"""简化模型 MPC。
theta 可以包含 Q/R 权重、摩擦估计、残差模型参数。
真正实现中会调用 iLQR/SQP,并在 backward 使用隐式求导。
"""
u_seq = differentiable_mpc_layer(x0, theta)
return u_seq
def mjx_rollout(sim_model, x0, u_seq, physical_params):
"""用可微分仿真评估 MPC 输出。
这里假设 mjx_step 对状态、控制和物理参数可微。
"""
xs = [x0]
x = x0
for u in u_seq:
# 中文注释:仿真器提供更高保真的状态转移。
# 这一步的梯度描述“控制变化会怎样改变真实运动”。
x = mjx_step(sim_model, x, u, physical_params)
xs.append(x)
return jnp.stack(xs)
def outer_loss(theta, x0, target, sim_model, physical_params):
u_seq = srbd_mpc_solve(x0, theta)
xs = mjx_rollout(sim_model, x0, u_seq, physical_params)
# 中文注释:外层任务可以是仿真中的轨迹误差,
# 不必等于 MPC 内部的二次代价。
tracking = jnp.mean(jnp.sum((xs[:, :3] - target[:, :3]) ** 2, axis=-1))
smoothness = 1e-3 * jnp.mean(jnp.sum(u_seq ** 2, axis=-1))
return tracking + smoothness
# 对 theta 求梯度。
# 如果 differentiable_mpc_layer 的 backward 是隐式求导,
# 这里就同时利用了 MPC 梯度和仿真梯度。
grad_theta = jax.grad(outer_loss)
这个结构可以做四类实验。
第一,固定 MPC,学习仿真物理参数。
第二,固定仿真,学习 MPC 代价权重。
第三,同时学习残差模型和权重。
第四,把外层 loss 替换为 critic 估计,形成 actor-critic MPC。
S5.2.6 接触梯度与 MPC 约束的边界¶
S4 已经说明:接触梯度不是免费午餐。
这里要补上 MPC 视角。
MPC 中的接触常通过约束表达。
可微仿真中的接触常通过状态转移表达。
两者对“脚是否接触地面”的处理方式不同。
| 位置 | 接触表示 | 优点 | 风险 |
|---|---|---|---|
| MPC 约束 | 接触模式、摩擦锥、法向力非负 | 可解释,可做硬约束 | 模式给错时预测错误 |
| 可微仿真 | 接触求解器、软约束、冲量/力 | 高保真,能模拟意外接触 | 梯度在切换处偏差大 |
如果把可微仿真梯度直接用于学习接触丰富的步态,梯度可能鼓励策略利用软接触穿透。
如果 MPC 接触模式过于刚硬,实际落脚延迟会造成不可行。
折中策略通常包括:
| 策略 | 作用 |
|---|---|
| 过渡模式 | 在接触建立/释放时平滑约束 |
| barrier smoothing | 让活动集切换附近的灵敏度有界 |
| contact-invariant loss | 外层 loss 不过度依赖单个接触事件时间 |
| gradient clipping | 防止接触边界产生巨大更新 |
| finite-difference check | 用数值差分抽查解析梯度方向 |
本质洞察:MPC 约束和可微分仿真梯度不是替代关系。 约束负责告诉优化器哪些动作物理可行;仿真梯度负责告诉学习器参数变化怎样影响闭环结果。 一个守边界,一个给方向;把二者混成一个黑盒,调试就会失去抓手。
S5.2.7 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 用高保真仿真直接做实时 NMPC 模型 | 求解时间暴涨,线性化失败 | 高维接触状态不适合作为低频优化模型 | 用简化模型做 MPC,高保真仿真做评估 |
| 认为软接触一定可微且正确 | 训练策略学会“压进地面”获取虚假反力 | 软化改变了物理问题 | 加穿透惩罚、接触力上限、真实数据校验 |
| 忽略模型双轨制 | MPC 训练好但部署失效 | 优化模型和评估模型误差未监控 | 同时记录两套模型预测误差 |
| 把接触模式固定得过死 | 一次落脚延迟导致整个预测窗口错误 | 真实接触事件有噪声和延迟 | 加过渡模式和反馈修正 mode schedule |
S5.2.8 练习¶
- 框图题:画出“SRBD MPC + MJX 外层评估”的计算图,标出哪些边需要梯度,哪些边可以停止梯度。
- 实验题:在一个二维弹跳球环境中,比较硬接触、软接触和光滑 barrier 接触的梯度方向。
- 综合题:结合足式/70_腿足简化模型理论,说明为什么四足 trot 的 MPC 内部常用 SRBD,而不是完整 MuJoCo 仿真器。
S5.3 Shooting 与 Collocation:MPC 离散化如何影响梯度 ⭐⭐⭐¶
这一节解决的问题是:同一个连续时间最优控制问题,用 shooting 或 collocation 离散化后,最优解和梯度有什么差别?
S5.3.1 连续时间问题¶
连续时间 OCP 写成:
计算机不能直接求解无限维问题。
必须离散化。
离散化不只是数值细节。
它决定了优化变量、约束结构、稀疏性和梯度路径。
S5.3.2 Single Shooting¶
Single shooting 只把控制序列作为决策变量。
状态由前向积分得到。
优化问题:
Single shooting 的梯度通过整条 rollout 链传播。
这个式子解释了长 horizon 的梯度爆炸/消失。
如果每个 \(\partial F/\partial x\) 的谱半径略大于 1,乘起来会爆炸。
如果略小于 1,乘起来会消失。
| 优点 | 缺点 |
|---|---|
| 变量少,实现简单 | 对初值敏感,长 horizon 梯度病态 |
| 动力学自动满足 | 不容易处理路径约束和不稳定系统 |
| 适合小系统、短 horizon | 接触事件会让 rollout 梯度不稳定 |
S5.3.3 Multiple Shooting¶
Multiple shooting 把每个节点的状态也作为决策变量。
动力学通过缺陷约束连接:
优化问题:
Multiple shooting 的核心好处是把长链乘法变成局部约束。
状态轨迹可以被求解器整体调整。
这对不稳定系统尤其重要。
回顾足式/70 中的 LIPM:倒立摆含有 \(e^{+\omega t}\) 发散模态。
如果用 single shooting 从错误控制初值开始 rollout,状态会迅速发散。
Multiple shooting 可以把中间状态作为变量,让 SQP 在每个节点局部修正。
| 优点 | 缺点 |
|---|---|
| 适合不稳定系统 | 变量更多 |
| 稀疏约束结构清晰 | 需要处理等式约束 |
| 适合 SQP/acados/OCS2 | 初始状态轨迹仍需要热启动 |
S5.3.4 Direct Collocation¶
Collocation 不只要求节点间积分一致,还在区间内部放置 collocation 点。
例如梯形法:
这个写法使用节点控制 \(u_0,\ldots,u_N\)。如果你的 OCP 只把 \(u_0,\ldots,u_{N-1}\) 作为决策变量,就需要改成区间常值控制、区间中点控制,或额外定义 \(u_N\) 的插值/保持规则。否则最后一个区间的 \(u_{k+1}\) 没有定义。
Hermite-Simpson collocation 使用区间中点:
并要求:
Collocation 的特点是对轨迹优化更稳。
它也更适合状态约束。
但在高速实时 MPC 中,collocation 的每个区间约束更复杂。
因此嵌入式 NMPC 常使用 multiple shooting + RK 积分。
离线轨迹优化常使用 direct collocation。
S5.3.5 梯度视角的对比¶
| 方法 | 决策变量 | 梯度链长度 | 稀疏性 | 适用 |
|---|---|---|---|---|
| Single shooting | 控制 | 长链 | 密集 | 短 horizon、小系统、采样 MPC |
| Multiple shooting | 状态 + 控制 | 局部链 | 块稀疏 | 实时 NMPC、acados、OCS2 SQP |
| Collocation | 状态 + 控制 + 中点 | 局部 implicit 约束 | 块稀疏 | 离线优化、高精度轨迹 |
从可微 MPC 角度看,multiple shooting 和 collocation 更适合隐式求导。
因为它们天然形成 KKT 系统。
KKT 矩阵具有块稀疏结构。
伴随灵敏度可以利用这个结构。
Single shooting 也能求导。
但梯度更像普通 RNN 的反向传播。
长 horizon 时更病态。
S5.3.6 代码示例:multiple shooting 的缺陷约束¶
#include <Eigen/Dense>
#include <vector>
struct DoubleIntegratorNode {
Eigen::Vector2d x; // 中文注释:状态 [位置, 速度]
double u; // 中文注释:控制输入,加速度
};
Eigen::Vector2d dynamicsEuler(const Eigen::Vector2d& x, double u, double dt) {
// 中文注释:最简单的欧拉离散化。
// x_next[0] = position + dt * velocity
// x_next[1] = velocity + dt * acceleration
Eigen::Vector2d next;
next(0) = x(0) + dt * x(1);
next(1) = x(1) + dt * u;
return next;
}
Eigen::VectorXd buildDefectConstraints(
const std::vector<DoubleIntegratorNode>& nodes,
double dt)
{
const int N = static_cast<int>(nodes.size()) - 1;
Eigen::VectorXd defect(2 * N);
for (int k = 0; k < N; ++k) {
// 中文注释:multiple shooting 的等式约束。
// 如果 defect 为零,说明相邻节点满足动力学积分关系。
Eigen::Vector2d predicted = dynamicsEuler(nodes[k].x, nodes[k].u, dt);
Eigen::Vector2d residual = nodes[k + 1].x - predicted;
defect.segment<2>(2 * k) = residual;
}
return defect;
}
这段代码在真实 SQP 中会配套 Jacobian。
缺陷约束的 Jacobian 只依赖相邻两个状态和当前控制。
因此矩阵呈块带状。
这就是 multiple shooting 高效的原因。
S5.3.7 梯形 collocation 的 C++ 残差¶
Eigen::Vector2d continuousDynamics(const Eigen::Vector2d& x, double u) {
// 中文注释:双积分器连续动力学 xdot = [v, u]。
return Eigen::Vector2d(x(1), u);
}
Eigen::Vector2d trapezoidDefect(
const Eigen::Vector2d& xk,
const Eigen::Vector2d& xkp1,
double uk,
double ukp1,
double dt)
{
// 中文注释:梯形法要求区间两端动力学的平均值解释状态变化。
const Eigen::Vector2d fk = continuousDynamics(xk, uk);
const Eigen::Vector2d fkp1 = continuousDynamics(xkp1, ukp1);
return xkp1 - xk - 0.5 * dt * (fk + fkp1);
}
注意这里的残差同时依赖 \(x_k\) 和 \(x_{k+1}\)。
这使得 collocation 的每个区间是隐式的。
隐式积分通常更稳定。
但求解和求导也更复杂。
S5.3.8 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 以为离散化不影响学习 | 同一模型不同 dt 学出不同权重 | 离散动力学改变梯度尺度和稳定性 | 固定 dt 后再调权重,记录离散化方式 |
| single shooting 用长 horizon 接触任务 | 梯度爆炸,优化器只会缩小动作 | 状态转移 Jacobian 长链乘法病态 | 使用 multiple shooting 或短 horizon |
| collocation 用在强冲击接触 | 求解器在接触点附近振荡 | 隐式约束假设区间内动力学平滑 | 引入事件分段、过渡模式或非光滑处理 |
| 忽略缺陷约束残差 | 外层 loss 下降但动力学不满足 | 状态变量被优化器当自由变量使用 | 记录每个节点 defect norm |
S5.3.9 练习¶
- 推导题:对双积分器写出 multiple shooting 缺陷约束的 Jacobian。
- 编程题:比较 Euler、RK4、梯形法在同一 LIPM 模型上的预测误差和梯度误差。
- 思考题:为什么 acados 这类实时 NMPC 更偏好 multiple shooting,而不是完整 collocation?
S5.4 iLQR、DDP 与 SQP:求解器如何提供梯度 ⭐⭐⭐¶
这一节解决的问题是:不同 MPC 求解器的 forward pass 和 backward pass 到底在算什么,可微化时应复用哪些结构?
S5.4.1 iLQR 的局部二次化¶
iLQR 从一条名义轨迹开始:
在每个时刻,把动力学一阶线性化:
其中:
把代价二阶近似:
然后用 Riccati 递推求局部控制律:
这里 \(k_k\) 是前馈修正。
\(K_k\) 是反馈增益。
前馈项推动名义控制更新。
反馈项让 rollout 偏离名义状态时仍能被拉回。
S5.4.2 iLQR backward pass 的完整递推¶
递推从终端代价开始:
定义 value function 的二次近似:
构造 Q 函数:
对线性化动力学代入后,得到:
这里撇号表示下一时刻。
最优 \(\delta u\) 满足:
所以:
这就是 iLQR backward pass 的核心。
有了 \(k\) 和 \(K\) 之后,还必须把 value function 继续向前一时刻回传:
实现时通常会对 \(V_{xx}\) 做对称化:
避免浮点误差让 Hessian 失去对称性。没有这两条 \(V_x,V_{xx}\) 更新,就只能得到当前一步的增益,不能从 \(N-1\) 一直递推到 \(0\)。
如果 \(Q_{uu}\) 不正定,需要加正则:
正则不是数值小技巧。
它控制了局部二次模型的可信程度。
当线性化不可靠时,增大 \(\lambda\) 让更新更保守。
S5.4.3 DDP 与 iLQR 的差别¶
DDP 比 iLQR 更完整。
iLQR 忽略动力学的二阶导数。
DDP 保留:
这些项进入 \(Q_{xx}\)、\(Q_{ux}\)、\(Q_{uu}\)。
| 方法 | 动力学展开 | 代价展开 | 计算量 | 适用 |
|---|---|---|---|---|
| LQR | 线性动力学固定 | 二次代价固定 | 最低 | 线性系统 |
| iLQR | 一阶动力学 | 二阶代价 | 中 | 大多数实时 MPC |
| DDP | 二阶动力学 | 二阶代价 | 高 | 需要高精度二阶信息 |
| FDDP | DDP + 缺陷处理 | 二阶 | 高 | Crocoddyl 轨迹优化 |
DDP 的二阶信息在强非线性系统中更准确。
但在机器人实时控制中,动力学二阶导数代价很高。
很多工程框架使用 iLQR 或 Gauss-Newton 近似。
这就是 OCS2 SLQ、MJPC iLQG 和许多移动机器人 MPC 的选择。
S5.4.4 约束如何进入 iLQR¶
无约束 iLQR 只处理动力学约束。
真实 MPC 还需要控制限幅、状态限幅、摩擦锥和避碰。
约束处理有几种方式。
| 方法 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| clamp | 把控制投影到上下界 | 简单快速 | 梯度在边界处不光滑 |
| barrier | 把约束写入代价 | 光滑,可用 iLQR | 不能严格保证可行 |
| augmented Lagrangian | 罚函数 + 乘子 | 约束处理更强 | 参数调节复杂 |
| SQP 子问题 | 每步解带约束 QP | 约束准确 | 计算量更高 |
Amos 2018 的 differentiable MPC 处理 box-constrained iLQR。
它的核心是把控制上下界的活动集也放入 KKT 条件。
这让 backward pass 仍然类似一次 LQR,但需要考虑哪些控制维度被夹住。
S5.4.5 SQP 的局部 QP¶
SQP 从一般非线性规划开始:
在当前迭代点 \(z^i\) 处,构造 QP:
其中:
\(H_i\) 可以是拉格朗日函数 Hessian。
也可以是 Gauss-Newton 近似。
也可以是 BFGS 更新。
实时 MPC 常用 SQP-RTI。
RTI 指每个控制周期只做一次 SQP 迭代。
它依赖热启动和高频滚动。
如果上一周期解接近当前最优解,一次迭代足够。
如果突然换模式或目标跳变,一次迭代可能不够。
S5.4.6 SQP 与 iLQR 的关系¶
iLQR 可以看成特殊结构的 SQP。
它利用动力学约束的时序结构,把 QP 通过 Riccati 递推高效求解。
一般 SQP 则把问题交给 QP 求解器。
| 维度 | iLQR/DDP | SQP |
|---|---|---|
| 动力学约束 | 通过 rollout 和 Riccati 内隐处理 | 显式线性化等式约束 |
| 不等式约束 | 需要特殊处理 | QP 中自然表达 |
| 稀疏结构 | 时序结构强 | 多种稀疏结构 |
| 求解速度 | 无约束时非常快 | 约束多时更稳 |
| 可微化 | Riccati backward 或 KKT | KKT/IPM/active set |
这解释了工具选择。
Crocoddyl 偏 DDP。
OCS2 支持 SLQ/SQP。
acados 偏 SQP-RTI + HPIPM。
mpc.pytorch 是 box-constrained iLQR 的教学经典。
S5.4.7 求解器结构与 backward 的复用¶
可微 MPC 最重要的工程事实是:forward 求解器中的分解可以在 backward 中复用。
iLQR forward 已经计算了 \(Q_{uu}\) 分解。
SQP/IPM forward 已经因子化了 KKT 矩阵。
隐式 backward 需要求解的线性系统与 forward 末端的 KKT 系统同构。
因此 backward 不应该重新从零构造一个密集逆矩阵。
应该求解伴随线性系统。
如果外层只需要 \(\nabla_\theta\mathcal{L}\),不需要完整 Jacobian:
那么伴随法更高效。
它先解:
再计算:
若外层损失不直接依赖 \(\theta\),\(\mathcal{L}_\theta\) 才可以省略。很多学习型 MPC 会把正则、先验或参数边界直接写进外层 loss,此时这个直接项必须保留。
这比显式形成巨大 Jacobian 快得多。
S5.4.8 C++ 示例:Riccati backward 的核心形状¶
#include <Eigen/Dense>
#include <vector>
struct ILQRBackwardStep {
Eigen::VectorXd k; // 中文注释:前馈控制修正
Eigen::MatrixXd K; // 中文注释:反馈增益,处理状态偏差
};
ILQRBackwardStep computeBackwardStep(
const Eigen::MatrixXd& A,
const Eigen::MatrixXd& B,
const Eigen::VectorXd& lx,
const Eigen::VectorXd& lu,
const Eigen::MatrixXd& lxx,
const Eigen::MatrixXd& luu,
const Eigen::MatrixXd& lux,
const Eigen::VectorXd& Vx_next,
const Eigen::MatrixXd& Vxx_next,
double regularization)
{
// 中文注释:构造 Q 函数的一阶与二阶项。
Eigen::VectorXd Qx = lx + A.transpose() * Vx_next;
Eigen::VectorXd Qu = lu + B.transpose() * Vx_next;
Eigen::MatrixXd Qxx = lxx + A.transpose() * Vxx_next * A;
Eigen::MatrixXd Qux = lux + B.transpose() * Vxx_next * A;
Eigen::MatrixXd Quu = luu + B.transpose() * Vxx_next * B;
// 中文注释:正则化保证 Quu 可逆且更新不过激。
Quu += regularization * Eigen::MatrixXd::Identity(Quu.rows(), Quu.cols());
// 中文注释:使用 LDLT 分解,不显式求逆。
Eigen::LDLT<Eigen::MatrixXd> ldlt(Quu);
ILQRBackwardStep out;
out.k = -ldlt.solve(Qu);
out.K = -ldlt.solve(Qux);
return out;
}
这段代码体现一个重要原则。
数学上写 \(Q_{uu}^{-1}\)。
工程上永远不要显式求逆。
使用 Cholesky、LDLT 或稀疏 KKT 求解。
S5.4.9 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 把 DDP backward 等同于神经网络 backward | 对控制约束和活动集完全忽略 | DDP backward 是动态规划,不是普通链式反传 | 分清 Riccati 递推和 autograd 反传 |
| \(Q_{uu}\) 非正定仍强行更新 | rollout 代价上升,控制发散 | 局部二次模型不可信 | 加正则、线搜索、信赖域 |
| SQP-RTI 在目标跳变时仍只做一次迭代 | MPC 输出大幅滞后 | 热启动失效 | 检测残差,临时增加迭代或重置初值 |
| backward 显式求大 Jacobian | 显存/内存爆炸 | 不需要完整灵敏度矩阵 | 用伴随法求向量-Jacobian 乘积 |
S5.4.10 练习¶
- 推导题:从 \(Q_u+Q_{ux}\delta x+Q_{uu}\delta u=0\) 推导 \(k\) 和 \(K\)。
- 编程题:实现双积分器 iLQR,记录 \(Q_{uu}\) 的最小特征值和正则化次数。
- 对比题:解释为什么摩擦锥约束较多时,SQP 通常比纯 iLQR 更自然。
S5.5 KKT 条件与隐式函数定理 ⭐⭐⭐⭐¶
这一节是本章数学核心:梯度如何穿过优化器。
S5.5.1 从无约束最优化开始¶
先看最简单的问题:
最优性条件是:
定义:
于是:
对 \(\theta\) 求导:
如果 \(\frac{\partial F}{\partial z}\) 可逆,则:
因为 \(F=\nabla_z\phi\),所以:
这就是 Hessian。
无约束问题的隐式求导就是解一个 Hessian 线性系统。
S5.5.2 一个手算例子¶
令:
最优性条件:
所以:
显式求导:
隐式求导:
所以:
两者一致。
这个例子看似简单,但已经包含所有核心结构。
复杂 MPC 只是把 \(z\) 换成整条轨迹,把 \(F\) 换成 KKT 条件。
S5.5.3 带等式约束的 KKT¶
考虑:
拉格朗日函数:
KKT 条件:
把变量合并:
定义:
对 \(y\) 的 Jacobian 是 KKT 矩阵:
其中:
隐式求导:
S5.5.4 带不等式约束的 KKT¶
考虑:
拉格朗日函数:
KKT 条件:
最后一条是互补松弛。
它制造了非光滑性。
如果一个约束从不活跃变成活跃,\(\mu_i\) 从 0 变成正数。
最优解对参数的导数可能跳变。
这就是可微 MPC 的困难之一。
S5.5.5 活动集视角¶
如果已知活动集 \(\mathcal{A}\):
可以把活动不等式当等式处理。
非活动约束不进入 KKT 线性系统。
在活动集不变、约束 Jacobian 满秩(LICQ)、二阶充分条件成立,并且 KKT 系统非退化的小邻域内,最优解才是局部光滑的。若还希望活动/非活动边界稳定,通常还需要严格互补这类条件。
活动集变化时,导数不连续。
这解释了为什么 box-constrained iLQR 可以高效求导。
控制限幅的活动集相对容易判断。
一般接触约束、自碰撞约束和摩擦锥活动集更复杂。
S5.5.6 IPM 平滑视角¶
内点法把不等式约束转成 barrier:
\(\tau>0\) 时,最优解留在约束内部。
梯度是光滑的。
\(\tau\to 0\) 时,解接近原始约束问题。
但灵敏度也可能变得不连续。
这就是 Frey 等可微 NMPC 中 barrier smoothing 的直觉。
较大的 \(\tau\) 给出更平滑的梯度。
较小的 \(\tau\) 给出更精确但更尖锐的梯度。
| \(\tau\) | 梯度 | 解的精度 | 训练表现 |
|---|---|---|---|
| 大 | 平滑 | 离真实活动集更远 | 稳定但可能有偏 |
| 中 | 折中 | 可接受 | 常用 |
| 小 | 接近真实灵敏度 | 高 | 活动集切换处容易抖 |
| 0 | 非光滑 | 原问题 | 需要活动集处理 |
S5.5.7 伴随法推导¶
外层损失依赖最优解:
直接求:
代入隐式求导:
不要显式计算 \(F_y^{-1}\)。
定义伴随变量 \(a\):
则:
这就是伴随灵敏度。这正是 Pontryagin 极大值原理(1962)的离散版本——伴随变量 \(a\) 的递推方程与经典的协态方程完全对应。在连续时间最优控制中,协态变量 \(\lambda(t)\) 满足 \(\dot{\lambda}=-H_x^T\),其中 \(H\) 是 Hamilton 函数;这里的离散伴随方程 \(F_y^T a = r_y^T\) 是同一思想在 KKT 条件上的体现。
伴随法只需要解一次转置线性系统。
当 \(\theta\) 维度很高时,这比前向灵敏度更划算。
S5.5.8 KKT 矩阵为什么可能奇异¶
隐式函数定理需要 \(F_y\) 可逆。
实际中这不总成立。
常见原因如下。
| 原因 | 现象 | 处理 |
|---|---|---|
| Hessian 半正定但不正定 | KKT 求解不稳定 | 正则化、Gauss-Newton、信赖域 |
| 约束 Jacobian 不满秩 | 乘子不唯一 | 删除冗余约束、SVD 检查 |
| 活动集退化 | 梯度跳变巨大 | barrier smoothing、活动集锁定 |
| 最优解不唯一 | Jacobian 不存在或集合值 | 加小正则,定义选择规则 |
| 求解器未收敛 | KKT 残差大 | 增加迭代或停止外层更新 |
这也是为什么“梯度能算出来”不等于“梯度可信”。
KKT 残差和条件数必须进入日志。
S5.5.9 小型 QP 的隐式求导代码¶
下面用一个等式约束 QP 展示 KKT 求导。
问题:
KKT:
若 \(q(\theta)=M\theta\),则:
import torch
def equality_qp_solution(H, A, q, b):
"""求解等式约束 QP。
中文注释:这里直接构造 KKT 系统用于教学。
大规模 MPC 不应使用 dense solve,而应利用块稀疏结构。
"""
n = H.shape[0]
m = A.shape[0]
KKT = torch.zeros(n + m, n + m, dtype=H.dtype, device=H.device)
KKT[:n, :n] = H
KKT[:n, n:] = A.T
KKT[n:, :n] = A
rhs = torch.cat([-q, b], dim=0)
sol = torch.linalg.solve(KKT, rhs)
return sol[:n], sol[n:], KKT
def implicit_grad_q(H, A, q, b, grad_z):
"""计算外层损失对 q 的梯度。
若 loss 对 z 的梯度为 grad_z,
则先解 KKT^T * adj = [grad_z, 0],
再由 stationarity 方程得到 dloss/dq = -adj_z。
"""
z, lam, KKT = equality_qp_solution(H, A, q, b)
n = H.shape[0]
m = A.shape[0]
rhs = torch.cat([grad_z, torch.zeros(m, dtype=H.dtype, device=H.device)])
adj = torch.linalg.solve(KKT.T, rhs)
grad_q = -adj[:n]
return grad_q
这段代码故意写得直白。
真实系统会把 KKT 分解缓存起来。
如果只需要反传到 \(q\),伴随法不需要构造完整 \(\partial z/\partial q\)。
S5.5.10 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 忘记互补松弛的非光滑性 | 有限差分和解析梯度在边界处差异大 | 活动集变化 | 对边界点做单独测试,使用平滑或子梯度策略 |
| 对 KKT 矩阵直接求逆 | 小例子能跑,大问题崩溃 | 逆矩阵数值不稳定且破坏稀疏 | 用线性求解和分解复用 |
| 忽略乘子含义 | 学到的参数让约束长期贴边 | 乘子记录了约束压力 | 日志中保存 active constraints 和 multipliers |
| 对不可唯一解求导 | 梯度随机跳变 | 最优解映射不是单值函数 | 加正则或定义稳定的 tie-breaker |
S5.5.11 练习¶
- 手推题:对等式约束 QP 推导 KKT 矩阵,并写出对 \(q\) 的灵敏度。
- 数值题:用有限差分验证上面代码的
implicit_grad_q。 - 研究题:构造一个一维不等式问题,观察最优解在活动集切换处的导数跳变。
S5.6 把求解器作为可微层 ⭐⭐⭐¶
这一节解决工程问题:一个求解器怎样变成深度学习框架中的 layer?
S5.6.1 可微层的 forward¶
可微 MPC layer 的 forward 通常做四件事。
| 步骤 | 内容 | 日志信号 |
|---|---|---|
| 参数解包 | 把神经网络输出转成 OCP 参数 | 参数范围、正定性 |
| 求解 OCP | iLQR/SQP/IPM 求出 \(z^\star\) | 迭代数、KKT 残差、求解时间 |
| 输出选择 | 返回 \(u_0^\star\) 或整条轨迹 | 控制限幅、约束裕度 |
| 保存 backward 上下文 | 保存分解、轨迹、乘子、活动集 | 条件数、active set |
如果 forward 没有收敛,backward 的数学前提就不成立。
因此 layer 应该把求解状态返回给训练循环。
训练代码不能只看 loss。
它也要看 solver health。
S5.6.2 可微层的 backward¶
Backward 有三种路线。
| 路线 | 做法 | 适用 |
|---|---|---|
| unroll | 展开求解器迭代并让 AD 反传 | 小问题、固定迭代、教学 |
| implicit | 对 KKT/固定点条件求导 | 收敛求解器、生产可微层 |
| straight-through / custom | forward 用投影或离散操作,backward 用近似梯度 | 非光滑但需要训练信号 |
对 MPC 来说,implicit 是主线。
但在早期实验中,unroll 仍有价值。
它更容易定位某一步导数是否写错。
一旦进入高维机器人问题,unroll 的内存和稳定性会成为瓶颈。
S5.6.3 PyTorch 自定义 autograd 的形状¶
import torch
class MPCFunction(torch.autograd.Function):
@staticmethod
def forward(ctx, x0, theta, solver_handle):
"""前向:调用外部 MPC 求解器。
中文注释:solver_handle 可以封装 C++/acados/自研 iLQR。
forward 返回最优第一步控制和必要的轨迹信息。
"""
with torch.no_grad():
result = solver_handle.solve(x0, theta)
u0 = result.u0
# 中文注释:保存 backward 所需信息。
# 真实实现应避免保存巨大对象,可保存指针、压缩轨迹或 KKT 分解句柄。
ctx.solver_handle = solver_handle
ctx.result = result
ctx.save_for_backward(x0, theta)
return u0
@staticmethod
def backward(ctx, grad_u0):
"""反向:用隐式求导计算 VJP。
grad_u0 是外层 loss 对 u0* 的梯度。
返回 loss 对 x0 和 theta 的梯度。
"""
x0, theta = ctx.saved_tensors
result = ctx.result
solver = ctx.solver_handle
# 中文注释:调用求解器提供的伴随灵敏度接口。
grad_x0, grad_theta = solver.adjoint_sensitivity(
result=result,
grad_output=grad_u0,
)
# solver_handle 不是 Tensor,没有梯度。
return grad_x0, grad_theta, None
class MPCLayer(torch.nn.Module):
def __init__(self, solver_handle):
super().__init__()
self.solver_handle = solver_handle
def forward(self, x0, theta):
return MPCFunction.apply(x0, theta, self.solver_handle)
这段代码的重点是 adjoint_sensitivity。
真正的可微 MPC 工作量在这里。
它需要知道求解器最终的 KKT 系统。
它需要知道外层梯度对应的是第一步控制、整条控制轨迹还是状态轨迹。
它还需要把梯度映射回 \(\theta\) 的参数化方式。
S5.6.4 acados 与 leap-c 的角色¶
acados 的核心价值在于快速 NMPC 求解。
它使用 CasADi 建模、C 代码生成、SQP-RTI、HPIPM/BLASFEO 这类高性能线性代数后端。
可微 NMPC 关注的是 solution sensitivity。
leap-c 的价值在于把这种能力包装进 PyTorch 训练循环。
工程上可理解为:
CasADi OCP 定义
-> acados 生成求解器
-> NMPC forward 求 z*
-> sensitivity/adjoint backward 求梯度
-> PyTorch optimizer 更新 theta
这类框架最适合:
| 场景 | 理由 |
|---|---|
| 自动驾驶/无人机 | 低维、约束明确、模型较准确 |
| 系统辨识 | 参数物理意义清楚,数据可对齐 |
| 学 MPC 权重 | 仍保留求解器和约束 |
| 安全关键策略改进 | 更新的是控制器参数,不是直接动作 |
它不一定适合 1kHz 全身控制内环。
因为在线仍需求解 MPC。
对腿足 1kHz 关节层,常用做法是 MPC 低频、WBC/PD 高频,或把 MPC 蒸馏成网络。
S5.6.5 Theseus 的启发¶
Theseus 把非线性最小二乘问题作为可微层。
虽然它不是专门的 MPC 工具,但它提供了重要思想:
Gauss-Newton 的 normal equation:
在最优点附近,隐式求导同样通过线性系统完成。
SLAM 因子图、位姿图优化、接触位姿估计、短 horizon 轨迹优化都可以共享这套模式。
这说明可微优化层是一个更大的范畴。
可微 MPC 是其中带时序动力学约束的一支。
S5.6.6 输出整条轨迹还是只输出第一步¶
MPC 部署只执行第一步。
但学习时输出可以有多种选择。
| 输出 | 外层 loss | 适用 |
|---|---|---|
| \(u_0^\star\) | 动作模仿、即时 reward | 部署一致 |
| \(u_{0:N-1}^\star\) | 控制序列模仿、平滑性 | 训练信号更丰富 |
| \(x_{0:N}^\star\) | 轨迹形状、终端任务 | 规划层学习 |
| 乘子/裕度 | 安全学习、约束压力 | 调参和诊断 |
只输出第一步的梯度较少。
输出整条轨迹的梯度更丰富。
但外层可能会优化未执行的未来控制。
实践中常组合:
S5.6.7 训练循环中的求解器健康监控¶
可微层训练必须记录求解器状态。
| 信号 | 异常含义 |
|---|---|
| KKT residual | 隐式梯度前提不满足 |
| primal infeasibility | 约束或初值不可行 |
| dual infeasibility | 乘子或 Hessian 结构异常 |
| line search failures | 局部模型不可信 |
| active set changes | 梯度可能跳变 |
| barrier parameter | 梯度平滑程度变化 |
| solve time p99 | 部署实时性风险 |
如果训练时 loss 下降但 KKT 残差上升,说明网络在把求解器推向难问题。
这不是进步。
这通常是外层损失缺少可行性正则。
S5.6.8 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| forward 失败还继续 backward | 梯度出现 NaN 或巨大尖峰 | 最优性条件不成立 | 对失败样本跳过更新或使用保守 fallback |
| 保存完整求解器对象到 autograd ctx | 显存和内存泄漏 | 上下文生命周期被计算图持有 | 只保存必要张量或外部句柄 |
| 外层网络输出任意参考轨迹 | MPC 经常不可行 | 参考不满足动力学和约束 | 对参考生成器加可达域和速度限制 |
| 只监控训练 loss | 部署时超时或违反约束 | 求解器健康被忽略 | 把 residual、solve time、slack 写入训练日志 |
S5.6.9 练习¶
- 接口题:设计一个
MPCLayer的 forward 返回值,要求同时支持训练和部署。 - 工程题:列出可微 MPC 训练日志中必须记录的 10 个字段,并解释每个字段的用途。
- 对比题:说明 Theseus 的可微最小二乘层与可微 MPC 层的共同点和差别。
S5.7 学习代价、模型参数与残差动力学 ⭐⭐⭐¶
这一节解决的问题是:可微 MPC 到底学什么,哪些参数适合学,哪些参数不适合交给神经网络?
S5.7.1 学习代价权重¶
经典 MPC 代价常写成:
人工调 \(Q/R\) 很困难。
可微 MPC 可以让外层数据调权重。
但必须保持权重结构。
最简单的参数化:
更一般的正定矩阵:
其中 \(L\) 是下三角矩阵。
| 参数化 | 优点 | 缺点 |
|---|---|---|
| 对角 softplus | 稳定、易解释 | 不能表达交叉耦合 |
| Cholesky | 可表达完整 SPD | 参数更多,易过拟合 |
| 分组权重 | 对应物理任务 | 表达能力中等 |
| 状态相关权重 | 自适应能力强 | 稳定性分析更难 |
S5.7.2 权重学习的物理解释¶
在复合/30_多模态MPC 中,末端跟踪权重不能压过 locomotion 稳定权重。
本章从学习角度重新看这个原则。
如果让网络自由学习所有权重,它可能为了降低短期末端误差,把 CoM 稳定项降到很小。
仿真中也许能过。
实机上会失去支撑裕度。
因此权重学习需要先验边界。
| 权重 | 可学习范围建议 | 理由 |
|---|---|---|
| CoM / 基座姿态 | 下界较高 | 跌倒是硬失败 |
| 末端位置 | 可随任务调节 | 操作精度任务相关 |
| 控制正则 | 保持正下界 | 避免过激动作 |
| slack 惩罚 | 高且有限 | 允许恢复但不掩盖失败 |
| 碰撞 barrier | 下界高 | 安全边界不能被学没 |
这就是“结构化学习”的含义。
学习器可以调节控制器。
但不能删除控制器的安全骨架。
S5.7.3 学习参考轨迹和终端代价¶
有时不应该直接学 \(Q/R\)。
更合适的是学参考或终端价值。
例如无人机竞速中,MPC 的动力学和约束很清楚。
难的是如何在弯道前选择速度和姿态。
可以让网络输出:
MPC 负责在约束内跟踪。
另一种方式是学习终端价值:
这能补偿有限 horizon 的短视。
AC4MPC 和 actor-critic MPC 类方法都利用了类似思想。
终端价值的优点是保留 MPC 的局部约束,同时让 critic 表达长期收益。
风险是 critic 外推错误会把 MPC 推向危险状态。
因此终端价值也需要 trust region 或保守下界。
S5.7.4 学习物理参数¶
系统辨识是可微 MPC 的自然应用。
参数 \(p\) 可以是:
| 参数 | 例子 | 可辨识性风险 |
|---|---|---|
| 质量/惯量 | 载荷变化、人形手持物体 | 与控制增益耦合 |
| 摩擦 | 地面摩擦、关节摩擦 | 接触数据不足时不可辨 |
| 阻尼 | 关节阻尼、空气阻力 | 与速度范围有关 |
| 延迟 | 电机延迟、通信延迟 | 离散时间建模敏感 |
| 接触参数 | solref/solimp、刚度 | 与地面材料和步长有关 |
轨迹误差 loss:
如果 \(x_t^{pred}\) 由 MPC 闭环产生,则:
这时参数既影响模型预测,也影响控制动作。
可微 MPC 能捕捉这种闭环敏感性。
普通开环辨识捕捉不到。
S5.7.5 学习残差动力学¶
残差模型写成:
为了安全,残差不应无限制。
常用约束包括:
| 约束 | 作用 |
|---|---|
| 残差幅值限制 | 防止网络推翻物理模型 |
| Lipschitz 正则 | 防止局部剧烈变化 |
| 能量一致性 | 防止凭空注入能量 |
| 坐标不变性 | 保持 SE(3) / 接触坐标结构 |
| 不确定性输出 | 分布外时降低信任 |
残差模型尤其适合无人机空气阻力、轮地滑移、关节摩擦、软接触偏差。
不适合让它学习所有物理。
如果残差比主模型还大,说明名义模型选错了。
S5.7.6 代码示例:安全的权重参数化¶
import torch
from torch import nn
class PositiveDiagonalWeights(nn.Module):
"""把网络输出映射为有上下界的正权重。"""
def __init__(self, dim, min_weight, max_weight):
super().__init__()
self.raw = nn.Parameter(torch.zeros(dim))
self.register_buffer("min_weight", torch.full((dim,), min_weight))
self.register_buffer("max_weight", torch.full((dim,), max_weight))
def forward(self):
# 中文注释:sigmoid 保证权重在 [min, max] 内。
# 比无上界 softplus 更适合安全关键权重。
alpha = torch.sigmoid(self.raw)
w = self.min_weight + alpha * (self.max_weight - self.min_weight)
return torch.diag(w)
class StructuredMPCWeights(nn.Module):
"""按物理分组学习 MPC 权重。"""
def __init__(self):
super().__init__()
# 中文注释:稳定项设置高下界,不能被学习器降为零。
self.base_Q = PositiveDiagonalWeights(dim=6, min_weight=10.0, max_weight=1000.0)
# 中文注释:末端任务允许更大范围,适应不同操作阶段。
self.ee_Q = PositiveDiagonalWeights(dim=6, min_weight=0.1, max_weight=500.0)
# 中文注释:控制正则保持正值,避免控制过激。
self.R = PositiveDiagonalWeights(dim=12, min_weight=1e-4, max_weight=10.0)
def forward(self):
return {
"base_Q": self.base_Q(),
"ee_Q": self.ee_Q(),
"R": self.R(),
}
这段代码体现了一个原则。
学习不是放弃先验。
学习是在先验允许的范围内自动调参。
S5.7.7 代码示例:残差模型的幅值限制¶
class BoundedResidualDynamics(nn.Module):
"""物理模型 + 有界残差。
中文注释:残差只修正模型误差,不允许完全改写动力学。
"""
def __init__(self, state_dim, action_dim, max_residual):
super().__init__()
self.max_residual = max_residual
self.net = nn.Sequential(
nn.Linear(state_dim + action_dim, 128),
nn.Tanh(),
nn.Linear(128, 128),
nn.Tanh(),
nn.Linear(128, state_dim),
)
def nominal_step(self, x, u):
# 中文注释:这里应调用解析动力学或可微仿真简化步。
return nominal_dynamics_step(x, u)
def forward(self, x, u):
x_nominal = self.nominal_step(x, u)
raw_residual = self.net(torch.cat([x, u], dim=-1))
# 中文注释:tanh 限制残差幅值,避免网络制造不物理的状态跳变。
residual = self.max_residual * torch.tanh(raw_residual)
return x_nominal + residual
如果模型误差很大,不要简单增大 max_residual。
应先检查名义模型、单位、坐标系、延迟和输入定义。
S5.7.8 学习参数的可辨识性¶
不是所有参数都能从数据中辨识。
如果系统没有激励某个方向,该方向的参数梯度会接近零。
例如机器人一直低速直行,很难辨识横向摩擦。
例如四旋翼只悬停,很难辨识高速阻力。
例如机械臂没有接触,很难辨识接触刚度。
可辨识性可以用 Fisher 信息近似判断:
如果 \(\mathcal{I}\) 某些特征值很小,对应参数方向不可辨。
这时继续学习会让参数漂移。
正确做法是:
| 情况 | 处理 |
|---|---|
| 激励不足 | 设计辨识动作 |
| 参数耦合 | 固定一部分参数,只学关键参数 |
| 噪声太大 | 加先验和正则 |
| 分布外 | 降低残差信任,回到保守控制 |
S5.7.9 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 所有权重都自由学习 | 稳定项被降为零 | 外层 loss 没有覆盖所有失败模式 | 给安全相关权重设置下界 |
| 残差模型过强 | 仿真训练好,真实部署差 | 网络学会利用仿真漏洞 | 限制残差幅值和变化率 |
| 同时学习太多物理参数 | 参数漂移且不可解释 | 数据对参数不可辨 | 逐步解冻参数,做 Fisher/灵敏度检查 |
| 学到的终端价值过乐观 | MPC 把状态推向边界 | critic 分布外外推 | 终端价值加保守约束和 uncertainty |
S5.7.10 练习¶
- 设计题:为 Go2 站立推扰任务设计可学习的 \(Q/R\) 权重分组,并说明每组上下界。
- 辨识题:给定四旋翼飞行数据,哪些动作最有利于辨识质量,哪些动作最有利于辨识阻力?
- 实现题:把上面的
BoundedResidualDynamics接到一个 double integrator MPC 中,比较有界和无界残差的训练稳定性。
S5.8 稳定性、安全边界与可学习性的冲突 ⭐⭐⭐⭐¶
这一节解决的问题是:学习会改变 MPC 参数,怎样避免学到一个在训练 loss 上更好、但在物理上更危险的控制器?
S5.8.1 MPC 稳定性的传统条件¶
经典 MPC 稳定性通常依赖三件事。
| 条件 | 作用 |
|---|---|
| 终端集合 \(\mathcal{X}_f\) | 保证 horizon 结束时还能被局部控制器稳定 |
| 终端代价 \(V_f(x)\) | 近似无限时域 cost-to-go |
| 递归可行性 | 当前可行意味着下一步仍有可行解 |
典型离散系统:
若终端集合内存在局部控制律 \(\kappa_f(x)\),满足:
并且:
则有限时域 MPC 可以继承稳定性。
这个条件的直觉是:
窗口结束时,系统不是掉进未知状态,而是进入一个已知可稳定区域。
终端代价则告诉优化器不要在窗口末端留下烂摊子。
S5.8.2 学习会破坏什么¶
学习参数可能破坏上述条件。
| 学习对象 | 可能破坏的性质 |
|---|---|
| \(Q/R\) 权重 | 破坏 Lyapunov 下降方向,过度偏向任务误差 |
| 终端价值 \(V_\theta\) | 过度乐观,诱导系统进入边界 |
| 动力学残差 | 改变可达集合和约束可行性 |
| 约束软化参数 | 让安全边界变成可被牺牲的代价项 |
| 参考生成器 | 输出不可达或不可稳定的参考 |
这说明可微 MPC 训练不能只优化平均任务损失。
必须同时约束学习空间。
S5.8.3 安全边界的分层¶
安全条件应分成三层。
硬边界:绝不允许违反
例如关节硬限位、急停、最大电流、不可碰撞区域
控制边界:MPC 应显式满足
例如摩擦锥、力矩限幅、ZMP/支撑裕度、CBF 约束
性能边界:允许短暂退化但要记录
例如末端误差、速度误差、能耗、舒适性
学习器最多调节第二层和第三层之间的权衡。
第一层不应被学习器改写。
例如安全距离可以根据感知不确定性增大。
但不能让网络把安全距离学成负数。
S5.8.4 CBF 与 MPC 的接口¶
控制障碍函数 CBF 给出安全集合:
离散时间可写成:
在 MPC 中,它是路径约束。
学习器可以调整参考和权重。
但 CBF 约束负责守住安全集合。
如果外层学习让任务目标穿过障碍,MPC 应拒绝而不是跟随。
这就是“学习意图,控制守边界”的结构。
S5.8.5 鲁棒裕度¶
真实系统存在模型误差。
如果约束写成:
训练中最优解长期贴着 \(g=0\),实机很容易违反。
所以需要裕度:
\(\rho\) 可以固定。
也可以随不确定性变化:
其中 \(\sigma(x)\) 是模型不确定性。
这与 S4 中可微仿真边界一致。
梯度能让参数更优。
裕度让系统不把最优解压在悬崖边。
S5.8.6 分布外检测¶
可微 MPC 常和神经网络结合。
神经网络最怕分布外输入。
可用的检测信号包括:
| 信号 | 含义 |
|---|---|
| encoder 输出接近上下界 | 网络试图使用极端参数 |
| MPC slack 长期非零 | 任务或模型超出可行域 |
| KKT 残差上升 | 求解器面对陌生问题 |
| 残差模型不确定性高 | 学习模型缺少数据 |
| 实测预测误差上升 | sim-to-real 偏差变大 |
一旦检测到分布外,应进入保守模式。
保守模式可以是固定权重 MPC。
也可以是安全停机。
也可以是低速重新观察。
不要让网络在分布外继续自由调节关键参数。
S5.8.7 代码示例:训练中的安全过滤¶
def safe_parameter_filter(theta_raw, solver_stats, bounds):
"""对学习到的 MPC 参数做安全过滤。
bounds[name] = (low, high, default)
中文注释:这不是替代 MPC 约束,而是在进入求解器前防止明显危险参数。
"""
theta = {}
for name, value in theta_raw.items():
low, high, default = bounds[name]
theta[name] = torch.clamp(value, low, high)
# 中文注释:如果上一轮求解器健康状态差,收缩参数到保守默认值附近。
if solver_stats["kkt_residual"] > 1e-3 or solver_stats["solve_failed"]:
for name, value in theta.items():
_, _, default = bounds[name]
theta[name] = 0.8 * default + 0.2 * value
return theta
这里的过滤器看起来保守。
但安全关键控制中,保守是结构的一部分。
学习器应该在安全壳内探索。
S5.8.8 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 把安全约束写成普通代价 | 任务压力大时越界 | 代价权重有限,约束可被交换 | 安全项用硬约束或高优先级软约束 |
| 学习终端价值但不限制外推 | MPC 故意进入危险状态 | critic 在未知区域过乐观 | 加终端集合、置信度和保守剪裁 |
| 训练只看平均回报 | 少数失败被平均掩盖 | 安全是尾部风险问题 | 记录 worst-case、CVaR 和约束违反率 |
| 可微梯度覆盖安全监控 | 实机出现高频危险动作 | 学习更新直接影响底层控制 | MPC 低频学习,高频 WBC/安全监控独立兜底 |
S5.8.9 练习¶
- 推导题:写出一个离散 CBF 约束,并说明它如何放入 multiple shooting MPC。
- 设计题:为四足臂“边走边抓”任务列出硬边界、控制边界和性能边界。
- 实验题:训练可学习末端权重时,比较有安全过滤和无安全过滤的约束违反次数。
S5.9 何时不该端到端求导 ⭐⭐⭐⭐¶
这一节很重要:可微并不等于应该求导。
S5.9.1 不该求导的第一类场景:离散决策主导¶
如果任务成败主要由离散模式决定,端到端梯度往往帮助有限。
例子:
| 任务 | 离散决策 |
|---|---|
| 四足换步 | 哪条腿何时接触 |
| 抓取 | 抓哪个点,何时闭合 |
| 开门 | 是否已接触门把手 |
| 避障 | 走左边还是右边 |
| 多接触操作 | 哪个接触面激活 |
这些决策改变问题结构。
梯度只能描述局部连续变化。
它很难从“走左边”连续变成“走右边”。
正确方法通常是:
不要指望连续梯度替代离散规划。
S5.9.2 不该求导的第二类场景:硬接触事件时间决定结果¶
如果 loss 对接触事件时间高度敏感,梯度会非常不稳定。
例如球刚好撞墙。
控制稍微改变,碰撞发生或不发生。
损失会跳变。
这类问题中,有限差分也会对步长极其敏感。
可微仿真给出的局部梯度可能方向错误。
更稳妥的方法:
| 方法 | 作用 |
|---|---|
| 采样式 MPC / MPPI | 用零阶采样跨越事件边界 |
| SHAC/AHAC 短 horizon | 限制梯度穿过太多接触 |
| 接触模式显式建模 | 把事件变成模式切换 |
| 真实数据校验 | 防止仿真梯度过拟合 |
S5.9.3 不该求导的第三类场景:安全认证链路¶
如果某个模块是安全认证的一部分,不应让学习梯度直接改变它。
例如:
| 模块 | 原因 |
|---|---|
| 急停逻辑 | 必须独立于学习系统 |
| 电流/力矩硬限幅 | 保护硬件 |
| 碰撞保护区 | 保护人和设备 |
| 状态估计异常检测 | 防止错误状态进入控制器 |
| 低层电机保护 | 高频实时安全 |
这些模块可以影响 loss。
但不应被梯度更新。
它们是控制系统的护栏。
S5.9.4 不该求导的第四类场景:求解器残差主导梯度¶
如果求解器没有收敛,反传得到的是“算法残差敏感性”,不是“最优策略敏感性”。
判断标准:
| 信号 | 不该继续隐式反传的阈值例子 |
|---|---|
| KKT 残差 | 大于训练初期基线 10 倍 |
| QP 失败 | primal/dual infeasible |
| 线搜索失败 | 多次 step rejected |
| 正则过大 | \(\lambda\) 长期处于上限 |
| 迭代超时 | 超过实时预算 |
此时应跳过该样本、降低学习率、回退参数或使用保守默认控制。
不要让 optimizer 用坏梯度更新网络。
S5.9.5 端到端求导的决策流程¶
是否有明确连续参数需要学习?
├── 否 → 不需要可微 MPC
└── 是
├── 任务是否主要由离散模式决定?
│ ├── 是 → 分离离散规划和连续 MPC
│ └── 否
│ ├── 接触是否频繁且刚性?
│ │ ├── 是 → 短 horizon / 平滑 / 采样校验
│ │ └── 否
│ │ ├── 求解器能稳定收敛吗?
│ │ │ ├── 否 → 先修求解器与模型
│ │ │ └── 是 → 使用隐式求导
│ │ └── 安全边界能独立守住吗?
│ │ ├── 否 → 先加安全过滤
│ │ └── 是 → 可以端到端训练
这张流程图是工程选型工具。
当它给出“不要端到端求导”时,不代表技术落后。
它代表控制边界清晰。
S5.9.6 与纯 RL、纯 MPC、蒸馏的比较¶
| 方案 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 纯 MPC | 可解释、约束强 | 权重和模型人工调 | 模型准确、安全关键 |
| 可微 MPC | 样本效率高、保留结构 | 梯度边界复杂 | 学参数、学残差、低中维系统 |
| 纯 RL | 表达能力强 | 样本多、安全弱 | 高维接触、仿真训练充分 |
| MPC 蒸馏 | 推理极快 | 约束保证弱化 | 1kHz 部署、已有大量 MPC 数据 |
| 采样式 MPC | 跨非光滑边界 | 采样成本高 | 多峰、非光滑、离散性强 |
本质洞察:可微 MPC 位于“纯模型控制”和“纯数据策略”之间。 它最适合学习那些人难以手调、但又必须留在物理约束内的连续参数。 如果问题的关键是离散选择、感知语义或强接触事件,强行求导常常不如把边界拆清楚。
S5.9.7 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 为了可微而软化所有约束 | 训练顺滑,部署危险 | 安全边界被变成可交换代价 | 只软化可训练边界,硬边界保留 |
| 用可微 MPC 解决离散规划 | 卡在局部最优 | 连续梯度不能跨模式 | 上层用搜索/采样/策略选择模式 |
| 把真实系统也当可微环境 | 期待实机能直接反传 | 真实系统只给数据,不给解析梯度 | 用离线数据、有限差分、策略梯度或 sim surrogate |
| 忽略求解器失败样本 | 训练后期突然崩溃 | 坏梯度累计 | 失败样本进入诊断队列而非正常反传 |
S5.9.8 练习¶
- 判断题:下面任务是否适合端到端可微 MPC:四旋翼轨迹跟踪、四足乱石地形落脚选择、机械臂慢速插孔、从图像开抽屉。
- 设计题:为“四足跨障碍”设计一个混合方案,说明哪些部分用采样/搜索,哪些部分用可微 MPC。
- 实验题:构造一个带障碍的二维导航问题,比较梯度法和采样法在左右绕行选择上的差异。
S5.10 调试可微 MPC:从梯度到行为 ⭐⭐⭐¶
这一节给出排查顺序。
可微 MPC 的错误通常不在一个层级。
它可能是数学梯度错。
可能是求解器没收敛。
可能是模型坐标系错。
可能是外层 loss 错。
也可能是训练数据不可辨。
S5.10.1 五层排查链¶
| 层级 | 先问的问题 | 证据 |
|---|---|---|
| 模型层 | 动力学、代价、约束写对了吗? | 单步预测、单位、坐标系 |
| 求解层 | MPC 是否真正收敛到可行解? | KKT 残差、slack、迭代数 |
| 梯度层 | backward 是否匹配有限差分? | VJP 测试、方向导数 |
| 学习层 | 外层 loss 是否表达真实目标? | 权重变化、训练/验证差距 |
| 行为层 | 闭环行为是否有裕度? | 摩擦裕度、力矩余量、安全距离 |
不要反过来。
如果模型层错了,调学习率没有意义。
如果求解层没收敛,梯度层测试没有意义。
如果梯度不对,外层训练看似下降也不可信。
S5.10.2 有限差分检查¶
对任意参数方向 \(v\),检查方向导数:
\(\epsilon\) 不能太大。
太大会跨过活动集边界。
\(\epsilon\) 也不能太小。
太小会被浮点误差淹没。
常用扫描:
如果只有中间几个 \(\epsilon\) 对得上,通常是正常。
如果全部对不上,梯度实现有问题。
如果在约束边界附近对不上,可能是非光滑活动集切换。
S5.10.3 VJP 测试¶
完整 Jacobian 很大。
更实用的是测试 VJP。
给定输出空间随机向量 \(w\) 和参数空间随机方向 \(v\):
应该与有限差分的标量方向导数匹配:
这正是 autograd backward 需要的量。
不要为了测试构造完整 Jacobian。
它既慢,又不符合实际训练路径。
S5.10.4 梯度异常的典型症状¶
| 症状 | 可能原因 | 检查 |
|---|---|---|
| 梯度全零 | 参数未接入 OCP,活动约束屏蔽了影响 | 打印 \(\partial F/\partial\theta\) 范数 |
| 梯度巨大 | KKT 矩阵病态,活动集切换 | 条件数、正则、barrier |
| 梯度方向与有限差分相反 | 符号错误,左/右误差定义错 | 单参数手算例子 |
| 训练 loss 下降但约束违反上升 | 外层 loss 缺少安全项 | 加裕度 loss 和硬约束 |
| 参数贴上下界 | 学习器想逃出安全壳 | 检查任务是否不可行 |
S5.10.5 结构化排查表¶
| 症状 | 可能原因 | 排查步骤 | 修复方向 |
|---|---|---|---|
loss.backward() 出 NaN |
KKT 线性系统奇异,或 barrier 中 \(g\ge 0\) | 打印最小 slack、KKT 条件数、\(Q_{uu}\) 特征值 | 加正则、增大 barrier、跳过失败样本 |
| 有限差分不匹配 | 活动集切换或 backward 符号错误 | 扫描 \(\epsilon\),固定活动集测试,小维度手算 | 修正符号,远离边界测试 |
| MPC forward 经常 infeasible | 网络输出不可达参考或危险权重 | 打印参考轨迹、约束残差、slack | 限制参考速度,加参数过滤 |
| 训练越久求解越慢 | 学习器把问题推向约束边界 | 统计 active set 数量和迭代数 | 加 solve-time penalty 或裕度正则 |
| 仿真表现好实机差 | 学到软接触漏洞或残差过拟合 | 对比真实预测误差、穿透量、接触力 | 降低残差信任,加域随机化 |
| 末端任务好但机器人失稳 | 外层 loss 过度奖励末端误差 | 看 CoM/ZMP/摩擦裕度曲线 | 提高稳定权重下界,加入安全 loss |
S5.10.6 调试日志建议¶
每次 MPC solve 至少记录:
time_stamp
mode_id
solve_time_ms
sqp_iterations
kkt_residual
primal_residual
dual_residual
max_slack
active_constraints_count
min_friction_margin
min_collision_distance
max_torque_ratio
theta_min
theta_max
outer_loss
grad_norm_theta
finite_difference_check_status
这些字段能把学习问题和控制问题连接起来。
例如 grad norm 很大且 min friction margin 很小,说明学习正在把控制器推向摩擦边界。
例如 solve time 上升但 loss 下降,说明学习器在制造更难的优化问题。
S5.10.7 常见陷阱¶
| 陷阱 | 现象 | 根本原因 | 正确做法 |
|---|---|---|---|
| 只用最终 reward 判断好坏 | 训练曲线漂亮但控制器不可部署 | reward 没有反映约束压力 | 同时画约束裕度和求解器健康 |
| 有限差分只测一个 \(\epsilon\) | 误判梯度错误或正确 | 步长敏感 | 做 \(\epsilon\) 扫描 |
| 只测随机大问题 | 很难定位符号错误 | 变量太多 | 先用 1D/2D 手算问题验证 |
| 不记录活动集 | 梯度跳变无法解释 | 约束状态缺失 | 保存 active constraints 和 multipliers |
S5.10.8 练习¶
- 实现题:为一个等式约束 QP 写 VJP 测试,比较隐式梯度与有限差分。
- 排查题:给定“loss 下降但求解时间上升”的日志,列出三种可能原因和对应修复。
- 综合题:为可微 MPC 训练设计一张 TensorBoard 面板,至少包含 12 条曲线。
S5.11 累积项目:可微双积分器 MPC 到四旋翼参数学习 ⭐⭐⭐¶
本章项目分四个阶段。
它从最小系统开始,逐步加入求解器求导、物理参数学习和安全边界。
阶段 1:双积分器可微 QP¶
目标:实现一个 1D 双积分器 MPC。
状态:
控制:
离散动力学:
要求:
| 内容 | 验收 |
|---|---|
| 组装 QP | 能跟踪目标位置 |
| 隐式求导 | 对 \(Q/R\) 的梯度通过有限差分 |
| 约束 | 控制满足 $ |
| 日志 | 记录 KKT 残差和活动集 |
阶段 2:学习代价权重¶
用专家轨迹训练 \(Q/R\)。
专家可以来自手调 MPC。
也可以来自解析 LQR。
外层 loss:
要求权重有上下界。
记录训练前后的控制平滑度和跟踪误差。
阶段 3:加入可微仿真评估¶
用一个带摩擦误差的环境评估。
名义模型:
真实模型:
学习参数 \(c\) 或残差模型。
比较:
| 方法 | 预期表现 |
|---|---|
| 只学 \(Q/R\) | 能补偿一部分误差 |
| 学物理参数 \(c\) | 预测误差下降 |
| 学无界残差 | 训练快但容易不稳定 |
| 学有界残差 | 稳定且可解释 |
阶段 4:迁移到四旋翼简化模型¶
四旋翼平动简化模型:
可学习参数:
| 参数 | 物理意义 |
|---|---|
| \(m\) | 质量 |
| \(D\) | 阻力系数 |
| \(Q_p,Q_v\) | 位置和速度跟踪权重 |
| \(R_T\) | 推力正则 |
| \(V_f\) | 终端价值 |
任务是用轨迹数据学习 \(m\) 和 \(D\)。
MPC 仍然保持推力限幅和倾角约束。
这能体现可微 MPC 的真正价值:学习改善模型,但不放弃控制约束。
S5.12 延伸阅读与项目精读¶
| 资料 | 难度 | 重点 |
|---|---|---|
| Amos 等,Differentiable MPC for End-to-End Planning and Control | ⭐⭐⭐ | KKT 隐式求导和 box-constrained iLQR |
| locuslab/mpc.pytorch | ⭐⭐⭐ | 教学级 PyTorch 实现,适合读 backward |
| Frey 等,Differentiable Nonlinear Model Predictive Control | ⭐⭐⭐⭐ | 通用 NMPC 灵敏度、SQP/IPM 平滑 |
| leap-c | ⭐⭐⭐ | acados NMPC 作为 PyTorch 层的工程接口 |
| Theseus | ⭐⭐⭐ | 可微非线性最小二乘层,理解优化层通用模式 |
| TD-MPC2 | ⭐⭐ | model-based RL 强基线,用于区分采样 MPC 与可微 MPC |
| Actor-Critic MPC | ⭐⭐⭐ | MPC 作为 actor,RL 学习 MPC 参数 |
| S4 可微分仿真理论 | ⭐⭐⭐ | 接触梯度偏差和 SHAC/AHAC 边界 |
阅读顺序建议:
- 先读 mpc.pytorch 的接口和示例,建立“求解器作为层”的直觉。
- 再读 Amos 2018 的 KKT 推导,理解 backward 为什么像一次 LQR。
- 再读 acados/leap-c 相关材料,理解一般约束 NMPC 的灵敏度。
- 最后读 TD-MPC2 和 AC-MPC,形成“学习世界模型、学习 MPC 参数、采样规划”之间的边界。
S5.13 本章小结¶
S5.13.1 一张总表¶
| 主题 | 核心结论 | 工程落点 |
|---|---|---|
| 可微 MPC 定义 | 对最优解映射 \(z^\star(\theta)\) 求导 | 求解器需要自定义 backward |
| MPC 与仿真接口 | 仿真梯度和 MPC 梯度通过链式法则连接 | 简化模型做优化,高保真模型做评估 |
| Shooting / Collocation | 离散化决定梯度链和稀疏结构 | 实时 NMPC 常用 multiple shooting |
| iLQR/DDP/SQP | backward 复用 Riccati/KKT 结构 | 不显式求逆,使用分解和伴随 |
| 隐式函数定理 | 对 KKT 条件求导 | KKT 残差和条件数决定梯度可信度 |
| 可微层实现 | forward 求解,backward 解伴随系统 | 记录 solver health |
| 学习对象 | 权重、参考、终端价值、物理参数、残差 | 必须有正定性、上下界和安全先验 |
| 稳定与安全 | 学习不能删除硬边界 | CBF、终端集合、裕度、过滤器 |
| 不该求导 | 离散模式、硬接触事件、安全认证链路 | 拆分离散规划和连续优化 |
S5.13.2 三个核心公式¶
第一个公式是最优控制问题:
第二个公式是 KKT 隐式求导:
第三个公式是伴随梯度:
把这三个公式连起来,就是可微 MPC 的数学骨架。
S5.13.3 本质洞察回收¶
可微 MPC 不是把控制器变成黑盒。
它恰恰要求更清楚地暴露控制器结构。
你必须知道状态是什么。
必须知道约束是什么。
必须知道哪个参数可学。
必须知道求解器是否收敛。
必须知道梯度是否可信。
它比普通端到端策略更麻烦。
也正因为麻烦,它保留了物理系统最需要的东西:约束、解释和边界。
S5.13.4 章末自检题¶
- 数学自检:不看正文,推导无约束最优化的隐式求导公式。
- 算法自检:解释 iLQR backward pass 与 KKT 隐式 backward 的关系。
- 工程自检:写出一个可微 MPC layer 的 forward/backward 接口,说明需要保存哪些上下文。
- 安全自检:列出三个不应该被学习器直接修改的安全边界。
- 综合自检:为“四旋翼竞速”设计一个 AC-MPC 风格系统,说明 actor、critic、MPC 参数和外层 loss 分别是什么。
🔧 故障排查手册¶
| 症状 | 可能原因 | 排查步骤 | 修复方向 |
|---|---|---|---|
| 反向传播出现 NaN | KKT 矩阵奇异、barrier 参数过小、求解器未收敛 | 打印 KKT 残差、最小 slack、条件数、\(Q_{uu}\) 特征值 | 加正则、增大平滑、失败样本跳过 |
| 隐式梯度与有限差分不一致 | 活动集切换、符号错误、步长不合适 | 对 \(\epsilon\) 做扫描,固定活动集,在小问题上手算 | 修正符号,避开非光滑点测试 |
| 训练 loss 下降但实机更差 | 学到仿真漏洞、残差过强、安全裕度不足 | 对比 sim/real 预测误差、穿透量、摩擦裕度 | 限制残差、加入真实数据、增大鲁棒裕度 |
| MPC 求解越来越慢 | 学习器把参数推向边界,活动约束增多 | 统计 active constraints、迭代数、solve time p99 | 加 solve-time 正则,收缩参数范围 |
| 控制动作过激 | 控制正则过小,终端价值过乐观 | 检查 \(R\) 权重、控制限幅乘子、终端状态 | 提高 \(R\) 下界,限制 critic 外推 |
| 约束长期依赖 slack | 任务不可行或权重冲突 | 按约束类型打印 slack 时间序列 | 调整参考、增大安全距离、降低任务权重 |
| 梯度全零 | 参数未进入 OCP,或对应约束非活动 | 打印 \(F_\theta\) 范数和参数到代价/动力学的路径 | 修复参数接线,改用更有激励的数据 |
| 学到的物理参数漂移 | 数据不可辨,参数耦合 | 计算灵敏度矩阵特征值,设计激励轨迹 | 固定不可辨参数,加先验正则 |
附:术语速查¶
| 术语 | 一句话解释 |
|---|---|
| unroll differentiation | 把求解器迭代展开成计算图后反传 |
| implicit differentiation | 对最优性条件或固定点方程求导 |
| KKT residual | 最优性条件的残差,衡量求解是否可信 |
| active set | 当前恰好贴住边界的不等式约束集合 |
| adjoint sensitivity | 用一次转置线性系统求外层梯度 |
| solution sensitivity | 最优解对参数的导数 |
| RTI | 每个 MPC 周期只做一次 SQP 迭代的实时策略 |
| barrier smoothing | 用内点法屏障让不等式灵敏度更光滑 |
| residual dynamics | 在物理模型之外学习的小修正项 |
| terminal value | 用于弥补有限 horizon 短视的终端代价 |
⚠️ 章末陷阱:把优化器未收敛的解拿去反传¶
可微 MPC 的 backward 通常默认 forward 解接近最优。若 KKT 残差很大、约束违反明显或求解器提前中止,反传得到的梯度会混合求解误差和真实灵敏度。训练 loop 应记录 solve status,并对失败样本降权或跳过。
⚠️ 章末陷阱:让学习器直接改安全约束¶
外层学习可以调代价权重、模型残差或参考轨迹,但不应随意放松碰撞、力矩、关节限位和摩擦锥这类安全边界。若必须学习约束边界,应加物理先验和人工下界。
⚠️ 章末陷阱:忽略 active set 切换¶
不等式约束的活动集一旦变化,最优解对参数的导数可能出现不连续。有限差分验证时应同时记录 active set,否则同一个参数扰动可能比较的是两个不同局部问题。
练习:从一个小 QP 开始理解可微 MPC¶
A 型:写出一维约束 QP \(\min_x (x-\theta)^2\),约束 \(x \leq 1\)。分别在 \(\theta<1\)、\(\theta=1\)、\(\theta>1\) 三种情况下求 \(dx^*/d\theta\)。
B 型:实现一个无约束 iLQR 小车问题,记录 backward pass 中 \(Q_{uu}\) 的最小特征值。解释为什么需要 regularization。
C 型:给一个可微 MPC layer 设计日志字段,至少包含 KKT 残差、求解时间、active set 数量、slack 最大值、外层 loss 和梯度范数。
参考资料¶
- Amos, Jimenez, Sacks, Boots, Kolter, “Differentiable MPC for End-to-End Planning and Control”, NeurIPS 2018.
- locuslab/mpc.pytorch:
https://github.com/locuslab/mpc.pytorch - Frey, Baumgärtner 等, “Differentiable Nonlinear Model Predictive Control”, arXiv 2505.01353.
- FreyJo/differentiable_nmpc:
https://github.com/FreyJo/differentiable_nmpc - leap-c:
https://github.com/leap-c/leap-c - Pineda, Amos, Zhang, Mukadam, Boots, “Theseus: A Library for Differentiable Nonlinear Optimization”, NeurIPS 2022.
- facebookresearch/theseus:
https://github.com/facebookresearch/theseus - Hansen, Su 等, “TD-MPC2: Scalable, Robust World Models for Continuous Control”, ICLR 2024.
- Romero, Bauersfeld, Scaramuzza, “Actor-Critic Model Predictive Control”, T-RO 2025.