在讲解随机网格搜索之前,我们梳理了决定枚举网格搜索运算速度的因子:
1 参数空间的大小:参数空间越大,需要建模的次数越多
2 数据量的大小:数据量越大,每次建模时需要的算力和时间越多
面对枚举网格搜索过慢的问题,sklearn中呈现了两种优化方式:其一是调整搜索空间,其二是调整每次训练的数据。调整搜索空间的方法就是随机网格搜索,而调整每次训练数据的方法就是对半网格搜索。
假设现在存在数据集 D D D,我们从数据集 D D D中随机抽样出一个子集 d d d。如果一组参数在整个数据集 D D D上表现较差,那大概率这组参数在数据集的子集 d d d上表现也不会太好。反之,如果一组参数在子集 d d d上表现不好,我们也不会信任这组参数在全数据集 D D D上的表现。参数在子集与全数据集上反馈出的表现一致,如果这一假设成立,那在网格搜索中,比起每次都使用全部数据来验证一组参数,或许我们可以考虑只带入训练数据的子集来对超参数进行筛选,这样可以极大程度地加速我们的运算。
但在现实数据中,这一假设要成立是有条件的,即任意子集的分布都与全数据集D的分布类似。当子集的分布越接近全数据集的分布,同一组参数在子集与全数据集上的表现越有可能一致。根据之前在随机网格搜索中得出的结论,我们知道子集越大、其分布越接近全数据集的分布,但是大子集又会导致更长的训练时间,因此为了整体训练效率,我们不可能无限地增大子集。这就出现了一个矛盾:大子集上的结果更可靠,但大子集计算更缓慢。对半网格搜索算法设计了一个精妙的流程,可以很好的权衡子集的大小与计算效率问题,我们来看具体的流程:
1、首先从全数据集中无放回随机抽样出一个很小的子集 d 0 d_0 d0,并在 d 0 d_0 d0上验证全部参数组合的性能。根据 d 0 d_0 d0上的验证结果,淘汰评分排在后1/2的那一半参数组合
2、然后,从全数据集中再无放回抽样出一个比 d 0 d_0 d0大一倍的子集 d 1 d_1 d1,并在 d 1 d_1 d1上验证剩下的那一半参数组合的性能。根据 d 1 d_1 d1上的验证结果,淘汰评分排在后1/2的参数组合
3、再从全数据集中无放回抽样出一个比 d 1 d_1 d1大一倍的子集 d 2 d_2 d2,并在 d 2 d_2 d2上验证剩下1/4的参数组合的性能。根据 d 2 d_2 d2上的验证结果,淘汰评分排在后1/2的参数组合……
持续循环。如果使用S代表首次迭代时子集的样本量,C代表全部参数组合数,则在迭代过程中,用于验证参数的数据子集是越来越大的,而需要被验证的参数组合数量是越来越少的:
迭代次数 | 子集样本量 | 参数组合数 |
---|---|---|
1 | S | C |
2 | 2S | 1 2 \frac{1}{2} 21C |
3 | 4S | 1 4 \frac{1}{4} 41C |
4 | 8S | 1 8 \frac{1}{8} 81C |
…… |
当备选参数组合只剩下一组,或剩余可用的数据不足,循环就会停下,具体地来说,当 1 n \frac{1}{n} n1C <= 1或者nS > 总体样本量,搜索就会停止。在实际应用时,哪一种停止条件会先被触发,需要看实际样本量及参数空间地大小。同时,每次迭代时增加的样本量、以及每次迭代时不断减少的参数组合都是可以自由设定的。
在这种模式下,只有在不同的子集上不断获得优秀结果的参数组合能够被留存到迭代的后期,最终选择出的参数组合一定是在所有子集上都表现优秀的参数组合。这样一个参数组合在全数据上表现优异的可能性是非常大的,同时也可能展现出比网格/随机搜索得出的参数更大的泛化能力。
然而这个过程当中会存在一个问题:子集越大时,子集与全数据集D的分布会越相似,但整个对半搜索算法在开头的时候,就用最小的子集筛掉了最多的参数组合。如果最初的子集与全数据集的分布差异巨大的化,在对半搜索开头的前几次迭代中,就可能筛掉许多对全数据集D有效的参数,因此对半网格搜索最初的子集一定不能太小。
在对半网格搜索过程中,子集的样本量时呈指数级增长:
在初始子集样本量为10的前提下,7、8次迭代就会消耗掉2500+数据资源。在初始子集一定不能太小、且对半搜索的抽样是不放回抽样的大前提下,整体数据的样本量必须要很大。从经验来看,对半网格搜索在小型数据集上的表现往往不如随机网格搜索与普通网格搜索,事实上,如果我们在Kaggle房价数据集上使用对半网格搜索,会发现其搜索结果还不如枚举网格搜索、且搜索时间长于之前我们尝试过的任何一种搜索方式。但在大型数据集上(比如,样本量过w的数据集上),对半网格搜索则展现出运算速度和精度上的巨大优势。
因此在对半网格搜索实现时,我们使用一组拓展的房价数据集,有2w9条样本。
对半网格搜索的类如下所示:
class sklearn.model_selection.HalvingGridSearchCV
(estimator, param_grid, *, factor=3, resource=‘n_samples’, max_resources=‘auto’, min_resources=‘exhaust’, aggressive_elimination=False, cv=5, scoring=None, refit=True, error_score=nan, return_train_score=True, random_state=None, n_jobs=None, verbose=0)
全部参数如下所示:
Name | Description |
---|---|
estimator | 调参对象,某评估器 |
param_grid | 参数空间,可以是字典或者字典构成的列表 |
factor | 每轮迭代中新增的样本量的比例,同时也是每轮迭代后留下的参数组合的比例 |
resource | 设置每轮迭代中增加的验证资源的类型 |
max_resources | 在一次迭代中,允许被用来验证任意参数组合的最大样本量 |
min_resources | 首次迭代时,用于验证参数组合的样本量r0 |
aggressive_elimination | 是否以全部数被使用完成作为停止搜索的指标,如果不是,则采取措施 |
cv | 交叉验证的折数 |
scoring | 评估指标,支持同时输出多个参数 |
refit | 挑选评估指标和最佳参数,在完整数据集上进行训练 |
error_score | 当网格搜索报错时返回结果,选择’raise’时将直接报错并中断训练过程 其他情况会显示警告信息后继续完成训练 |
return_train_score | 在交叉验证中是否显示训练集中参数得分 |
random_state | 控制随机抽样数据集的随机性 |
n_jobs | 设置工作时参与计算的线程数 |
verbose | 输出工作日志形式 |
每轮迭代中新增的样本量的比例,同时也是每轮迭代后留下的参数组合的比例。例如,当factor=2时,下一轮迭代的样本量会是上一轮的2倍,每次迭代后有1/2的参数组合被留下。如果factor=3时,下一轮迭代的样本量会是上一轮的3倍,每次迭代后有1/3的参数组合被留下。该参数通常取3时效果比较好。
设置每轮迭代中增加的验证资源的类型,输入为字符串。默认是样本量,输入为"n_samples",也可以是任意集成算法当中输入正整数的弱分类器,例如"n_estimators"或者"n_iteration"。
首次迭代时,用于验证参数组合的样本量r0。可以输入正整数,或两种字符串"smallest",“exhaust”。
输入正整数n,表示首次迭代时使用n个样本。
输入"smallest",则根据规则计算r0:
当资源类型是样本量时,对回归类算法,r0 = 交叉验证折数n_splits * 2
当资源类型是样本量时,对分类算法,r0 = 类别数量n_classes_ * 交叉验证折数n_splits * 2
当资源类型不是样本量时,等于1
输入"exhaust",则根据迭代最后一轮的最大可用资源倒退r0。例如,factor=2, 样本量为1000时,一共迭代3次时,则最后一轮迭代的最大可用资源为1000,倒数第二轮为500,倒数第三轮(第一轮)为250。此时r0 = 250。"exhaust"模式下最有可能得到好的结果,不过计算量会略大,计算时间会略长。
现在,我们依然使用网格搜索最初的,空间大小为1536的参数空间:
#2.9w个样本在factor=2, min_resource = 100的情况下可以迭代多久?
for i in range(100):
if 100*2**i > 29062:
break
print(i+1,100*2**i)
#1536种参数组合在factor=2的情况下可以迭代多久?
for i in range(100):
if 1536//2**i < 1:
break
print(i+1,int(1536//2**i+1)) #向上取整
不难发现,当factor=2的时候,数据集不足的条件会先被触发,最多只能迭代9次。也就是说,最终我们将在7组参数中选择表现最好的一组参数,而不会一直让搜索持续直到找出唯一最优的参数。如果我们无论如何都希望能够找到唯一最后的参数,那我们可以使用下面的参数:
输入布尔值,默认False。当数据总样本量较小,不足以支撑循环直到只剩下最后一组备选参数时,可以打开该参数。
参数设置为True时,会重复使用首次迭代时的样本量,直到剩下的数据足以支撑样本量的增加直到只剩下最后一组备选参数
参数设置为False时,以全部样本被用完作为搜索结束的指标
对于对半网格搜索应用来说,最困难的部分就是决定搜索本身复杂的参数组合。在调参时,如果我们希望参数空间中的备选组合都能够被充分验证,则迭代次数不能太少(例如,只迭代3次),因此factor不能太大。但如果factor太小,又会加大迭代次数,同时拉长整个搜索的运行时间。同时,迭代次数还会影响我们最终能够使用的数据量,以及迭代完毕之后我们还需进一步验证的参数组合数量,两者都不能太少。因此,我们一般在使用对半网格搜索时,需考虑以下三个点:
1、min_resources的值不能太小,且在全部迭代过程结束之前,我们希望使用尽量多的数据
2、迭代完毕之后,剩余的验证参数组合不能太多,10以下最佳,如果无法实现,则30以下也可以接受
3、迭代次数不能太多,否则时间可能会太长
import re
import sklearn
import numpy as np
import pandas as pd
import matplotlib as mlp
import matplotlib.pyplot as plt
import time
from sklearn.ensemble import RandomForestRegressor as RFR
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import KFold, HalvingGridSearchCV, cross_validate, RandomizedSearchCV
param_grid_simple = {"criterion": ["squared_error","poisson"]
, 'n_estimators': [*range(20,100,5)]
, 'max_depth': [*range(10,25,2)]
, "max_features": ["log2","sqrt",16,32,64,"auto"]
, "min_impurity_decrease": [*np.arange(0,5,10)]
}
factor = 1.5
n_samples = X.shape[0]
min_resources = 500
space = 1536
for i in range(100):
if (min_resources*factor**i > n_samples) or (space/factor**i < 1):
break
print(i+1,"本轮迭代样本:{}".format(min_resources*factor**i)
,"本轮验证参数组合:{}".format(space//factor**i + 1))
#建立回归器、交叉验证
reg = RFR(random_state=1412,verbose=True,n_jobs=-1)
cv = KFold(n_splits=5,shuffle=True,random_state=1412)
#定义对半搜索
search = HalvingGridSearchCV(estimator=reg
,param_grid=param_grid_simple
,factor=1.5
,min_resources=500
,scoring = "neg_mean_squared_error"
,verbose = True
,random_state=1412
,cv = cv
,n_jobs=-1)
#训练对半搜索评估器
#=====【TIME WARNING: 30~50min】=====#
start = time.time()
search.fit(X,y)
end = time.time() - start
print(end/60)
#查看最佳评估器
search.best_estimator_
#查看最佳评估器
abs(search.best_score_)**0.5
#验证最佳参数组合的效力
rebuild_on_best_param(search.best_estimator_)
以随机网格搜索作为对比,我们来看看随机网格搜索的结果:
param_grid_simple = {"criterion": ["squared_error","poisson"]
, 'n_estimators': [*range(20,100,5)]
, 'max_depth': [*range(10,25,2)]
, "max_features": ["log2","sqrt",16,32,64,"auto"]
, "min_impurity_decrease": [*np.arange(0,5,10)]
}
reg = RFR(random_state=1412,verbose=True,n_jobs=-1)
cv = KFold(n_splits=5,shuffle=True,random_state=1412)
#定义随机搜索
search = RandomizedSearchCV(estimator=reg
,param_distributions=param_grid_simple
,n_iter = 800 #使用全域空间的一半作为子空间
,scoring = "neg_mean_squared_error"
,verbose = True
,random_state=1412
,cv = cv
,n_jobs=-1)
#训练随机搜索评估器
#=====【TIME WARNING: 1个半小时~2小时】=====#
start = time.time()
search.fit(X,y)
end = time.time()-start
print(end/60)
#查看最佳评估器
search.best_estimator_
#查看最终评估指标
abs(search.best_score_)**0.5
#验证最佳参数组合的效力
rebuild_on_best_param(search.best_estimator_)
HPO方法 | 随机搜索 | 对半搜索 |
---|---|---|
搜索空间/全域空间 | 800/1536 | 1536/1536 |
运行时间(分钟) | 103.20 | 25.638(↓) |
搜索最优(RMSE) | 1055.555 | 1068.281 |
重建最优(RMSE) | 1054.359 | 1082.916 |
可以看到,随机网格搜索的结果略微占优,但能够尝试的参数组合只有800个,且耗费的时间是对半搜索的4倍(1小时45分钟)。对于对半搜索,我们可以继续精细化调整整体的参数空间,进一步寻找更优的参数,但面对上万样本量的数据集,随机搜索的运算速度不足以支撑精细化调参,就更别提网格搜索了。之后,我们会给大家更详细地讲解更快速、更高效的优化方法。