简介:超体积指标是多目标优化中衡量解集质量的全局性指标,常被用于多目标进化算法的性能评估。这套 MATLAB 工具包面向需要计算超体积、排序非支配解集的算法研究者与学习者,围绕帕累托前沿评估中的参考点设定、体积计算和排序效率等关键问题提供可运行代码。压缩包共包含 3 个文件,全部为 .m 脚本,整体大小仅 6KB,涵盖模式计算、排序算法和超体积核心实现三个模块,结构清晰,便于阅读和二次开发。目前已有 956 人学习下载。通过学习代码,读者能深入理解超体积原理,掌握非支配解集排序与体积计算的具体实现,还可将其集成到多目标进化算法中作为适应度函数,指导种群进化以获得更优的帕累托前沿,适合科研入门和实践应用。
1. hypervolume 究竟是什么:多目标优化里最被低估的一个数字
在多目标优化里,算法跑完一轮,摆在你面前的是几十上百个非支配解,老板问“哪个算法效果更好”,你盯着散点图说不上来。hypervolume 正是解决这个问题的指标:它用参考点和解集围出的超体积大小,把收敛性和分布性压进一个数。值越大,说明解集离真实前沿越近、覆盖面越广,这是它成为 NSGA-III、MOEA/D 等算法内部比较基准的原因,也是你做实验时最该先算的一项。这篇笔记适合刚接触多目标优化的新手,也适合一直用 IGD 但想换一个更稳健指标的熟手。
2. 从支配关系到超立方体:为什么 hypervolume 值得算
2.1 几何直觉:一个以参考点为锚的多面体
以最小化双目标问题为例,每个解是坐标平面上的一个点,两个目标值越小越好。hypervolume 的定义看起来很抽象,几何上却很简单:给定一组解集 S 和一个参考点 r,S 里每个解 p 与参考点 r 围成一个以 p 为顶点、r 为对角点的超矩形,hypervolume 就是这些超矩形并集的体积。
更直白地说:你在目标空间里把解集所在的那块区域,从参考点方向“填满”,填出来的体积就是 HV。解越收敛到左下角(最小值问题),这个多面体往左下方的延伸越好;解分布得越开,多面体横向和纵向的跨度越大。这两点正好对应多目标优化的两个核心诉求:收敛性和多样性。
# 二维示意:参考点取 (5,5),两个解 p1=(1,2), p2=(3,1) # p1 围出的矩形是 (1,2) 到 (5,5),p2 围出的矩形是 (3,1) 到 (5,5) # 并集面积 = 这两个矩形重叠后的总面积这个例子看着简单,但请注意“并集”两个字。如果直接算两个矩形面积相加,重叠区域会被重复计算,这正是后面精确实现最需要注意的细节。
2.2 数学定义与那个“不想要就删”的参考点
正式定义是用 Lebesgue 测度写的:HV(S, r) = λ({q | ∃s∈S, s ⪯ q ⪯ r})。翻译成人话:所有被 S 中至少一个解支配、同时本身又支配参考点 q 的点组成的区域体积。这里要求解的目标值都优于参考点 r,也就是 r 必须被 S 里每个解支配,或者说 r 要在各个目标维度上都是极差的值。
对最小化问题,参考点 r 通常取各目标上界向量加上一个小偏移。选得太小,某些解与 r 之间没有超矩形空间,HV 计算出来偏小甚至为 0;选得太大,会导致 HV 数值巨大,解集之间的差距被稀释。这不是玄学,而是决定实验结果可比性的关键参数,具体取值方法放到第 4 章说。
这里补一张常用指标对比,方便理解 hypervolume 的特长:
| 指标 | 需要真实前沿 | 同时反映收敛与分布 | 适用维度 | 主要短板 |
|---|---|---|---|---|
| hypervolume | 不需要 | 是 | 2-5 维常用 | 精确计算代价高 |
| IGD | 需要 | 是 | 任意 | 依赖参考前沿质量 |
| GD | 需要 | 只剩收敛性 | 任意 | 不反映分布 |
| Spread | 需要 | 只剩分布性 | 任意 | 对前沿形状敏感 |
2.3 为什么说它是“无需真实前沿”的黑匣子指标
你对比两个算法的效果,用 GD、IGD 都需要先知道真实 Pareto 前沿,而多数工程问题的真实前沿是未知的。hypervolume 不需要外部参考前沿,只需要一个你自己定的参考点,这个参考点本质上是对“目标范围”的合理估计。因此 HV 可用来比较任何两批解集,哪怕它们来自不同的算法、不同的初始化解、甚至不同的目标函数归一化方式。
也正因为这样,HV 常被用来做算法早停判据:每迭代一轮算一次 HV,HV 增长曲线变平缓,说明解集已经收敛到稳定区域,再跑只是微调。这个用法是第 6 章的重点。
3. 最小可运行实现:用 Python 写出第一个 hypervolume 计算
3.1 环境准备与数据约定
实现 hypervolume 前先把数据格式定清楚。我常用的约定是:一个 2D NumPy 数组,每一行是一个解,每一列是一个目标值,所有目标统一为最小化方向。如果你的问题是最大化,先做取反或取倒数变换。
import numpy as np # 模拟一批双目标解,目标均为最小化 S = np.array([ [1.0, 3.0], [2.0, 2.0], [3.0, 1.0], [1.5, 2.5], ]) ref = np.array([4.0, 4.0]) # 参考点,每个目标都劣于所有解这段代码里 S 的每一行是一个解,ref 是参考点。先确认一件事:S 里每个解的所有目标值都必须严格小于 ref,否则该解不参与 HV 贡献,甚至会发生方向性错误。我一般会在计算前加一个检查语句,过滤掉不满足条件的解。
3.2 二维精确计算:扫描线法
二维场景下 hypervolume 有很简洁的精确算法:把所有解按 x 轴(第一个目标)降序排序,然后从左往右扫,维护 y 轴贡献的最大值,累加每个解带来的增量面积。这也是理解 HV 几何构造最直观的方式。
def hypervolume_2d(points, ref): """精确计算二维超体积。points、ref 均为 [x, y] 结构,最小化方向。""" points = np.array(points, dtype=float) points = points[np.all(points < ref, axis=1)] # 只保留被参考点包围的解 if len(points) == 0: return 0.0 # 按 x 降序排列,相同 x 时 y 降序,确保扫描顺序正确 order = np.lexsort((points[:, 1], points[:, 0]))[::-1] points = points[order] hv = 0.0 max_y = 0.0 for x, y in points: # 当前解相对参考点的矩形高度,要扣除已被覆盖过的部分 if y > max_y: hv += (ref[0] - x) * (y - max_y) max_y = y return hv hv = hypervolume_2d(S, ref) print("HV =", hv) # HV = 7.75核心逻辑在循环里:x 降序扫描时,已处理过的解在 x 维度上更靠左,它们与参考点围出的矩形会覆盖当前解的一部分 y 区间。max_y 记录的是“已完成扫描的解在 y 方向上覆盖到的最大上界”。只有当当前解的 y 值高于 max_y,才会产生新区块,新区块面积是宽度 (ref[0] - x) 乘以新增高度 (y - max_y)。这个扣除操作避免重复计算,也就是前面说的并集关键点。
参数说明:lexsort 的第一个键是 y、第二个是 x,配合 [::-1] 实现“先按 x 降序、再按 y 降序”;如果不做这个排序,扫描顺序错误会让结果偏大或偏小。ref[0] - x 是当前解到参考点横向距离,y - max_y 是未被覆盖的纵向新增高度。这个算法的时间复杂度是 O(n log n),主要花在排序上,n 是解的数量,适合解集规模在几千以内的场景。
3.3 三维及以上:蒙特卡洛近似与它的适用边界
三维以上精确计算超体积要复杂得多。精确算法用递归切面法,每层递归分解超矩形,复杂度随维度指数增长,工程里超过 5 个目标基本跑不动。常见做法是蒙特卡洛采样近似:在参考点约束的区域内随机撒点,统计落在解集支配区域内的点占比,乘以总体积就是 HV 的近似值。
def hypervolume_mc(points, ref, n_samples=100_000, seed=42): """蒙特卡洛近似超体积,适用于 3 维以上。""" rng = np.random.default_rng(seed) points = np.asarray(points, dtype=float) n_dim = len(ref) samples = rng.uniform(ref * 0, ref, size=(n_samples, n_dim)) # 每个样本检查是否被至少一个解支配 dominated = np.zeros(n_samples, dtype=bool) for p in points: # 解 p 支配样本 s 的条件:p 在每一维都小于等于 s,且至少一维严格小于 less_equal = np.all(samples >= p, axis=1) strict_less = np.any(samples > p, axis=1) dominated |= (less_equal & strict_less) volume = np.prod(ref) return volume * dominated.mean()这段代码先按参考点构成的空间均匀采样,默认假设原点(0,...,0)是理想边界,适用于目标值已经归一化到从 0 到参考点的区间。然后逐一判断样本是否被某个解支配,最后统计被支配样本占比。n_samples 参数直接决定精度:建议从 10 万起步,结果波动大就加到 100 万,但注意每次给 seed 固定值,否则同批解集两次计算结果不可复现。
蒙特卡洛的明显短板是精度受目标维度影响大:维度越高,被支配区域占总体积的比例可能越小,需要更多采样点才能达到同样的相对误差。在 3-4 维把它当趋势参考没问题,但别拿它和一维二维的精确结果做严格数值对比。
3.4 现有工具怎么选:先算完再决定要不要自己造轮子
实现方式很多,常见做法是先用成熟库验证自己的理解,再在性能瓶颈时改造。pymoo 里有pymoo.indicators.hv.Hypervolume,deap 也有 HV 工具,platypus 内置了指标计算。不过这些库在新版本里 API 变过好几次,如果你只是算个指标,建议直接读它们的源码实现思路。我一般只在二维时用自己写的扫描线,三维以上直接用 pymoo 或者自己写蒙特卡洛,视精度需求而定。
4. 参考点设定与数据归一化:决定 HV 数值可信度的三个参数
4.1 参考点取值的三种常用策略
参考点不是随便给的,它左右 HV 的绝对数值和可比较性。常见策略有三种,按场景选用:
| 策略 | 取值方式 | 适用场景 | 注意点 |
|---|---|---|---|
| 固定边界 | 目标函数已知上下界时,取各维上界+小偏移 | 仿真问题、约束明确 | 偏移过小会丢解信息 |
| 数据驱动 | 该批解集每维最大值基础上加 10%-20% | 算法对比实验 | 不同解集间要统一 |
| 前沿反推 | 用已知参考前沿计算极值再加偏移 | 有部分先验知识 | 偏移量要大于数值噪声 |
固定边界最省事,但工程问题里目标边界往往不精确,比如调度问题的完工时间上限是拍脑袋定的。实验对比时我倾向于数据驱动,但关键约束是“所有参比解集必须使用完全相同的参考点”,否则 HV 数值不可比。
代码层面直接在调用前统一处理:
# 多个算法结果统一参考点 all_poynts = np.vstack([res1, res2, res3]) ref = all_poynts.max(axis=0) * 1.2这里 1.2 是常用的经验偏移量,参考点因此严格劣于所有解。如果原始目标值跨数量级,比如第一个目标在 0-1,第二个目标在 100-1000,先做归一化再算 HV,否则第二个目标几乎主导整个体积数值,第一个目标的贡献完全被吞掉。
4.2 归一化细节:什么时候做,怎么做
做算法对比实验时,同一个问题跑不同算法,解集的目标值范围差异可能很大。如果不归一化,边界目标占主导,hypervolume 的比较结果基本失真。我一般把每个目标归一到 [0,1],取所有参比解集在每一维的全局最小值和最大值作为缩放依据,而不是用单个算法自己的极值。
之所以强调“全局”,是因为单个算法的极值会奖励分布窄的算法:它自己归一化自己,把本不拥挤的区域拉伸到满格,HV 虚高。这是踩坑重灾区,代码上只需要注意统一缩放参数:
min_vals = np.min(np.vstack([res1, res2, res3]), axis=0) max_vals = np.max(np.vstack([res1, res2, res3]), axis=0) norm1 = (res1 - min_vals) / (max_vals - min_vals + 1e-12)分母里加 1e-12 是为了防止某一维所有解值相同造成除零。归一化后目标区间是 [0,1],参考点取 1.1 或 1.0 均可,但要统一。注意:如果只是单次运行展示结果、不跨算法对比,归一化就不是必需的,对 HV 的单调排序没有影响。
4.3 最小化最大化问题:方向变换的常见误区
hypervolume 的定义基于最小化结构:解越靠近坐标原点越好。如果你的问题是最大化目标(比如收益、召回率),直接拿原始值算 HV 会得到完全错误的体积。常见做法是取负号把最大化转成最小化,但取负后目标区间变成负数区域,参考点不好确定。更稳的方法是取倒数或取 1 - 归一化值,让目标范围重新回到正数区间。
我习惯的做法:先做数据驱动归一化,把每个目标映射到 [0,1],最大化的目标用 1 - x 转换,最小化的目标保持 x,然后统一参考点为 [1.2]*n。这样做的好处是目标方向一致,HV 计算逻辑不用在函数里区分最大最小。很多现成库默认最小化,不处理方向就会一头扎进负数区域。
5. 常见问题与避坑:算出来的 HV 不对,多半是这五个地方
5.1 现象一:HV 计算结果是负数
这个现象一般会把人吓一跳。原因很直接:解集里有解不满足“被参考点支配”的约束,比如某个目标值大于参考点,导致算法内部的支配方向翻转,计算出来的数值失去意义。即使我的扫描线函数里有过滤逻辑,用第三方库时默认不检查,出了问题要自己排查。
解决方式:计算前显式断言np.all(points < ref)。如果不满足,优先检查参考点设置是否偏移太小,再检查是否忘了做最大化到最小化的方向转换。这也是我自己最常犯的,在新问题的目标范围没摸清时随手设个参考点,一跑就翻车。
5.2 现象二:两个算法 HV 一样,但散点图看起来差很多
参考点取得过大时,解集之间的差异被稀释到小数点后很多位,打印出来都是同一个值。原因很简单:体积的绝对大小被参考点主导,而解集的差异只体现在一块很小的区域。解决方式是压缩参考点,让它贴近解集边界。比如数据驱动策略下把 1.2 的偏移系数改成 1.05,或者先画散点图看解集范围,再定参考点。
5.3 现象三:同一批解,两次计算 HV 不同
这里有两个可能来源。第一种是蒙特卡洛近似没设固定随机种子,每次采样结果自然不同。第二种是解集排序不稳定,尤其是包含重复解时排序结果依赖初始顺序,扫描线算法对顺序敏感。解决方式:采样加 seed,排序前先对重复解去重或加稳定排序参数。去重时注意只去完全相同的目标向量,不是去支配重复,两回事。
5.4 现象四:解集包含被支配点,导致 HV 偏大
很多人把整个存档都丢进 HV 计算,但存档里可能残留被其他解支配的解。这些点的超矩形会被真实非支配解的矩形覆盖,理论上不影响并集面积。但在二维扫描线实现里,如果被支配点先进入循环并抬高了 max_y,后面真实前沿解的新增面积会被错误扣除,结果不但没偏大反而偏小。原因在于“被支配点对并集无贡献”这句话成立的前提是它排在正确位置,而排序时它可能干扰覆盖状态。解决方式:计算 HV 前现做一轮非支配排序过滤掉被支配点,这是最稳的做法。
5.5 现象五:3 维以上 HV 和论文对不上
如果你复现论文,HV 数值对不上先别怀疑自己算法错。论文的参考点原始向量没写明归一化区间是常见事,目标归一化方式不同,HV 数值就会差出几十倍。另外有些论文用精确算法,有些用蒙特卡洛,采样数不足导致误差。解决方式:仔细看论文的参考点定义,尽量让目标和参考点使用同一套归一化,再比较相对趋势而不是绝对数值。
6. 进阶用法:用 HV 增量做收敛判定,顺便验证求解器
6.1 从 HV 到 HV 增量:每个新解贡献的大小
HV 增量(ΔHV)指把一个新解 p 加入现有解集 S 后,HV 的增加量。由于 HV 衡量的是并集面积,这个增量天然等于“p 独立覆盖出、且未被 S 覆盖”的区域面积。用二维扫描线实现增量计算很容易:去掉当前解的 y 值与 max_y 比较时取 0,得到的就是净新增贡献。三维以上近似计算则复杂一些,但工程里常用蒙特卡洛差值的简化估算。
增量计算的最大用途是算法早停判断:每轮迭代结束后,把新产生的非支配解逐个并入存档,如果连续 N 轮 HV 增量都低于某个阈值,说明再跑下去收益微薄,可以停了。这比看种群代数更精准,因为种群代数只能告诉你“跑了多少”,不能告诉你“还有多少进步空间”。
6.2 一个简单的收敛判定循环
把增量判定做成一个通用循环,可以在任何基于种群的多目标算法上复用:
THRESHOLD = 0.001 # 增量阈值,按问题规模调整 STALE_ROUNDS = 5 # 连续几轮低于阈值就停 prev_hv = 0.0 stale_count = 0 for generation in range(max_gen): # 假设 solve_one_generation() 返回本轮的非支配解集 archive = get_archive_after_evolution(generation) hv_now = hypervolume_2d(archive, ref) delta = hv_now - prev_hv prev_hv = hv_now if delta < THRESHOLD: stale_count += 1 if stale_count >= STALE_ROUNDS: print(f"早停在第 {generation} 代,当前 HV = {hv_now:.4f}") break else: stale_count = 0这段逻辑的核心是“相对停滞”而不是“绝对停滞”,因为 HV 增量会随代际递减,前期大概率快速上升,后期逐渐平滑。阈值设置按 HV 绝对量级调整,建议先跑一次完整过程观察增量衰减曲线,再取曲线变平处数值的 1/10 作阈值。另外,早停依据 HV 增量需要保证每轮归档的解集具有可比性,比如都使用同一参考点、同一归一化参数,否则前后轮的 HV 值根本不在同一坐标系里。
6.3 验证 HV 计算器本身的正确性
把自己的 HV 计算器投入实验前,先做一次交叉验证。常见做法是用已知真实前沿的问题(比如 zdt1、dtlz1),求出理论 HV 值或直接用成熟库的计算结果做基准。取同一批解,同一参考点,对比我的扫描线实现和第三方库输出,差值小于 1e-6 才算通过。蒙特卡洛近似则用固定种子跑三次,看标准差在可接受范围内。
我自己的习惯是在新问题里先用二维小规模解集做人工验算:三个点手动画图,算矩形面积并集,再和代码输出比对。这一步虽然原始,却能最快抓出排序方向、参考点偏移这类基础错误。之后再做跨库交叉验证。经验是验证步骤不要省,hypervolume 的实现写错时,结果差一点会让你误判算法优劣,这种“看着不对又说不上哪里不对”的状态在实验里最花时间。
最后提醒一个我自己常犯的教训:hypervolume 的一切比较都建立在“测量条件一致”的前提上。参考点、归一化、去不去被支配点、精确还是近似,任何一项不一致都会毁掉可比性。早停判定里尤其要固定测量条件,否则拿到的收敛曲线只是假象。希望帮到你。
本文还有配套的精品资源,点击获取