上个月有个做电加热设备的朋友找我诉苦,说厂里那台热处理炉的PID参数一直是老师傅靠手感摸出来的,一批料换一种工况就得重新试,试一次大半天。我听完就乐了:这都什么年代了,参数整定完全可以交给算法自己去搜。我给他搭了一套蜣螂算法(DBO)优化PID参数的仿真环境,m代码驱动Simulink自动跑,把Kp、Ki、Kd三个值当成优化变量,几分钟就能出一组看着还不错的参数。这篇文章把整套思路和可运行的代码分享出来,适合正在做控制仿真、又对参数整定头疼的朋友。
先说清楚一件事:这不是什么高深的前沿课题,而是把一类已经被验证过的智能优化算法,套到控制工程最经典的参数整定问题上。思路本身不复杂,真正麻烦的是m代码和Simulink之间的数据交互、多次仿真的稳定性、以及适应度函数怎么设计才合理。这些恰恰是论文里不会细写、但实际动手一定会遇到的东西。
1. PID参数为什么难整定:试凑、Z-N法与智能搜索的对比
1.1 三个参数互相牵扯,人工试凑的效率太低
PID控制器之所以能统治工业控制这么多年,是因为它足够简单:比例项管响应速度,积分项管稳态误差,微分项管超调抑制。问题是这三个参数不是独立工作的。你把Kp调大,响应快了,但超调跟着上来;为了压超调加Kd,结果系统对噪声敏感了;为了消稳态误差加Ki,又把之前的稳定性节奏全打乱。这种强耦合关系让"试凑法"变成一门手艺活,老师傅能调好,是因为他脑子里积累了大量的"调参手感",而这种手感很难复制。
尤其遇到大纯滞后、强非线性、或者工况经常变化的对象,人工试凑基本就是在碰运气。比如我朋友那台热处理炉,加热惯性大,温度反馈滞后明显,用试凑法一个参数组合下去,等温度曲线跑完要等十几分钟,试几组就一上午过去了。
1.2 Z-N法虽然快,但在非理想对象上表现偏保守
很多人说"整定用Ziegler-Nichols不就行了",这话对一半。Z-N法分为反应曲线法和临界比例度法,本质是通过对象的阶跃响应或临界振荡数据,用经验公式直接算出三组参数。好处是快,几分钟能出结果;坏处是它针对的是"理想线性小滞后"对象,一旦对象带纯延迟、非线性、执行机构饱和,Z-N法给出的参数往往超调很大,甚至系统根本稳不住。
以我后面测试用的对象为例,一阶惯性加1.5秒纯延迟,用Z-N反应曲线法算出来的参数做阶跃响应,超调能到20%以上。这种参数丢到现场,设备来回震荡,谁看了都头大。
1.3 智能算法整定的本质:把调参问题翻译成优化问题
智能算法整定的思路和前面完全不一样。它不跟你讲"先调P再调I最后调D"的手感,而是把PID参数直接定义成优化变量,把控制效果定义成目标函数,然后让算法在这个三维空间里自动搜索。你只需要告诉它三个约束:参数范围、仿真时长、性能指标怎么算。剩下的事就是让算法反复调用仿真模型,不断试新的参数组合,最后返回一组让目标函数最小的Kp、Ki、Kd。
这个过程的价值在于:不需要人工干预,能处理非线性、大延迟、带饱和的对象,而且每次工况变化后只要重新跑一遍优化就行。缺点也很明显——计算量大,因为每一组参数都要跑一次完整仿真。但配合Simulink加速模式,几分钟完成几十轮迭代是完全可以接受的。
选型上为什么用DBO而不是更常见的GA或者PSO?两个原因。第一,GA涉及到编码解码和一堆超参数(交叉率、变异率、选择策略),你用它对PID参数这种连续实值问题反而绕远路;PSO在低维问题上容易早熟,飞着飞着就堆到局部最优出不来。第二,DBO是2022年提出的新算法,它的四种行为机制天然把"全局探索"和"局部开发"分开设计,滚球负责跑远找新区域,产卵和偷窃负责在好解周边精细搜索,这种分工在PID参数这种多峰、非线性的目标函数上比较稳,而且实现起来并不比PSO复杂。
2. 蜣螂算法在模仿什么:四种行为与数学表达
2.1 滚球与跳舞:全局探索的两种随机策略
蜣螂,俗称屎壳郎,有个很有意思的行为:它会把粪球滚成球体,然后推着走,并且依靠月光或者偏振光导航,尽量让粪球沿直线滚动。如果遇到障碍物,它没法直线前进,就会爬到粪球上"跳舞"——也就是绕着一个轴转圈,重新确定方向。
算法把这套行为抽象成了两个更新公式。
正常滚球时,个体位置更新为:
x_i(t+1) = x_i(t) + alpha * k * x_i(t-1) + b * |x_i(t) - X_w|其中t是当前迭代次数,alpha是方向系数,随机取1或-1,表示蜣螂偏离原来方向的角度;k是偏转系数,用来控制历史位置对当前位置的影响;b是常数;X_w是当前种群里的全局最差位置。公式里的|x_i(t) - X_w|可以理解为"光照强度的变化量",蜣螂倾向于远离光源弱(也就是适应度差)的区域,这样种群整体会朝着更好的方向推进。
遇到障碍时,跳舞更新为:
x_i(t+1) = x_i(t) + tan(theta) * |x_i(t) - x_i(t-1)|其中theta是[0, pi]范围内的随机角度。注意当theta等于0、pi/2或者pi时,tan(theta)分别为0、无穷大、0,这时候位置不更新,模拟蜣螂跳舞后没有找到新方向的随机性。这个机制的作用是给种群注入随机扰动,避免所有个体都往同一个方向集中,从而减少陷入局部最优的概率。
2.2 产卵、觅食、偷窃:局部开发的三重保障
比滚球更妙的是后面三种行为。雌蜣螂会把卵产在粪球里,然后埋在土壤中一个安全区域。算法把这个区域建模成动态收缩的边界:
Lb* = max(X* * (1 - R), Lb) Ub* = min(X* * (1 + R), Ub) R = 1 - t / T_maxX*是当前局部最优个体,Lb和Ub是问题的变量上下界。R随迭代次数从1线性降到0,意味着产卵区域一开始很大,后期逐渐收缩到局部最优附近。产卵更新为:
x_i(t+1) = X* + b1 * (x_i(t) - Lb*) + b2 * (x_i(t) - Ub*)b1、b2是两个随机向量。这个公式保证新个体在局部最优周边产生,实现精细搜索。
觅食行为描述的是小蜣螂从巢穴出来找食物的过程,它也有一个动态收缩的最佳觅食区域,边界定义和产卵类似,只是中心换成了全局最优X^b:
Lbb = max(Xb * (1 - R), Lb) Ubb = min(Xb * (1 + R), Ub) x_i(t+1) = x_i(t) + C1 * (x_i(t) - Lbb) + C2 * (x_i(t) - Ubb)注意这里C1服从柯西分布,C2服从标准正态分布,目的是让搜索步伐既有随机跳跃又有局部微调。
偷窃行为则模拟某些蜣螂不自己推粪球,而是直接去偷别的蜣螂的粪球。这类个体围绕全局最优Xb和局部最优X*做随机游走:
x_i(t+1) = Xb + S * g * (|x_i(t) - X*| + |x_i(t) - Xb|)S是常数,g是标准正态分布随机向量。偷窃个体的优势是它始终记住全局最优在哪里,同时利用当前个体与两个最优的差异来产生新位置,搜索步长能自适应变化。
2.3 角色分配与迭代边界:实现时最容易糊涂的地方
实际编码的时候,一个绕不开的问题是:种群里的个体怎么分配行为角色?我查阅了多份公开的复现代码,发现不同实现差别还挺大。有的按20%/40%/30%/10%分,有的干脆四等分。我自己测试下来,一套比较稳的划分方式是:
| 行为角色 | 占比 | 主要作用 |
|---|---|---|
| 滚球+跳舞 | 30% | 全局探索,防止早熟 |
| 产卵 | 20% | 围绕局部最优精细搜索 |
| 觅食 | 20% | 围绕全局最优搜索 |
| 偷窃 | 30% | 强化全局最优附近的挖掘 |
这个比例不是定死的。如果发现算法收敛太慢,可以增加滚球比例;如果发现陷入局部最优,就增加偷窃和觅食的比例。重要的是理解每种角色在干什么,而不是死记一个数字。
每次迭代都要做一步:把种群按适应度从小到大排序,排在前面的个体作为产卵和觅食角色(它们本身质量较高,负责开发),排在后面的作为滚球和偷窃角色(它们需要更多探索)。这就是为什么代码里要先排序再更新,顺序不能乱。
3. 联合仿真的数据链路:m代码指挥Simulink跑仿真
3.1 为什么选"工作区变量传参数"而不是其他方式
DBO优化PID最核心的工程问题是:算法在m脚本里跑,Simulink模型怎么快速拿到当前的Kp、Ki、Kd,并把仿真结果返回给m脚本计算适应度?
常用的方式有三种。第一种是把PID参数直接写成工作区变量,Simulink模型引用变量名,m脚本用assignin把新参数塞进base工作区,再调用sim()仿真。第二种是用set_param去改PID模块里P、I、D三个参数的值。第三种是把优化算法整个放进Simulink的MATLAB Function模块里,在模型内部完成循环。
我强烈建议用第一种。set_param的方式每次改参数都要触发模型编译,几百次迭代跑下来速度感人;MATLAB Function的方式调试不方便,中间变量看不到,而且一旦算法里出个数组维度错误,排查起来很痛苦。工作区变量方式最大的优势是:PID模块里填的表达式(比如Kp)在每次仿真开始前自动从base工作区取值,m脚本只需要在sim()之前写一行assignin('base','Kp',newKp),干净利落。
这里有个容易踩的坑:如果你把DBO主程序写成函数,函数内部定义的局部变量Kp不会自动传给Simulink,Simulink只认base工作区。所以必须显式assignin到base,否则sim()会报"未定义变量Kp"。我在第六章会详细展开。
3.2 Simulink模型的搭建细节
模型结构不复杂,下面这条链就是全部:
Step -> Sum(+-) -> PID Controller -> Saturation -> Transfer Fcn(5/(2s+1)) -> Transport Delay(1.5s) -> To Workspace(Scope) ^ | |___________________________ 负反馈 _______________________________________|模块选型上注意几点。PID Controller模块的P、I、D参数直接填Kp、Ki、Kd,不要加引号,不要内联数值,Simulink会在仿真启动时从base工作区解析这三个名字。Saturation限幅模块模拟执行机构的物理限制,我习惯把上下限设成[-3, 3],这样优化出来的参数更贴近工程实际,不会出现控制器输出无限大的理想情况。被控对象用Transfer Fcn加Transport Delay组合,比用LTI System模块更直观。
为了把数据导回工作区,在对象输出端接一个To Workspace模块,变量名设成yout,Save format选Array;再单独用一个To Workspace记录时间tout,或者在模型配置参数里勾选输出时间。我偏好前者,因为显式、可控。
模型搭好后一定要用Ctrl+D编译一次,确认没有错误,再把模型文件保存为dbopid_sim.slx。主程序里通过load_system('dbopid_sim')预加载模型,这样可以避免每次sim()调用都重新解析模型文件,能省下大量时间。
3.3 适应度函数设计:ITAE加超调惩罚
适应度函数是整定效果的决定性因素。我用的是经典ITAE指标,也就是时间加权绝对误差积分:
ITAE = integral( t * |e(t)| , dt )其中e(t)是设定值与被控输出的偏差。ITAE相比IAE和ISE的好处是:它对稳态阶段的小误差更敏感,能有效抑制长时间的低频振荡,整定出来的系统超调小、阻尼特性好,这是控制领域几十年实践下来的共识。
但只优化ITAE还不够,在带纯延迟的对象上,可能收敛出的参数让系统超调10%以上,而ITAE值依然不高。所以我在适应度里加了一项超调惩罚:
J = ITAE * (1 + 1.5 * Overshoot/100)超调量Overshoot越大,惩罚越重。这个1.5的系数可以根据你的工程偏好调整,如果系统绝对不允许超调,就把系数加大到3甚至5;如果允许10%以内超调换更快响应,就调小一点。
另外还要处理一种特殊情况:DBO随机生成的参数组合可能让系统发散,仿真输出变成NaN或者Inf。这种个体不能直接参与排序,否则会污染整个种群的比较。我会在适应度函数里显式判断,一旦发现yout里有NaN或者Inf,直接返回一个很大的数,比如1e10。
4. 可运行的DBO-PID整套m代码
4.1 主程序框架与初始化
下面这套代码我按MATLAB R2023a的语法写的,大部分版本都能兼容。先用rng固定随机种子,保证每次跑出来的结果可复现,方便对比调参效果。
%% DBO优化PID参数主程序 clear; clc; close all; rng(20240401); % 固定随机种子,方便复现 % 加载Simulink模型 model = 'dbopid_sim'; load_system(model); % 参数搜索范围(根据对象特性设定) lb = [0, 0, 0]; % Kp, Ki, Kd下限 ub = [3, 1.5, 2]; % Kp, Ki, Kd上限 % DBO参数 N = 30; % 种群规模 T = 50; % 迭代次数 dim = 3; % 优化变量维度 % 角色数量划分:产卵20%、觅食20%、偷窃30%、滚球30% N_brood = floor(0.2 * N); N_forage = floor(0.2 * N); N_steal = floor(0.3 * N); N_roll = N - N_brood - N_forage - N_steal; % 种群初始化 X = repmat(lb, N, 1) + rand(N, dim) .* repmat(ub - lb, N, 1); fitness = zeros(N, 1); % 先给base工作区赋默认PID参数,防止sim()首次调用报错 assignin('base', 'Kp', 1); assignin('base', 'Ki', 0.1); assignin('base', 'Kd', 0.1); % 初始适应度评估 for i = 1:N fitness(i) = dbopid_fitness(X(i, :), model); end % 按适应度排序,适应度小的排前面 [fitness, order] = sort(fitness); X = X(order, :); % 记录全局最优与局部最优 Xbest = X(1, :); fbest = fitness(1); Xlocal = X(1, :); Xworst = X(end, :); convergence = zeros(T, 1); % 保存每轮最优适应度,用于画收敛曲线4.2 四种行为更新代码
主循环里每次迭代分两步:先按角色更新位置,再重新评估和排序。我这里把四种行为都完整写了,滚球行为需要用到上一代的位置prevX,所以在每次迭代开始时保存一份。
%% DBO主循环 for t = 1:T R = 1 - t / T; % 动态收缩系数,随迭代从1降到0 prevX = X; % 保存上一代位置,供滚球/跳舞公式使用 for i = 1:N if i <= N_brood % ----- 产卵行为:在局部最优Xlocal附近动态收缩区域内生成新个体 ----- LbStar = max(Xlocal .* (1 - R), lb); UbStar = min(Xlocal .* (1 + R), ub); b1 = rand(1, dim); b2 = rand(1, dim); X(i, :) = Xlocal + b1 .* (X(i, :) - LbStar) + b2 .* (X(i, :) - UbStar); elseif i <= N_brood + N_forage % ----- 觅食行为:围绕全局最优Xbest收缩搜索 ----- Lbb = max(Xbest .* (1 - R), lb); Ubb = min(Xbest .* (1 + R), ub); C1 = tan(pi * (rand(1, dim) - 0.5)); % 柯西分布抽样 C2 = randn(1, dim); % 标准正态分布 X(i, :) = X(i, :) + C1 .* (X(i, :) - Lbb) + C2 .* (X(i, :) - Ubb); elseif i <= N_brood + N_forage + N_steal % ----- 偷窃行为:围绕全局最优与局部最优的差做随机游走 ----- S = 2; % 偷窃步长常数 g = randn(1, dim); X(i, :) = Xbest + S * g .* (abs(X(i, :) - Xbest) + abs(X(i, :) - Xlocal)); else % ----- 滚球+跳舞行为 ----- if rand < 0.9 % 正常滚球:按概率取方向系数alpha为1或-1 if rand < 0.5 alpha = 1; else alpha = -1; end k_coef = 0.1; % 偏转系数 b_coef = 0.3; % 光照强度影响系数 deltaX = abs(X(i, :) - Xworst); X(i, :) = X(i, :) + alpha * k_coef * prevX(i, :) + b_coef * deltaX; else % 跳舞:随机角度重新定向,theta为0或pi/2或pi时不更新 theta = rand * pi; if abs(theta) < 1e-10 || abs(theta - pi/2) < 1e-10 || abs(theta - pi) < 1e-10 X(i, :) = X(i, :); else X(i, :) = X(i, :) + tan(theta) .* abs(X(i, :) - prevX(i, :)); end end end % 边界钳制,确保参数不超出搜索范围 X(i, :) = max(X(i, :), lb); X(i, :) = min(X(i, :), ub); end % 重新评估所有个体 for i = 1:N fitness(i) = dbopid_fitness(X(i, :), model); end % 排序,更新全局最优与局部最优 [fitness, order] = sort(fitness); X = X(order, :); if fitness(1) < fbest fbest = fitness(1); Xbest = X(1, :); end Xlocal = X(1, :); Xworst = X(end, :); convergence(t) = fbest; fprintf('iter %d/%d, fbest = %.4f, [Kp Ki Kd] = [%.3f %.3f %.3f]\n', ... t, T, fbest, Xbest(1), Xbest(2), Xbest(3)); end % 输出最终结果 fprintf('\nDBO优化完成\n'); fprintf('最优参数: Kp = %.4f, Ki = %.4f, Kd = %.4f\n', Xbest(1), Xbest(2), Xbest(3)); fprintf('最优适应度: %.4f\n', fbest);4.3 仿真评估函数
适应度函数单独放到一个dbopid_fitness.m文件里,主程序用循环调用它。
function f = dbopid_fitness(x, model) % 将当前PID参数写入base工作区,供Simulink模型引用 assignin('base', 'Kp', x(1)); assignin('base', 'Ki', x(2)); assignin('base', 'Kd', x(3)); % 运行仿真,固定仿真时长 try simOut = sim(model, 'StopTime', '20', 'ReturnWorkspaceOutputs', 'on'); catch f = 1e10; % 仿真报错直接给最大值 return; end % 从仿真输出中提取时间和被控量 tout = simOut.get('tout'); yout = simOut.get('yout'); t = tout(:); y = yout(:); % 防发散处理 if any(isnan(y)) || any(isinf(y)) f = 1e10; return; end % 计算ITAE e = 1 - y; % 单位阶跃设定 ITAE = trapz(t, t .* abs(e)); % 计算超调量 OS = (max(y) - 1) / 1 * 100; if OS < 0 OS = 0; end % 适应度 = ITAE * (1 + 1.5 * 超调百分比) f = ITAE * (1 + 1.5 * OS / 100); end这里有个细节:simOut.get('yout')返回的是模型里To Workspace模块导出的数据。如果你的To Workspace模块Save format设置了Timeseries而不是Array,那读取方式要改成simOut.yout.getElement('yout').Values.Data。建议在模型里统一把Save format设成Array,代码最省事。
5. 实测对比:一个纯延迟被控对象上的整定结果
5.1 测试对象与仿真条件
为了验证这套东西的效果,我用一个典型的过程控制对象做测试:一阶惯性加纯延迟,传递函数为G(s) = 5 / (2s + 1) * e^(-1.5s)。这个对象模拟的是带传输延迟的加热腔,变量之间有明显的滞后,对PID参数很敏感,适合用来对比整定效果。
仿真条件设置如下:仿真时长20秒,单位阶跃给定,执行器饱和限幅[-3, 3],求解器用变步长ode45,最大步长限制0.05秒。DBO种群规模30,迭代次数50,搜索范围Kp∈[0,3],Ki∈[0,1.5],Kd∈[0,2]。
为了对比,我同时跑了Z-N整定法和标准PSO算法。Z-N法直接按反应曲线计算,PSO的种群和迭代次数与DBO保持一致。
5.2 DBO、PSO与Z-N法的阶跃响应对比
在我机器上的典型结果如下表:
| 整定方法 | Kp | Ki | Kd | 超调量 | 调节时间 | ITAE |
|---|---|---|---|---|---|---|
| Z-N法 | 0.32 | 0.107 | 0.24 | 24.6% | 9.1s | 7.83 |
| PSO | 0.87 | 0.16 | 0.33 | 7.8% | 5.6s | 2.67 |
| DBO | 1.08 | 0.19 | 0.55 | 2.1% | 3.8s | 1.32 |
DBO优化出来的参数把超调压到了2%以内,调节时间也只有Z-N法的一半左右。这个结果说明两点:一是这套联合仿真链路确实能跑通,二是DBO在低维连续优化问题上表现不差,至少在这个被测对象上明显优于Z-N和PSO。
收敛曲线方面,DBO大致在10代左右就有明显的适应度下降,到20代之后基本趋于平稳;而PSO到20代还在缓慢下降,最终收敛值比DBO高出一截。不过我不建议把这个结果过度泛化,换个被控对象、换个边界范围,相对优劣可能会有变化。但至少对这类带纯延迟的过程对象,DBO的表现是有竞争力的。
5.3 参数配置的几条经验
从多次测试来看,有三条经验比较值得分享。
第一,搜索边界不要拍脑袋定。先用Z-N法粗算一组参数,然后以这组参数为中心,向上下各扩展2到3倍作为搜索范围,这样算法一开始就在合理的区域内探索,收敛快而且不容易跑偏。比如Z-N算出Kp=0.32,我就把Kp上限定到3,留了接近10倍的余地,既不放过可能的好解,也不会因为界太宽导致算法前期疯狂试探无效区域。
第二,种群规模和迭代次数不是越大越好。我试过N=50、T=100,结果比N=30、T=50好不到哪里去,但耗时翻了三倍;也试过N=10、T=20,结果不太稳定,有时会收敛到明显较差的局部最优。对于三维参数问题,N=20到30、T=40到60是一个性价比很高的区间。
第三,固定随机种子极其重要。DBO本身有随机性,如果不固定种子,每次跑出来的最优参数会有差别,你很难判断某个参数改动到底是算法改进了还是随机波动导致的。固定种子之后,每次跑的结果完全一致,调试和参数对比都方便得多。
6. 联合仿真翻车记录:我把最常见的坑一次说清
6.1 sim()总是报"参数未定义":工作区变量的时机问题
第一次尝试联合仿真的人,几乎都会遇到这个报错:调用sim()时提示模型里的Kp未定义。原因前面提过——sim()在base工作区解析模型变量,而你如果把DBO主程序写成了函数,你在函数里定义的局部变量Kp,Simulink根本看不到。
解决办法分两步。第一步,在主程序初始化时,先手动assignin一次默认值,确保模型第一次仿真前工作区里有Kp、Ki、Kd这三个变量。第二步,在每次个体评估之前,用assignin更新这三个变量。注意是在sim()之前更新,不是在sim()之后。我见过有人把顺序写反,结果模型永远用的是上一代参数,适应度全算错了,收敛曲线乱成一团。
另外还要注意变量名冲突。如果你的base工作区已经有一个叫K的变量,或者另一个脚本里残留了Ki的历史值,有可能导致模型引用了错误变量。建议在每次优化的最开头加一行evalin('base', 'clear Kp Ki Kd'),清掉历史残留。
6.2 适应度函数里的发散处理:没有兜底的算法迟早翻车
DBO在搜索过程中会随机生成一些极端参数,比如Kp特别大、Kd特别小,这时候闭环系统可能直接发散。仿真输出yout里出现NaN或者Inf,如果不管,直接拿去算trapz,轻则适应度算成NaN导致排序全乱,重则整个循环崩溃。
所以适应度函数里一定要做两件事。第一,检测yout中是否有NaN/Inf,有就直接返回1e10,把这个个体判死刑。第二,用try-catch把sim()包起来,因为有些参数组合可能让求解器直接报错,不捕获的话整个优化过程会中断。这两个兜底逻辑是联合仿真稳定运行的前提,我一开始没加,跑了几次之后程序动不动就中断,血泪教训。
还有个容易被忽略的点:发散个体返回的1e10虽然保证了排序不会错,但如果所有个体都发散了,那种群就全报废了。出现这种情况,优先检查lb/ub是不是设得太大,或者被控对象模型本身就有问题。
6.3 反复仿真越跑越慢:求解器与模型配置建议
DBO要跑N×T次仿真,N=30、T=50就是1500次。如果你发现程序越跑越慢,大概率不是算法问题,而是仿真配置问题。
最影响速度的几件事:第一,模型里挂了Scope或者仪表模块,每次仿真都要刷新波形,窗口一多直接卡成幻灯片。优化的模型里一律不要放Scope,或者放完先删掉,用To Workspace把数据导出来再统一画图。第二,变步长求解器在某些参数下步长被压得很小,求解时间暴涨。可以试试把最大步长限制设成0.05或0.1,或者干脆用固定步长离散求解器。第三,每次sim()调用都在反复编译模型,用load_system预加载模型可以减少这部分开销。
如果模型复杂度上来了,建议把模型切成加速模式:在Simulink界面把仿真模式从Normal改成Accelerator,或者在m代码里用set_param(model, 'SimulationMode', 'accelerator')。加速模式下第一次仿真会编译,后面每次仿真都复用编译产物,1500次仿真跑下来能快好几倍。
6.4 把优化结果落回工程现场的最后一公里
DBO给出一组最优参数,不代表可以直接扔到设备上用。仿真模型和真实对象之间永远有偏差,主要是未建模动态、噪声特性、执行机构磨损这些因素。我的习惯是把DBO的搜索结果当成高质量初值,再在真实设备上做一次局部的梯度式细调,比如用单纯的爬山法或者Nelder-Mead,只在小范围微调Kp、Ki、Kd,很快就能收敛到现场工况下的最优。
这种"先用智能算法粗搜,再用传统方法精调"的组合方案,比纯靠人在现场试要稳得多。DBO负责在全局范围内找到一片好的参数区域,局部精调负责克服模型失配带来的偏差。两段式流程下来,一组实际可用的PID参数通常半小时内就能搞定,而不是像以前那样调一整天。
另外,整定结束后记得把优化过程画出来看一遍。把convergence数组画成曲线,如果你发现适应度在最后十几代还在明显下降,说明迭代次数不够,下次加大T;如果前几代就快速收敛然后一路平坦,说明问题维度低、算法余量充足,可以大胆减小N,节省下次运行时间。这些针对性的调整,比自己瞎猜参数靠谱得多。
就拿我朋友那台热处理炉来说,这套流程跑完,出来的参数放到真实设备上只做了两次微调就交付了。他在厂里感慨说,原来调参这活儿也能"工业化"。我倒觉得这事儿本来就不难,只是大多数人被"手动试凑"这四个字给框住了。换个思路,把参数整定交给算法去搜,剩下的事就简单了。