超参数优化工具之Optuna

简介

Optuna是一个自动超参数搜索的超参数优化框架, 可应用于机器学习和深度学习模型.主要具备如下特点:

  • Define-by-run API: 允许用户动态构建参数搜索空间 > 论文举例: 以针对 MLPClassifier 优化层数与隐藏层单元数为切入点与 Hyperoptdefine-and-run 式相对比, 凸显其动态构建空间的便捷; 该方式也是目前主流深度学习(Pytorch、TensorFlow Eager等)风格.
  • 针对搜索和修剪两者结合的有效实现
    • 动态构造参数空间的采样⽅法: 关系采样(参数间相关性)和独立采样(各个参数) > 在 define-by-run 中实现关系采样较难, 采用识别实验结果的方式(独立采样结果推断并发关系), 如 CMA-ES 和 GP-BO 算法.
    • ⾼效剪枝算法: 采用异步连续减半算法(ASHA), 详见理论部分, 极其适合分布式环境.
  • 易于配置的通用架构, 可扩展分布式并通过交互界面进行试验 > 训练结果可存储于数据库从而被多个节点分享, 并且可通过 Dashboard 展示优化过程.
图1: Optuna 架构

架构图参考: OPTUNA: A Flexible, Efficient and Scalable Hyperparameter Optimization Framework

与目前其他工具对比:

图2: Optuna与其他超参数优化框架对比

官方文档及其页面如下:

理论

该项目主要基于2019年发表的Optuna: A Next-generation Hyperparameter Optimization Framework论文提供理论支持.

采样算法

  • optuna.samplers.TPESampler: 实现的 Tree-structured Parzen Estimator 算法
  • optuna.samplers.CmaEsSampler: 实现的 CMA-ES 算法
  • optuna.samplers.GridSampler: 实现的网格搜索
  • optuna.samplers.RandomSampler: 实现的随机搜索

TPE 算法

该部分参考论文Algorithms for Hyper-Parameter Optimization

TPE 算法(Tree-structured Parzen Estimator Approach)是基于顺序模型的优化方法(SMBO), 理论基础为贝叶斯优化(Bayes optimizaion, BO), 选择的评价标准为 Expected Improvement(EI), 即 \(f(x)\) 会小于某个阈值 \(y^{\ast}\) 的期望, 其中: \(x\) 代表特定参数, \(y\) 代表其对应的模型表现.

关于 \(EI\) 公式推导, 可以参见AutoML HPO 学习笔记(一)- 贝叶斯优化

\[ EI_{y^{\ast}}(x) = \int_{-\infty}^{\infty} \max(y^{\ast} - y, 0) p(y|x) dy \]

由于 \(\max\) 分段特性, 定义如下两个概率密度

\[ p(y|x) = \begin{cases} l(x), & y < y^{\ast} \\ g(x), & y \ge y^{\ast} \end{cases} \]

此时思考如何确定 \(y^{\ast}\), 作者采用用分位值 \(\gamma\) 来进行确定, 则 \(p(y < y^{\ast}) = \gamma\)

由此将 \(EI\) 进行转换

\[ \begin{aligned} EI_{y^{\ast}}(x) &= \int_{-\infty}^{\infty} \max(y^{\ast} - y, 0) p(y|x) dy \\ &= \int_{-\infty}^{y^{\ast}} (y^{\ast} - y) p(y|x) dy \\ &= \int_{-\infty}^{y^{\ast}} (y^{\ast} - y) \frac{p(x|y) p(y)}{p(x)} dy \\ &= \frac{\int_{-\infty}^{y^{\ast}} (y^{\ast} - y) p(x|y) p(y) dy}{\int_{-\infty}^{\infty} p(x|y)p(y)dy} \\ &= \frac{l(x) \int_{-\infty}^{y^{\ast}} (y^{\ast} - y) p(y) dy}{ \gamma l(x) + (1 - \gamma) g(x)} \\ &= \frac{\gamma y ^{\ast} l(x) - l(x)\int_{-\infty}^{y^{\ast}} y p(y) dy}{ \gamma l(x) + (1 - \gamma) g(x)} \\ &\varpropto (\gamma + \frac{g(x)}{l(x)} (1 - \gamma))^{-1} \end{aligned} \]

由此可见: 若获得更高 \(EI\), 则倾向于选择 \(l(x)\) 较大, \(g(x)\) 较小的 \(x\)

CMA-ES 算法

CMA-ES 算法(Covariance Matrix Adaptation Evolutionary Strategies)中文名称是协方差矩阵自适应进化策略, 主要用于解决连续优化问题, 尤其在病态条件下的连续优化问题.

图3: CMA-ES 算法图

该部分博主仔细研究下, 有兴趣可参考进化策略及其在深度学习中的应用

剪枝算法

  • optuna.pruners.SuccessiveHalvingPruner: 实现的 Asynchronous Successive Halving 算法.
  • optuna.pruners.HyperbandPruner: 实现的 Hyperband 算法.
  • optuna.pruners.MedianPruner: 实现的中位数剪枝算法 > 条件: 当前轮次中间结果不如轮次中位数即被抛弃
  • optuna.pruners.ThresholdPruner: 实现的阈值剪枝算法 > 条件: 中间结果大于阈值即被抛弃

Asynchronous Successive Halving algorithm

论文见: A System for Massively Parallel Hyperparameter Tuning

基于动态资源分配的超参优化算法最早以SHA算法(连续减半算法)出现.

核心思想: 针对超参数的训练过程, 通过在每一轮的超参数表现筛选其中 \(1 / \eta\) 组表现最好的参数继续训练 \(\eta \star r\) 步骤, 由此可见随着训练, 其评估参数组合越少, 但两次评估间的训练步骤增多.

图4: SHA 算法

从 SHA 可以看出可将单轮的多层次评估分布式, 但存在单个评估较慢, 从而拖累整体速度, ASHA 则是优化评估过程, 无需评估完成整体一轮即可开始下一轮的训练过程(其参数组合表现较优).

图5: ASHA 算法

两者算法在分布场景的资源利用率测试如图, 图片源引论文解读笔记: 大规模并行超参数优化

图 6: SHA 算法分布式场景表现
图 7: ASHA 算法分布式场景表现

Hyperband algorithm

论文见: Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization

背景: 在 SHA 算法中, 由于随着评估, 单参数训练资源逐步释放, 因此存在前期的资源少导致的训练局限和后期的资源多导致的训练浪费, 即 \(B\)\(\frac{B}{n}\) 的权衡.

核心思想: 通过对 n 进行控制, 开始时至少一个 configuration 可保证 R 资源, 末期每个均可保证.

图 8: Hyperband 算法

关于Hyperband具体流程与SHA的对比可以查看如下示例:

\(R = 81\), \(\eta = 3\), 此时可以推断出 \(s_{max} = \log_{\eta} R = 4\), \(B = (s_{max} + 1) \times R\)

图 9: Hyperband 示例

上图中的左半部代表着从第一轮到最后一轮的资源分配情况, 其中可以看出 \(s = 4\) 时, 首先依照 SHA 计算当前轮的 81 次实验依照 1 份资源进行训练, 之后依照 Hyperband 要求逐步放大资源, 从而保证其中至少一个实验拥有单实验最大资源.

上图中的右半部代表最优结果并非从 SHA 所采用的 \(s = 0\) 中筛选而来, 而是在 \(s = 3\) 中筛选而来

使用

使用部分主要基于 LightGBM 模型调试进行.

快速启动

# 1. 定义目标函数(最大化)
def objective(trial):
    ...

    # 2. 配置超参数
    param = {
        'objective': 'binary',
        'metric': 'binary_logloss',
        'verbosity': -1,
        'boosting_type': 'gbdt',
        'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),
        'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
    }

    gbm = lgb.train(param, dtrain)
    ...
    return accuracy

# 3. 创建学习任务, 优化 100 个实验
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)

利用上面示例即可针对一个 lightGBM 模型进行超参数探索, 一般关于目标函数部分使用者可以根据自己的想法进行实现. 例如博主一般通过 K-fold 来求得该参数组合(trial)的表现.

def lgb_objective(trial):
    param = {}

    lgb_cv_results = lgb.cv(
        params = param,
        train_set = dtrain,
        num_boost_round = 1000,
        nfold = N_FOLDS,
        stratified = True,
        early_stopping_rounds = 10,
        seed = SEED,
    )

    lgb_cv_results = pd.DataFrame.from_dict(lgb_cv_results)
    # 由于采用 early_stopping_rounds 导致实际训练的 num_boost_round 可能不同, 将其提取和整体训练参数合并作为用户自定义信息保留
    param["n_estimators"] = len(lgb_cv_results)
    trial.set_user_attr("param", param)

    # 保留最终CV-AUC结果
    best_score = lgb_cv_results["auc-mean"].values[-1]
    return best_score

针对存在想直接获取对应每次实验模型参数文件的情况, 可以通过如下方式进行保存提取, 即可调参结束就获得最优模型文件.

def objective(trial):
    ...
    with open("{}.pickle".format(trial.number), "wb") as fout:
        pickle.dump(model, fout)
    return best_score

# 读取最优模型结果
with open("{}.pickle".format(study.best_trial.number), "rb") as fin:
    best_model = pickle.load(fin)

持久化训练

Optuna 支持利用数据库存储训练过程数据, 也可通过 joblib 进行持续化, 参见如何保存和恢复 study?, 博主建议数据库方式.

import logging
import sys

import optuna

# 增加数据流句柄, 重定向输出
optuna.logging.get_logger("optuna").addHandler(logging.StreamHandler(sys.stdout))
# 启动训练过程, 并启动数据库(若已存在, 可配置是否断点继续训练)
study_name = "example-study"
storage_name = "sqlite:///{}.db".format(study_name)
study = optuna.create_study(study_name = study_name, storage = storage_name, load_if_exists=True)
# 打印整体训练过程数据
df = study.trials_dataframe(attrs=("number", "value", "params", "state"))
#    number      value  params_x     state
# 0       0   8.747372 -0.957596  COMPLETE
# 1       1   0.073070  2.270314  COMPLETE
# 2       2   1.814622  0.652921  COMPLETE
# 3       3  65.618463 -6.100522  COMPLETE
# 4       4  35.448785 -3.953888  COMPLETE
# 5       5   4.600108 -0.144786  COMPLETE

可视化结果

如果利用 Optuna 自身模块定制化查看自己想要的调参影响图, 博主一般直接通过 optuna-dashboard 工具粗略查看调参效果, 并且可以单点查看每次实验结果.

conda install -c conda-forge optuna-dashboard
optuna-dashboard sqlite:///study.db

参考资料


超参数优化工具之Optuna
https://www.windism.cn/1484990906.html
作者
windism
发布于
2022年2月20日
许可协议