目录
贝叶斯优化是当今黑盒函数估计领域最为先进和经典的方法,在同一套序贯模型下使用不同的代理模型以及采集函数、还可以发展出更多更先进的贝叶斯优化改进版算法,因此,贝叶斯优化的其算法本身就多如繁星,实现各种不同种类的贝叶斯优化的库也是琳琅满目,几乎任意一个专业用于超参数优化的工具库都会包含贝叶斯优化的内容。我们可以在以下页面找到大量可以实现贝叶斯优化方法的HPO库:AutoML | Overview of HPO Tools ,其中大部分库都是由独立团队开发和护,因此不同的库之间之间的优劣、性格、功能都有很大的差异。下面将介绍如下三个可以实现贝叶斯优化的库:bayesian-optimization
,hyperopt
,optuna
。
HPO库 | 优劣评价 | 推荐指数 |
---|---|---|
bayes_opt | ✅实现基于高斯过程的贝叶斯优化 ✅当参数空间由大量连续型参数构成时 ⛔包含大量离散型参数时避免使用 ⛔算力/时间稀缺时避免使用 | ⭐⭐ |
hyperopt | ✅实现基于TPE的贝叶斯优化 ✅支持各类提效工具 ✅进度条清晰,展示美观,较少怪异警告或报错 ✅可推广/拓展至深度学习领域 ⛔不支持基于高斯过程的贝叶斯优化 ⛔代码限制多、较为复杂,灵活性较差 | ⭐⭐⭐⭐ |
optuna | ✅(可能需结合其他库)实现基于各类算法的贝叶斯优化 ✅代码最简洁,同时具备一定的灵活性 ✅可推广/拓展至深度学习领域 ⛔非关键性功能维护不佳,有怪异警告与报错 | ⭐⭐⭐⭐ |
注意,以上三个库都不支持基于Python环境的并行或加速,大多数优化算法库只能够支持基于数据库(如MangoDB,mySQL)的并行或加速,但以上库都可以被部署在分布式计算平台。
三个库极其辅助包的安装方法分别如下,使用pip或conda安装时注意关闭梯子。
- Bayes_opt
#!pip install bayesian-optimization
#!conda install -c conda-forge bayesian-optimization
- Hyperopt
#!pip install hyperopt
- Optuna
#!pip install optuna
#!conda install -c conda-forge optuna
- Skopt(作为Optuna辅助包安装,也可单独使用)
#!pip install scikit-optimize
导入库,确认使用数据:
import numpy as np
import pandas as pd
import time
import os #修改环境设置
#算法/损失/评估指标等
import sklearn
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.model_selection import KFold, cross_validate
#优化器
from bayes_opt import BayesianOptimization
import hyperopt
from hyperopt import hp, fmin, tpe, Trials, partial
from hyperopt.early_stop import no_progress_loss
import optuna
data=pd.read_csv("F:\\Jupyter Files\\机器学习进阶\\datasets\\House Price\\train_encode.csv")
data.drop('Unnamed: 0', axis=1, inplace=True)
X=data.iloc[:,:-1]
y=data.iloc[:,-1]
X.shape #(1460, 80)
1. 基于Bayes_opt实现GP优化
bayes-optimization是最早开源的贝叶斯优化库之一,也是为数不多至今依然保留着高斯过程优化的优化库。由于开源较早、代码简单,bayes-opt常常出现在论文、竞赛kernels或网络学习材料当中,因此理解Bayes_opt的代码是极其重要的课题。不过,bayes-opt对参数空间的处理方式较为原始,也缺乏相应的提效/监控功能,对算力的要求较高,因此它往往不是我们进行优化时的第一首选库。通常来说,当且仅当我们必须要实现基于高斯过程的贝叶斯优化,且算法的参数空间中带有大量连续型参数时,我们才会优先考虑Bayes_opt库。我们可以在github上找到bayes-optmization的官方文档(https://github.com/fmfn/BayesianOptimization) ,想要进一步了解其基本功能与原理的小伙伴可以进行阅读。
1.1 定义目标函数
目标函数的值即𝑓(𝑥)的值。贝叶斯优化会计算𝑓(𝑥)在不同𝑥上的观测值,因此𝑓(𝑥)的计算方式需要被明确。在HPO过程中,我们希望能够筛选出令模型泛化能力最大的参数组合,因此𝑓(𝑥)应该是损失函数的交叉验证值或者某种评估指标的交叉验证值。需要注意的是,bayes_opt库存在三个影响目标函数定义的规则:
① 目标函数的输入必须是具体的超参数,而不能是整个超参数空间,更不能是数据、算法等超参数以外的元素,因此在定义目标函数时,我们需要让超参数作为目标函数的输入。
② 超参数的输入值只能是浮点数,不支持整数与字符串。因此当算法的实际参数需要输入字符串时,该参数不能使用bayes_opt进行调整,当算法的实际参数需要输入整数时,则需要在目标函数中规定参数的类型。
③ bayes_opt只支持寻找𝑓(𝑥)的最大值,不支持寻找最小值。因此当我们定义的目标函数是某种损失时,目标函数的输出需要取负(即,如果使用RMSE,则应该让目标函数输出负RMSE,这样最大化负RMSE后,才是最小化真正的RMSE。)当我们定义的目标函数是准确率,或者auc等指标,则可以让目标函数的输出保持原样。
# 定义目标函数
def bayesopt_objective(n_estimators,max_depth,max_features,min_impurity_decrease):
#定义评估器
#需要调整的超参数等于目标函数的输入,不需要调整的超参数则直接等于固定值
#默认参数输入一定是浮点数,因此需要套上int函数处理成整数
reg = RFR(n_estimators = int(n_estimators)
,max_depth = int(max_depth)
,max_features = int(max_features)
,min_impurity_decrease = min_impurity_decrease
,random_state=412
,verbose=False) #可自行决定是否开启森林建树的verbose
#定义损失的输出,5折交叉验证下的结果,输出负根均方误差(-RMSE)
#注意,交叉验证需要使用数据,但我们不能让数据X,y成为目标函数的输入
cv = KFold(n_splits=5,shuffle=True,random_state=412)
validation_loss = cross_validate(reg,X,y
,scoring="neg_root_mean_squared_error"
,cv=cv
,verbose=False
,error_score='raise'
#如果交叉验证中的算法执行报错,则告诉我们错误的理由
)
#交叉验证输出的评估指标是负根均方误差,因此本来就是负的损失
#目标函数可直接输出该损失的均值
return np.mean(validation_loss["test_score"])
1.2 定义参数空间
在任意超参数优化器中,优化器会将参数空格中的超参数组合作为备选组合,一组一组输入到算法中进行训练。在贝叶斯优化中,超参数组合会被输入我们定义好的目标函数𝑓(𝑥)中。
在bayes_opt中,我们使用字典方式来定义参数空间,其中参数的名称为键,参数的取值范围为值。且任意参数的取值范围为双向闭区间,以下方的空间为例,在n_estimators的取值中,80与100都可以被取到。
param_grid_simple = {'n_estimators': (80,100)
, 'max_depth':(10,25)
, "max_features": (10,20)
, "min_impurity_decrease":(0,1)
}
需要注意的是,bayes_opt只支持填写参数空间的上界与下界,不支持填写步长等参数,且bayes_opt会将所有参数都当作连续型超参进行处理,因此bayes_opt会直接取出闭区间中任意浮点数作为备选参数。例如,取92.28作为n_estimators的值。这也是在目标函数中,我们需要对整数型超参的取值都套上int函数的原因。假设优化器取出92.28作为n_estimators的值,实际传入随机森林算法的会是int(92.28) = 92,如此我们可以保证算法运行过程中不会因参数类型不符而报错。也因为bayes_opt的这个性质,输入bayes_opt的参数空间天生会比其他贝叶斯优化库更大/更密,因此需要的迭代次数也更多。
1.3 定义优化目标函数的具体流程
在有了目标函数与参数空间之后,就可以按bayes_opt的规则进行优化了。在任意贝叶斯优化算法的实践过程中,一定都有涉及到随机性的过程——例如,随机抽取点作为观测点,随机抽样部分观测点进行采集函数的计算等等。在大部分优化库当中,这种随机性是无法控制的,即便允许我们填写随机数种子,优化算法也不能固定下来。因此我们可以尝试填写随机数种子,但需要记住优化算法每次运行时一定都会不一样。虽然优化算法无法被复现,但是优化算法得出的最佳超参数的结果却是可以被复现的。只要优化完毕之后,可以从优化算法的实例化对象中取出最佳参数组合以及最佳分数,该最佳参数组合被输入到交叉验证中后,是一定可以复现其最佳分数的。如果没能复现最佳分数,则是交叉验证过程的随机数种子设置存在问题,或者优化算法的迭代流程存在问题。
# 定义优化目标函数的具体流程
def param_bayes_opt(init_points,n_iter):
#定义优化器,先实例化优化器
opt = BayesianOptimization(bayesopt_objective #需要优化的目标函数
,param_grid_simple #备选参数空间
,random_state=412 #随机数种子,虽然无法控制住
)
#使用优化器,记住bayes_opt只支持最大化
opt.maximize(init_points = init_points #抽取多少个初始观测值
, n_iter=n_iter #一共观测/迭代多少次
)
#优化完成,取出最佳参数与最佳分数
params_best = opt.max["params"]
score_best = opt.max["target"]
#打印最佳参数与最佳分数
print("\n","\n","best params: ", params_best,
"\n","\n","best cvscore: ", score_best)
#返回最佳参数与最佳分数
return params_best, score_best
4. 定义验证函数(非必须)
优化后的结果是可以复现的,即我们可以对优化算法给出的最优参数进行再验证,其中验证函数与目标函数高度相似,输入参数或超参数空间、输出最终的损失函数结果。在使用sklearn中自带的优化算法时,由于优化算法自己会执行分割数据、交叉验证的步骤,因此优化算法得出的最优分数往往与我们自身验证的分数不同(因为交叉验证时的数据分割不同)。然而在贝叶斯优化过程中,目标函数中的交叉验证即数据分割都是我们自己规定的,因此原则上来说,只要在目标函数中设置了随机数种子,贝叶斯优化给出的最佳分数一定与我们验证后的分数相同,所以当你对优化过程的代码比较熟悉时,可以不用进行二次验证。
# 定义验证函数(非必须)
def bayes_opt_validation(params_best):
reg = RFR(n_estimators = int(params_best["n_estimators"])
,max_depth = int(params_best["max_depth"])
,max_features = int(params_best["max_features"])
,min_impurity_decrease = params_best["min_impurity_decrease"]
,random_state=412
,verbose=False)
cv = KFold(n_splits=5,shuffle=True,random_state=412)
validation_loss = cross_validate(reg,X,y
,scoring="neg_root_mean_squared_error"
,cv=cv
,verbose=False
)
return np.mean(validation_loss["test_score"])
5. 执行实际优化流程
start = time.time()
params_best, score_best = param_bayes_opt(20,280) #初始看20个观测值,后面迭代280次
print('It takes %s minutes' % ((time.time() - start)/60))
validation_score = bayes_opt_validation(params_best)
print("\n","\n","validation_score: ",validation_score)
best params: {'max_depth': 17.46260664763851, 'max_features': 15.346340025451044, 'min_impurity_decrease': 0.6860574610899137, 'n_estimators': 98.7940452937946} best cvscore: -27728.966347439833 It takes 8.198244913419087 minutes validation_score: -27728.966347439833
HPO方法 | bayes_opt 贝叶斯优化(基于GP) |
---|---|
搜索空间/全域空间 | 300/无限 |
运行时间(分钟) | 8.20 |
搜索最优(RMSE) | 27728.966 |
重建最优(RMSE) | 27728.966 |
- 原理上有优越性
可以看到,基于高斯过程的贝叶斯优化在8.20分钟内锁定了最佳分数27728.966,这比之前使用随机搜索时获得的最佳分数还要高。贝叶斯优化作为从原理上高于网格优化的HPO方法,能够以更短的时间获得与随机网格搜索相同的结果,可见其原理上的优越性。
- 优化过程无法复现,但优化结果可以复现
由于贝叶斯优化每次都是随机的,因此我们并不能在多次运行代码时复现出27728.966这个结果,事实上如果我们重复运行,也只有很小的概率可以再次找到这个最低值(这一点对于随机搜索来说也是类似的,如果不规定随机数种子,我们也无法复现最低值)。当修改随机数种子时也可能能找到更低的值。因此我们在执行贝叶斯优化时,往往会多运行几次观察模型或者修改随机数种子找出的结果。同时,验证分数与目标函数最后输出的分数一模一样,可见最终输出的超参数组合的效力是可以复现的。
- 效率不足
不难发现,bayes_opt的速度虽然快,效率却不高。实际上在迭代到170次时,贝叶斯优化就已经找到了最小损失,但由于没有提前停止机制,模型还持续地迭代了130次才停下,如果bayes_opt支持提前停止机制,贝叶斯优化所需的实际迭代时间可能会更少。同时,由于Bayes_opt只能够在参数空间提取浮点数,bayes_opt在随机森林上的搜索效率是较低的,即便在10次不同的迭代中分别取到了[88.89, 88.23, 88.16, 88.59……]等值,在取整之后也只能够获得一个备选值88,但bayes_opt无法辨别这种区别,因此可能取出了众多无效的观测点。如果使用其他贝叶斯优化器,贝叶斯优化的效率将会更高。
- 支持灵活修改
虽然在我们的代码中没有体现,但bayes_opt是支持灵活修改采集函数与高斯过程中的种种参数的,具体可以参考这里:https://github.com/fmfn/BayesianOptimization/blob/master/examples/advanced-tour.ipynb
2. 基于HyperOpt实现TPE优化
Hyperopt优化器是目前最为通用的贝叶斯优化器之一,Hyperopt中集成了包括随机搜索、模拟退火和TPE(Tree-structured Parzen Estimator Approach)等多种优化算法。相比于Bayes_opt,Hyperopt的是更先进、更现代、维护更好的优化器,也是我们最常用来实现TPE方法的优化器。在实际使用中,相比基于高斯过程的贝叶斯优化,基于高斯混合模型的TPE在大多数情况下以更高效率获得更优结果,该方法目前也被广泛应用于AutoML领域中。TPE算法原理可以参阅原论文Multiobjective tree-structured parzen estimator for computationally expensive optimization problems,下面将重点介绍关于Hyperopt中使用TPE进行超参数搜索的过程。
在基于HyperOpt实现TPE(Tree-structured Parzen Estimator)优化时,TPE使用了高斯混合模型(Mixture of Gaussian Models)来建模目标函数的概率分布。这是因为TPE算法的核心思想是通过建立一个先验模型和一个后验模型来进行优化。
TPE算法将参数空间划分为两个部分:前半部分用于建立先验模型,后半部分用于建立后验模型。先验模型用于估计目标函数概率密度较高的区域,而后验模型则用于估计目标函数概率密度较低的区域。
在先验模型的建立过程中,TPE使用了高斯混合模型来对目标函数的概率分布进行建模。高斯混合模型假设目标函数的概率分布是由多个高斯分布组成的混合物,每个高斯分布都有自己的均值、协方差矩阵和权重。通过对这些参数进行优化,可以得到一个能够较好地拟合目标函数概率分布的混合高斯模型。
在后验模型的建立过程中,TPE使用了条件概率密度函数来估计目标函数在不同参数取值下的概率密度。这里的条件概率密度函数也可以使用高斯混合模型来建模,但与先验模型不同的是,后验模型的参数是通过优化过程动态更新得到的。
综上所述,TPE算法在基于HyperOpt实现时使用了高斯混合模型来建模目标函数的概率分布,以便进行参数优化。这样可以更好地适应不同的目标函数分布,并在优化过程中动态调整概率模型来提高搜索效果。
2.1 定义目标函数
在定义目标函数𝑓(𝑥)时,需要严格遵守需要使用的当下优化库的基本规则。与Bayes_opt一样,Hyperopt也有一些特定的规则会限制我们的定义方式,主要包括:
① 目标函数的输入必须是符合hyperopt规定的字典,不能是类似于sklearn的参数空间字典、不能是参数本身,更不能是数据、算法等超参数以外的元素。因此在自定义目标函数时,我们需要让超参数空间字典作为目标函数的输入。
② Hyperopt只支持寻找𝑓(𝑥)的最小值,不支持寻找最大值,因此当定义的目标函数是某种正面的评估指标时(如准确率,auc),需要对该评估指标取负。如果定义的目标函数是负损失,也需要对负损失取绝对值。当且仅当定义的目标函数是普通损失时,我们才不需要改变输出。
def hyperopt_objective(params):
#定义评估器
#需要搜索的参数需要从输入的字典中索引出来
#不需要搜索的参数,可以是设置好的某个值
#在需要整数的参数前调整参数类型
reg = RFR(n_estimators = int(params["n_estimators"])
,max_depth = int(params["max_depth"])
,max_features = int(params["max_features"])
,min_impurity_decrease = params["min_impurity_decrease"]
,random_state=412
,verbose=False)
#交叉验证结果,输出负根均方误差(-RMSE)
cv = KFold(n_splits=5,shuffle=True,random_state=412)
validation_loss = cross_validate(reg,X,y
,scoring="neg_root_mean_squared_error"
,cv=cv
,verbose=False
,error_score='raise'
)
#最终输出结果,由于只能取最小值,所以必须对(-RMSE)求绝对值
#以求解最小RMSE所对应的参数组合
return np.mean(abs(validation_loss["test_score"]))
2.2 定义参数空间
在任意超参数优化器中,优化器会将参数空格中的超参数组合作为备选组合,一组一组输入到算法中进行训练。在贝叶斯优化中,超参数组合会被输入我们定义好的目标函数𝑓(𝑥)中。在hyperopt中,我们使用特殊的字典形式来定义参数空间,其中键值对上的键可以任意设置,只要与目标函数中索引参数的键一致即可,键值对的值则是hyperopt独有的hp函数,包括了:
hp.quniform("参数名称", 下界, 上界, 步长) - 适用于均匀分布的浮点数
hp.uniform("参数名称",下界, 上界) - 适用于随机分布的浮点数
hp.randint("参数名称",上界) - 适用于[0,上界)的整数,区间为前闭后开
hp.choice("参数名称",["字符串1","字符串2",...]) - 适用于字符串类型,最优参数由索引表示
hp.choice("参数名称",[*range(下界,上界,步长)]) - 适用于整数型,最优参数由索引表示
hp.choice("参数名称",[整数1,整数2,整数3,...]) - 适用于整数型,最优参数由索引表示
hp.choice("参数名称",["字符串1",整数1,...]) - 适用于字符与整数混合,最优参数由索引表示
在hyperopt的说明当中,并未明确参数取值范围空间的开闭,根据实验,如无特殊说明,hp中的参数空间定义方法应当都为前闭后开区间。
param_grid_simple = {'n_estimators': hp.quniform("n_estimators",80,100,1)
, 'max_depth': hp.quniform("max_depth",10,25,1)
, "max_features": hp.quniform("max_features",10,20,1)
, "min_impurity_decrease":hp.quniform("min_impurity_decrease",0,5,1)
}
由于hp.choice最终会返回最优参数的索引,容易与数值型参数的具体值混淆,而hp.randint又只能够支持从0开始进行计数,因此我们常常会使用quniform获得均匀分布的浮点数来替代整数。对于需要取整数的参数值,如果采用quniform方式构筑参数空间,则需要在目标函数中使用int函数限定输入类型。例如,在范围[0,5]中取值时,可以取出[0.0, 1.0, 2.0, 3.0,...]这种均匀浮点数,在输入目标函数时,则必须确保参数值前存在int函数。当然,如果使用hp.choice则不会存在该问题。
由于不涉及到连续型变量,因此我们可以计算出当前参数空间的大小:
len([*range(80,100,1)])*len([*range(10,25,1)])*len([*range(10,20,1)])*len([range(0,5,1)])
3000
2.3 定义优化目标函数的具体流程
在有了目标函数和参数空间,接下来就可以进行优化了。在Hyperopt中,用于优化的基础功能叫做fmin,在fmin中,可以自定义使用的代理模型(参数algo
),一般来说有tpe.suggest
以及rand.suggest
两种选项,前者指代TPE方法,后者指代随机网格搜索方法。另外还可以通过partial功能来修改算法涉及到的具体参数,包括模型具体使用了多少个初始观测值(参数n_start_jobs
),以及在计算采集函数值时究竟考虑多少个样本(参数n_EI_candidates
)。当然,我们也可以不填写这些参数,就使用默认的参数值。
除此之外,Hyperopt当中还有两个值得注意的功能,一个记录整个迭代过程的trials
,另一个是提前停止参数early_stop_fn
。其中,trials
直译为“实验”或“测试”,表示我们不断尝试的每一种参数组合,这个参数中我们一般输入从hyperopt库中导入的方法Trials(),当优化完成之后,我们可以从保存好的trials中查看损失、参数等各种中间信息;而提前停止参数early_stop_fn
中我们一般输入从hyperopt库导入的方法no_progress_loss(),这个方法中可以输入具体的数字n,表示当损失连续n次没有下降时,让算法提前停止。由于贝叶斯方法的随机性较高,当样本量不足时需要多次迭代才能够找到最优解,因此一般no_progress_loss()中的数值不会设置得太高。在下面的过程实现中,由于数据量较少,所以设置了一个较高的值来避免迭代停止太早。
def param_hyperopt(max_evals=100):
#保存迭代过程
trials = Trials()
#设置提前停止
early_stop_fn = no_progress_loss(100)
#定义代理模型
#algo = partial(tpe.suggest, n_startup_jobs=20, n_EI_candidates=50)
params_best = fmin(hyperopt_objective #目标函数
, space = param_grid_simple #参数空间
, algo = tpe.suggest #代理模型你要哪个呢?
#, algo = algo
, max_evals = max_evals #允许的迭代次数
, verbose=True
, trials = trials
, early_stop_fn = early_stop_fn
)
#打印最优参数,fmin会自动打印最佳分数
print("\n","\n","best params: ", params_best,
"\n")
return params_best, trials
2.4 定义验证函数(非必要)
def hyperopt_validation(params):
reg = RFR(n_estimators = int(params["n_estimators"])
,max_depth = int(params["max_depth"])
,max_features = int(params["max_features"])
,min_impurity_decrease = params["min_impurity_decrease"]
,random_state=412
,verbose=False
)
cv = KFold(n_splits=5,shuffle=True,random_state=412)
validation_loss = cross_validate(reg,X,y
,scoring="neg_root_mean_squared_error"
,cv=cv
,verbose=False
)
return np.mean(abs(validation_loss["test_score"]))
2.5 执行实际优化流程
params_best, trials = param_hyperopt(30) #1%的空间大小
100%|█████████████████████████████████████████████████| 30/30 [00:39<00:00, 1.33s/trial, best loss: 27768.55280372222] best params: {'max_depth': 17.0, 'max_features': 15.0, 'min_impurity_decrease': 3.0, 'n_estimators': 94.0}
params_best, trials = param_hyperopt(100) #3%的空间大小
100%|██████████████████████████████████████████████| 100/100 [02:17<00:00, 1.38s/trial, best loss: 27762.432832056867] best params: {'max_depth': 17.0, 'max_features': 15.0, 'min_impurity_decrease': 5.0, 'n_estimators': 96.0}
params_best, trials = param_hyperopt(300) #10%的空间大小
100%|██████████████████████████████████████████████| 300/300 [06:50<00:00, 1.37s/trial, best loss: 27696.731890271396] best params: {'max_depth': 17.0, 'max_features': 15.0, 'min_impurity_decrease': 0.0, 'n_estimators': 89.0}
hyperopt_validation(params_best)
27696.731890271396
#打印所有搜索相关的记录
trials.trials
#打印全部搜索的目标函数值
trials.losses()
HPO方法 | bayes_opt 贝叶斯优化(基于GP) | hyperopt 贝叶斯优化(基于TPE) |
---|---|---|
搜索空间/全域空间 | 300/无限 | 300/3000 |
运行时间(分钟) | 8.20 | 6.50 |
搜索最优(RMSE) | 27728.966 | 27696.732 |
重建最优(RMSE) | 27728.966 | 27696.732 |
由于具有提前停止功能,因此基于TPE的hyperopt优化可能在我们设置的迭代次数被达到之前就停止,也因此hyperopt迭代到实际最优值所需的迭代次数可能更少。同时,TPE方法相比于高斯过程计算会更加迅速,因此在运行100次迭代的情况下,hyperopt只需要2分钟时间,而运行300次迭代的bayes_opt却需要8.20分钟,可见,即便运行同样的迭代次数,hyperopt也是更有优势的,这或许是因为hyperopt的参数空间更加稀疏、在整数型参数搜索上更高效。
不过HyperOpt的缺点也很明显,那就是代码精密度要求较高、灵活性较差,略微的改动就可能让代码疯狂报错难以跑通。同时,HyperOpt所支持的优化算法也不够多,如果我们专注地使用TPE方法,则掌握HyperOpt即可,如果我们希望拥有丰富的HPO手段,则可以更深入地接触Optuna库。
3. 基于Optuna实现多种贝叶斯优化
Optuna是目前为止最为成熟、拓展性最强的超参数优化框架,与古老的bayes_opt相比,Optuna明显是专门为机器学习和深度学习所设计。为了满足机器学习开发者的需求,Optuna拥有强大且固定的API,因此Optuna代码简单,编写高度模块化,是我们介绍的库中代码最为简练的库。Optuna的优势在于,它可以无缝衔接到PyTorch、Tensorflow等深度学习框架上,也可以与sklearn的优化库scikit-optimize结合使用,因此Optuna可以被用于各种各样的优化场景。在我们的课程中,我们将重点介绍Optuna实现贝叶斯优化的过程,其他优化方面内容可以参考以下页面:GitHub - optuna/optuna: A hyperparameter optimization framework 。
3.1 定义目标函数与参数空间
Optuna的目标函数相当特别。在其他优化库中,我们需要单独输入参数或参数空间,优化器会在具体优化过程中将参数空间一一放入我们的目标函数进行优化,但在Optuna中,我们并不需要将参数或参数空间输入目标函数,而是需要直接在目标函数中定义参数空间。特别的是,Optuna优化器会生成一个指代备选参数的变量trial,该变量无法被用户获取或打开,但该变量在优化器中生存,并被输入目标函数。在目标函数中,我们可以通过变量trail所携带的方法来构造参数空间,具体如下所示:
def optuna_objective(trial):
#定义参数空间
n_estimators = trial.suggest_int("n_estimators",80,100,1) #整数型,(参数名称,下界,上界,步长)
max_depth = trial.suggest_int("max_depth",10,25,1)
max_features = trial.suggest_int("max_features",10,20,1)
#max_features = trial.suggest_categorical("max_features",["log2","sqrt","auto"]) #字符型
min_impurity_decrease = trial.suggest_int("min_impurity_decrease",0,5,1)
#min_impurity_decrease = trial.suggest_float("min_impurity_decrease",0,5,log=False) #浮点型
#定义评估器
#需要优化的参数由上述参数空间决定
#不需要优化的参数则直接填写具体值
reg = RFR(n_estimators = n_estimators
,max_depth = max_depth
,max_features = max_features
,min_impurity_decrease = min_impurity_decrease
,random_state=412
,verbose=False
)
#交叉验证过程,输出负均方根误差(-RMSE)
#optuna同时支持最大化和最小化,因此如果输出-RMSE,则选择最大化
#如果选择输出RMSE,则选择最小化
cv = KFold(n_splits=5,shuffle=True,random_state=412)
validation_loss = cross_validate(reg,X,y
,scoring="neg_root_mean_squared_error"
,cv=cv #交叉验证模式
,verbose=False #是否打印进程
,error_score='raise'
)
#最终输出RMSE
return np.mean(abs(validation_loss["test_score"]))
3.2 定义优化目标函数的具体流程
在HyperOpt当中我们可以调整参数algo
来自定义用于执行贝叶斯优化的具体算法,在Optuna中同样也可以。大部分备选的算法都集中在Optuna的模块sampler中,包括TPE优化、随机网格搜索以及其他各类更加高级的贝叶斯过程,对于Optuna.sampler中调出的类,我们也可以直接输入参数来设置初始观测值的数量、以及每次计算采集函数时所考虑的观测值量。在Optuna库中并没有集成实现高斯过程的方法,但我们可以从scikit-optimize里面导入高斯过程来作为optuna中的algo
设置,而具体的高斯过程相关的参数则可以通过如下方法进行设置:
def optimizer_optuna(n_trials, algo):
#定义使用TPE或者GP
if algo == "TPE":
algo = optuna.samplers.TPESampler(n_startup_trials = 10, n_ei_candidates = 24)
elif algo == "GP":
from optuna.integration import SkoptSampler
import skopt
algo = SkoptSampler(skopt_kwargs={'base_estimator':'GP', #选择高斯过程
'n_initial_points':10, #初始观测点10个
'acq_func':'EI'} #选择的采集函数为EI,期望增量
)
#实际优化过程,首先实例化优化器
study = optuna.create_study(sampler = algo #要使用的具体算法
, direction="minimize" #优化的方向,可以填写minimize或maximize
)
#开始优化,n_trials为允许的最大迭代次数
#由于参数空间已经在目标函数中定义好,因此不需要输入参数空间
study.optimize(optuna_objective #目标函数
, n_trials=n_trials #最大迭代次数(包括最初的观测值的)
, show_progress_bar=True #要不要展示进度条呀?
)
#可直接从优化好的对象study中调用优化的结果
#打印最佳参数与最佳损失值
print("\n","\n","best params: ", study.best_trial.params,
"\n","\n","best score: ", study.best_trial.values,
"\n")
return study.best_trial.params, study.best_trial.values
3.3 执行实际优化流程
Optuna库虽然是当今最为成熟的HPO方法之一,但当参数空间较小时,Optuna库在迭代中容易出现抽样BUG,即Optuna会持续抽到曾经被抽到过的参数组合,并且持续报警告说"算法已在这个参数组合上检验过目标函数了"。在实际迭代过程中,一旦出现这个Bug,那当下的迭代就无用了,因为已经检验过的观测值不会对优化有任何的帮助,因此对损失的优化将会停止。如果出现该BUG,则可以增大参数空间的范围或密度。或者使用如下的代码令警告关闭:
import warnings
warnings.filterwarnings('ignore', message='The objective has been evaluated at this point before.')
best_params, best_score = optimizer_optuna(10,"GP") #默认打印迭代过程
[I 2023-07-22 19:22:43,156] Trial 0 finished with value: 28442.95077735297 and parameters: {'n_estimators': 98, 'max_depth': 18, 'max_features': 12, 'min_impurity_decrease': 4}. Best is trial 0 with value: 28442.95077735297. [I 2023-07-22 19:22:44,599] Trial 1 finished with value: 28895.249719365303 and parameters: {'n_estimators': 84, 'max_depth': 12, 'max_features': 17, 'min_impurity_decrease': 3}. Best is trial 0 with value: 28442.95077735297. [I 2023-07-22 19:22:45,995] Trial 2 finished with value: 28642.66523113222 and parameters: {'n_estimators': 89, 'max_depth': 20, 'max_features': 12, 'min_impurity_decrease': 3}. Best is trial 0 with value: 28442.95077735297. [I 2023-07-22 19:22:47,323] Trial 3 finished with value: 28156.970223223692 and parameters: {'n_estimators': 91, 'max_depth': 13, 'max_features': 12, 'min_impurity_decrease': 2}. Best is trial 3 with value: 28156.970223223692. [I 2023-07-22 19:22:48,839] Trial 4 finished with value: 27987.064608128287 and parameters: {'n_estimators': 84, 'max_depth': 22, 'max_features': 15, 'min_impurity_decrease': 0}. Best is trial 4 with value: 27987.064608128287. [I 2023-07-22 19:22:50,350] Trial 5 finished with value: 28366.49664005019 and parameters: {'n_estimators': 93, 'max_depth': 24, 'max_features': 12, 'min_impurity_decrease': 1}. Best is trial 4 with value: 27987.064608128287. [I 2023-07-22 19:22:51,915] Trial 6 finished with value: 28098.63392974549 and parameters: {'n_estimators': 84, 'max_depth': 20, 'max_features': 16, 'min_impurity_decrease': 0}. Best is trial 4 with value: 27987.064608128287. [I 2023-07-22 19:22:53,372] Trial 7 finished with value: 28022.211175952554 and parameters: {'n_estimators': 85, 'max_depth': 23, 'max_features': 16, 'min_impurity_decrease': 1}. Best is trial 4 with value: 27987.064608128287. [I 2023-07-22 19:22:54,444] Trial 8 finished with value: 28312.840205422126 and parameters: {'n_estimators': 95, 'max_depth': 14, 'max_features': 10, 'min_impurity_decrease': 4}. Best is trial 4 with value: 27987.064608128287. [I 2023-07-22 19:22:55,569] Trial 9 finished with value: 28734.58782549189 and parameters: {'n_estimators': 85, 'max_depth': 13, 'max_features': 14, 'min_impurity_decrease': 3}. Best is trial 4 with value: 27987.064608128287. best params: {'n_estimators': 84, 'max_depth': 22, 'max_features': 15, 'min_impurity_decrease': 0} best score: [27987.064608128287]
optuna.logging.set_verbosity(optuna.logging.ERROR) #关闭自动打印的info,只显示进度条
#optuna.logging.set_verbosity(optuna.logging.INFO) #打印全部信息
best_params, best_score = optimizer_optuna(300,"TPE")
best params: {'n_estimators': 85, 'max_depth': 20, 'max_features': 18, 'min_impurity_decrease': 1} best score: [27869.040946212906]
optuna.logging.set_verbosity(optuna.logging.ERROR)
best_params, best_score = optimizer_optuna(300,"GP")
best params: {'n_estimators': 86, 'max_depth': 17, 'max_features': 15, 'min_impurity_decrease': 0} best score: [27705.114886531792]
HPO方法 | bayes_opt 贝叶斯优化 (基于GP) | hyperopt 贝叶斯优化 (基于TPE) | Optuna 贝叶斯优化 (基于TPE) | Optuna 贝叶斯优化 (基于GP) |
---|---|---|---|---|
搜索空间/全域空间 | 300/无限 | 300/3000 | 300/3000 | 300/3000 |
运行时间(分钟) | 8.20 | 6.50 | 7.15 | 13.23 |
搜索最优(RMSE) | 27728.966 | 27696.732 | 27705.115 | 27869.041 |
重建最优(RMSE) | 27728.966 | 27696.732 | 27705.115 | 27869.041 |
很显然,基于高斯过程的贝叶斯优化是比基于TPE的贝叶斯优化运行更加缓慢的。在Optuna进行调试时,这里并没有多次运行并取出Optuna表现最好的值。不过在TPE模式下,其运行速度与HyperOpt的运行速度高度接近。通常来说,除非特殊说明,将默认使用TPE方法进行优化。