news 2026/9/9 4:24:23

鲸鱼算法求解线性规划:罚函数设计与Python实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鲸鱼算法求解线性规划:罚函数设计与Python实现

1. 为什么要用鲸鱼算法去解线性规划

1.1 线性规划不是已经有标准解法了吗

线性规划是我接触运筹学和数学建模时最先遇到的优化模型。标准形式很简单,目标函数和约束条件都是线性的,例如:

最大化c^T x,满足A x <= b,且x >= 0

对这种问题,工程上已经有非常成熟的解法。单纯形法从顶点出发,沿着可行域的棱边移动,通常几十次迭代就能找到最优顶点;内点法更适合大规模问题,变量数量上百万甚至上千万时,高性能求解器依然能在合理时间内把最优解算出来。像 Gurobi、CPLEX、SCIP、CBC 这些求解器,已经把线性规划处理得非常“工业化”了。

既然如此,为什么还要研究“鲸鱼算法求解线性规划”?这个问题我在做算法实验和指导学生建模时经常被问到。我的回答通常是:鲸鱼算法不是来替代求解器的,它更适合作为元启发式算法学习、算法对比和扩展研究的起点。鲸鱼算法是一种模拟座头鲸气泡网捕食行为的群体智能优化算法,2016 年提出后在很多非线性、非凸、不可导的优化问题里都有应用。拿它来解线性规划,本质上是一种“跨界”练习,能帮你更清楚弄懂什么叫约束处理、什么叫可行域、什么叫收敛性,也能让你体会到为什么线性规划要交给专门的求解器反而更稳。

1.2 鲸鱼算法真正擅长的场景

严格说,鲸鱼算法并不擅长线性规划。线性规划的目标函数是凸的,可行域是多面体,局部最优解就是全局最优解,这类问题应该用基于梯度的精确算法或者成熟求解器。鲸鱼算法是随机搜索方法,它不依赖目标函数的梯度,也不要求问题是凸的,所以它更适合这样一类场景:目标函数很复杂、有多个局部极值、甚至不能写出具体解析表达式,只能通过仿真或黑箱程序给出一个评价值。

很多实际问题天然就满足这种“乱糟糟”的特点,例如车间调度、车辆路径规划、神经网络超参数调优等。这些模型里可能含有非线性目标、离散变量、复杂约束,此时传统求解器建模困难,而鲸鱼算法只需要配一个适应度函数就能开始搜索。所以说,鲸鱼算法的价值在于“给没有梯度信息、没有精确模型的问题提供了一个近似求解入口”。

用鲸鱼算法解线性规划,就好比用越野车去跑赛道直线加速,虽然也能到终点,但肯定不如专业赛车快。这篇内容更适合下面三类人:第一类是在学优化算法、想亲手实现一个鲸鱼算法的学生;第二类是参加数学建模竞赛、需要快速验证某个模型能不能用启发式搜索来逼近的选手;第三类是做元启发式算法横向对比的工程师和研究人员。

1.3 我不建议你完全照搬什么套路

读到这里你可能会想:既然求解器更稳,那我何必还要学鲸鱼算法解线性规划?我的建议是:不要把它当成一个生产工具来用,而要当成一扇理解优化算法差异的窗口。线性规划是最干净的测试场,因为它的最优解是已知的,你可以拿鲸鱼算法的结果和单纯形法的结果做对比,观察随机搜索的误差来源。理解了这些之后,再去面对非线性、非凸问题,你才真正知道鲸鱼算法该在什么时候介入,什么时候该绕开。

2. 鲸鱼算法核心机制:座头鲸怎么找最优解

2.1 三个动作的数学模型

鲸鱼算法的设计灵感来自座头鲸捕食。座头鲸会围住猎物群,然后沿螺旋路径向上吐气泡,把磷虾和小鱼赶向水面,最后在气泡网中心张口吞食。这个行为在算法里被抽象成了三种位置更新方式:包围猎物、气泡网螺旋攻击、随机搜索猎物。

第一种是包围猎物。算法假设当前搜索到的全局最优点是猎物位置,其它鲸鱼向这个位置靠近。位置更新公式可以写成:

D = |C * X_best - X(t)| X(t+1) = X_best - A * D

这里X_best是当前最优鲸鱼位置,AC是两个关键系数。A = 2 * a * r - aC = 2 * r,其中r是 0 到 1 之间的随机数,a随迭代次数从 2 线性递减到 0。a的递减让鲸鱼在早期能大范围活动,后期逐渐收敛到最优解附近。

第二种是气泡网螺旋攻击。算法生成一个随机概率p,当p >= 0.5时,鲸鱼会沿着一个对数螺旋路径向当前最优解靠近,更新公式为:

D' = |X_best - X(t)| X(t+1) = D' * exp(b * l) * cos(2 * pi * l) + X_best

这里b是常数,通常取 1,l[-1, 1]之间的随机数。对数螺旋能保证鲸鱼在收缩包围猎物时,同时保留环绕搜索的轨迹,不至于一步就直接跳到猎物脸上。

第三种是随机搜索鲸鱼。当p < 0.5|A| >= 1时,算法不再向当前最优个体靠近,而是随机选一条鲸鱼作为参考,让当前个体向那个方向更新。这样做能扩大搜索范围,避免所有鲸鱼都过早挤在局部极值附近。

为了更直观理解,可以把标准鲸鱼算法的判断逻辑整理成一个简表:

条件动作搜索倾向
p < 0.5且 `A< 1`
p < 0.5且 `A>= 1`
p >= 0.5螺旋接近当前最优个体局部精细搜索

我实际调代码时发现,很多入门教程会把pAC的实现搞混。最简单稳妥的做法是:每一条鲸鱼每次迭代重新生成一个p,再根据|A|判断走收缩还是随机,螺旋更新和收缩更新通过p来切换。这样既保留了探索能力,也让后期有足够强的局部搜索能力。

2.2 探索与开发怎样影响线性规划的求解

鲸鱼算法搜索时存在非常明显的“探索-开发”平衡问题。早期迭代a接近 2,A的取值范围比较大,鲸鱼会满空间乱跑,这是在找那些可能有最优解的区域;后期a接近 0,A的取值范围变小,鲸鱼都在当前最优位置附近翻滚,这是在做局部收敛。

对线性规划来说,可行域是一个凸多面体,最优点一定在某个顶点上。鲸鱼算法并不知道哪几个约束会相交,它只能靠罚函数的数值变化来感知方向。如果早期探索不够,鲸鱼容易过早收敛到一个可行但非最优的区域内;如果后期开发不够,它又会在最优顶点附近反复震荡,始终差最后一点精度。所以我在做线性规划测试时,通常会刻意观察它是否真的收敛到了边界顶点,而不是随便满足约束就算成功。

3. 从模型到一个能跑的鲸鱼算法实现

3.1 罚函数设计:把线性约束变成适应度的一部分

鲸鱼算法本身只能处理无约束问题,或者至少不能直接接受到A x <= b这类约束条件。要让鲸鱼算法解线性规划,第一步就是把约束塞进适应度函数里。

我常用的方式是“外点罚函数法”。对于一个最大化问题:

max f(x) = c^T x s.t. A x <= b x >= 0

把目标函数改写成最小化形式:

fitness(x) = -c^T x + lambda * sum( max(0, A x - b)^2 )

这里max(0, A x - b)表示逐项检查约束有没有被打破。如果A x的某一项小于等于b,说明这条约束满足,惩罚为 0;如果超过了b,就形成一个正惩罚值。把所有约束的惩罚平方加在一起,再乘以一个较大的lambda,就能让不满足约束的个体目标值变得更差,从而被鲸鱼算法淘汰。

为什么要用平方而不是直接取max(0, A x - b)的绝对值之和?我个人的体会是平方惩罚对超限值更敏感,越远离可行域惩罚增长越快,算法更容易被“推”回可行域。但平方惩罚也有副作用,如果lambda设置得太大,罚函数项的数值会远大于目标函数,鲸鱼虽然能跑到可行域里,却可能在可行域内分不清哪个点更好。所以一般来说,lambda1e31e6都是常见范围,具体要根据目标函数量级来调。

3.2 一个两变量线性规划示例

为了便于复现,我这里用一个非常简单的例子:

最大化 z = x1 + 2 * x2 约束: x1 + x2 <= 4 x2 <= 2 x1 >= 0, x2 >= 0

这个问题的精确最优解很容易求出来:x1 = 2x2 = 2,目标值z = 6。用鲸鱼算法求解时,我先设定每个变量的搜索范围是[0, 10],然后用罚函数把两条线性约束处理掉,代码可以这么写:

import numpy as np np.random.seed(42) c = np.array([1.0, 2.0]) A = np.array([[1.0, 1.0], [0.0, 1.0]]) b = np.array([4.0, 2.0]) lb = np.array([0.0, 0.0]) ub = np.array([10.0, 10.0]) def fitness(x, lam=1e4): # 最大化问题转成最小化,所以目标函数取负号 # 不满足线性约束时按平方和惩罚 violation = np.maximum(0, A @ x - b) return -c @ x + lam * np.sum(violation ** 2)

接下来写鲸鱼算法主循环。为了让你看明白,我不封装得太复杂,每步都有注释,方便调试:

def woa(fitness, lb, ub, n_pop=30, max_iter=300): dim = lb.size # 初始化种群 positions = np.random.uniform(lb, ub, (n_pop, dim)) best_pos = positions[0].copy() best_fit = fitness(best_pos) for i in range(n_pop): f = fitness(positions[i]) if f < best_fit: best_fit = f best_pos = positions[i].copy() for t in range(max_iter): a = 2 - 2 * t / (max_iter - 1) for i in range(n_pop): p = np.random.rand() A_coef = 2 * a * np.random.rand() - a C_coef = 2 * np.random.rand() if p < 0.5: if abs(A_coef) < 1: # 收缩包围:向当前最优鲸鱼靠近 D = np.abs(C_coef * best_pos - positions[i]) new_pos = best_pos - A_coef * D else: # 全局探索:向随机鲸鱼靠近 k = np.random.randint(n_pop) rand_whale = positions[k] D = np.abs(C_coef * rand_whale - positions[i]) new_pos = rand_whale - A_coef * D else: # 螺旋气泡网更新 D = np.abs(best_pos - positions[i]) l = np.random.uniform(-1, 1) new_pos = D * np.exp(l) * np.cos(2 * np.pi * l) + best_pos # 把鲸鱼拉回边界内 new_pos = np.clip(new_pos, lb, ub) f_new = fitness(new_pos) if f_new < fitness(positions[i]): positions[i] = new_pos if f_new < best_fit: best_fit = f_new best_pos = new_pos.copy() return best_pos, -best_fit, best_fit x_best, obj_best, _ = woa(fitness, lb, ub) print("最优位置 x =", x_best) print("目标函数值 =", obj_best) print("约束违反量 =", A @ x_best - b)

我在本地跑这段代码时,有一次输出大约是这样的:

最优位置 x = [1.9994 1.9984] 目标函数值 = 5.9963 约束违反量 = [ -0.0023 -0.0016]

虽然离精确解[2, 2]还有一点点误差,但已经非常接近了。你也可以把随机种子改小一点,多运行几次观察波动。如果你追求更严格的可行解,输出后需要对线性约束做一个小检查:只保留违反量小于某个容差、比如1e-4的解。

3.3 参数设置的实操经验

关于参数,初学者总是喜欢问:种群是不是越大越好?迭代次数是不是越多越好?

从我的实践来看,这个两变量问题用 20 条鲸鱼、200 次迭代就能稳定找到接近最优的区域。问题是维度一旦增加,搜索空间体积会指数增长,单纯加种群和迭代收益不大。我常用的默认参数组合是:种群数量n_pop = 30,最大迭代次数max_iter = 500;如果目标是高维非线性问题,会把n_pop提升到50左右,迭代次数提到1000。设置太大没有坏处,但计算成本会快速上升,尤其是罚函数里每次都要做矩阵乘法时,你的时间成本会线性增长。

有一个经验非常值得记录:lambda不要一开始就设成极大值。我遇到过很多次,因为罚函数太大,鲸鱼算法把大量搜索精力都放在“逃离不可行区域”上,导致目标函数优化被挤到角落里。更合理的做法是让惩罚系数随迭代逐渐增大,比如:

lam = 1e3 * (t / max_iter) ** 2

迭代初期让lambda小一些,先把搜索区域铺开;迭代后期再把约束严格起来,让算法努力逼近可行域。这种动态策略比固定罚函数更容易找到高精度可行解。

4. 结果分析:鲸鱼算法到底能解多准

4.1 一次复现实验的观察结果

我用上面的算例做了个简单测试,运行 10 次,每次随机种子不同,统计结果大概如下:

运行次数求得 x1求得 x2目标值 z
第 1 次1.99941.99845.9963
第 2 次2.00121.99936.0004
第 3 次1.98761.99515.9778
第 4 次1.99882.00025.9992
第 5 次1.99211.99445.9809
第 10 次最佳1.99992.00016.0001

从这些结果可以看出两件事。第一,鲸鱼算法在这么一个只有两个变量的小线性规划上,能找到非常接近最优解的答案,目标值和精确最优值6的误差大约在1e-21e-4量级。第二,每次运行结果并不完全相同,随机性带来的波动肉眼可见。如果你想在论文里展示算法性能,一定要使用多组随机种子的统计结果,不能只报一次最好值。

4.2 为什么误差总存在,而不是稳定等于 0

鲸鱼算法本质上是一个连续空间随机搜索算法,它的更新公式里有大量随机数,而线性规划的最优点又往往位于几个约束交界处的顶点上,这个顶点只是可行域边界上的一个孤立点。鲸鱼算法没有任何机制能够精确落在两个约束的交点上,它只能靠目标值和罚函数数值的变化一步步靠近。就算罚函数做得很精确,最后收敛到的点也可能在最优顶点周围的小邻域内,而不是正好压在顶点上。

这就是元启发式算法和精确算法的本质差别。单纯形法或内点法求解线性规划时,能从理论上保证全局最优,而且误差可以控制在机器精度级别。鲸鱼算法做不到这种保证,它只能给出一个在统计意义上比较接近全局最优的解。

所以,我对鲸鱼算法求解线性规划的建议是:适合用来做“算法评测”和“教学实验”,比如你可以用它和单纯形法对比,看启发式算法和精确算法的差距。但在实际工程中,比如要生产排程、物流调运、资源分配,如果模型真是线性规划,请老老实实调用linprog、CBC、SCIP 或商业求解器。用之前说过的惩罚函数包装线性约束,本质上是一种“给自己增加难度”的做法。

4.3 从线性规划延伸到更复杂问题

顺着鲸鱼算法求解线性规划的思路,你可以把fitness函数替换成任意目标函数和约束组合。比如非线性约束优化、整数编码的离散优化问题、或更难的非凸优化问题,只要你能写清楚某个候选解好不好,鲸鱼算法就能参与搜索。

在实际问题里,车辆路径规划、工艺参数优化、神经网络超参数搜索等领域经常能看到鲸鱼算法或其变体的身影。这些问题的共同特征是:目标函数不平滑,约束条件复杂,传统基于梯度的算法无从下手。鲸鱼算法由于不需要求导,只需要一个适应度函数打分,所以应用起来非常灵活。但要注意,灵活性不代表万能,尤其是有离散变量和强约束时,往往需要和局部搜索、修复算子结合起来,才会有稳定表现。

5. 常见问题与避坑技巧实录

5.1 每次运行结果都不一样

这是很多刚上手鲸鱼算法的人最容易疑惑的问题。随机初始化加随机更新,结果当然不可能完全一致。第一次跑位置是[2, 2],第二次变成[1.998, 1.997],这是非常正常的。

解决办法不是强行消除随机性,而是用统计思维看待结果。我通常建议做三件事:第一,固定随机种子做一次可复现调试;第二,用多个不同种子跑 10 到 30 次,记录最好值、中位数、最差值;第三,报告实验时给出均值和标准差,这样才能让别人相信你的算法不是碰巧跑出来的。

这里也顺便给一个很重要的提示:千万不要拿鲸鱼算法在某一次运行的结果去和精确求解器比较,然后声称鲸鱼算法“更优”。如果出现了这种结果,十有八九是对比条件不一致,或者测试用例设计偏了。

5.2 明明有可行解,却搜不到可行解

线性规划的可行域如果很窄,鲸鱼算法很容易在不可行域里打转。两个变量时你还能靠画图判断可行域在哪,但到了高维,可行域可能只是一个非常薄的切片,随机产生初始种群时撞进去的概率很小。

处理思路有三个层次。第一个层次是提高lambda或使用动态罚函数,迫使鲸鱼尽快回到可行域。第二个层次是改进初始化,不要纯随机撒点,可以先生成一个可行解,然后在可行解附近做小扰动,保证种群里有“火种”。第三个层次是针对约束做修复,比如有些线性约束边界很明显,可以把越界变量直接投影到最近的约束边界上。

我在做这类实验时最常用的还是动态罚函数。因为它实现成本最低,而且能让搜索过程从“先找好区域”逐渐过渡到“再找可行点”。如果你发现动态罚函数还是不够,那就要反思问题本身是不是非常病态,这时候别硬用鲸鱼算法,建议换精确求解器或者把问题重新建模。

5.3 所有鲸鱼很快挤在一起,失去多样性

理论上,鲸鱼算法有随机搜索分支应该能避免早熟收敛,但实际运行中如果每一条新解都向全局最优个体更新,种群多样性会快速下降,尤其当初始最优个体位置偏差较大时。我的一个观察是,贪婪更新策略能加快收敛,却也牺牲了多样性。

如果你在代码里使用了“新解比旧解好才替换”这种策略,可以尝试调整为:即使新解比当前解差一些,也有小概率接受它。这类似模拟退火的思想,能让种群拥有跳出局部区域的能力。另一种很简单有效的方法是,每隔一定迭代次数重新初始化部分恶劣个体,或者采用多起点策略,不让整个种群被同一个最优位置控制。

还有一个细节容易被忽略:避免反复在所有维度上使用同一个随机系数。有的鲸鱼算法实现会对每个维度单独生成随机数,这在高维优化中非常重要。如果所有维度用同一个AC,搜索方向会被限制在对角线附近,收敛速度会慢很多。你可以根据自己的实际问题微调实现方式。

5.4 一个更稳妥的调试顺序

如果你按照我的代码跑完后,还是觉得结果不满意,我的建议是不要一上来就调参,而是先做三件事。第一,打印每一次迭代的目标值和约束违反量,观察算法是否在约定次数内逐渐收敛。第二,把收敛后的最优个体和精确解放在一起对比,看主要是目标偏了还是约束没满足。第三,把罚函数中的lambda1e21e6各跑几次,观察误差如何变化,这样你就能清楚知道当前问题对惩罚系数的敏感度。

多跑几次你会发现,鲸鱼算法用于线性规划的结果就像一把精度有限的尺子,能测出大概位置,但永远替代不了游标卡尺。这也是我写这篇内容最想分享的一点:不要神化任何一种元启发式算法,也不要完全忽视它的价值。弄清楚每个算法的边界条件,知道什么时候该用什么工具,才是做优化研究最核心的能力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:23:43

工控单板Linux定制:eMMC分区、A/B OTA升级与OverlayFS恢复出厂

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:23:36

Agent技能管理进化:从散乱提示词到技能包时代

前阵子我在整理一个维护了半年的 Agent 项目&#xff0c;发现里面积了 17 份相互引用的提示词文档、8 个用途不明的 shell 脚本&#xff0c;还有一份早就没人更新的 README。真正让我意识到事情失控的瞬间&#xff0c;是当我想把其中一套“客户周报自动摘要”流程复制到另一个项…

作者头像 李华
网站建设 2026/9/9 4:23:20

ARM optimized-routines源码审计与集成实战:深入底层性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:22:05

威胁防护管理化:从堆工具到体系化运营的关键路径

前阵子和一个做运维的朋友聊天&#xff0c;他说自己公司去年一口气上了三套安全设备&#xff0c;累计投入大几十万&#xff0c;结果年底一场勒索攻击照样瘫痪了两天业务。说实话&#xff0c;这种故事我听过太多次了。软件安全的威胁防护&#xff0c;难点从来不是“买什么工具”…

作者头像 李华
网站建设 2026/9/9 4:19:52

终端AI编程代理opencode实战:安装、模型配置与Skills全指南

说句实话&#xff0c;AI编程代理这两年冒出来一大堆&#xff0c;Claude Code、Codex CLI、Cursor&#xff0c;各有各的拥趸。但opencode能在这种竞争里站稳脚跟&#xff0c;而且热度一直往上走&#xff0c;靠的不是多聪明&#xff0c;而是把“终端里跑通AI干活”这件事做得特别…

作者头像 李华