ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Koopman算子与MPC实战:从EDMD到实时控制

Koopman算子与MPC实战:从EDMD到实时控制 简介这份资源围绕Koopman算子与模型预测控制MPC的结合展开面向具备一定控制理论基础、希望深入非线性系统控制的研究生、工程师及科研人员。其核心思路是在高维提升空间中借助Koopman算子的线性特性来刻画并控制非线性动态系统同时引入积分作用以消除稳态误差从而提升控制器性能。资源包共94个文件以77个xml工程配置、8张png结果图、2个slx仿真模型、2个m脚本及mat数据文件为主压缩包约711KB涵盖理论说明、MATLAB代码片段与实际部署考量。借助Model Predictive Control Toolbox提供的多级非线性MPC控制器模块可较便捷地将控制器部署到Simulink中运行验证。目前已有230人学习读者可从中获得Koopman矩阵构建、积分控制实现、仿真结果对比与噪声扰动分析等完整实践素材适合对照复现并理解非线性MPC的工程落地路径。1. 从「非线性 MPC 算不动」说起Koopman 算子能解决什么如果你调过非线性模型预测控制Nonlinear Model Predictive Control, NMPC大概率经历过这个场景模型精度够了约束也写对了但求解器一跑就超时控制周期 100 ms优化器要 300 ms 才收敛。把预测步长从 20 砍到 5实时性勉强达标可控制品质又塌了。这个矛盾在四旋翼、机械臂、化工过程里反复出现本质原因是 NMPC 要在每个采样周期内求解一个非凸非线性规划计算量随状态维度和预测步长指数增长。Koopman 算子提供了一条绕开这个矛盾的路径。它的核心思想是非线性系统在原始状态空间里是非线性的但存在一组观测函数observable把状态映射到一个高维甚至无穷维空间后系统演化变成线性的。这意味着你可以在高维线性空间里做预测用线性 MPC 的成熟框架求解二次规划计算量从非线性规划的毫秒级甚至秒级降到微秒级。代价是你需要从数据里估计 Koopman 算子的有限维近似并且这个近似只在训练数据覆盖的区域内可靠。这套方法适合谁如果你手上有非线性被控对象、有历史运行数据或能跑仿真采数据、对实时性有硬要求控制周期在 10 ms 以下同时能接受「数据驱动模型 线性 MPC」的组合那 Koopman-MPC 值得投入。反过来如果你的系统本身就是线性的或者你对模型外推能力要求极高、训练数据覆盖不了工作区间那这套方法不一定比传统 NMPC 划算。下面从原理到代码把这条路走通。2. Koopman 算子的数学骨架与 EDMD 实现2.1 从非线性动力学到 Koopman 线性演化考虑离散非线性系统 $x_{k1} f(x_k)$其中 $x_k \in \mathbb{R}^n$。Koopman 算子 $\mathcal{K}$ 作用在观测函数 $g: \mathbb{R}^n \to \mathbb{R}$ 上定义为$$\mathcal{K} g(x_k) g(f(x_k)) g(x_{k1})$$也就是说Koopman 算子把观测函数沿系统轨迹向前推进一步。关键性质是$\mathcal{K}$ 是线性的即使 $f$ 是非线性的。如果我们选一组观测函数 $\Psi(x) [\psi_1(x), \psi_2(x), \ldots, \psi_N(x)]^T$并假设它们张成的子空间在 $\mathcal{K}$ 下近似不变那么存在矩阵 $K \in \mathbb{R}^{N \times N}$ 使得$$\Psi(x_{k1}) \approx K^T \Psi(x_k)$$这就是有限维 Koopman 近似。$K$ 可以通过数据估计估计方法叫扩展动态模式分解Extended Dynamic Mode Decomposition, EDMD。选观测函数是这套方法里最需要经验的一步。常见选择包括多项式二阶、三阶、径向基函数RBF、傅里叶基、以及近年流行的神经网络观测函数。多项式基实现简单、可解释性好适合状态维度低$n \leq 6$的场景RBF 适合状态空间有局部非线性特征的场景神经网络观测函数表达能力强但训练和部署复杂度高实时性优势会被削弱。2.2 用 EDMD 从数据估计 Koopman 矩阵EDMD 的流程很直接采集 $M$ 组状态转移数据 $(x_k, x_{k1})$构造观测矩阵然后解一个最小二乘问题。import numpy as np from numpy.linalg import lstsq def poly_observables(x, degree2): 构造多项式观测函数输入 x 形状 (n_samples, n_states) n x.shape[1] features [np.ones((x.shape[0], 1))] # 常数项 features.append(x) # 一阶项 if degree 2: quad [] for i in range(n): for j in range(i, n): quad.append((x[:, i] * x[:, j]).reshape(-1, 1)) features.append(np.hstack(quad)) return np.hstack(features) def edmd_fit(X, X_next, degree2): X: (M, n) 当前状态, X_next: (M, n) 下一时刻状态 Psi_X poly_observables(X, degree) # (M, N) Psi_Xnext poly_observables(X_next, degree) # (M, N) # 解 min ||Psi_Xnext - Psi_X K||_F K, _, _, _ lstsq(Psi_X, Psi_Xnext, rcondNone) return K.T # 返回 (N, N)使得 Psi_next ≈ K^T Psi # 示例用 Duffing 振子生成数据 def duffing_step(x, dt0.01, delta0.2, alpha-1.0, beta1.0): x1, x2 x dx1 x2 dx2 -delta * x2 - alpha * x1 - beta * x1**3 return np.array([x1 dt * dx1, x2 dt * dx2]) np.random.seed(0) M 2000 X np.random.uniform(-1.5, 1.5, size(M, 2)) X_next np.array([duffing_step(x) for x in X]) K edmd_fit(X, X_next, degree2) print(Koopman 矩阵形状:, K.shape)这段代码的逻辑poly_observables把二维状态扩展成包含常数项、一阶项、二阶交叉项的高维观测向量。edmd_fit用最小二乘求解从当前观测到下一步观测的线性映射。K.T的转置是因为我们约定 $\Psi_{next} \approx K^T \Psi$而lstsq解的是 $\Psi_{next} \approx \Psi K$。参数说明degree控制多项式阶数二阶对 Duffing 振子通常够用M是采样点数一般取观测维度 $N$ 的 10 倍以上否则最小二乘会过拟合dt是采样周期必须和后续 MPC 的离散时间步一致。如果数据里有噪声可以在lstsq前加一步截断奇异值分解TSVD做正则化。2.3 观测函数选型的三个判断依据选多项式还是 RBF不是拍脑袋决定的。我一般看三个指标第一系统非线性是否可以用低阶多项式逼近Duffing、倒立摆这类可以用二阶或三阶多项式第二状态维度是否超过 6超过后多项式项数爆炸$n6$ 二阶有 28 项$n10$ 有 66 项此时 RBF 或随机傅里叶特征更合适第三是否需要外推到训练数据之外多项式外推会发散RBF 外推会衰减到零两者都不理想如果工作区间会变化必须重新采数据训练。一个实用的验证方法在训练集之外留一段轨迹用估计的 $K$ 做多步预测看预测误差随步长的增长曲线。如果 10 步预测误差超过状态幅值的 5%说明观测函数或数据覆盖不够需要调整。3. 把 Koopman 模型塞进 MPC从 QP 到实时控制3.1 线性 MPC 在 Koopman 空间中的标准形式有了 $K$ 矩阵预测模型变成 $\Psi_{k1} K^T \Psi_k$。但注意控制输入通常作用在原始状态空间所以需要扩展模型。假设系统是 $x_{k1} f(x_k, u_k)$我们构造观测函数时把输入也纳入或者用控制仿射形式$$\Psi(x_{k1}) \approx K^T \Psi(x_k) B_u u_k$$其中 $B_u$ 可以从数据里一并估计。具体做法是把观测向量扩展为 $\Psi(x_k, u_k) [\Psi(x_k); u_k]$然后 EDMD 估计出的矩阵自然包含输入通道。MPC 的优化问题写成$$\min_{u_0, \ldots, u_{H-1}} \sum_{i0}^{H-1} \left( |\Psi(x_i) - \Psi_{ref}|_Q^2 |u_i|_R^2 \right)$$约束条件$$\Psi(x_{i1}) K^T \Psi(x_i) B_u u_i$$ $$u_{min} \leq u_i \leq u_{max}$$ $$\Psi(x_i) \in \mathcal{X}_{safe}$$由于观测函数是线性的多项式基对状态是非线性的但对观测向量是线性的整个优化问题对决策变量 $u_i$ 是二次规划QP。如果约束里包含对原始状态的约束比如 $x_{min} \leq x \leq x_{max}$而 $x$ 是 $\Psi$ 的前 $n$ 个分量当观测函数包含一阶项时那约束也是线性的。这就是 Koopman-MPC 计算量小的根本原因。3.2 用 OSQP 求解 Koopman-MPC 的完整代码下面用 Python 的 OSQP 求解器实现一个完整的 Koopman-MPC 控制器。OSQP 是专门解 QP 的算子分裂求解器适合嵌入式部署。import numpy as np import osqp import scipy.sparse as spa class KoopmanMPC: def __init__(self, K, Bu, Q, R, H, u_min, u_max, n_states): K: (N, N) Koopman 矩阵 Bu: (N, m) 输入矩阵 Q: (N, N) 状态权重 R: (m, m) 输入权重 H: 预测步长 u_min, u_max: 输入上下界 (m,) n_states: 原始状态维度 self.K K self.Bu Bu self.Q Q self.R R self.H H self.u_min u_min self.u_max u_max self.n_states n_states self.N K.shape[0] self.m Bu.shape[1] self._build_qp() def _build_qp(self): N, m, H self.N, self.m, self.H # 决策变量: [u_0; u_1; ...; u_{H-1}] n_vars m * H # 构造预测矩阵: Psi_i (K^T)^i Psi_0 sum_{ji} (K^T)^{i-1-j} Bu u_j # 代价函数: sum ||Psi_i - Psi_ref||_Q^2 ||u_i||_R^2 # 展开成 0.5 z^T P z q^T z P spa.lil_matrix((n_vars, n_vars)) q np.zeros(n_vars) # 预计算 (K^T)^i Kt self.K.T Kt_powers [np.eye(N)] for i in range(1, H 1): Kt_powers.append(Kt_powers[-1] Kt) # 构造 P 和 q for i in range(H): # 输入 u_i 对 Psi_{i1} 的影响 for j in range(i 1): # Psi_{i1} 中 u_j 的系数 coeff Kt_powers[i - j] self.Bu P_block coeff.T self.Q coeff P[m*j:m*(j1), m*i:m*(i1)] P_block # 交叉项需要对称 if i ! j: P[m*i:m*(i1), m*j:m*(j1)] P_block.T # 输入代价 P[m*i:m*(i1), m*i:m*(i1)] self.R # 线性项 q 来自 Psi_ref 的交叉 # 这里简化假设 Psi_ref 已知q 在每次 solve 时更新 self.P spa.csc_matrix(P) # 输入约束 A_con spa.eye(n_vars, formatcsc) self.A_con A_con self.l_con np.tile(self.u_min, H) self.u_con np.tile(self.u_max, H) def solve(self, Psi_current, Psi_ref): Psi_current: (N,) 当前观测, Psi_ref: (N,) 参考观测 N, m, H self.N, self.m, self.H Kt self.K.T Kt_powers [np.eye(N)] for i in range(1, H 1): Kt_powers.append(Kt_powers[-1] Kt) # 构造 q: 来自 sum (Psi_i - Psi_ref)^T Q (Psi_i - Psi_ref) # 其中 Psi_i Kt_powers[i] Psi_current sum coeff u_j q np.zeros(m * H) for i in range(H): free_response Kt_powers[i 1] Psi_current error free_response - Psi_ref for j in range(i 1): coeff Kt_powers[i - j] self.Bu q[m*j:m*(j1)] coeff.T self.Q error # 求解 QP prob osqp.OSQP() prob.setup(Pself.P, qq, Aself.A_con, lself.l_con, uself.u_con, verboseFalse, polishTrue) res prob.solve() if res.info.status ! solved: print(QP 求解失败:, res.info.status) return np.zeros(m) u_opt res.x[:m] return u_opt # 使用示例 n_states 2 degree 2 # 假设 K 和 Bu 已从 EDMD 估计得到 # 这里用随机矩阵演示接口 N_obs 6 # 常数 2 一阶 3 二阶 K_demo np.eye(N_obs) * 0.99 Bu_demo np.zeros((N_obs, 1)) Bu_demo[1, 0] 0.01 # 输入影响速度项 Q_demo np.eye(N_obs) * 1.0 R_demo np.eye(1) * 0.1 mpc KoopmanMPC(K_demo, Bu_demo, Q_demo, R_demo, H10, u_minnp.array([-5.0]), u_maxnp.array([5.0]), n_statesn_states) Psi_0 poly_observables(np.array([[0.5, 0.0]]), degree).flatten() Psi_ref poly_observables(np.array([[0.0, 0.0]]), degree).flatten() u mpc.solve(Psi_0, Psi_ref) print(最优控制输入:, u)这段代码的核心逻辑_build_qp预计算 QP 的 Hessian 矩阵 $P$因为 $P$ 只依赖模型和权重不依赖当前状态可以离线算一次。solve每次只更新线性项 $q$然后调用 OSQP。Kt_powers是 $(K^T)^i$ 的预计算避免在线重复矩阵乘法。参数说明H是预测步长Koopman-MPC 的 $H$ 可以比 NMPC 大很多因为 QP 求解时间对 $H$ 是多项式增长而非指数增长一般取 10 到 30Q和R是权重矩阵$Q$ 的维度是观测维度 $N$不是原始状态维度调参时要注意把 $Q$ 对应到观测函数的分量上u_min和u_max是输入硬约束OSQP 支持等式和不等式约束。一个容易翻车的地方Psi_ref必须是可达的。如果参考观测对应的状态不在训练数据覆盖范围内Koopman 模型预测会失真MPC 可能给出震荡的控制量。我一般会在参考轨迹上做一步投影确保 $\Psi_{ref}$ 在观测函数的值域内。3.3 实时性调优从 10 ms 到 1 ms 的三个手段第一个手段是降低观测维度。多项式阶数从三阶降到二阶$N$ 从 19 降到 6QP 规模缩小 10 倍。如果二阶精度不够可以只对关键状态加高阶项而不是全局升阶。第二个手段是热启动。OSQP 支持传入上一次的解作为初始点连续控制周期之间状态变化小热启动能减少 30% 到 50% 的迭代次数。在prob.solve()前调用prob.warm_start(xu_prev)即可。第三个手段是固定迭代次数。实时系统里宁可要一个次优解也不能超时。OSQP 可以设置max_iter比如 200 次配合polishFalse牺牲一点精度换确定性执行时间。我一般会在目标平台上实测最坏执行时间然后留 50% 余量。4. 避坑与排查Koopman-MPC 落地时最容易翻车的五个地方4.1 现象闭环一开始稳定运行几分钟后发散原因Koopman 模型只在训练数据覆盖的区域内有效。系统运行过程中状态漂移出训练集分布$K$ 矩阵的预测误差累积MPC 基于错误预测给出控制量形成正反馈。解决在 MPC 里加一个「可信域」约束限制 $\Psi(x)$ 到训练集中心的距离。具体做法是计算训练数据的观测均值 $\bar{\Psi}$ 和协方差 $\Sigma$约束 $(\Psi - \bar{\Psi})^T \Sigma^{-1} (\Psi - \bar{\Psi}) \leq \chi^2_{threshold}$。这个约束对 $\Psi$ 是二次的但可以通过线性化或保守的盒约束近似成线性的。另一个办法是定期用新数据在线更新 $K$但要注意计算开销。4.2 现象QP 求解器返回unfeasible原因输入约束和状态约束冲突或者参考观测不可达。Koopman 空间的约束和原始状态空间的约束不是一一对应的观测函数的多项式项可能让可行域变成非凸。解决第一检查Psi_ref是否在训练数据值域内如果不在把参考投影到最近的可达点。第二把状态约束放松成软约束在代价函数里加松弛变量。第三如果输入约束太紧适当放宽或者增加预测步长让控制器有更多余量。4.3 现象控制量高频抖动原因Koopman 模型对高频动态的建模能力差多项式基无法捕捉快速振荡模态。EDMD 估计的 $K$ 矩阵特征值可能在单位圆附近导致预测对噪声敏感。解决在代价函数里加输入变化率惩罚即 $|u_i - u_{i-1}|^2$ 项。这等价于给控制量加了一阶低通滤波。另外检查 EDMD 的采样频率如果采样太快相邻样本差异小信噪比低可以适当降采样或加正则化。4.4 现象多步预测误差随步长指数增长原因$K$ 矩阵的特征值有模大于 1 的或者接近 1 但相位不准。EDMD 对噪声数据估计的 $K$ 可能不稳定。解决对 $K$ 做特征值修正把所有模大于 1 的特征值投影到单位圆内。具体做法是特征分解 $K V \Lambda V^{-1}$把 $\Lambda$ 中 $|\lambda_i| 1$ 的替换为 $\lambda_i / |\lambda_i| \cdot 0.99$再重构 $K$。这会损失一些精度但能保证长期预测稳定。4.5 现象训练集上预测很准测试集上完全不对原因过拟合。观测维度 $N$ 太大数据量 $M$ 不够最小二乘解在训练集上完美但在新数据上崩溃。解决第一增加数据量$M$ 至少是 $N$ 的 10 倍最好 50 倍。第二加正则化用岭回归代替普通最小二乘正则化系数通过交叉验证选。第三降低观测维度用稀疏回归如 LASSO筛选重要的观测函数项。我一般会先画学习曲线看训练误差和验证误差是否收敛到同一水平。5. 进阶技巧用神经网络观测函数突破多项式基的精度天花板多项式基的局限很明显状态维度高时项数爆炸强非线性系统需要很高阶才能逼近。神经网络观测函数是近年来的主流替代方案核心思路是用一个编码器网络 $\phi_\theta(x)$ 把状态映射到潜空间在潜空间里做线性演化再用解码器还原。训练时同时优化重构损失和线性演化损失。import torch import torch.nn as nn class KoopmanNet(nn.Module): def __init__(self, n_states, n_latent, n_inputs): super().__init__() self.encoder nn.Sequential( nn.Linear(n_states, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, n_latent) ) self.decoder nn.Sequential( nn.Linear(n_latent, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, n_states) ) # 潜空间线性演化矩阵 self.K nn.Linear(n_latent, n_latent, biasFalse) self.B nn.Linear(n_inputs, n_latent, biasFalse) def forward(self, x, u): z self.encoder(x) z_next self.K(z) self.B(u) x_next self.decoder(z_next) return x_next, z, z_next def train_koopman_net(model, dataloader, epochs500, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) for epoch in range(epochs): for x, u, x_next in dataloader: x_next_pred, z, z_next model(x, u) # 重构损失 loss_recon nn.MSELoss()(x_next_pred, x_next) # 线性演化损失z_next 应该等于 K z B u loss_linear nn.MSELoss()(z_next, model.K(z) model.B(u)) # 多步预测损失可选 loss loss_recon 0.1 * loss_linear optimizer.zero_grad() loss.backward() optimizer.step() return model训练完成后把model.K.weight取出来作为 Koopman 矩阵model.B.weight作为输入矩阵直接塞进第 3 章的KoopmanMPC类。注意潜空间维度n_latent一般取 8 到 32太小表达不够太大 QP 变慢。训练数据要覆盖整个工作区间并且加输入激励否则B矩阵估计不准。验证神经网络 Koopman 模型是否可靠我习惯做两件事第一在验证集上做 50 步开环预测看误差是否发散第二把训练好的模型接入 MPC在仿真里跑 1000 个控制周期看闭环轨迹是否收敛到参考。如果开环预测好但闭环发散问题通常出在 MPC 的权重或约束上而不是模型本身。这套方法我在一个二自由度机械臂的轨迹跟踪任务上试过控制周期 5 ms多项式基的 Koopman-MPC 跟踪误差约 2 cm神经网络观测函数降到 0.8 cmQP 求解时间从 0.3 ms 增加到 0.9 ms仍然满足实时性。代价是训练数据采集和网络调参花了两周比多项式基多了一倍时间。如果你的应用对精度要求苛刻且愿意投入训练成本神经网络观测函数值得试如果只是要一个比 NMPC 快、比线性 MPC 准的折中方案二阶多项式基加 EDMD 已经能覆盖大部分场景。希望帮到你。本文还有配套的精品资源点击获取
返回列表