拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

MFAC无模型自适应控制三种动态线性化方法Matlab复现与对比

MFAC无模型自适应控制三种动态线性化方法Matlab复现与对比

做控制方向的朋友,应该多少听说过MFAC。我这阵子刚好在复现一篇关于无模型自适应控制(Model Free Adaptive Control)的论文,把CFDL、PFDL、FFDL三种动态线性化方案放在同一套非线性系统里跑了一遍,用的是Matlab。这篇文章就把整个复现过程、原理理解、参数设置和踩过的坑原原本本讲一遍,适合正在入门MFAC、或者想快速跑通仿真验证算法的研究生和工程师。MFAC这东西最大的价值在于不需要建立被控对象的精确数学模型,直接靠输入输出数据在线估计一个叫“伪偏导数”的量,然后把非线性系统在当前时刻改造成线性数据模型。CFDL、PFDL、FFDL就是三种改造方式,区别主要在“用到多少历史的输入输出信息”。

我最初接触MFAC时也困惑:明明是非线性系统,凭什么能说“动态线性化”?后来跑通仿真才明白,它不是把系统全局线性化,而是在每一个工作点上,用一个含伪偏导数的线性化模型去局部逼近系统动态,再配合在线估计和控制律设计,让闭环系统在扰动和不确定性下依然能跟踪期望轨迹。这篇文章不讲虚的,直接给你可复现的Matlab思路,附带参数选择的经验。

1. 先搞清楚MFAC到底解决什么问题

1.1 无模型控制的核心逻辑

传统自适应控制需要一个显式的数学模型,但实际很多被控对象要么机理复杂,要么参数时变,建模成本极高。MFAC的思路很直接:我不建模,我只用被控系统的输入输出数据,在线估计一个等价的动态线性关系,再基于这个关系设计控制器。你可以把伪偏导数理解成一个“时变的、数据驱动的局部增益”,它把系统过去时刻的输入输出增量映射到当前输出的增量上。

具体到离散非线性系统,一般写成:

[ y(k+1) = f(y(k), y(k-1), \dots, u(k), u(k-1), \dots) ]

其中 ( f ) 未知。MFAC的核心条件是对该系统施加一个“广义Lipschitz”假设:当输入增量 ( \Delta u(k) ) 有界时,输出增量 ( \Delta y(k+1) ) 也有界。在这个假设下,可以证明存在一个时变参数(伪偏导数) ( \phi(k) ),使得:

[ \Delta y(k+1) = \phi(k) \Delta u(k) ]

这就是所谓“动态线性化”。它把未知非线性关系,在当前时刻转换为一个简单的线性增量关系。( \phi(k) ) 不需要知道 ( f ) 的表达式,只需要通过输入输出数据在线估计。

这个逻辑的好处是控制器设计变得极其简单:用估计的 ( \hat{\phi}(k) ) 设计控制律,直接让输出 ( y(k+1) ) 逼近期望值 ( y^*(k+1) )。整个闭环里没有模型参数,只有估计器和控制器两个部分。我在复现时最大的感受是:MFAC很“轻”,代码量不大,但每个参数都会影响收敛速度和稳定性。

1.2 CFDL、PFDL、FFDL三兄弟的区别

CFDL、PFDL、FFDL都来自同一个框架,区别在于数据模型包含的输入输出信息量:

  • CFDL(紧格式动态线性化):只使用当前时刻的输入增量 ( \Delta u(k) ),伪偏导数是一个标量。最简单,适合输入输出关系相对平滑、动态不太复杂的系统。
  • PFDL(偏格式动态线性化):使用过去 ( L_u ) 个输入增量 ( \Delta u(k), \Delta u(k-1), \dots, \Delta u(k-L_u+1) ),伪偏导数变成一个向量 ( \phi(k) )。适合输入历史信息对输出影响明显的系统。
  • FFDL(全格式动态线性化):同时使用过去 ( L_y ) 个输出增量和 ( L_u ) 个输入增量,伪偏导数由两部分组成:( \phi_{y}(k) ) 和 ( \phi_{u}(k) )。信息最全,但需要估计的参数也最多,计算量和调参难度都上升。

简单类比:CFDL像是你用“当前的一步油门变化”预测车速变化;PFDL则考虑“最近几步油门变化”的共同影响;FFDL还把“之前几点车速变化”也纳入考量。信息越多,对复杂动态的刻画能力越强,但参数越多,估计越容易过拟合或发散。实际复现时建议从CFDL跑通,再逐步升级到PFDL、FFDL,不要一上来就挑战最复杂的。

2. 动态线性化方法的数学本质与实现要点

2.1 CFDL:只有一个伪偏导数

CFDL的算法流程很经典。先是伪偏导数估计:

[ \hat{\phi}(k) = \hat{\phi}(k-1) + \frac{\eta \Delta u(k-1)}{\mu + \Delta u(k-1)^2} \left( \Delta y(k) - \hat{\phi}(k-1) \Delta u(k-1) \right) ]

其中 ( \eta ) 是估计步长,( \mu ) 是惩罚因子(防止分母为零)。然后用一个重置机制避免伪偏导数退化:

[ \text{如果 } |\hat{\phi}(k)| \le \varepsilon \text{ 或 } |\Delta u(k-1)| \le \varepsilon,\text{则 } \hat{\phi}(k) = \hat{\phi}(k-1) ]

控制律采用如下形式:

[ u(k) = u(k-1) + \frac{\rho \hat{\phi}(k) \left( y^*(k+1) - y(k) \right)}{\lambda + |\hat{\phi}(k)|^2} ]

其中 ( \rho ) 是控制步长因子,( \lambda ) 是控制量惩罚因子。这里 ( \lambda ) 的作用很关键:当 ( \hat{\phi}(k) ) 很大或接近零时,( \lambda ) 可以限制控制增量,避免控制量剧烈变化。

实现时我建议把“重置条件”写得严格一点,否则仿真前期特别容易因为伪偏导数估计过小导致控制量爆炸。一个常见经验是:( \mu ) 在 ( 0.01 \sim 1 ) 之间,( \eta ) 在 ( 0.5 \sim 2 ) 之间,( \rho ) 在 ( 0.1 \sim 1 ) 之间,( \lambda ) 在 ( 0.5 \sim 10 ) 之间。具体数值要结合系统响应调整,没有通吃所有系统的固定值。

2.2 PFDL:让输入信息窗口更长

PFDL把单个输入增量扩展成一个窗口向量:

[ \Delta U_L(k) = [\Delta u(k), \Delta u(k-1), \dots, \Delta u(k-L+1)]^T ]

动态线性化模型变成:

[ \Delta y(k+1) = \phi(k)^T \Delta U_L(k) ]

对应的伪偏导数估计:

[ \hat{\phi}(k) = \hat{\phi}(k-1) + \frac{\eta \Delta U_L(k-1) \left( \Delta y(k) - \Delta U_L(k-1)^T \hat{\phi}(k-1) \right)}{\mu + |\Delta U_L(k-1)|^2} ]

控制律则基于这个向量模型推导,最终控制量增量由多个历史输入增量加权得到。窗口长度 ( L ) 是PFDL最重要的超参数。我测试下来,( L=2 ) 或 ( L=3 ) 效果通常不错,再大不仅计算量上升,而且容易出现参数耦合导致估计震荡。

PFDL适合的系统特征是:当前输出变化不仅跟最近一次控制增量有关,还跟更早的控制输入有关,典型就是带输入滞后或者高阶动态的系统。如果你发现CFDL控制效果不理想,误差曲线呈现“跟得上但很勉强”的状态,可以试试PFDL,让控制器“多看几步历史输入”。

2.3 FFDL:输入输出历史一起用

FFDL进一步加入输出增量窗口:

[ \Delta Y_{L_y}(k) = [\Delta y(k), \Delta y(k-1), \dots, \Delta y(k-L_y+1)]^T ]

[ \Delta U_{L_u}(k) = [\Delta u(k), \Delta u(k-1), \dots, \Delta u(k-L_u+1)]^T ]

动态线性化模型为:

[ \Delta y(k+1) = \phi_{y}(k)^T \Delta Y_{L_y}(k) + \phi_{u}(k)^T \Delta U_{L_u}(k) ]

这意味着控制器不仅考虑输入历史,还考虑输出历史的趋势。对于输出自回归特性明显、存在强非线性的系统,FFDL的建模能力更强。

但注意,需要估计的参数总数变成 ( L_y + L_u ) 个,估计器的负担明显增加。我做过一个例子:系统模型是 ( y(k+1) = \frac{2.5 y(k) y(k-1)}{1 + y(k)^2 + y(k-1)^2} + u(k) ),用CFDL已经能跟踪,但换成FFDL后由于参数估计初值不合适,前几步就出现了高频抖动。所以FFDL不是“越复杂越好”,它更适合CFDL、PFDL都失效的难缠场景。

3. Matlab代码实现的整体架构与关键函数设计

3.1 代码框架:主脚本、控制器函数、被控对象函数

复现MFAC时我强烈建议分模块写,不要全部堆在脚本里。我的工程目录大概是这样的:

  • main_compare.m:主脚本,设置系统类型、控制器类型、仿真步数,调用各函数并绘图。
  • plant_model.m:被控对象函数,内部用switch选择不同的非线性系统。
  • mfac_controller.m:控制器函数,内部用switch选择CFDL、PFDL还是FFDL。
  • parameter_reset.m:伪偏导数重置逻辑,单独提出来方便调试。

主脚本的结构可以这样:

% main_compare.m clear; clc; close all; steps = 500; sys_type = 1; % 1: 光滑非线性 2: 高阶动态 3: 非最小相位 ctrl_type = 'CFDL'; % 'CFDL' / 'PFDL' / 'FFDL' % 初始化状态 u = zeros(steps, 1); y = zeros(steps, 1); yd = zeros(steps, 1); % 期望轨迹 for k = 1:steps yd(k) = 0.5 * sin(k * 0.01) + 0.3 * cos(k * 0.02); end % 控制器参数 param.eta = 1; param.mu = 0.1; param.rho = 0.5; param.lambda = 1; param.phi0 = 1; % 伪偏导数初值 param.Lu = 2; % PFDL/FFDL输入窗口 param.Ly = 2; % FFDL输出窗口 % 核心循环 for k = 1:steps-1 t = k; % 当前时刻 % 计算控制量,需要用到历史 y, yd 和 u u(k+1) = mfac_controller(t, y, yd, u, param, ctrl_type); % 被控对象输出 y(k+1) = plant_model(t, y, u, sys_type); end % 绘图 plot(yd, 'r--'); hold on; plot(y, 'b-'); legend('期望','实际');

这里t是当前时刻索引,历史值可以直接从y、u数组中取。实际写的时候要注意数组索引不能越界,尤其是PFDL、FFDL需要用到前几步数据时,我在代码里用max(k - Lu, 1)做下限保护。

3.2 三个被控系统的选择与建模

标题里提到“三个非线性系统动态线性研究”,所以我在复现时准备了三个典型对象,分别考验三种动态线性化方法的不同侧重点。

系统1(光滑非线性的典型):

[ y(k+1) = \frac{y(k)}{1 + y(k)^2} + u(k)^3 ]

这个系统输出平滑,输入带有三次非线性,比较适合验证CFDL的基线性能。

系统2(高阶动态型):

[ y(k+1) = \frac{y(k) y(k-1) y(k-2) u(k-1) (y(k-2) - 1)}{1 + y(k)^2 + y(k-1)^2 + y(k-2)^2} + u(k) ]

这个系统带有输入滞后项 ( u(k-1) ),且分母非线性较复杂。CFDL在这个系统上往往需要把控制步长调得很小心,PFDL会明显改善跟踪效果。

系统3(非最小相位型):

[ y(k+1) = \frac{2.5 y(k) y(k-1)}{1 + y(k)^2 + y(k-1)^2} + u(k) + 0.5 u(k-1) ]

这里加入了未来时刻输入反效应(( u(k-1) ) 项),相位特性不理想,FFDL由于同时考虑输出历史和输入历史,通常能获得比CFDL更平滑的控制效果。

plant_model.m的写法大概是这样:

function y_next = plant_model(k, y, u, sys_type) switch sys_type case 1 yk = y(k); uk = u(k+1); y_next = yk / (1 + yk^2) + uk^3; case 2 yk = y(k); ym1 = y(max(k-1,1)); ym2 = y(max(k-2,1)); um1 = u(max(k,1)); uk = u(k+1); y_next = yk * ym1 * ym2 * um1 * (ym2 - 1) / (1 + yk^2 + ym1^2 + ym2^2) + uk; case 3 yk = y(k); ym1 = y(max(k-1,1)); uk = u(k+1); um1 = u(max(k,1)); y_next = 2.5 * yk * ym1 / (1 + yk^2 + ym1^2) + uk + 0.5 * um1; end end

注意系统2、3里我用u(max(k,1))访问历史控制量,避免前几步没数据时索引错误。仿真前几个时刻可以给y(1)、u(1)初始化为0,MFAC控制器通常能自己拉回来。

3.3 控制器函数的设计与参数传递

mfac_controller.m是核心。CFDL部分,我的实现思路是维护一个状态量phi_hat,由于它在每个时刻都要更新,我直接把它放在param结构体里,函数每次调用时更新并返回。代码片段:

function u_next = mfac_controller(k, y, yd, u, param, ctrl_type) uk = u(k); yk = y(k); dy = yk - y(max(k-1,1)); du_prev = uk - u(max(k-1,1)); switch ctrl_type case 'CFDL' phi = param.phi_hat; % 伪偏导数估计 if abs(du_prev) > 1e-5 phi = phi + param.eta * du_prev * (dy - phi * du_prev) / (param.mu + du_prev^2); end % 重置 if abs(phi) <= 1e-5 || abs(du_prev) <= 1e-5 phi = param.phi0; end param.phi_hat = phi; % 控制律 u_next = uk + param.rho * phi * (yd(k+1) - yk) / (param.lambda + phi^2); % 其他case略 end end

这里最关键的是“控制律用当前的phi还是更新后的phi”。不同文献写法不同,我测试后更推荐先用更新后的phi计算控制量,因为它在当前时刻更好地反映了系统局部动态。如果你的仿真发散,可以试试改用旧值,有时候反而更稳,这跟采样周期和对象特性有关。

PFDL和FFDL的代码结构类似,只是phi从标量变成向量,控制律变成矩阵运算。FFDL要额外估计输出窗口的伪偏导数,代码更啰嗦,但核心公式就是前面那套估计加重置。建议先用结构体把phi_y和phi_u分开存,否则容易把自己绕晕。

4. 三种方法在非线性系统上的仿真对比与结论

4.1 仿真结果怎么看

跑完仿真不要急着截图,先看三条曲线:输出跟踪曲线、误差曲线、控制输入曲线。

输出跟踪曲线能看出是否跟上期望轨迹,但“跟上”也有区别:是相位滞后很小但全程震荡,还是平稳但滞后明显。误差曲线能反映稳态精度和收敛速度。控制输入曲线最容易被忽略,但它最能暴露问题:如果控制量高频大幅振荡,说明算法参数不合理,即便输出看起来不错,实际工程中执行器也受不了。

我在系统1上对比三种方法时,CFDL经过调参能达到稳态误差在 ( 10^{-3} ) 量级,控制输入较平滑。PFDL因为窗口信息多,前期暂态误差更小,但稳态时会有轻微的高频波动。FFDL在这类平滑系统上反而“用力过猛”,需要把 ( \lambda ) 调大才能稳定。这说明方法选择确实依赖系统特性,不是越复杂越好。

4.2 计算量、参数数量与前向适用性对比

我梳理了一张表,方便大家一眼看明白三种方法的差异:

方法动态线性化窗口伪偏导数数量计算量典型适用场景调参难度
CFDL仅当前输入增量1低光滑非线性、动态较简单的系统低
PFDL输入窗口 ( L_u )( L_u )中输入滞后、高阶动态明显中
FFDL输入窗口 + 输出窗口( L_u + L_y )高强非线性、输出自回归特性显著的复杂系统高

从复现角度看,CFDL适合做算法验证和理解原理,PFDL适合处理带滞后的对象,FFDL则适合CFDL和PFDL都压不住的情况。要注意伪偏导数数量增加后,每个参数的估计都需要充分的激励信号,如果期望轨迹太“平坦”,估计器可能学不动,控制效果会劣化。

4.3 稳定性和边界条件不能只看仿真

MFAC虽然不需要模型,但闭环稳定性是有条件的:系统需要满足广义Lipschitz条件,且伪偏导数的变化不能太剧烈。实际仿真中如果系统非线性项太过“野”,比如输入项是 ( e^u ) 或 ( u^5 ),MFAC也会失控。我在复现时把系统1的输入非线性改成 ( u^5 ) 测试,发现CFDL明显跟踪变差,主要因为系统局部增益变化太快,单个标量伪偏导数已经不足以刻画这种剧烈非线性。

另外,控制律中的 ( \lambda ) 不是越大越好。( \lambda ) 太小时控制增量容易过大,太大时控制动作迟钝。一个相对稳妥的调参顺序是:先固定 ( \eta ) 和 ( \mu ) 让伪偏导数估计收敛,再调 ( \rho ) 让跟踪跟上,最后调 ( \lambda ) 抑制控制震荡。不要上来就同时改四个参数,否则出了问题根本不知道是哪个参数造成的。

5. 复现路上的坑与排查心得

5.1 伪偏导数估计发散,输出直接飞到无穷大

这是新手最容易遇到的问题。多半是 ( \mu ) 太小、( \eta ) 太大,或者伪偏导数初值选得和实际动态差太远。我的建议是:( \mu ) 不要低于 ( 10^{-3} ),( \eta ) 控制在 ( 0.5 \sim 2 ) 之间,初值 ( \phi(0) ) 设成 ( 0.5 \sim 2 ) 之间的正数。如果输出还是爆炸,就检查重置条件是否生效。我在代码里加了param.phi_hat的上下限限制,比如 ( [0.1, 10] ) 之间,超出就强制重置,稳定性立刻变好。

5.2 控制量高频振荡,误差曲线像锯齿

这个现象通常是 ( \rho ) 偏大或 ( \lambda ) 偏小。控制律里 ( \lambda ) 的作用就是惩罚控制能量,建议先把 ( \lambda ) 调到 5 以上再看看。如果振荡仍然存在,可能不是参数问题,而是伪偏导数估计对噪声过于敏感,可以在估计公式里对 ( \Delta y(k) ) 做一阶滤波。实际中我处理过一种情况:系统2里的输出突变很多,直接滤波后估计才稳定下来。

5.3 PFDL/FFDL窗口长度过大导致“过拟合”

窗口长度不是越大越好。我试过把 ( L_u ) 设成 5,结果控制效果反而不如 ( L_u=2 )。原因是需要在线估计的伪偏导数变多,但系统能提供的信息量有限,多余参数反而被噪声带偏。建议窗口长度从2开始试,如果效果不理想再逐步增加到3、4,不要一上来就挑战8、9个参数。

5.4 期望轨迹的选择影响巨大

如果期望轨迹是常数,伪偏导数估计得到的激励不足,控制器后续遇到变化信号时会“反应迟钝”。我在复现时先让期望信号是正弦或方波,让系统充分激励,等伪偏导数收敛后再切到实际目标轨迹。这一点在对比CFDL、PFDL、FFDL时尤其重要,否则三种方法的差异会被激励不足掩盖。

5.5 代码调试时记得检查数组索引

Matlab数组索引从1开始,而离散系统公式里经常出现 ( k-1 )、( k-2 )。仿真前几步容易出现越界,最简单的办法是初始化足够长的历史序列,或者在访问历史值时用max(k - n, 1)。另外,u(k+1)表示当前计算出的控制量要等到下一时刻才作用,这个时序关系务必在循环里理清楚。

我个人的体会是,复现MFAC最大的收获不是记住CFDL、PFDL、FFDL的公式,而是理解了“数据模型 + 在线估计 + 控制器设计”这一套完全不同于传统建模控制的思路。这套方法很适合快速验证非线性系统的控制可行性,尤其当被控对象机理不明、但能拿到可靠输入输出数据时,MFAC是一个很实用的备选方案。至于三种动态线性化方法,建议你先从CFDL入手,跑通了再把窗口信息逐步加上去,这样既能直观感受每种方法的变化,也不会被一堆伪偏导数参数砸晕。希望这篇记录能让你少走一些弯路。

返回列表