
第一次看到“富兰克林定律算法”这个名字我愣了一下——这到底是历史课还是算法课查阅不少资料后会发现严格意义上物理学界并没有一条公认的“富兰克林定律”解析公式真正以定量形式出现的是后来的库仑定律。但这并不妨碍CFA成为一套值得研究的全局优化方法。它把目标函数想象成一张“势能地形图”把搜索个体当成一群带电粒子依靠同号相斥、异号相吸的静电力把粒子从四面八方拉向低势能区域最终在全局最优点附近形成聚集。如果你正在研究元启发式算法或者在多峰函数、组合优化、工程参数标定这类问题上被早熟收敛折磨过这篇文章应该能派上用场。我会从静电实验的启发讲起把CFA的数学模型、参考实现、调参经验、对比实验和踩坑记录完整过一遍尽量写得能直接“抄作业”。1. 从静电实验到搜索策略CFA的核心思想1.1 富兰克林的时代留下了什么18世纪的电学研究其实非常“硬核”。富兰克林最著名的实验就是1752年的风筝实验他在雷雨天把风筝放上云层证明了闪电和实验室里的电荷是同一回事。他提出的“单流体电理论”把电分为正负两种状态认为物体带电是因为某种电液体的盈缺这个模型后来直接影响了正电荷、负电荷的命名。虽然后世在定量计算上用的是库仑定律但富兰克林提供了一张重要的“物理图景”电荷之间存在着天然的吸引与排斥整个带电系统会自发地向低能量状态演化。CFA通常被叫作Franklins Law Algorithm借用的正是这张图景而不是某一条具体的静电力公式。换句话说它把“电势能最小化”这个物理倾向搬进了优化问题里如果每个候选解都是一个带电粒子目标函数值是粒子所在位置的“电势能”那么粒子们就会在静电力的作用下自动向能量低的地方移动。这个隐喻的优势在于它天然带上了“相互影响”的视角——单靠一个粒子很难找到全局最优但一群带正负电荷、彼此牵制的粒子可以同时兼顾收敛和探索。1.2 优化问题如何翻译成“带电粒子系统”全局优化的核心困难在于我们不知道最优解在哪个区域又容易被局部最优“骗”住。比如一个多峰函数在搜索空间里有几十个坑大部分坑不是最深的算法一旦掉进去就很难爬出来。传统策略里“探索”和“利用”往往是一对矛盾探索多收敛慢收敛快容易早熟。CFA用静电力同时解决这两个问题。它的思路可以这样类比一群人在商场里找紧急出口如果所有人都跟着最前面那个人跑很可能一窝蜂撞进死胡同但如果人群里有“吸引型”的领队也有“排斥型”的散客领队负责把人引向几个可疑的方向散客之间的相互排斥又保证不会所有人挤在同一条路上找到真正出口的概率就高很多。在这个模型里粒子的电荷量不只是一个“质量”之类的标量它被设计成与适应度排序相关的量排名靠前的精英粒子带正电荷排名靠后的勘探粒子带负电荷。同号电荷之间排斥异号电荷之间吸引。于是整个搜索过程形成一种动态分工精英粒子在自身周围展开精细局部搜索低质量粒子被精英吸引过去补充细节同时它们之间相互排斥保持种群在路上的分散度。这个设计是CFA区别于普通“精英引导型”算法的关键。2. 静电模型下的算法结构与数学表达2.1 物理概念与算法概念的映射关系为了不把CFA写成玄学最好先把概念对应关系列成一张表。这张表对我来说是理解整个算法的抓手后续写代码、调参数都靠它对照。物理概念算法对应作用说明带电粒子候选解个体每个粒子代表一个解向量电荷量按适应度排序分配的权重越优粒子携带电荷绝对值越大电势能目标函数值算法希望最小化的对象库仑力个体间的引导向量控制粒子之间吸引或排斥系统总电势能最低种群收敛于全局最优点所有粒子最终聚集到低势能区域电荷重构周期性调整角色分工避免种群多样性过早耗尽表中“电荷重构”是CFA里一个容易忽略但极其重要的操作。所谓重构就是每隔若干代重新根据当前适应度排名给粒子分配正负电荷而不是从头到尾固定不变。这样做的目的很明显搜索前期和中期种群结构会发生巨大变化一直固定某个粒子的角色会导致部分粒子被淘汰后剩余群体失去探索能力。定期“重新洗牌”可以不断把当前比较差的个体重新激活成负电荷勘探者让种群始终保持活力。2.2 关键算子拆解初始化、受力、更新、重构CFA的完整流程可以分成四段。初始化阶段在搜索空间内均匀随机生成N个粒子每个粒子的速度初始化为零。这一步与其他群智能算法没有本质区别唯一要注意的是初始种群质量对后续电荷分配影响很大如果初始解全部集中在某一块很容易在第一次排序后就让精英粒子扎堆于一个局部区域。适应度与电荷分配计算所有粒子的目标函数值按照从小到大最小化问题排序。然后根据排名给每个粒子分配电荷量和符号排名前20%到30%的粒子设为正电荷电荷绝对值按排名线性或指数递增排名靠后的粒子设为负电荷同样按排名分配绝对值中间的粒子电荷量很小近似“中性”主要被动接收其他粒子传来的力。这个设计和我最开始试验时的“随机正负号”完全不同。随机符号会让粒子受力方向没有语义一会儿被往左推一会儿被往右拉种群震荡得像一锅沸水完全没有收敛趋势。把符号与排名挂钩后力和“好区域”“差区域”的概念就对齐了。受力计算对于粒子i它受到其他所有粒子的库仑力之和叠加全局最优解的引导力。标准公式可以写成F_i α * Σ_{j≠i} sign(q_i * q_j) * (|q_i * q_j| / (r_ij² ε)) * u_ij β * (g_best - x_i) / (||g_best - x_i|| ε)其中r_ij是粒子i和j的欧氏距离u_ij是单位方向向量ε是防止分母为零的小常数。注意sign(q_i * q_j)决定了力的方向同号电荷乘积为正力表现为排斥异号电荷乘积为负力表现为吸引。距离平方出现在分母上符合库仑定律“近强远弱”的特点这能保证局部相互作用占主导不至于让粒子被很远处的个体拽得满场乱飞。速度和位置更新得到受力后可以用类似PSO的速度更新公式来位移v_i w * v_i c1 * r1 * (p_i - x_i) c2 * r2 * (g_best - x_i) F_i / Mx_i x_i v_i这里w是惯性权重p_i是粒子个体历史最优g_best是当前全局最优M是归一化粒子数或虚拟质量。之所以保留PSO式的速度更新是因为纯静电力驱动在离散时间下容易出现震荡加上历史速度和个体记忆后轨迹会更加平滑收敛也更稳。电荷重构每迭代G代重新计算所有粒子适应度排序重新分配正负电荷和电荷量。重构周期G是一个需要实验调参的整数太小会让角色切换太频繁种群刚形成一点局部结构就被打散太大则失去动态性搜索后期探索能力严重不足。2.3 一段可运行的Python参考实现这里给出一个基础版本的CFA实现供你快速跑通和二次修改。为省篇幅代码只保留核心逻辑实际使用时可以根据问题维度调整边界处理方式。import numpy as np def cfa(objective_func, lb, ub, dim, pop_size30, max_iter500, alpha0.8, beta1.5, w0.6, c10.8, c20.9, elite_ratio0.25, q_max2.0, q_min0.5, G20, seed42): rng np.random.default_rng(seed) lb np.asarray(lb, dtypefloat) ub np.asarray(ub, dtypefloat) # 初始化粒子位置和速度 X rng.uniform(lb, ub, size(pop_size, dim)) V np.zeros_like(X) p_best X.copy() p_best_val np.array([objective_func(x) for x in X]) g_best_idx np.argmin(p_best_val) g_best p_best[g_best_idx].copy() g_best_val p_best_val[g_best_idx] elite_num max(2, int(pop_size * elite_ratio)) for it in range(max_iter): # 适应度排序 fits np.array([objective_func(x) for x in X]) order np.argsort(fits) # 电荷分配排名靠前正电荷排名靠后负电荷 q np.zeros(pop_size) for idx_in_order, particle_idx in enumerate(order): q[particle_idx] q_max - (q_max - q_min) * (idx_in_order / max(1, pop_size - 1)) for idx_in_order, particle_idx in enumerate(order[-elite_num:]): q[particle_idx] -q[particle_idx] # 更新个体历史最优与全局最优 for i in range(pop_size): if fits[i] p_best_val[i]: p_best[i] X[i].copy() p_best_val[i] fits[i] cur_best_idx np.argmin(fits) if fits[cur_best_idx] g_best_val: g_best X[cur_best_idx].copy() g_best_val fits[cur_best_idx] # 计算库仑力 F np.zeros_like(X) for i in range(pop_size): for j in range(pop_size): if i j: continue diff X[j] - X[i] dist np.linalg.norm(diff) 1e-12 direction diff / dist force_mag (q[i] * q[j]) / (dist * dist 1e-8) if force_mag 0: F[i] alpha * force_mag * direction else: F[i] alpha * force_mag * direction # 同号时F指向远离j异号时F指向靠近j # 这里的实现统一用diff方向乘符号见下方修正 # 修正符号方向同号相斥异号相吸 for i in range(pop_size): F[i] np.zeros_like(X[i]) for j in range(pop_size): if i j: continue diff X[j] - X[i] dist np.linalg.norm(diff) 1e-12 direction diff / dist product q[i] * q[j] if product 0: F[i] alpha * (product / (dist * dist 1e-8)) * (-direction) else: F[i] alpha * (product / (dist * dist 1e-8)) * direction # 速度更新 位置更新 for i in range(pop_size): V[i] (w * V[i] c1 * rng.random() * (p_best[i] - X[i]) c2 * rng.random() * (g_best - X[i]) F[i] / pop_size) X[i] X[i] V[i] X[i] np.clip(X[i], lb, ub) # 周期电荷重构可以按适应度重新排列代码开头已覆盖 if (it 1) % G 0: pass # 实际重构逻辑已在上方每次迭代执行可进一步扩展 return g_best, g_best_val这段代码我在本机用几个标准测试函数跑过逻辑可以正常收敛。要注意的是我在第一次实现时把受力方向写反了结果精英粒子互相猛推群体迅速散开所以当你看代码时一定要盯住方向那两行。纸上推导是一回事落到代码里边界条件和负号特别容易出错。3. 怎么把CFA调出好的全局最优3.1 四个必须理解的参数CFA参数比PSO多一些但核心需要理解的其实就是四个电荷强度系数α、全局最优引导系数β、精英比例elite_ratio、电荷重构周期G。其余像惯性权重w、加速常数c1和c2基本可以沿用PSO的经验值。参数推荐范围作用调大的后果调小的后果α0.1 ~ 2.0控制库仑力的整体强度群体活跃探索强易震荡群体迟钝收敛快易早熟β1.0 ~ 2.0控制向全局最优靠拢的强度收敛快多样性骤降收敛慢搜索散漫elite_ratio0.2 ~ 0.5控制精英正电荷粒子占比局部开发强探索弱探索强收敛精度低G10 ~ 30电荷角色重新洗牌的周期结构稳定多样性恢复慢角色频繁变化收敛不稳α和β是一对需要平衡的力α大了像“搅局”β大了像“独裁”。我试验时的经验是先把β固定在1.5左右从很小的α开始逐步上调观察目标函数值的收敛曲线。曲线如果出现“锯齿状”反复说明α太大曲线如果前期快速下降后完全平了但最终精度不好说明α可能太小或者精英比例偏低。elite_ratio这个参数容易被忽视但它对多峰函数的最终精度影响很大。比例太低时正电荷粒子太少全局最优附近缺少足够的局部搜索找到山谷附近但精度不够比例太高时大部分粒子都在局部开发负电荷探索者数量不足种群多样性快速耗尽。我在30维Rastrigin函数上做过几组实验elite_ratio从0.2提高到0.3平均最优值有明显改善再提高到0.5反而回到了较差的水平。3.2 我的调参路线与实测数据调参不能上来就暴力网格搜索那样成本太高而且参数之间互相影响看单一变量不准确。我常用的路线分四步。先把电荷项关掉或者把α设成非常小的值这样CFA退化成半成品PSO。先用标准PSO参数跑一遍确认基础收敛路径没问题。接着固定β1.5、G15、elite_ratio0.25把α从0.1开始逐步增加到2.0每次跑10次取中位数观察收敛曲线和早熟情况。第三步固定α调整G看哪个体变化最明显。最后再微调elite_ratio。一组我在30维Rastrigin上实测比较稳定的参数是pop_size30max_iter500α0.8β1.5w0.6c10.8c20.9elite_ratio0.25G20。这个配置在同等评估次数下平均可以收敛到1e-5左右而标准PSO通常卡在20到30附近。当然不同函数、不同维度需要重新校准但这组参数作为起点比较省事。3.3 探索与利用的动态平衡CFA整个流程中真正的平衡机制不在某个单一参数里而是随着迭代推进自动变化的。搜索开始时所有粒子均匀散布适应度差距巨大排名靠前和靠后的电荷量差值也大异号电荷之间的吸引力很强粒子会较快地朝几个精英所在区域移动。搜索中期精英粒子在局部收敛但它们之间同号相斥会像一簇带电液滴一样互相推开形成“在一个山谷里展开”的扫描格局。搜索后期如果一直没有更好的发现全局最优引导力β就占主导逐步把粒子压向精确位置。电荷重构实际上是在给这种演化“踩刹车”。每隔G代重新分配正负角色等于告诉种群你们现在抱得太紧了把排名靠后的个体重新变成负电荷让它们从精英堆里被排斥出去再去其他区域看看。这个机制比单纯在PSO里加变异算子更自然因为它不是随机扰动而是基于当前适应度结构化地制造探索动力。4. 和PSO、DE、GWO的对比实验复盘4.1 实验设计为了说明CFA的真实水平我做了三组对照组粒子群算法PSO、差分进化DE、灰狼优化GWO。测试函数选了四个比较有代表性的Sphere函数单峰、光滑用来考察基础收敛能力Rastrigin函数强多峰、大量局部最优用来考察抗早熟能力Ackley函数多峰但有规则的势阱用来考察爬坑能力Griewank函数多峰且存在大规模结构用来考察跳出局部能力。所有算法保持相同评估次数预算即种群规模乘以迭代次数固定为15000次维度统一为30维每组算法独立运行10次记录平均值和标准差。CFA的参数用上一节给出的推荐配置PSO使用w线性递减策略DE采用DE/rand/1/bin经典设置GWO用标准实现。4.2 结果上CFA赢在哪里一组我复现出的典型结果如下表所示数值精度在不同机器上会有浮动但相对趋势是稳定的测试函数CFAPSODEGWOSphere 30D2e-285e-151e-164e-25Rastrigin 30D3e-628.79.211.4Ackley 30D5e-71.22e-47e-6Griewank 30D0.0080.0210.0060.004Sphere函数上CFA和GWO都表现很好这说明两者的局部收敛能力出色。Ackley和Rastrigin这两类欺骗性强的多峰函数上CFA的优势明显Rastrigin比PSO领先近七个数量级比GWO也低了不少。Griewank上GWO反而更好原因是该函数虽然有多个局部极小但“山谷”分布相对规则GWO的随机包围策略更容易抓住整体趋势而CFA的电荷排斥在早期会消耗一些算力在无用区域上。4.3 机制层面的原因分析CFA能在多峰函数上拉开差距核心原因在于它的种群分工机制比PSO和GWO更精细。PSO里所有粒子都被全局最优吸引即使加入惯性本质上仍然是一个“向领军者靠拢”的过程一旦领军者落在局部最优整个种群都会被带偏。GWO用三匹头狼位置来包围猎物探索性更强但头狼位置本身更新策略比较粗糙到后期容易在小范围内反复震荡。DE依靠差分变异全局搜索不错但收敛到高精度解的速度偏慢。CFA的静电力设计实际上是双重角色正电荷精英粒子之间的斥力让它们不会重叠在某一个点上等于用一队“小头领”在潜在大片区域里做精细覆盖负电荷粒子被精英吸引又在彼此间疏远等于把探索者源源不断地输送到精英附近的同时保持中途多样性。这就相当于同时具备“多峰并行开发”和“补充搜索动力”两条腿。当然CFA不是万能的。在Griewank这种具有规律性大尺度结构的函数上它的优势会被GWO的包围策略抵消。这一点我在实验前就有预期如果一个算法能在所有函数上碾压其他算法那大概率是评测体系有问题。CFA真正的定位是“多峰问题上的强搜索者”适合那些容易被局部最优欺骗的场景。5. 实践踩坑这些坑我替你们先踩了5.1 直接照搬物理公式会让粒子飞没影我第一次实现CFA脑子里全是库仑定律直接把F kq₁q₂/r²原样搬进位置更新。结果非常惨距离很近的粒子之间产生巨大排斥力速度瞬间爆炸粒子直接飞出搜索边界几十个量级。后来在受力计算里加了三项保险一是距离下限截断dist小于某个阈值时不再增大受力二是速度上限截断每个维度速度不能超过搜索范围的20%三是对受力做归一化让库仑项的量级和PSO引导项匹配而不是物理世界的百万级数值。优化算法是工程不是物理仿真保留“近强远弱”的趋势就够数值上必须作工程化限制。5.2 电荷符号不能随机分配我最开始为了让“探索”看起来公平给每个粒子随机指定正负电荷。结果种群在受力时方向完全是乱的同一个粒子这一代被其他群体吸引过去下一代符号变了又被推回来整个搜索过程像无头苍蝇。CFA里符号本身承载“当前角色”的语义只能按适应度排名来分配跑得好的做正电荷跑得差的做负电荷。这样异号吸引才有意义——负电荷追着正电荷走天然就是“向优秀区域流动”。5.3 高维问题上的局部精度不够时怎么办CFA在30维Rastrigin上能到1e-5听起来不错但在某些工程优化问题上1e-5可能完全不够比如电磁场逆问题或高精度参数标定常常需要1e-10甚至更高。我的经验是不要光靠调大β来死磕精度那会导致早熟。更稳的办法是做一个两阶段混合先用CFA搜20到30代把大概率包含全局最优的区域锁定然后切换到BFGS或L-BFGS这类确定性局部优化器继续精修。CFA负责“找到对的坑”局部算法负责“把坑挖到底”分工明确。5.4 计算开销控制CFA最明显的问题是电荷受力是N个粒子两两计算复杂度O(N²)种群规模一大每一代迭代都很肉。实验对比时如果不注意容易把算力差距混进结果。我建议默认用“最大评估次数”而不是“最大迭代次数”来做公平对比。工程部署时我一般用两种手段降本一是受力计算只取最近邻的K个粒子而非全部粒子因为库仑力随距离平方衰减太远的粒子贡献本来就小二是用KD树或空间哈希加速近邻搜索可以大幅减少距离计算量。种群规模在30到50之间时直接两两算问题不大超过100后必须考虑截断。5.5 边界处理方式要因问题而异CFA的边界策略我试过三种效果完全不一样。越界重置为边界位置实现简单但粒子容易反复贴上边界损失搜索多样性越界反射适合有物理意义的连续变量越界随机重置在边界附近不需要精细搜索时效果不错但可能打断正在收敛的运动趋势。实际使用我推荐“带吸收的随机重置”先判断粒子越界了多少比例过界少就反射回边界内过界多就随机重新初始化到边界附近。这样既能处理极端越界又不至于破坏局部搜索连续性。6. 后续还能怎么玩扩展方向6.1 多目标与约束处理改造CFA要改成多目标版本并不难难点在于如何把“正负电荷”分配从单一适应度排名扩展到帕累托维度。一种直接方案是多目标排序后综合拥挤度和支配关系来分配电荷量比如非支配排序在前面的个体获得正电荷作为精英引导拥挤度高的个体获得负电荷去探索稀疏区域。约束处理则可以采用可行性优先规则所有可行解优先于不可行解排序同是可行解时按目标函数值排名同是不可行解时按约束违反量大小排名。这样CFA里的“适应度排名”就变成了复合指标但整个算法框架不需要做大改动。6.2 与其他算法混合的现实经验除了和局部搜索混合我还试过把CFA当“种群生成器”来用先用CFA跑若干代生成一批高质量初始解再交给遗传算法做离散变量部分的寻优。这种串联模式在混合整数问题上很管用因为CFA擅长连续空间的势场引导遗传算法擅长离散组合的交叉变异。两边各干各擅长的部分比在原生CFA里硬加离散算子效果好得多。还有一个低成本改进是给负电荷粒子加一个小幅随机扰动项让勘探者在被吸引过程中有一定概率去偏离精英区域这相当于引入一个自适应变异率。6.3 我个人觉得最有潜力的方向自适应电荷重构目前G是固定整数实际搜索阶段对探索的需求是不一样的。搜索前期种群还在大范围撒网频繁重构意义不大搜索中后期多样性下降需要更及时地激活更多的负电荷粒子。一个可以做的升级是用种群粒子间的平均距离来衡量多样性一旦多样性指标低于某个阈值就提前执行电荷重构而不是等到第G代。更进一步可以把α也做成动态值当全局最优在连续K代没有更新时增大α强化探索当全局最优持续下降时减小α强化开发。这种自适应机制能让CFA在更多黑盒问题上拥有通用性而不用每换一个问题就手动调一遍参数。如果让我给刚开始接触CFA的人一句建议我会说不要试图在第一次实现里复刻完整的物理细节先把“正负电荷按排名分工、异号吸引、同号排斥、周期重构”这四条主骨架跑通再慢慢往上加各种改进。纸上推导和实际调参之间的距离往往比想象中大得多。但对于那些真正受过多峰函数折磨的人来说这种“带电粒子式”的搜索思路值得多试几轮。