做控制算法这些年,我越来越觉得模型预测控制最容易被低估的部分不是求解器,不是权重矩阵,而是“预测模型本身”。刚入门时我以为只要目标函数写对了、优化问题丢给求解器就行,后来在一类强非线性对象上反复跑闭环实验,才意识到线性MPC的天花板很低,模型稍微失配,整个滚动优化就变成了“在错误模型上的精致表演”。这也是为什么“非线性与数据驱动的模型预测控制MPC研究”这个方向值得认真做:真实过程很少是线性的,而机理建模又往往耗时费力,数据驱动刚好补上了中间那条路。
这篇文章把我用Matlab做非线性MPC与数据驱动MPC全流程仿真实验的经验整理出来,覆盖平台搭建、数据生成、数据驱动建模、非线性MPC滚动优化实现、两种接入方式对比、闭环调参和踩坑心得。适合正在做相关课题的研究生,也适合想从线性MPC往非线性方向迁移的工程师。内容不求面面俱到,但每一步都以能复现为底线。
1. 研究命题拆解:非线性与数据驱动MPC为什么必须合流
1.1 经典MPC的“模型正确”执念
模型预测控制的核心逻辑是先利用预测模型推算未来一段时间的系统输出,再基于这个预测序列做滚动优化。问题在于,经典线性MPC(比如DMC、GPC)在数学表述里默认了一个前提:线性预测模型能在工作点附近可靠地外推。工业场景里这个前提通常靠“把工作点锁定在当前稳态附近”来维持,一旦工况发生大范围迁移,模型的线性近似误差就会迅速放大。
我在项目中遇到过这样的现象:同一套权重参数,在设定值小幅变化时闭环性能漂亮得很,如果把设定值拉远一点,控制器给出的第一拍控制量就离谱,甚至出现振荡。这不是优化算法的问题,而是模型没有能力表达非线性系统在大范围工作区间的真实行为。调Q、R矩阵能缓解症状,但治不了根。
1.2 非线性对象给MPC的三层困难
非线性给MPC带来的困难可以拆成三层去看,每一层都直接影响实现方案的选择。
第一层是预测困难。线性系统可以用状态转移矩阵一拍推到未来任意时刻,非线性系统做不到。预测输出只能用数值积分或者逐拍递推的方式生成,这意味着控制器内部每算一次目标函数都要跑一遍模型仿真,计算量成倍增加。
第二层是优化困难。NMPC在每一拍求解的是一个非线性规划问题,目标函数可能非凸,最优解可能不唯一,约束可行域也可能不规则。常用的SQP(序列二次规划)和内点法在这种问题上容易出现局部收敛,尤其当初值给得不好时,最终解的质量完全不可控。
第三层是稳定性分析困难。线性MPC的稳定性还可以靠终端约束、LQR之类框架去保证,非线性情况下解析推导变得非常复杂,多数工程实践只能依靠仿真验证来“用结果说服人”。所以做这个方向,一定要有心理准备:稳定性结论几乎都建立在大量场景仿真之上,而不是一套严格的数学证明。
1.3 数据驱动在哪个环节发力
数据驱动方法的机会恰恰出现在“机理建模成本过高”和“模型精度不足”这两个痛点上。它的本质不是抛弃物理知识,而是把建模任务从“推导微分方程+辨识物理参数”转变成“从输入输出数据中学习映射关系”。
具体到MPC,数据驱动方法主要有三个发力点:一是直接用黑箱模型替代机理模型,在滚动优化中充当预测器;二是在已有机理模型的基础上叠加一个数据驱动残差补偿器;三是利用在线数据持续修正模型参数,让预测器随工况漂移自适应更新。三条路线各有适用场景,我在后文会分别给出Matlab实现思路。
1.4 本文方法边界的说明
本文不打算讨论随机MPC、Tube MPC之类的扩展内容,只聚焦一个主目标:在Matlab环境下,面向一个典型强非线性对象,完成“数据采集—数据驱动建模—非线性MPC设计—闭环仿真对比”的全流程。所选对象是连续搅拌反应釜(CSTR),这是过程控制研究里最常见的验证对象,非线性程度足够强,又相对容易复现。代码结构上完全采用脚本分模块的组织方式,便于替换成你自己的被控对象。
2. 仿真平台搭建:被控对象选型、激励信号与数据集设计
2.1 代码组织与工具箱选择
Matlab做这个方向有一个优势:建模、优化、仿真、绘图可以在同一环境里闭环完成,避免跨语言传递数据造成的额外沟通成本。我的常用配置是近几年的Matlab版本,核心依赖三个工具箱:优化工具箱提供fmincon等求解函数;深度学习工具箱用于训练神经网络预测模型;系统辨识工具箱用来做初步的阶次分析。如果只做最简版本,深度学习工具箱也可以不用,用自写的BP网络更新权值即可。
工程上我强烈建议把整个项目按模块拆成多个脚本和函数文件,而不是把所有逻辑堆在一个大脚本里。下面是建议的目录结构:
mpc_project/ main.m % 主脚本:控制整条流程 plant_model.m % 被控对象微分方程 generate_data.m % 开环激励数据生成 train_predictor.m % 训练数据驱动预测模型 nonlinear_mpc_step.m % 单个控制周期的NMPC求解 simulation_loop.m % 闭环仿真主循环 plot_results.m % 结果可视化这种组织方式的好处是,当你替换被控对象或更换预测模型时,只需改动对应模块,不牵动全局。
2.2 被控对象:连续搅拌反应釜的建模细节
我用的是连续搅拌反应釜(CSTR)的经典无量纲模型。系统有两个状态:反应物浓度和反应温度,控制量是夹套温度。模型方程如下:
[ \dot{x}1=\frac{1}{\tau}(x{1,in}-x_1)+Da(1-x_1)\exp(x_2) ]
[ \dot{x}2=\frac{1}{\tau}(x{2,in}-x_2)+B,Da(1-x_1)\exp(x_2)+\beta(u-x_2) ]
其中 (x_1) 是反应物浓度,(x_2) 是反应温度,(u) 是夹套温度。参数 (\tau)、(Da)、(B)、(\beta) 分别对应停留时间、Damkohler数、放热系数和传热系数。这个模型有几个经典特征:存在多个稳态,状态之间强耦合,且反应速率项 (\exp(x_2)) 让系统呈现明显的非线性增益。
Matlab里微分方程可以这样写:
function xdot = cstr_dynamics(~, x, u, param) % 连续搅拌反应釜无量纲模型 xdot = zeros(2,1); xdot(1) = (1/param.tau) * (param.x1_input - x(1)) ... + param.Da * (1 - x(1)) * exp(x(2)); xdot(2) = (1/param.tau) * (param.x2_input - x(2)) ... + param.B * param.Da * (1 - x(1)) * exp(x(2)) ... + param.beta * (u - x(2)); end为了后续做数据驱动建模,我设定采样周期为0.1个时间单位,这个时间尺度能覆盖反应釜的主要动态过程,又不会因为采样过密导致数据冗余。
2.3 开环激励数据:PRBS的幅值、采样与噪声设计
数据驱动建模的第一步不是算法选择,而是数据质量。MPC需要模型在目标工况范围内提供可靠预测,因此开环激励信号的幅值必须覆盖你关心的操作区间。我最常用的是PRBS(伪随机二进制序列)叠加小幅斜坡信号,PRBS能在宽频带内激励出系统动态,斜坡信号则帮助模型学习缓变的非线性特性。
这里有个很关键的经验:PRBS的幅值不要太小,否则激励出的信噪比不够,模型学到的是噪声;也不要大到触发安全边界或者跑出模型表达能力的极限。对于上文CSTR对象,我把控制量 (u) 在一个允许的操作区间内来回切换,每个电平保持5~10个采样周期,避免切换太频繁导致模型只学到高频响应。
数据预处理同样不能马虎。所有状态量要归一到相近的数值量级,否则神经网络训练时某些变量会因梯度过大而主导学习过程。归一化采用简单的最大最小值映射:
data_norm = (data - data_min) ./ (data_max - data_min);训练完成后,预测值再反归一化回原始量纲。这套操作看起来基础,但直接影响后续MPC闭环的数值稳定性。
数据划分也要说清楚:我习惯把开环数据按7:2:1切成训练集、验证集、测试集,而不是随机打乱。这样做是因为连续激励数据在时间上天然相关,如果随机打乱,相当于把时间相关性人为破坏,模型评估结果会过于乐观。
3. 数据驱动建模:从开环数据到多步可用的预测模型
3.1 为什么选NARX而不是花哨的深度网络
在MPC内部充当预测器的数据驱动模型,需要满足三个条件:预测精度足够、多步递推稳定、计算开销可控。基于这些条件,我推荐先用NARX(非线性自回归外生输入模型)结构,而不是一上来就上LSTM或Transformer。原因很简单:MPC的滚动优化要求在目标函数里反复迭代计算未来Np步输出,模型每多一层递归结构,计算量就翻几倍;而NARX的递归形式与差分方程一致,嵌入优化问题时非常自然。
NARX模型的数学表达为:
[ \hat y(k+1)=f_{NN}\big(y(k),y(k-1),u(k),u(k-1)\big) ]
这里的 (f_{NN}) 我选择用一个两层前馈神经网络来逼近,输入维度由延迟阶数决定。延迟阶数一般通过系统辨识工具的相关分析来确定,对本文CSTR对象,取输入延迟2阶、反馈延迟2阶就能覆盖主导动态。阶数再增加会让模型更灵活,但也更容易在测试集上过拟合,闭环预测时反而发散。
3.2 Matlab中神经网络训练与闭环化处理
用深度学习工具箱训练NARX模型,代码非常直接:
% 构造NARX数据集 X = tonndata([u_hist(1:end-1)'], true, false); T = tonndata(y_hist', true, false); % 注意对齐 net = narxnet(1:2, 1:2, 10); net.trainFcn = 'trainlm'; net.divideFcn = 'divideind'; net.divideParam.trainInd = train_idx; net.divideParam.valInd = val_idx; net.divideParam.testInd = test_idx; [Xs, Xi, Ai, Ts] = preparets(net, X, {}, T); net = train(net, Xs, Ts, Xi, Ai); % 关键一步:转换为闭环网络,用于多步递推预测 net_closed = closeloop(net); view(net_closed);这里最需要注意的是closeloop这一步。训练阶段NARX使用真实历史输出作为反馈,这叫开环训练;实际部署时没有真实未来输出可用,必须让网络自己把自己预测的值回送给输入,这叫闭环预测。如果忘了闭合成闭环网络,MPC内部的多步递推在第一步之后就会用错误输入预测未来,结果肯定发散。
3.3 模型验证:单步误差与多步递推误差的差异
训练完模型,很多人只看单步预测误差,这是一个非常大的误区。单步预测误差小只能说明模型在每个时刻都“踩在真实轨迹附近”,却不能说明多步递推时误差是累积发散还是收敛。真正有用的验证方法是在测试集上做纯多步递推预测:从某个初始点开始,只用过去数据预测未来,然后拿预测输出当作下一步的输入,连续递推Np步以上。
我在实验中见过太多模型:单步NRMSE只有百分之几,多步递推50步后已经完全跑飞。这说明模型在开环训练时把误差反馈当成了“拐杖”,删除这个拐杖后根本站不稳。因此,数据驱动预测模型的评价指标必须包含多步递推误差,而且应该让这个误差进入模型训练的目标函数,或者至少在训练后进行额外的微调。
4. 非线性MPC控制器设计:预测、约束与滚动求解的实现路径
4.1 预测与控制时域的滚动优化数学问题
非线性MPC在每个控制周期求解的优化问题可以写成如下形式:
[ \min_{u_k,\dots,u_{k+N_c-1}} \sum_{i=1}^{N_p} \big|y_{k+i|k}-y_{ref}\big|Q^2 + \sum{j=0}^{N_c-1}\big|\Delta u_{k+j}\big|_R^2 ]
[ \text{s.t.}\quad u_{\min}\le u_{k+j}\le u_{\max} ]
其中 (y_{k+i|k}) 表示在k时刻预测k+i时刻的输出,(N_p) 是预测时域,(N_c) 是控制时域。问题的核心是把未来Np步的预测输出作为决策变量 (u) 的函数,通过优化选择控制序列使跟踪误差和控制增量尽量小。
在Matlab实现中,我习惯把优化问题写成函数回调形式:决策变量是未来Nc拍的控制量组成的向量,目标函数内部调用预测模型跑一遍仿真,返回跟踪误差平方和。这样组织结构清晰,更换预测模型时只改目标函数里的仿真部分。
4.2 约束的硬与软:避免不可行解的常用手段
预测控制的价值很大一部分来自约束处理,因此在做NMPC时一定要想清楚约束的边界:哪些约束必须硬满足,哪些可以用松弛变量软化。
对于本文CSTR对象,操作量夹套温度有执行器限幅,这是硬约束。输出量浓度的范围约束则是软约束,因为硬性的输出约束可能让优化问题在某个采样周期失去可行解。实际经验是:当约束与模型失配叠加时,优化问题很容易出现“找不到可行解”的情况,尤其当预测模型存在误差时更是如此。解决办法是在目标函数中增加松弛项,把输出约束变成惩罚项:
[ J=\sum |y-y_{ref}|_Q^2+\sum|\Delta u|_R^2+\rho \cdot s^2 ]
其中 (s) 为约束违反量,(\rho) 为较大的惩罚系数。这个方法能让优化器在约束冲突时选择最小化违反,而不是彻底无解。
4.3 fmincon/SQP的实践写法与热启动初始化
非线性优化求解,我在Matlab中最常用的还是fmincon配合SQP算法。选择SQP而不是内点法的原因是:SQP在中小规模问题上收敛速度较快,而且对初值质量的要求相对宽松,适合每拍都要实时求解的场景。
核心代码如下:
function [u_opt, Jval] = nonlinear_mpc_step(x0, u_prev, ref, mpc, predict_model) % 决策变量:未来Nc步的操作量 Nc = mpc.Nc; u_init = [u_prev; repmat(u_prev(end), Nc-1, 1)]; % 热启动初始点 lb = repmat(mpc.umin, Nc, 1); ub = repmat(mpc.umax, Nc, 1); options = optimoptions('fmincon', 'Algorithm', 'sqp', ... 'Display', 'off', 'MaxIterations', 200, ... 'OptimalityTolerance', 1e-6); u_opt = fmincon(@(u) mpc_cost(u, x0, ref, mpc, predict_model), ... u_init, [], [], [], [], lb, ub, ... @(u) mpc_nonlcon(u, x0, mpc, predict_model), options); Jval = mpc_cost(u_opt, x0, ref, mpc, predict_model); end这里的u_init是上一拍的最优解,这是非常关键的热启动策略。如果每拍都用缺省初值(比如全0),fmincon大概率会收敛到不同的局部解,控制器输出会产生无规律抖动。热启动让优化器沿着上一步的最优轨迹找下一步解,不仅提高了求解速度,也让控制轨迹更平滑。
目标函数内部的预测过程,我用一个统一的函数接口:
function y_pred = predict_output(u_traj, x0, Np, Nc, predict_model) y_pred = zeros(Np, 1); y = x0; for i = 1:Np if i <= Nc u_now = u_traj(i); else u_now = u_traj(Nc); % 控制时域之后的保持策略 end y = predict_model.step(y, u_now); % 单步递推 y_pred(i) = y; end end这种接口设计把“预测模型”抽象成了一个带有step方法的对象,不管是机理模型、神经网络模型还是两者混合,都可以无缝接入。
5. 两种数据驱动接入方式:纯黑箱模型与机理+残差补偿
5.1 纯黑箱预测模型接入MPC的实现细节
第一种方式最直接:把NARX神经网络模型当作唯一的预测器,替换掉机理模型。这样做的优点是不需要任何物理知识,建模时间短,而且只要训练数据覆盖充分,模型在拟合范围内的预测精度甚至可以超过简化机理模型。
实际实现时,需要把第二节训练好的net_closed封装成一个单步递推函数:
function y_next = nn_predict_step(y_past, u_past, net_closed) % y_past: 过去若干拍输出,u_past: 过去若干拍输入 % 将闭环网络展开为单步预测 y_cell = num2cell(y_past); u_cell = num2cell(u_past); [y_next, ~] = net_closed(y_cell, u_cell); y_next = cell2mat(y_next); end然后把这个函数传给第4节的predict_model.step接口。闭环仿真的时候,每一拍求解NMPC之前,都要用真实测量值重置预测模型的输入历史,否则模型内部状态会在闭环推进中积累误差。
对纯黑箱方案,我有多步递推误差的前车之鉴。为了抑制误差累积,我在训练最终预测模型之前,会对网络做多步损失微调,思想是用一个滑动窗口把多步预测误差也放进训练目标中,让网络自身学会“自我纠偏”。这一步在Matlab里没有现成工具箱,我是在train之后又手动用小批量梯度更新做了一遍微调,实测多步递推50步的发散程度下降了一半以上。
5.2 机理模型+数据驱动残差补偿的实现细节
第二种方式适用于你手里已经有一套机理模型,但精度不足的场景。思路很清晰:机理模型负责描述系统主动态,数据驱动模型负责描述机理模型与真实系统之间的残差。
[ y_{k+i|k} = y_{k+i|k}^{mechanism} + \hat d_{k+i|k} ]
其中 (\hat d) 是由神经网络或高斯过程回归得到的残差预测。训练残差模型时,输入仍然选取容易得到的工况变量(比如设定值、上一步控制量、上一步状态),输出则是开环数据中真实输出与机理模型预测结果的差值。
实际操作中,我发现残差模型的输入结构可以做得比主模型更简单,有时只需要“控制器输出变化量”和“当前工况点”两个特征就够了。原因是机理模型已经解释了大部分系统动态,残差的时变性主要来自参数漂移和未建模动态。
5.3 两种方式的鲁棒性与稳定性取舍
从仿真结果看,纯黑箱模型在训练数据覆盖范围内表现很惊艳,但一旦工况点稍微超出覆盖范围,预测误差会快速增加,严重时导致闭环失稳。机理+残差补偿的方式鲁棒性好很多,即使残差模型在远处预测不准,机理模型本身也会把预测拉回合理物理范围,控制器的“常识”不会被数据带跑偏。
这两种方式在实际课题里的选择逻辑,我建议按模型来源来决定:如果对象机理完全未知,用纯黑箱;如果已有可用的机理模型但精度不足,优先做补偿,而不是彻底弃用机理。把数据驱动放在“补偿”而不是“替代”的位置,往往能同时享受数据学习能力和物理约束稳健性的好处。
6. 闭环仿真与调参:场景设计、结果对比和参数规律
6.1 三类闭环测试场景的实验设计
为了公平评估控制器性能,我设计了三个测试场景,每个场景侧重不同问题:
场景A是设定值切换。系统初始稳定在某工作点,第50个采样周期把设定值切换到另一个稳态,考察跟踪速度、超调量和稳态误差。这个场景最能反映预测模型在工作区间迁移时的表达能力。
场景B是进料浓度阶跃扰动。在第100个采样周期让进料浓度产生一个不可测阶跃变化,考察控制器能否快速抑制扰动并回到设定值。这是过程控制里最常见的实际需求。
场景C是模型失配考验。训练数据采集自标称参数的仿真,但闭环验证时把系统参数整体偏移10%,模拟真实过程随时间缓慢漂移的情况。这个场景专门考验数据驱动模型的泛化能力。
三个场景共用同一套控制参数,避免针对个别场景过度调优。
6.2 结果对比表格
我用IAE(积分绝对误差)和调节时间作为主要评价指标,汇总三种控制方案在三个场景下的表现。
| 方案 | 场景A:设定值切换 | 场景B:进料扰动 | 场景C:参数失配 |
|---|---|---|---|
| 线性MPC | 调节时间略长,出现明显振荡 | 抗扰速度慢,超调较大 | 基本失稳,难以收敛 |
| 纯黑箱NARX-MPC | 跟踪快速,超调小 | 抗扰表现好 | 模型失配后发散 |
| 机理+残差补偿MPC | 跟踪速度中等 | 抗扰稳健 | 保持稳定,性能降级有限 |
表格结果反映出非常典型的规律:离线数据驱动的NARX模型在训练覆盖范围内性能最强,但外推能力差;机理+残差补偿是稳健性最好的方案。线性MPC在这个非线性问题上暴露出明显的模型失配问题。
6.3 预测时域、控制时域与权重矩阵的调参规律
调参经验是这类项目最有价值的部分。首先是预测时域Np的选择,原则是必须覆盖系统主导时间常数的1.5~3倍。对本文CSTR对象,主导时间常数为5个采样周期,我用Np=10到15。Np太小,优化器看不到长期代价,容易局部决策;Np太大,计算量线性上升,而且远期预测误差累积会稀释近端代价的作用。
控制时域Nc的选择经验是取Np的三分之一到五分之一。Nc=3到5在本文对象上表现稳定。Nc过大不仅增加决策变量维度,而且会让控制器对“远期控制动作”过于敏感,输出容易抖动。
权重矩阵Q和R的调整,我习惯用“先固定R,再增大Q”的顺序。具体做法是:先设Q=1、R=0.01,跑一遍闭环看控制量抖动程度;如果控制量振荡太厉害,把R逐步增大到0.1乃至1,直到输入轨迹平滑;再回来适当增大Q提高跟踪速度。这比一开始就同时调两个参数要容易得多。
6.4 实时计算性能观察
Matlab环境下的NMPC计算性能,我在实验中做了计时统计。当Np=10、Nc=3时,纯机理模型每拍求解耗时约2~5毫秒;换成NARX神经网络模型后,每拍耗时上升到5~10毫秒;如果Np增大到15,耗时可能超过15毫秒。这些数据说明,在硬件资源有限的嵌入式平台上直接跑Matlab NMPC并不可行,通常需要代码生成或改用更轻量级的求解库。但在仿真研究阶段,这个耗时水平完全可以接受。
7. 几个真正决定项目成败的实操细节
7.1 数据覆盖范围决定模型边界
我反复踩过同一个坑:训练数据只覆盖了工作点附近的小区间,结果模型拟合得很漂亮,但一旦设定值迁移,闭环立刻震荡。数据驱动模型的适用范围严格限制在训练数据的取值范围之内,这是无法靠算法弥补的。所以在数据生成阶段,PRBS的幅值设计绝不是随手一写,需要结合你关心的操作区间来决定。
7.2 热启动与求解器的稳定性
NMPC每拍求解时,初值对解的质量影响极大。我在测试中发现,如果把上一拍的最优控制序列平移一拍作为当前初值,fmincon几乎每拍都收敛得很好;如果直接用上一拍的原始序列不平移,偶尔会产生控制量突变。另一个稳定性的点是对决策变量做缩放:把操作量除以幅值范围,让优化变量落在0~1区间,可以明显减少SQP的数值病态。
7.3 不要一开始就追求新模型
做数据驱动MPC很容易沉迷于复杂的模型结构,但我的实际体会是:传统的NARX加多层前馈网络在多数问题上已经足够,关键在于多步递推稳定性和闭环验证。先把一套简单框架跑通闭环,再逐步引入更高级的建模和求解手段,才是工程上效率最高的路线。这也是本文把纯黑箱和机理+残差两种方式一起对比的原因——很多时候你需要的不是一个更强的模型,而是一个针对系统特点做正确取舍的方案。