HPO,全称Hyper-parameter Optimization,即超参优化。之所以做这项工作是出于机器学习领域的两个基本事实:
- no free lunch。即天下没有免费午餐,用在机器学习领域是指没有一种算法可以适用于所有的机器学习问题,换言之A算法可能在这个数据集上表现最优,但在另一个数据集上表现最好的则是另一个B算法。
- 对于同一算法,不同的超参数可能对算法性能影响很大。例如线性模型中的正则化系数、决策树模型中树的最大深度等,这些都属于模型拟合之外的参数,需要认为指定,故而称之为超参数。
正因如此,所以算法工程师们在提升算法性能时常常需要对比多个模型,同时在各模型内部又要调整多组超参,以期实现最优效果。在这个超参调优过程中,当前的实现方式主要是如下三种:
- 最为简单也最为熟知的莫过于网格搜索,在sklearn中的实现是GridSearch,通过对各超参数提供所有可能的候选值,该算法会自动暴力尝试所有可能的超参组合,并给出最佳结果。该实现方法直观易懂,但缺点也很明显,那就是效率不高,而且只能接受离散取值
- 与网格搜索类似、但不再暴力枚举的一种方法是随机搜索,其优化过程其实也更为简单:即对每个超参数均随机选取一个候选值,而后组成一次随机抽选的超参组合。最后返回所有随机尝试后的最优组合。这种方法实现简单,搜索次数可大可小,但却往往能取得不错的效果。但所得到的最好结果可能不是最优解。
- 贝叶斯优化。除了网格搜索和随机搜索外,贝叶斯优化可能是目前最为理想和高效的超参优化(从其名字可以看出,这类方法跟贝叶斯大神有一定关系,大概是由于其中要用到的代理函数与贝叶斯后验概率有关吧)。基于贝叶斯优化算法实现的HPO,其一般形式可抽象为如下SMBO的过程:
《Bayesian Optimization Primer》
其中,各符号及变量的含义如下:
- f:目标函数,在机器学习场景中即为根据超参数组合xi得到评估指标yi的过程
- X:超参搜索空间,其中每个xi即为X中的一组取值;
- yi:目标函数的得分,在机器学习场景中即为评估指标结果,例如accuracy_score
- D:所有(xi, yi)组成的数据集
- M:代理函数,即要得到的由xi得到yi的映射方法。从机器学习的视角来理解,既然是由一组超参数(可理解为特征)拟合一个连续的得分结果(回归目标),所以可用一个回归模型来实现。这里之所以称之为代理函数,则是因为正常情况下,应该是真正的用对应的算法模型+超参数进行实际的训练和评估,得到真实的评估结果,而此时为了避免这种大计算量的过程(expensive),所以才选择用一个简单快速的函数加以拟合替代
- S:采集函数,即根据当前得到的代理函数M和超参搜索空间X,如何获取下一组可能带来性能提升的超参组合。
基于上述符号定义,SMBO过程如下:
- 指定输入参数f、X、M、S,给定一组初始的(xi, yi),作为初始训练集完成代理函数M的评估;
- 根据采集函数S和代理函数M,得到当前情况下可能获得最优得分的超参组合xi+1
- 将新一组超参数带入待优化的机器学习,得到真实的yi+1
- 将最新数据(xi+1, yi+1)加入到数据集D中,重新拟合代理函数M
- 如此迭代执行T次,或者达到目标效果结束
这一优化过程是逐一选取潜在的最优超参数,并将其结果加入到数据集中继续完成代理函数的优化过程,所以这也就是其称之为Sequential的原因,代理函数M则呼应model-based。而毫无疑问,这其中有两个重要细节实现:一个是代理函数M的选取和建模;另一个是采集函数S的设计。这两个过程的差异,也决定了具体的贝叶斯优化实现的不同。
这里简单介绍几种主流的代理函数M的选取:
- 高斯过程,即将K个超参数到评分的映射关系抽象为K维联合高斯分布,从而每次都根据数据集D来计算该联合高斯分布的均值和方差即可。但这种方式的一个显著缺点是仅适用于连续性超参,且一般仅在较低维度下适用;
- TPE,tree-structured Parzen estimator,主要思想是用到核密度函数估计(KDE,kernel density estimator),会根据yi的取值高低将数据集划分为两个区域,从而在两个区域分别用kde方法拟合其分布。最后的目标就是尽可能的最大化高分的概率g(x)同时最小化低分的概率l(x)(实际用到的是最小化比值:l(x)/g(x));
- Random Forest,即将代理函数M用一个随机森林回归模型加以拟合,其中每棵子树均通过在数据集D的随机子集进行拟合确保随机性。当拿到一组新的超参组合时,即可通过该随机森林模型中每棵子树的评分结果的均值作为衡量该组超参数的潜力。
至于采集函数的选取,则也有不同的设计,例如PI(Probability of improvement)和EI(Expected Improvement)等,这里不再展开。
对于这三种代理函数的抽象实现,Python中均有相应的库可直接调用。本文选取三个库,分别对应一种代理函数的贝叶斯优化方法:
- GP,对应python库bayes_opt
- TPE,对应python库hyperopt
- RandomForest,对应python库scikit-optimizer,简称skopt
这里以sklearn中提供的经典二分类数据集breast_cancer为例,给出三个优化库的基本实现方法:
- 数据集加载和默认参数随机森林的baseline
from sklearn.datasets import load_breast_cancer from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, train_test_split X, y = load_breast_cancer(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split(X, y) rf = RandomForestClassifier() rf.fit(X_train, y_train) rf.score(X_test, y_test) # 默认参数RF得分:0.958041958041958
- bayes_opt实现
from bayes_opt import BayesianOptimization # bayes_opt中的目标函数 def fun_bo(n_estimators, max_depth, min_samples_split, min_samples_leaf): rf = RandomForestClassifier(n_estimators=int(n_estimators), max_depth=int(max_depth), min_samples_split=int(min_samples_split), min_samples_leaf=int(min_samples_leaf)) score = cross_val_score(rf, X_train, y_train) return score.mean() # 贝叶斯优化中的搜索空间 space_bo = { 'n_estimators': (10, 300), 'max_depth': (1, 21), 'min_samples_split': (2, 20), 'min_samples_leaf': (2, 20) } bo = BayesianOptimization( fun_bo, space_bo ) bo.maximize() # 一键完成优化 # 得到最优超参结果 param = {k:int(v) for k, v in bo.max['params'].items()} rf_hp = RandomForestClassifier(**param) rf_hp.fit(X_train, y_train) rf_hp.score(X_test, y_test) # bayes_opt优化得分:0.965034965034965
- hyperopt实现
from hyperopt import fmin, hp, tpe, Trials # hyperopt中的目标函数 def fun_hp(param): rf = RandomForestClassifier(**param, random_state=3) score = cross_val_score(rf, X_train, y_train) return 1-score.mean() # hyperopt中的搜索空间 space_hp = { "n_estimators":hp.uniformint("n_estimators", 10, 300), "max_depth":hp.uniformint("max_depth", 1, 21), "min_samples_split":hp.uniformint("min_samples_split", 2, 20), "min_samples_leaf":hp.uniformint("min_samples_leaf", 2, 20) } # 记录优化过程,fmin实现一键优化,采用优化算法是tpe trials = Trials() param = fmin(fun_hp, space_hp, tpe.suggest, max_evals=100, trials=trials) param = {k:int(v) for k, v in param.items()} # 最优超参数 rf_hp = RandomForestClassifier(**res) rf_hp.fit(X_train, y_train) rf_hp.score(X_test, y_test) # hyperopt优化得分:0.965034965034965
- skopt实现
from skopt import forest_minimize, space # skopt中的目标函数 def fun_sk(param): param = dict(zip(['n_estimators', 'max_depth', 'min_samples_split', 'min_samples_leaf'], param)) rf = RandomForestClassifier(**param) score = cross_val_score(rf, X_train, y_train) return 1 - score.mean() # skopt中的搜索空间 space_sk = [ space.Integer(10, 300, name='n_estimators'), space.Integer(1, 21, name='max_depth'), space.Integer(2, 20, name='min_samples_split'), space.Integer(2, 20, name='min_samples_leaf') ] # 采用RF进行优化,得到最优超参结果 res = forest_minimize(fun_sk, space_sk) param = dict(zip(['n_estimators', 'max_depth', 'min_samples_split', 'min_samples_leaf'], res.x)) rf_hp = RandomForestClassifier(**param) rf_hp.fit(X_train, y_train) rf_hp.score(X_test, y_test) # skopt优化得分:0.965034965034965
在上述超参优化过程中,由于所用数据集较小,所以在制定相应的目标函数时均采用交叉验证的方式以提高泛华性能。同时,三种超参优化方式所得到最优优化结果相同,这一方面源于数据集较小造成的,另一方面其本身也有一定的随机性。但无论如何,三个优化库在具体使用上是相近的,在优化效果方面也算相当的。