超参数优化
超参数是用于控制学习过程的不同参数值,对机器学习模型的性能有显著影响。例如,随机森林算法中的估计器数量、最大深度和分裂标准等。超参数优化是找到超参数值的正确组合,以便在合理的时间内实现数据最大性能的过程。这个过程在机器学习算法的预测准确性中起着至关重要的作用。因此,超参数优化被认为是构建机器学习模型中最棘手的部分。
目前来说sklearn支持两种类型的超参数优化:
- GridSearchCV 网格搜索是一种广泛使用的传统方法,详尽地考虑了所有参数组合
- RandomizedSearchCV 随机搜索可以从具有指定分布的参数空间中抽样给定数量的候选者
贝叶斯优化方法 (Bayesian Optimization)是当前超参数优化领域的SOTA手段(State of the Art),可以被认为是当前最为先进的优化框架。
贝叶斯优化的工作原理是:首先对目标函数的全局行为建立先验知识(通常用高斯过程来表示),然后通过观察目标函数在不同输入点的输出,更新这个先验知识,形成后验分布。基于后验分布,选择下一个采样点,这个选择既要考虑到之前观察到的最优值(即利用),又要考虑到全局尚未探索的区域(即探索)。这个选择的策略通常由所谓的采集函数(Acquisition Function)来定义,比如最常用的期望提升(Expected Improvement),这样,贝叶斯优化不仅可以有效地搜索超参数空间,还能根据已有的知识来引导搜索,避免了大量的无用尝试。
具体的算法细节可以参考:https://zhuanlan.zhihu.com/p/643095927?utm_id=0
本文介绍一些实用的超参数优化技术:
- Hyperopt
- Scikit Optimize
- Optuna
# read the dataset |
Hyperopt
Hyperopt优化器是目前最通用的贝叶斯优化器之一,它集成了包括随机搜索、模拟退火和TPE(Tree-structured Parzen Estimator Approach)等多种优化算法。
官方文档:https://hyperopt.github.io/hyperopt/
安装库
pip install hyperopt |
Hyperopt 优化过程主要分为4步:
Step 1 定义参数空间
Step 2 定义目标函数
Step 3 执行优化
Step 4 评估输出
Step 1 定义参数空间
我们使用dict()
来定义超参数空间,其中key可以任意设置,value则需用hyperopt的hp函数:
hyperopt.hp | 说明 |
---|---|
hp.choice(label, options) | 用于分类参数,返回options 中的元素 |
hp.pchoice(label, p_list) | 返回 (probability, option) 元素对 |
hp.randint(label, low, high) | 返回区间 [low, upper) 内的随机整数 |
hp.uniform(label, low, high) | 均匀返回 low, high 之间的浮点数 |
hp.quniform(label, low, high, q) | 均匀返回 low, high 之间的浮点数,适用于离散值 |
hp.uniformint(label, low, high) | 均匀返回 low, high 之间均的整数,适用于离散值 |
hp.loguniform(label, low, high) | 对数均匀返回 elow,ehigh 之间浮点数 |
hp.qloguniform(label, low, high, q) | 对数均匀返回 elow, ehigh 之间浮点数,适用于离散值 |
hp.normal(label, mu, sigma) | 正态分布返回实数 |
hp.qnormal(label, mu, sigma, q) | 正态分布返回实数,适用于离散值 |
hp.lognormal(label, mu, sigma) | 对数正态分布返回实数 |
hp.qlognormal(label, mu, sigma, q) | 正态分布返回实数,适用于离散值 |
每个hp函数都有一个label作为第一个参数,这些label用于在优化过程中将参数传递给调用方。
# define a search space |
Step 2 定义目标函数
Hyperopt 目前只支持目标函数的最小化
# define an objective function |
Step 3 执行优化
hyperopt 使用 fmin 函数进行优化。
fmin接收两种搜索算法:
- tpe.suggest 指代TPE (Tree Parzen Estimators) 方法
- rand.suggest 指代随机网格搜索方法
# minimize the objective over the space |
Output:
100%|██████| 1000/1000 [02:35<00:00, 6.44trial/s, best loss: 8.932729710763638] |
其中 Trials 对象用于保存所有的超参数、损失和其他信息。
Step 4 评估输出
print(space_eval(space, best)) |
Output:
{'learning_rate': 0.2, 'max_depth': 5, 'max_features': 'sqrt', 'n_estimators': 54, 'subsample': 0.9} |
分布式优化
超参数调优通常涉及训练数百或数千个模型,Hyperopt 允许分布式调优。通过 trials 参数将 SparkTrials 传递给 fmin 函数,在Spark集群上并行运行这些任务。
# We can run Hyperopt locally (only on the driver machine) |
SparkTrials可以通过3个参数进行配置,所有这些参数都是可选的:
- parallelism 最大并行数,默认为 SparkContext.defaultParallelism。
- timeout 允许的最大时间(以秒为单位),默认为None。
- spark_session 如果没有给出,SparkTrials将寻找现有的SparkSession。
除了单机训练算法(例如 scikit-learn 中的算法)以外,还可以将 Hyperopt 与分布式训练算法配合使用。将 Hyperopt 与分布式训练算法配合使用时,请不要将 trials
参数传递给 fmin()
,尤其是不要使用 SparkTrials
类。 SparkTrials
旨在为本身不是分布式算法的算法分配试运行。 对于分布式训练算法,请使用在群集驱动程序上运行的默认 Trials
类。 Hyperopt 评估驱动程序节点上的每个试运行,使 ML 算法本身可以启动分布式训练。
Scikit-optimize
Scikit-optimize 建立在 Scipy、Numpy 和 Scikit-Learn之上。非常易于使用,它提供了用于贝叶斯优化的通用工具包,可用于超参数调优。
官方文档:https://scikit-optimize.github.io/stable/
安装库
pip install scikit-optimize |
Scikit-optimize 优化过程主要分为4步:
Step 1 定义参数空间
Step 2 定义目标函数
Step 3 执行优化
Step 4 评估输出
Step 1 定义参数空间
使用 Scikit-optimize 提供的方法定义参数空间:
skopt.space | comment |
---|---|
space.Real(low, high, prior, name) | 用于浮点数参数 |
space.Integer(low, high, prior, name) | 用于整数参数 |
space.Categorical(categories, prior, name) | 用于分类参数 |
通过可选的prior参数可以对整型或浮点型取对数操作,或给类别型先验概率
# define a search space |
Step 2 定义目标函数
Scikit-optimize 支持目标函数最小化。
from sklearn.ensemble import GradientBoostingRegressor |
一般使用交叉验证来避免过拟合。used_named_args装饰器允许目标函数将参数作为关键字参数接收。
Step 3 执行优化
有四种优化算法可供选择:
skopt.optimizer | 说明 |
---|---|
dummy_minimize | 随机搜索 |
forest_minimize | 使用决策树的贝叶斯优化 |
gbrt_minimize | 使用GBRT的贝叶斯优化 |
gp_minimize | 使用高斯过程的贝叶斯优化 |
from skopt import gp_minimize |
Step 4 评估输出
打印最佳得分和最佳参数
# summarizing finding: |
打印优化过程中的目标函数值
print(result.func_vals) |
绘制收敛轨迹
# plot convergence traces |
Scikit-Learn API
Scikit-optimize 提供了一个类似于 GridSearchCV 和 RandomizedSearchCV 的接口 BayesSearchCV,实现了 fit 和 score 方法,以及 predict, predict_proba, decision_function, transform and inverse_transform 等常用方法。
from skopt.searchcv import BayesSearchCV |
Optuna
Optuna是目前为止最成熟、拓展性最强的超参数优化框架,它是专门为机器学习和深度学习所设计。为了满足机器学习开发者的需求,Optuna拥有强大且固定的API,因此Optuna代码简单,编写高度模块化,
Optuna可以无缝衔接到PyTorch、Tensorflow等深度学习框架上,也可以与sklearn的优化库scikit-optimize结合使用,因此Optuna可以被用于各种各样的优化场景。
官方文档:https://optuna.org/
安装库
pip install optuna |
Optuna 优化过程主要分为3步:
Step 1 构建目标函数及参数空间
Step 2 执行优化
Step 3 评估输出
Step 1 构建目标函数及参数空间
Optuna 基于 Trial 和 Study 两个组件实现优化(optimization)。在优化过程中,Optuna 反复调用目标函数,在不同的参数下对其进行求值。一个 Trial 对应着目标函数的单次执行。在每次调用目标函数的时候,它都被内部实例化一次。而 suggest API (例如 suggest_uniform()) 在目标函数内部调用,被用于获取单个 trial 的参数。
Optuna 允许在目标函数中定义参数空间和目标,优化器会通过trail所携带的方法来构造参数空间。
optuna.trial.Trial | 说明 |
---|---|
trial.suggest_categorical(name, choices) | 适用于分类参数 |
trial.suggest_int(name, low, high, step=1, log=False) | 适用于整数参数 |
trial.suggest_float(name, low, high, *, step=None, log=False) | 适用于浮点参数 |
trial.suggest_uniform(name, low, high) | 均匀分布 |
trial.suggest_loguniform(name, low, high) | 对数均匀分布 |
trial.suggest_discrete_uniform(name, low, high, q) | 离散均匀分布 |
通过可选的 step 与 log 参数,我们可以对整形或者浮点型参数进行离散化或者取对数操作。
from sklearn.ensemble import GradientBoostingRegressor |
通常使用交叉验证来避免过拟合。
Step 2 执行优化
下面是几个常用术语:
- Trial: 目标函数的单次调用
- Study: 一次优化过程,包含一系列的 trials.
- Parameter: 待优化的参数
在 Optuna 中,我们用 study 对象来管理优化过程。 create_study() 方法会返回一个 study 对象 ,可以填写minimize或maximize确定优化方向,然后通过 .optimize 方法执行优化过程。
可以通过模块optuna.sampler来定义优化算法:
- GridSampler:网格搜索
- RandomSampler:随机抽样
- TPESampler:使用TPE (Tree-structured Parzen Estimator) 算法
- CmaEsSampler:使用 CMA-ES算法
from optuna.samplers import TPESampler |
获得 trial 的数目:
len(study.trials) |
Out: 100
再次执行 optimize(),可以继续优化过程
study.optimize(objective, n_trials=100) |
获得更新后的的 trial 数量:
len(study.trials) |
Out: 200
Step 3 评估输出
打印最佳最佳分数和超参数值
print(f'Best score: {study.best_value}') |
Optuna 中提供了不同的方法来可视化优化结果:
函数 | 说明 |
---|---|
plot_contour(study) | 将参数关系绘制成等值线 |
plot_intermidiate_values(study) | 绘制所有trial的学习曲线 |
plot_optimization_history(study) | 绘制所有trial的优化历史记录 |
plot_param_importances(study) | 绘制超参数重要性及其值 |
plot_edf(study) | 绘制study目标值的edf |
optuna.visualization.plot_optimization_history(study) |
参数空间进阶
在 Optuna 中,我们使用和 Python 语法类似的方式来定义搜索空间,其中包含条件和循环语句。
分支:
import sklearn.ensemble |
循环:
import torch |
多目标优化
from sklearn.metrics import make_scorer, root_mean_squared_error |
注意:多目标优化使用的参数 directions 和单目标参数direction不同。
分布式优化
可使用 Joblib Apache Spark 后端将 Optuna 试验分发到 Azure Databricks 群集中的多台计算机。
import joblib |
常见问题
官方链接:https://optuna.readthedocs.io/en/stable/faq.html
如何定义带有额外参数的目标函数?
有两种方法可以实现这类函数。
首先,如下例所示,可调用的 objective 类具有这个功能:
import optuna |
其次,你可以用 lambda
或者 functools.partial
来创建带有额外参数的函数(闭包)。 下面是一个使用了 lambda
的例子:
import optuna |
其他例子参见 sklearn_addtitional_args.py .
如何在目标函数中保存训练好的机器学习模型?
Optuna 会保存超参数和对应的目标函数值,但是它不会存储诸如机器学习模型或者网络权重这样的中间数据。要保存模型或者权重的话,请利用你正在使用的机器学习库提供的对应功能。
在保存模型的时候,我们推荐将 optuna.trial.Trial.number
一同存储。这样易于之后确认对应的 trial.比如,你可以用以下方式在目标函数中保存训练好的 SVM 模型:
def objective(trial): |
在优化study时,如何避免内存不足(OOM)?
如果你运行更多trials时,导致内存增加,尝试定期运行垃圾收集器。 在回调中调用optimize()
或调用``gc.collect()时,设置
gc_after_trial=True`。
def objective(trial): |
如何保存和恢复 study?
有两种方法可以将 study 持久化。具体采用哪种取决于你是使用内存存储 (in-memory) 还是远程数据库存储 (RDB). 通过 pickle
或者 joblib
, 采用了内存存储的 study 可以和普通的 Python 对象一样被存储和加载。比如用 joblib
的话:
study = optuna.create_study() |
恢复 study:
study = joblib.load("study.pkl") |
Ray[tune]
Ray Tune 是一个标准的超参数调优工具,集成了多种参数搜索算法,并且支持分布式计算,使用方式简单。同时支持pytorch、tensorflow等训练框架,和tensorboard可视化。
官方文档:Welcome to Ray!
首先需要安装 Ray 的 Tune 模块,可以使用以下命令:
pip install "ray[tune]" |
Ray Tune 优化过程主要分为4步:
Step 1 定义参数空间
Step 2 定义目标函数
Step 3 执行优化
Step 4 评估输出
Step 1 定义参数空间
我们使用字典来定义超参数空间
ray.tune | 说明 |
---|---|
tune.choice(categories) | 分类数据采样 |
tune.randint(lower, upper) | 在区间 lower, upper 均匀采样整数 |
tune.qrandint(lower, upper, q) | 在区间 lower, upper 离散均匀采样整数 |
tune.lograndint(lower, upper, base=10) | 在区间 10lower,10upper 对数均匀采样整数 |
tune.qlograndint(lower, upper, q, base=10) | 在区间 10lower,10upper 对数离散均匀采样整数 |
tune.uniform(lower, upper) | 在区间 lower, upper 均匀采样浮点数 |
tune.quniform(lower, upper, q) | 在区间 lower, upper离散均匀采样浮点数 |
tune.loguniform(lower, upper, base=10) | 在区间 10lower,10upper 对数均匀采样浮点数 |
tune.qloguniform(lower, upper, q, base=10) | 在区间 10lower,10upper 对数离散均匀采样浮点数 |
tune.randn(mean, std) | 正态分布采样浮点数 |
tune.qrandn(mean, std, q) | 正态分布离散采样浮点数 |
tune.grid_search(values) | 指定网格搜索 |
tune.sample_from(func) | 配置采样函数 |
# define a search space |
Step 2 定义目标函数
该函数模型训练函数(trainable)接受超参数字典,在整个训练过程结束后 return {"score": score}
# define an objective function |
或在训练过程中的每个epoch后进行报告 train.report({"score": score})
,便于在训练过程中监控指标的变化趋势。
def objective(x, a, b): # Define an objective function. |
Step 3 执行优化
使用 Tuner
创建用于调参的优化器(turner),其输入参数包括:
-
trainable
:模型训练函数 -
parame_space
:超参数搜索空间 -
tune_config
:以tune.TuneConfig
实例作为输入,配置优化算法、度量指标等。 -
run_config
:以tune.RunConfig
实例作为输入,配置训练终止条件,check point,运行结果存储路径等
然后调用方法 .fit
启动优化。默认情况下,Tune 会自动使用全部资源并行运行。
from ray.tune.search.hyperopt import HyperOptSearch |
tune 具有与许多流行的优化库集成的搜索算法,如果未指定搜索算法,将默认使用随机搜索。
SearchAlgorithm | Summary | Website | Code Example |
---|---|---|---|
Random search/grid search | Random search/grid search | tune_basic_example | |
AxSearch | Bayesian/Bandit Optimization | [Ax] | AX Example |
HyperOptSearch | Tree-Parzen Estimators | [HyperOpt] | Running Tune experiments with HyperOpt |
BayesOptSearch | Bayesian Optimization | [BayesianOptimization] | BayesOpt Example |
TuneBOHB | Bayesian Opt/HyperBand | [BOHB] | BOHB Example |
NevergradSearch | Gradient-free Optimization | [Nevergrad] | Nevergrad Example |
OptunaSearch | Optuna search algorithms | [Optuna] | Running Tune experiments with Optuna |
Step 4 评估输出
Tuner.fit()
返回一个ResultGrid
对象
best_result = results.get_best_result() # Get best result object |
此对象还可以转化为 DataFrame, 进行临时数据分析。
# Get a dataframe with the last results for each trial |