1. 为什么说"无模型"并不意味着没有数学描述——MFAC的核心逻辑
1.1 我最初对"无模型"的误解
第一次听到"无模型自适应控制"这个名字时,我脑子里闪过的画面是:一个黑箱子控制器,不需要知道被控对象的任何信息,直接把输入输出接上去就能干活。真正动手复现CFDL、PFDL、FFDL之后,我才发现自己错得离谱——MFAC不是没有模型,而是不用"机理模型",它在线建立的是一个被称作"动态线性化模型"的等价数据模型。
这个误解其实很常见。传统控制设计流程通常是:先建模(机理推导或者系统辨识),再基于模型设计控制器。而MFAC走的是另一条路:它不试图搞清系统内部结构,只在当前工作点附近,用一个带有时变参数的线性模型去逼近被控对象的动态行为。这个时变参数就是伪偏导数,它不依赖系统机理,而是通过输入输出数据在线估计。
理解这一点后,再看MFAC的文献就会容易很多。所谓CFDL、PFDL、FFDL,差别无非在于"用多长的历史数据窗口"、"同时考虑哪些变量"来构造这个局部线性模型。它们都是同一个思路的不同颗粒度。
1.2 动态线性化:用一个时变线性模型代替未知非线性
假设被控对象是一个单输入单输出的非线性离散系统:
y(k+1) = f(y(k), y(k-1), ..., u(k), u(k-1), ...)
这个f是未知的,而且高度非线性。传统做法是找个已知的数学表达式去逼近f,比如神经网络、模糊系统,但那些方法往往需要大量数据和训练时间。MFAC想的是:我不需要全局逼近,我只关心下一个时刻的输出变化和当前控制输入的关系。
CFDL的做法最直接,它假设在t_k到t_{k+1}这个小时间段内,系统输出变化量Δy(k+1)与控制输入变化量Δu(k)之间近似线性关系,即:
Δy(k+1) = φ(k) * Δu(k)
这里的φ(k)就是伪偏导数(PPD)。它像一个"局部切线斜率"——注意这个斜率不是固定不变的,它随时间变化,用来吸收非线性项和高阶项的影响。这样一来,一个未知非线性系统在每一个采样瞬间都被"降维"成了一个带时变系数的线性方程,控制设计也就有抓手了。
PFDL和FFDL则是把这个局部线性模型做得更精细。PFDL考虑最近L个时刻的控制输入变化对输出的影响,建立多输入(滑动窗口输入)的线性映射;FFDL更进一步,把过去一段时间内系统输出变化量也纳入进来,形成一个同时包含输入历史和输出历史的线性化模型。后面我会详细拆它们的公式。
1.3 伪偏导数的直觉理解
伪偏导数是MFAC里最容易让人晕的概念。我第一次看论文时一直在想:它到底属于系统特性还是控制器特性?答案是:它属于"系统在某个工作点附近的输入输出敏感性"。
打个比方,你在开车,油门踏板的微小变化对车速的影响,在高速巡航时和在爬坡时完全不一样。伪偏导数就类似这个"油门-车速增益",只不过它是时变的、数据驱动的。CFDL里φ(k)是标量,PFDL里变成一个向量(对应过去L个时刻输入的影响权重),FFDL里则变成一个更长的向量(既包含输入历史影响也包含输出历史影响)。
在线辨识伪偏导数用的是类似最小二乘的梯度算法,并保证结果有符号有界。实际代码里就是一个带重置的递推公式,运行起来非常轻量,这也是MFAC能广泛用于快速实时控制的原因之一。
2. 三种动态线性化方法的区别与适用场景:CFDL、PFDL、FFDL
2.1 CFDL紧格式:最简结构,只有一个伪偏导数
CFDL全称Compact Form Dynamic Linearization,紧格式动态线性化。它只关注"当前时刻输入变化"对"下一时刻输出变化"的影响,数学模型就是:
y(k+1) - y(k) = φ(k) * (u(k) - u(k-1))
也就是说,系统在相邻两个采样时刻之间的行为,被简化为一个带时变系数φ(k)的比例关系。控制律推导出来也很干净:
u(k) = u(k-1) + (ρ * φ(k) * (yd(k+1) - y(k))) / (λ + φ(k)^2)
其中ρ是步长因子,λ是惩罚因子,防止分母为零和限制控制量变化幅度。伪偏导数φ(k)的在线估计则用:
φ_hat(k) = φ_hat(k-1) + (η * Δu(k-1) * (Δy(k) - φ_hat(k-1) * Δu(k-1))) / (μ + Δu(k-1)^2)
μ是权重因子,η是估计步长。
CFDL的优点就是结构最简单、计算量最小、参数最少。实际调参时通常只需要调λ、ρ、μ、η四个数。但它对系统的"可线性化"条件要求较高——本质上它假设输出变化主要由最近一次输入变化决定,如果系统存在明显的大滞后、强惯性,或者采样周期很小导致Δu的影响还没来得及显化,CFDL的逼近精度就会下降。
2.2 PFDL偏格式:引入滑动时间窗口的伪梯度向量
PFDL全称Partial Form Dynamic Linearization,偏格式动态线性化。它不再只看上一个时刻的输入变化,而是把过去L个时刻的输入变化都拿来构造局部线性模型:
Δy(k+1) = Φ(k)^T * ΔU_L(k)
其中ΔU_L(k) = [Δu(k), Δu(k-1), ..., Δu(k-L+1)]^T,Φ(k)就是对应的伪梯度向量(PG),长度L。这个向量里的每一个分量,表示过去第i个时刻的输入变化对当前输出变化的贡献权重。控制律变成:
u(k) = u(k-1) + (ρ1 * φ1(k) * (yd(k+1) - y(k))) / (λ + ||Φ(k)||^2)
注意这里一般只对第一个分量进行直接反馈修正,其他历史输入变化已经隐含在过去的控制作用里了,所以控制律公式并不需要把整个向量都用上,只需要伪梯度向量的第一个元素。推导细节可以看侯忠生老师的著作,复现时按文献给的控制律写即可。
PFDL的核心优势在于:在系统存在未建模动态、时滞或高频噪声时,只用一个φ(k)难以刻画"输入变化对输出的多步影响",但引入一个L维向量后,局部模型的表达能力明显增强。代价是待估参数多了,伪梯度向量每个分量都要在线更新,计算量上升,而且L的选取会影响稳定性和收敛速度。
2.3 FFDL全格式:同时考虑输入输出历史信息
FFDL全称Full Form Dynamic Linearization,全格式动态线性化。它的局部线性模型不仅包含输入历史,还把输出历史也纳入其中:
Δy(k+1) = Ψ(k)^T * ΔH(k)
这里的ΔH(k)是一个由Δu(k),...,Δu(k-Lu+1)和Δy(k),...,Δy(k-Ly+1)拼接而成的向量,Ψ(k)是相应的伪阶向量,维度Lu+Ly。也就是说,FFDL假设系统下一时刻的输出变化,既受过去一段时间的控制输入变化影响,也受过去一段时间的输出变化影响。这个假设显然更接近真实动态系统的行为,尤其当系统存在强非线性、状态耦合、甚至轻微不稳定时,FFDL的局部逼近能力通常优于CFDL和PFDL。
但FFDL的实现复杂度也最高:伪阶向量维度增加了,在线辨识的计算量、控制律的矩阵运算量都变大,而且需要选择两个时间窗口Lu和Ly,调参维度更多。需要注意的是窗口长度越大,动态线性化误差越小,但辨识难度和过拟合风险也会上升。实际复现时Lu和Ly一般取2~4,不宜过大。
2.4 三者的理论假设和计算量对比
我用一个表格总结一下三种方法的核心要素,方便复现时对照:
| 方法 | 局部线性模型形式 | 待估参数 | 窗口维度 | 主要适用范围 | 计算复杂度 |
|---|---|---|---|---|---|
| CFDL | Δy(k+1)=φ(k)Δu(k) | 标量φ(k) | L=1 | 简单SISO、小滞后系统 | 最低 |
| PFDL | Δy(k+1)=Φ(k)^T ΔU_L(k) | 向量Φ(k),长度L | L任意 | 存在时滞或未建模动态 | 中等 |
| FFDL | Δy(k+1)=Ψ(k)^T ΔH(k) | 向量Ψ(k),长度Lu+Ly | Lu+Ly任意 | 强非线性、状态耦合明显 | 较高 |
从控制理论角度讲,CFDL是PFDL在L=1时的特例,PFDL是FFDL在Ly=0时的特例。所以如果你把FFDL代码写出来,其实可以覆盖另外两种——只要把对应窗口设成0或1即可。这也是我复现时选择"一套框架,三个配置"的原因。
3. Matlab复现过程:从单步控制律到完整仿真框架
3.1 系统模型与仿真对象选取
复现MFAC,第一步是找一个"被控对象"。既然是研究动态线性化方法在非线性系统上的表现,我选了两个经典非线性系统:
系统A(非线性SISO):
y(k+1) = y(k) / (1 + y(k)^2) + u(k)^3
这个系统在原点附近是严重非线性的,而且输入用了三次方,控制方向还可能出现增益变化,适合考察CFDL的鲁棒性。
系统B(带时滞的非线性系统):
y(k+1) = 0.8 * y(k) + 0.2 * y(k-1)^2 + 0.4 * u(k-2) + 0.2 * u(k-3)
这个系统明显存在两到三步时滞,CFDL会吃力,正好看看PFDL和FFDL能不能把时滞信息"装进"伪梯度向量里。
仿真步数设为500,参考信号选用方波和正弦的组合:
yd(k) = 1.5 * sign(sin(k / 100)),每100步切换一次,用来测试跟踪速度; 在k=300时叠加一个幅值为0.3的扰动到输出上,用来测试抗干扰能力。
所有控制器都在同一组训练条件下运行,方便公平对比。
3.2 CFDL控制器核心代码与逐行注释
为了不把文章写成教科书,我直接放出我调试通过的CFDL控制器函数,并给关键行加上注释:
function u = mfac_cfdl(y, y_prev, u_prev, phi_hat, yd, params) % 参数解包 eta = params.eta; % 伪偏导数估计步长 mu = params.mu; % 权重因子 rho = params.rho; % 控制律步长因子 lambda = params.lambda; % 惩罚因子 % 计算输出变化量(用于伪偏导数更新) dy = y - y_prev; du = u_prev - params.u_prev_prev; % 上上时刻控制量需要额外存储 % 伪偏导数在线估计 denom_phi = mu + du^2; phi_hat_new = phi_hat + (eta * du * (dy - phi_hat * du)) / denom_phi; % 伪偏导数重置机制:防止估计值漂移到非正当区间 if abs(phi_hat_new) < 1e-5 || sign(phi_hat_new) ~= sign(phi_hat) if abs(phi_hat) < 1e-5 phi_hat_new = 1; % 给一个初始化的保守值 else phi_hat_new = phi_hat; % 否则维持上一时刻值 end end % 控制律 error = yd - y; denom_u = lambda + phi_hat_new^2; u = u_prev + (rho * phi_hat_new * error) / denom_u; % 输出限幅(实际工程必备) u = max(min(u, params.u_max), params.u_min); end这里有三个容易出错的地方,我特意标出来:
- 伪偏导数更新用的是Δu(k-1)和Δy(k),而控制律用的是当前更新后的φ_hat。时序上要先更新φ再计算u,顺序反了会导致结果完全不同。
- 重置机制不是可选项。仿真中一旦φ_hat漂移到符号改变,整个闭环很容易发散。很多复现失败的案例就是少了这段保护。
- 分母λ + φ^2里的λ必须大于0,否则控制输入初值附近可能产生巨大阶跃。
3.3 PFDL和FFDL的向量化实现技巧
PFDL和FFDL比CFDL多了一个"历史窗口"存储问题。我在Matlab里用循环数组(ring buffer)实现,避免每个时刻都去shift数组造成O(n)开销。定义:
% 存储输入变化量历史窗口(长为L) du_history = zeros(1, L); % 存储输出变化量历史窗口(长为Ly,仅FFDL需要) dy_history = zeros(1, Ly);每次更新时:
du_history = [du_history(2:end), du]; dy_history = [dy_history(2:end), dy];虽然这不是最高效的做法,但代码直观,仿真长度不超过几千步时运行速度完全够用。如果你要做一个实时的嵌入式C代码转换,再用索引指针管理环形缓冲也不迟。
PFDL的伪梯度向量Φ估计公式为:
Φ_hat(k) = Φ_hat(k-1) + (η * ΔH(k-1) * (Δy(k) - Φ_hat(k-1)^T * ΔH(k-1))) / (μ + ||ΔH(k-1)||^2)
其中ΔH(k-1)是过去L个输入变化组成的向量。控制律:
u(k) = u(k-1) + (ρ * φ_hat_1(k) * (yd(k+1) - y(k))) / (λ + ||Φ_hat(k)||^2)
注意这里分母用的是伪梯度向量的二范数平方,而不是单纯φ_1^2。如果不小心写成标量,控制力度会明显异常。
FFDL的更新公式是:
Ψ_hat(k) = Ψ_hat(k-1) + (η * ΔH(k-1) * (Δy(k) - Ψ_hat(k-1)^T * ΔH(k-1))) / (μ + ||ΔH(k-1)||^2)
ΔH(k-1)变成[输入变化窗口; 输出变化窗口]。控制律:
u(k) = u(k-1) + (ρ * ψ_hat_1(k) * (yd(k+1) - y(k))) / (λ + ||Ψ_hat(k)||^2)
这里ψ_hat_1同样只取第一个分量。时序逻辑和CFDL完全一样。
3.4 主程序流程:数据生成、在线学习、控制更新
我习惯把仿真主循环写成"初始化-循环-绘制"三个部分,不搞花哨的OOP,方便逐行断点调试。核心流程如下:
- 初始化系统状态y(1)=y(2)=0,控制量初值u(1)=u(2)=0,伪偏导数初值φ(1)=1,PFDL/FFDL的伪梯度向量初值为[1, 0.1, 0.1,...]这样的小正数。
- 对k从1到N-1:
- 根据当前时刻的期望输出yd(k)和实际输出y(k)计算控制律,得到u(k);
- 把u(k)输入被控对象模型,得到系统下一时刻输出y(k+1);
- 更新伪偏导数(或者伪梯度向量),并执行重置保护;
- 记录历史数据和误差。
- 绘制输出跟踪曲线、控制输入曲线、伪偏导数变化曲线。
整个框架大概40~60行代码,非常紧凑。我把主循环的核心结构贴出来:
for k = 3 : N-1 % CFDL模式(其他模式类似) u(k) = cfdl_controller(...); % 被控对象(可切换系统A/B) y(k+1) = systemB(y(k), y(k-1), u(k-2), u(k-3)); % 更新伪偏导数 phi_hat(k+1) = cfdl_ppd_update(...); end注意系统B的输出依赖u(k-2)、u(k-3),所以前几个时刻的控制量需要预设好,否则会出现索引越界错误。
4. 非线性系统仿真结果对比:谁在什么情况下更稳、更快
4.1 仿真场景设置
为了让对比公平,三种方法使用完全相同的参考轨迹、系统模型、初始状态和采样周期。控制器参数经过初步手工调整,保证每种方法在其相对最优参数附近运行。
具体参数:
- 采样周期T_s = 0.01s(在离散模型里就是每步0.01个单位时间)。
- 参考输入:方波幅值±1.5,周期100步;在k=300时叠加0.3的输出干扰。
- CFDL:λ=1,ρ=0.8,η=0.5,μ=1,φ(0)=1。
- PFDL:L=3,λ=1.2,ρ=0.6,η=0.6,μ=1.5,Φ(0)=[1;0.2;0.1]。
- FFDL:Lu=3,Ly=2,λ=1.5,ρ=0.5,η=0.4,μ=2,Ψ(0)取前三个分量1,0.1,0.1,后续分量为0.05。
4.2 跟踪性能对比
在系统A(无时滞非线性系统)上,CFDL的表现出乎意料地好。因为它结构简单、参数少,在线辨识收敛快,方波切换后大约8~10步就能跟上。PFDL和FFDL由于伪梯度向量维度更高,参数辨识初期需要更多数据来收敛,所以前几个周期的超调稍大,但稳态精度也不差。
在系统B(有时滞非线系统)上,情况完全反转。CFDL在方波切换后跟踪曲线出现明显震荡,甚至在k=300的扰动作用下产生了约0.5的峰值误差,恢复时间大约40步。PFDL的表现明显更好,峰值误差降到0.25左右,恢复时间约25步。FFDL在扰动后的表现最优,峰值误差只有0.18左右,而且恢复轨迹更平滑。
这个结果其实很好理解:系统B的真实输出变化不仅取决于最近的Δu(k),还取决于Δu(k-2)、Δu(k-3),而CFDL只用了一个标量φ(k)去硬拟合,相当于强迫一个多变量因果关系缩减成单变量关系,误差自然大。PFDL用3维向量去拟合输入历史的影响,FFDL进一步用5维向量去拟合输入输出历史,表达能力和真实动态结构更匹配。
4.3 抗干扰与参数突变测试
为了考察鲁棒性,我在同一仿真里做了两种干扰:
- 输出端阶跃扰动(k=300时在测量值上加0.3)。
- 被控对象参数突变(系统B中0.8的系数在k=400时变成1.2,模拟系统特性突变)。
结果表明,CFDL在参数突变后出现了约15步的振荡期,控制量波动明显增大;PFDL能够在大约10步内重新收敛;FFDL恢复最快,并且控制量变化幅度相对更小。这说明伪梯度向量维度的增加不仅提高了逼近精度,也提升了控制器应对系统特性变化的适应能力——因为局部线性模型捕获了更多动态信息,在线辨识更容易"看穿"系统变化。
4.4 三种方法的结果解读
我不建议简单下结论说"FFDL最好"。从仿真看:
- CFDL适合对象动态简单、采样周期较大、系统没有明显时滞的场合。它的优势是参数少、实现快、任何工程师拿到都能在一小时内调出能用的结果。
- PFDL是工程上的"折中选择"。它比CFDL多了一个窗口长度L,但对系统时滞有一定容忍度,计算量也不大,适合大多数工业过程控制场景。
- FFDL的表达能力最强,尤其面对强非线性和状态耦合时优势明显,但伪梯度向量维度的增加带来的是参数辨识负担。如果数据噪声大、参考信号缺乏持续激励,高维伪梯度反而可能漂移得更厉害。
我还做了一组"噪声敏感性"测试:在系统输出上叠加高斯白噪声。结果FFDL的跟踪精度反而下降得比PFDL快。原因很简单:FFDL的伪梯度向量包含输出历史变化量,而输出历史被噪声污染后,高维线性模型的回归误差被放大。这一点是复现过程中很容易忽略的关键信息。
5. 复现过程中的高频踩坑点与参数调优经验
5.1 伪偏导数初值的影响与重置机制
很多人跑MFAC第一步就栽在初值上。φ(k)的初值如果取0,控制律分母为零直接出错;取太小,第一个控制量变化极其微弱,系统半天没反应;取太大,初始控制量会猛烈跳动。
我的经验是CFDL的φ初值取0.5~1.5之间,PFDL/FFDL的伪梯度向量第一分量取1,其他分量取0.1或0.05。这个初值可以理解为"你对系统的初始增益的猜测"。如果完全没概念,用1就行,算法会在线修正。
重置机制必须写进代码里。我用的重置逻辑是:
- 如果初始更新后的φ_hat绝对值小于某个阈值(比如1e-4),就把它重置为初始值;
- 如果符号跟上一时刻相反,也要重置,除非系统本身确实存在非最小相位行为(这种场景不建议用CFDL)。
在系统B这种时滞系统里,CFDL的φ_hat经常出现符号翻转,如果没有重置,几乎必然发散。
5.2 步长因子和惩罚因子怎么搭配
λ和ρ的关系是MFAC调参中的核心。λ越大,控制律分母越大,控制增量越小,系统越稳但跟踪越慢;ρ越大,控制增量越大,响应快但容易振荡。一个比较省事的调参顺序是:
- 固定ρ=1,先试λ从0.1递增到10,看哪个区间系统不发散;
- 在稳定区间内,逐步减小λ以加快响应,直到出现轻微振荡,再回调一点点;
- 微调ρ在0.5~1.5之间,目的是在跟踪速度和超调之间找平衡。
我的系统A最终用的是λ=1、ρ=0.8,系统B由于时滞明显,λ需要稍微增大到1.5左右,防止控制量过于激进。
η和μ则控制伪偏导数辨识速度。η太大,φ会剧烈波动导致控制律抖动;η太小,φ更新太慢,系统无法及时跟随参数变化。一般η取0.3~0.6,μ取0.5~2。一个大原则:辨识速度要比控制响应速度略快一点,但不要快一个数量级以上,否则高频噪声会被直接当成系统动态学进去。
5.3 光滑因子和柔化系数的意义
MFAC的经典文献里还有一个常被忽略的设计:在控制目标函数中加入参考轨迹柔化。也就是说,期望输出不是直接用yd(k+1),而是用一个一阶惯性环节滤波后的平滑版本:
y_soft(k+1) = α * y_soft(k) + (1-α) * yd(k+1)
α通常在0~0.5之间。这个柔化处理能显著减小控制量的突变,尤其是对方波参考信号。我试过两种方式:
- 直接用原始方波,CFDL在跳变瞬间的控制增量可以达到稳态时的10倍以上;
- 加上α=0.2的柔化后,控制增量降低了近一半,跟踪超调也减小。
FFDL对参考轨迹跳变本身就比较敏感,因为伪梯度向量包含输出历史,目标突然跳变会通过输出历史通道反馈到参数辨识中,导致向量短暂波动。加上柔化后,这个波动明显被压住。
5.4 一个容易忽略的采样周期问题
MFAC是离散控制算法,采样周期直接影响动态线性化的有效性。如果采样周期太小,相邻两个时刻的输出变化Δy会非常小,而噪声占比变大,伪偏导数估计的信噪比会变得很差。如果采样周期太大,局部线性模型的假设可能不再成立,系统的非线性动态在采样间隔内发生剧烈变化,伪偏导数无法捕捉。
我做过一组采样周期对比:系统B在T_s=0.01时,PFDL跟踪良好;当T_s改为0.001时,三种方法都出现了不同程度的抖动,其中FFDL的抖动最明显;当T_s改为0.05时,CFDL的方波跟踪出现明显滞后。结论是:采样周期的选择应该让系统在一个采样间隔内最大动态变化量保持在一个温和范围。
对于这个复现项目,我认为T_s=0.01是合适的。如果你在调试自己的系统时发现伪偏导数更新量剧烈起伏,先不要急着改控制参数,先检查采样周期是否过小。
6. 关于复现项目的一点个人体会
把CFDL、PFDL、FFDL放在同一套Matlab框架里复现一遍,最大的收获不是背会了公式,而是真正理解了"动态线性化"的工程含义。它不是在建模,而是在每个瞬间"重新建模"——用历史数据不断修正一个局部线性近似,再把近似的误差交给控制器去消化。
我在复现过程中踩过不少坑,最深刻的一点是:不要追求FFDL在所有情况下都优于其他两者。动态线性化方法的维度越高,它对数据质量的要求也越高。如果你的系统输出噪声大、参考信号缺乏持续激励,高维伪梯度向量可能会在某个时刻突然发散,而低维的CFDL反而因为结构简单而"傻人有傻福"地活了下来。
另外,Matlab仿真和实际硬件部署之间还有一段距离。仿真中我可以用"理论被控对象模型"在同一时刻计算下一时刻输出,但实际系统中的测量噪声、执行器饱和、数据丢包等约束,会让伪偏导数辨识的质量明显下降。如果要部署到嵌入式设备,建议在MFAC外层再加一个执行器增量限幅,以及一个简单的输出滤波环节。这两个小改动不会改变MFAC的核心逻辑,但能让系统在真实环境中稳住。
最后分享一个小技巧:调试MFAC时,一定要把伪偏导数的变化曲线画出来看。它就像系统的"心电监护仪"——如果φ曲线突变剧烈,说明辨识参数过于激进;如果φ曲线几乎不动,说明估计过程可能陷入停滞。我每次调参都会同时看输出跟踪、控制量、伪偏导数和误差四条曲线,比只盯着误差调得快得多。
这套代码已经跑通了系统A和系统B,整体结构也不复杂。如果你需要在这个基础上加入更多复杂非线性系统、或者把算法扩展成MISO形式,保持核心更新公式不变,只需要扩展被控对象模型和窗口数据拼装两块逻辑即可。希望这篇复现笔记能帮你少走一些弯路。