简介:PlatEMO v2.0(2019-3)是面向多目标优化研究的进化算法平台,适合科研人员、工程师以及高校学生用于算法开发、性能对比与教学演示。压缩包共593个文件,其中579个m源文件构成完整Matlab工具箱,另含md与txt说明文档、mat示例数据及pdf文档,整体仅1.45MB。目前已有697人学习/下载。平台内置NSGA-II、MOEA/D等常用算法与标准测试问题库,提供可视化界面、实验管理及可扩展接口;包内可见GLOBAL.m、newGUI.m、module_experiment.m、module_test.m等核心模块,可直接运行标准实验并扩展自定义算法,非常适合快速搭建算法对比实验、系统掌握多目标进化算法的实践流程。
1. PlatEMO v2.0:为什么说它把多目标实验从“手动档”换成了“自动档”
PlatEMO v2.0(2019年3月发布)是MATLAB生态里知名度很高的多目标进化算法实验平台。很多人把它当成“带界面的算法集”,实际上它最大的价值是把算法、测试问题、性能指标和实验记录这四件事焊成了一个整体。你只需要写好一个算法类,剩下的对比实验、指标计算、结果存档全由平台接管。这套设计对两类人特别有用:一是做进化计算研究、需要快速验证新机制的论文作者,二是想把不同算法拉到同一标准下做横向对比的工程师。v2.0相比早期版本最大的变化是引入了统一的包结构和算法接口,这也让它成了后续版本里被沿用最久的骨架。后面所有内容都以这个版本的实际目录结构为准。
2. 把 PlatEMO v2.0 跑起来:从解压到第一组结果的三个动作
2.1 文件结构和本命文件夹:先看懂 +PlatEMO 是什么
下载下来的v2.0是一个完整目录,里面比较关键的有三块。
第一块是+PlatEMO,这是MATLAB的包目录。+号是包标识,文件夹名不能随意改,改了类就找不到了。所有算法类、问题类、工具函数都按子包组织,比如+PlatEMO/+Algorithm里放各种算法,+PlatEMO/+Problem里放测试问题。第二块是platemo.m,这是整个平台的入口,GUI从这里启动,批处理也从这里走。第三块是Data目录,里面存着测试问题的真实PF(Pareto前沿)文件,IGD指标的计算依赖这些文件。
这里有个新手常踩的岔路:有人说把所有文件夹都addpath就完事,结果一运行就报错“Undefined class 'PlatEMO.Algorithm'”。原因是你把某个子目录单独拷出来用了。v2.0的包结构必须保持完整,我一般直接把整个PlatEMO根目录用genpath加进去:
addpath(genpath('D:/tools/PlatEMO')); % 换成你的实际路径 savepath;genpath会递归地把所有子目录加入MATLAB搜索路径,savepath把路径保存下来,下次打开MATLAB不用重复添加。需要注意,如果你的目录名带有中文或特殊符号,addpath通常会失败,尽量把平台放在纯英文路径下。这一步做完,在命令行敲which platemo,能返回完整路径,说明环境已经就绪。
2.2 GUI 首跑:选问题、选算法、设种群的 30 秒流程
环境就绪后在命令行输入:
platemo界面启动后是左右两栏布局。左侧是问题面板,右侧是算法面板,中间是运行控制和结果展示区。第一组实验我建议按最保守的组合来:问题选 ZDT1,算法选 NSGA-II,种群大小设100,最大评估次数设10000。这么配置是因为ZDT1是连续无约束两目标问题,NSGA-II在v2.0里默认参数足够在它上面稳定收敛,适合拿来验证平台本身没问题。
GUI里几个设置的对应关系可以记成一张表:
| 面板区域 | 作用 | 第一组实验推荐值 |
|---|---|---|
| Problem | 选择测试问题及其维度 | ZDT1,目标数M=2 |
| Algorithm | 选择算法并显示参数 | NSGA-II,默认参数 |
| N | 种群大小 | 100 |
| maxFE | 最大函数评估次数 | 10000 |
| 指标勾选 | 决定记录哪些指标 | IGD、HV都勾上 |
点运行后,中间会实时显示种群在目标空间的分布,右下方是IGD和HV随评估次数变化的曲线。ZDT1的真实前沿是凹曲线,NSGA-II跑完后种群会贴在这条曲线附近,IGD曲线应该在一万次评估内明显下降并趋于平缓。如果这一步能出这个结果,你的环境就是健康的,可以开始做自定义实验了。
这里提前说一个理解终止条件的角度:maxFE是函数评估总次数,不是种群代数。种群100、maxFE=10000意味着大约100代。你在GUI里看到的所有曲线,横轴都是评估次数,不是代数,后面自己写循环判断时一定要用平台提供的终止判断,而不是自己数代。
2.3 读回结果:.mat 文件里存了什么
运行结束后,v2.0会把这次实验完整存成一个.mat文件。文件默认放在平台目录下的Results文件夹里,命名规则通常是算法名_问题名_日期_序号.mat,例如NSGA-II_ZDT1_20190303_1.mat。不要直接双击打开,建议用命令行加载:
res = load('Results/NSGA-II_ZDT1_20190303_1.mat'); whos('-file', 'Results/NSGA-II_ZDT1_20190303_1.mat')whos -file会列出.mat文件里所有变量名和维度,这是最稳妥的查看方式,因为不同小版本里变量名可能会微调。常见会存下来的有:算法名、问题名、最终种群、每一代的IGD和HV序列、以及这次运行的基本参数。我习惯把原始.mat当作原始数据存档,绘图和分析都用单独的脚本读取,不直接在GUI里反复操作,这样多组实验之间更好对齐。
验证第一组结果是否可信,一个很硬的标准是拿IGD数值和平台上内置算法跑出来的参考值对比。v2.0的Data目录里存了各问题已知最优PF,你可以加载ZDT1的PF文件,把最终种群的obj画在同一个坐标里,肉眼确认贴合。这个习惯能避免后面改算法时把问题方向搞反。
3. 拆解 v2.0 的三角核心:Algorithm、Problem 与 Population 怎么协同
3.1 Algorithm 是容器,不是流程
v2.0里写一个算法,不是写一串脚本,而是写一个继承PlatEMO.Algorithm的类。这个类的职责不是“跑流程”,而是“提供参数和入口”。你在GUI右侧看到的参数面板,就是平台读取算法类的ParameterNames和ParameterValues两个属性渲染出来的。一旦在GUI上改了参数,平台会把新的值写回ParameterValues,然后调用你的算法入口。
这个设计的好处是:问题怎么评估、种群怎么存储、指标怎么记录,算法类都不用操心。算法只需要关心每一步产生什么解、保留哪些解。比如你写一个新算法,继承Algorithm类后只需要实现一个solve方法,平台会在满足终止条件之前反复调用它内部的控制逻辑。回头看v1.x时代的脚本式写法,v2.0等于把“怎么组织实验”和“怎么写算法”彻底分开了。
之所以强调“容器”这个说法,是因为很多人第一次打开内置算法时会被里面的结构绕晕:又是构造函数、又是受保护方法。其实你只需要抓住两个锚点:构造函数决定了GUI上展示什么,solve方法决定了算法怎么做决策。其他地方都是围绕这两个锚点的辅助代码。理解了这一点,你改任何内置算法都不会心虚。
3.2 Problem 对象:问题参数与评估入口
每个测试问题在v2.0里也是一个类,统一放在+PlatEMO/+Problem下,而且它们都继承同一个问题基类。基类里约定了算法一定会用到的信息:目标数M、决策变量数D、决策变量下界lower和上界upper。算法初始化种群时,需要根据Problem.D和边界生成随机决策变量;评估个体时,通过问题对象提供的评估接口计算目标值。
这样就出现了一个很关键的分工:算法代码里不应该出现任何“ZDT1的目标函数是 f1 = x1, f2 = g*(1-sqrt(x1/f1))”这类硬编码。想换问题,只要在GUI里换下拉框,算法类和问题类通过统一接口完成对接。也是因为这个约定,v2.0才能用同一套算法代码在ZDT、DTLZ、WFG、MaF这些风格完全不同的问题系列之间自由切换。
当你想往平台里加一个新测试问题时,也走同一个套路:写一个继承问题基类的子类,把M、D、lower、upper填对,再实现目标函数评估方法。平台只认接口,不认问题名字。换句话说,你在v2.0里每加一个算法或一个问题,都是在往这套统一接口上挂新节点,而不是在改平台本体。
3.3 Population 数据交换格式:dec、obj、cons 的约定
种群在v2.0里是一组Solution对象的数组。每个Solution对象身上有三个主要字段:dec是决策变量行向量,obj是目标值行向量,cons是约束违反量。如果你研究过早期版本,会觉得这个结构很眼熟,因为它就是从结构体数组进化来的对象化版本。
这里有个最容易被新手踩的约定:obj不是由算法自己算完塞进去的,而是调用问题对象的评估方法后返回的。你如果直接改Population(i).obj,平台记录的指标曲线不会更新,因为评估计数器没有增加。正确做法是拿到新的dec后,调用Problem.CalObj(dec)这类评估接口,把返回的目标值写回obj。约束问题同理,cons表示约束被违反的程度,0是可行,大于0就是不可行,算法做环境选择时要用这个值来区分可行域内外的个体。
我用一段很短的代码说明这个约定的常见写法,你后面看内置算法也会反复看到这个模式:
newDec = Problem.lower + rand(1, Problem.D) .* (Problem.upper - Problem.lower); newObj = Problem.CalObj(newDec); % 评估接口返回目标值 Population(i).dec = newDec; Population(i).obj = newObj;注意Problem.CalObj的返回维度,多数情况下是行向量。如果换成某些旧版工具箱养成的习惯传了列向量,后面的非支配排序会报维度不一致,这个坑在5.5节还会再提一次。
3.4 指标计算模块:IGD 与 HV 在 v2.0 内部怎么处理
跑完一次实验,GUI里会显示IGD和HV曲线。IGD计算需要两个输入:算法得到的最终种群,以及这个问题已知的最优前沿。v2.0把最优前沿预处理成了.mat文件放在Data目录里,指标模块会从中读取并计算,所以IGD这一步基本是零成本,你不需要自己去采真实前沿。
我在做问题对比时,经常直接用脚本把真实前沿加载出来和算法种群叠在一张图上检查,判断算法是否真的收敛到了正确位置上:
pf = load('Data/PF/ZDT1.mat'); % 加载真实前沿 plot(pf.obj(:, 1), pf.obj(:, 2), 'k-', 'LineWidth', 1.5); hold on; plot(Population.objs(:, 1), Population.objs(:, 2), 'ro');pf.obj的字段名在不同小版本里可能有差异,先用whos -file看一眼再画。如果算法种群的散点大面积偏离真实前沿,IGD数值再好看也是假的,这种情况多半是目标数M设置和PF文件不匹配,回问题设置里检查。
HV的计算形态和IGD不同,它需要一个参考点。参考点选取不同,HV的绝对值也会不同,论文里直接比HV数值时必须保证双方参考点一致。v2.0默认会按各问题的边界和种群目标值动态生成参考点,同一问题同一配置下是可比的,但跨问题比较HV没有意义。在自定义问题的时候,这个参考点的选取要自己格外小心,不然HV曲线会给出一个“自欺欺人”的漂亮上升。
4. 写一个自己的算法接进 v2.0:NSGA-II 加一个局部搜索的完整改法
4.1 先读内置算法再动手:v2.0 自定义算法的最小框架
网上搜PlatEMO自定义算法,帖子不少,但很多人第一句话就是“继承Algorithm类然后写solve方法”,听起来挺简单,真动手的时候连文件放哪都不知道。我建议你第一步不是新建文件,而是先打开一个内置算法当模板:
edit('+PlatEMO/+Algorithm/NSGAII.m')打开后你会看到这个类的基本结构:开头是构造函数,里面给obj.Name、obj.ParameterNames、obj.ParameterValues赋值;接着是一个受保护的solve方法,里面是完整的进化循环。你把这个文件另存为MyNSGA2LS.m,放在同一个目录里,然后全局替换类名,这就得到了一个和内置NSGA-II行为完全一致的新算法。这一步的意义在于避开接口细节,因为不同小版本里工具函数的名字可能略有出入,照抄模板永远比记忆接口可靠。
注意文件名必须是MyNSGA2LS.m,不能是my_nsga2ls.m或者测试1.m。MATLAB的类加载规则是文件名找类定义,文件名和类名不一致时,平台在运行阶段才会报类未定义,那时候排查起来比现在多花半小时。
4.2 solve 方法实现:初始化、主循环、终止条件一次说清
在模板基础上,我们做第一个改动:把构造函数里的参数重新声明一遍。模板里默认是100个个体、交叉概率1、交叉分布指数20、变异概率1、变异分布指数20。这些参数会被GUI的参数面板读取,改成什么样,面板就显示什么样:
classdef MyNSGA2LS < PlatEMO.Algorithm methods function obj = MyNSGA2LS() obj.Name = 'MyNSGA2LS'; % GUI算法列表里显示的名字 obj.ParameterNames = {'N', 'proC', 'disC', 'proM', 'disM'}; obj.ParameterValues = {100, 1, 20, 1, 20}; end end methods (Access = protected) function solve(Algorithm, Problem) % 初始种群直接交给问题对象生成,边界和维度它负责 Population = Problem.Initialization(Algorithm.ParameterValues{1}); gen = 0; while Algorithm.NotTerminated(Population) gen = gen + 1; % ---- 下面两段原本是内置NSGA-II的选择、交叉、变异 ---- % MatingPool = TournamentSelection(2, N, ...); % Offspring = OperatorGA(MatingPool, ...); % ---- 在环境选择前插入局部搜索,见4.3节 ---- if mod(gen, 10) == 0 Population = Algorithm.LocalSearch(Problem, Population); end % ---- 下面是环境选择:合并父子代、非支配排序、保留N个 ---- end end end end这个结构里有两个点要特别说明。第一,ParameterValues{1}对应N,也就是种群大小;所有参数在ParameterNames里的顺序必须和ParameterValues一一对应,平台不做任何自动对齐,顺序错一个,你调的就是另一个参数。第二,NotTerminated(Population)是平台提供的终止判断,内部会看当前评估次数是否达到maxFE,也会在每一代结束时把当前种群和指标数据记录到结果文件里。你的循环不能自己用for写死代数,否则结果文件里不会有完整的收敛过程。
我在模板里用了一个本地变量gen来记录迭代次数,纯粹是给局部搜索的触发条件用的。不要用平台之外自己维护的计数器来替代终止判断,平台的评估计数器和结果记录是绑在一起的,绕开它,结果文件就不完整。
4.3 加一个局部搜索:高斯扰动与边界拉回
现在给这个类增加一个局部搜索方法。它做的事很朴素:每10代,对当前种群里的可行个体做一次小步高斯扰动,如果扰动后的解目标值更小,就替换掉原个体。
methods (Access = protected) function Population = LocalSearch(Algorithm, Problem, Population) % 步长用问题边界跨度的10%,不同尺度的问题都能适应 sigma = 0.1 * (Problem.upper - Problem.lower); for i = 1 : length(Population) % cons > 0 表示约束被违反,跳过不可行个体 if any(Population(i).cons > 0) continue; end newDec = Population(i).dec + sigma .* randn(1, Problem.D); % 拉回边界:越界的坐标直接夹到边界值 newDec = min(max(newDec, Problem.lower), Problem.upper); newObj = Problem.CalObj(newDec); % 目标值比较:多目标严格做法是支配判断,这里用求和近似 if sum(newObj) < sum(Population(i).obj) Population(i).dec = newDec; Population(i).obj = newObj; end end end end这个方法的逻辑分四步。第一步,sigma按问题边界动态计算,决策变量尺度是0.01还是10000,扰动幅度都相对合理,不用手工调步长。第二步,跳过cons > 0的不可行个体,防止局部搜索把算法带进不可行区域。第三步,min(max(newDec, ...))是边界拉回,把超出上下界的坐标直接夹回边界,这也是进化算法里处理边界约束最常见的方式。第四步,比较新旧解时用求和是为了这段代码在单目标和多目标问题里都能跑通,严格场景下应该换成“新解支配旧解才接受”,多目标问题里求和比较可能会丢掉Pareto前沿上的一些好解。
关于步长参数,我把0.1写死在代码里了,更精细的做法是把它也加进ParameterNames,让GUI面板能暴露出来调。这类“算法自身的超参”是v2.0留给使用者最大的自由空间,也是新算法能不能超过基线的关键。局部搜索的触发间隔也一样,10代只是起始值,你完全可以放面板里做成可调参数。
4.4 在 GUI 中注册与验证:三种该看到的信号
文件保存好后,回到命令行重新启动GUI:
platemo新算法会出现在右侧算法列表里,名字就是你在obj.Name里写的MyNSGA2LS。如果列表里看不到,先检查文件名是不是MyNSGA2LS.m,文件名和类名不一致MATLAB会拒绝加载。这是新手最容易卡住的地方。
验证时做三件事。第一,跑ZDT1,同样100个个体、10000次评估,IGD曲线应该比内置NSGA-II略好或者持平——如果差很多,问题通常出在你的改动破坏了原模板的某种机制,回滚对比一下。第二,看最终种群图,散点应该贴合真实前沿,如果出现一大批点堆在角落或者偏离前沿,说明局部搜索的接受准则太宽松了,把求和比较换成支配判断。第三,把局部搜索间隔从10改成1、5、30各跑一遍,观察性能变化,确认这个机制在你的问题设置里是正增益。这一步做完,你的自定义算法就算正式接入平台了。
5. PlatEMO v2.0 避坑指南:五个让人想砸电脑的场景与解法
5.1 类未定义:文件在路径里,MATLAB 却说找不到
现象:自己写的算法文件明明放在算法目录下,一运行就报Undefined variable "MyNSGA2LS" or class "MyNSGA2LS".
原因:绝大多数情况是文件名和类名不一致。MATLAB对类的加载是按文件名找的,你把类定义成MyNSGA2LS,文件名却是test1.m,平台自然找不到。另一个常见原因是文件放进了某个子目录,而这个子目录不在MATLAB搜索路径里。package目录的规则比较严格,放进+PlatEMO/+Algorithm内层再套自己的子目录,有时也会导致类加载失败。
解决:类名、文件名、文件所在目录三者对齐。最简单的是把新算法文件放在+PlatEMO/+Algorithm目录下,和内置算法平级。文件保存后重新执行clear classes,再重启GUI,90%的“找不到类”都能解决。
5.2 IGD 曲线横着不走:目标值没进 Population
现象:算法能跑,界面上的种群点也在动,但IGD曲线几乎是一条水平线,数值还特别大。
原因:你的代码里新个体的目标值没有通过问题对象的评估接口计算,或者说算完没写回Population。有的写法是newDec = ...之后直接Population(i).obj = newObj,但newObj是从某个黑匣子函数里拿来的,不是Problem.CalObj的返回值,评估计数器和结果记录完全没感知到这些个体。平台按自己的计数器记录指标,你的实际迭代和它的记录脱节了。
解决:检查所有新解生成路径,确认每一条都经过Problem.CalObj之类的评估接口。还有一个排查技巧:在GUI上肉眼数一数,如果界面左下角的评估次数增长异常慢,说明有部分个体被跳过了评估,顺着代码里所有赋值obj的地方排查。
5.3 并行开启后结果和单核不一致:随机数流的锅
现象:同一配置单核跑出来的IGD是0.08,开8核并行后变成0.15,换了台机器结果又不一致。
原因:并行计算时每个worker有自己的随机数流,如果代码里直接用全局rand/randn而没有做随机数流控制,不同worker会生成重复或者高度相关的随机序列。这会让种群多样性在某几代里突然消失,进化停滞。这是个玄学问题,不跑固定种子几乎抓不到规律。
解决:v2.0的并行内部管理了部分随机逻辑,但自定义算法里新增的随机操作要自己做处理。最稳的方法是先在单核下完成算法调试,确信逻辑没问题后再开并行;并行实验务必固定种子,让每个worker的随机数流可复现。批量对比的时候也尽量让所有算法吃同一套随机种子配置,不然指标差异里混进了随机性。
5.4 参数面板改了没反应:ParameterValues 的坑
现象:GUI里把proC从1改成0.8,跑出来的结果和默认值一模一样,自己代码里打印ParameterValues发现还是老值。
原因:ParameterNames和ParameterValues的顺序没对齐,或者类型不对。GUI面板按ParameterNames渲染输入框,提交后按顺序写回ParameterValues;如果你在solve里写死了参数,比如proC = 1,那GUI改什么都没用。另一种情况是面板返回的是字符串,你取值后没做类型转换就参与数值计算。
解决:在solve开头加一行打印实际值来确认传递链路:
fprintf('N=%s proC=%s disC=%s\n', Algorithm.ParameterValues{1}, ... Algorithm.ParameterValues{2}, Algorithm.ParameterValues{3});凡是留在ParameterValues里的参数,代码里一律通过Algorithm.ParameterValues{index}引用,不要出现字面量常量。
5.5 自定义问题报错:边界维度与约束符号各错一遍
现象:跑内置问题没事,换成自己写的问题就报维度错误,或者约束问题跑完所有个体都不可行。
原因:自定义问题类里upper和lower的行向量维度要和D一致,很多人定义了D = 30,边界却写成1×10,初始化阶段就会崩。约束方面,v2.0的cons约定通常是0表示可行、大于0表示违反,如果你在问题里把违反量写成了负数,平台里所有默认的约束处理都会方向性失效。
解决:写自定义问题先在别的算法下跑一遍ZDT1验证环境,再逐步换成自己的问题。边界维度用size(Problem.upper)打印确认。约束值统一用“违反量越大数值越大、0为可行”的约定输出,并且评估函数里不存在约束时返回全0行向量,不要返回空数组。
6. 批量实验与后处理:用一个小脚本把多组对比跑出可发表的数据
单次跑GUI当然方便,但论文级对比通常需要“同一批问题 × 同一批算法 × 多次独立运行”的矩阵。v2.0的platemo函数可以直接用键值对参数调用,不弹GUI,适合写进脚本批量执行:
% 批量实验:MyNSGA2LS 和内置 NSGA-II 对比,各跑 5 次独立重复 algorithms = {@NSGAII, @MyNSGA2LS}; problems = {@ZDT1, @DTLZ2, @MaF1}; for a = 1 : length(algorithms) for p = 1 : length(problems) for r = 1 : 5 platemo('algorithm', algorithms{a}, ... 'problem', problems{p}, ... 'N', 100, 'maxFE', 10000, ... 'folder', ['Results/Batch_a' num2str(a) '_p' num2str(p)]); end end end这里需要注意platemo的参数键名在不同小版本里略有差异,第一次跑之前先help platemo看当前版本的帮助,以实际键名为准。folder参数把不同子实验的结果分目录存放,比全都堆在默认Results里好整理得多。批量跑的时候建议先把总运行次数控制在半小时内,确认脚本没问题再放开规模。
结果文件都是.mat,后处理时用whos -file看变量名,然后把每一代的指标列提出来画平均收敛曲线:
files = dir('Results/Batch_a*_p*/*.mat'); allIGD = []; for i = 1 : length(files) data = load(fullfile(files(i).folder, files(i).name)); % 变量名以 whos -file 输出为准,这里用最常见的名字 if isfield(data, 'IGD') allIGD(:, end+1) = data.IGD; %#ok<SAGROW> end end figure; plot(mean(allIGD, 2), 'LineWidth', 1.5);跑批量时我有个血泪教训:第一版脚本把folder键名写错了,结果所有结果都堆在默认目录里,小版本命名还带了时间戳,最后整理数据时花了两个小时手动分文件。后来形成的习惯是每条批量实验固定一个母目录,实验结束当晚就把均值、方差和最优值三个表生成出来,数据新鲜的时候最容易发现异常值。v2.0的批量能力不算快,但对标准对比实验,它已经比手工保存每张图、手工抄IGD数值的流程高效太多了。希望这套流程能帮你在做算法对比时少走一段弯路。
本文还有配套的精品资源,点击获取