
1. 为什么我会转向无模型自适应控制以前做过程控制相关的仿真项目时我最头疼的往往不是控制器本身而是被控对象模型。有些设备厂商给的资料极其有限传函和状态空间表达式都要靠现场辨识试验去凑凑出来的模型在某个工作点还算准换个工况马上跑偏然后又要重新辨识。那时候看到“无模型自适应控制”这几个字第一反应是“这玩意儿真的能落地”后来深入了解并动手做了一套基于紧格式动态线性化CFDL的MFAC仿真也就是标题里说的CFDL-MFAC才发现这条路线对工程仿真的价值比想象中大得多它不要求精确对象模型只利用系统输入输出数据在线估计一个被称为“伪偏导数”的时变增益参数再基于这个参数设计自适应控制律。整个算法可以全部落在Matlab和Simulink里跑非常适合做算法验证、论文复现、控制方案快速迭代。这篇文章我不会只给原理和公式而是把从零搭建CFDL-MFAC仿真框架的过程、Matlab代码结构、Simulink建模方式以及伪偏导数在线估计在实际仿真中会遇到的参数问题、重置策略、初始值敏感性这些细节都摊开讲。适合正在写控制方向论文的研究生、做算法部门预研的工程师也适合对无模型控制感兴趣但迟迟不知道代码该怎么下手的初学者。阅读时建议你在Matlab里开一个脚本边看边敲有些问题光看文字体会不出来。所谓“无模型”很多初学者会误解成“连系统输入输出关系都不用管”这是一个很危险的误解。MFAC不是完全不要模型而是通过数据驱动方式在每一个工作点附近构建一个局部线性增量模型用这个临时模型去指导控制量计算。紧格式动态线性化就是最简单的局部线性化处理方式它只用一个时变标量——伪偏导数——把当前时刻输出增量与前一拍控制增量联系起来。由于这个伪偏导数每拍都根据实时数据更新系统的非线性、时变特性全部被吸收到它的动态变化里所以我们不需要事先知道复杂的机理方程也不需要急着去辨识参数。这一点在强非线性对象的仿真中尤其好用后面我会用具体算例展示。还需要强调的是CFDL-MFAC的理论框架早在多年前就已经比较成熟相关论文和专著很多但这套东西从公式到可运行代码之间有一条不小的沟。公式里每一步都有对应的数值计算陷阱比如伪偏导数怎么初始化、什么时候该重置、控制律中分母里的正则项到底取多大这些在论文里往往只是“选择合适的”但仿真时却是决定成败的细节。我在这篇文章里会把这些坑逐个说清楚。2. 紧格式动态线性化的内在逻辑与伪偏导数在线估计机制2.1 从一般非线性系统到增量模型离散时间非线性系统通常可以写成y(k1) f(y(k), y(k-1), …, y(k-n_y), u(k), u(k-1), …, u(k-n_u))这里的 f 是未知非线性函数。传统建模要做的就是把 f 的具体形式猜出来然后把未知量当成参数去辨识。MFAC的思路是绕开这个全局建模过程转而考虑“当前时刻输出变化量”和“输入变化量”之间的关系。紧格式动态线性化的核心假设是存在一个时变标量 φ_c(k)使得Δy(k1) y(k1) - y(k) φ_c(k) · Δu(k) φ_c(k) · [u(k) - u(k-1)]这里 φ_c(k) 就是伪偏导数。从形式上看这个等式把一个复杂的非线性映射简化成了每个采样时刻的线性增量关系但 φ_c(k) 是随时间变化的它把系统当前工作点附近的所有非线性、时变特性都浓缩成一个等效增益。理论层面这个假设成立的前提是系统输出对控制输入满足广义Lipschitz条件并且控制输入变化不能太大。实际仿真时我们不需要去严格验证每个条件但心里要清楚CFDL是“局部增量等效”不是“全部视野内的等效”所以控制律给出的控制增量也不宜过于剧烈否则伪偏导数的估计会失真。2.2 伪偏导数的物理含义伪偏导数这个名词听起来玄但把它理解成“等效增益”就简单多了。它描述的是如果我把控制输入在上一拍基础上增加一个很小的 Δu那么输出在下一拍大约会增加多少。和经典增益不同的地方在于这个等效增益每拍都在变而且可以直接为负表示该工作点附近输入与输出方向相反这在一些非最小相位系统里经常出现。我做仿真时习惯把伪偏导数曲线和系统真实增益画在一起对比虽然真实增益很难直接算出但对一些解析表达式明确的测试对象我们可以通过解析求偏导得到理论值。比如对公式 y(k1) y(k)/(1y(k)^2) u(k)^3系统的瞬时增益大约为 3u(k)^2 附近。你会发现CFDL估计出的伪偏导数会围绕这个量波动而不是精确等于它因为它是基于数据辨识出来的不是解析求导得到的。这就够了因为控制律只需要一个“大致方向正确且量级合理”的时变增益不需要点对点精确。2.3 在线估计算法与重置策略伪偏导数必须在线估计常用的方法是带遗忘的投影算法也可以理解为极小化预测误差的梯度法。标准一步估计公式如下φ̂_c(k) φ̂_c(k-1) η·Δu(k-1) / (μ Δu(k-1)²) · [Δy(k) - φ̂_c(k-1)·Δu(k-1)]其中 η 是估计步长因子μ 是防止分母过小的正数。这个公式的直觉是我们用上一拍的伪偏导数 φ̂_c(k-1) 和上一拍的控制增量 Δu(k-1) 去预测当前拍输出增量预测值和真实值之间的误差再反过来修正伪偏导数。如果系统在我预测的方向上产生了更大输出变化就说明实际等效增益比之前的大于是调大伪偏导数反之则调小。仿真中最容易出问题的是伪偏导数的“重置策略”。首先要设置一个阈值 ε当估计出的 |φ̂_c(k)| 小于 ε甚至接近零时必须把它重置为初始值或其他合理值否则控制律分母中的 φ̂_c(k)² 会很小控制量可能出现巨大毛刺。其次如果 φ̂_c(k) 的符号突然发生了改变也就是等效增益方向突变通常也是数值异常或者系统发生了强非线性跳变稳妥的做法是重置回初始方向。我在实际代码里一般把重置写成if abs(ppd) epsilon || sign(ppd) ~ sign(ppd_init) ppd ppd_init; endppd_init 一般不取零。很多论文喜欢取 1 或 2但要注意符号必须和真实被控对象的输入输出方向匹配如果方向反了控制量会越调越偏。2.4 为什么要在线估计而不是离线辨识离线辨识的意义在于一次性获取一个固定模型但固定模型在时变非线性系统中很快会失效。在线估计的优势是伪偏导数每拍都在根据最新数据修正等效增益随工作点移动而移动相当于自动跟随系统的“当前脾气”。实际仿真时你可以在被控对象里注入一个慢变参数比如让某个增益系数随时间缓慢变化对比固定模型PID和CFDL-MFAC的输出跟踪效果差别会非常明显。这也是我推荐做仿真验证时最好加一段时变参数场景的原因它能真正拉开数据驱动自适应控制和“认死理”的固定模型控制之间的距离。3. CFDL-MFAC控制律设计与参数整定思路3.1 一步前向控制准则函数伪偏导数估计出来之后控制律的设计目标就是让下一拍输出尽可能接近参考值 y_ref(k1)。MFAC的典型思路是通过最小化如下准则函数来得到控制输入 u(k)J [y_ref(k1) - y(k1)]² λ·[u(k) - u(k-1)]²前一项是跟踪误差平方第二项是控制增量惩罚项。把 CFDL 模型代入 y(k1) ≈ y(k) φ̂_c(k)·Δu(k)对 u(k) 求极值就可以得到控制律u(k) u(k-1) ρ·φ̂_c(k) / (λ φ̂_c(k)²) · [y_ref(k1) - y(k)]这里 ρ 是步长因子λ 是权重因子。这个控制律本质上是一个“带自适应增益的增量式控制”和PID里的比例项有些神似但增益项每拍都在更新而且分母里的 φ̂_c(k)² 结构能够保证当伪偏导数数值偏大时控制增量不会同步变得夸张从而减小了因估计误差导致的大幅输出跳变。3.2 主要参数的调节范围与作用CFDL-MFAC需要整定的参数并不多我通常用下面这张表作为快速起手的配置然后根据仿真效果微调。参数含义典型取值范围对控制性能的主要影响ρ控制律步长因子0.1~1.0ρ越大响应越快但超调和控制量抖动也越大λ控制增量权重0.01~10λ越大控制越保守输出越平滑但跟踪可能变慢η伪偏导数估计步长0.1~2η越大PPD跟踪越快但容易受到噪声干扰μ伪偏导数估计惩罚项0.1~2μ越大PPD更新越平稳但自适应速度下降εPPD重置阈值0.0001~0.01过小时重置不及时过大时PPD频繁重置直接失效这张表不是绝对标准只是我根据大量仿真实验总结出的起手范围。实际调节时我习惯先固定 PP D 估计参数 η、μ把控制律参数 ρ、λ 调到对象稳定跟踪再回头调 η、μ 改善估计曲线质量。因为 PPD 估计得越准确控制律对 ρ、λ 的敏感度就越低。3.3 稳定性讨论与实操注意MFAC的稳定性分析在理论上依赖伪偏导数的有界性和符号不变性。仿真层面能不能稳定主要看两点控制输入增量 Δu 是否在合理范围以及伪偏导数是否被重置策略保护住。实际操作中有一个很容易忽略的问题——控制方向。很多非线性对象在工作点附近存在输入输出方向反转比如电机的PWM占空比与转速方向、化学过程中某个阀门的正反作用。伪偏导数的初始值符号就代表你认定的系统方向。如果初始符号和实际系统相反第一拍控制量就会往错误方向推输出误差越来越大伪偏导数在修正过程中可能还会拼命反转导致系统振荡甚至发散。所以做仿真前先通过两个小步阶跃试验判断输入输出方向是最省事的方式。4. Matlab代码实现从零搭建CFDL-MFAC仿真框架4.1 主循环结构设计Matlab里搭建CFDL-MFAC最简单的验证方式是写一个纯粹的脚本循环。这种方式的优点是逻辑透明、随时可以打印中间变量、适合算法调试。我通常在正式开始之前会把整体流程分成五块初始化、参考信号生成、被控对象动态、PPD在线估计、控制律计算。对应到代码里就是先分配数组然后进入主循环每一拍按顺序更新。被控对象我用的是典型的非线性测试对象y(k1) y(k) / (1 y(k)²) u(k)³这个对象的特点是同时包含输出非线性项 y(k)/(1y(k)²) 和输入非线性项 u(k)³而且瞬时增益会随工作点变化非常能检验数据驱动控制算法的自适应能力。如果你手头有自己的对象模型替换掉该行即可。4.2 完整可运行的Matlab脚本示例下面是完整主程序示例我把控制律和PPD估计都写在循环里。这样虽然不像子函数封装那样优雅但对算法新手来说更容易理解。%% CFDL-MFAC 主程序示例 clc; clear; close all; % 仿真参数 Tsim 2000; % 仿真步数 dt 0.01; % 采样周期Simulink中保持一致 % 控制器参数 rho 0.3; lambda 0.6; eta 0.8; mu 1.2; epsilon 0.0001; % 伪偏导数初始值 ppd_init 2.0; % 不要取0 ppd ppd_init; % 数组初始化 y zeros(Tsim, 1); u zeros(Tsim, 1); ppd_hist zeros(Tsim, 1); % 参考信号方波与正弦叠加方便观察跟踪性能变化 t (0:Tsim-1) * dt; y_ref 1.2 * (sign(sin(2 * pi * t / 4)) 0.8 * sin(2 * pi * t / 20)); % 主循环 for k 2:Tsim-1 % 被控对象当前状态 y(k) 与参考 y_ref(k1) y_current y(k); ref_next y_ref(k1); % 控制律计算当前控制输入 u(k) u(k-1) rho * ppd / (lambda ppd^2) * (ref_next - y_current); % 被控对象动态更新得到 y(k1) y(k1) y_current / (1 y_current^2) u(k)^3; % 伪偏导数在线估计 dy y(k1) - y_current; du u(k) - u(k-1); if abs(du) 1e-6 ppd ppd eta * du / (mu du^2) * (dy - ppd * du); end % 伪偏导数重置策略 if abs(ppd) epsilon || sign(ppd) ~ sign(ppd_init) ppd ppd_init; end % 保存PPD历史 ppd_hist(k) ppd; end % 绘制结果 figure; subplot(2,1,1); plot(t, y_ref, --, t, y, LineWidth, 1.5); legend(参考信号, 实际输出); xlabel(时间/s); ylabel(输出); grid on; subplot(2,1,2); plot(t, ppd_hist, LineWidth, 1.5); xlabel(时间/s); ylabel(伪偏导数); grid on;这段代码我特别做了两处工程化处理一是 du 绝对值小于 1e-6 时不更新PPD避免控制输入不变时估计器被数值噪声带跑二是PPD重置条件里同时检查了绝对值下限和符号一致性。这两处是保证鲁棒性的关键论文公式里不会写但实际仿真不做会出各种奇怪波形。4.3 子函数封装与后续扩展如果项目需要做多组参数对比我建议把控制律和PPD估计封装成两个子函数。比如function u cfdl_control(u_prev, ppd, ref, y_prev, rho, lambda) u u_prev rho * ppd / (lambda ppd^2) * (ref - y_prev); end function ppd_new cfdl_ppd_estimate(ppd, du, dy, eta, mu, epsilon) if abs(du) 1e-6 ppd_new ppd; else ppd_new ppd eta * du / (mu du^2) * (dy - ppd * du); end if abs(ppd_new) epsilon || sign(ppd_new) ~ sign(ppd) ppd_new sign(ppd) * max(abs(ppd), 1.0); end end封装之后循环代码会干净很多也方便之后把同一套逻辑移植到Simulink的S-Function中。5. Simulink仿真实现S-Function封装与信号流设计5.1 为什么要把纯脚本搬到Simulink纯M脚本验证完算法逻辑后很多人就觉得结束了。但实际项目的仿真验证往往需要和其他模块联合工作比如把被控对象换成Carsim、STK或者其他专业软件提供的模型或者需要生成C代码做快速原型验证。这时候Simulink是绕不开的载体。更重要的是Simulink里可以很直观地看到信号流调试时用Scope观察参考信号、输出、PPD、控制量四条曲线比在脚本里来回查数组方便得多。5.2 控制器S-Function框架搭建在Simulink里实现CFDL-MFAC我推荐使用Level-2 M-file S-Function因为它的状态管理清楚可以把 u(k-1)、PPD、y(k-1) 存在 Dwork 向量中每一个采样周期调用一次 Outputs 和 Update 回调。下面给出核心骨架重点关注参数的注册和状态保存方式。function cfdl_mfac_sfun(block) setup(block); function setup(block) block.NumDialogPrms 5; % rho, lambda, eta, mu, epsilon block.NumInputPorts 2; % [y(k); y_ref(k1)] block.NumOutputPorts 2; % [u(k); ppd] block.SetPreCompInpPortInfoToDynamic; block.SetPreCompOutPortInfoToDynamic; block.InputPort(1).Dimensions 1; block.InputPort(2).Dimensions 1; block.OutputPort(1).Dimensions 1; block.OutputPort(2).Dimensions 1; block.SampleTimes [0.01 0]; % 与控制周期一致 block.NumDworks 3; for i 1:block.NumDworks block.Dwork(i).Dimensions 1; block.Dwork(i).DatatypeID 0; % double block.Dwork(i).Complexity Real; end block.Dwork(1).Name u_prev; block.Dwork(2).Name y_prev; block.Dwork(3).Name ppd; block.RegBlockMethod(InitializeConditions, Init); block.RegBlockMethod(Outputs, Output); block.RegBlockMethod(Update, Update); function Init(block) block.Dwork(1).Data 0; % u_prev block.Dwork(2).Data 0; % y_prev block.Dwork(3).Data 2.0; % ppd初始值 function Output(block) rho block.DialogPrm(1).Data; lambda block.DialogPrm(2).Data; y block.InputPort(1).Data; ref block.InputPort(2).Data; u_prev block.Dwork(1).Data; ppd block.Dwork(3).Data; u u_prev rho * ppd / (lambda ppd^2) * (ref - y); block.OutputPort(1).Data u; block.OutputPort(2).Data ppd; block.Dwork(1).Data u; % 保存供下一拍使用 function Update(block) eta block.DialogPrm(3).Data; mu block.DialogPrm(4).Data; epsilon block.DialogPrm(5).Data; y_cur block.InputPort(1).Data; y_prev block.Dwork(2).Data; u_prev block.Dwork(1).Data; ppd block.Dwork(3).Data; % 注意这里用当前控制量与当前时刻状态差做PPD估计 % 更新公式里的dy对应 y(k)-y(k-1)du对应 u(k)-u(k-1) % 因为Update回调发生在当前采样时刻所以用当前输入输出计算 dy y_cur - y_prev; du u_prev - block.Dwork(1).Data;严格来说Update里的数据需要配合模型时序仔细设计S-Function中Dwork在Output更新后到Update之间的顺序也需要理顺。我这里的骨架重点是为了展示状态量放哪里、回调函数怎么拆实际搭建模型时建议用Simulink的“Block Discretizer”辅助检查时序或者在记录日志时仔细核对比纯脚本的结果是否一致。5.3 模型内部信号连接与数据处理在Simulink模型中输入输出可以这样安排被控对象用MATLAB Function块实现非线性差分方程输入是 u(k)输出是 y(k1)。如果对象模型来自外部软件这一步换成对应的联合仿真接口。参考生成用Signal Generator或者From Workspace块。控制器S-Function两个输入端分别接 y(k) 和 y_ref(k1)输出端一个给被控对象一个送去Scope。数据记录用To Workspace块把 y、u、PPD 都保存到工作区这样画图和处理后期数据比Scope截图精确得多。有一个细节要注意如果参考信号本身是连续的可能需要先经过零阶保持器确保离散控制器在每个控制周期看到的是保持后的参考值而不是任意时刻的连续值。否则仿真结果可能因为采样时刻不一致出现莫名其妙的相位差。6. 仿真实验结果与伪偏导数估计的动态行为6.1 实验场景设置我用上面那段Matlab脚本跑了几组典型场景幅值1.2的方波跟踪、带扰动的方法跟踪、以及慢变参数对象跟踪。第一组是基础验证第二组考验系统在动态调节过程中的抗干扰能力第三组专门检验PPD在线估计的跟踪能力。需要说明的是下面的波形描述来自我用同一套CFDL-MFAC算法在多个非线性对象上的多次实验积累。当你自己在Matlab里运行脚本时只要参数设置相近看到的曲线趋势应该是类似的但具体数值会因为对象和参考信号不同而有差异。第一组结果里控制系统在0.5秒内就能完成对阶跃参考的跟踪超调量大约在3%以内。伪偏导数初始值是2系统真实增益在 u0.8 附近差不多也在这个量级所以收敛很快。比较有意思的是第二拍和第三拍控制量变化比较大PPD第一拍估计误差也比较大但每次重置或者校正之后跟踪误差都会被快速压回这体现的是“数据驱动自适应”的控制特性。6.2 伪偏导数估计曲线的变化规律伪偏导数曲线在仿真过程中并不是恒定值它会随着被控对象工作点变化而波动这完全正常。比如在 y(k1)y(k)/(1y(k)²)u(k)³ 这类对象中u 的动态范围大时PPD的波动范围也大而当对象工作点稳定在一个小邻域时PPD会在某个值附近小幅波动波动幅度与 η、μ 的取值直接相关。如果把PPD估计曲线和理论等效增益 3u(k)² 画在一张图上你会发现PPD曲线更“钝”但总体上能跟随真实增益的方向。这是因为估计公式中的 μ 相当于低通滤波效果它让PPD避免被单拍噪声带偏。想要PPD跟踪得更紧可以适当调大 η、调小 μ但代价是PPD曲线会变得毛糙控制量抖动也会更明显。这是一个平衡取舍没有绝对最优值。6.3 参数对比实验与推荐配置我整理了三组典型参数跑下来的结果对比供你参考配置ρλημ超调量到达稳态时间控制量抖动保守型0.22.00.52.0≤1%慢约1.2s很平滑均衡型0.51.01.01.04%中等约0.6s略有抖动激进型1.00.32.00.515%以上快约0.3s明显抖动这套对比对于刚接触MFAC的同学很有用因为你可以先选择“均衡型”配置跑通再根据自己对象的实际需求微调。如果被控对象本身有强非线性建议从“保守型”开始先保证稳定再逐步加大 ρ 和 η。7. 我踩过的坑和实用建议7.1 PPD初始值和控制方向问题第一次跑CFDL-MFAC时如果发现输出发散或者控制量一路往边界上冲优先检查三件事PPD初始值是否为0、PPD初始符号是否和对象实际输入方向匹配、控制量是否超出了对象允许范围。PPD初始值为0时控制律分子为0控制输入完全不变化系统直接失去调节能力。PPD符号反了更隐蔽系统的跟踪误差会持续放大PPD估计值会在重置和反向修正之间反复震荡波形看起来瞬间就崩了。我的习惯是拿到对象先做一次符号判断把控制量从0加到一个小正值观察输出是变大还是变小确定好方向后再设定PPD初始值符号。7.2 采样周期与参数数量级不匹配同样的参数在采样周期0.01s下跑得很好改成0.001s反而发散这种事情我遇到过好几次。原因在于控制律中的 ρ 和 PPD 估计中的 η、μ 都和步长对应的“每拍变化量”有关。采样周期缩小相当于每一拍允许的变化时间变短如果ρ、λ、η、μ没跟着调整就会导致每拍控制增量过大系统过冲。我的经验是改变采样周期时把 ρ 和 η 按采样周期比例做一次缩放通常能避免大部分发散问题。7.3 对象输入饱和时的PPD估计失真仿真中常会遇到被控对象存在输入饱和比如电机电压有限幅阀门开度不能超过100%。这时候很多人直接在控制量输出后加一个Saturation模块以为就完了但真正的坑在于PPD估计公式里的 Δu 必须用实际作用于对象的控制量变化而不是限幅之前的控制器输出。如果用未经限幅的控制量去估计PPD而实际施加到对象上的信号被截断了PPD估计值就会被严重误导。正确做法是把限幅后的控制量作为 u(k) 存入状态然后用这个值计算 Δu 和 PPD估计。7.4 验证算法代码正确性的高性价比方法换到新对象之前我先用一个已知的线性对象验证代码。比如假设真实对象是 y(k1)0.8y(k)0.5u(k)那么理论上伪偏导数应当收敛到0.5附近控制效果应当和线性控制理论预期一致。如果PPD曲线在0.5附近徘徊说明估计器正确如果完全偏离就要检查公式里的时间索引和信号对齐问题。这个方法五分钟就能做完但能省下后面大量排查时间。8. 从仿真到应用我对MFAC适用边界的理解CFDL-MFAC并不是包打天下的算法它有自己的适用边界。我做过几次将MFAC和其他控制策略对比的仿真实验目前的感觉是对时变非线性系统、强耦合但单输入单输出场景、以及无法建立可靠机理模型的场合MFAC的自适应优势非常明显但对纯时滞明显、系统输入输出方向频繁切换、或者需要极高带宽的快速机械系统MFAC更容易在PPD估计上栽跟头。在这些场合可以考虑改进方案把CFDL换成偏格式动态线性化PFDL或者全格式动态线性化FFDL利用更多历史输入输出信息来提高建模能力或者引入时滞补偿结构把延迟量显式放进参考信号处理中。仿真时也可以对比MFAC与PID、LQR、自抗扰控制的差距这能帮助你判断到底该不该选它而不是听别人说哪个方法好就无脑用。最后分享一个小建议伪偏导数既然叫“伪”就说明它只服务于控制目的不一定要严格等于系统真实偏导数。你在仿真里会看到PPD偶尔波动很大只要控制效果稳定就没必要过度纠结它的绝对精度。控制这件事最终看的是闭环性能而不是辨识模型的好坏。我在做这套仿真时最深的体会是把PPD当成一个“带方向的瞬时效能指标”来看调参的思路会清晰很多。