NSWOA多目标鲸鱼优化算法MATLAB实现详解
2026/9/11 21:29:54 网站建设 项目流程

简介:基于鲸鱼算法(NSWOA)的多目标优化求解Matlab代码包,面向智能优化算法、神经网络预测、路径规划等方向的本科生、研究生及科研人员,适合算法对比、课程实验与论文复现等场景。代码包以NSWOA为核心,完整集成非支配排序、变量初始化、目标函数评价、种群更新、替换染色体和结果绘图等子模块,并内置zdt1、zdt2、zdt3等多组常用测试函数;运行后可直接查看多目标Pareto前沿的收敛效果,直观展示算法在多个目标间的权衡能力,有助于理解多目标进化算法的迭代逻辑。压缩包共13个文件,其中11个为.m源文件,便于逐行阅读和修改;另附1个说明文档和1个示例效果图,整体仅24KB,结构精简、核心算法集中,能快速定位到主程序与关键子函数。以上代码同时支持Matlab 2014及2019a环境,且描述中注明内含运行结果,适合学习时对照验证。目前已有259人学习下载,尤其适合刚接触多目标优化、希望从代码层面入手掌握鲸鱼算法原理的教研人员。

1. 从单目标到多目标:鲸鱼算法为什么要改造成 NSWOA

做优化的人都有个直觉:单目标问题解出来是个点,多目标问题解出来是条线、一个面,甚至一片不规则的云。鲸鱼算法(Whale Optimization Algorithm, WOA)是 2016 年提出的群体智能算法,模拟座头鲸的泡泡网捕食策略,在单目标连续优化上表现亮眼,收敛快、参数少、实现简单。但把 WOA 直接扔到多目标问题上会立刻暴露短板:它没有维护种群多样性的机制,也没有判断一个解是否被另一个解支配的框架,最后所有鲸鱼会朝一个方向挤,得到一堆彼此雷同的解。

NSWOA 的全称是 Non-dominated Sorting Whale Optimization Algorithm,思路很直白:把 NSGA-II 里那套非支配排序和拥挤度距离搬进 WOA 的寻优循环里,让鲸鱼在搜索过程中既保留 WOA 的位置更新机制,又用 Pareto 支配关系筛选精英解。这样得到的不是唯一最优解,而是一组分布均匀、覆盖前沿的折中方案。工程上的价值在于,物流配送路径、车间调度、资源分配这类问题往往有多个互相冲突的目标,运维工程师、算法工程师和科研人员需要在一组 Pareto 解里根据业务偏好做决策,而不是被迫接受一个单点答案。这篇文章就用 MATLAB 把 NSWOA 的完整实现拆开讲,从核心算子到参数设置,再到和 NSGA-II 的对比,一步步写清楚。

2. 核心机制拆解:NSWOA 如何在迭代中同时顾全收敛性与分布性

2.1 非支配排序:给每个解一个「等级」而不是一个「分数」

多目标优化和单目标最大的区别在于比较方式。单目标可以用数值大小直接排序,多目标需要先判断支配关系。一个解 x1 支配 x2,当且仅当 x1 在所有目标上都不劣于 x2,且至少在一个目标上严格优于 x2。不满足这个条件时,两个解互不支配,它们属于同一个 Pareto 前沿。NSWOA 在每次迭代后都要对当前种群做一遍非支配排序,把解分成 F1、F2、F3 多个层级,F1 是当前最优前沿。

实现上,常见做法是用一个双层循环记录每个解被谁支配、支配了多少个解,然后逐层提取前沿。MATLAB 里写起来大约是这样:

function front = non_dominated_sort(fitness) [N, M] = size(fitness); % N个解,M个目标 dominate_count = zeros(N, 1); % 记录支配当前解的解数量 dominated_set = cell(N, 1); % 记录当前解支配的解集合 front = zeros(N, 1); % 每个解的Pareto层级 for i = 1:N for j = i+1:N if dominates(fitness(i,:), fitness(j,:)) dominated_set{i} = [dominated_set{i}, j]; dominate_count(j) = dominate_count(j) + 1; elseif dominates(fitness(j,:), fitness(i,:)) dominated_set{j} = [dominated_set{j}, i]; dominate_count(i) = dominate_count(i) + 1; end end end current_front = find(dominate_count == 0); % 第一前沿 rank = 1; while ~isempty(current_front) for idx = current_front front(idx) = rank; end next_front = []; for idx = current_front for dominated_idx = dominated_set{idx} dominate_count(dominated_idx) = dominate_count(dominated_idx) - 1; if dominate_count(dominated_idx) == 0 next_front = [next_front, dominated_idx]; end end end rank = rank + 1; current_front = next_front; end end

这段代码的核心逻辑是:先统计每个解被多少解支配,被零个解支配的划定到 F1,然后把 F1 的解从支配关系中「剔除」,更新计数,再找下一批零支配解,重复直到所有解都有等级。front数组最终给出每个解所在的层级。注意 MATLAB 的下标从 1 开始,代码里所有索引都对应这个习惯,直接用 0 会报错。

2.2 拥挤度距离:在同一前沿里挑「住得最宽敞」的鲸鱼

层级只解决了解的优劣问题,没解决分布问题。如果筛选精英时只看层级,同一前沿内随机选,解很容易堆在一起,Pareto 前沿中间缺一大块,边缘却挤满了点。拥挤度距离的思路是:对同一前沿的解,按每个目标分别排序,计算每个解与相邻两个解在目标空间的距离之和,距离越大说明周围越空旷,越值得保留。

function distance = crowding_distance(fitness, front) [N, M] = size(fitness); distance = zeros(N, 1); for level = 1:max(front) idx = find(front == level)'; if length(idx) <= 2 distance(idx) = inf; % 少于3个解时都设为无穷大,全部保留 continue; end for m = 1:M [~, order] = sort(fitness(idx, m)); sorted_idx = idx(order); distance(sorted_idx(1)) = inf; distance(sorted_idx(end)) = inf; f_min = fitness(sorted_idx(1), m); f_max = fitness(sorted_idx(end), m); if f_max - f_min < 1e-12 continue; % 目标值全相同,距离记为0 end for k = 2:length(sorted_idx)-1 distance(sorted_idx(k)) = distance(sorted_idx(k)) ... + (fitness(sorted_idx(k+1), m) - fitness(sorted_idx(k-1), m)) / (f_max - f_min); end end end end

边界解的距离被直接设为inf,这个细节很关键。边界解承担着扩展前沿的任务,即使中间区域密度很大,也必须把两端保住,否则下一次迭代前沿就向内收缩了。距离计算时要按目标分别归一化,不然量纲大的目标会主导距离值,量纲小的目标在筛选中几乎不起作用。如果某个目标的所有解值都相同,分母趋近于零,代码里用continue跳过,把这个目标的贡献记为零,避免产生 NaN。

2.3 鲸鱼位置更新机制:包围、泡泡网与随机搜索三选一

NSWOA 保留了 WOA 的位置更新策略,每次迭代每头鲸鱼以一定概率选择三种行为之一。第一种是包围猎物,位置向当前最优解靠拢;第二种是泡泡网攻击,用螺旋方程模拟座头鲸吐气泡向上盘旋的动作;第三种是随机搜索,向种群中随机一个个体移动,维持全局探索能力。

function new_positions = whale_update(positions, best_pos, a, b, l, p, r1, r2) [N, dim] = size(positions); new_positions = zeros(N, dim); for i = 1:N if p(i) < 0.5 if abs(a(i)) < 1 % 收缩包围:向当前最优解靠近 new_positions(i,:) = best_pos - a(i) * abs(b(i) * best_pos - positions(i,:)); else % 随机搜索:选择一个随机个体作为参照 rand_idx = randi(N); rand_whale = positions(rand_idx,:); new_positions(i,:) = rand_whale - a(i) * abs(b(i) * rand_whale - positions(i,:)); end else % 螺旋更新:以螺旋路径逼近最优解 dist = abs(best_pos - positions(i,:)); new_positions(i,:) = dist .* exp(b * l(i)) .* cos(2 * pi * l(i)) + best_pos; end end end

参数a随迭代从 2 线性递减到 0,控制了探索和开发的平衡。abs(a) < 1时鲸鱼向最优解收缩,abs(a) >= 1时跳出局部陷阱做全局随机搜索。注意这里用的是randi(N)随机选参照个体,和单目标 WOA 完全一致。问题在于多目标场景下「最优解」不再唯一——F1 前沿里每个解都是最优,选哪个作为best_pos直接影响搜索方向。NSWOA 的常见做法是在 F1 前沿里随机挑一个,每次迭代不同鲸鱼可能朝向不同的 Pareto 解移动,这样种群不至于全部塌缩到同一个点。我在实际写代码时会在每次迭代开始时先对 F1 前沿做一次洗牌,然后每头鲸鱼按自己的序号取模选参照,比每次都重新调用randi更稳定。

3. 在 MATLAB 中搭建 NSWOA 求解多目标问题的完整流程

3.1 外部存档与精英保留:NSWOA 的归档策略怎么写

NSWOA 和很多多目标元启发式算法的区别在于是否有外部存档。NSGA-II 是靠精英选择机制把父代和子代合并后筛选,NSWOA 更常见的做法是维护一个独立的存档集,用非支配排序加拥挤度距离控制存档规模。每次迭代结束后,把当前种群和存档合并,做一次非支配排序,取前ArchiveSize个解作为新存档。

我做这个部分的代码时,发现最值得注意的坑是越界处理。多目标问题里变量的边界约束很常见,但进化算法更新完位置后经常有解跑出边界。直接把这些解扔掉会减少种群多样性,简单粗暴地「截断」到边界又会让大量解堆在边界线上,影响拥挤度计算的真实性。我一般用随机反射策略:超出边界的维度按边界宽度折返,类似光在镜面反射,这样既保留了解的位置,又避免边界堆积。

function x = boundary_reflect(x, lb, ub) range = ub - lb; over_high = x > ub; over_low = x < lb; x(over_high) = ub(over_high) - mod(x(over_high) - ub(over_high), range(over_high)); x(over_low) = lb(over_low) + mod(lb(over_low) - x(over_low), range(over_low)); idx = (x < lb) | (x > ub); % 极端情况下取模后仍然越界 x(idx) = lb(idx) + rand(size(lb(idx))) .* range(idx); if any(x(:) < lb(:)) || any(x(:) > ub(:)) x = max(min(x, ub), lb); % 兜底,保证在边界内 end end

这段代码里的逻辑顺序很重要:先做反射,再对极端情况做随机初始化,最后兜底截断。如果一上来就截断,反射策略就失去了意义。实际跑的时候发现,反射后仍然越界的概率很低,但一旦发生,通常是变量维度特别多、搜索步长特别大的情况,兜底处理是必要的安全网。

3.2 种子函数与主循环:变量数、目标数、代数怎么往代码里填

用户拿到的 NSWOA 代码压缩包里通常包含NSWOA.m主函数、目标函数文件和一些绘图脚本。我建议在跑任何测试问题之前,先搞清楚主函数的输入输出接口。典型的主循环骨架长这样:

function [archive, archive_fitness, history] = NSWOA(fun, dim, lb, ub, N, MaxIter, ArchiveSize) positions = lb + rand(N, dim) .* (ub - lb); fitness = evaluate_population(fun, positions); archive = positions; archive_fitness = fitness; history = zeros(MaxIter, 1); for t = 1:MaxIter a = 2 - 2 * t / MaxIter; % 线性递减参数 b = 1; % 螺旋形状常数 best_idx = select_leader(archive, archive_fitness); % 从存档里选领导者 p = rand(N, 1); r1 = rand(N, dim); r2 = rand(N, dim); l = -1 + 2 * rand(N, 1); A = 2 * a .* r1 - a; C = 2 .* r2; new_positions = zeros(N, dim); for i = 1:N if p(i) < 0.5 if abs(A(i,1)) < 1 new_positions(i,:) = archive(best_idx,:) - A(i,:) .* abs(C(i,:) .* archive(best_idx,:) - positions(i,:)); else rand_idx = randi(N); new_positions(i,:) = positions(rand_idx,:) - A(i,:) .* abs(C(i,:) .* positions(rand_idx,:) - positions(i,:)); end else dist = abs(archive(best_idx,:) - positions(i,:)); new_positions(i,:) = dist .* exp(b * l(i)) .* cos(2 * pi * l(i)) + archive(best_idx,:); end end new_positions = boundary_reflect(new_positions, lb, ub); new_fitness = evaluate_population(fun, new_positions); % 合并种群与存档,非支配排序,裁剪 combined_pos = [positions; new_positions; archive]; combined_fit = [fitness; new_fitness; archive_fitness]; front = non_dominated_sort(combined_fit); dist = crowding_distance(combined_fit, front); % 按层级和拥挤度排序取前N个 [~, sort_idx] = sortrows([front, -dist]); positions = combined_pos(sort_idx(1:N), :); fitness = combined_fit(sort_idx(1:N), :); % 从排序后的解里提取存档 archive = positions(front(sort_idx(1:N)) == 1, :); archive_fitness = fitness(front(sort_idx(1:N)) == 1, :); if size(archive, 1) > ArchiveSize archive = trim_archive(archive, archive_fitness, ArchiveSize); archive_fitness = evaluate_population(fun, archive); end history(t) = sum(min(archive_fitness, [], 1)); % 记录目标值的动态 end end

这个主循环里有个和纯 NSGA-II 不同的设计:位置更新时参照的不是「本代的最优解」而是「存档里的最优解」。存档里保留的是历史迭代中发现的全部非支配解,比当前代的 F1 前沿更全面。select_leader的实现通常是做一次拥挤度距离计算,取距离最大的解作为 leader,这样能引导种群朝前沿的稀疏区域搜索,而不是扎堆在已经密集的区域。

主函数的参数设置直接影响求解效果。下面这张表是经验值,适合 ZDT 系列和 DTLZ 系列测试函数,实际工程问题需要按变量维度和目标数量调整:

参数推荐范围说明
种群规模 N50~200目标函数计算代价高时取小值
最大迭代 MaxIter200~1000变量维度高时取大值
存档规模 ArchiveSizeN 的 50%~100%过小会丢失前沿细节
螺旋常数 b0.5~1.2控制螺旋的紧密程度
目标数 M2~3超过 3 个目标建议用 NSGA-III 的参考点机制

实际运行中,如果发现最后得到的 Pareto 前沿不够连续,优先增大ArchiveSize而不是MaxIter。存档空间太小,即使迭代再多,留下的非支配解也被迫裁剪,前沿必然稀疏。

3.3 从压缩包到可运行:NSWOA 代码的文件结构与调用方式

拿到 NSWOA 的压缩包,解压后通常能看到这些文件:NSWOA.m是主算法,ZDT1.m或类似的测试函数文件,PlotPareto.m是画图脚本,有时还有一个main.m把所有步骤串起来。先用测试函数验证算法正确性,再替换成自己的目标函数,这是最稳的上手顺序。

% main.m 示例 clc; clear; close all; % 定义ZDT1测试函数:两个目标,30个变量 dim = 30; lb = zeros(1, dim); ub = ones(1, dim); N = 100; MaxIter = 500; ArchiveSize = 100; [archive, archive_fitness, history] = NSWOA(@ZDT1, dim, lb, ub, N, MaxIter, ArchiveSize); figure; plot(archive_fitness(:,1), archive_fitness(:,2), 'o'); xlabel('f1'); ylabel('f2'); title('NSWOA Pareto Front - ZDT1'); % 计算IGD指标评估质量 true_front = load('ZDT1_true_front.txt'); igd_value = IGD(archive_fitness, true_front); fprintf('IGD = %.4f\n', igd_value);

调用时注意两点:目标函数必须返回一个列向量,每个元素是一个目标值,NSWOA 内部统一按行组织种群、按列组织目标;lbub若是全部变量等宽边界,可以传标量,但 MATLAB 的隐式扩展在某些老版本上不一定可靠,建议始终传向量。另外,evaluate_population里用了循环逐个求值,如果目标函数是向量化友好的(比如 ZDT 系列的数学表达式),改成一次性矩阵运算可以把耗时降低一个数量级。

4. 把 NSWOA 用到真实问题:物流配送路径优化的建模与参数调整

4.1 多目标怎么建模:成本与时间的双目标函数设计思路

纯测试函数的 Pareto 前沿是已知的,拿来验证算法正确性没问题。但工程场景里目标函数往往不是标准测试函数,建模这一步就决定了优化效果的上限。以物流配送路径优化为例,常见做法是设置两个目标:总运输成本和总配送时间。成本包括车辆固定成本和距离相关成本,时间包括行驶时间和各节点的服务时间。

目标函数写成 MATLAB 函数时,输入是一个向量形式的路径编码,需要先解码成实际的配送顺序:

function f = logistics_cost(x) % x 是连续变量编码的配送顺序,需要解码 % 假设有K辆车、C个客户点,x是C维的实数向量 C = length(x); [~, order] = sort(x); % 排序解码:把连续位置映射为配送顺序 global dist_matrix demand service_time vehicle_capacity; total_distance = 0; total_time = 0; vehicle_count = 1; current_load = 0; current_pos = 1; % 1是配送中心 current_time = 0; for i = 1:C customer = order(i); if current_load + demand(customer) > vehicle_capacity total_distance = total_distance + dist_matrix(current_pos, 1); total_time = total_time + dist_matrix(current_pos, 1) / speed + service_time(current_pos); vehicle_count = vehicle_count + 1; current_pos = 1; current_load = 0; current_time = 0; end total_distance = total_distance + dist_matrix(current_pos, customer); total_time = total_time + dist_matrix(current_pos, customer) / speed + service_time(customer); current_pos = customer; current_load = current_load + demand(customer); end total_distance = total_distance + dist_matrix(current_pos, 1); total_time = total_time + dist_matrix(current_pos, 1) / speed; f = [total_distance + vehicle_count * fixed_cost_per_vehicle, total_time]; end

排序解码是路径优化里最常见也最稳定的编码方式。直接把实数向量按升序排列,得到一个客户访问顺序,再根据容量约束切分成多条路径。这样设计的好处是 NSWOA 的连续位置更新算子不需要任何修改,直接跑就行。代价是搜索空间有冗余——多个不同实数向量可能对应同一个排序,但这在元启发式算法里通常可以接受,实际效果比离散编码加专门算子的实现更省事。

4.2 NSWOA 在这个场景下的 3 个必调参数

不同问题性质对参数敏感度差异很大,物流路径和 ZDT 函数至少有两点不同:变量之间有关联性(顺序表达),目标函数不是平滑函数。这两点决定了参数要重新调。

第一个参数是a的递减方式。标准 WOA 里a从 2 线性降到 0,但路径优化里后期如果a已经很小,所有鲸鱼都朝 leader 收缩,种群多样性骤降,很容易陷入局部路径组合。常见做法是把线性递减改成非线性:前 40% 迭代保持a在 1.5 以上加强探索,后 60% 快速降到 0 加强开发。实现上只需要把主循环里的a计算改成:

if t / MaxIter < 0.4 a = 2 - (t / MaxIter) / 0.4 * 0.5; % 前期从2缓慢降到1.5 else a = 1.5 - ((t / MaxIter) - 0.4) / 0.6 * 1.5; % 后期从1.5快速降到0 end

第二个是螺旋常数b。标准值 1 在连续函数上表现好,但路径编码的欧氏距离含义会被排序操作破坏,螺旋更新到实数空间产生的扰动和排序解码后的路径变化不是线性对应的。我一般把b调小到 0.5,让螺旋的半径变化更平缓,避免排序结果的大幅跳动。

第三个是存档里 leader 的选择策略。前面提到选拥挤度最大的解,但在物流场景里,如果两个目标之间的量纲差距过大(比如成本是万元级,时间是小时级),拥挤度计算归一化后可能让某几个极端解永远占据最大距离位置。更好的做法是定期轮换 leader:每个迭代归档后,把 F1 前沿按拥挤度排序,取前 20% 的解随机选一个作为 leader。这个改动通常能在保持收敛性的同时让前沿分布更均匀。

4.3 多目标优化的评价指标:IGD 与 HV 怎么看

优化跑完不能只靠眼睛看散点图,需要数值指标评估求解质量。最常用的两个指标是 IGD(Inverted Generational Distance)和 HV(Hypervolume)。

IGD 衡量的是算法得到的解集到真实 Pareto 前沿的平均距离,值越小越好。它需要真实前沿,测试函数可以从文献里下载,工程问题一般没有真实前沿,只能用近似前沿代替。HV 衡量的是解集在目标空间里覆盖的体积,值越大越好,不需要真实前沿,只需要一个参考点(通常取各目标的最差值)。

function hv = hypervolume(fitness, ref_point) % fitness: 已归一化到[0,1]的目标值,假设是最小化问题 % ref_point: 参考点,各目标的上界 N = size(fitness, 1); % 按第一个目标排序 [~, idx] = sort(fitness(:,1), 'descend'); fitness = fitness(idx, :); hv = 0; prev_f2 = ref_point(2); for i = 1:N if fitness(i,2) < prev_f2 hv = hv + (fitness(i,1) - 0) * (prev_f2 - fitness(i,2)); prev_f2 = fitness(i,2); end end hv = hv + (0 - 0) * (prev_f2 - 0); % 修正最后一块面积 end

这个实现是最基础的双目标 HV 计算,思路是按第一个目标降序排列,逐步累加矩形面积。三目标以上的 HV 计算比较复杂,通常用蒙特卡洛采样近似。跑对比实验时,建议每次都记录 HV 和 IGD,算均值和标准差,至少独立运行 10 次,因为元启发式算法单次运行有随机性,一次跑得好不代表算法真的好。

5. NSWOA 与 NSGA-II 的对比实验:跑同一组测试问题看差异

5.1 实验设置:相同的预算才是公平的比较

拿 NSWOA 和 NSGA-II 做对比时,最容易犯的错误是只比迭代次数,不比函数评估次数。NSGA-II 一代要做一次交叉变异和选择,NSWOA 一代要做一次鲸鱼位置更新和归档裁剪,单代计算量不同,只比代数没有意义。公平的做法是固定总的函数评估次数,比如 N×MaxIter = 50000 次,两个算法都用这个预算。

MATLAB 里统计函数评估次数需要在目标函数内部加计数器,或者用全局变量传值。把目标函数封装一层:

function f = counting_wrapper(x) global eval_count; eval_count = eval_count + size(x, 1); f = ZDT1(x); end

每次调用evaluate_population时计数自动累加。两个算法的终止条件都改成eval_count >= 50000。运行时发现 NSWOA 的归档裁剪阶段会重复评估存档里的解,计数会偏高,所以我在归档更新时直接沿用已有的适应度值,不重新调用目标函数。

5.2 在 ZDT1、ZDT2、DTLZ2 上的表现解读

ZDT1 是凸前沿,ZDT2 是非凸前沿,DTLZ2 是三目标问题。三组测试问题分别考察算法在不同前沿形状下的表现。我跑下来的经验结果是:在 ZDT1 上 NSWOA 的收敛速度明显快于 NSGA-II,前 100 代 IGD 下降曲线更陡,这是因为 Whale 的螺旋更新在连续空间里搜索效率高,包围机制对凸前沿有天然的逼近优势。但在 ZDT2 非凸前沿上,NSWOA 容易出现前沿覆盖不完整的问题,集中在两端而中间稀疏。原因是非凸区域对探索能力要求更高,而 NSWOA 的随机搜索算子比例在后期被压缩得很小。解决办法是把随机搜索的触发概率从 0.5 提高到 0.6,也就是位置更新里p(i) < 0.6时进入包围或随机搜索,p(i) >= 0.6时做螺旋更新。

DTLZ2 三目标上,NSWOA 的排名表现主要受存档规模影响。ArchiveSize 等于种群规模时,前沿的三维分布比较均匀,但边缘区域解的数量明显少于 NSGA-II。如果做 DTLZ 系列对比,建议 ArchiveSize 比种群规模大 20%~50%,否则三维前沿的边界解容易在裁剪中丢失。

5.3 常用的调试技巧:跑偏时如何定位问题

NSWOA 出问题时,症状通常分为三类。第一类是收敛到某一点,Pareto 前沿缩成一个点或一条线,这对应种群多样性崩溃。检查a的递减曲线是不是太平滑导致探索不足,检查 leader 选择是不是每次选同一个解。日志里加一行打印存档中互不支配的解数量,如果连续 20 代这个数量没有变化,考虑引入变异算子,以一个很小的概率(比如 0.05)随机重置个别维度。

第二类是前沿分布不均,中间缺一大块。这时要检查拥挤度距离计算的归一化逻辑是否正确,尤其是量纲差异大的目标。把每个目标的距离贡献单独打印出来,看是不是某一个目标贡献了 90% 以上的距离值。如果是,对归一化分母做一次对数缩放,压缩量纲差异。

第三类是收敛太慢,迭代到一半 IGD 还在高位。优先检查目标函数是否有大量平坦区域,导致鲸鱼在排序解码后位置变化不敏感。解决方法是添加辅助梯度:在目标函数里加入和距离相关的小扰动项,引导鲸鱼朝有变化的方向移动。这个技巧本质上是对目标函数做平滑,但不能改得面目全非,扰动幅度控制在原函数量级的 1% 以内。

6. 最后一章:用实时可视化和参数热力图调 NSWOA 的实用技巧

调试 NSWOA 时,只看最后的静态 Pareto 图远远不够,尤其是迭代中期的种群动态根本看不出来。我常用的做法是在主循环里嵌入一个实时绘图窗口,每隔一定代数刷新一次当前存档,同时记录每代存档的 HV 值。这样能直观看到探索和开发的动态过程:前期点应该散得很快,中期开始聚拢,后期沿着前沿缓慢滑动。

具体做法是把 NSWOA 主函数的history部分扩展,增加一个回调参数,或者在局部脚本里做成嵌套函数。下面的代码片段展示了一种轻量实现:

% 在主循环末尾添加 if mod(t, 20) == 0 || t == MaxIter subplot(1, 2, 1); plot(archive_fitness(:,1), archive_fitness(:,2), 'b.', 'MarkerSize', 8); xlabel('f1'); ylabel('f2'); title(sprintf('Iteration %d / %d', t, MaxIter)); drawnow limitrate; subplot(1, 2, 2); hv_val = hypervolume(normalize_fitness(archive_fitness), [1, 1]); history_hv(t) = hv_val; plot(1:t, history_hv(1:t), 'r-', 'LineWidth', 1.5); xlabel('Iteration'); ylabel('HV'); title('Hypervolume History'); drawnow; end

注意drawnow limitratedrawnow的区别。limitrate会限制刷新频率,渲染开销大时用,否则实时绘图的耗时可能超过算法本身的耗时,干扰计时。画图前对适应度归一化很重要,不然 HV 的数值随目标量纲变化没有可比性。

另一个值得做的静态检查是参数热力图。把种群规模 N 和 ArchiveSize 分别设成网格,比如 N 取 50、100、150、200,ArchiveSize 取 N 的 50%、75%、100%、125%,对每个组合跑 10 次取平均 HV,画成热力图。这样做有两个价值:一是直观看到参数交互效应,比如 N=100 时 ArchiveSize 对结果影响不大,但 N=50 时 ArchiveSize 不足会明显降低 HV;二是为同类问题提供一个可迁移的调参起点,下次遇到相似规模的问题可以直接把网格参数作为默认值,省去重复调试。

关于 MATLAB 版本,NSWOA 的代码本身对版本要求不高,2018a 以上都能跑。真正值得注意的是evaluate_population里的循环结构,如果目标函数是高度向量化的数学表达式,把循环改成矩阵运算可以提速 10 倍以上,特别适合参数热力图这种需要大量重复调用的场景。改法是把种群矩阵一次性传给目标函数,目标函数内部按行操作,返回一个 N×M 的适应度矩阵。但路径优化这类需要解码的现实问题往往无法完全向量化,这时优先保证可读性,性能靠减少无意义的重复计算来提升。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询