做过几年非线性控制的人,基本都经历过被在线优化折磨的夜晚。MPC(模型预测控制)本身是个好东西,能处理约束,能处理多变量耦合,但模型一旦变成非线性,事情就变味了:在线要解非凸优化问题,初值敏感,计算量大,控制周期稍微短一点就跑不完。这也是Koopman算子这条路近几年越来越受关注的原因——它把非线性系统通过一组观测函数映射到高维空间,在高维空间里系统反而呈现线性定常特性。这样一来,MPC的预测模型可以从非线性变成线性,直接套成熟、稳定、快的线性MPC求解框架,同时还能保留非线性动力学的特征。
这篇文章我从思路、原理、Matlab实现到状态估计的接入,完整梳理一遍。适合正在做非线性MPC但苦于实时性,或者刚接触Koopman方法想快速上手的同学。项目比较典型的Duffing系统为例,但方法论可以平移到大多数受控动力系统。
1. 非线性MPC为什么不好解,Koopman升维思路是如何破局的
1.1 从局部线性化到全局升维:换个角度看待非线性
先回忆一下线性MPC和非线性MPC的本质差别。线性MPC在每个采样时刻解一个二次规划(QP)问题,目标函数是凸函数,约束是线性约束,这类问题在多项式时间内可解,而且全局最优,求解器非常成熟。非线性MPC就不一样了,预测模型如果是非线性函数,优化问题变成非线性规划(NLP),目标函数可能是非凸的,约束也可能是非凸的,在线求解要迭代、要选初值,算得慢不说,还可能收敛到局部最优。
传统的应对方案是在工作点附近做雅可比线性化。这个方法本身没问题,在小扰动范围内效果很好,但一旦工作范围扩大,模型失配就变得非常明显。雅可比线性化本质上是在一个局部领域用切平面近似曲面,离工作点越远,误差越大。对于强非线性系统,或者需要在较大范围内做状态转移的控制任务,这种近似是不够的。
Koopman算子的思路完全不同。它不试图在低维状态空间里做局部近似,而是构造一个新的坐标系统,把原始状态提升到高维空间,在这个高维空间里动力学是线性定常的。你可以把观测函数理解成给系统重新选取了一组坐标轴,原始坐标下的非线性轨迹,在提升坐标下可能就变成了线性轨迹。
一个生活化的类比是:在平面上画一个半径为1的圆,用笛卡尔坐标描述这个圆上的匀速运动,x和y两个坐标都是正弦曲线,耦合且非线性;但如果你转成极坐标,半径r恒为1,角度θ线性增长。Koopman的思路本质上就是找这样一组"极坐标",只不过系统更复杂,往往需要升到很高的维度甚至无穷维才能实现真正的线性化。
1.2 Koopman MPC的整体架构与定位
Koopman MPC的结构其实并不复杂,可以拆成离线和在线两个阶段。
离线阶段要做的是构建线性预测器。收集系统的输入输出数据(或者从仿真模型采样),选择一组观测函数φ(x),用EDMD(Extended Dynamic Mode Decomposition,扩展动态模态分解)这类数据驱动方法学习一个线性定常模型:
z_{k+1} = A z_k + B u_k
其中z = φ(x)是提升后的状态。这个线性模型就是后续MPC的预测模型。
在线阶段则非常接近标准线性MPC:每个采样时刻把当前状态(可能是估计值)提升成z,在提升空间里解一个QP问题,得到控制输入序列并施加第一个控制量,进入下一个周期。
这个架构的好处是:非线性的信息被编码进了提升空间和A、B矩阵里,在线优化完全不需要处理非线性项。和NMPC相比,求解速度和稳定性都有质的提升。和SLMPC(逐次线性化MPC)相比,Koopman模型的适用域更大,因为它是通过数据直接学习非线性系统的全局动力学,而不是围绕单个工作点做局部近似。
当然这也不是万能的,关键难点在于观测函数选什么、数据怎么采、模型学到什么程度,这些决定了Koopman线性预测器的精度上限。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Koopman线性预测器是怎么生成的:观测函数与EDMD实操细节
2.1 Koopman算子在离散动力学中的意义
先看理论基础,但我会尽量用工程语言而不是数学论文的语言来讲。
考虑离散时间受控非线性系统:
x_{k+1} = f(x_k, u_k)
其中x属于状态空间,u是控制输入。Koopman算子作用于函数(而不是状态向量),它的核心思想是:不直接关注状态轨迹x_k本身,而是关注一组由观测函数g(x)构成的函数空间。在这个函数空间里,系统的演化由一个线性算子K描述,满足:
(Kg)(x) = g(f(x))
也就是说,Koopman算子把"状态如何演化"变成了"函数如何演化"。由于Koopman算子是线性的,理论上完备地描述了非线性系统,但问题是它的作用空间通常是无穷维的。
工程上我们没法处理无穷维,所以退而求其次:选取N个观测函数φ_1(x), ..., φ_N(x),构成提升映射:
z = φ(x) = [φ_1(x), φ_2(x), ..., φ_N(x)]^T
然后希望在有限维空间中近似满足:
z_{k+1} ≈ A z_k + B u_k
这里A和B就是通过数据学习得到的矩阵。当这组观测函数张成的空间在动力学作用下近似不变时,这个线性模型就很准;如果空间选择得刚好使得Koopman算子在有限维子空间内精确不变,那么线性模型就是精确的,这种情况对多项式非线性系统特别容易碰到。
2.2 用EDMD从数据中学习Koopman模型
EDMD是目前最常用的Koopman模型学习算法,核心思想非常简单:用数据拟合线性回归。
假设我们采集了T+1个连续的状态转移样本,每个样本包含(x_k, u_k, x_{k+1})。对每个样本做提升映射,得到:
Z = [φ(x_0), φ(x_1), ..., φ(x_{T-1})]
Z_next = [φ(x_1), φ(x_2), ..., φ(x_T)]
U = [u_0, u_1, ..., u_{T-1}]
我们希望找到A和B使Z_next ≈ A Z + B U。把问题写成矩阵形式,令G = [Z; U](把状态提升和控制输入纵向拼接),W = [A, B],那么目标是最小化:
min_W ||Z_next - W G||_F
这个最小二乘问题有闭式解:
W = Z_next G^T (G G^T)^(-1)
在Matlab里通常不直接写这个公式,而是用反斜杠或者伪逆来求解,数值上更稳定:
matlab复制W = Z_next / [Z; U];
% 或者
W = Z_next * pinv([Z; U]);
然后:
matlab复制A = W(:, 1:N); % N是提升状态维度
B = W(:, N+1:end);
需要注意几点:G G^T可能病态,尤其当观测函数之间存在相关性或者数据量不够的时候。解决办法是加岭回归正则化:
W = Z_next G^T (G G^T + λI)^(-1)
λ取一个比较小的正数(比如1e-6到1e-3),能显著改善数值稳定性。我自己的经验是,在EDMD训练前先把提升状态做一下标准化,把不同尺度的大数小数据拉到相近范围,训练出来的模型往往更稳。
2.3 字典函数怎么选:多项式、傅里叶还是径向基
观测函数的选择是整个Koopman MPC里最影响结果的一步。没有免费的午餐,也没有一套字典能适用于所有系统,但我可以给几条经验性的指导。
如果系统的非线性项有明确的多项式形式(比如阻尼系统中的x^3项),优先选多项式字典,并且让字典包含所有原始状态变量以及出现的非线性项。这样有机会让提升空间成为一个近似不变子空间,模型精度极高。
以Duffing系统为例:
x1' = x2
x2' = -δ x2 - α x1 - β x1^3 + u
非线性项只有x1^3,那么一个自然的提升映射是:
z = [1, x1, x2, x1^3, x1^2 x2, x1 x2^2, x2^3]
把常数1放进去是为了捕获偏置项。对三次多项式系统,包含所有三次单项式的字典往往就能在Koopman算子下近似封闭,模型预测效果会比只含线性项好一大截。
如果系统结构完全未知,工程上比较常用的方案是高斯径向基函数(RBF)字典,在状态空间的关键区域撒点:
φ_i(x) = exp(-||x - c_i||^2 / σ^2)
这种字典通用性强,但维度会比较大,而且参数(中心点c_i和带宽σ)需要仔细调。另一个选择是傅里叶基函数,适合振荡性比较强的系统。
我个人的建议是,先从多项式字典开始,因为它和控制系统里常见的非线性类型匹配度最高(多项式非线性、Lipshitz非线性基本都能覆盖)。如果多项式字典效果不理想,再考虑RBF或者深度Koopman学特征。
字典函数选完别忘了一个细节:提升之后,原始状态x是z的前几个分量吗?如果不是,你需要通过φ(x)的定义保证这一点,这对后续MPC约束的设置和状态估计非常重要。一个惯例是把原始状态放在提升向量的最前面:z = [x1, x2, x3, ..., 其他函数]。这样在提升空间里,关于x的线性约束可以直接转化为关于z的约束。
3. Matlab代码实现Koopman MPC:数据、训练、求解一把梭
3.1 仿真模型与训练数据生成
我以一个受控Duffing系统为例,这也是非线性动力学里最经典的测试对象之一:
x1' = x2
x2' = -0.5 x2 - x1 - x1^3 + u
这个系统有一个明显的非线性恢复力项x1^3,线性化丢了这个项在大范围转移时就会出错,非常适合展示Koopman模型的优势。
训练数据生成要注意两点:状态空间覆盖要广,控制输入激励要充分。我的做法是随机生成多组初始条件,每组初始条件下施加一段随机阶跃输入序列,用ode45积分得到轨迹,然后以固定采样时间dt重采样成离散数据。
matlab复制dt = 0.05;
numTrajectories = 50;
dataX = []; dataU = []; dataXNext = [];
for i = 1:numTrajectories
x0 = [randn*0.5; randn*0.5];
T = 20; % 单条轨迹时长
tspan = 0:dt:T;
u = 0.5 * randn(1, length(tspan)); % 随机激励
u = update_u_by_step(u, 10); % 每10步换一次台阶
[t, x] = ode45(@(t,x) duffing_ode(x, u_interp(t)), tspan, x0);
...
end
这里有个小技巧:控制输入尽量用台阶信号,而不是纯白噪声。台阶信号包含更丰富的低频激励成分,能让动力学遍历更广泛的状态区域,EDMD学习出来的模型泛化性更好。
3.2 训练Koopman模型(关键代码)
定义提升函数,然后进行EDMD:
matlab复制function z = lift_state(x)
% 原始状态
x1 = x(1); x2 = x(2);
% 多项式提升到指定阶数,这里选择包含三次项
z = [1; x1; x2; x1^2; x1*x2; x2^2; x1^3; x1^2*x2; x1*x2^2; x2^3; sin(x1)];
end
注意这里我加了sin(x1)项,是为了让字典具备一些处理非多项式非线性成分的能力,Duffing系统本身用不到,但加了通常没有坏处,只要数据量能支撑起更大的维度。
训练部分:
matlab复制% 对每条轨迹做提升
Z = []; Z_next = []; U = [];
for i = 1:length(dataTraj)
for k = 1:length(dataTraj{i}.x)-1
Z = [Z, lift_state(dataTraj{i}.x(:,k))];
U = [U, dataTraj{i}.u(:,k)];
Z_next = [Z_next, lift_state(dataTraj{i}.x(:,k+1))];
end
end
% 拼接并做岭回归
G = [Z; U];
W = Z_next * G' * inv(G * G' + 1e-4 * eye(size(G,1)));
A = W(:, 1:N_lift);
B = W(:, N_lift+1:end);
训练完先别急着接MPC,先做一步验证:用训练好的线性模型做开环多步预测,对比真实非线性系统在相同输入下的轨迹。这一步能暴露字典选得差不差。
3.3 在线MPC求解(关键代码)
Koopman MPC的在线部分和标准线性MPC几乎一样。在每个采样时刻,把当前状态提升为z0,解如下QP问题:
min Σ_{k=1}^{Np} ||z_k - z_ref||^2_Q + Σ_{k=0}^{Np-1} ||u_k - u_ref||^2_R
约束:
z_{k+1} = A z_k + B u_k
u_min ≤ u_k ≤ u_max
这里Q是提升空间的权重矩阵,作用在z上。由于原始状态x是z的前两个分量(根据我们lift_state的定义),对位置的惩罚可以直接通过Q的前两个对角元素实现。
用Matlab自带quadprog求解:
matlab复制function u_opt = koopman_mpc(z0, z_ref, A, B, Np, Q, R, u_min, u_max)
[nx, nu] = size(B);
% 决策变量 u_0 ... u_{Np-1}
H = blkdiag(kron(eye(Np), R), kron(eye(Np), Q));
% 线性项计算略...
% 约束 Aineq*u <= bineq
...
options = optimoptions('quadprog', 'Display', 'off');
[u_seq, fval, exitflag] = quadprog(H, f, Aineq, bineq, Aeq, beq, lb, ub, [], options);
u_opt = u_seq(1:nu); % 只取第一步
end
构建QP矩阵时有个繁琐的矩阵拼接过程,需要花点耐心。优化向量通常可以设为所有控制输入u_0,...,u_{Np-1}以及所有预测状态z_1,...,z_Np的串联,这样约束和成本函数的结构都很清晰。Aeq矩阵用来表达状态递推关系z_{k+1} = A z_k + B u_k。
3.4 闭环仿真与结果评估
主循环大概是这样的:
matlab复制for k = 1:Nsim
% 测量或估计当前状态
x_meas = x_true(:,k); % 假设完全可测,先这么做
z0 = lift_state(x_meas);
% 求解MPC
u_k = koopman_mpc(z0, z_ref, A, B, Np, Q, R, u_min, u_max);
% 施加控制,用真实非线性系统推进一个采样周期
x_true(:,k+1) = rk4_step(@duffing_ode, x_true(:,k), u_k, dt);
% 记录数据
end
仿真跑完,我一般先用两个指标看效果:一个是跟踪误差的均方根值,一个是控制输入的变化率和幅度是否超出预期约束。如果跟踪误差大,先画一下预测轨迹和实际轨迹的对比,判断是模型本身不准,还是MPC参数不合理。这个排查习惯能省大量时间。
4. 状态估计怎么接入Koopman MPC:观测器设计实战
4.1 为什么需要状态估计,提升空间估计的天然优势
实际工程里并不是所有状态都可测。Duffing系统里,位置x1通常有传感器可以测,但速度x2就不一定了。如果直接把不可测的状态当成已知量送进MPC,闭环大概率会出问题,需要一个状态估计器。
在传统非线性系统上做状态估计,常用的手段是EKF(扩展卡尔曼滤波)和UKF(无迹卡尔曼滤波)。EKF需要对非线性函数做雅可比线性化,在线计算雅可比矩阵又费时间又容易出错;UKF虽然不需要解析雅可比,但为了传播协方差要采样一堆sigma点,计算量也不小。
在Koopman提升空间里,这个问题被大大简化了:提升后的模型是线性定常的,所以可以直接用线性观测器理论,包括Luenberger观测器或者标准卡尔曼滤波。这意味着所有在本科控制理论课上学过的东西都可以直接用,不需要任何在线线性化。
4.2 在提升空间设计观测器的步骤与代码
观测器设计分三步走。第一步是先明确测量方程。假设测量输出为y = C_x x,比如只测位置x1:
y_k = x1_k
那么在提升空间里,因为x1是z的第一个非零分量,所以测量方程可以写成:
y_k = C z_k
其中C = [0, 1, 0, ..., 0](如果z = [1; x1; x2; ...],常数项对输出没有贡献,所以第一列是0,第二列是1)。
第二步是设计观测器增益L。对于线性定常系统,可以用极点配置或者LQR对偶问题求增益。Matlab里place命令可以直接用:
matlab复制% 观测器极点一般选择比系统闭环极点快3到5倍
p_obs = [-5; -6; -7; -8; -9; -10]; % 假设提升维数是6
L = place(A', C', p_obs)';
第三步是观测器主流程。每个采样周期执行两步:
matlab复制% 预测步
z_pred = A * z_est + B * u_k;
% 更新步(用当前测量修正)
y_k = x1_measured;
z_est = z_pred + L * (y_k - C * z_pred);
这样得到的z_est可以直接当作MPC的初始状态z0送入。
一个需要注意的细节是,Koopman观测器估计的ẑ不一定恰好落在可行流形M = {z = φ(x)}上。也就是说,可能不存在一个x使得φ(x) = ẑ。这在理论上不是致命问题,因为MPC只关心z的发展轨迹;但如果测量模型或者约束强烈依赖于原始状态x,那么需要对这个偏差有所警觉。实际中,只要观测器和MPC的更新频率足够高,ẑ偏离流形的程度通常不大。
4.3 观测器设计与MPC联合调试的注意点
联合调试时最容易踩的坑是观测器增益太大。增益大会让估计快速收敛,但也会放大测量噪声,导致送入MPC的初始状态抖动,控制量来回跳动。一个保守的调试法则是:先把观测器极点设得比系统闭环主导极点快3倍左右,跑通闭环之后,再逐步增大极点位置(加快收敛)或加入噪声协方差调优(如果转用Kalman滤波)。
另外一个值得说的点是:线性系统的分离原理在提升空间里是成立的。也就是说,你可以独立地设计状态反馈(这里是MPC)和观测器,两者组合起来仍然稳定。这在理论上给Koopman MPC + Koopman观测器这个组合提供了充足的底气。传统NMPC + EKF组合并没有这么干净的分离性质,很多时候要靠大量仿真试错。
5. Koopman MPC典型翻车现场与调参避坑指南
5.1 EDMD模型训练的几个深坑
我见过太多人在这块翻车,而且翻得非常像。第一个坑是矩阵病态。如果提升函数里的不同项尺度差异巨大(比如x1^3从0.1变到10,数量级差了上千倍),G G^T矩阵很容易病态。解决办法很简单:训练前先对样本做归一化,或者在回归时加岭正则化项。
第二个坑是过拟合训练轨迹。如果训练数据全部来自同一初始条件或者同一类型的激励输入,模型会对这条轨迹过拟合,换一个工况预测误差就爆炸。我的做法是训练数据至少覆盖20到50条不同初始条件下的轨迹,控制输入也要混合不同幅值和切换频率的台阶信号。
第三个坑是离散化步长。EDMD学到的线性模型本质上是离散模型,离散步长太大,高频动力学被抹掉,预测精度上不去;步长太小,得到的数据轨迹每一步变化都很小,回归问题条件数变大。合适的步长是系统动态主导时间常数的十分之一到五分之一。这个需要在实践中微调。
5.2 MPC参数调试顺序与稳定性
MPC参数调试我总结出一个固定顺序,照着走不容易乱。
第一步,先确定控制输入约束u_min和u_max,这是硬性约束,不能妥协。第二步,确定预测时域Np。Np至少要覆盖系统主要动态的上升时间,否则MPC目光短浅,容易产生振荡。比如系统上升时间约为2秒,采样时间0.05秒,Np取40到60比较合理。第三步,再调权重矩阵Q和R。Q决定状态跟踪的优先级,R决定控制能耗的惩罚力度。一般先把Q放到单位量级,R放到0.1量级,观察闭环响应,再根据是否出现抖动、跟踪误差是否收敛来微调。
一个常见的错误是预测时域太短又不加输入约束,导致MPC规划出来的控制序列非常激进。这种激进通常表现为初始几步控制量顶到约束极限,系统过度振荡。调整方向是加大Np,同时稍微增大R。
如果系统在平衡点附近稳定,但大范围转移时控制效果差,先别急着调MPC,回到Koopman模型验证。用训练好的线性模型做开环预测,看它在大范围转移的路径上是否还跟得住真实系统。如果预测轨迹已经显著偏离,说明提升空间没有把非线性抓全,需要扩充字典或者增加训练数据。
5.3 几个判断模型好坏的经验指标
第一,开环预测误差。在训练集之外的随机工况上做50步预测,如果预测轨迹和真实轨迹的平均误差在可接受范围内,说明模型可信。
第二,特征值的分布。A矩阵的特征值如果出现很大的模值(大于1.1甚至更大),提示模型可能存在不稳定的虚假动态,通常是因为数据中缺乏对该方向的激励。这时需要检查数据覆盖。
第三,闭环输入的平滑性。如果控制输入在短时间内高频抖动,很多时候不是MPC参数的问题,而是观测器输出的噪声被直接放大进了MPC的初始状态。这种情况优先降低观测器增益,或者在观测器中引入轻微的输入滤波。
最后分享一个我自己的调试小习惯:每次改完字典或MPC参数,不要只跑一条轨迹就下结论。固定生成10条不同的参考轨迹和初始条件,批量跑一轮闭环仿真,统计跟踪误差和控制能量的分布,再判断改动是否有效。这样能避免被某一次运气好的仿真误导,也让参数调整有了可比较的客观依据。
Koopman MPC这套方法,在非线性系统的线性预测这块确实提供了一个很务实的折中方案:既保留了非线性系统的全局特征,又能用线性MPC的高效可靠求解方式落地。代码的框架并不复杂,真正的功夫都在字典选择、数据采集和参数的匹配上。希望这篇文章能帮你在自己的系统上少走一些弯路。
