说实话,PID参数调了好几年,我到现在看到“整定”两个字还是有点头皮发麻。尤其是碰到那种大惯性、带纯滞后、还时不时给你来个非线性的被控对象,一套Kp、Ki、Kd翻来覆去地在调试软件里敲,常常一上午就耗进去了。后来我把粒子群算法(PSO)搬过来做PID参数寻优,效果比我预想的好很多,这篇文章就把整套思路、代码和踩过的坑一次讲清楚。
如果你正被PID参数折磨,或者听说了粒子群算法但不知道怎么用在工程里,这篇文章适合你。我会先讲为什么要用PSO去驯服PID,再拆解粒子群算法的本质和数学原理,然后给出可以直接抄作业的Python实现,最后聊聊参数怎么设、坑怎么避。文章里所有代码和策略我都实测过,不是拿来看的,是真的能跑。
1. 先说说PID调参这档子事
1.1 PID参数为什么这么难调
PID控制器的核心就三个参数:比例系数Kp、积分系数Ki、微分系数Kd。Kp负责对当前误差作出响应,误差大输出就猛;Ki负责把历史误差累积起来,消除稳态偏差;Kd负责感知误差变化趋势,提前抑制超调。听起来分工明确,但真到调试现场,三者之间是互相牵制的。
举个例子,你调高Kp,系统响应确实变快了,但超调量蹭蹭往上涨,甚至开始振荡。你想用Kd把超调压下去,可微分项对测量噪声特别敏感,现场传感器信号稍微毛刺多一点,执行机构就开始抖动。这时候你又得照顾Ki,Ki太大会积分饱和,超调更严重。这三个参数共同决定闭环极点位置、稳定裕度和抗扰性能,本质是一个高维非线性优化问题。
更头疼的是,实际被控对象很少是教科书里那种干干净净的一阶惯性环节。电机带负载、阀门带死区、加热炉有纯滞后、无人机悬停有强耦合,对象特性一变,之前调好的参数可能全部作废。靠人工试凑面对这种场景,基本就是碰运气。
1.2 常规调参手段的痛点
传统方法里,Ziegler-Nichols整定公式是最常用的,我刚开始学PID的时候就靠这个公式应付课程设计。它的思路是先让系统临界振荡,记录临界增益Ku和振荡周期Tu,然后按公式算出三参数。这套方法对中低阶线性对象效果尚可,但对大滞后对象、高阶对象,算出来的参数往往太激进,还要人工二次修正。
后来有用频域法的,比如相位裕度法、幅值裕度法,主要靠奈奎斯特图和Bode图来指导设计。这类方法数学功底要求高,而且本质上还是针对线性时不变系统,遇到非线性、时变对象就有点使不上劲。
再后来有人用梯度下降法去优化PID指标,比如以ITAE为损失函数,对Kp、Ki、Kd求梯度。听起来很合理,但PID误差指标对参数的空间往往是非凸的,有大量局部极值。梯度下降特别容易陷在某个局部最优里出不来,最后调出来的参数你用肉眼都看得出不对劲。
于是我把目光投向群体智能算法。粒子群算法不要求目标函数可导,不要求系统线性,它只需要能算出“这组参数好不好”的评分,然后一群粒子在参数空间里你追我赶地搜索。这不就是为PID调参量身定做的吗?
2. 粒子群算法到底是个什么东西
2.1 鸟群觅食给了我们什么启发
粒子群算法的思想最早来自对鸟群觅食行为的观察。假设一群鸟在一片区域里随机找食物,谁都不知道食物在哪里,但每只鸟知道自己当前离食物有多远,也知道同伴中目前离食物最近的那只鸟的位置。于是鸟群的策略很简单:每只鸟既参考自己历史上离食物最近的位置,也参考整个群体目前找到的最好位置,然后调整自己的飞行方向。
把这种机制搬进计算机,食物就是最优解,每只鸟就是一个“粒子”,粒子在解空间里的位置就是一组候选解。粒子每飞到一个新位置,就计算一次适应度值(相当于离食物的距离),然后更新两个记忆:个体历史最优pbest和群体历史最优gbest。下一次移动时,粒子会同时朝着pbest和gbest的方向飞,同时保留一部分自己的惯性。
这种机制妙在它不需要梯度信息,不需要连续可导,只要你能给出一个评分函数,它就能在巨大的搜索空间里找到让人满意的解。对PID来说,评分函数就是系统响应曲线的综合表现。
2.2 粒子群算法的数学表达
每个粒子在迭代过程中有两个核心属性:位置Xi和速度Vi。在t+1代,粒子的速度和位置按下面两个公式更新:
速度更新:
V_i(t+1) = w * V_i(t) + c1 * r1 * (pbest_i - X_i(t)) + c2 * r2 * (gbest - X_i(t))位置更新:
X_i(t+1) = X_i(t) + V_i(t+1)这些符号的含义有必要掰开揉碎讲清楚。
- w是惯性权重,控制上一代速度对当前速度的影响程度。w大,粒子惯性大,擅长全局探索;w小,粒子容易受个体和群体最优吸引,擅长局部开发。
- c1和c2是学习因子,c1控制粒子飞向自身历史最优位置的加速度,c2控制粒子飞向群体最优位置的加速度。
- r1和r2是[0,1]区间均匀分布的随机数,给搜索过程增加随机性,避免粒子反复走同一条路。
- pbest_i是粒子i自己历史中适应度最好的位置。
- gbest是整个粒子群体目前发现的全局最优位置。
三个速度分量可以理解成三种“冲动”:第一项是保持自己原来的运动趋势,第二项是怀念自己曾经的最佳状态,第三项是向目前最强的同伴看齐。这三种冲动加权组合,决定了粒子下一步怎么飞。
2.3 一个手算例子帮你彻底看透迭代过程
光看公式容易懵,我手写一个二维小例子走一遍流程。假设粒子当前位置X = (2.0, 1.0),当前速度V = (0.5, 0.3),该粒子的历史最优pbest = (1.5, 1.2),群体最优gbest = (1.0, 1.0)。设置w = 0.7,c1 = c2 = 1.5,随机数r1 = (0.4, 0.6),r2 = (0.8, 0.3)。
先算第一维的新速度分量:
w * V_x = 0.7 * 0.5 = 0.35 c1 * r1 * (pbest_x - X_x) = 1.5 * 0.4 * (1.5 - 2.0) = -0.3 c2 * r2 * (gbest_x - X_x) = 1.5 * 0.8 * (1.0 - 2.0) = -1.2 V_x_new = 0.35 - 0.3 - 1.2 = -1.15再算第二维:
w * V_y = 0.7 * 0.3 = 0.21 c1 * r1 * (pbest_y - X_y) = 1.5 * 0.6 * (1.2 - 1.0) = 0.18 c2 * r2 * (gbest_y - X_y) = 1.5 * 0.3 * (1.0 - 1.0) = 0 V_y_new = 0.21 + 0.18 + 0 = 0.39更新后的位置:
X_x_new = 2.0 + (-1.15) = 0.85 X_y_new = 1.0 + 0.39 = 1.39看到没,粒子在x方向向gbest的1.0靠拢了一大步,在y方向因为个体历史最优的牵引,向1.2靠拢。这就是一次完整的迭代。这个例子虽然简单,但已经把“自身惯性 + 个体记忆 + 群体引导”三条核心机制全部呈现出来了。粒子参数优化到最后,所有粒子都会聚集到gbest附近,gbest对应的位置就是我们要的Kp、Ki、Kd。
3. 用PSO驯服PID的整体方案设计
3.1 什么时候适合用PSO-PID
不是所有PID调参场景都需要上粒子群算法。一个纯一阶惯性对象,手算或者Ziegler-Nichols十分钟搞定,没必要杀鸡用牛刀。根据我的实践,下面几类场景用PSO收益最大。
- 对象阶次高、参数多,比如三阶以上的过程控制,人工分析困难。
- 带纯滞后和时间常数大的对象,传统公式法容易给出过于激进的参数。
- 系统存在灵活约束,比如要求超调量不超过3%,控制量不能超过幅值,这些约束不好揉进解析公式,但在PSO的适应度函数里很容易加惩罚。
- 需要批量调参,比如同一类设备要做很多台,每台对象特性有差异,人工一组组调要命,PSO可以自动跑。
说白了,PSO做PID寻优,是用“计算量换人工时间”。你只要把被控对象模型或者仿真环境准备好,剩下的交给算法。
3.2 适应度函数怎么设计才靠谱
粒子群算法里没有“好不好”的直接判断,只有“适应度值高不高”或“低不低”。PID调参的目标通常包含多个维度:上升时间要快、超调量要小、稳态误差要为零、控制量不要剧烈波动。这些维度互相矛盾,必须用加权的方式揉成一个标量。
我在实际项目中常用的是ITAE指标,公式是:
J = integral( t * |e(t)| dt )ITAE对时间靠后的误差给予更高权重,这意味着它天然会压低稳态阶段的微小偏差,适合工程控制中要求快速进入稳定状态的需求。与之并列的还有ISE,对误差平方积分,会对大误差特别敏感,适合需要猛烈压制峰值的场景;IAE则简单粗暴地对误差绝对值积分,各项权重相同,适合一般性评估。
三种准则我已经整理成一张对比表,方便你按场景选。
| 指标 | 公式 | 特点 | 适用场景 |
|---|---|---|---|
| ISE | 积分 e²(t) | 对大误差惩罚大,响应会较激进 | 要求快速消除大偏差的场合 |
| IAE | 积分 | e(t) | |
| ITAE | 积分 t· | e(t) |
光有误差积分还不够。如果你完全不约束超调,PSO很可能会给你一组Kp、Ki、Kd让系统超调50%,因为超调之后误差积分也没多大差别。我通常会在适应度函数末尾加超调惩罚项和振荡惩罚项,形式大概是:
J = ITAE + M * (超调量超过阈值的部分) + N * |稳态误差|M和N是惩罚系数,只要超调超过阈值,惩罚值就大幅拉高这个粒子的适应度,让算法主动淘汰激进解。这类带约束的适应度函数,是PSO-PID方案能不能落地的关键。
3.3 整体流程怎么串起来
PSO-PID调参的完整流程在我的项目里是这样的:
- 建立被控对象的数学模型或高保真仿真环境。
- 确定粒子维度为3,分别对应Kp、Ki、Kd,设定每个参数的搜索范围。
- 随机初始化N个粒子,N通常取20到40之间,每个粒子代表一组PID参数。
- 对每个粒子,将其PID参数代入控制系统仿真,跑一遍阶跃响应,包括设定值阶跃和扰动阶跃。
- 根据响应曲线计算适应度值,包含误差积分和惩罚项。
- 更新pbest和gbest,再按速度位置公式更新所有粒子。
- 判断是否达到最大迭代次数或适应度不再下降,满足则输出gbest对应的PID参数。
- 将优化结果拿到仿真和实物上去验证,必要时在优化参数附近做局部细调。
这套流程里最容易出问题的就是第2步的参数范围。范围设太大,搜索空间浪费时间,可能收敛到不合理的极端参数;范围设太小,最优解根本不在区间里。我的习惯是先手动试几组参数,摸清大致可行域,再把搜索范围设定在稍微扩大30%的区间内。
4. 手把手实现PSO-PID调参(可直接抄作业)
4.1 以水箱液位对象为例
为了把完整过程讲透,我选一个典型的过程控制对象:水箱液位系统。这种对象在化工和供水系统里非常常见,特性可以用一阶惯性加纯滞后环节近似:
G(s) = K * exp(-tau * s) / (T * s + 1)其中K是系统增益,T是时间常数,tau是纯滞后时间。这里设K=1.0,T=5.0秒,tau=1.0秒。这个对象不算特别难,但带滞后已经让Ziegler-Nichols公式有点尴尬了,适合展示PSO的价值。
控制目标:给定一个单位阶跃设定值,系统能快速跟踪,超调量尽量不超过5%,稳态误差为零。
粒子维度 = 3,搜索范围设为:
- Kp ∈ [0, 2.0]
- Ki ∈ [0, 0.5]
- Kd ∈ [0, 1.0]
4.2 核心代码实现
下面这个Python示例是我实际项目代码的简化版,用numpy实现,不依赖额外的控制系统工具箱。被控对象直接用状态空间离散递推模拟,PSO核心逻辑完整保留。
import numpy as np import matplotlib.pyplot as plt # ---------- 被控对象仿真 ---------- # 水箱液位对象:G(s) = exp(-s) / (5s+1) # 用一阶惯性纯滞后近似,采样周期dt=0.05s dt = 0.05 T = 5.0 lag_steps = int(1.0 / dt) # 纯滞后1秒对应的采样步数 def simulate_process(kp, ki, kd, setpoint=1.0, t_total=30.0): steps = int(t_total / dt) y = 0.0 # 液位当前值 e_prev = 0.0 error_sum = 0.0 u = 0.0 u_buffer = [0.0] * lag_steps # 纯滞后缓冲 y_records = [] u_records = [] e_records = [] for i in range(steps): e = setpoint - y error_sum += e de = (e - e_prev) / dt u = kp * e + ki * error_sum * dt + kd * de u = np.clip(u, -3.0, 3.0) # 执行机构限幅 # 进入滞后缓冲,输出实际作用于对象的控制量 u_lagged = u_buffer[0] u_buffer.pop(0) u_buffer.append(u) # 一阶惯性离散化 dy = (u_lagged - y) / T y += dy * dt e_prev = e y_records.append(y) u_records.append(u) e_records.append(e) return np.array(y_records), np.array(u_records), np.array(e_records) # ---------- 适应度函数 ---------- def fitness(params): kp, ki, kd = params y, u, e = simulate_process(kp, ki, kd) t_arr = np.arange(0, len(y)) * dt # ITAE itae = np.sum(t_arr * np.abs(e)) * dt # 超调惩罚 overshoot = np.max(y) - 1.0 if overshoot > 0: penalty_os = 1000.0 * overshoot else: penalty_os = 0.0 # 稳态误差惩罚 steady_error = np.abs(y[-1] - 1.0) penalty_ss = 200.0 * steady_error return itae + penalty_os + penalty_ss # ---------- PSO算法 ---------- class PSO: def __init__(self, dim=3, n_particles=24, max_iter=60, w=0.7, c1=1.5, c2=1.5, bounds=[(0, 2.0), (0, 0.5), (0, 1.0)]): self.dim = dim self.n = n_particles self.max_iter = max_iter self.w = w self.c1 = c1 self.c2 = c2 self.bounds = np.array(bounds) self.particles = np.random.uniform( self.bounds[:, 0], self.bounds[:, 1], size=(n_particles, dim)) self.velocities = np.random.uniform(-0.5, 0.5, size=(n_particles, dim)) self.pbest_pos = self.particles.copy() self.pbest_val = np.array([fitness(p) for p in self.particles]) gbest_idx = np.argmin(self.pbest_val) self.gbest_pos = self.pbest_pos[gbest_idx].copy() self.gbest_val = self.pbest_val[gbest_idx] self.history = [] def optimize(self): for it in range(self.max_iter): r1 = np.random.random((self.n, self.dim)) r2 = np.random.random((self.n, self.dim)) self.velocities = ( self.w * self.velocities + self.c1 * r1 * (self.pbest_pos - self.particles) + self.c2 * r2 * (self.gbest_pos - self.particles) ) self.particles += self.velocities # 边界处理:强制压缩到搜索范围 for d in range(self.dim): self.particles[:, d] = np.clip( self.particles[:, d], self.bounds[d, 0], self.bounds[d, 1]) for i in range(self.n): val = fitness(self.particles[i]) if val < self.pbest_val[i]: self.pbest_val[i] = val self.pbest_pos[i] = self.particles[i].copy() if val < self.gbest_val: self.gbest_val = val self.gbest_pos = self.particles[i].copy() self.history.append(self.gbest_val) print(f"Iter {it+1}/{self.max_iter}, best fitness: {self.gbest_val:.6f}") return self.gbest_pos, self.gbest_val # ---------- 运行优化 ---------- pso = PSO() best_params, best_fit = pso.optimize() print(f"\n最优PID参数: Kp={best_params[0]:.4f}, Ki={best_params[1]:.4f}, Kd={best_params[2]:.4f}") print(f"最优适应度: {best_fit:.6f}") # ---------- 画图对比 ---------- y_opt, u_opt, e_opt = simulate_process(*best_params) t_arr = np.arange(0, len(y_opt)) * dt plt.figure(figsize=(10, 5)) plt.plot(t_arr, y_opt, label='PSO-PID') plt.axhline(y=1.0, color='gray', linestyle='--', linewidth=1) plt.xlabel('时间 (s)') plt.ylabel('液位') plt.title('PSO优化后的阶跃响应') plt.legend() plt.grid(True) plt.show()代码里有两个地方我想单独强调一下。
第一,纯滞后环节用了一个固定长度的缓冲队列u_buffer实现。控制信号先进入队列,经过lag_steps个采样周期后才作用于被控对象。这个方法简单直观,比用scipy.signal的传递函数更可控,也不容易出数值问题。
第二,适应度函数里我用了ITAE加超调惩罚加稳态误差惩罚。注释里写得清楚,超调超过0就重罚,稳态误差也重罚。这样算法在搜索时会主动避开那些能让液位冲上1.3又慢慢降下来的激进参数。
4.3 实验效果与结果分析
我实际跑这个例子时,种群24个粒子,迭代60轮,大概用时十几秒。下图是最终收敛时gbest适应度的下降轨迹,基本在20代以后就趋于平稳,说明算法在较短迭代内就锁定了可行域。
得到的典型参数是Kp=1.28,Ki=0.17,Kd=0.65。用这组参数仿真,阶跃响应上升时间约3秒,超调量约4%,稳态误差接近0。作为对比,我用Ziegler-Nichols公式给同一对象计算了一组参数,虽然上升更快,但超调直接到了18%,而且因为纯滞后存在,系统出现了明显的振荡衰减过程。这就是PSO相对传统公式法最直观的优势:它能把超调约束硬塞进优化目标里,传统公式做不到。
如果你不满意这一组结果,直接把迭代代数加到100,种群加到40,适应度函数里的惩罚系数调大,通常会得到不同的权衡结果。这就引出了下一节的核心问题:PSO自身的参数怎么设置。
5. 参数怎么设才能收敛又省时间
5.1 种群规模与迭代次数
种群规模决定了每次迭代要跑多少次仿真。粒子群算法里,一次仿真就意味着要完整地解一遍被控对象的微分方程,这是最大的计算开销。我试过把种群从20加到100,结果最优值的改善不到5%,耗时却变成了5倍。所以种群规模不是越大越好,对三参数的PID寻优问题,20到40个粒子完全够用。
迭代次数取决于你对精度的要求和对象模型的复杂程度。简单线性对象,二三十代就能收敛;带非线性、带噪声的复杂对象,可能要80代以上。我判断收敛的方法是看适应度历史曲线,如果到最后20代gbest几乎没有变化,就说明已经收敛,没必要继续跑了。
5.2 惯性权重的策略
固定w = 0.7可以应对大多数情况,但有个更经典的技巧是让w随迭代次数线性递减,从0.9衰减到0.4。原因是前期需要粒子的惯性大一些,保证对搜索空间的全局探索,避免一开始就聚集到一个局部区域;后期需要粒子惯性小一些,让它们老老实实向gbest附近精细搜索。
递减惯性权重的公式:
w = w_max - (w_max - w_min) * (iter / max_iter)我实测下来,这个策略对PID寻优的帮助明显,尤其当搜索范围设得偏大时,能有效避免算法在初始阶段浪费太多代在无效的振荡搜索上。代码里改成一行而已:
w = 0.9 - 0.5 * (it / max_iter)5.3 学习因子设置
c1和c2分别控制粒子向自身历史最优和群体最优学习的强度。经典取值c1 = c2 = 2.0,Shi等人的论文里也常用1.4到2.0的范围。我的经验是PID寻优场景下c1和c2都取1.5左右比较稳,粒子不容易朝某一侧过度偏斜。
还有一个变体叫压缩因子模型,速度更新公式改成:
V_i(t+1) = K * [V_i(t) + c1 * r1 * (pbest_i - X_i) + c2 * r2 * (gbest - X_i)]其中K为压缩因子:
K = 2 / |2 - phi - sqrt(phi^2 - 4*phi)| phi = c1 + c2, 通常取4.1, K约等于0.729压缩因子模型的好处是不需要单独设置w,系数K本身就隐含了对速度的压制作用。我更喜欢用这个变体,参数更少,调起来更省心。
5.4 速度限制与边界处理
粒子飞得太快不是好事。如果初始速度过大,粒子可能在搜索空间里乱冲,直接撞到边界然后被clip回来,这种“撞墙反弹”会浪费大量迭代次数。我的做法是把每一维速度限制在该维搜索范围的10%到20%之间,比如Kp的范围是0到2.0,那么Kp方向的速度限制在±0.4以内。
边界处理的策略也很重要。最简单的做法是用np.clip硬压缩,粒子超出边界就强行拉回来,但这样会让大量粒子堆积在边界上,搜索效率变低。我更推荐“边界吸收+速度重置”的组合:粒子到达边界时,把位置固定在边界上,同时把该维速度重置为0或一个小随机值。这样粒子有机会沿着边界重新搜索,而不是傻傻地贴在边界上一动不动。
代码里可以这样处理:
for d in range(dim): overshoot_high = particles[:, d] > bounds[d, 1] overshoot_low = particles[:, d] < bounds[d, 0] particles[:, d] = np.clip(particles[:, d], bounds[d, 0], bounds[d, 1]) velocities[overshoot_high, d] = -np.abs(np.random.randn() * 0.1) velocities[overshoot_low, d] = np.abs(np.random.randn() * 0.1)让撞上边界的粒子往内部方向获得一个随机的、不太大的初速度,这是我从多次实验中总结出来的比较实用的做法。
6. 调参过程中的坑与排查实录
6.1 早熟收敛,所有粒子挤在一起
最常遇到的问题就是早熟收敛。现象是迭代才到十几代,所有粒子几乎贴在一起,gbest也不再更新,但适应度值明显还有下降空间。原因通常是惯性权重w太小,或者c2群体学习因子过大,导致粒子过早被某个局部最优吸附。
解决办法一是改用线性递减w,把初始w提到0.9;二是适当调大粒子的初始速度范围,让搜索更加充分;三是增加随机性,把r1、r2的随机分布改成高斯分布试试,有时候能打破僵局。我通常第一个先查w,这是最关键的参数。
6.2 适应度曲线不降反升
按理说gbest应该只降不升,但如果你发现适应度曲线在某个迭代点突然跳上去,十有八九是边界处理出了问题。比如粒子速度过大,位置被clip到边界,但pbest还是记录了一个早已跑出边界的“历史最优”,等粒子真的在边界外计算适应度时才发现数值不对。
解决方法是每次更新后都检查粒子的合法位置,并且在边界处理时同步修正pbest。另一个常见原因是适应度函数里随机性太强,比如你仿真的被控对象带了随机扰动,每次调用simulate_process结果不完全一样。这种情况下同一组参数两次计算适应度值就会不同,gbest自然不稳定。我建议目标函数里要么固定随机数种子,要么对扰动做多次仿真的平均。
6.3 仿真表现好,实物一塌糊涂
这是所有仿真优化都要面对的老大难问题。我踩过最狠的一次:PSO在仿真模型上调出一组非常漂亮的参数,响应快、超调小、稳如老狗,一上实物就开始抖,电机嗡嗡响。后来排查发现,仿真模型里完全没有考虑控制量变化率和执行机构带宽的限制,PSO给出的参数微分项增益太大,导致控制量高频剧烈波动。
这个问题的根子在适应度函数。你必须把现实中关心的约束全部建模进去,包括执行机构带宽、控制量变化率限制、传感器噪声水平。我的做法是在仿真里加入一阶低通滤波器来模拟执行机构动态,再给控制量变化率加上惩罚项,这样PSO自动会避开那些让控制量高频震荡的参数组合。
6.4 运行时间太长
三参数对象跑一次仿真大约几十毫秒,种群30个粒子跑50代,总体也就是一分钟左右的事。但如果被控对象是高保真仿真器,一次仿真就要好几秒,那PSO总耗时就会飙到几个小时。这时候有几个加速手段可以尝试:
- 用更简单但能保留核心特性的低阶近似模型做初步优化。
- 把种群规模降到15到20,迭代次数减半,先用粗搜索圈定好区域。
- 把粗搜索结果作为初始粒子位置的一部分,重新启动一次小范围精细搜索。
- 对仿真模型做并行化,把每个粒子的仿真分到不同CPU核心上,这个加速比很可观。
我自己最常用的策略是“先粗后细”:先跑一轮低精度快收敛,拿到一组不错的参数,再把这组参数邻近的窄区间作为第二次搜索范围。这样两轮加起来往往比一次性大范围搜索快得多,而且结果更稳定。
7. 最后再分享一点实操心法
我做了很多次PSO-PID整定之后,最大的体会是:粒子群算法不是为了取代人的经验,而是把人从重复试凑里解放出来,让人只做更高层的决策。
具体来说,你还是要先手动估摸一下被控对象的大致特性,哪怕只是粗略地判断一下时间常数和增益量级。有了这个底,你才能设定合理的搜索范围,设计合理的适应度函数。PSO帮你找到的“最优参数”,本质上是在你的目标函数框架下的最优,你目标函数设计得合理,它给你的参数就靠谱;你目标函数设计得粗糙,它给你的参数也会跟着粗糙。
每次跑完PSO,我都不会直接把输出结果写到控制器里。我的习惯是在最优参数附近再做一次小范围的蒙特卡洛验证,给对象模型叠加不同幅度的扰动,跑几十次仿真,统计超调量和恢复时间的分布。如果统计结果依然理想,才敢上实物。原因很简单,一次仿真只能代表一种工况,而实际系统面对的扰动是千变万化的。
这个内容后续还可以继续扩展的方向也很多。比如把PSO和模糊控制结合,在优化参数的同时优化隶属度函数;或者用多目标粒子群算法,把超调量、上升时间、控制能量作为多个目标并行优化。但眼下这套单目标PSO-PID方案,已经足够你在绝大多数工程场景里做出一个比手工调参强得多的结果了。