10.7&10.8 基于HyperOpt实现TPE优化&基于Optuna实现多种优化

 2 基于HyperOpt实现TPE优化

Hyperopt优化器是目前最为通用的贝叶斯优化器之一,Hyperopt中集成了包括随机搜索、模拟退火和TPE(Tree-structured Parzen Estimator Approach)等多种优化算法。相比于Bayes_opt,Hyperopt的是更先进、更现代、维护更好的优化器,也是我们最常用来实现TPE方法的优化器。在实际使用中,相比基于高斯过程的贝叶斯优化,基于高斯混合模型的TPE在大多数情况下以更高效率获得更优结果,该方法目前也被广泛应用于AutoML领域中。TPE算法原理可以参阅原论文这里我们将重点介绍关于Hyperopt中使用TPE进行超参数搜索的过程。

import hyperopt
from hyperopt import hp, fmin, tpe, Trials, partial
from hyperopt.early_stop import no_progress_loss

print(hyperopt.__version__)
#0.2.7
  • 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=1412
              ,verbose=False
              ,n_jobs=-1)
    
    #交叉验证结果,输出负根均方误差(-RMSE)
    cv = KFold(n_splits=5,shuffle=True,random_state=1412)
    validation_loss = cross_validate(reg,X,y
                                     ,scoring="neg_root_mean_squared_error"
                                     ,cv=cv
                                     ,verbose=False
                                     ,n_jobs=-1
                                     ,error_score='raise'
                                    )
    
    #最终输出结果,由于只能取最小值,所以必须对(-RMSE)求绝对值
    #以求解最小RMSE所对应的参数组合
    return np.mean(abs(validation_loss["test_score"]))
  • 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
  • 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 #代理模型你要哪个呢?#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

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=1412
              ,verbose=False
              ,n_jobs=-1
             )
    cv = KFold(n_splits=5,shuffle=True,random_state=1412)
    validation_loss = cross_validate(reg,X,y
                                     ,scoring="neg_root_mean_squared_error"
                                     ,cv=cv
                                     ,verbose=False
                                     ,n_jobs=-1
                                    )
    return np.mean(abs(validation_loss["test_score"]))
  • 5 执行实际优化流程
params_best, trials = param_hyperopt(30) #1%的空间大小

params_best, trials = param_hyperopt(100) #3%的空间大小

params_best, trials = param_hyperopt(300) #10%的空间大小

hyperopt_validation(params_best)
#28346.672687223065

#打印所有搜索相关的记录
trials.trials[0]
# {'state': 2,
#  'tid': 0,
#  'spec': None,
#  'result': {'loss': 28766.452192638408, 'status': 'ok'},
#  'misc': {'tid': 0,
#   'cmd': ('domain_attachment', 'FMinIter_Domain'),
#   'workdir': None,
#   'idxs': {'max_depth': [0],
#    'max_features': [0],
#    'min_impurity_decrease': [0],
#    'n_estimators': [0]},
#   'vals': {'max_depth': [13.0],
#    'max_features': [18.0],
#    'min_impurity_decrease': [4.0],
#    'n_estimators': [80.0]}},
#  'exp_key': None,
#  'owner': None,
#  'version': 0,
#  'book_time': datetime.datetime(2021, 12, 24, 13, 33, 19, 633000),
#  'refresh_time': datetime.datetime(2021, 12, 24, 13, 33, 19, 840000)}

#打印全部搜索的目标函数值
trials.losses()[:10]
#[28766.452192638408,
# 29762.22885008687,
# 29233.57333898302,
# 29257.33343872428,
# 29180.63733732971,
# 29249.676793746046,
# 29309.41793204717,
# 28915.33638544984,
# 29122.269575607537,
# 29150.39720576636]

10.7&10.8 基于HyperOpt实现TPE优化&基于Optuna实现多种优化_第1张图片

由于具有提前停止功能,因此基于TPE的hyperopt优化可能在我们设置的迭代次数被达到之前就停止,也因此hyperopt迭代到实际最优值所需的迭代次数可能更少。同时,TPE方法相比于高斯过程计算会更加迅速,因此在运行277次迭代的情况下,hyperopt只需要1分钟时间,而运行300次迭代的bayes_opt却需要2.11分钟,可见,即便运行同样的迭代次数,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 。

import optuna
print(optuna.__version__)
#2.10.0
  • 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=1412
              ,verbose=False
              ,n_jobs=-1
             )
    
    #交叉验证过程,输出负均方根误差(-RMSE)
    #optuna同时支持最大化和最小化,因此如果输出-RMSE,则选择最大化
    #如果选择输出RMSE,则选择最小化
    cv = KFold(n_splits=5,shuffle=True,random_state=1412)
    validation_loss = cross_validate(reg,X,y
                                     ,scoring="neg_root_mean_squared_error"
                                     ,cv=cv #交叉验证模式
                                     ,verbose=False #是否打印进程
                                     ,n_jobs=-1 #线程数
                                     ,error_score='raise'
                                    )
    #最终输出RMSE
    return np.mean(abs(validation_loss["test_score"]))
  • 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)#默认最开始有10个观测值,每一次计算采集函数随机抽取24组参数组合
    elif algo == "GP":
        from optuna.integration import SkoptSampler #skilearn——optimize
        import skopt
        algo = SkoptSampler(skopt_kwargs={'base_estimator':'GP', #选择高斯过程
                                          'n_initial_points':10, #初始观测点10个
                                          'acq_func':'EI'} #选择的采集函数为EI,期望增量
                           )
    
    #实际优化过程,首先实例化优化器
    study = optuna.create_study(sampler = algo #要使用的具体算法 sampler对样本进行抽样
                                , 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 执行实际优化流程

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") #默认打印迭代过程
#   0%|          | 0/10 [00:00

很显然,基于高斯过程的贝叶斯优化是比基于TPE的贝叶斯优化运行更加缓慢的。在Optuna进行调试时,我并没有多次运行并取出Optuna表现最好的值,因此我们可以不将Optuna的结果最终放入表格进行比较,不过在TPE模式下,其运行速度与HyperOpt的运行速度高度接近。在未来的课程中,除非特殊说明,我们将默认使用TPE方法进行优化。

你可能感兴趣的:(机器学习,HyperOpt,Optuna)