简介:这是一套面向多目标优化算法研究与初学者的Matlab测试环境,围绕NSGA-II、MOPSO等经典多目标进化算法提供完整实现与可视化对比方案,适用于算法性能测试、实验复现和教学演示。压缩包共105个文件,以87个m脚本为核心,涵盖算法主程序、指标计算与辅助函数;6个fig和6个png用于展示解集分布与收敛过程;2个txt提供说明,2个mat保存实验数据,另有2个zip便于二次扩展,整体仅4.89MB,轻量易用。目前已有200人学习下载。借助这套环境,读者可以直接运行典型多目标算法,观察网格搜索、随机搜索等不同策略下的求解效果,结合图形结果快速理解Pareto前沿、解集分布等概念,也可基于现有代码修改测试函数或评价指标,节省搭建实验环境的时间。
1. 解开这个 zip 之前,先想清楚你要拿它做什么
做多目标优化的人手头几乎都有一个“基准测试包”:一份压缩包里放着 ZDT、DTLZ、WFG 等标准测试问题集,配上 MATLAB 实现的评估脚本,套上自己的算法就能跑出 Pareto 前沿和评价指标。这套东西应用价值明确——新提出的多目标进化算法(MOEA)要在统一环境里和 NSGA-II、MOEA/D、SPEA2 比出高低,审稿人和业务方都认这套流程。标题里“测试环境”就是一个可控的算法评估平台,zip 里的 matlab 代码是整套评估的核心工具链。
这个包能解决的是两类问题:一是你的算法需要一套公认的复现测试场景,否则跑一两个私有数据无法令人信服;二是你需要一个正确的、可比较的基线代码库,避免自己写测试函数时埋入不连续或多峰陷阱。适合的人群包括准备投递论文的研究者、刚接手多目标项目的工程师,以及想评估现有算法库性能的架构师。与其拿到压缩包就解压运行,不如先把包内结构、测试函数特性和指标含义搞清楚,后面调优才不会瞎忙。
2. 多目标优化测试环境的基本构成与选型逻辑
2.1 多目标优化的核心矛盾:牵制解与 Pareto 前沿
多目标优化之所以比单目标麻烦,在于多个目标之间经常互相干扰,不存在一个解同时让所有目标达到最优。用数学语言说,给定决策向量 x,目标向量为 f(x) = (f1(x), f2(x), …, fm(x)),当且仅当没有一个解 y 能保证 fi(y) ≤ fi(x) 对所有 i 成立且至少一个严格小于时,x 称为非支配解。所有非支配解构成的集合叫 Pareto 最优集,映射到目标空间就是 Pareto 前沿。实践中你接触到的多目标优化测试环境,本质就是让你在这个前沿附近评估算法的收敛性和多样性。
% pareto_front_demo.m % 两个目标的简单示例,演示如何从一组解中筛出非支配解 solutions = rand(50, 3); % 列1=目标1, 列2=目标2, 列3=解编号 n = size(solutions, 1); is_dominated = false(n, 1); for i = 1:n for j = 1:n if i ~= j && all(solutions(j, 1:2) <= solutions(i, 1:2)) ... && any(solutions(j, 1:2) < solutions(i, 1:2)) is_dominated(i) = true; break; end end end pareto_solutions = solutions(~is_dominated, :); fprintf('非支配解数量: %d\n', size(pareto_solutions, 1));这段代码先把每个解与全部解逐一比较,只有没有被任何其他解在两个目标上同时压制时,才保留下来。注意第 5 行的条件序列:all判断目标全部不差,any保证至少一个目标更好,两个条件缺一不可。很多入门资料写成<=时不区分相等的支配关系,容易把重复解误判为互不支配,这里建议严格按上述逻辑写。测试环境中的函数集通常在目标数、前沿几何形状和模态上做了区分,正是为了让你的算法在多样化的压力下暴露弱点。
2.2 测试函数分代的选型理由:从 ZDT 到 DTLZ 再到 WFG
ZDT 系列是最早的一批双目标测试问题,适合快速验证算法基本行为,但它的前沿形状相对简单,多为凸、凹或者离散段,部分函数比如 ZDT4 带有大量局部前沿,专门考察算法跳出局部陷阱的能力。DTLZ 系列的优点是目标数可以扩展,从 3 目标到十几目标都行,常用于高维目标空间的压力测试,其中 DTLZ1 的前沿是超平面,DTLZ2 是球面,几何差异明显。WFG 系列则把可变长度决策变量、混合前沿、偏好转化的场景做了系统化,适合细粒度分析算法在复杂 Pareto 几何下的表现。
选型逻辑很简单:如果空间受限,优先用 ZDT1、ZDT2、ZDT4 加 DTLZ1、DTLZ2 这五个问题,它们覆盖了凸前沿、凹前沿、多峰和可扩展目标数这几类主要特征。如果你的算法主打高维目标优化,必须引入 DTLZ5 或 DTLZ7,前者是降维前沿,后者是非连续前沿,这两类能测量算法对前沿连通性的敏感程度。WFG 系列虽然覆盖面广,但计算量偏大,不适合在开发流程中频繁跑。
% ZDT1 目标函数实现 function [f] = ZDT1(x) n = numel(x); f1 = x(1); g = 1 + 9 * sum(x(2:end)) / (n - 1); f2 = g * (1 - sqrt(x(1) / g)); endZDT1 的 Pareto 最优解在 g = 1 时取得,前沿形状为凸,决策变量 x1 直接决定 f1,剩下变量共同参与 g 的计算。测试环境里往往还会附加真实前沿数据文件,一般以pf_zdt1.dat形式存在。如果你发现某个 zip 包只附带目标函数代码没有参考前沿文件,可以自己用大种群、多代数的 NSGA-II 逼近一次,并把结果保存为参考。这种做法不是最优解,但在没有官方数据的测试问题上可作替代。
3. 解析 zip 包结构与 MATLAB 代码的复现路径
3.1 zip 包内典型目录结构:测试集、指标与运行脚本
拿到「多目标优化测试环境附matlab代码.zip」后,正常情况解压出来体积不大,但结构组织直接决定你后续改代码的便捷性。常见的分类方式是problems、metrics、algorithms和run_experiments.m四个部分,problems 下按测试问题单独放 m 文件,metrics 目录放 HV、IGD、GD 等指标函数,algorithms 放待比较的算法代码,根目录脚本负责串联全部流程。
在 MATLAB 中建议用unzip('多目标优化测试环境附matlab代码.zip')解压后,cd进入根目录,再执行addpath(genpath(pwd))将所有子目录加入搜索路径。genpath的作用是把当前目录下所有嵌套子目录完整添加,避免每次调用测试函数时找不到文件的异常。如果你发现运行脚本时报“Undefined function or variable”,十有八九是路径没加全,这时先用which ZDT1检查函数是否可见。
% run_experiments.m 核心流程 clc; clear; close all; addpath(genpath(pwd)); % 1. 加载测试问题列表 problem_names = {'ZDT1', 'ZDT2', 'ZDT4', 'DTLZ1', 'DTLZ2'}; n_runs = 30; % 2. 为每个问题执行指定算法并记录 for p = 1:numel(problem_names) prob = str2func(problem_names{p}); for run = 1:n_runs [best_solutions, best_obj] = run_moea(prob, ... struct('popsize', 100, 'maxgen', 500)); % 保存结果到 data 文件夹 filename = sprintf('results/%s_run%d.mat', problem_names{p}, run); save(filename, 'best_solutions', 'best_obj'); end end这段脚本把测试环境的使用方式交待得很清楚:先通过str2func把字符串变成可调用的函数句柄,外层循环挨个处理测试问题,内层循环跑多次独立实验。这里设置 30 次运行是业界常规,因为多目标算法是随机优化器,单次结果波动大,必须用统计手段消除随机性。run_moea是算法封装函数,需要你根据 zip 包内提供的算法代码对接,输入参数用结构体统一传递,后续扩展新算法时不需要修改主脚本。
3.2 MATLAB 版本兼容性与工具包依赖检查
zip 包多数时候只能保证作者当时的运行环境正确,不保证你本地也能原样跑通。解压后第一步先看根目录有没有requirements.txt或者 README 列出依赖工具包,常见依赖有 Global Optimization Toolbox 和 Statistics and Machine Learning Toolbox。如果你的环境缺少某些工具箱,未必要去装完整工具箱,很多测试函数和指标可以自己实现,比如 IGD 指标计算只要会写距离运算即可,不需要额外工具包。
% 检查工具包是否可用 if license('test', 'Optimization_Toolbox') disp('优化工具箱可用'); else disp('缺少优化工具箱,部分高级函数不可用'); end这里license('test', 'Optimization_Toolbox')返回逻辑真或假,用于提前判断哪些代码执行必然会报错。多目标测试环境最致命的问题不是缺少工具箱,而是 MATLAB 版本太老导致str2func、arrayfun这类函数的语法兼容性差异。我的经验是 2014a 版之后运行 ZDT/DTLZ 测试集问题不大,但如果代码里用了tall、timetable这类新数据类型,老旧版本的报错会很隐蔽,建议直接升级到 2020a 以上版本。
% 检查 MATLAB 版本 v = ver('matlab'); fprintf('当前 MATLAB 版本: %s\n', v.Release);代码的运行报错大致分三类:一是函数不存在,二是维度不匹配,三是内存不足。对于维度不匹配,多目标测试函数最常见的是把目标数 m 和决策变量数 n 搞混,比如 DTLZ 系列的决策变量数必须满足 n ≥ m,否则部分目标无法正确计算。遇到这类问题请检查测试函数的调用约定,特别是 DTLZ2 的输入参数顺序,多数实现是 (x, M),其中 M 是目标数。
4. 参数应该如何设:种群大小、迭代次数与算法参数
4.1 种群大小与最大评价次数的经验值对照表
多目标测试环境里的算法性能高度依赖参数配置,同一算法在不同测试问题上最优参数可能截然不同。但科研评审里习惯固定参数,所以你必须知道一套底线配置。下面这张表是我常年在 ZDT/DTLZ 测试集上积累出来的起点值,适用性广,如果你要用于论文实验,建议在此基础上再做小范围扰动试验。
| 测试问题 | 种群大小 | 最大迭代 | 最大评价次数 | 交叉概率 | 变异概率 |
|---|---|---|---|---|---|
| ZDT1/ZDT2 | 100 | 250 | 25000 | 0.9 | 1/n |
| ZDT4 | 200 | 500 | 100000 | 0.9 | 1/n |
| DTLZ1 | 200 | 500 | 100000 | 0.9 | 1/n |
| DTLZ2 | 120 | 300 | 36000 | 0.9 | 1/n |
| WFG6 | 200 | 500 | 100000 | 1.0 | 1/n |
变异概率写成1/n表示每个决策变量有 1/n 的概率被扰动,n 是决策变量个数。这是基于二进制编码和实数编码两种场景下的折中方案,ZDT 家族的决策变量数通常是 30,因此变异率约 0.033。交叉概率 0.9 是 SBX(模拟二进制交叉)算子常用的值,分布指数 ηc 一般取 20;如果你的 zip 包里用的是差分进化算子,那交叉概率 CR 建议在 [0.3, 0.9] 里选,测试环境里多做几组对比更稳妥。
% 典型 NSGA-II 参数配置(测试环境通用版本) params.popsize = 100; % 种群大小 params.maxgen = 250; % 最大迭代代数 params.pc = 0.9; % 交叉概率 params.pm = 1 / 30; % 变异概率,30 为决策变量数 params.eta_c = 20; % SBX 分布指数 params.eta_m = 20; % 多项式变异分布指数4.2 最大评价次数的统计口径怎么对齐
多目标优化社区对比算法时,普遍使用“最大函数评价次数”(Max Function Evaluations, MaxFE)而非迭代代数作为终止条件。两者差别很关键:同一个迭代代数下,不同算法的种群大小不同,导致实际评价次数天差地别。所以在测试环境里跑对比实验时,要统一到 MaxFE。转换公式是 MaxFE = popsize × 最大迭代代数。你会发现以上表中 ZDT1 为例,250 代对应 25000 次评估,这也是大量文献里的常见配置。
如果你的 zip 包里的算法代码只接受迭代代数作为终止条件,可以手动在外层包一层判断。写一个计数器,每次调用目标函数评估时自增,当计数达到 MaxFE 就强制终止。这里有个细节:多目标算法在每一代里不只是种群个体做评估,有些算子(比如多项式变异)会做边界检查和扰动,但不会触发目标函数调用,所以计数器只用挂在目标函数里最准确。
% 带评估计数器的目标函数封装 function [f] = counted_eval(prob, x, counter_handle) counter_handle.count = counter_handle.count + 1; f = prob(x); if counter_handle.count >= counter_handle.maxeval error('MAXEVAL_REACHED'); end end计数器用结构体counter_handle传递,MATLAB 里结构体是值类型,如果直接作为参数传进函数,修改不会带回到外层,所以必须用句柄类或者全局变量,这里用结构体加handle后缀暗示其为句柄对象。更稳妥的做法是定义成containers.Map或者继承handle类的对象,否则计数器只在函数内部生效而无法终止外层循环。这些细节在你拿到陌生 zip 包时才最容易踩坑。
5. 评价指标怎么算:IGD、HV 与 GD 的 MATLAB 实现
5.1 世代距离 GD 与反世代距离 IGD 的原理与代码
GD 衡量算法所得解集与真实前沿的逼近程度,计算每个解到参考前沿最近点的欧氏距离,然后取平均。IGD 恰好反过来,从参考前沿上的每个点出发,找到解集中距离最近的点,再取平均。IGD 同时惩罚收敛性和多样性:如果解集漏掉了前沿上某一段,那么那段参考点的距离就会很大,整体 IGD 值飙升。这一点对测试环境中的算法评估至关重要。
用测试环境自带的数据文件时,重点检查参考前沿点是否足够密集,通常每个测试问题参考前沿应该有数百到数千个点。密度不足会导致指标失真,我一般会先跑一遍 IGD,如果结果异常偏大,先查参考点密度而不是质疑算法。
% compute_igd.m function igd = compute_igd(pf, approx_set) % pf: 真实Pareto前沿,每行一个目标向量 % approx_set: 算法所得非支配解的目标向量 n_pf = size(pf, 1); distances = zeros(n_pf, 1); for i = 1:n_pf diff = approx_set - repmat(pf(i, :), size(approx_set, 1), 1); sq_dist = sum(diff.^2, 2); distances(i) = sqrt(min(sq_dist)); end igd = mean(distances); end核心逻辑在第 5 行到第 8 行:先用repmat把参考点复制成与解集相同的行数,然后整体做差求平方和,min取最近距离。这里不建议写成两层 for 循环,MATLAB 矩阵运算效率远高于循环,特别是解集规模达到 1000 行时差距明显。对于 3 目标以上的问题,直接用pdist2(approx_set, pf, 'euclidean')生成距离矩阵再按列取最小。
5.2 超体积 HV 的实现方式与归一化陷阱
HV 指标度量解集在目标空间里覆盖的体积大小,以参考点(通常取目标空间各维度的最差值)为边界,把解集与参考点之间围成的区域体积算出来。HV 的好处是不需要知道真实前沿,因此在真实前沿未知的实际业务场景中依然可用。HV 越大越好,因为意味着解集在目标空间中覆盖更多区域,且收敛性越好覆盖越完整。HV 计算成本高,尤其三维以上目标,需要用到蒙特卡洛采样或递归划分逼近算法。
% compute_hv.m 使用蒙特卡洛近似计算 HV function hv = compute_hv(approx_obj, ref_point, sample_num) % approx_obj: 算法所得非支配解的目标值(已全部转化为最小化方向) % ref_point: 参考点,通常取各目标上界的 1.1 倍或最大值 dim = size(approx_obj, 2); % 在参考点范围内均匀采样 samples = rand(sample_num, dim); ranges = repmat(ref_point, sample_num, 1); samples = samples .* ranges; % 判断样本点是否被解集中的点支配 dominated = false(sample_num, 1); for i = 1:sample_num diff = approx_obj - repmat(samples(i, :), size(approx_obj, 1), 1); if any(all(diff <= 0, 2) && any(diff < 0, 2)) dominated(i) = true; end end hv_ratio = sum(dominated) / sample_num; hv = hv_ratio * prod(ref_point); end这个实现用采样点判断是否被支配来近似体积占比。第 8 行到第 11 行是关键:样本点如果在目标空间中比某个解差,即解向量每个维度都不大于样本且至少一个维度严格小,那就认为这个样本点被解集覆盖。这里要求目标都做了最小化处理,如果你的测试问题默认求最大化,需要先取负号或取倒数,否则一切指标失真。参考点的选取同样敏感,常用策略是取各目标范围最大值的 1.1 倍,既能包住整个解集又不至于引入太多无效体积。高维目标时 1.1 倍会让 HV 数值极大,建议改成 1.05 倍或对每个目标单独归一化后再算。
6. 把 zip 包变成你自己的算法测试平台的一个技巧
测试环境的最大价值在于可扩展:你不需要只跑包内预设的算法,把自定义算法塞进去才是重点。建议先在 problems 目录里新增一个自己的测试问题函数,比如制造一个双目标混合前沿问题,观察现成的对比算法能否正确描述前沿形状,以此验证平台的灵活性。再写一个适配器脚本,把自己的算法封装成和包内算法相同的输入输出接口,这样后续跑批量和统计检验时不需要改动任何测试环境代码。
% 自定义测试问题:双目标,前沿为凹形加离散段 function [f] = MyProblem(x) f1 = x(1); g = 1 + 9 * sum((x(2:end) - 0.5).^2); f2 = g * (1 - sqrt(f1 / g) - (f1 / g) * sin(10 * pi * f1)); end这个函数参考了 ZDT3 的非连续前沿结构,把正弦项加进去后前沿会分裂成几段,测试算法的连通性保持能力。接口保持f = myproblem(x)形式,就可以被 run_experiments.m 直接调用。输送进指标计算函数前,注意把求最小化的目标方向统一,最好在函数顶部注释说明哪些目标已转换为最小化。最后做一个对比实验,把自定义算法和 NSGA-II 在新增问题上各跑 30 次,用signrank做 Wilcoxon 符号秩检验,这种统计显著性判定在论文或者技术方案评审中远比只看均值有效。
本文还有配套的精品资源,点击获取