最近在复现侯忠生老师提出的MFAC(无模型自适应控制)经典仿真,核心是把CFDL、PFDL、FFDL三种动态线性化方法分别作用在三个不同的非线性系统上,用Matlab完整跑通轨迹跟踪实验。这套代码的价值在于:它把MFAC从论文里的抽象公式变成了能亲眼看到的仿真曲线,能直观感受到“不用被控对象数学模型也能完成控制器设计”这件事到底是怎么发生的。如果你正在学自适应控制、数据驱动控制,或者需要完成一个关于无模型控制的课程项目,又或者只是对“非线性系统如何在线动态线性化”感兴趣,这篇笔记应该能帮你节省大量找代码、调参数、填坑的时间。
1. 复现前必须搞懂的MFAC底层层逻辑
1.1 为什么“无模型”反而能控制:从输入输出数据说事
传统的自适应控制,哪怕说得再天花乱坠,第一步通常还是要建立被控对象的数学模型:要么是传递函数,要么是状态空间方程。复杂非线性系统想建立准确的机理模型非常困难,就算建出来了,模型误差和时变特性也足够让控制器设计变得很痛苦。
MFAC的思路完全不一样。它不尝试去描述整个系统的全局机理,而是利用系统的实时输入输出数据,在每一个工作时刻对系统做一个“局部动态线性化”。这个思想可以类比成你开车时不看整张地图,而是只看眼前这段路的车道线:不需要知道下一个路口怎么拐,只要当前路段的方向有偏差,就实时修正方向盘。MFAC正是用过去和当前的输入输出数据,在每一个采样时刻构造一个等价的线性数据模型,然后在这个数据模型上完成控制器设计。
这个项目标题里提到的“CFDL+PFDL+FFDL”,就是三种构造局部线性数据模型的具体方式。三种方法的核心区别在于:动态线性化到底利用了哪些历史数据,以及把哪些信息当作在线估计的对象。
1.2 三种动态线性化方法的数学形式与适用逻辑
先说CFDL(紧格式动态线性化)。它的假设非常紧凑:系统在当前时刻的输出变化量,近似等于某个时变系数乘上当前时刻的输入增量,数学上写成:
Δy(k+1) = φ(k)·Δu(k)
其中的φ(k)就是伪偏导数(PPD,Pseudo Partial Derivative)。你可以把它理解为系统在k时刻附近的等效增益:输入增加一点点,输出大概增加多少。CFDL最大的优点就是结构最简单,只有一个待估参数,估计压力小,计算量低。但它隐含的假设是:当前时刻输入的变化对下一时刻输出的影响是主导因素。如果系统的真实动态还明显依赖更早时刻的输入,CFDL就会显得力不从心。
PFDL(偏格式动态线性化)在CFDL的基础上,把过去L个时刻的输入增量都考虑进来,变成:
Δy(k+1) = Φ(k)·ΔU_L(k)
其中ΔU_L(k)是由Δu(k)、Δu(k-1)一直到Δu(k-L+1)堆成的列向量,Φ(k)是一个对应长度的行向量,叫做伪梯度(PG)。这个方法的好处是:当系统存在明显的输入延迟、或者输出对历史输入有依赖时,PFDL能把这一段“历史效应”显式建模,从而比CFDL更接近真实动态。代价是待估参数从1个变成了L个,估计难度和参数整定复杂度随之增加。
FFDL(全格式动态线性化)则是更彻底的做法:它同时把历史输入增量和历史输出增量都放进线性化模型里,写成:
Δy(k+1) = Φ(k)·ΔH(k)
其中ΔH(k)同时包含过去L_u个时刻的输入增量和过去L_y个时刻的输出增量,Φ(k)对应地叫作广义伪梯度(GPJ)。FFDL的描述能力最强,几乎可以覆盖大多数可线性化的非线性系统场景,尤其适合那种自身动态惯性大、输入输出耦合关系复杂的系统。但它的参数数量也是最多的,初值选择、重置机制、步长设置稍微不合适,就容易出现估计发散。
1.3 伪偏导数估计与控制律设计的联动关系
我需要特别强调一点:MFAC不是一个单独的控制器,而是“在线参数估计器+控制律计算器”的组合。每一拍循环中,先用最近的输入输出数据更新伪偏导数/伪梯度估计值,然后用这个估计值计算控制律,再把这个控制量施加给系统,最后用新的输出数据再次更新估计值。周而复始,这就是整个算法的运转闭环。
其中伪偏导数的估计通常采用改进投影算法。它的目标函数是:让估计值对应的模型预测误差尽量小,同时又要限制估计值自身的变化幅度,避免一步跳变太大。所以估计律里会出现一个步长因子η,以及一个分母上的常数项,这个常数项的本质作用是防止除零,我习惯叫它“防奇异微调量”。控制律的推导同样是一个带惩罚的最小化问题:既要让系统输出尽快逼近参考轨迹,又要避免控制输入变化过于剧烈。于是控制律里会出现惩罚因子λ,它的作用就是给控制增量的“幅值”设置一个软约束。
2. 三个非线性系统的选取与仿真设计思路
2.1 系统一:适合CFDL的基础非线性系统
复现项目里三个系统不是随便选的,它们对应着三种动态线性化方法的“主场”。第一个系统我选的是一个常见的基础非线性系统:
y(k+1) = y(k) / (1 + y(k)²) + u(k)³
这个系统的输出由上一拍的输出和当前拍输入共同决定,输入u(k)直接、无延迟地影响输出y(k+1),非线性主要体现在输出自反馈项和输入的三次方项上。这种结构下,CFDL的“当前输入增量主导输出变化”假设基本成立,所以用CFDL就能得到不错的跟踪效果,伪偏导数估计值也会比较平稳。
选这个系统作为第一个,还有一个重要考虑:它好调参,不容易把新手劝退。参考轨迹可以用幅值0.5、频率约0.05Hz的正弦波。这个系统上,CFDL的跟踪曲线基本能贴着参考轨迹走,很适合用来验证代码框架是不是写对了。
2.2 系统二:检验PFDL的强非线性与时滞系统
第二个系统我刻意加入了对历史输入的依赖,用来突出PFDL的优势。比如:
y(k+1) = 0.6·y(k) + (1 + 0.3·sin(y(k)))·u(k) + 0.2·u(k-1)
这里u(k-1)的作用是模拟系统的输入记忆特性:当前拍的控制输入不会立即全部反映到输出上,有一小部分要等到下一拍才体现出来。这种场景下,CFDL因为只考虑Δu(k),会把“上一拍输入带来的输出变化”误判到“当前拍输入的增益”上,导致伪偏导数估计值波动变大,跟踪性能明显变差。而PFDL只要把线性化长度L取为2或3,这0.2·u(k-1)的贡献就能被对应的伪梯度分量捕捉到,控制效果会有本质改善。
我在复现时把PFDL的L设为3,多出来的一个输入历史分量其实是在给模型“留余量”,让估计算法有更多自由去调整自身参数。
2.3 系统三:逼出FFDL的高阶耦合系统
第三个系统的设计目标是让输入输出两边都有历史依赖,而且存在交叉耦合,这样CFDL和PFDL都会捉襟见肘,FFDL的优势才会真正体现。我用的形式是:
y(k+1) = 0.8·y(k)·sin(y(k-1)) + 0.5·u(k) + 0.3·u(k-1)·y(k)
这里面y(k-1)的引入意味着输出动态本身存在滞后,u(k-1)·y(k)这种乘性耦合又让输入增益随着历史输出水平变化。这种系统已经不是简单的“输入延迟”或“输出自反馈”能概括的了,需要同时利用输入增量和输出增量的历史信息。FFDL把过去L_u个输入增量和L_y个输出增量同时作为线性化模型的输入特征,恰好能覆盖这种耦合动态。
我用FFDL时设置L_u=3、L_y=2,也就是线性化向量由3个输入增量分量和2个输出增量分量组成,一共5个待估计参数。参数多了,每次更新的信息量要求更高,所以仿真步长和参考轨迹变化率都不能太激进,否则GPJ估计会跟不上系统动态。
3. Matlab代码实现与关键模块拆解
3.1 工程文件结构怎么组织
复现时我建议不要把所有逻辑塞进一个脚本里。Matlab项目最忌讳的就是单个main.m写几百行,后面想改参数只能满屏找。我用的结构是这样的:
- main_run.m:总入口,负责参数初始化、循环调用、结果绘图
- plant_model.m:三个非线性系统的被控对象函数,输入是k时刻状态和控制量,输出是k+1时刻输出
- controller_cfdl.m / controller_pfdl.m / controller_ffdl.m:三个控制器模块,每个模块内部包含伪偏导数(或伪梯度)估计与控制律计算
- reset_ppd.m:伪偏导数/伪梯度重置函数
- plot_results.m:绘图模块,统一画跟踪曲线、控制量曲线、参数估计曲线
这种模块化组织方式最大的好处是:想改某一个非线性系统,只需要动plant_model.m;想对比不同参数对同一系统的影响,只需要改main_run.m里的参数区。代码量不大,但结构清晰,调试起来定位问题很快。
3.2 核心控制与估计逻辑的代码级拆解
以CFDL为例,完整的一拍循环逻辑大致如下:
%% 初始化 N = 2000; % 仿真步数 y = zeros(N,1); % 系统输出 u = zeros(N,1); % 控制输入 phi_hat = 2; % 伪偏导数初值,常见取1~10 eta = 0.5; % 估计步长因子 mu = 1; % 防奇异微调量 lambda = 0.5; % 控制律惩罚因子 y_ref = 0.5*sin(0.1*pi*(1:N)); % 参考轨迹 %% 主循环 for k = 1:N-1 % 1. 被控对象输出(实际系统中这里是真实传感器采集值) y(k+1) = plant_model_1(y(k), u(k)); % 2. 伪偏导数在线估计 delta_u = u(k) - u(max(k-1,1)); delta_y = y(k) - y(max(k-1,1)); phi_hat_next = phi_hat + ... eta * delta_u / (mu + delta_u^2) * (delta_y - phi_hat * delta_u); % 3. 重置机制:防止伪偏导数符号突变或过小 if abs(phi_hat_next) < 1e-4 || sign(phi_hat_next) ~= sign(phi_hat) phi_hat_next = 2; end phi_hat = phi_hat_next; % 4. 控制律计算 u(k+1) = u(k) + ... (y_ref(k+1) - y(k)) * phi_hat / (lambda + phi_hat^2); end这段代码里,第2步的估计公式就是改进投影算法,它同时完成了“拟合历史数据”和“限制估计跳变”两个任务。第3步的重置机制是MFAC工程化落地时特别关键的一环,没有它,伪偏导数一旦被估计成负值或趋近于零,控制量就会畸变,整个闭环直接发散。第4步的控制律一眼就能看出来:当参考轨迹高于当前输出时,如果等效增益为正,就增加控制输入;增益越大,分母的λ+φ²越大,控制增量被自然限制住。
PFDL和FFDL的代码结构完全一样,差别只在“状态向量”的构造方式。PFDL需要维护一个历史输入增量向量delta_U_L,FFDL还需要额外维护历史输出增量向量。控制律部分不再是简单的标量乘除,而是变成了向量内积和矩阵运算,原理上依然是“最小化跟踪误差+最小化控制增量”的同一个优化问题。
3.3 参数整定经验值参照
参数整定是复现过程中最耗时的环节之一,我直接给出我实测下来比较稳的一组初始参数:
| 方法 | 估计步长η | 惩罚因子λ | 防奇异μ | 线性化长度 | 伪偏导初值 |
|---|---|---|---|---|---|
| CFDL | 0.5 | 0.5 | 1 | 无 | 2 |
| PFDL | 0.3 | 0.3 | 1 | L=3 | 分量全取1 |
| FFDL | 0.2 | 0.2 | 1 | L_u=3, L_y=2 | 分量全取1 |
需要强调的是,这组参数只是起点,不是标准答案。仿真时如果发现系统输出围绕参考轨迹高频抖动,优先加大λ;如果发现跟踪响应太慢、跟不上参考轨迹变化,优先加大η;如果伪偏导数估计值频繁触发重置,优先检查初值是否取得太小,或者是否该增大μ。
提示:三个方法各跑一个系统的时候,参数不用完全统一,因为“无模型自适应控制”不等于“一套参数走天下”。真正有意义的是看每个方法在自己的适配场景下能表现出多好的控制性能,以及在不适配的场景下暴露什么问题。
4. 仿真结果看什么:对比指标与调参方向
4.1 跟踪曲线里藏着哪些关键信息
跑完三个系统之后,第一步不是急着看误差数值,而是先看输出跟踪曲线和控制量曲线的基本形态。正常收敛的情况下,输出曲线应该经过一个短暂的暂态过程后贴住参考轨迹,控制量曲线则保持连续、无明显尖峰。我复现时把三条曲线画在同一张画布里,用subplot布局:上一排是三个系统的输出跟踪对比,下一排是对应的控制输入变化。
这里有一个很容易被忽略的细节:伪偏导数估计曲线的形态。CFDL在系统二上跑出来的PPD估计曲线往往会呈现出不规则的波动,这就是它试图用单一标量去解释“历史输入+时变增益”动态,导致等效增益估计被来回拉扯。而在系统一上,PPD估计曲线则会很快收敛到一个较窄的区间内,说明这个“局部等效增益”确实存在且稳定。
PFDL和FFDL由于参数数量多,我更倾向于看控制能量和误差指标,而不逐一分析每个伪梯度分量,因为向量里的分量之间存在协商分配关系,单个分量的绝对值参考意义有限。
4.2 用RMSE和控制能量来横向比较
三个方法分别在三个系统上跑完后,可以汇总成一个简单的表格,直观展示“哪种线性化方法更适合哪种系统”:
| 系统 | CFDL的RMSE | PFDL的RMSE | FFDL的RMSE |
|---|---|---|---|
| 系统一(简单非线性) | 较小 | 接近 | 接近 |
| 系统二(输入时滞) | 明显偏大 | 较小 | 较小 |
| 系统三(输入输出耦合) | 发散或很大 | 偏大 | 较小 |
RMSE代表跟踪精度,控制能量则看控制量的平方累加。我的复现结果显示,系统一上用CFDL,计算量最小,控制能量也最低;系统二上PFDL显著优于CFDL;系统三上FFDL是唯一能稳定收敛的,代价是控制能量略高。这正好印证了理论结论:线性化格式的复杂度必须和系统动态的复杂度匹配,不是方法越复杂就一定越好。
注意:如果你发现某个系统上三种方法全都发散,绝大多数情况下不是MFAC框架本身的问题,而是参数、初值、重置机制三件事至少有一件没有做好。先把控制目标换成单位阶跃信号,排除参考轨迹变化太快的影响,再逐步恢复为正弦跟踪。
5. 复现过程中遇到的高频问题与排查思路
5.1 伪偏导数估计值突变或发散怎么办
这是复现MFAC时遇到最多的问题。典型场景是:前面几百拍还好好的,突然PPD从2跳到几百,然后控制量开始乱飞。我的排查顺序是:先看是不是系统输出出现了剧烈的数值变化,再看估计步长η是不是超过0.5,最后检查重置条件是否触发过。多数情况下,发散都源于估计律的分母太小,也就是μ + Δu²里μ取太小。
这里有一个实践经验:μ取1,η取0.3到0.5,是相对安全的区间。如果你需要处理的是变化特别快的系统,可以尝试把μ降到0.1,把η同时降到0.1,给估计器“踩刹车”的余地。另外,重置条件不要只判断绝对值小于阈值,还要判断符号突变:如果上一拍PPD是正数、这一拍突然变负,即使绝对值不小,也应该强制重置,因为符号突变往往意味着估计已经失真。
5.2 控制量高频震荡怎么定位原因
控制量震荡通常有两类来源。一类是λ太小,导致控制律对跟踪误差过于“敏感”,误差的微小波动被放大成控制输入的大幅变化。解决办法是按0.1的步长逐步加大λ,直到控制量曲线变得平滑。另一类是参考轨迹本身变化过快,比如正弦信号频率太高或幅值跳变太大,这时候即使加大λ也不一定能完全根治,更推荐给参考轨迹加一个速率限制或过渡滤波。
我在复现系统三时还遇到过一个特殊情况:FFDL的控制量在某个区间出现规律性脉冲,排查了半天,最后发现是线性化向量ΔH里的输出增量分量与输入增量分量数值尺度差异过大,导致GPJ估计在数值上“偏爱”某个分量。解决办法是对输入输出增量分别做归一化处理,或者调整L_u和L_y的配比。
5.3 三个系统用同一套参数跑结果差异巨大正常吗
完全正常,而且这正是这个复现项目的教学价值所在。CFDL设计的初衷就是处理“主要动态可由当前输入增量主导”的系统,PFDL针对“输入历史依赖明显”的系统,FFDL面向“输入输出交叉耦合”的复杂系统。强行让CFDL去跑系统三,本质上是在线性化模型里遗漏了重要信息,发散或者性能变差是理论上的必然,不是代码Bug。
反过来,FFDL虽然理论描述能力最强,但在简单系统上反而可能因为参数过多而出现估计噪声放大、控制量抖动的现象。所以复现完成后,建议做一个交叉实验:把三种方法在三个系统上都跑一遍,把9组结果全部记录下来。这个交叉结果表的价值远远大于“每个方法各跑一个成功案例”,它能让你真正看清每种线性化方法适用边界在哪里。
最后说点个人体会
这个复现项目给我最大的启发是:数据驱动控制并不神秘,本质上是“在线数据建模+在线控制器设计”的强强联合。MFAC选择了最朴素、最直接的建模路径——用当前和历史输入输出数据构造局部线性模型,再用带惩罚的最小化问题设计控制律。这种思路的优势是工程实现简单、对计算资源要求低、不需要对象任何先验知识,非常适合数字控制平台落地。
我建议看完这篇笔记后,先不要急着同时跑三个系统,而是从CFDL加系统一这条路走通流程:确认代码循环逻辑无误后,再逐步加PFDL、FFDL。调参时每次只改一个参数,记录下曲线变化,你的手感会很快建立起来。后面如果想继续深入,还可以把这个方法延伸到时滞系统、多输入多输出系统,甚至把估计器从投影算法换成带遗忘因子的递推最小二乘,对比不同估计器的收敛速度与抗噪能力,这些都是非常值得玩的方向。