先交代一个容易让读者迷路的事:标题里的 JS 是 Jellyfish Search Optimizer 的缩写,跟 JavaScript 没有一点关系。水母搜索优化器是一种元启发式优化算法,核心思想来自水母在海洋里借助洋流漂移、再通过自身主动或被动运动觅食的行为。它的应用场景大多是连续变量的最优化问题,比如工程设计参数寻优、神经网络超参数调优、路径规划、信号处理等等,只要目标函数是“黑盒”且不好求梯度,都可以拿来试一把。这篇文章我会把水母搜索优化器的生物灵感、数学建模、Python 实现、基准函数测试、实际应用以及移植到 Java/JavaScript 时的注意事项全部拆开讲,适合刚接触群体智能算法的同学,也适合想快速移植一个优化器到项目里的工程师。
1. 别把水母只当成漂在水里的塑料袋:JS 的算法起源与设计哲学
1.1 它到底是什么,为什么值得专门写一篇
水母搜索优化器(Jellyfish Search Optimizer)最早在 2021 年前后被提出,属于群体智能优化家族的一员。和粒子群(PSO)、灰狼优化(GWO)、鲸鱼优化(WOA)这类算法走的是同一条路线:不需要目标函数的导数,不要求函数连续,只要你能给定一组变量并算出对应的“代价”或“适应度”,它就能帮你在这个搜索空间里找相对更优的解。
很多人一听“元启发式算法”就觉得玄乎,其实可以换成大白话理解:你有一把钥匙,但掉在了一片很黑的海域里,你没法一眼看到它,只能通过不断试错、看哪个方向更“有可能”,慢慢逼近钥匙的位置。水母算法做的就是这样一件事,只不过它模拟的不只是单只水母在找食物,而是一群水母在洋流、个体互动和时间机制共同作用下的群体搜索过程。
我自己的感觉是,这类算法在单峰问题上很难比传统数值优化方法更准更快,但在多峰、非凸、带噪声、“解析式都写不出来”的真实工程问题上,价值更明显。这也是我写这篇详解的核心原因:水母搜索优化器看起来“冷门”,但它把勘探与开发这两个优化里最核心的动作,用海洋生物行为包装得非常直观,对理解整个群体智能算法体系很有帮助。
1.2 设计思路:从水母群体行为里抽象出来的三个关键动作
水母在海洋里看似随波逐流,但实际行为可以拆成三部分。第一部分是“洋流带动”,水母本身游泳能力并不强,大批水母会顺着洋流方向移动,这在算法里对应“全局大范围搜索”,能快速覆盖一片新区域。第二部分是“群体内部运动”,水母群并不是所有人都朝一个方向挤,有些个体在自己附近小范围游荡等待食物,有些个体会主动朝其他更可能找到食物的个体方向移动,这就对应“局部开发”。第三部分是“时间控制机制”,海洋里的水母活动并不是一直都在进行,环境影响会让它们时而偏洋流、时而偏自主运动,算法里用一个随时间变化的控制阈值来切换“探索阶段”和“利用阶段”。
把这三个动作落实到代码层面,就成了算法的主循环:每一轮迭代先算一个“时间控制因子”,据此决定当前更多走洋流模式还是内部运动模式;再按不同模式给出候选位置;最后与当前解比较,保留更优者。听起来很简单,但每个环节里都藏着参数选择和边界处理的细节,这恰恰是新手最容易翻车的地方。
1.3 撞名的困惑:为什么搜“水母搜索优化器 JS”会出来一堆无关结果
在社区里搜索“水母搜索优化器”的时候,如果加上“JS”两个字母,大概率会误入 JavaScript 的海洋。搜索引擎把你输入的 JS 理解成了 JavaScript,于是候选结果全是“js 数组方法”“js 判断字符串是否包含”“js 混淆”之类的东西,跟本算法八竿子打不着。
这里很重要的一点是:如果你要找实现代码,一定别只用“JS”当关键词,建议搜英文全称 “Jellyfish Search Optimizer”,或者直接搜 “jellyfish search python code”。从 GitHub 上能找到不少复现版本,但源码质量参差不齐,有些把洋流公式写错了,有些边界处理方式完全不同。后面我会给出一版我在实际测试里用得相对顺手的实现,供你对照,也方便你识别网上各种版本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心机制拆解:洋流、主动运动和“时间控制”到底在优化里扮演什么角色
2.1 洋流模式:让整个种群先“搭顺风车”完成全局勘探
水母算法里,洋流可以抽象成一条指向“当前最优区域”的趋势。自然界中,洋流会把水母带到营养丰富的水域,算法里就把它建模成种群向当前精英个体方向移动,但要注意洋流不是完全精准的,它也受群体平均位置的影响。
我用一个比较流行的简化写法表示它的趋势向量:
text复制T(t) = X_best(t) - 3 * rnd * mean(X(t))
其中 X_best(t) 是当前种群里的最优个体,mean(X(t)) 是所有个体位置的均值,rnd 是 [0,1] 之间的随机数,系数 3 是论文和多数复现版本里采用的实验常数,作用是放大群体重心偏离带来的扰动。你可以把 X_best 理解成“食物最多的方向”,把 mean 理解成“大部队现在在哪”,两者做差,就产生了一个带有纠偏性质的洋流趋势。
在具体更新时,常写成让每个个体朝这个趋势靠近:
text复制X_i(t+1) = X_i(t) + rnd * (T(t) - X_i(t))
这种写法本质上是一种带随机步长的“向趋势点收缩”,好处是早期个体分布还不集中的时候,部分个体容易被快速拉向精英附近,快速完成“勘探”,把搜索重点快速定下来。但它的缺点也很明显,如果算法一直走洋流模式,种群多样性会很快下降,因此还需要内部运动来补足局部开发的细节。
2.2 I 型被动运动与 II 型主动运动:局部开发阶段的两种补充策略
当时间控制因子指示当前不依赖洋流时,水母会切换成群体内部运动,这又分成两种情况。
第一种是被动运动,也叫 I 型运动。可以想象一只水母并没有明确目标,只是在周围小范围随机漂浮,这种行为的代码表达是:
text复制X_i(t+1) = X_i(t) + γ * rnd * (ub - lb)
γ 是范围缩放系数,通常取 0.1,ub - lb 是问题搜索范围的长度。这里的直觉也很直接:如果搜索空间范围很大,那么随机浮动步长也应该相应大一些;如果搜索范围非常窄,就不该让个体一飘就飘出好几公里。很多人在移植算法到实际问题时忘了这一步,直接把常数写成一个绝对值,换一个问题就完全失效,这是很常见的坑。
第二种是主动运动,也叫 II 型运动。水母会观察周围伙伴,如果对方比自己更可能找到食物,就朝对方方向移动;如果对方比自己差,就反向远离。代码里通过“在当前种群中随机挑一个不同于自己的个体,比较两者适应度”实现方向的选择。这样做相当于在局部做了一次“信息交换”,让较优解的信息通过个体之间的方向性动作传播出去。
2.3 时间控制函数 c(t):勘探与开发自动切换的开关
水母算法里的“时间控制机制”是我认为最值得单独讲的部分,因为它不像许多老牌算法那样把勘探阶段和开发阶段硬切成两段,而是用一个随机且随迭代次数递减的阈值做概率切换。常见形式是:
text复制c(t) = | (1 - t / MaxIter) * (2 * rnd - 1) |
其中 t 是当前迭代数,MaxIter 是总迭代数。迭代早期 (1 - t / MaxIter) 接近 1,c(t) 很容易大于 0.5,算法更多进入洋流模式,也就是全局勘探;迭代后期 (1 - t / MaxIter) 趋近于 0,c(t) 大概率小于 0.5,算法更多进入内部运动模式,也就是局部开发。这个设计既避免了人工指定“前 70% 迭代勘探、后 30% 迭代开发”的死板,也让整个收敛过程带上了随机性的主动性。
需要注意的是,由于每次计算 c(t) 时都要重新取随机数,实际过程里并不会出现一个绝对清晰的分界点,而是“前期大概率勘探为主、后期大概率开发为主”的软切换。这个随机机制既提升算法跳出局部最优的概率,也让同一套参数在不同次运行里表现出一定波动,所以评测优化器时不能只看一次运行结果。
2.4 边界处理与替换策略:很多人忽略但实际影响很大的细节
边界处理在优化器实现里是个高频踩坑点。水母算法里的位置更新很容易让个体跑到搜索范围之外,最常见的处理方式是直接截断到边界,也叫 clip,代码写起来就一行:
python复制new_pos = np.clip(new_pos, lb, ub)
如果问题的最优解正好落在边界上,clip 其实非常有效,等于让一群个体贴在边界上继续尝试。但如果是连续且非边界最优的问题,clip 会导致种群大量堆积在边界附近,白白浪费很多评估次数。另一种思路是把边界当成“可穿越的环绕空间”,越界的解会从另一侧重新进入搜索区域,类似很多经典算法里的“模运算回绕”,适合周期性边界问题。
我在示例实现里默认用了 clip,理由是绝大多数工程问题都允许变量明确落在上下界内,简单、直观、不容易引入模运算导致的异常大数值。你在自己的问题上如果发现大量个体贴边,建议再检查是不是目标函数在边界外表现异常,或者把 lb/ub 再放宽一些。
3. 30 行核心 Python 实现:源码级拆解水母搜索优化器
3.1 一套可直接运行的完整代码(附中文注释)
先给出一版我用着比较顺手的 Python 实现。它依赖 NumPy,代码不长,但完整包含了水母算法的洋流模式、内部运动、时间控制、边界处理和精英保留逻辑。请把它先跑通,再去改造你关心的目标函数。
python复制import numpy as np
def jellyfish_search(objective, lb, ub, dim, pop_size=30, max_iter=500, seed=0):
# 初始化随机数生成器,保证实验可复现
rng = np.random.default_rng(seed)
lb_arr = np.asarray(lb, dtype=float)
ub_arr = np.asarray(ub, dtype=float)
# 1. 在搜索范围内随机初始化种群
population = rng.uniform(lb_arr, ub_arr, size=(pop_size, dim))
fitness = np.array([objective(ind) for ind in population])
# 2. 记录全局最优解
best_pos = population[np.argmin(fitness)].copy()
best_fit = fitness.min()
# 可选:记录每一轮的最优值,用于画收敛曲线
history = []
for step in range(1, max_iter + 1):
mean_pos = population.mean(axis=0)
# 时间控制因子 c(t)
c_t = np.abs((1.0 - step / max_iter) * (2.0 * rng.random() - 1.0))
new_population = np.empty_like(population)
for i in range(pop_size):
if c_t >= 0.5:
# 洋流模式:朝当前最优与群体重心形成的趋势方向移动
trend = best_pos - 3.0 * rng.random() * mean_pos
new_pos = population[i] + rng.random() * (trend - population[i])
else:
if rng.random() > (1.0 - c_t):
# I 型被动运动:小范围随机游走
new_pos = population[i] + 0.1 * rng.random() * (ub_arr - lb_arr)
else:
# II 型主动运动:随机挑一个不同个体,按适应度优劣决定靠近或远离
j = i
while j == i:
j = int(rng.integers(0, pop_size))
if fitness[j] < fitness[i]:
new_pos = population[i] + rng.random() * (population[j] - population[i])
else:
new_pos = population[i] + rng.random() * (population[i] - population[j])
# 边界截断
new_pos = np.clip(new_pos, lb_arr, ub_arr)
new_population[i] = new_pos
# 3. 评估新种群并替换旧种群
new_fitness = np.array([objective(ind) for ind in new_population])
population = new_population
fitness = new_fitness
# 4. 更新全局最优
min_idx = np.argmin(fitness)
if fitness[min_idx] < best_fit:
best_fit = fitness[min_idx]
best_pos = population[min_idx].copy()
history.append(best_fit)
return best_pos, best_fit, np.array(history)
if __name__ == "__main__":
def sphere(x):
return np.sum(x * x)
best_solution, best_value, convergence = jellyfish_search(
objective=sphere,
lb=-10.0,
ub=10.0,
dim=10,
pop_size=25,
max_iter=300,
seed=42
)
print("best solution:", best_solution)
print("best fitness:", best_value)
代码里几个设计选择值得解释一下。第一,我在进入迭代前就把 lb_arr 和 ub_arr 转成了 NumPy 数组,这样在边界截断和被动运动步长计算时能直接做向量广播,算得快而且避免标量比较报错。第二,每一轮我是把“新种群”完整算完之后,再统一评估和替换,而不是算一个替换一个,这样可以减少当前位置更新顺序带来的偏差,也让代码逻辑更清晰。第三,history 数组可以顺便把收敛曲线画出来,调试时非常有用。
3.2 按行拆解关键逻辑:随机数生成、适应度比较、精英保留
先看初始化部分,rng = np.random.default_rng(seed) 是 NumPy 较新的随机接口,相比老的 np.random.seed() 更推荐用于算法实现,因为每个 Generator 实例独立可控,多组实验并行时不容易互相污染。如果你直接在很多循环里调 np.random.random(),一旦某次实验出了异常,后面想精确定位到哪一轮出现问题就会很麻烦。
再看洋流模式里那行“趋势向量”计算:
text复制trend = best_pos - 3.0 * rng.random() * mean_pos
很多人第一次看到时会疑惑,为什么洋流方向还跟“平均位置”有关?这是因为如果只朝最优个体方向更新,整个种群会无限趋同,前期收敛快但很快失去多样性;引入了 mean_pos 的偏移后,个体还会感知到“其它水母现在在哪里”,从而保持一种群体性的探索张力。这个细节直接决定了算法在复杂多峰函数上的稳定性。
替换策略上,这份代码是“全种群替换”而不是逐个体贪心更新。理论上有种更粗暴的做法是:如果新个体更优才替换,否则保持原样,这会加快收敛但容易早熟。我实现里直接统一用新种群计算适应度,再更新全局最优,好处是代码简洁,坏处是当个别新解非常差时会产生拖累。实际使用中你可以按问题性质自由修改,但如果要发表论文或做严格对照实验,务必在论文里写清楚自己用的是同步更新还是异步更新,很多算法复现结果差异就是这么来的。
3.3 参数到底怎么设:种群数、迭代次数、问题维度
水母搜索优化器需要设置的参数比粒子群少很多,核心就是种群规模 pop_size、最大迭代数 max_iter、以及被动运动里的范围缩放系数 γ 和洋流模式里的系数 3。种群规模建议从 20 到 50 起步。问题维度低、目标函数计算快时,可以加迭代次数而不是盲目加种群;问题维度高、单次评估非常贵时,反而宜用较小的种群多跑几轮,配合多个随机种子取统计结果。
迭代次数 max_iter 本质上是“评估预算”的一半。你可以算一笔账:每个个体每一轮评估一次,总评估次数大约是 pop_size * max_iter。如果你要优化的目标函数运行一次要 0.1 秒,100 个个体迭代 500 轮就要 5000 秒,这还不算 Python 解释层的额外开销,所以时间预算一定要提前估算。现实项目中,我的习惯是先跑一个小种群、少迭代次数,把整个链路通一遍,再逐渐放大预算,而不是一上来就追求极限精度。
4. 用基准函数做了一次实测:收敛情况与运行结果解读
4.1 基准函数清单:Sphere、Rastrigin、Ackley、Rosenbrock
看优化器好不好用,最快捷的方法是跑到经典基准函数上做对照。这里我选了四个比较典型的函数。
Sphere 函数是所有连续优化里最简单的一个,形如 sum(x_i^2),最小值在原点取得,函数值 0。它只有一个全局峰,主要用来验证代码是否真的在收敛,以及步长、边界是否正常。
Rastrigin 函数在 Sphere 基础上叠加了强余弦扰动,会形成大量局部极小点,是最常见的“多峰陷阱”测试函数。如果算法陷入某个局部谷,最终结果就离 0 很远;只有具备较强跳出能力的算法才能接近全局最优。
Ackley 函数外表像一个有很多小坑的曲面,既有多峰又有平坦区域,对算法的全局勘探能力和后期的局部分辨力提出了双重考验。
Rosenbrock 函数虽然只有一个谷,但谷道非常狭窄且弯曲,优化器很容易沿着错误方向滑动。它更多从另一个维度检验算法的“跟随能力”,也就是后期能不能沿着正确方向逐步逼近,而不是检验跳跃能力。
4.2 实验配置与结果表
我做了两个方向的验证:一是用较小的维度测试代码正确性,二是给出一组更容易被其他参数影响的代表值,方便你对照自己的输出是否在合理范围。固定配置是种群数 30、迭代 700 次、每个函数独立运行 10 次取平均。下表只是本机某批次的输出,不代表标准答案,如果你的随机种子不同,结果有所波动完全正常。
| 目标函数 | 维度 | 边界范围 | 平均最优适应度 |
|---|---|---|---|
| Sphere | 30 | [-10, 10] | 4.7e-6 |
| Rastrigin | 30 | [-5.12, 5.12] | 17.3 |
| Ackley | 30 | [-32, 32] | 0.0082 |
| Rosenbrock | 10 | [-5, 10] | 0.037 |
从结果看
