1. 从论文公式到可运行代码:MFAC复现的起点
拿到这篇MFAC复现任务时,我原以为又是一场被公式符号劝退的硬仗。读了几篇关于无模型自适应控制的论文之后发现,CFDL、PFDL、FFDL这三个缩写本身并不复杂,真正让人头疼的是论文里绕来绕去的递推公式和“动态线性化”这个看起来很高深的概念。等我把三类方法的Matlab代码跑通,回头再看这些公式,其实核心思想就一句话:不建立被控对象的精确数学模型,只利用输入输出数据在线估计一个“伪偏导数”,把这个伪偏导数当成对象梯度的替代品来设计控制器。
这玩意儿的实用价值在于,你不需要知道系统内部长什么样——它到底是机电系统、化工过程还是交通流模型,对MFAC来说区别不大,只要有输入输出数据就能干活。对于做非线性系统仿真、快速验证控制算法、或者在参数辨识困难场景下需要一份基线控制器的朋友来说,MFAC是非常实用的备选方案。这篇博文适合三类人:一是打算复现MFAC但被论文符号劝退的研究生,二是在Simulink或者Matlab脚本里需要快速跑非线性系统控制对比的工程师,三是想搞清楚CFDL、PFDL、FFDL三者区别但一直没找到清晰讲解的初学者。
我要强调的是,这不是把别人的代码拿来跑一遍就完事,而是要把“为什么这么写”“参数怎么选”“三个系统怎么设计才公平”这些东西全部讲透。下面我会先从MFAC的原理说起,再拆开三个动态线性化方法,接着给出可运行的Matlab框架,最后用三个非线性系统的仿真对比收尾,并把我在复现过程中踩过的坑一并列出来。
1.1 无模型自适应控制的“无模型”到底是什么意思
很多第一次接触MFAC的人会犯一个理解偏差,以为“无模型”就是完全不需要任何对象知识、随便乱调参数就能控制。实际上,MFAC是指不显式地辨识对象的数学模型,不需要你给出传递函数或者状态空间表达式,但控制器内部并不是没有“模型”——它在线构建了一个时变线性替代模型,这个替代模型每拍都在更新,用局部的输入输出关系去逼近真实对象的动态特性。
打个比方,传统基于模型的控制就像给一个人发了张精确地图,他按图索骥就能到达目的地;MFAC则像一个没有地图但也方向感很强的人,每走一步先感知脚下地形变化,估计出一个“下一步往哪走更接近目标”的方向,然后不断修正。地图可能因为道路翻修而过时,但实时感知不会——这正是MFAC在非线性、时变、难以建模场景下的优势。
好的,把这句话展开说一下。传统控制如果要用模型,通常要先做系统辨识,然后基于辨识结果设计控制器。问题在于非线性系统、时变参数、外部干扰复杂的场景里,模型误差很容易让控制器性能崩掉。MFAC的做法是:每一控制周期,根据最近的输入输出数据,在线估计一个描述“输入变化如何影响输出变化”的参数——这就是伪偏导数,简称PPD。然后用这个PPD去计算当前应该施加的控制增量。整个过程不需要任何对象机理知识,只要求一个很弱的条件:系统在正常工作点附近满足广义Lipschitz条件,也就是输入变化有限时输出变化不会无限大。这个条件对绝大多数实际被控对象来说都是成立的。
1.2 复现之前必须统一的概念
动手写代码之前,先把符号约定统一好,否则后面看CFDL、PFDL、FFDL的公式时会晕。我用的是离散时间表示,k表示当前采样时刻。设系统输出为y,控制输入为u,参考轨迹为yd。定义输入差分Δu(k)=u(k)-u(k-1),输出差分Δy(k)=y(k)-y(k-1)。这三个方法本质上都是在回答同一个问题:怎么用差分的形式去描述“输入变化导致输出变化”这件事,并且让这个描述在每一拍都成立。
伪偏导数是我需要反复强调的一个概念。在CFDL里它是一个标量φ(k),在PFDL和FFDL里是一个向量。它不要求物理含义完全等同于真实梯度,只要求它在数值上能反映“此刻输入变化Δu对下一拍输出变化Δy的影响强度”。由于它在线实时估计,所以即便对象本身高度非线性,也能用这个时变参数去适配局部动态。理解了这一点,后面所有公式的主线就清楚了:先估计φ,再代入控制律算出新的u,然后让对象跑一拍,产生新的y,循环往复。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CFDL、PFDL、FFDL:三种动态线性化到底差在哪
这三个方法的核心差异在于“动态线性化”展开的维度不同。CFDL只用一个标量伪偏导数描述输入输出关系,PFDL引入一个长度为L的历史输入窗口,FFDL则把历史输入和历史输出一起纳入线性化框架。它们各有适用的对象特征,也各有参数调节的侧重。这一章我把三种方法逐个拆开讲,包括数学形式、控制律推导、适用条件和Matlab实现要点。
2.1 CFDL:最紧凑的一阶替代模型
CFDL的全称是紧格式动态线性化,它的数学形式最为简洁:
Δy(k+1)=φ(k)·Δu(k)
这个式子表示:当前时刻控制输入的变化量乘以伪偏导数φ(k),就近似等于下一时刻输出的变化量。这里的φ(k)是有界的,但不要求符号恒定,正负都可能,这意味着对象可能存在非最小相位特性,MFAC框架依然能处理。
基于这个替代模型设计控制器,用一步向前预测控制加惩罚项的思路。设期望输出为yd(k+1),定义跟踪误差e(k)=yd(k+1)-y(k),我们希望找一组Δu(k)使得φ(k)Δu(k)尽量逼近e(k),同时对控制量变化本身施加一个惩罚λ|Δu(k)|²,防止控制量剧烈跳变。极小化这个目标函数,得到控制律:
u(k+1)=u(k)+ρ·φ(k)·e(k)/(λ+φ(k)²)
式中的ρ是步长因子,用来进一步调节控制幅度,取值范围通常在(0,1]之间。λ是控制量惩罚系数,它出现在分母里,所以λ越大控制增量越小,系统响应越保守。
PPD的在线估计采用投影算法。已知上一拍的输入差分Δu(k-1)和输出差分Δy(k),则估计公式为:
φ̂(k)=φ̂(k-1)+η·Δu(k-1)·[Δy(k)-φ̂(k-1)·Δu(k-1)]/(μ+Δu(k-1)²)
μ是一个防止分母为零的小正数,η是估计步长。当|Δu(k-1)|太小时,估计公式会因除零风险而失效,所以需要一个重置机制:一旦|φ̂(k)|低于某个阈值ε,或者|Δu(k-1)|低于ε,就把φ̂(k)重置为初值φ_0。这个重置机制不是可有可无的,它是保证算法稳定性的关键,后面踩坑部分我会专门展开。
CFDL的Matlab实现很直接,核心就是三个步骤:更新伪偏导数、算控制律、跑对象模型。我建议把估计逻辑封装成一个函数,方便后面切换PFDL和FFDL时复用。
matlab复制function phi = ppd_update_CFDL(phi, du, dy, eta, mu, phi_0, eps)
if abs(du) <= eps
phi = phi_0;
return;
end
phi = phi + eta * du * (dy - phi * du) / (mu + du^2);
if abs(phi) <= eps
phi = phi_0;
end
end
2.2 PFDL:把“历史输入窗口”放进线性化
CFDL的假设是输出变化只由当前控制变化决定,这等于忽略了控制信号的历史延后影响。对存在时滞、惯性较大、或者控制通道带有“记忆”的系统来说,CFDL的模型形式太簿了,一个标量φ描述不了多步滞后的因果关系。这时候需要用偏格式动态线性化,PFDL。
PFDL引入一个滑动时间窗口长度L,假设输出变化不仅受当前Δu(k)影响,还受前L-1个时刻的输入差分影响。定义:
ΔU_L(k)=[Δu(k),Δu(k-1),...,Δu(k-L+1)]ᵀ
则动态线性化形式为:
Δy(k+1)=Φ(k)ᵀ·ΔU_L(k)
这里的Φ(k)=[φ₁(k),φ₂(k),...,φ_L(k)]ᵀ是一个伪梯度向量,长度跟窗口一致。相比CFDL,PFDL只是在“输入变了多少”这个信息上做了扩展,输出的因果关系仍然用线性组合表示。
控制律的推导值得多花点篇幅,因为很多复现者都困惑过:PFDL的控制律为什么比CFDL多了一长串?推导思路是这样的。我们想让Φ(k)ᵀΔU_L(k)=yd(k+1)-y(k),但ΔU_L(k)有L个分量,方程只有一个,解不唯一。于是引入惩罚项,求带λ||ΔU_L(k)||²的正则化极小化问题:
J=[yd(k+1)-y(k)-ΦᵀΔU_L]²+λ||ΔU_L||²
对ΔU_L求梯度并令其为零,整理后得到:
ΔU_L(k)=Φ(k)·[yd(k+1)-y(k)]/(λ+||Φ(k)||²)
注意分母是标量,所以整个向量共享同一个分母。实际控制时只取第一行分量作为Δu(k),剩下的分量其实对应历史时刻已经发生的输入差分,它们不是“要输入的量”,而是已经存在的事实。把第一行分量写开,就得到PFDL控制律:
u(k+1)=u(k)+ρ·[φ₁(k)·e(k)+φ₂(k)·Δu(k)+...+φ_L(k)·Δu(k-L+1)]/(λ+||Φ(k)||²)
这里e(k)=yd(k+1)-y(k)。这个形式初看像PID加了一堆前馈项,实际上它是极小范数解的自然结果。PFDL的PPD估计与CFDL类似,只是把标量换成了向量:
Φ̂(k)=Φ̂(k-1)+η·ΔU_L(k-1)·[Δy(k)-ΔU_L(k-1)ᵀ·Φ̂(k-1)]/(μ+||ΔU_L(k-1)||²)
重置条件变成||Φ̂(k)||≤ε或者||ΔU_L(k-1)||≤ε时重置。Matlab实现时,核心是维护一个长度L的滑动窗口向量,每一拍把最新Δu塞进头部,丢掉最旧的数据。
matlab复制function [psi, DeltaU] = ppd_update_PFDL(psi, DeltaU, du, dy, eta, mu, psi_0, eps)
DeltaU = [du; DeltaU(1:end-1)];
denom = mu + DeltaU' * DeltaU;
if denom < eps
psi = psi_0;
return;
end
psi = psi + eta * DeltaU * (dy - DeltaU' * psi) / denom;
if norm(psi) <= eps
psi = psi_0;
end
end
PFDL和CFDL的控制器代码几乎一致,唯一区别是分子里多出的历史项,以及更新窗口的顺序问题。窗口长度L的选取就是接下来要说的关键问题。
2.3 FFDL:输入输出全格式统一框架
FFDL,全格式动态线性化,是三者里最一般的框架。PFDL只考虑了输入窗口,FFDL则把历史输出差分量也纳入进来:假设输出变化不仅与输入差分的历史有关,还与输出差分自身的延后有关。这么做对高阶对象、具有明显输出记忆效应的系统更友好。
定义Lu为输入窗口长度,Ly为输出窗口长度,构造扩展向量:
ΔH_{Lu,Ly}(k)=[Δu(k),...,Δu(k-Lu+1),Δy(k),...,Δy(k-Ly+1)]ᵀ
对应的伪梯度向量为Φ(k)=[φ₁(k),...,φ_{Lu+Ly}(k)]ᵀ,于是动态线性化形式为:
Δy(k+1)=Φ(k)ᵀ·ΔH_{Lu,Ly}(k)
控制律的推导思路跟PFDL完全一致,仍然是带惩罚的极小化问题,最后把全向量的第一行分量取出来作为当前控制增量:
u(k+1)=u(k)+ρ·[φ₁(k)·e(k)+φ₂(k)·Δu(k)+...+φ_{Lu}(k)·Δu(k-Lu+1)+φ_{Lu+1}(k)·Δy(k)+...+φ_{Lu+Ly}(k)·Δy(k-Ly+1)]/(λ+||Φ(k)||²)
注意这里从第二个分量开始,乘的都是“已经在历史中发生过”的差分值,所以控制器不需要预测这些历史值,只需要存储。FFDL的PPD估计同样套用投影算法,只是把ΔH_{Lu,Ly}整体当作向量使用。
FFDL的优势是更一般:CFDL相当于FFDL取Lu=1,Ly=0的特例,PFDL相当于FFDL取Ly=0的特例。但泛化也带来代价——需要调的参数变多了,伪梯度向量的维度更大,在线估计更容易受噪声影响。实际使用中,除非对象明显存在输出记忆效应,否则我通常不会一上来就上FFDL,先用CFDL或PFDL试跑,确实不行再加Ly。
2.4 三种方法的适用边界与选择
三种动态线性化方法之间没有绝对的优劣之分,只有“匹配不匹配对象特征”的问题。从Matlab复现和参数调试的角度给出一张对比表,方便快速选型。
| 方法 | 线性化形式 | 待调参数 | 最适合的对象特征 |
|---|---|---|---|
| CFDL | Δy=φΔu | λ, ρ, η, μ | 快速、平滑、控制通道直接的系统 |
| PFDL | Δy=ΦᵀΔU_L | 加L窗口长度 | 存在输入时滞、惯性大的系统 |
| FFDL | Δy=ΦᵀΔH_ | 加Lu, Ly窗口长度 | 高阶动态、输出记忆明显的系统 |
选型经验上,我建议遵循“最小充分原则”:先用CFDL,如果跟踪发散明显、存在稳定滞后或者控制通道有延后,再引入PFDL的L,通常L取2到5就够了。只有发现输出自历史对当前影响不可忽略时,才上FFDL。三个方法在代码结构上几乎一样的框架,切换只是把估计函数和控制律的分子替换一下,这让我们可以在仿真里快速地做三方法横向对比。
3. Matlab复现框架与核心函数实现
复现MFAC的关键不是写出一段能跑的单脚本,而是把框架搭干净,让切换CFDL、PFDL、FFDL变成换函数调用的事情。这章我会给出一个完整且经过验证的Matlab结构,包含主循环、PPD估计器、控制器和被控对象模型四个部分,同时解释每个环节的索引逻辑和参数设置依据。
3.1 仿真主流程设计:数据索引是复现的第一道坎
我在第一次写MFAC脚本时踩过一个很隐蔽的坑:输出y(k+1)是由输入u(k+1)驱动的,而控制律算出u(k+1)又需要用到y(k)和目标yd(k+1),所以u和y的索引不能同步更新,否则就错位了。正确的顺序是:先用第k时刻的数据估计PPD,再用控制律算出u(k+1),最后把u(k+1)代入被控对象得到y(k+1),然后k加一继续循环。
下面给出CFDL主脚本的框架,这个结构同样适用于PFDL和FFDL,只要替换估计器和控制律两行即可:
matlab复制%% MFAC-CFDL主循环
N = 500;
y = zeros(N + 1, 1);
u = zeros(N + 1, 1);
y(1) = 0.5; u(1) = 0.1;
y(2) = y(1) / (1 + y(1)^2) + u(1)^3; % 预跑一拍,避免索引错位
u(2) = u(1);
% 控制器参数
lambda = 0.5; rho = 0.6; eta = 1.0; mu = 1.0; eps = 1e-5;
phi = 1.0; % PPD初值
yd = 0.5 * (mod((1:N + 1)', 100) < 50); % 方波参考轨迹
for k = 2:N
du = u(k) - u(k - 1);
dy = y(k) - y(k - 1);
phi = ppd_update_CFDL(phi, du, dy, eta, mu, phi, eps);
e = yd(k + 1) - y(k);
u(k + 1) = u(k) + rho * phi * e / (lambda + phi^2);
% 被控对象
y(k + 1) = y(k) / (1 + y(k)^2) + u(k + 1)^3;
end
代码里我把u(2)预置为等于u(1),并且提前用u(1)驱动对象算了一拍y(2)。这样从k=2开始循环时,du和dy都是真实存在的值,PPD估计不会用到无效的索引。很多复现帖子里的代码直接让循环从k=1开始,Matlab里u(0)不合法,就只能把数组整体移位,容易搞混。预跑一拍的做法更稳妥。
3.2 PPD估计器与重置机制的细节
PPD估计器是三个方法共用的核心模块。估计公式本质上是梯度下降式的投影算法:用“实际dy与估计dy之差”作为修正量,往梯度方向迈一小步。这一小步的大小由η和分母μ+du²共同决定。有两点容易被忽略:一是du特别小时整条公式会失控,二是φ估计跑到零附近时控制器增益会失效。所以重置条件必须放在更新前后各查一次。
我用一个统一的估计函数风格来处理三种方法,函数签名保持一致,方便主脚本复用。CFDL的函数已经在上文给出,PFDL和FFDL的实现也类似。对于FFDL,窗口中要同时保存输入差分历史和输出差分历史,更新时注意顺序:先构造新窗口,再更新Φ,再用更新后的Φ和窗口计算控制律。
matlab复制function [Phi, DH] = ppd_update_FFDL(Phi, DH, du, dy, eta, mu, Phi_0, eps, Lu, Ly)
du_hist = DH(1:Lu);
dy_hist = DH(Lu + 1:end);
du_hist = [du; du_hist(1:end - 1)];
dy_hist = [dy; dy_hist(1:end - 1)];
DH = [du_hist; dy_hist];
denom = mu + DH' * DH;
if denom < eps
Phi = Phi_0;
return;
end
Phi = Phi + eta * DH * (dy - DH' * Phi) / denom;
if norm(Phi) <= eps
Phi = Phi_0;
end
end
这里Phi_0在循环里需要保持不变,不能跟着更新的Phi一起变,否则每次重置后初值一次一个样。我把初值在进入循环前存好,重置时直接赋值回去。细节虽小,但直接决定算法能不能复现论文里的收敛曲线。
3.3 评估指标与参数扫描方式
光看跟踪曲线只能得到“好像还行”的结论,要做三方法公平对比,得量化指标。我这里固定用三个指标来评价控制性能。第一个是均方根误差RMSE,直接反映跟踪精度;第二个是控制输入总变差TV,也就是相邻控制量差分的绝对值之和,用来衡量控制量抖动的剧烈程度;第三个是调节时间,定义为输出首次进入参考值±5%误差带之后不再越出的时间步数。这三个指标分别对应性能、执行机构负担、响应速度三个维度,几张表下来就能看出不同方法在不同对象上的取舍。
参数扫描我用的是一个简单但实用的策略:网格扫描加控制变量。比如要调λ和ρ,就先把η和μ固定,λ取[0.1, 0.3, 0.5, 0.8, 1.0],ρ取[0.1, 0.3, 0.5, 0.7, 1.0],跑完25组之后把RMSE画成热图。不要同时扫四个参数,组合爆炸后既看不清规律又浪费时间。
matlab复制lambda_set = [0.1 0.3 0.5 0.8 1.0];
rho_set = [0.1 0.3 0.5 0.7 1.0];
for ii = 1:length(lambda_set)
for jj = 1:length(rho_set)
RMSE_mat(ii, jj) = run_mfac_single(lambda_set(ii), rho_set(jj), ...);
end
end
跑完参数扫描后,取RMSE最低的一组作为该方法的代表参数。这里有一个不太直观但很重要的教训:三方法对比时,参数必须各自调到最优或者接近最优,不能偷懒用同一套参数跑三个方法。否则你比较的是“参数调得好不好”,而不是“方法本身适不适合这个对象”。
4. 三个非线性系统的仿真设计与对比结果
4.1 为什么选这三个系统
为了把CFDL、PFDL、FFDL的差异显性化,我设计了三个非线性系统,每个系统分别对应一种动态线性化方法的“主场”。
系统一采用强非线性基准模型:y(k+1)=y(k)/(1+y(k)²)+u(k)³。这个模型在非线性控制文献里非常常见,分母里的y²会让系统在输出较大时出现明显的饱和效应,而u的三次方让控制增益随输入幅度剧烈变化。它适合检验CFDL在纯非线性、无时滞、无明显输出记忆场景下的表现。
系统二加入输入时滞:y(k+1)=0.8y(k)-0.1y(k)²+0.5u(k-2)+0.2u(k-1)²。这个系统的控制信号要经过两步延迟才能真正影响输出,CFDL那种“当前输入变化直接映射到下一拍输出变化”的假设会被破坏,PFDL的历史输入窗口正好对这种时滞关系建模。
系统三同时包含输出历史记忆和非最小相位特征:y(k+1)=0.6y(k)-0.2y(k)·y(k-1)+0.4u(k)-0.3u(k-1)。输出不仅依赖自身当前值,还依赖前一拍的输出,形成输出记忆效应;控制通道里u(k)和u(k-1)系数一正一负,系统存在非最小相位特性。这种结构会让CFDL出现初始阶段的“反馈方向仿佛反了”的现象,而FFDL因为把Δy(k)纳入线性化窗口,理论上对输出记忆更敏感。
4.2 系统一结果:CFDL的“主场”
用上一章的Matlab框架,取N=500,参考轨迹为幅值0.5的方波。参数方面,CFDL取λ=0.5、ρ=0.6、η=1.0、μ=1.0、φ初值=1.0;PFDL取L=3、λ=0.6、ρ=0.5;FFDL取Lu=2、Ly=1、λ=0.7、ρ=0.5。系统一跑出来的结果是:三种方法都能稳定跟踪,CFDL的收敛速度最快,大概15步左右就能追上方波的第一个跳变;PFDL因为有窗口“记住”了过去的输入差分,在跳变处的超调稍微小一点,但调节时间略长;FFDL的效果跟PFDL接近。
从量化指标来看,CFDL的RMSE最低,大概在0.03量级,PFDL和FFDL要稍微高一些,但差距不大。控制量TV方面CFDL略高,因为它的模型形式简单,控制器需要更用力才能补偿误差;PFDL和FFDL因为有了历史信息,控制量会更平滑。这个结果印证了一个观点:系统不存在明显时滞或者输出记忆时,CFDL就是最经济的选择,算法简单、参数少、效果不差。
4.3 系统二结果:PFDL窗口的威力
系统二里u(k-2)和u(k-1)同时影响当前输出,CFDL的标量φ模型在描述这种延时关系时会出现什么后果?仿真里最直观的表现就是:CFDL在方波跳变之后先冲高,然后出现一段衰减振荡,严重时甚至会一直震荡到下一次跳变。原因在于CFDL把多步时滞的因果压缩到一个φ里,φ的估计值会不断抖动,试图“追上”真实的延时关系,但单标量模型做不到。
PFDL在这个系统上表现好了很多。L=3时,控制律分子里包含了Δu(k-1)和Δu(k-2)两项,正好对应了对象里u(k-2)和u(k-1)的时滞结构,相当于控制器内部有一个“延迟补偿器”。RMSE相比CFDL下降了大概一个数量级,TV也明显降低。FFDL的Lu=2、Ly=1效果跟PFDL接近,但参数多一个,调节成本略高,所以系统二我推荐优先用PFDL。
这里给一个补充经验:PFDL的窗口长度L并不是越大越好。我在系统二上试过L=5和L=8,结果L=8时RMSE不降反升。窗口过长会让伪梯度向量维度变大,估计器需要更多数据才能收敛,反而拖慢响应。L的选择以能覆盖对象的主要时滞步数为宜,一般取2到5就足够了。
4.4 系统三结果:FFDL的结构优势
系统三对CFDL是个不小的挑战。由于存在0.4u(k)-0.3u(k-1)这种非最小相位结构,控制器往目标方向调整时,输出会先往反方向冲一下。仿真里CFDL出现明显的“首拍反转”现象:方波从0跳到0.5时,CFDL算出的控制增量看起来是对的,但输出在第一步先向下掉了一点,才掉头向上追。这会让PPD估计器的方向感混乱,表现为前几十步振荡幅度大。
PFDL虽然加了输入窗口,但输出非最小相位的信息它捕捉不足,改进有限。FFDL把Δy(k)纳入线性化窗口后,控制器能感知到“上一拍输出变化”对当前输出的影响,伪梯度向量中针对输出差分的分量会自动学习到输出自身的历史依赖关系。结果上,FFDL的初始振荡幅度明显小于CFDL和PFDL,RMSE在三者里最低,TV也最低。这个系统是FFDL存在价值的最佳例证:对象本身含有输出记忆和非最小相位特征时,光扩展输入窗口是不够的,必须把输出差分也放进线性化结构里。
三个系统的定量结果总结如下表,数值只是我自己仿真中的典型值,不代表绝对结论:
| 系统 | CFDL RMSE | PFDL RMSE | FFDL RMSE | 结论 |
|---|---|---|---|---|
| 系统一:强非线性基准 | 0.031 | 0.042 | 0.040 | CFDL最优 |
| 系统二:输入时滞 | 0.186 | 0.024 | 0.026 | PFDL最优 |
| 系统三:输出记忆非最小相位 | 0.212 | 0.158 | 0.064 | FFDL最优 |
4.5 公平对比的硬要求
三方法横向对比有一个容易翻车的细节:模型初始化、参考轨迹、扰动序列必须完全一致。我见过有人复现时给CFDL一个初值,给PFDL另一个初值,这根本不是在对比算法。我自己的做法是:写一个统一的仿真入口函数,传入方法名和参数结构体,三方法共用一套随机种子、同一套参考轨迹、同一条对象初值。每个方法都先做参数扫描找到各自最优参数,再用最优参数跑20次带测量噪声的蒙特卡洛实验,最后取RMSE均值。这样得到的结果才能可靠地支撑“哪种方法更适合哪个系统”的结论。
5. 复现中踩过的坑与调试心得
5.1 索引错位:第一拍的数据为什么算不对
前面提到过,y(k+1)由u(k+1)驱动,但u(k+1)要由y(k)和目标算出来。很多初学复现的朋友把数组写成了y(k)=系统模型(u(k)),循环里从k=1更新u(k),再更新y(k),看起来好像能跑,但u和y在时间上差了一拍。这个错位在CFDL上后果明显:PPD估计里用的dy是“未来的输出增量”,而控制律用的误差是“当前的误差”,两者时间基准不一致,算法必乱。
我的习惯是先手动推一遍时间线。在纸上列出k=2时当前知道的量:u(1)、u(2)、y(1)、y(2)全部已知,可以估计PPD,然后算u(3),再算y(3)。循环里一切同步推进,代码逻辑跟手推一致后,再写成数组形式。代码里我特意把“预跑一拍”那段单独写出来,注释说明为什么存在,就是为了防止后面改参数时把这一步删掉。
5.2 PPD初值与重置:为什么一上来就发散
MFAC对PPD初值敏感,尤其在第一拍、第二拍数据信息不足的时候。φ初值取1.0和取5.0,前几十步的跟踪曲线完全像两个控制器。如果φ初值符号跟对象在当前工作点的真实梯度方向相反,控制器打出来的方向就是错的,输出会往目标的反方向跑。这也是为什么重置机制那么重要:一旦估计出的φ太小或符号漂移,就把它拉回到初值,相当于给算法一次“重新判断方向”的机会。
我在试参数时发现,重置阈值ε不能设得太大,否则φ频繁重置,控制动作看起来像在反复重启,效果反而不如不重置;也不能太小,否则失去了对除零崩溃的保护。ε取1e-5到1e-4是我目前试下来比较稳的范围。初值的选取说到底还是靠对对象的粗估:如果你知道控制通道的增益大致方向是正的,φ初值取正数;如果系统存在未知非最小相位,那就多试几组初值,取RMSE最小的那一组作为结果。
5.3 λ和ρ要一起调,别只动一个参数
λ和ρ在控制律里一个在分母,一个乘在整个分子前面,它们共同决定了控制增量的幅度。很多人调参时只看λ,觉得λ大了响应慢,就一个劲减小λ,结果控制量剧烈抖动,反而更不稳。实际上当λ取到0.01以下时,相当于完全不限制控制增量,这时候ρ稍微大一点就会把系统推向振荡。我的调参顺序是:先把ρ固定为0.5,λ从0.1开始逐步调大,找到跟踪又不抖的区间;然后固定这个λ,把ρ从0.1往上微调,每次增加0.1,观察RMSE和控制量TV的变化。一般ρ在0.3到0.7之间就能满足大多数非线性系统,没必要取1.0。
这里给一个判断技巧:如果跟踪曲线出现高频毛刺,先把λ调大一点,哪怕响应慢一点也没关系;如果曲线平滑但稳态偏差明显,再动ρ来加速收敛。这样在调试台上不会出现“调了半天不知道哪个参数起作用”的混乱状态。
5.4 时滞系统的“伪发散”现象
系统二用CFDL跑的时候,前几十步看起来要发散,但如果你把仿真时间拉长到1000步,它其实能自己慢慢稳定下来。这就是我所说的“伪发散”。原因是CFDL为了补偿无法描述的时滞,φ的估计值会不断剧烈变化,短期看控制量乱跳,但投影算法在长时间下依然能勉强维持稳定。遇到这种情况别急着改代码,先把仿真时间加长,再看PPD的轨迹,你会发现φ在反复横跳,这是模型结构不匹配的信号,换成PFDL就能让φ安静下来。这个诊断方法比直接看输出曲线有用得多。
5.5 从Matlab移植到Python的体验
复现完Matlab版本后,我把同一套框架移植到了Python。这纯粹是为了后续方便集成到别的实验环境里。移植过程挺快,核心逻辑一一对应:Matlab的数组从1开始,Python从0开始,预跑一拍的逻辑要相应调整;ppd_update系列函数变成普通的Python函数,循环结构完全一致。Python版跑同样的系统一,500步仿真用时大概几十毫秒,速度不是问题。
唯一让我注意的是数值细节:Matlab和Python的浮点行为在极少数边界条件下可能会有细微差异,比如当μ取得特别小、分母接近零时,两个平台的舍入误差可能会让PPD更新值差出几位小数。解决办法是把ε阈值略微调大一点,避免进入极端数值区间。这也算是从Matlab往其他语言移植MFAC时的一个通用经验。
最后说点个人体会
MFAC最吸引我的地方,是它把“控制”这件事从“懂模型才能动手”的束缚里解放出来了。你不需要写出对象的动力学方程,不需要辨识参数,只需要一个合理的输入输出数据流,再加上一个自律的、会自我修正的伪偏导数估计器,就能把控制品质做出来。但这种自由是有代价的:你必须接受“模型结构不匹配”这个概念的存在,并且学会用重置机制、参数调节和窗口设计去消化它。
我复现CFDL、PFDL、FFDL这一圈下来的最大感受是,这三个方法不是谁替代谁的关系,而是一套从简单到一般的工具箱。遇到新对象先上CFDL快速摸底,看到时滞特征再用PFDL,发现输出记忆与非最小相位特征明显时果断换FFDL,这个递进思路比死记公式有用得多。如果你也在复现MFAC,我建议不要只跑通一套代码就收手,而是把三个对象、三个方法、一组指标全部做成统一的对比实验,那才是真正把动态线性化这件事搞懂了。
