Koopman算子加速非线性MPC:从数据驱动建模到QP求解的工程实践
2026/9/23 14:06:50 网站建设 项目流程

简介:这份资源围绕Koopman算子与模型预测控制(MPC)的结合展开,面向具备一定控制理论基础、希望深入非线性系统控制的研究生、工程师及科研人员。其核心思路是在高维提升空间中借助Koopman算子的线性特性来刻画并控制非线性动态系统,同时引入积分作用以消除稳态误差,从而提升控制器性能。压缩包共94个文件,约711KB,以77个xml工程配置、8张png结果图、2个slx仿真模型、2个m脚本及mat数据文件为主,另含md说明与prj工程文件,结构完整便于直接运行与二次开发。资源内含理论推导、MATLAB代码片段与实际部署考量,并借助Model Predictive Control Toolbox提供的多级非线性MPC控制器模块,可较便捷地将控制器迁移至Simulink环境。目前已有230人学习,适合希望掌握Koopman-MPC实现路径、对照仿真结果排查问题的读者参考。

1. 从非线性 MPC 的算力困局说起:Koopman 算子能解决什么

如果你调过非线性模型预测控制(Nonlinear Model Predictive Control),大概率经历过这种局面:被控对象刚建好一个还算准的非线性模型,控制器一跑,求解时间直接飙到几十毫秒甚至上百毫秒,采样周期稍微压一压就实时不了。更难受的是非凸优化本身没有全局最优保证,初值给偏一点,求解器要么慢,要么直接失败。非线性 MPC 的痛点从来不是「控制理论不够漂亮」,而是「在线滚动优化太贵」。

Koopman 算子提供了一条绕开这个困局的路径:它不去在线求解非线性优化,而是先把非线性系统通过一组可观测函数(observables)提升到一个高维空间,在这个空间里,系统演化近似是线性的。一旦拿到这个线性表示,滚动优化就退化成二次规划(QP),求解速度和凸性都有了保障。这就是基于 Koopman 算子的非线性模型预测控制(Nonlinear Model Predictive Control Using Koopman Operator)的核心思路。

这套方案适合谁?适合那些被控对象非线性明显、但算力有限、采样率要求又不低的场景,比如无人机姿态、机械臂关节、化工过程、车辆动力学。它不适合对模型精度要求极端苛刻、或者非线性强到线性提升根本兜不住的场合。下面我把从数据到控制器的完整链路拆开讲,包括参数怎么设、坑在哪。

2. Koopman 算子的数学底座与数据驱动近似

2.1 为什么提升到高维就能线性化

Koopman 算子的出发点是:任何一个非线性动态系统,都存在一个无穷维的线性算子,作用在可观测函数空间上。设离散系统为 $x_{k+1} = f(x_k)$,Koopman 算子 $\mathcal{K}$ 定义为 $(\mathcal{K}g)(x_k) = g(f(x_k)) = g(x_{k+1})$,其中 $g$ 是任意可观测函数。关键在于:$\mathcal{K}$ 对 $g$ 是线性的,哪怕 $f$ 本身非线性。

实际落地时不可能用无穷维,所以做有限维近似:选一组可观测函数 $\phi(x) = [\phi_1(x), \dots, \phi_N(x)]^T$,假设存在矩阵 $K$ 使得 $\phi(x_{k+1}) \approx K \phi(x_k)$。这个 $K$ 就是有限维 Koopman 矩阵。控制输入怎么进来?常见做法是把状态和控制一起提升,或者用扩展形式 $\phi(x_{k+1}) \approx K \phi(x_k) + B u_k$,后者在 MPC 里更顺手。

选型理由很直接:一旦有了 $(K, B)$,预测方程就是线性的,MPC 的滚动优化变成标准 QP。代价是维度 $N$ 通常远大于原始状态维数,而且 $K$ 的精度依赖数据质量和可观测函数的选择。

2.2 用 EDMD 从轨迹数据里估出 K 和 B

估 Koopman 矩阵最常用的方法是扩展动态模式分解(EDMD)。给定一批轨迹数据,构造提升后的数据矩阵,然后解最小二乘。下面是一段可直接跑的最小实现。

import numpy as np def lift(x, u): # 可观测函数:原始状态 + 二次项 + 控制输入 # x: (n,), u: (m,) phi = np.concatenate([ x, x**2, np.array([x[0]*x[1]]) if len(x) >= 2 else np.array([]), u ]) return phi def build_edmd_matrices(X, U, Xnext): # X: (T, n), U: (T, m), Xnext: (T, n) T = X.shape[0] Phi = np.array([lift(X[i], U[i]) for i in range(T)]) Phi_next = np.array([lift(Xnext[i], U[i]) for i in range(T)]) # 最小二乘: Phi_next ≈ Phi @ K_aug^T K_aug, _, _, _ = np.linalg.lstsq(Phi, Phi_next, rcond=None) return K_aug.T # 形状 (N, N) # 示例:采集 2000 步数据 # X, U, Xnext 由仿真或实验得到 # K_aug = build_edmd_matrices(X, U, Xnext)

逻辑说明:lift把原始状态映射到高维可观测空间,这里用了状态本身、平方项、一个交叉项和控制输入。build_edmd_matrices把所有时刻的提升向量堆成矩阵,用最小二乘求从当前提升到下一时刻提升的线性映射。返回的K_aug就是增广后的 Koopman 矩阵,它同时包含了状态演化和控制输入的影响。

参数说明:可观测函数的项数和形式是第一个要调的参数。项太少,线性近似误差大;项太多,数据需求量和计算量都上去,还容易过拟合。我一般从状态维数的 3 到 5 倍开始试。rcond控制最小二乘的截断,数据噪声大时适当放大。数据量方面,经验是至少覆盖状态空间里你关心的区域,每个维度上采样点不少于 50 个,否则 $K$ 在没见过的区域会飘。

2.3 从 K 里拆出预测用的 A 和 B

上面得到的K_aug是作用在整个提升向量上的,但 MPC 预测时通常希望写成 $\phi_{k+1} = A \phi_k + B u_k$ 的形式,把控制单独拎出来。如果lift里控制输入是直接拼在末尾的,可以按块拆分。

def split_AB(K_aug, n_phi, m): # K_aug: (n_phi + m, n_phi + m) # 假设提升向量 = [phi(x); u] A = K_aug[:n_phi, :n_phi] B = K_aug[:n_phi, n_phi:n_phi+m] return A, B # n_phi 是可观测函数(不含 u)的维数 # A, B = split_AB(K_aug, n_phi, m)

逻辑说明:K_aug的前n_phi行描述了提升状态如何演化,其中前n_phi列对应状态自身的贡献(即 $A$),后m列对应控制输入的贡献(即 $B$)。这样拆完就能直接塞进线性 MPC 的预测模型。

参数说明:这里有个容易翻车的点——如果lift里控制输入不是简单拼接,而是和状态有交叉项(比如 $x \cdot u$),那K_aug就不能这么直接拆,需要把交叉项也纳入提升向量,或者改用其他辨识结构。我一般先用简单拼接,效果不够再上交叉项。

3. 把 Koopman 模型接进 MPC:QP formulation 与约束处理

3.1 预测方程与代价函数的线性化写法

有了 $A$ 和 $B$,预测就是纯线性递推。设预测时域为 $H_p$,控制时域为 $H_c$,提升状态为 $z_k = \phi(x_k)$,则:

$$z_{k+i+1} = A z_{k+i} + B u_{k+i}$$

代价函数通常写成对原始状态和控制输入的二次型。但注意:我们预测的是提升状态 $z$,而代价往往定义在原始状态 $x$ 上。如果 $x$ 是 $z$ 的前几维(常见做法),那可以直接取 $z$ 的前 $n$ 维作为 $x$ 的估计。代价函数:

$$J = \sum_{i=1}^{H_p} (z_{k+i|k} - z_{ref})^T Q (z_{k+i|k} - z_{ref}) + \sum_{i=0}^{H_c-1} u_{k+i}^T R u_{k+i}$$

其中 $Q$ 和 $R$ 是权重矩阵。因为 $z$ 的维数比 $x$ 高,$Q$ 要相应扩展,通常只对前 $n$ 维(对应原始状态)给权重,其余维给零或很小的权重。

3.2 用 OSQP 或 quadprog 求解滚动优化

把预测方程代入代价函数,整理成标准 QP 形式 $\min \frac{1}{2} U^T H U + g^T U$,然后调求解器。下面用 OSQP 写一个最小可跑的 MPC 步。

import numpy as np import osqp import scipy.sparse as sp def mpc_step(A, B, z0, z_ref, Q, R, Hp, Hc, u_min, u_max): nz = A.shape[0] m = B.shape[1] # 构造预测矩阵(简化版,假设 Hc=Hp) # 这里只给单步示例,完整版需堆叠 # 决策变量 U = [u0, u1, ..., u_{Hc-1}] # 预测: z_i = A^i z0 + sum_{j=0}^{i-1} A^{i-1-j} B u_j # 代价: sum (z_i - z_ref)^T Q (z_i - z_ref) + u_i^T R u_i # 展开成 QP: 0.5 U^T H U + g^T U # 约束: u_min <= U <= u_max # 以下为 Hp=Hc=2 的显式构造,便于理解 Hp = Hc = 2 # 构造 H 和 g(略去繁琐推导,实际项目用自动微分或符号工具生成) # 这里直接给一个占位,重点看求解器调用 H = sp.csc_matrix(np.eye(Hc * m) * 0.1) g = np.zeros(Hc * m) # 约束 A_con = sp.csc_matrix(np.eye(Hc * m)) l = np.tile(u_min, Hc) u = np.tile(u_max, Hc) prob = osqp.OSQP() prob.setup(P=H, q=g, A=A_con, l=l, u=u, verbose=False) res = prob.solve() return res.x[:m] # 返回第一个控制量

逻辑说明:这段代码的重点不是完整的矩阵推导(那需要几十行),而是展示 QP 求解器的调用方式。实际项目里,$H$ 和 $g$ 的构造建议用符号工具或自动微分生成,手推容易出错。osqp适合稀疏 QP,quadprog适合稠密小规模问题。

参数说明:$H_p$ 和 $H_c$ 是 MPC 最核心的两个参数。$H_p$ 太短,控制器短视;太长,计算量和模型误差累积都上去。我一般取系统主要时间常数的 2 到 3 倍。$H_c$ 通常取 $H_p$ 的 1/3 到 1/2,再长对性能提升有限但计算量线性增长。$Q$ 和 $R$ 的比值决定控制 aggressiveness,$Q$ 大响应快但容易震荡,$R$ 大平稳但迟钝。约束方面,控制量约束直接写成 $u_{min} \le U \le u_{max}$,状态约束需要写成 $A_{con} U \le b_{con}$ 的形式,注意提升状态的约束不等价于原始状态约束,这是后面要讲的坑。

3.3 提升状态约束与原始状态约束的换算

这是 Koopman MPC 里最容易出问题的地方。你在原始状态 $x$ 上有约束,比如 $x_{min} \le x \le x_{max}$,但优化变量和预测都在提升空间 $z$ 里。如果 $x$ 恰好是 $z$ 的前 $n$ 维,那约束可以直接加在前 $n$ 维上。但如果 $x$ 和 $z$ 的关系不是简单截取(比如用了非线性可观测函数),就需要把原始约束映射到提升空间,或者反过来在优化后把 $z$ 映射回 $x$ 再检查。

常见做法是:如果可观测函数包含原始状态作为子集,就直接对那几维加约束;否则,在 QP 里加软约束,或者在求解后做投影。我一般优先选包含原始状态的可观测函数集,省掉这层换算。

4. 避坑与排查:Koopman MPC 落地时最容易翻车的五个点

4.1 现象:控制器在训练数据范围内表现好,一出范围就发散

原因:Koopman 矩阵是从有限数据估出来的,它只在数据覆盖的区域里近似成立。出了这个区域,线性模型外推能力极差,预测直接跑偏。

解决:采集数据时就要覆盖 MPC 可能用到的整个状态空间,包括约束边界附近。如果做不到,加一个在线更新机制,用最新数据定期重估 $K$,或者加一个误差补偿项。我一般会在调试阶段画一张预测误差随状态位置的分布图,误差大的区域就是数据盲区。

4.2 现象:提升维数一高,QP 求解反而变慢

原因:提升维数 $N$ 增大后,$A$ 和 $B$ 的规模上去,QP 的决策变量和约束数量都增加。如果 $N$ 到了几百维,QP 求解时间可能比原来非线性 MPC 还长。

解决:控制提升维数,别盲目堆可观测函数。先用少量项试,不够再加。另外可以用稀疏化方法,或者对 $K$ 做降阶。经验是 $N$ 控制在原始状态维数的 5 到 10 倍以内,QP 求解时间通常能压在毫秒级。

4.3 现象:控制量抖得厉害,执行器受不了

原因:Koopman 模型的高频误差被 MPC 放大,或者 $R$ 权重太小,控制器过于激进。

解决:增大 $R$,或者在代价函数里加控制增量惩罚 $\Delta u^T R_d \Delta u$。另外检查数据采样率,如果采样太快,噪声会被 Koopman 矩阵学进去,适当降采样或滤波。

4.4 现象:QP 求解器报 infeasible

原因:约束之间互相冲突,或者提升状态约束和原始状态约束不一致。常见于状态约束加得太紧,而 Koopman 模型预测的轨迹又必须经过某些区域。

解决:把硬约束改成软约束,加松弛变量并惩罚。或者放宽约束边界,先保证可行再逐步收紧。检查约束的数学形式,确保没有把提升状态的约束错误地当成原始状态约束。

4.5 现象:稳态误差消不掉

原因:Koopman 模型没有积分作用,或者参考点处的线性化不准。

解决:在控制器里加积分项,或者把参考点也提升到可观测空间,用提升后的参考做跟踪。另一个办法是辨识时把稳态工作点附近的数据加权,让 $K$ 在参考点附近更准。

5. 进阶技巧:用闭环数据迭代提升 Koopman 模型精度

5.1 开环辨识的局限与闭环迭代的必要性

开环采集的数据分布和闭环运行时的数据分布往往不一样。开环辨识出的 $K$ 在闭环里可能表现打折。一个实用技巧是:先用开环数据训一版 $K$,跑闭环,把闭环轨迹收集起来,和开环数据混在一起重新辨识。迭代两三轮,$K$ 在闭环工作区域里的精度会明显提升。

def iterative_koopman(X_open, U_open, Xnext_open, controller, sim_env, iters=3): X, U, Xnext = X_open, U_open, Xnext_open for it in range(iters): K_aug = build_edmd_matrices(X, U, Xnext) A, B = split_AB(K_aug, n_phi, m) # 用当前 K 跑闭环,收集新数据 X_cl, U_cl, Xnext_cl = run_closed_loop(A, B, controller, sim_env) # 合并数据 X = np.vstack([X, X_cl]) U = np.vstack([U, U_cl]) Xnext = np.vstack([Xnext, Xnext_cl]) return build_edmd_matrices(X, U, Xnext)

逻辑说明:每轮用当前 Koopman 模型跑闭环,把闭环数据并入训练集,重新辨识。这样 $K$ 会逐渐适应闭环工作点附近的动态。

参数说明:迭代次数一般 2 到 3 轮就够,再多提升有限且有过拟合风险。闭环数据的量控制在开环数据的 1/3 到 1/2,太多会淹没开环覆盖的多样性。

5.2 验证 Koopman MPC 是否值得上的三个指标

指标含义可接受范围(经验)
单步 QP 求解时间滚动优化一次耗时小于采样周期的 50%
闭环跟踪 RMSE与参考轨迹的偏差比非线性 MPC 差不超过 20%
约束违反率超出约束的步数占比软约束下小于 1%

这三个指标能帮你判断:Koopman MPC 是不是真的比原来的非线性 MPC 更划算。如果求解时间没降下来,或者跟踪精度差太多,那可能提升维数或可观测函数选得不对,得回头调。

5.3 一个我常犯的错误

我早期做 Koopman MPC 时,总想把可观测函数堆得特别全,觉得项越多模型越准。结果提升维数飙到几百,QP 求解比非线性 MPC 还慢,而且过拟合严重,闭环一跑就抖。后来学乖了:先用最简的几项跑通闭环,再根据预测误差有针对性地加项。Koopman 算子的优势在于线性化带来的求解效率,不是模型越复杂越好。控制器的目标是「够用且快」,不是「精确但跑不动」。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询