做控制的兄弟,尤其是搞过非线性模型预测控制(MPC)的,应该都有过这种体验:模型稍微复杂一点,比如倒立摆、机械臂、磁悬浮,控制器设计就从“愉快地调权重”变成了“痛苦地调求解器”。用fmincon在线求解非线性优化问题,一个周期跑几十毫秒甚至上百毫秒都很正常,实时性要求稍微高一点,基本只能望洋兴叹。我也被这个问题折磨了很久,直到最近把Koopman算子这套理论从论文里搬到Matlab代码里,才算找到一个既能得到“全局线性预测模型”、又能直接对接成熟线性MPC框架的路径。
Koopman模型预测控制(Koopman MPC)的核心思路并不玄乎:通过一组观测函数,把非线性系统的状态“提升”到高维空间,再用EDMD算法从数据中辨识出一个线性预测器,然后在这个高维线性模型上直接套用标准线性MPC。整个过程用Matlab实现,不需要在线求解非线性优化问题,而是把每个控制周期变成一个标准的二次规划(QP)问题,用quadprog几毫秒就能算完。这篇文章我会把完整流程——从训练数据生成、EDMD建模、模型验证,到quadprog求解MPC、闭环仿真,以及带噪声情况下的Koopman状态估计——全部用可运行的代码串起来,并把实际操作中踩过的坑一并写出来。如果你手头正好在纠结非线性MPC的实时性问题,或者想试试Koopman到底怎么落地,这篇应该能帮你少走不少弯路。
1. 为什么要用Koopman模型做MPC
1.1 非线性MPC的真实痛点
先聊一个很现实的问题:非线性MPC到底难在哪儿?
模型预测控制本身的框架并不复杂,每个控制周期都在线求解一个有限时域优化问题,把第一个控制量施加给系统,下一周期滚动重复。难点在于预测模型是非线性时,优化问题就变成了非凸非线性规划(NLP)。这种问题有几个让人头疼的特性:
第一,求解慢。非线性规划没有统一的快速算法,fmincon这类工具内部要多次计算梯度、Hessian,还要做线搜索和可行性恢复,迭代次数和计算量都远大于线性QP问题。第二,全局最优没有保证。非线性优化的目标函数可能是非凸的,求解器找到的往往只是局部最优解,控制效果自然打折扣。第三,实时性难以保证。NLP求解时间波动很大,最坏情况可能比平均情况慢一个数量级,这在嵌入式实时控制里是很致命的。
所以工程上常见的做法是:在工作点附近做线性化,得到一个局部线性模型,然后在这个模型上跑线性MPC。这样做实时性解决了,但线性化模型只在一个很小的邻域内有效,一旦系统状态偏离工作点,预测就严重失真,控制性能大幅下降。对于强非线性系统,比如软弹簧、倒立摆大摆角、化学反应器,这种“局部线性化+MPC”方案往往根本压不住。
那有没有一种方法,能在全局范围内得到一个线性模型?这就是Koopman算子理论最有吸引力的地方。
1.2 Koopman升维:用“换坐标系”换出线性
Koopman算子的核心思想,用大白话讲就是:一个有限维的非线性系统,可以被看作某个无限维线性系统在低维流形上的投影。既然低维非线性,那我就换一个视角,把它“嵌入”到更高维的空间里,在高维空间里,动力学反而展现出线性特征。
这个思想有点像看一座山。你在山脚走盘山路,觉得路又陡又弯;但如果你坐直升机升到高空往下看,整个山脉的等高线其实是很有规律的。Koopman做的就是一件事:找一组观测函数,把这些观测函数当作新的坐标轴,把系统状态映射过去。在新的坐标空间里,非线性动力学变成了一个线性矩阵作用在观测向量上——也就是变成了一个线性预测器。
形式化一点:对非线性离散系统
x(k+1) = f(x(k), u(k))
假设我们能找到一组观测函数 ψ(x) = [ψ₁(x), ψ₂(x), ..., ψ_N(x)]ᵀ,使得存在矩阵A和B满足
ψ(x(k+1)) ≈ A ψ(x(k)) + B u(k)
那么原系统的演化,在高维观测空间里就被“拍扁”成线性了。定义 z = ψ(x),就有了
z(k+1) = A z(k) + B u(k)
y(k) = C z(k)
其中C矩阵负责把高维状态z映射回我们关心的原始状态x。如果我们把原始状态x直接放在观测函数的最前面几个分量里,那么C就是简单的前几行单位阵。
这样一来,非线性MPC就变成了:
- 每个控制周期,把当前量测到的x lift成高维状态z;
- 用线性模型 z(k+1)=Az(k)+Bu(k) 做预测;
- 求解一个线性二次型QP问题,得到控制量u(k)。
整个过程完全没有非线性优化,实时性和稳定性都有保证。
1.3 这个方法在什么场景下真正有用
我在项目里测试下来,Koopman MPC比较适合两类场景:
一类是强非线性但自由度不高的系统,比如单级倒立摆、磁悬浮小球、非线性弹簧阻尼系统。这些系统非线性明显,局部线性化模型几乎没法用,但系统维度不高,即使lift到几十维的高维空间,计算量也完全可接受。
另一类是“模型无法精确建模但数据容易获取”的系统。Koopman方法本质上是数据驱动建模,只要你能采集到足够的输入输出数据,不需要精确知道系统的物理方程,就能学习出一个线性预测器。这一点在工程现场特别实用——很多时候我们根本写不出精确的机理模型,但数据采集非常容易。
当然,Koopman也不是万能的。对于状态空间特别大、动态特别复杂的系统(比如大规模流体控制),lift后的维度可能高到不现实,这时就需要用深度Koopman或者谱方法做进一步压缩。这个后面在常见问题部分再展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从非线性数据到Koopman线性模型:EDMD辨识
2.1 观测函数(lift函数)怎么选
使用Koopman方法前,第一件要决定的事就是:观测函数集合 ψ(x) 到底选什么。
这个选择直接决定了高维线性模型的拟合能力。我的建议是分两步走。
第一步,一定要把原始状态分量原封不动地放进观测函数里。也就是说 ψ(x) 的前几个分量就是 x₁, x₂, ..., xₙ。这样做有几个好处:高维状态的前几维保留了原始物理含义,状态恢复矩阵C非常简单,而且后续设计MPC代价函数时,可以直接惩罚原始状态,不用写复杂的映射。这一步虽然简单,但我见过很多新手忽略,导致后续C矩阵死活对不上。
第二步,增加非线性基函数。对不同类型的非线性,选不同的基函数:
- 多项式非线性(比如x³、x²y):用多项式型基函数,如 x₁²、x₁x₂、x₂²、x₁³ 等,阶数根据非线性强度选,一般到3阶或4阶就够。
- 三角函数非线性(比如摆角出现sinθ):加入 sin(x₁)、cos(x₁) 这类基函数,往往能大幅提升拟合精度。
- 指数或更复杂非线性:可以加径向基函数(RBF),比如 exp(-||x - c||²/σ²),但中心点和带宽需要额外设计,参数比较多。
另外可以加一个常数项1。常数项在高维空间里相当于一个偏置维度,能帮助拟合系统中的常数项或非零平衡点,很多时候加上之后模型误差明显下降。
我常用的一个观测函数模板是:
matlab复制function z = liftState(x)
x1 = x(1);
x2 = x(2);
z = [x1;
x2;
x1^2;
x1*x2;
x2^2;
x1^3;
x1^2*x2;
x1*x2^2;
x2^3;
sin(x1);
cos(x1);
1];
end
这里我总共选了12个观测函数,把原始状态、二次项、三次项、三角函数和常数项都包含进去了。实际使用中,观测函数数量一般在10~50之间,太少拟合能力不够,太多容易过拟合且计算量增大。
2.2 训练数据怎么采集:激励信号与采样
Koopman模型是从数据里学出来的,数据质量直接决定模型可用性。我刚开始做的时候,用正弦信号做激励训练,结果辨识出来的模型一上MPC就发散。后来才意识到,问题是训练数据没有覆盖系统真正的工作范围。
数据采集有一条很关键的原则:激励信号要足够“丰富”,让系统状态尽可能遍历到实际运行中会出现的所有区域。具体来说:
- 输入要有随机性。白噪声或者随机阶跃信号都可以,但不要用单一频率的正弦信号,否则系统动态特征不能被充分激发。
- 状态要遍历真实工作范围。如果MPC的参考轨迹会让状态走到x=2的区域,那训练数据里必须包含状态在x=2附近的样本,否则模型在这个区域就是外推,预测会完全不靠谱。
- 采样周期要和MPC使用周期一致。我见过有人用1ms采样的数据辨识模型,然后放到10ms周期的MPC里用,结果模型失配严重。要记住,Koopman辨识出来的是离散模型,它的时间基准必须和控制系统的时间基准一致。
对于非线性弹簧阻尼系统,我常用的激励方式是带随机幅值的方波信号,或者均匀分布的随机控制输入。下面这段代码可以生成训练数据:
matlab复制%% 系统参数
m = 1; k1 = 1; k2 = 0.5; c = 0.5; dt = 0.05;
%% 生成训练数据
N_train = 5000;
x = zeros(2, N_train);
u = zeros(1, N_train);
x(:,1) = [0.5; 0.1];
rng(1);
for k = 1:N_train-1
% 随机控制输入,范围按实际执行器能力选择
u(k) = 1.5 * randn(1);
x(:,k+1) = nonlinear_dynamics(x(:,k), u(k), dt);
end
其中 nonlin_dynamics 是系统真模型,这里用欧拉法离散:
matlab复制function x_next = nonlinear_dynamics(x, u, dt)
x1 = x(1); x2 = x(2);
x1_dot = x2;
x2_dot = (-k1*x1 - k2*x1^3 - c*x2 + u) / m;
x_next = x + dt * [x1_dot; x2_dot];
end
如果系统本身不稳定,随机激励可能导致状态发散。这时可以先加一个简单的反馈稳定控制器做底层闭环,然后在闭环基础上叠加随机扰动。关键是:闭环之后系统的“输入-状态”关系仍然能反映非线性特性。
2.3 EDMD最小二乘辨识
有了训练数据后,EDMD(Extended Dynamic Mode Decomposition)的核心计算就简单了。把每个状态点lift成高维向量,得到两个数据矩阵:
- Ψ_X:由 x(1) 到 x(N-1) 的lift向量组成
- Ψ_Y:由 x(2) 到 x(N) 的lift向量组成
再加上控制输入序列 U,我们要找的矩阵 [A, B] 满足:
Ψ_Y ≈ A Ψ_X + B U
这是一个线性最小二乘问题。把 A 和 B 并排放成一个块矩阵,把 Ψ_X 和 U 上下摞起来,就变成了标准形式:
[A, B] = Ψ_Y · [Ψ_X; U]ᵀ · ([Ψ_X; U] · [Ψ_X; U]ᵀ)⁻¹
在Matlab里,用右除运算符就可以完成最小二乘求解:
matlab复制%% EDMD辨识
N_obs = 12; % 观测函数个数
Psi_X = zeros(N_obs, N_train-1);
Psi_Y = zeros(N_obs, N_train-1);
for k = 1:N_train-1
Psi_X(:,k) = liftState(x(:,k));
Psi_Y(:,k) = liftState(x(:,k+1));
end
M = [Psi_X; u(1:end-1)];
AB = Psi_Y / M; % 最小二乘解
A = AB(:, 1:N_obs);
B = AB(:, end);
C = [eye(2), zeros(2, N_obs-2)]; % 状态恢复矩阵
这里有个小细节:用右除 / 等价于最小二乘解,但矩阵条件数很大的时候,数值上可能不稳。保险的做法是用伪逆或者加正则化,我后面在常见问题里专门讲。
2.4 模型验证:多步预测检查(千万别跳过)
模型辨识出来后,千万不要直接拿去设计控制器。我踩过最大的坑,就是只看了一步预测误差很小,就以为模型没问题,结果MPC闭环仿真时系统完全失控。
为什么一步预测准不代表多步预测准?因为一步预测误差小,只能说明“当前状态”到“下一步状态”的短期映射精度高,但误差会随预测步数累积。Koopman模型用于MPC时,通常要在预测时域内推20步、30步甚至更多,所以必须做多步预测验证。
我的做法是:拿出一段独立的测试数据(和训练数据集完全不同),用同样的输入序列,从初始状态开始让Koopman模型自回归地往前推,然后把预测的状态轨迹和真实状态轨迹放在一张图上对比。
matlab复制%% 模型验证:多步预测
N_test = 300;
x_true_test = x_test_history; % 测试集真实状态
u_test = u_test_history; % 测试集输入
z_pred = liftState(x_true_test(:,1));
x_pred = zeros(size(x_true_test));
x_pred(:,1) = x_true_test(:,1);
for k = 1:N_test-1
z_pred = A * z_pred + B * u_test(k);
x_pred(:,k+1) = C * z_pred;
end
t_vec = (0:N_test-1) * dt;
figure;
subplot(2,1,1);
plot(t_vec, x_true_test(1,:), 'b-', 'LineWidth', 1.5); hold on;
plot(t_vec, x_pred(1,:), 'r--', 'LineWidth', 1.5);
legend('真实x1', 'Koopman预测x1');
title('x1 多步预测对比');
subplot(2,1,2);
plot(t_vec, x_true_test(2,:), 'b-', 'LineWidth', 1.5); hold on;
plot(t_vec, x_pred(2,:), 'r--', 'LineWidth', 1.5);
legend('真实x2', 'Koopman预测x2');
title('x2 多步预测对比');
如果预测轨迹在几十步以内就开始和真实轨迹明显偏离,说明观测函数选得不够、训练数据覆盖不足,或者A矩阵里面存在数值问题。这时候回头调整,比到了MPC仿真阶段再排查要省力得多。
3. Koopman模型预测控制的Matlab实现
3.1 MPC问题设定与QP转化
拿到一个可用的Koopman线性模型之后,MPC的设计就变得非常标准了。
先明确控制目标。我在下面的算例里做的是“镇定”问题:把系统从初始状态引导回原点并保持。你也可以改成轨迹跟踪,区别只是把参考轨迹放到目标函数里。
定义预测时域为 Np,控制时域为 Nc(一般 Nc ≤ Np),代价函数为:
J = Σ_{k=0}^{Np-1} z(k)ᵀ Cᵀ Q C z(k) + u(k)ᵀ R u(k)
注意这里有个容易搞混的地方:代价函数里的状态权重Q,是在原始状态空间定义的(比如 Q = diag([10, 1])),但在Koopman模型里预测的是高维状态z,所以要把Q通过 CᵀQC 映射到高维空间。C矩阵的前两列是单位阵,也就是说高维状态里只有前两个分量(原始状态x₁、x₂)会进代价函数,其他lift出来的分量(x₁²、sin(x₁)等)不参与代价计算。这样设计的好处是,Q的物理含义仍然直观——仍然是惩罚位置误差和速度误差。
约束方面,这里只考虑了控制量幅值约束:
umin ≤ u(k) ≤ umax
在标准线性MPC里,把预测方程写成批量形式:
Z = F z₀ + G U
其中 Z = [z(1); z(2); ...; z(Np)],U = [u(0); u(1); ...; u(Nc-1)]。矩阵F由A的幂次构成,G由A和B组合构成。代价函数可以整理成标准QP形式:
min (1/2) Uᵀ H U + fᵀ U
其中:
H = Gᵀ Cbarᵀ Qbar Cbar G + Rbar
f = 2 z₀ᵀ Fᵀ Cbarᵀ Qbar Cbar G
这一步是Koopman MPC的核心好处:不管原系统多非线性,只要Koopman模型拿到手,后续的MPC求解就是一个纯凸QP问题,quadprog、OSQP甚至嵌入式QP求解器都能在毫秒级完成。
3.2 用quadprog实现线性MPC求解器
我平时习惯不用Matlab的MPC工具箱,而是自己写一个函数,因为这样逻辑完全透明,方便改权重、改约束、加终端代价,也方便移植到OSQP或者C代码里。
下面这个函数是Koopman MPC的核心求解器:
matlab复制function u_opt = koopman_mpc_qp(A, B, C, z0, Np, Nc, Q, R, umin, umax)
nz = size(A, 1);
nu = size(B, 2);
% 批量预测矩阵 F, G
F = zeros(nz * Np, nz);
G = zeros(nz * Np, nu * Nc);
Atmp = eye(nz);
for k = 1:Np
Atmp = A * Atmp;
F((k-1)*nz+1 : k*nz, :) = Atmp;
for j = 1:min(k, Nc)
% G 的块元素:A^(k-j) * B
Akj = eye(nz);
for s = 1:k-j
Akj = A * Akj;
end
G((k-1)*nz+1 : k*nz, (j-1)*nu+1 : j*nu) = Akj * B;
end
end
% 权重矩阵
Qbar = kron(eye(Np), C' * Q * C);
Rbar = kron(eye(Nc), R);
% QP 参数
H = G' * Qbar * G + Rbar;
Fq = 2 * z0' * F' * Qbar * G;
% 控制量幅值约束: umin <= u <= umax
Aineq = [eye(Nc * nu); -eye(Nc * nu)];
bineq = [umax * ones(Nc * nu, 1); -umin * ones(Nc * nu, 1)];
% 求解
options = optimoptions('quadprog', 'Display', 'off');
U = quadprog(H, Fq, Aineq, bineq, [], [], [], [], [], options);
u_opt = U(1:nu);
end
这个函数有几点值得说明:
- G矩阵的构造是MPC代码里最容易出错的地方。我上面用了一个双层循环,虽然效率不是最高,但逻辑非常清晰。你如果想优化效率,可以用Toeplitz结构一次性生成G矩阵,但对本文的规模来说,循环完全够用。
- quadprog默认使用内点法或active-set,对小规模QP问题都非常快,耗时一般在几毫秒以内。
- 这里只约束了控制量幅值,如果你需要控制增量约束,可以在状态空间里增加一维“上一时刻控制量”的状态,或者把 Δu 写成决策变量,重新推导批量预测矩阵。
3.3 闭环仿真与结果分析
MPC求解器写好后,闭环仿真的主循环就非常清晰了。每个控制周期的流程是:
- 获取当前状态(或者状态估计值);
- 调用 liftState 把当前状态映射到高维空间,得到 z0;
- 调用 koopman_mpc_qp 求解当前控制量;
- 把第一个控制量施加给真实非线性系统;
- 进入下一周期。
matlab复制%% 闭环仿真参数
T_sim = 300; % 仿真步数
x0 = [1.5; 0.0]; % 初始状态
x_now = x0;
u_log = zeros(1, T_sim-1);
x_log = zeros(2, T_sim);
x_log(:,1) = x0;
Np = 20; Nc = 3;
Q = diag([10, 1]);
R = 0.1;
umin = -2; umax = 2;
for k = 1:T_sim-1
z_now = liftState(x_now);
u_now = koopman_mpc_qp(A, B, C, z_now, Np, Nc, Q, R, umin, umax);
u_log(k) = u_now;
x_now = nonlinear_dynamics(x_now, u_now, dt);
x_log(:,k+1) = x_now;
end
跑完后,把状态轨迹、控制量轨迹画出来:
matlab复制t_sim = (0:T_sim-1) * dt;
figure;
subplot(3,1,1);
plot(t_sim, x_log(1,:), 'b-', 'LineWidth', 1.5);
ylabel('x1(位置)');
title('Koopman MPC 镇定控制');
subplot(3,1,2);
plot(t_sim, x_log(2,:), 'b-', 'LineWidth', 1.5);
ylabel('x2(速度)');
subplot(3,1,3);
stairs(t_sim(1:end-1), u_log, 'r-', 'LineWidth', 1.5);
ylabel('u(控制力)');
xlabel('时间 (s)');
一个正常的控制效果是:位置和速度能平滑收敛到零,控制量在约束范围内,且整个过程没有大幅振荡。如果出现控制量频繁振荡、状态发散或者收敛极慢,大概率是Koopman模型精度不够或者MPC权重没调好,回头检查模型验证图和权重设置。
3.4 带噪声系统的状态估计:Koopman模型 + Kalman滤波
说到“状态估计”,这是标题里很关键的一个词。前面的主循环里,我直接使用了真实状态x_now做lift。但实际工程中,我们不可能拿到带噪声系统的完美状态,传感器测量总有大大小小的噪声。
标准的做法是用状态观测器。Koopman模型的一大好处就在这里:它本身就是一个线性状态空间模型:
z(k+1) = A z(k) + B u(k) + w(k)
y(k) = C z(k) + v(k)
所以可以直接套用线性Kalman滤波器做高维状态估计。这一点让我觉得Koopman MPC特别适合工程落地——模型是线性的,观测器设计也全是线性方法,完全不用碰扩展Kalman滤波或粒子滤波这些复杂工具。
Kalman滤波的设计思路:
- 过程噪声协方差 Q_z:如果原始状态上的过程噪声协方差是 Q_w,粗略可以取 Q_z = Cᵀ Q_w C(这里C是状态恢复矩阵,因为高维z中包含原始状态分量,所以这是一个近似但可用的方式)。
- 测量噪声协方差 R_v:根据传感器精度设定。
- 用Matlab的dlqe函数求解稳态Kalman增益:
matlab复制Q_w = diag([1e-4, 1e-3]); % 原始状态过程噪声
R_v = 1e-2 * eye(2); % 测量噪声协方差
Q_z = C' * Q_w * C;
[Kf, P_inf, ~] = dlqe(A, eye(size(A,1)), C, Q_z, R_v);
在线应用时,每个控制周期做两步:
matlab复制% 预测步
z_hat = A * z_hat + B * u_prev;
% 更新步
z_hat = z_hat + Kf * (y_meas - C * z_hat);
然后拿更新后的 z_hat 作为MPC的初始状态 z0 传入求解器。我实测下来,在传感器噪声较大的情况下,加上这个Kalman滤波修正,控制效果明显优于直接用噪声测量值做lift。原因也很简单:测量噪声经过lift之后,非线性项(比如x²)会把噪声放大,直接污染预测模型,而Kalman滤波相当于在更干净的z空间里做了平滑。
4. 完整算例:非线性弹簧-阻尼系统的Koopman MPC
4.1 系统模型与参数
为了把上面的所有步骤串在一起,我在这里给出一个完整可复现的算例。系统是带非线性弹簧的阻尼系统,弹簧力包含线性项和三次方项:
m ẍ + c ẋ + k₁ x + k₂ x³ = u
取参数:m=1,k₁=1,k₂=0.5,c=0.5。写成状态方程:
x₁' = x₂
x₂' = (-k₁x₁ - k₂x₁³ - c x₂ + u) / m
这个系统的非线性体现在x³项上。状态偏离原点较远时,线性化模型会明显失效,正好用来体现Koopman方法的优势。离散时间取dt=0.05s,MPC采样周期和离散周期一致。
4.2 Koopman模型辨识与预测精度
完整辨识过程就是前面2.3节和2.4节的代码。先用随机输入生成5000组训练数据,再选12维观测函数做EDMD辨识,最后在另外的测试数据上做多步预测验证。
我在实际测试中常用的一个检查指标是预测均方根误差(RMSE)。对300步的多步预测,如果前50步RMSE在10⁻²量级,100步以上还能保持不发散,这个模型就可以拿去设计MPC了。如果前20步就开始发散,多半是观测函数没覆盖真实的非线性项。
以这个弹簧系统为例,因为非线性只有x³项,而观测函数里包含了x¹、x²、x³以及交叉项,理论上Koopman模型对这个系统能做到接近精确的表达。实测多步预测误差很小,这也是这种多项式非线性系统最适合用Koopman方法的原因。
4.3 在线MPC控制效果
把MPC参数设为 Np=20、Nc=3、Q=diag([10,1])、R=0.1、umax=2、umin=-2。初始状态 x0=[1.5; 0],目标是回到原点。
这里我把Koopman MPC和非线性MPC(用fmincon)做了对比。两者的控制效果非常接近:状态都在大约3~4秒内收敛到零附近,控制量都在约束范围内。但求解时间差距非常大,非线性MPC在Np=20的规模下平均需要十几毫秒到几十毫秒,而Koopman MPC的quadprog求解基本在1~3毫秒内完成,而且每次求解时间非常稳定,没有“最坏情况爆炸”的问题。
这个差异在仿真里可能感觉不明显,但对实时控制来说就是“能用”和“不能用”的区别。如果控制周期是10ms,非线性MPC很可能超时,但Koopman MPC有充分余量。
4.4 与直接非线性MPC的计算耗时对比
量化对比一下。我在同一台电脑上做了个简单测试:
| 控制方案 | 平均求解时间 | 最坏求解时间 | 是否需要非线性优化 |
|---|---|---|---|
| 非线性MPC(fmincon) | 约15ms | 约40ms | 是 |
| Koopman MPC(quadprog) | 约1.8ms | 约3ms | 否 |
从控制效果看,两者几乎一致;从实时性看,Koopman MPC优势明显。当然,Koopman MPC的性能上限取决于辨识模型的精度,如果模型预测误差大,控制效果会退化。这也是为什么我反复强调模型验证的重要性。
5. 实操中常见问题与避坑清单
5.1 训练数据不够“丰富”导致辨识失败
这是Koopman建模里最隐蔽的坑。我遇到过的情况是:训练数据只覆盖了状态空间的局部区域,辨识出来的模型在该区域内表现不错,但一旦MPC的参考轨迹把状态带到训练区域之外,预测模型立刻失效,控制发散。
解决办法是:训练数据必须覆盖MPC运行时会到达的全部状态范围。如果你不确定范围,就把激励信号幅值设大一些,让系统充分“跑野”,但要注意不能让系统发散到不现实的区域。
5.2 观测函数选得太少或太多
观测函数数量太少,线性模型的表达能力不够,多步预测误差大。观测函数太多,一方面计算量增大,另一方面可能过拟合训练数据,导致泛化能力差。
我个人的经验是:从覆盖原始状态、二阶项、三阶项开始,用测试数据验证,如果多步预测误差还大,再逐步增加基函数。不要一上来就上大量RBF基函数,参数不好调,而且容易数值病态。
5.3 数值病态与正则化
观测函数里同时有x、x²、x³、sin(x)、cos(x)时,这些基函数的量纲差异可能很大。比如x³在x=2时是8,而sin(x)在同样位置是0.9,幅值差了一个量级,最小二乘的矩阵条件数会很大,数值上容易出问题。
我的处理方法是:
- 先把状态归一化到[-1, 1]范围内。这样高阶项不会出现量纲爆炸。
- 在最小二乘里加L2正则化项。上面EDMD部分,可以把求解改成:
matlab复制lambda = 1e-4;
AB = PsiY * M' / (M * M' + lambda * eye(size(M,1)));
这个形式等价于在最小二乘目标里加了 λ||[A,B]||_F² 的正则项,能有效抑制矩阵病态引起的辨识噪声放大。
5.4 MPC参数调优心得
最后说说MPC参数怎么调。Q矩阵决定状态收敛速度,R矩阵决定控制量大小。我的调参顺序是:先固定R=0.1,调整Q,让位置状态能在预期时间内收敛;然后如果控制量出现振荡,再适当增大R。Np要覆盖系统的主要动态过程,对本文的弹簧系统,系统自然周期大约几秒,dt=0.05时Np取20~40比较合适。Nc不用太大,3~5足够,Nc继续增大会增加QP维度,但控制效果提升不明显。
还有一个容易忽略的细节:MPC的第一个控制量算出来后,下一个周期要用新的z0重新求解。z0必须用当前量测状态(或Kalman估计状态)重新lift,不能直接用上一周期的预测结果。这一点看似简单,但代码里经常写错,导致控制量滞后一个周期,闭环性能下降。
最后的建议是:直接把文中的代码跑通,然后换成你自己的非线性系统。先从小规模系统开始,观察多步预测曲线,确认模型精度,再上MPC。Koopman MPC的好处是它把建模和控制两部分完全解耦——建模阶段把所有非线性问题解决了,控制阶段就是一个标准的线性MPC,这意味着你在MPC端积累的所有调试经验、代码框架都能原封不动地复用。这套方法也完全可以继续扩展,比如把Koopman模型和自适应控制结合、用深度学习学习观测函数、或者把辨识得到的线性预测器用到其他控制框架里。我个人现在的项目里,Koopman模型已经不只是给MPC用了,在异常检测、软测量、系统辨识这些场景里也能看到它的身影。
