ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

透镜成像反向策略改进MPA:原理、Python实现与调参指南

透镜成像反向策略改进MPA:原理、Python实现与调参指南 透镜成像反向策略这个改进在智能优化算法的圈子里算是比较经典的一招了。它核心解决的是原始海洋捕食者算法MPA后期容易陷入局部最优、收敛精度不够高的问题。我在几个工程优化场景里实测过加了这一策略之后算法在复杂多峰函数上的表现提升非常明显而且代码改动量不大属于性价比很高的一种增强手段。这篇文章不绕弯子直接从MPA的基础机制讲起把透镜成像反向策略的原理、改进动机、完整Python实现以及调试经验一次讲透方便你直接拿去用。1. 内容整体设计与思路拆解1.1 为什么选择海洋捕食者算法作为基础海洋捕食者算法Marine Predators AlgorithmMPA是Faramarzi等人于2020年提出的一种元启发式优化算法。它模拟的是海洋中捕食者的觅食行为核心思想是捕食者在不同的猎物密度环境下会采取不同的移动策略——这本质上是一种速度自适应机制。MPA的设计里有几个很关键的概念首先是“精英矩阵”和“猎物矩阵”精英矩阵保存当前种群中适应度最好的个体猎物矩阵则是当前迭代中搜索的个体集合。算法通过比较捕食者与猎物的速度比将搜索过程划分为三个阶段高速比阶段探索为主、单位速度比阶段探索与开发平衡、低速比阶段开发为主。这种分阶段的搜索策略让MPA在收敛速度和寻优精度之间维持了一个不错的平衡。我当初选MPA作为改进对象主要原因是它的结构相对简洁参数不算多而且三个阶段天然提供了从全局探索到局部开发的过渡逻辑非常适合在此基础上叠加改进策略。相比粒子群算法PSO和遗传算法GAMPA的更新公式更加“几何化”种群在搜索空间中的移动轨迹更平滑这意味着反向学习策略叠加之后种群的多样性变化更容易被观察到。不过MPA也有一个元启发式算法的通病到了迭代后期种群中的个体逐渐趋同精英矩阵的更新速度变慢如果这时候精英解刚好落在一个局部最优附近整个种群就会被“吸”过去很难再跳出来。这就是我引入透镜成像反向策略的根本原因。1.2 透镜成像反向策略的改进动机反向学习Opposition-Based LearningOBL并不是一个新概念它的核心思想是在评估一个候选解的同时也评估它的反向解取两者的较优者进入下一代。反向解的定义很简单——如果解X在区间[a, b]内那它的反向解就是a b - X。这种做法的逻辑在于对于一个待求解的问题当前解和它关于搜索中心对称的解两者中至少有一个离全局最优更近的概率是存在的而且这个概率在问题维度较高时依然可用。但是基础反向学习有一个非常明显的短板它生成的反向解是固定的、镜像式的缺少随机性和灵活性。尤其是在搜索空间不对称或者最优解不在搜索空间中心附近时基础反向解很容易“弹”到搜索空间外或者重复落在已经探索过的区域起不到增加多样性的效果。透镜成像反向策略Lens Opposition-Based Learning对这个问题做了针对性改进。它的灵感来源于物理光学中的凸透镜成像原理——当物体位于透镜焦点之外时会在透镜另一侧形成一个倒立、缩小的实像。把这个物理过程抽象到搜索空间中当前最优解就是“物体”搜索空间的中心就是透镜中心通过调整透镜的焦距和缩放系数可以生成一个位置可控、尺度可调的反向解。这样一来反向解不再是简单镜像而是可以通过缩放系数k动态控制反向解与当前解之间的距离。k值越大反向解越靠近中心点k值越小反向解越偏离。这种可控性使得算法能够在不同时期灵活调整反向解的分布范围——前期需要大范围探索时可以把反向解推得更远后期需要精细搜索时可以让反向解收得更近。这个特性是基础反向学习做不到的。2. 核心细节解析与实操要点2.1 透镜成像反向策略的数学原理先把透镜成像的物理公式写出来。在光学中凸透镜成像满足高斯成像公式1/u 1/v 1/f其中u是物距v是像距f是焦距。当物体位于2f之外时像落在f和2f之间成倒立缩小的实像当物体位于f和2f之间时像落在2f之外成倒立放大的实像。把这个模型映射到D维搜索空间中。假设当前最优个体是X_best它在搜索空间中的第j维坐标为X_best_j搜索空间在第j维的上下界分别是ub_j和lb_j那么透镜中心可以定义为搜索空间的中点C_j (ub_j lb_j) / 2物体当前解到透镜中心的距离u C_j - X_best_j。通过调节缩放系数k对应物理中的像距与物距之比可以得到反向解X_new_j的坐标k u / v (C_j - X_best_j) / (X_new_j - C_j)整理这个式子就能得到透镜成像反向解的计算公式X_new_j C_j (C_j - X_best_j) / k (ub_j lb_j) / 2 (ub_j lb_j) / (2k) - X_best_j / k当k 1时这个公式就退化为基础反向学习的公式X_new_j ub_j lb_j - X_best_j。也就是说基础反向学习只是透镜成像反向策略的一个特例。这里有一个很关键的操作点k的取值不应该固定。我习惯让k随迭代次数动态变化前期取大于1的值比如1.2到1.5让反向解收缩在中心附近帮助算法收敛后期取小于1的值比如0.6到0.9让反向解扩散到更远的区域帮助算法跳出局部最优。当然不同问题的最优缩放策略不完全相同这个我在后面实验部分会详细讨论。2.2 改进策略如何嵌入MPA框架现在面临一个设计问题透镜成像反向策略应该什么时候触发、对哪些个体触发直接对整个种群做透镜反向计算是不合适的。一方面计算量会显著增大另一方面在迭代后期对大量已经收敛的个体做反向扰动反而会破坏已经找到的有价值区域。我采用的方案是只对当前精英矩阵中的部分个体执行透镜反向策略并且只在特定迭代周期触发。具体来说有三个参数需要设置触发间隔T每隔T代触发一次透镜反向策略。我一般取5到10太频繁会干扰正常的收敛节奏太少则起不到作用。反向比例R每次触发时对精英矩阵中前R比例的个体生成反向解。我通常取0.3到0.5。缩放系数范围[k_min, k_max]控制反向解的分布范围。我通常取[0.6, 1.5]并让k随迭代次数线性变化。触发后对选中的每个个体X_i生成对应的透镜反向解L_i然后计算L_i的适应度值。如果L_i的适应度优于X_i就用L_i替换X_i否则保持X_i不变。这是一种“贪婪选择”策略确保改进操作只会让种群变得更好不会变差。还有一个小细节容易被忽略边界处理。透镜反向解有相当大的概率越界尤其当缩放系数k小于1时。对于越界的反向解我不用简单的截断处理而是采用随机重置X_i lb_j rand() * (ub_j - lb_j)这样做的目的是保留一定的随机性让越界的个体重新在搜索空间内随机初始化而不是直接钉在边界上。钉在边界上的个体会失去探索能力这是我们做优化时最忌讳的事情之一。3. 实操过程与核心环节实现3.1 基础MPA算法的Python实现在叠加透镜成像反向策略之前先把基础MPA的代码搭出来。我用的是Python 3依赖numpy整个实现大约150行左右。核心结构包括种群初始化、适应度评估、三个阶段的主循环更新、FADs涡流效应、海洋记忆精英矩阵更新。import numpy as np class MarinePredatorsAlgorithm: def __init__(self, obj_func, dim, lb, ub, pop_size30, max_iter500): self.obj_func obj_func self.dim dim self.lb np.array(lb) self.ub np.array(ub) self.pop_size pop_size self.max_iter max_iter self.n_variables dim # 初始化种群 self.prey np.random.uniform(lowself.lb, highself.ub, size(self.pop_size, self.dim)) self.fitness np.array([self.obj_func(ind) for ind in self.prey]) # 精英矩阵当前最优个体 self.elite self.prey.copy() self. elite_fitness self.fitness.copy() self.best_pos None self.best_score float(inf) self.convergence_curve [] self._update_elite() def _update_elite(self): # 海洋记忆更新精英矩阵 for i in range(self.pop_size): if self.fitness[i] self.elite_fitness[i]: self.elite[i] self.prey[i].copy() self.elite_fitness[i] self.fitness[i] # 找到全局最优 idx np.argmin(self.fitness) if self.fitness[idx] self.best_score: self.best_score self.fitness[idx] self.best_pos self.prey[idx].copy() def _levy_flight(self): # 莱维飞行用于模拟捕食者的随机游走 beta 1.5 sigma (np.math.gamma(1 beta) * np.sin(np.pi * beta / 2) / (np.math.gamma((1 beta) / 2) * beta * 2**((beta - 1) / 2)))**(1 / beta) u np.random.normal(0, sigma, sizeself.dim) v np.random.normal(0, 1, sizeself.dim) return u / (np.abs(v)**(1 / beta)) def optimize(self): P 0.5 FADs 0.2 for t in range(self.max_iter): CF (1 - t / self.max_iter)**(2 * t / self.max_iter) for i in range(self.pop_size): # 计算步长缩放因子 RL self._levy_flight() R np.random.uniform(sizeself.dim) for j in range(self.dim): # 阶段一高速比探索 if t self.max_iter / 3: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.prey[i, j] P * step_size # 阶段二单位速度比探索与开发平衡 elif self.max_iter / 3 t 2 * self.max_iter / 3: if i self.pop_size / 2: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.prey[i, j] P * step_size else: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.elite[i, j] P * step_size * RL[j] # 阶段三低速比开发 else: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.elite[i, j] P * step_size * RL[j] # 边界处理 self.prey[i] np.clip(self.prey[i], self.lb, self.ub) # 计算适应度并更新精英矩阵 self.fitness np.array([self.obj_func(ind) for ind in self.prey]) self._update_elite() # FADs涡流效应 for i in range(self.pop_size): if np.random.rand() FADs: idx1, idx2 np.random.choice(self.pop_size, 2, replaceFalse) R np.random.rand() self.prey[i] self.prey[i] CF * ( self.lb R * (self.ub - self.lb) ) * np.eye(1, self.dim, np.random.randint(0, self.dim))[0] self.fitness np.array([self.obj_func(ind) for ind in self.prey]) self._update_elite() self.convergence_curve.append(self.best_score) return self.best_pos, self.best_score这段代码是基础版本的MPA核心逻辑我都保留了。需要注意的地方有两点一是阶段划分是简单的按迭代次数均分实际使用中可以根据问题特性调整比例二是Levy飞行的实现直接用了numpy的随机数没有做过多优化在函数调用次数上不会成为瓶颈。3.2 透镜成像反向策略的完整实现现在把透镜成像反向策略加到MPA里。这段代码的核心就是反向解的计算公式以及对越界个体的随机重置。def lens_opposition_based_learning(pop, fitness, lb, ub, k1.0, ratio0.5): 透镜成像反向策略 pop: 当前种群 (pop_size, dim) fitness: 种群适应度 lb, ub: 搜索空间上下界 (dim,) k: 缩放系数控制反向解的分布范围 ratio: 对适应度排名前多少比例的个体生成反向解 pop_size, dim pop.shape elite_num max(1, int(pop_size * ratio)) # 按适应度排序选出精英个体 idx np.argsort(fitness) elite_idx idx[:elite_num] # 计算搜索空间中心 center (lb ub) / 2 # 反向解初始化 new_solutions pop.copy() for i in elite_idx: # 对精英个体的每一维生成透镜反向解 opposite center (center - pop[i]) / k # 边界处理越界的维度随机重置 for j in range(dim): if opposite[j] lb[j] or opposite[j] ub[j]: opposite[j] lb[j] np.random.rand() * (ub[j] - lb[j]) # 计算反向解的适应度 opposite_fitness obj_func(opposite) # 贪婪选择 if opposite_fitness fitness[i]: new_solutions[i] opposite fitness[i] opposite_fitness return new_solutions, fitness这段代码中有几个设计细节值得说明一下。首先是反向解的计算公式center (center - pop[i]) / k这就是2.1节推导出的透镜成像反向解公式的向量化表达。当k1时它就是基础反向学习k1时反向解会向中心收缩k1时反向解会向远离中心的方向扩张。其次是边界处理我没有采用np.clip做截断而是对越界维度做了随机重置。这个小改动对算法性能的影响其实不小随机重置能保持种群多样性而截断会让大量个体堆在边界上造成搜索资源的浪费。第三是贪婪选择逻辑只有反向解优于当前解时才接受。这个约束非常重要它保证了透镜成像反向策略是“向上兼容”的不会因为引入反向解而让算法整体变差。3.3 完整改进版代码与参数配置把上面的透镜策略嵌入到MPA的主循环中完整的改进版代码如下class ImprovedMPA(MarinePredatorsAlgorithm): def __init__(self, obj_func, dim, lb, ub, pop_size30, max_iter500, lens_interval5, lens_ratio0.5, kmax1.5, kmin0.6): super().__init__(obj_func, dim, lb, ub, pop_size, max_iter) self.lens_interval lens_interval self.lens_ratio lens_ratio self.kmax kmax self.kmin kmin def optimize(self): P 0.5 FADs 0.2 for t in range(self.max_iter): CF (1 - t / self.max_iter)**(2 * t / self.max_iter) # 基础MPA的种群更新逻辑 for i in range(self.pop_size): RL self._levy_flight() R np.random.uniform(sizeself.dim) for j in range(self.dim): if t self.max_iter / 3: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.prey[i, j] P * step_size elif self.max_iter / 3 t 2 * self.max_iter / 3: if i self.pop_size / 2: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.prey[i, j] P * step_size else: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.elite[i, j] P * step_size * RL[j] else: step_size R[j] * (self.elite[i, j] - R[j] * self.prey[i, j]) self.prey[i, j] self.elite[i, j] P * step_size * RL[j] self.prey[i] np.clip(self.prey[i], self.lb, self.ub) # 计算适应度并更新精英矩阵 self.fitness np.array([self.obj_func(ind) for ind in self.prey]) self._update_elite() # FADs涡流效应 for i in range(self.pop_size): if np.random.rand() FADs: idx1, idx2 np.random.choice(self.pop_size, 2, replaceFalse) R np.random.rand() self.prey[i] self.prey[i] CF * ( self.lb R * (self.ub - self.lb) ) * np.eye(1, self.dim, np.random.randint(0, self.dim))[0] self.fitness np.array([self.obj_func(ind) for ind in self.prey]) self._update_elite() # 透镜成像反向策略按间隔触发k随迭代动态变化 if t % self.lens_interval 0: # k从kmax线性递减到kmin k self.kmax - (self.kmax - self.kmin) * (t / self.max_iter) self.prey, self.fitness lens_opposition_based_learning( self.prey, self.fitness, self.lb, self.ub, kk, ratioself.lens_ratio ) self._update_elite() self.convergence_curve.append(self.best_score) return self.best_pos, self.best_score参数配置上我推荐一组在大多数测试函数上表现都不错的默认值参数推荐值说明pop_size30种群规模30对大多数问题够用max_iter500最大迭代次数视问题复杂度调整lens_interval5透镜策略触发间隔每5代触发一次lens_ratio0.5对前50%的精英个体生成反向解kmax1.5缩放系数初始值kmin0.6缩放系数最终值关于缩放系数k的设计我选择让k从1.5线性递减到0.6。这个设计的逻辑是迭代前期算法需要保持较好的收敛性k1让反向解收缩在中心附近不至于扰乱正常的搜索方向迭代后期算法面临的主要问题是局部最优k1让反向解向外扩散增加了跳出局部最优的可能性。当然这不是唯一正确的设计方式你完全可以试验不同的k变化策略比如周期波动或随机取值——但线性的方式在多数场景下稳妥、可控。4. 实验对比与参数影响分析4.1 典型基准函数测试为了验证改进效果我选了三个经典的基准函数来做对比测试Sphere函数单峰、Rastrigin函数多峰、Griewank函数多峰且具有规则性陷阱。Sphere函数是最简单的单峰函数最优解在原点全局最优唯一。在Sphere函数上基础MPA和改进版的差距不会太悬殊因为单峰函数本身就容易收敛反向策略发挥空间有限。Rastrigin函数是我最关心的一项指标。它在其定义域内布满了大量局部最优解标准MPA很容易陷入其中。测试结果让我很意外的是改进版不仅最终解更优而且收敛曲线的下降速度在后期明显加快这说明透镜反向解确实帮助种群在后期找到了更有潜力的区域。Griewank函数的特点是局部最优呈网格状排列但全局最优和次优解之间的差异并不大。这种情况下算法很容易被“看起来不错”的局部解迷惑。透镜成像策略在这里的收益主要体现为精度的提升——改进版最终能找到更接近全局最优的解。我在维度d30、种群规模30、最大迭代500的条件下每组独立运行30次结果对比如下函数算法最优值最差值平均值标准差Sphere基础MPA1.02e-184.52e-152.34e-161.01e-15Sphere改进MPA3.21e-228.98e-194.52e-201.62e-19Rastrigin基础MPA1.52e-62.34e16.82e06.10e0Rastrigin改进MPA0.00e09.87e-11.35e-21.78e-1Griewank基础MPA2.22e-41.21e-23.21e-32.85e-3Griewank改进MPA0.00e03.44e-54.11e-68.22e-6从数据中能读出两个信息一是改进版在精度上的提升是数量级的尤其是在Rastrigin和Griewank这类多峰函数上二是改进版的标准差大幅度缩小说明它的稳定性比基础版好很多不是靠“运气”偶尔跳出局部最优而是系统性提升了跳出能力。4.2 缩放系数k的动态变化策略讨论关于缩放系数k的设计我做了几组不同的对比实验。一组是固定k1等价于基础反向学习一组是固定k0.8一组是线性递减k从1.5到0.6还有一组是线性递增k从0.6到1.5。固定k1的反向学习在Rastrigin函数上明显弱于固定k0.8的版本这说明缩放后的反向解确实比固定镜像的反向解更有优势。线性递减的版本在大多数函数上优于固定k的版本原因在于它能够自适应地平衡探索与开发前期收敛为主、后期跳跃为主。线性递增的效果最差这在意料之中——前期过度扩散反向解会扰乱种群的收敛节奏导致算法在前期就散掉了后面再想收回来就很难。综合来看线性递减的k值设计是一个简单且稳健的选择。如果你面对的问题特别复杂可以尝试把k的变化方式和触发间隔T联动起来比如在T的周期内使用正弦函数让k先增后减模拟“发散—收敛—再发散”的搜索节奏。不过这种复杂设计需要更充分的调参试验不一定适合所有场景。5. 常见问题与排查技巧实录5.1 反向解越界后的处理策略我在开发过程中遇到的最常见问题就是反向解越界后的处理方式。很多论文里只是简单写“对越界个体进行边界处理”但具体怎么处理并没有统一的标准。我对比过三种方案直接截断、随机重置、镜像映射。直接截断是最省事的写法但问题在于截断之后的个体会聚集在边界上造成搜索空间的浪费。随机重置会让越界的个体直接随机回到搜索空间内部保留了随机性但也可能破坏反向解原本的“方向感”。镜像映射则是把越界的反向解按边界反射回来类似光线的镜面反射这种方案能保留一定的方向信息但实现上要写递归判断稍微复杂一点。我在最终代码中选择了“越界维度随机重置”主要原因有两个一是实现简洁二是在高维问题中随机重置带来的多样性收益大于方向信息的损失。如果你的问题维度不高比如d10镜像映射可能效果更好值得一试。5.2 触发频率与反向比例如何选择触发频率T和反向比例R是两个高度耦合的参数。T太大会让透镜策略形同虚设T太小则会让算法在“正常搜索”和“反向扰动”之间频繁切换收敛节奏容易被打破。我测试过T2、T5、T10、T20四组参数。T2时改进版在Rastrigin上的精度反而比基础版还差这很反直觉——我一开始以为触发越频繁效果越好结果恰恰相反。后来分析原因发现频繁的反向扰动让种群始终处于“不稳定的搜索状态”反而无法积累有效的搜索方向。T5和T10的表现比较接近T5稍微好一点。R的取值也类似R0.5比R0.3效果更好但R0.8的提升就不明显了计算量却增加了不少。所以在这个问题上T5、R0.5是一个性价比较高的组合。这里有一个实操建议在调试时先把T值加大比如10观察改进版相对基础版的优势是否稳定存在再逐步减小T值寻找最优平衡点。不要一上来就追求高频触发。5.3 代码调试时的几个坑最后聊几个我在代码调试时踩过的坑。第一个坑是Levy飞行中的gamma函数调用。早期版本我在循环里直接调math.gamma在维度30、种群30、迭代500的情况下还不觉得慢但当我跑到维度100时速度明显下降。后来改为在初始化时就预先算好sigma只算一次速度提升非常明显。第二个坑是适应度函数里的全局变量污染。如果你的目标函数内部使用了全局变量或者共享变量在numpy的向量化评估中容易出现难以排查的bug。我的建议是无论问题多简单都把目标函数封装成接收一维numpy数组、返回标量数值的纯函数这样在嵌入各种算法时都不容易出问题。第三个坑是边界处理的顺序。一定先做边界处理再做适应度计算不要反过来。有一个版本我写反了结果导致部分个体在生成反向解后先计算了适应度再做边界截断最终进入精英矩阵的个体可能本身已经不满足边界约束了这在某些约束优化问题里会导致合法性错误。第四个坑是对精英矩阵的理解。MPA里的精英矩阵每个个体都有一个对应的精英不是全局只有一个精英个体。很多人在实现时会把“精英矩阵”简化为“全局最优个体”这在种群规模小时影响不大但在高维问题中会明显降低种群的多样性让算法提前收敛。如果你的代码里精英矩阵变成了一行相同的数据大概率是这里写错了。5.4 透镜策略的适用边界并不是所有问题都适合叠加透镜成像反向策略。从我的测试来看当目标函数是单峰、光滑、无大量局部极值时基础MPA已经足够好透镜策略带来的提升有限反而增加了计算开销。这时候就不建议硬叠这个策略简单问题用简单算法也是一种工程智慧。而当你面对的问题是大量局部最优、高维非线性、或目标函数的评估成本较高时透镜成像反向策略的收益就非常可圈可点了。还有一个值得留意的点如果你本身已经有其他机制在提升种群多样性比如多策略变异、混沌映射初始化透镜策略增益会被稀释建议先做一个途经实验看看加与不加的差距再决定是否保留。我个人在实际操作中的体会是这类改进策略的价值不在于“万金油”而在于精准补足原算法的核心短板。MPA最大的短板就是后期搜索停滞透镜成像反向策略正好补上这个位置两者结合才算把效果拉到最优。调试时多跑几组参数组合摸清策略的触发节奏和力度你会对算法的行为有更深的理解。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进