☰
CFDL-MFAC无模型自适应控制Matlab仿真:伪偏导数与参数调试
2026/10/7 3:44:49 网站建设 项目流程

做控制仿真这些年,我最怕的不是算法本身有多难,而是对象模型给得不靠谱。实验室里推了半天微分方程,凑出一份漂亮的高阶模型,拿到现场打上真实负载,参数一漂移,之前整条链路直接白费。后来我把目光转向无模型自适应控制(MFAC),尤其是基于紧格式动态线性化(CFDL)这条分支,才算找到一套“少依赖模型、多依赖数据”的折腾思路。市面上讲CFDL-MFAC的文章不少,但大多停留在公式层面,真正能把Matlab脚本、Simulink闭环、伪偏导数在线估计的工程细节串起来讲的并不多。这篇文章就把我跑通整套仿真的过程完整拆开:从原理到代码,从调参到踩坑,尽量给到能直接复现的干货。

这篇内容适合三类人:控制方向的研究生想快速验证MFAC算法;做过程控制或机电控制的工程师想评估“无模型”思路是否可用;以及被模型辨识搞到头大、想换条路试试的同行。你不需要有很深的自适应控制基础,但最好会一点Matlab基本操作,能看懂循环和差分方程。

1. 聊几句背景:模型不靠谱的时候,MFAC为什么值得试

先说个很现实的场景。你在Simulink里搭了个高精度模型,PID参数整定得漂漂亮亮,结果对象实际特性和模型差个百分之二三十,控制器性能立刻打折扣。传统自适应控制这时候会跳出来说“我可以在线辨识”,但它有个前提:你得知道模型的结构。结构都不对,辨识出来的参数再准也是白搭。

MFAC的思路完全不同。它不猜模型结构,而是在每个采样时刻,把当前的非线性系统等价成一个带时变增益的线性系统。这个时变增益就是伪偏导数(Pseudo Partial Derivative,PPD)。控制器只做两件事:在线估计这个PPD、然后用它计算下一拍的控制增量。整条链路只用得到系统的输入输出数据,不涉及任何对象机理知识。

这也是MFAC被归到“数据驱动控制”流派的原因。我没有黑传统建模方法的意思——能拿到可靠模型当然最好。但工程现场很多对象本身就是难建模的:化学反应过程、液压系统、大时滞环节,机理复杂不说,特性还随工况漂移。MFAC在这种场景下的优势是“不挑食”:你只要保证输入输出数据是持续激励的,它就能跟着对象的变化走。

在实际(仿真和半实物)使用中,MFAC的代码量非常轻,Matlab脚本能控制在几十行以内,控制器本身不占资源。这意味着它可以很方便地塞进嵌入式环境做在线控制,也能作为Simulink里的一个自定义模块快速验证算法性能。我见过不少同行用MFAC配合现有PID做增益调度,效果都还不错。

需要注意的是,MFAC不是万能药。如果对象本身存在很大的纯滞后,直接用标准SISO形式的CFDL-MFAC往往表现不佳;如果执行器长期饱和,PPD估计也可能被带偏。这些坑后面我会专门讲,但先说结论:MFAC更适合那些“不好建模但激励充分、动态跨度不大”的对象。

2. CFDL和伪偏导数:把非线性系统“压缩”成一个时变增益

2.1 为什么“动态线性化”是数据驱动的地基

先写一个通用离散时间非线性SISO系统:

y(k+1) = f(y(k), y(k-1), …, y(k-ny), u(k), u(k-1), …, u(k-nu))

这个f可以是未知的、非线性的、时变的,无所谓。MFAC只需要两个条件:

  • 系统关于当前控制输入u(k)的偏导连续;
  • 系统满足广义Lipschitz条件,也就是当输入变化有限时,输出变化被一个常数界住。

第二个条件用大白话说:输入动一小点,输出不应该像爆炸一样乱窜。工程上绝大多数开环稳定的对象都具备这个性质。

在这两个条件下,可以证明一个很关键的结论:在每一时刻k,都存在一个有界时变增益φ(k),使得:

Δy(k+1) = φ(k)·Δu(k)

其中Δy(k+1)=y(k+1)-y(k),Δu(k)=u(k)-u(k-1)。这个式子看着简单,但它不是近似,而是“精确等价”。所有被忽略的非线性、模型误差、参数变化,全部被吸收进φ(k)这个时变增益里了。这个φ(k)就是伪偏导数(PPD)。

很多人第一反应是:这不就是把非线性系统“假装”成线性系统吗?其实更准确的说法是:它在每个时刻重新“线性化”一次,并且这个线性化是精确等价的,不是泰勒展开那种局部近似。这就是CFDL“紧格式动态线性化”的地基。

2.2 伪偏导数到底是什么

伪偏导数的“伪”字很容易劝退初学者。我的理解是:它不是对象对输入的偏导,而是一个从输入输出数据中“挤”出来的等价增益,反映的是“在当前工作点附近,输入变化一单位,输出大概变化多少单位”。

比如一个对象在y=0附近增益是2,在y=1附近增益变成0.8,那么系统运行在不同工作点时,PPD估计值自然会跟着变化。这个变化趋势本身就是对象的“性格”。这也是为什么我后面会把PPD曲线当作诊断工具——它不是用来摆设的。

MFAC有两个常见变体:CFDL(紧格式)和PFDL(偏格式)。CFDL只用当前时刻的控制增量Δu(k),形式最简洁,适合SISO、动态不太复杂的对象。PFDL会把过去多个时刻的控制增量Δt u(k), …, Δt u(k-L+1)组合成一个向量,每个分量配一个伪梯度,处理带输入动态或者迟滞的系统更从容,但参数多、调试麻烦。本文只讲CFDL这条主线,如果你想做更复杂的对象,理解透CFDL之后再往PFDL扩会顺很多。

3. 估计器和控制律的推导:从两个准则函数到一套实用公式

3.1 估计器:约束旧估计漂移的投影算法

PPD是个时变参数,没法一步到位算出来,只能在每个采样周期不断修正。思路是构造一个准则函数,兼顾“拟合好最近数据”和“不要偏离旧估计太多”:

J1(φ̂(k)) = |Δy(k) - φ̂(k)·Δu(k-1)|² + μ·|φ̂(k) - φ̂(k-1)|²

第一项表示:如果当前PPD估计φ̂(k)放在上一拍数据上,应该能把Δy(k)解释得不错;第二项表示:新的估计不能和旧估计差太远,μ越大,越不信任新的激进修正。

对φ̂(k)求导并令导数为零,得到:

φ̂(k) = φ̂(k-1) + η·Δu(k-1)·(Δy(k) - φ̂(k-1)·Δu(k-1)) / (μ + Δu(k-1)²)

其中η是估计器步长,通常取0.5左右。这里是标准的投影算法在“伪偏导数在线估计”上的应用。分母μ+Δu(k-1)²有两个作用:防止Δu太小导致除零,同时控制修正幅度。

3.2 控制律:带惩罚项的增量式控制器

控制器同样有自己的准则函数。既要让下一拍输出逼近参考值,又不希望控制量变化太猛:

J2(u(k)) = |y*(k+1) - y(k+1)|² + λ·|u(k) - u(k-1)|²

把动态线性化结果y(k+1)=y(k)+φ̂(k)·Δu(k)代入,对u(k)求导并令导数为零,得到:

u(k) = u(k-1) + ρ·φ̂(k)·(y*(k+1) - y(k)) / (λ + φ̂(k)²)

其中λ是控制增量惩罚因子,ρ是控制律步长。λ越大,控制增量越克制,系统越不容易振荡,但响应会变慢;ρ直接缩放每一步修正力度。

整套算法就是一个“估计-控制-再估计”的闭环:先用旧PPD算控制量,对象输出变化后,再用新数据修正PPD。这就是伪偏导数在线估计的核心节奏。

3.3 重置机制不可省

标准CFDL-MFAC里还有一个重置机制:当|φ̂(k)|小到某个阈值ε,或者|Δu(k-1)|小到某个阈值ε时,把φ̂(k)直接重置回初值。

为什么必须有这一步?因为当输入几乎不变(Δu≈0)时,输出差分Δy和输入差分Δu之间没有有效的信息量,这时的PPD估计是靠不住的,继续让它漂移只会越漂越离谱。重置相当于给估计器“按下复位键”,宁可回到保守初值,也不要在错误的路上一路狂奔。这个机制在工程上是救命级的细节,后面调参章节还会再提。

4. 用Matlab把整套算法跑起来:完整脚本和结果解读

4.1 对象模型的选择

为了体现MFAC“无模型”的适应性,我选了一个带明显非线性的离散系统:

y(k+1) = 0.8·y(k)/(1 + y(k)²) + 1.2·u(k) + 0.2·u(k)³

这个对象的增益随工作点变化明显:当y较大时,0.8·y/(1+y²)这一项趋向于0,相当于系统开环增益在变化;u的三次方项则让输入侧的增益在不同幅值下不一致。用这种对象测试,PPD估计曲线的变化会更明显,也更容易看出算法是否真的在“自适应”。

4.2 完整Matlab脚本

以下脚本我实际跑过,参数也是验证过稳定性的一组组合:

%% CFDL-MFAC SISO非线性系统仿真 % 对象: y(k+1) = 0.8*y(k)/(1+y(k)^2) + 1.2*u(k) + 0.2*u(k)^3 clear; close all; clc; %% 参数设置 eta = 0.5; % PPD估计器步长 mu = 1.0; % PPD估计权重 rho = 0.5; % 控制律步长 lambda = 1.0; % 控制增量惩罚 eps = 1e-4; % 重置阈值 phi0 = 1.0; % 伪偏导数初值 %% 仿真参数 N = 2000; y = zeros(N,1); u = zeros(N,1); phi = zeros(N,1); phi(1) = phi0; y_ref = zeros(N,1); %% 主循环 for k = 1:N-1 % 参考信号:方波,验证设定值切换 if mod(k, 300) < 150 y_ref(k+1) = 1.0; else y_ref(k+1) = -0.5; end % 输入/输出差分 if k == 1 du = 0; dy = 0; else du = u(k) - u(k-1); dy = y(k) - y(k-1); end % 伪偏导数在线估计 phi(k) = phi(k-1) + eta * du * (dy - phi(k-1)*du) / (mu + du^2); % 重置机制 if abs(phi(k)) <= eps || abs(du) <= eps phi(k) = phi0; end % CFDL-MFAC控制律 u(k+1) = u(k) + rho * phi(k) * (y_ref(k+1) - y(k)) / (lambda + phi(k)^2); % 被控对象(非线性差分方程) y(k+1) = 0.8*y(k)/(1 + y(k)^2) + 1.2*u(k+1) + 0.2*u(k+1)^3; end %% 画图 figure; subplot(3,1,1); plot(1:N, y_ref, '--', 1:N, y, 'linewidth', 1.2); legend('参考','输出'); ylabel('y'); title('CFDL-MFAC 输出跟踪'); subplot(3,1,2); plot(1:N, u, 'linewidth', 1.0); ylabel('u'); title('控制输入'); subplot(3,1,3); plot(1:N, phi, 'linewidth', 1.0); ylabel('PPD'); title('伪偏导数估计'); xlabel('k');

注意几点:

  • 参考信号用了周期300步的方波,1000步内能看到好几次切换,便于观察PPD在设定值切换后的重新收敛过程。
  • 第一次进入循环时du和dy都取0,这是为了保住算法“没有历史数据时不做激进修正”的稳妥性。
  • 控制律里y_ref(k+1)用第k+1拍的目标,y(k)用当前输出,符合MFAC“一步前向预测”的基本设定。

4.3 初次运行最可能的三个结果

跑完脚本,你大概率会看到这样的现象:

第一,输出跟踪基本能跟上参考值,但切换瞬间会有超调。以我给的参数,切换到1.0时超调大约5%~10%,切换到-0.5时反向超调略小。这说明参数组合偏保守、但有效。

第二,PPD曲线不是一条直线,它会在参考信号切换后有一段明显波动,随后收敛到一个小区间。以这个对象为例,PPD在y=1附近会稳定在2.4~3.0左右,在y=-0.5附近会掉到更低的区间——这直接反映了对象在不同工作点增益的差异。

第三,控制量u在设定值切换瞬间会出现一个峰值,然后迅速回落。峰值越大,切换越快,但超调和控制量磨损也越大。后面调参时你会发现,lambda就是那个控制“切换时冲多猛”的旋钮。

5. 转战Simulink:MATLAB Function模块封装控制器的完整拓扑

5.1 Simulink顶层结构设计

Matlab脚本适合验证算法,但做闭环结构展示、或者后续要加入执行器模型、扰动、量测噪声时,Simulink会更直观。我的建议拓扑是这样:

  • 参考信号源:用Signal Generator或者Staircase Generator产生方波;
  • 控制器:用MATLAB Function模块封装CFDL-MFAC,输入是参考值和当前输出,输出是控制量u;
  • 被控对象:用Level-2 M S-Function写非线性差分方程,输入u,输出y;
  • 记录与显示:从被控对象输出引一路到Scope,同时连回控制器的反馈输入,再用To Workspace把y、u、PPD存下来供后续分析。

这个结构里最关键的是控制器内部的persistent变量。因为PPD估计有记忆,控制器本质上是一个带离散状态的动态系统,不能在函数外部另存状态。

5.2 控制器内部的persistent状态管理

MATLAB Function模块里这样写:

function u = CFDLMFAC(y_ref, y) % CFDL-MFAC 离散控制器 persistent phi uPrev uPrev2 yPrev if isempty(phi) phi = 1.0; % 伪偏导数初值 uPrev = 0.0; uPrev2 = 0.0; yPrev = 0.0; end eta = 0.5; mu = 1.0; rho = 0.5; lambda = 1.0; eps = 1e-4; du = uPrev - uPrev2; dy = y - yPrev; % PPD在线估计 phi = phi + eta * du * (dy - phi * du) / (mu + du^2); % PPD重置 if abs(phi) <= eps || abs(du) <= eps phi = 1.0; end % 控制律 u = uPrev + rho * phi * (y_ref - y) / (lambda + phi^2); % 更新记忆状态 uPrev2 = uPrev; uPrev = u; yPrev = y; end

第一次运行时du和dy都是0,PPD会先被重置到1,然后控制器用y_ref-y的偏差乘以rho/(lambda+1)算出第一个控制量。这个起步过程是可靠的。

需要注意,persistent变量的更新时机和Simulink求解器密切相关。如果模型里既有连续模块又有这个离散控制器,建议把求解器设为固定步长,并且给控制器一个明确的离散采样时间,否则persistent变量可能在一个仿真步内被调用多次,导致状态更新错乱。我见过有人用变步长ode45跑,结果输出像锯齿一样,其实就是求解器步进和离散状态更新不同步造成的。

5.3 被控对象用Level-2 M S-Function封装

被控对象我推荐用Level-2 M S-Function,因为它能把“离散状态”和“输出”分离清楚。直接复用前面脚本里的非线性差分方程:

function MFACPlant(block) setup(block); end function setup(block) block.NumInputPorts = 1; block.NumOutputPorts = 1; block.InputPort(1).Dimensions = 1; block.InputPort(1).DirectFeedthrough = true; block.OutputPort(1).Dimensions = 1; block.NumDworks = 1; block.Dwork(1).Name = 'y'; block.Dwork(1).Dimensions = 1; block.Dwork(1).DatatypeID = 0; % double block.Dwork(1).Complexity = 'Real'; block.SampleTimes = [0.1 0]; % 采样周期0.1s block.RegBlockMethod('Outputs', @Outputs); block.RegBlockMethod('Update', @Update); block.RegBlockMethod('InitializeConditions', @InitConditions); end function InitConditions(block) block.Dwork(1).Data = 0; end function Outputs(block) % 当前采样时刻输出上一拍存储的y(k) block.OutputPort(1).Data = block.Dwork(1).Data; end function Update(block) % 下一拍的y(k+1) y = block.Dwork(1).Data; u = block.InputPort(1).Data; y_next = 0.8*y/(1+y^2) + 1.2*u + 0.2*u^3; block.Dwork(1).Data = y_next; end

这个模板把“输出y(k)”和“内部状态更新到y(k+1)”分成两个回调,结构清晰。Solver配置里选Fixed-step、discrete,步长和模块采样时间保持一致即可。

6. 参数调试顺序和经验:四个参数各管什么,先动哪个

6.1 每个参数的工程含义

先把四个参数说清楚,再用表格速查:

参数所在环节作用调大效果调小效果
ηPPD估计单步学习率收敛快,易波动收敛慢,更平稳
μPPD估计旧估计漂移惩罚估计更平滑估计更敏感
ρ控制律控制修正步长响应更猛响应更保守
λ控制律控制增量惩罚控制量更克制控制量更激进

注意η和μ都在管PPD估计,但机制不同:η决定“单步跳多远”,μ决定“跳远了要付出多大代价”。如果PPD曲线乱跳,我一般先加μ而不是减η,因为减η会让系统变得反应迟缓,而加μ对跟踪速度的副作用相对小一些。

6.2 我的调试顺序

我惯用的流程是这样:

  1. 固定一个保守基准:η=0.5,μ=1,ρ=0.5,λ=1。先保证系统能稳定跟踪一个慢变参考信号。
  2. 看PPD曲线:如果PPD抖得像锯齿,先把μ加到大2~5;如果PPD长时间偏离合理区间,检查重置机制有没有被频繁触发。
  3. 调跟踪速度:在PPD稳定的前提下,把λ从1慢慢减到0.3左右,响应会明显变快。一旦出现振荡,回调λ或者把ρ降到0.3。
  4. 最后调动态切换表现:把ρ当作微调旋钮,每次改0.1,观察超调量变化。

这个顺序的核心逻辑是:先把“学习环节”(PPD估计)稳住,再动“控制环节”(ρ、λ)。很多人一上来就同时改四个参数,出问题根本分不清是哪一步导致的——这属于仿真调试里的经典自坑行为。

6.3 常见异常速查表

现象大概率原因推荐动作
输出发散ρ过大或λ过小减ρ到0.2再试,或λ加到2
输出持续振荡PPD估计波动传导到控制量增大μ、减小η
输出平滑但跟踪慢λ太大或ρ太小减λ、微增ρ
PPD曲线频繁重置激励不足或eps阈值太大调小eps,或让参考信号更活跃
控制量切换瞬间过冲λ太小加λ

7. 从PPD曲线判断系统状态:一次典型仿真的逐帧复盘

7.1 设置一个可重复的基准场景

我把参考信号定为周期300步的方波,幅值1和-0.5,仿真长度2000步。参数取η=0.5、μ=1、ρ=0.5、λ=1。这个设置的好处是:你能同时看到上升段、反向切换段和长时间稳态段,PPD在三个阶段的形态完全不一样。

7.2 切换瞬间到底发生了什么

第一次切换到1.0时,输出从0往上爬,此时PPD会在初期快速变化。因为对象在y=0附近增益较高,但PPD初值只有1,控制律一开始相对保守,输出爬升中段会明显加速——这是PPD学习到真实增益后的自然反应。到第120步左右,输出进入稳态,PPD也收敛到2.5附近。

切换到-0.5时更有意思:对象在这个工作点附近增益明显不同,PPD会在切换后的十几个采样周期内快速下探,然后稳定在1.8~2.2之间。如果看到PPD在切换后长期漂移不收敛,说明估计器步长η偏大或μ偏小,估计过程“学过头”了。

另外要盯着控制输入看。MFAC的u在切换瞬间一定会有峰值,峰值大小反映了λ/ρ的搭配。如果峰值过大,说明λ太小;如果峰值平滑但到达时间太慢,说明ρ偏小。把y、u、PPD三条曲线放在一起看,基本能把系统状态看透。

8. 容易翻车的细节和可以进阶的方向

8.1 激励不足与PPD漂移

MFAC最典型的坑就是参考信号长时间不变。当设定值恒定、输出稳定时,Δu会越来越小,PPD估计缺乏有效激励,只能在旧值附近打转。一旦此时来一个扰动,PPD估计就可能飞掉。前面说的重置机制能兜底,但兜不住“频繁重置”本身——如果eps设得太大,系统会陷入周期性重置-学习-再重置的循环,输出会出现锯齿状抖动。我自己的经验是eps取1e-4或1e-5比较稳,别为了“省事”设到1e-2。

如果你想让MFAC在长时间稳态工况下也保持警惕,可以给PPD估计加一个遗忘因子,让旧数据慢慢“过期”,这样即使Δu很小,估计器也会温和地向某个稳态值收敛。但这会牺牲一部分抗噪性,需要根据具体对象平衡。

8.2 饱和、噪声、初值等细节坑

执行器饱和是个很容易被忽视的问题。MFAC控制律计算出的u如果被Saturation模块限幅,控制器内部的uPrev还在按未饱和的值更新,那么实际对象和控制器之间就会出现“认知错位”。恢复时往往伴随一个反向修正的冲击。解决办法有两种:一是把控制器内部的uPrev改为实际限幅后的值;二是在控制律里加入简单的积分抗饱和逻辑。

量测噪声对PPD估计的影响也不小。输出差分Δy一旦含有高频噪声,PPD估计会跟着抖。可以先把估计器里的(Δy(k) - φ̂(k-1)Δu(k-1))项做一个死区,误差小于阈值时不更新。这和8.1里的遗忘修正是两个互补方向:一个抗噪声,一个抗漂移。

PPD初值的选择同样会影响起步表现。对象增益未知时φ0=1是个不错的默认值;如果你知道对象在某些工作点增益约2,那φ0取2能明显加快第一次上升段的收敛速度。但初值不能取得太离谱,否则起步控制量过大会引出大超调。

8.3 进阶方向

如果你做完这套仿真还有余力,可以往三个方向扩:

第一,把固定μ改为时变遗忘因子,让PPD估计器在参考信号切换时能更快跟上,在稳态时又足够稳;

第二,尝试PFDL动态线性化,把Δu的过去若干拍一起纳入估计,处理带输入动态的对象;

第三,往MIMO方向走,把伪偏导数的概念推广到伪雅可比矩阵(PJM),不过参数数量和调试复杂度会明显上升。

我个人的实际体会是,MFAC这套东西调试的“手感”和传统PID差别挺大:PID调的是“误差怎么响应”,MFAC调的是“学习过程怎么收敛”。你上手之后不要急着追求参数最优,先把PPD曲线在不同工况下的形态看熟,再动手调η、μ、ρ、λ,会顺很多。这套仿真光看文章觉得抽象,跑完一遍再回头看公式,感觉会完全不一样。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询