自动机器学习超参数调整(贝叶斯优化)

【导读】机器学习中,调参是一项繁琐但相当重要的任务,由于它很大程度上影响了算法的性能。手动调参十分耗时,网格和随机搜索不须要人力,但须要很长的运行时间。所以,诞生了许多自动调整超参数的方法。贝叶斯优化是一种用模型找到函数最小值方法,已经应用于机器学习问题中的超参数搜索,这种方法性能好,同时比随机搜索省时。此外,如今有许多Python库能够实现贝叶斯超参数调整。本文将使用Hyperopt库演示梯度提高机(Gradient Boosting Machine,GBM) 的贝叶斯超参数调整的完整示例。文章由贝叶斯优化方法、优化问题的四个部分、目标函数、域空间、优化过程、及结果展现几个部分组成。python

 

贝叶斯优化方法

贝叶斯优化经过基于目标函数的过去评估结果创建替代函数(几率模型),来找到最小化目标函数的值。贝叶斯方法与随机或网格搜索的不一样之处在于,它在尝试下一组超参数时,会参考以前的评估结果,所以能够省去不少无用功。算法

超参数的评估代价很大,由于它要求使用待评估的超参数训练一遍模型,而许多深度学习模型动则几个小时几天才能完成训练,并评估模型,所以耗费巨大。贝叶斯调参发使用不断更新的几率模型,经过推断过去的结果来“集中”有但愿的超参数。app

 

Python中的选择

Python中有几个贝叶斯优化库,它们目标函数的替代函数不同。在本文中,咱们将使用Hyperopt,它使用Tree Parzen Estimator(TPE)。其余Python库包括Spearmint(高斯过程代理)和SMAC(随机森林回归)。dom

 

优化问题的四个部分

贝叶斯优化问题有四个部分:机器学习

  • 目标函数:咱们想要最小化的内容,在这里,目标函数是机器学习模型使用该组超参数在验证集上的损失。函数

  • 域空间:要搜索的超参数的取值范围性能

  • 优化算法:构造替代函数并选择下一个超参数值进行评估的方法。学习

  • 结果历史记录:来自目标函数评估的存储结果,包括超参数和验证集上的损失。测试

 

数据集

在本例中,咱们将使用Caravan Insurance数据集,其目标是预测客户是否购买保险单。 这是一个有监督分类问题,训练集和测试集的大小分别为5800和4000。评估性能的指标是AUC(曲线下面积)评估准则和ROC(receiver operating characteristic,以真阳率和假阳率为坐标轴的曲线图)曲线,ROC AUC越高表示模型越好。 数据集以下所示:
优化

为Hyperopt最小化目标函数,咱们的目标函数返回1-ROC AUC,从而提升ROC AUC。

 

梯度提高模型

梯度提高机(GBM)是一种基于使用弱学习器(如决策树)组合成强学习器的模型。 GBM中有许多超参数控制整个集合和单个决策树,如决策树数量,决策树深度等。简单了解了GBM,接下来咱们介绍这个问题对应的优化模型的四个部分

 

目标函数

目标函数是须要咱们最小化的。 它的输入为一组超参数,输出须要最小化的值(交叉验证损失)。Hyperopt将目标函数视为黑盒,只考虑它的输入和输出。 在这里,目标函数定义为:

1 def objective(hyperparameters):
2    '''Returns validation score from hyperparameters'''
3 
4    model = Classifier(hyperparameters)
5    validation_loss = cross_validation(model, training_data)
6    return validation_loss

咱们评估的是超参数在验证集上的表现,但咱们不将数据集划分红固定的验证集和训练集,而是使用K折交叉验证。使用10折交叉验证和提早中止的梯度提高机的完整目标函数以下所示。

 1 import lightgbm as lgb
 2 from hyperopt import STATUS_OK
 3 
 4 N_FOLDS = 10
 5 
 6 # Create the dataset
 7 train_set = lgb.Dataset(train_features, train_labels)
 8 
 9 
10 def objective(params, n_folds=N_FOLDS):
11    '''Objective function for Gradient Boosting Machine Hyperparameter Tuning'''
12 
13    # Perform n_fold cross validation with hyperparameters
14    # Use early stopping and evalute based on ROC AUC
15    cv_results = lgb.cv(params, train_set, nfold=n_folds, num_boost_round=10000,
16                        early_stopping_rounds=100, metrics='auc', seed=50)
17 
18    # Extract the best score
19    best_score = max(cv_results['auc-mean'])
20 
21    # Loss must be minimized
22    loss = 1 - best_score
23 
24    # Dictionary with information for evaluation
25    return {'loss': loss, 'params': params, 'status': STATUS_OK}

关键点是cvresults = lgb.cv(...)。为了实现提早中止的交叉验证,咱们使用LightGBM函数cv,它输入为超参数,训练集,用于交叉验证的折数等。咱们将迭代次数(numboostround)设置为10000,但实际上不会达到这个数字,由于咱们使用earlystopping_rounds来中止训练,当连续100轮迭代效果都没有提高时,则提早中止,并选择模型。所以,迭代次数并非咱们须要设置的超参数。

一旦交叉验证完成,咱们就会获得最好的分数(ROC AUC),而后,由于咱们最小化目标函数,因此计算1- ROC AUC,而后返回这个值。

 

域空间

域空间表示咱们要为每一个超参数计算的值的范围。在搜索的每次迭代中,贝叶斯优化算法将从域空间为每一个超参数选择一个值。当咱们进行随机或网格搜索时,域空间是一个网格。在贝叶斯优化中,想法是同样的,可是不是按照顺序(网格)或者随机选择一个超参数,而是按照每一个超参数的几率分布选择。

而肯定域空间是最困难的。若是咱们有机器学习方法的经验,咱们能够经过在咱们认为最佳值的位置放置更大的几率来使用它来告知咱们对超参数分布的选择。然而,不一样数据集之间最佳模型不同,而且具备高维度问题(许多超参数),超参数之间也会互相影响。在咱们不肯定最佳值的状况下,咱们能够将范围设定的大一点,让贝叶斯算法为咱们作推理。

首先,咱们看看GBM中的全部超参数:

 1 import lgb
 2 # Default gradient boosting machine classifier
 3 model = lgb.LGBMClassifier()
 4 model
 5 LGBMClassifier(boosting_type='gbdt', n_estimators=100,
 6               class_weight=None, colsample_bytree=1.0,
 7               learning_rate=0.1, max_depth=-1,                      
 8               min_child_samples=20,
 9               min_child_weight=0.001, min_split_gain=0.0, 
10               n_jobs=-1, num_leaves=31, objective=None, 
11               random_state=None, reg_alpha=0.0, reg_lambda=0.0, 
12               silent=True, subsample=1.0, 
13               subsample_for_bin=200000, subsample_freq=1)

其中一些咱们不须要调整(例如objective和randomstate),咱们将使用提早中止来找到最好的n_estimators。 可是,咱们还有10个超参数要优化! 首次调整模型时,我一般会建立一个以默认值为中心的宽域空间,而后在后续搜索中对其进行细化。

例如,让咱们在Hyperopt中定义一个简单的域,这是GBM中每棵树中叶子数量的离散均匀分布:

1 from hyperopt import hp
2 # Discrete uniform distribution
3 num_leaves = {'num_leaves': hp.quniform('num_leaves', 30, 150, 1)}

这里选择离散的均匀分布,由于叶子的数量必须是整数(离散),而且域中的每一个值均可能(均匀)。

另外一种分布选择是对数均匀,它在对数标度上均匀分布值。 咱们将使用对数统一(从0.005到0.2)来得到学习率,由于它在几个数量级上变化:

1 # Learning rate log uniform distribution
2 learning_rate = {'learning_rate': hp.loguniform('learning_rate',
3                                                 np.log(0.005),
4                                                 np.log(0.2)}

下面分别绘制了均匀分布和对数均匀分布的图。 这些是核密度估计图,所以y轴是密度而不是计数!

如今,让咱们定义整个域:

 1 # Define the search space
 2 space = {
 3    'class_weight': hp.choice('class_weight', [None, 'balanced']),
 4    'boosting_type': hp.choice('boosting_type', 
 5                               [{'boosting_type': 'gbdt', 
 6                                    'subsample': hp.uniform('gdbt_subsample', 0.5, 1)}, 
 7                                 {'boosting_type': 'dart', 
 8                                     'subsample': hp.uniform('dart_subsample', 0.5, 1)},
 9                                 {'boosting_type': 'goss'}]),
10    'num_leaves': hp.quniform('num_leaves', 30, 150, 1),
11    'learning_rate': hp.loguniform('learning_rate', np.log(0.01), np.log(0.2)),
12    'subsample_for_bin': hp.quniform('subsample_for_bin', 20000, 300000, 20000),
13    'min_child_samples': hp.quniform('min_child_samples', 20, 500, 5),
14    'reg_alpha': hp.uniform('reg_alpha', 0.0, 1.0),
15    'reg_lambda': hp.uniform('reg_lambda', 0.0, 1.0),
16    'colsample_bytree': hp.uniform('colsample_by_tree', 0.6, 1.0)
17 }

这里咱们使用了许多不一样的域分发类型:

 1 choice:类别变量
 2 quniform:离散均匀(整数间隔均匀)
 3 uniform:连续均匀(间隔为一个浮点数)
 4 loguniform:连续对数均匀(对数下均匀分布)
 5 # boosting type domain 
 6 boosting_type = {'boosting_type': hp.choice('boosting_type', 
 7                                            [{'boosting_type': 'gbdt', 
 8                                                  'subsample': hp.uniform('subsample', 0.5, 1)}, 
 9                                             {'boosting_type': 'dart', 
10                                                  'subsample': hp.uniform('subsample', 0.5, 1)},
11                                             {'boosting_type': 'goss',
12                                                  'subsample': 1.0}])}

这里咱们使用条件域,这意味着一个超参数的值取决于另外一个超参数的值。 对于提高类型“goss”,gbm不能使用子采样(仅选择训练观察的子样本部分以在每次迭代时使用)。 所以,若是提高类型是“goss”,则子采样率设置为1.0(无子采样),不然为0.5-1.0。 这是使用嵌套域实现的

定义域空间以后,咱们能够从中采样查看样本。

 1 # Sample from the full space
 2 example = sample(space)
 3 
 4 # Dictionary get method with default
 5 subsample = example['boosting_type'].get('subsample', 1.0)
 6 
 7 # Assign top-level keys
 8 example['boosting_type'] = example['boosting_type']['boosting_type']
 9 example['subsample'] = subsample
10 
11 example
12 {'boosting_type': 'gbdt',
13 'class_weight': 'balanced',
14 'colsample_bytree': 0.8111305579351727,
15 'learning_rate': 0.16186471096789776,
16 'min_child_samples': 470.0,
17 'num_leaves': 88.0,
18 'reg_alpha': 0.6338327001528129,
19 'reg_lambda': 0.8554826167886239,
20 'subsample_for_bin': 280000.0,
21 'subsample': 0.6318665053932255}

 

优化算法

虽然这是贝叶斯优化中概念上最难的部分,但在Hyperopt中建立优化算法只需一行。 要使用Tree Parzen Estimator,代码为:

1 from hyperopt import tpe
2 # Algorithm
3 tpe_algorithm = tpe.suggest

在优化时,TPE算法根据过去的结果构建几率模型,并经过最大化预期的改进来决定下一组超参数以在目标函数中进行评估。

 

结果历史

跟踪结果并非绝对必要的,由于Hyperopt将在内部为算法执行此操做。 可是,若是咱们想知道幕后发生了什么,咱们可使用Trials对象来存储基本的训练信息,还可使用从目标函数返回的字典(包括损失和范围)。 制建立Trials对象也只要一行代码:

1 from hyperopt import Trials
2 # Trials object to track progress
3 bayes_trials = Trials()

为了监控训练运行进度,能够将结果历史写入csv文件,防止程序意外中断致使评估结果消失。

 1 import csv
 2 
 3 # File to save first results
 4 out_file = 'gbm_trials.csv'
 5 of_connection = open(out_file, 'w')
 6 writer = csv.writer(of_connection)
 7 
 8 # Write the headers to the file
 9 writer.writerow(['loss', 'params', 'iteration', 'estimators', 'train_time'])
10 of_connection.close()

而后在目标函数中咱们能够在每次迭代时添加行写入csv:

1 # Write to the csv file ('a' means append)
2 of_connection = open(out_file, 'a')
3 writer = csv.writer(of_connection)
4 writer.writerow([loss, params, iteration, n_estimators, run_time])
5 of_connection.close()

 

优化:

一旦咱们定义好了上述部分,就能够用fmin运行优化:

1 from hyperopt import fmin
2 MAX_EVALS = 500
3 # Optimize
4 best = fmin(fn = objective, space = space, algo = tpe.suggest, 
5            max_evals = MAX_EVALS, trials = bayes_trials)

在每次迭代时,算法从代理函数中选择新的超参数值,该代理函数基于先前的结果构建并在目标函数中评估这些值。 这继续用于目标函数的MAX_EVALS评估,其中代理函数随每一个新结果不断更新。

 

结果:

从fmin返回的最佳对象包含在目标函数上产生最低损失的超参数:

 1 {'boosting_type': 'gbdt',
 2   'class_weight': 'balanced',
 3   'colsample_bytree': 0.7125187075392453,
 4   'learning_rate': 0.022592570862044956,
 5   'min_child_samples': 250,
 6   'num_leaves': 49,
 7   'reg_alpha': 0.2035211643104735,
 8   'reg_lambda': 0.6455131715928091,
 9   'subsample': 0.983566228071919,
10   'subsample_for_bin': 200000}

一旦咱们有了这些超参数,咱们就可使用它们来训练完整训练数据的模型,而后评估测试数据。 最终结果以下:

The best model scores 0.72506 AUC ROC on the test set.
The best cross validation score was 0.77101 AUC ROC.
This was achieved after 413 search iterations.

做为参考,500次随机搜索迭代返回了一个模型,该模型在测试集上评分为0.7232 ROC AUC,在交叉验证中评分为0.76850。没有优化的默认模型在测试集上评分为0.7143 ROC AUC。

在查看结果时,请记住一些重要的注意事项:

最佳超参数是那些在交叉验证方面表现最佳的参数,而不必定是那些在测试数据上作得最好的参数。当咱们使用交叉验证时,咱们但愿这些结果能够推广到测试数据。

即便使用10倍交叉验证,超参数调整也会过分拟合训练数据。交叉验证的最佳分数显著高于测试数据。

随机搜索能够经过纯粹的运气返回更好的超参数(从新运行笔记本能够改变结果)。贝叶斯优化不能保证找到更好的超参数,而且可能陷入目标函数的局部最小值。

另外一个重点是超参数优化的效果将随数据集的不一样而不一样。相对较小的数据集(训练集大小为6000),调整超参数,最终获得的模型的提高并不大,但数据集更大时,效果会很明显。

所以,经过贝叶斯几率来优化超参数,咱们能够:在测试集上获得更好的性能;调整超参数的迭代次数减小.

 

可视化结果:

绘制结果图表是一种直观的方式,能够了解超参数搜索过程当中发生的状况。此外,经过将贝叶斯优化与随机搜索进行比较,能够看出方法的不一样之处。

首先,咱们能够制做随机搜索和贝叶斯优化中采样的learning_rate的核密度估计图。做为参考,咱们还能够显示采样分布。垂直虚线表示学习率的最佳值(根据交叉验证)。

 

咱们将学习率定义为0.005到0.2之间的对数正态,贝叶斯优化结果看起来与采样分布相似。 这告诉咱们,咱们定义的分布看起来适合于任务,尽管最佳值比咱们放置最大几率的值略高。 这可用于告诉域进一步搜索。

另外一个超参数是boosting_type,在随机搜索和贝叶斯优化期间评估每种类型的条形图。 因为随机搜索不关注过去的结果,咱们预计每种加强类型的使用次数大体相同。

 

根据贝叶斯算法,gdbt提高模型比dart或goss更有前途。 一样,这能够帮助进一步搜索,贝叶斯方法或网格搜索。 若是咱们想要进行更明智的网格搜索,咱们可使用这些结果来定义围绕超参数最有但愿的值的较小网格。

咱们再看下其余参数的分布,随机搜索和贝叶斯优化中的全部数值型超参数。 垂直线再次表示每次搜索的超参数的最佳值:

在大多数状况下(subsample_for_bin除外),贝叶斯优化搜索倾向于在超参数值附近集中(放置更多几率),从而产生交叉验证中的最低损失。这显示了使用贝叶斯方法进行超参数调整的基本思想:花费更多时间来评估有但愿的超参数值。

此处还有一些有趣的结果可能会帮助咱们在未来定义要搜索的域空间时。仅举一个例子,看起来regalpha和reglambda应该相互补充:若是一个是高(接近1.0),另外一个应该更低。不能保证这会解决问题,但经过研究结果,咱们能够得到可能适用于将来机器学习问题的看法!

 

搜索的演变

随着优化的进展,咱们指望贝叶斯方法关注超参数的更有但愿的值:那些在交叉验证中产生最低偏差的值。咱们能够绘制超参数与迭代的值,以查看是否存在明显的趋势。

黑星表示最佳值。 colsample_bytree和learning_rate会随着时间的推移而减小,这可能会指导咱们将来的搜索。

 

最后,若是贝叶斯优化工做正常,咱们预计平均验证分数会随着时间的推移而增长(相反,损失减小):

来自贝叶斯超参数优化的验证集上的分数随着时间的推移而增长,代表该方法正在尝试“更好”的超参数值(应该注意,仅根据验证集上的表现更好)。随机搜索没有显示迭代的改进。

 

继续搜索

若是咱们对模型的性能不满意,咱们能够继续使用Hyperopt进行搜索。咱们只须要传入相同的试验对象,算法将继续搜索。

随着算法的进展,它会进行更多的挖掘 - 挑选过去表现良好的价值 , 而不是探索 - 挑选新价值。所以,开始彻底不一样的搜索多是一个好主意,而不是从搜索中止的地方继续开始。若是来自第一次搜索的最佳超参数确实是“最优的”,咱们但愿后续搜索专一于相同的值。

通过另外500次训练后,最终模型在测试集上得分为0.72736 ROC AUC。 (咱们实际上不该该评估测试集上的第一个模型,而只依赖于验证分数。理想状况下,测试集应该只使用一次,以便在部署到新数据时得到算法性能的度量)。一样,因为数据集的小尺寸,这个问题可能致使进一步超参数优化的收益递减,而且最终会出现验证错误的平台(因为隐藏变量致使数据集上任何模型的性能存在固有限制未测量和噪声数据,称为贝叶斯偏差)

 

结论

可使用贝叶斯优化来完成机器学习模型的超参数自动调整。与随机或网格搜索相比,贝叶斯优化对目标函数的评估较少,测试集上的更好的泛化性能。

在本文中,咱们使用Hyperopt逐步完成了Python中的贝叶斯超参数优化。除了网格和随机搜索以外,咱们还可以提升梯度提高树的测试集性能,尽管咱们须要谨慎对待训练数据的过分拟合。此外,咱们经过可视化结果表看到随机搜索与贝叶斯优化的不一样之处,这些图表显示贝叶斯方法对超参数值的几率更大,致使交叉验证损失更低。

使用优化问题的四个部分,咱们可使用Hyperopt来解决各类各样的问题。贝叶斯优化的基本部分也适用于Python中实现不一样算法的许多库。从手动切换到随机或网格搜索只是一小步,但要将机器学习提高到新的水平,须要一些自动形式的超参数调整。贝叶斯优化是一种易于在Python中使用的方法,而且能够比随机搜索返回更好的结果。

 

参考自https://towardsdatascience.com/automated-machine-learning-hyperparameter-tuning-in-python-dfda59b72f8a

相关文章
相关标签/搜索