
Optuna Pruners 深度解析optuna.pruners 模块的剪枝策略体系与源码实现【免费下载链接】optunaA hyperparameter optimization framework项目地址: https://gitcode.com/GitHub_Trending/op/optuna本文以官方文档docs/source/reference/pruners.rst中optuna.pruners模块的 API 说明为骨架系统梳理 Optuna 的 9 个剪枝器Pruner类BasePruner抽象基类、MedianPruner、NopPruner、PatientPruner、PercentilePruner、SuccessiveHalvingPruner、HyperbandPruner、ThresholdPruner与WilcoxonPruner。读完本文你将掌握每个剪枝器的参数含义、默认值与适用场景并能通过trial.report/trial.should_prune将剪枝机制正确接入目标函数同时理解各剪枝器在 optuna/pruners/ 目录下的源码级判定逻辑。模块总览BasePruner 与剪枝判定契约官方文档对optuna.pruners模块的定义是该模块以一个抽象方法prune为特征定义了基类BasePruner——给定一个 trial 及其所属 studyprune方法返回一个布尔值表示该 trial 是否应被剪枝。这个判定依据的是此前通过optuna.trial.Trial.report为该 trial 上报过的目标函数中间值intermediate values。模块中其余类均是继承自BasePruner的子类各自实现不同的剪枝策略。官方文档同时给出了一条重要限制warning: 当前optuna.pruners模块预期仅用于单目标优化single-objective optimization。从源码看这一契约非常简洁。BasePruner 抽象类 只有一个抽象方法class BasePruner(abc.ABC): Base class for pruners. abc.abstractmethod def prune(self, study: optuna.study.Study, trial: optuna.trial.FrozenTrial) - bool: Judge whether the trial should be pruned based on the reported values. Note that this method is not supposed to be called by library users. Instead, :func:optuna.trial.Trial.report and :func:optuna.trial.Trial.should_prune provide user interfaces to implement pruning mechanism in an objective function. raise NotImplementedError两个关键信息签名prune(study, trial) - bool。实现者拿到FrozenTrial时文档提示“Take a copy before modifying this object”即 pruner 不应就地修改 trial 对象。调用方约束prune方法不期望被用户直接调用用户面向的接口是trial.report(value, step)与trial.should_prune()二者分别位于 Trial.report 和 Trial.should_prune。所有 9 个类在 optuna/pruners/init.py 中通过__all__导出。该文件还有一个值得注意的辅助函数def _filter_study(study: Study, trial: FrozenTrial) - Study: if isinstance(study.pruner, HyperbandPruner): # Create _BracketStudy to use trials that have the same bracket id. pruner: HyperbandPruner study.pruner return pruner._create_bracket_study(study, pruner._get_bracket_id(study, trial)) else: return study它揭示了 Hyperband 的架构细节Hyperband 内部由多个SuccessiveHalvingPruner即“bracket”组成当 pruner 是HyperbandPruner时会创建一个只包含同一 bracket 内 trial 的_BracketStudy视图再交给对应子 pruner 判定。剪枝的标准工作流绝大多数剪枝器Median、Percentile、SuccessiveHalving、Hyperband 等共享同一套目标函数编写范式import optuna def objective(trial): alpha trial.suggest_float(alpha, 0.0, 1.0) clf SGDClassifier(alphaalpha) n_train_iter 100 for step in range(n_train_iter): clf.partial_fit(X_train, y_train, classesclasses) intermediate_value clf.score(X_valid, y_valid) # 每步计算中间指标 trial.report(intermediate_value, step) # 上报中间值 if trial.should_prune(): # 询问剪枝器 raise optuna.TrialPruned() # 被判定剪枝则提前终止 return clf.score(X_valid, y_valid)配套的最小研究创建代码以文档 MedianPruner 示例 为例study optuna.create_study( directionmaximize, pruneroptuna.pruners.MedianPruner( n_startup_trials5, n_warmup_steps30, interval_steps10 ), ) study.optimize(objective, n_trials20)要点trial.report只负责把“第step步时的中间值”记录下来是否剪枝完全由 pruner 的prune逻辑决定raise optuna.TrialPruned()会让该 trial 以 PRUNED 状态结束从而把剩余计算预算释放给其他 trial。PercentilePruner 与 MedianPruner基于历史 trial 分位数的停止规则PercentilePruner的语义是保留处于前若干分位数的 trial把落在同一 step 下落后分位内的 trial 剪掉。它是最通用的“跨 trial 比较”式剪枝器。参数一览含默认值与校验源码位于 PercentilePruner 构造函数参数默认值含义与校验percentile必填分位数取值必须为[0, 100]闭区间。例如25.0表示只保留同 step 下最好的 25% trialn_startup_trials5完成 trial 数未达到该值前禁用剪枝必须 0n_warmup_steps0step n_warmup_steps时禁用剪枝最早可在step n_warmup_steps被剪必须 0interval_steps1两次剪枝检查之间的 step 间隔偏移基准是 warmup steps必须 1n_min_trials1某 step 上已有中间值的 trial 少于该数量时不剪枝必须 1percentile参数的官方示例optuna/pruners/_percentile.pystudy optuna.create_study( directionmaximize, pruneroptuna.pruners.PercentilePruner( 25.0, n_startup_trials5, n_warmup_steps30, interval_steps10 ), ) study.optimize(objective, n_trials20)MedianPrunerPercentilePruner 的 50% 特例MedianPruner源码直接继承PercentilePruner并固定percentile50.0即“中位数停止规则”若当前 trial 在截至当前步的最好中间值差于此前已完成 trial 在同一 step 的中间值中位数则剪枝。它接受与PercentilePruner相同的四个参数构造函数如下def __init__( self, n_startup_trials: int 5, n_warmup_steps: int 0, interval_steps: int 1, *, n_min_trials: int 1, ) - None: super().__init__( 50.0, n_startup_trials, n_warmup_steps, interval_steps, n_min_trialsn_min_trials )注意n_min_trials是关键字-only 参数。MedianPruner 文档还明确说明了 NaN 处理策略当前 trial 的全部中间值均为 NaN 时直接剪枝计算历史 trial 中位数时 NaN 会被忽略np.nanpercentile。源码级判定逻辑PercentilePruner.prune的判定链prune 方法可以概括为startup 保护读取study.get_trials(deepcopyFalse, states(TrialState.COMPLETE,))若已完成 trial 数为 0 或少于n_startup_trials返回False不剪枝warmup 保护step trial.last_step若为None尚未 report或step n_warmup_steps返回Falseinterval 节流通过 _is_first_in_interval_step 判断当前 step 是否落在[n_warmup_steps k*interval_steps]的网格上并考虑 trial 实际 report 过的 step 集合非网格点跳过检查从而降低计算开销自身指标_get_best_intermediate_result_over_steps取当前 trial 所有中间值中的nanmaxmaximize 方向或nanminminimize 方向若为 NaN 则直接返回True剪枝历史分位数_get_percentile_intermediate_result_over_trials收集所有完成 trial 在同一step的中间值若数量不足n_min_trials返回 NaN即不剪枝否则用np.nanpercentile计算阈值maximize 方向会做100 - percentile翻转最终比较maximize 方向下best_intermediate_result p则剪枝minimize 方向下best_intermediate_result p则剪枝。对应的单元测试在 tests/pruners_tests/test_percentile.py 与 tests/pruners_tests/test_median.py可用于验证“启动期不剪枝”“warmup 后按网格检查”“NaN 处理”等行为。NopPruner 与 ThresholdPruner基线与边界检测NopPruner永不剪枝的空实现NopPruner 是最简单的剪枝器实现只有一行class NopPruner(BasePruner): Pruner which never prunes trials. def prune(self, study: Study, trial: FrozenTrial) - bool: return False它主要用于对照实验对比有/无剪枝的效果、或在目标函数已含自报告剪枝如early_stopping时避免双重判定。其文档示例中甚至直接断言should_prune()永远为False。ThresholdPruner越界即剪的异常检测器ThresholdPruner源码用于检测离群指标中间值跌破下界、突破上界或变为nan时剪枝。它不像分位数类剪枝器那样依赖历史 trial只做单 trial 的阈值比较因此特别适合“loss 爆炸”“验证准确率归零”这类异常检测场景。参数默认值含义与校验lowerNone下界中间值小于该值则剪枝None时取-infupperNone上界中间值大于该值则剪枝None时取inflower与upper必须至少指定一个且lower uppern_warmup_steps0早于该 step 不剪枝interval_steps1剪枝检查间隔复用 percentile 模块的 _is_first_in_interval_step 网格逻辑官方示例展示了上、下两种边界用法示例代码from optuna import create_study from optuna.pruners import ThresholdPruner from optuna import TrialPruned def objective_for_upper(trial): for step, y in enumerate(ys_for_upper): trial.report(y, step) if trial.should_prune(): raise TrialPruned() return ys_for_upper[-1] def objective_for_lower(trial): for step, y in enumerate(ys_for_lower): trial.report(y, step) if trial.should_prune(): raise TrialPruned() return ys_for_lower[-1] ys_for_upper [0.0, 0.1, 0.2, 0.5, 1.2] ys_for_lower [100.0, 90.0, 0.1, 0.0, -1] study create_study(prunerThresholdPruner(upper1.0)) study.optimize(objective_for_upper, n_trials10) study create_study(prunerThresholdPruner(lower0.0)) study.optimize(objective_for_lower, n_trials10)其prune实现判定逻辑先看last_step是否存在、是否越过 warmup、是否命中 interval 网格然后依次检查math.isnan(latest_value)→ 剪latest_value self._lower→ 剪latest_value self._upper→ 剪。PatientPruner为“慢热型”trial 加容忍度的包装器PatientPruner源码实验性 APIexperimental_class(2.8.0)是一个装饰器式剪枝器它包装另一个 pruner并引入“耐心期”——只有当目标函数连续patience个 step 都没有实质性改善时才允许被包装的 pruner 执行剪枝判定。参数默认值含义wrapped_pruner必填被包装的BasePruner实例若为None则等价于仅基于本 trial 中间值的 early-stoppingpatience必填连续多少个 step 无改善后才允许剪枝必须 0min_delta0.0判定“有改善”的最小变化量必须非负官方示例示例study optuna.create_study( directionmaximize, pruneroptuna.pruners.PatientPruner(optuna.pruners.MedianPruner(), patience1), ) study.optimize(objective, n_trials20)从 prune 实现 看其逻辑是取出当前 trial 全部已 report 的中间值序列若可判定的 step 数不足patience 1则不剪枝否则把序列拆成“patience 步之前”与“patience 步之后”两段用nanmin/nanmax比较——若 patience 步之前的历史最优与之后的最优之差小于min_delta按 minimize/maximize 方向判定“变差”则认为进入平台期此时若wrapped_pruner非None转交self._wrapped_pruner.prune(study, trial)做最终裁决否则直接返回True。NaN 处理patience 期内全为 NaN 时不剪枝计算时忽略 NaN。这一“先容忍、再委托”的结构使它成为MedianPruner/PercentilePruner降低误剪率的通用外罩相关行为测试见 tests/pruners_tests/test_patient.py。SuccessiveHalvingPruner异步逐次减半ASHASuccessiveHalvingPruner源码实现了异步 Successive HalvingASHA——一个基于 bandit 的算法用于在多个配置中快速找出最优者。官方文档明确提示该 pruner不负责论文中的最大资源参数R单 trial 的最大资源通常由目标函数内部的step上限如迭代数来限制。参数一览参数默认值含义与校验min_resourceauto最小资源r。auto时用启发式估计也接受 1的整数。若不同 trial 的最终 step 可能变化需手动指定最小可能 stepreduction_factor4削减因子η必须 2。每层 rung 完成后约1/η的 trial 被晋升min_early_stopping_rate0最小早停率s必须 0bootstrap_count0一层 rung 中至少要有多少个 trial 完成才允许任何 trial 晋升到下一层必须 0与min_resourceauto互斥官方示例示例study optuna.create_study( directionmaximize, pruneroptuna.pruners.SuccessiveHalvingPruner() ) study.optimize(objective, n_trials20)源码级机制rung 与 system_attrs从 prune 实现 可以看清 ASHA 的落地方式rung 定位_get_current_rung 检查 trial 的system_attrs中是否存在completed_rung_0、completed_rung_1……键当前 rung 即第一个缺失键对应的层级复杂度O(log step)min_resource 自动估计当min_resourceauto时_estimate_min_resource 取已完成 trial 中最大last_step的last_step // 100下限 1在估计出来之前不剪枝晋升点检查晋升步为min_resource * reduction_factor ** (min_early_stopping_rate rung)未达到则返回False到达晋升点后若当前中间值为 NaN 直接剪枝否则把该值写入 trial 的system_attrsstudy._storage.set_trial_system_attr(trial._trial_id, rung_key, value)再收集所有在system_attrs中带有同rung_key的 competing values晋升判定_is_trial_promotable_to_next_rung 把 competing values 排序后取“前len(competing)//reduction_factor名”阈值特别地当 competing 数量不足reduction_factor时promotable_idx -1由于 Optuna 不支持挂起/恢复进行中的 trial实现对前η-1个 trial 采取“值最优者晋升”的兜底策略循环上移若当前 trial 在本层被晋升rung 1并循环回到第 2 步继续检查更高层的晋升点——这正是“异步”的关键一个 trial 在同一次prune调用中可能连升多层。单元测试见 tests/pruners_tests/test_successive_halving.py。HyperbandPruner多 bracket 并行运行 SuccessiveHalvingHyperbandPruner源码解决的是 ASHA 的一个超参数困境SHA 需要配置总数n在固定预算B下每个配置平均只分到B/n的资源B与B/n存在权衡。Hyperband 通过固定预算下尝试不同的n来攻击这一权衡——每个不同的n对应一个“bracket”。参数一览参数默认值含义与校验min_resource1最小资源r语义同SuccessiveHalvingPrunermax_resourceauto最大资源论文中R max_resource / min_resource应设为与实际最大迭代步数一致如神经网络的 epoch 数。auto时根据第一个完成的 trial 的last_step估计max(n_steps) 1在此之前不剪枝若各 trial 最终 step 可能变化需手动指定reduction_factor3削减因子η语义同上bootstrap_count0同SuccessiveHalvingPruner与max_resourceauto互斥bracket 数量由公式自动确定_try_initializationn_brackets floor(log_reduction_factor(max_resource / min_resource)) 1文档中特别建议把reduction_factor调到使 bracket 数约为 46 个。每个 bracketbracket_id对应一个SuccessiveHalvingPruner(min_resource..., reduction_factor..., min_early_stopping_ratebracket_id, ...)即每个 bracket 就是 ASHA 论文中不同的s值。官方示例示例study optuna.create_study( directionmaximize, pruneroptuna.pruners.HyperbandPruner( min_resource1, max_resourcen_train_iter, reduction_factor3 ), ) study.optimize(objective, n_trials20)使用注意事项来自源码文档注释与 TPE 采样器配合Optuna 默认使用TPESampler其启动需要若干默认 10 个trial。由于 Hyperband 按 bracket 分组收集 trial若内部有 4 个 bracket则启动期约消耗4 × 10个 trial因此文档建议配合更大的n_trials或timeoutbracket 分配与可复现性prune通过_get_bracket_id(study, trial)计算当前 trial 所属 bracket该函数依赖study_name与 trial number文档建议显式指定study_name以保证剪枝算法可复现bracket 视图如前文所述optuna/pruners/init.py 中的_filter_study会为 Hyperband 构造只含同 bracket trial 的_BracketStudy再调用对应子 pruner 的prune每个 bracket 还按 Hyperband 论文 Algorithm 1 的分配比例ceil(n_brackets * η^s / (s 1))计算 trial 分配预算_calculate_trial_allocation_budget。单元测试见 tests/pruners_tests/test_hyperband.py。WilcoxonPruner基于 Wilcoxon 符号秩检验的统计剪枝WilcoxonPruner源码实验性 APIexperimental_class(3.6.0)与其他 pruner 有本质区别它面向的是在一组问题实例上评估“均值/中位数”性能的场景例如启发式方法模拟退火、遗传算法、SAT 求解器等在一组实例上的平均表现机器学习模型的 k 折交叉验证得分LLM 输出一组问题的准确率。其原理是对当前 trial与当前最佳 trial的逐实例评分做 Wilcoxon 符号秩检验一旦在给定 p 值意义下确信当前 trial 更差就剪枝。因此使用方式与其他 pruner 不同每个 step 应传实例 id可以乱序、不必递增report 的 value 是“该实例上的得分”而非收敛中的历史均值同一实例在不同 trial 中必须使用相同的 id否则无法配对损失值剪枝效果会退化每个 trial 内建议打乱评估顺序避免对前几个实例过拟合若should_prune()返回True可以返回当前估计值如已评估值的平均而不是raise optuna.TrialPruned()——这是为了把剪枝 trial 的部分评估结果也告知 Optuna 的变通做法。参数默认值含义p_threshold0.1剪枝 p 值阈值必须在[0, 1]内越大剪枝越激进。注意该 pruner 是序贯检验样本不断增多时反复检验误判率与只检验一次不同如需名义误判率应使用 Pocock 校正后的 p 值n_startup_steps2可比较观察数达到该步数前不剪枝即使设为 0 或 1前两步也不会剪枝数据不足官方示例示例import optuna import numpy as np def evaluate(param, instance): # A toy loss function for demonstrative purpose. return (param - instance) ** 2 problem_instances np.linspace(-1, 1, 100) def objective(trial): param trial.suggest_float(param, -1, 1) results [] # For best results, shuffle the evaluation order in each trial. instance_ids np.random.permutation(len(problem_instances)) for instance_id in instance_ids: loss evaluate(param, problem_instances[instance_id]) results.append(loss) trial.report(loss, instance_id) # 用实例 id 作为 step if trial.should_prune(): return sum(results) / len(results) # 变通返回估计值而非抛异常 return sum(results) / len(results) study optuna.create_study(pruneroptuna.pruners.WilcoxonPruner(p_threshold0.1)) study.optimize(objective, n_trials100)从 prune 实现 可确认几个细节依赖scipyscipy.stats.wilcoxon惰性导入当前 trial 或 best trial 的中间值含inf/NaN 时发出警告且永不剪枝通过np.intersect1d按实例 id 配对两个 trial 的得分并求差alternative方向随 study 方向maximize 用lessminimize 用greater并有一个安全防护若统计检验判定当前 trial 更差、但 best trial 的均值并未优于当前 trial 中间值的均值则出于安全放弃剪枝average_is_best检查。测试见 tests/pruners_tests/test_wilcoxon.py。各 Pruner 速查表与选型建议Pruner判定依据关键参数默认值典型场景NopPruner永不剪枝无对照实验、目标函数自带 early-stoppingMedianPruner历史同 step 中位数50% 分位n_startup_trials5、n_warmup_steps0、interval_steps1、n_min_trials1通用迭代式训练默认首选PercentilePruner历史同 step 任意分位数增加percentile0–100想保留更多 trial 时调高分位如 75PatientPruner包装器patience 期无改善才委托内层 prunerpatience必填、min_delta0.0指标早期震荡大、易被误剪ThresholdPruner单 trial 越界/nanlower、upper至少一个检测 loss 爆炸等异常SuccessiveHalvingPrunerASHA 逐层晋升1/η存活率min_resourceauto、reduction_factor4、min_early_stopping_rate0、bootstrap_count0资源step可分级配置HyperbandPruner多 bracket 并行 ASHA自动遍历不同nmin_resource1、max_resourceauto、reduction_factor3、bootstrap_count0不确定最优 trial 数/预算分配WilcoxonPruner与 best trial 的逐实例 Wilcoxon 检验p_threshold0.1、n_startup_steps2多实例平均性能、k-fold CV、LLM 准确率选型上的一个结构性观察MedianPruner/PercentilePruner/ThresholdPruner共享同一套 warmup interval 网格机制_is_first_in_interval_step因此行为模式一致只是比较基准不同跨 trial 分位数 vs 单 trial 阈值PatientPruner是正交的组合维度可与上述任一 pruner 叠加SuccessiveHalvingPruner/HyperbandPruner则属于“资源分配”路线其晋升记录存放在 trial 的system_attrs键名completed_rung_{rung}这也是它们能跨 trial 异步比较的基础WilcoxonPruner则属于统计检验路线要求按实例 id 上报。延伸阅读与测试入口自定义剪枝器官方文档指向的 user-defined pruner 教程对应仓库中的 tutorial/20_recipes/006_user_defined_pruner.py演示了如何实现BasePruner.prune各剪枝器的行为测试集中在 tests/pruners_tests/包括 test_median.py、test_patient.py、test_percentile.py、test_successive_halving.py、test_hyperband.py、test_threshold.py、test_wilcoxon.py 与 test_nop.py是验证参数边界如percentile越界、reduction_factor 2、bootstrap_count与auto互斥等 ValueError 分支的一手依据本模块 API 文档源文件为 docs/source/reference/pruners.rst其导出的类清单与 optuna/pruners/init.py 的__all__一致。最后重申官方文档的限制optuna.pruners模块当前预期仅用于单目标优化。在多目标场景如 NSGA-II/III 采样器中应依赖 Pareto 前沿而非中间值比较来筛选个体这一点在选用任何上述剪枝器之前都应先确认你的 study 是单目标的。【免费下载链接】optunaA hyperparameter optimization framework项目地址: https://gitcode.com/GitHub_Trending/op/optuna创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考