XGBoost的参数空间与超参数优化
XGBoost的参数空间与超参数优化
1 确定XGBoost优化的参数空间
丰富的超参数为集成算法提供了无限的可能,以降低偏差为目的的Boosting算法们在调参之后的表现更是所向披靡,因此XGBoost的超参数自动优化也是一个重要的课题。在过去的课程当中我们已经讲解过大量关于树模型参数影响力的内容,因此在阅读本章之前,强烈建议学习GBDT课程当中超参数空间相关的内容。属于GBDT的参数空间会极大程度地帮助你理解XGBoost的参数空间。
对任意集成算法进行超参数优化之前,我们需要明确两个基本事实:
1、不同参数对算法结果的影响力大小
2、确定用于搜索的参数空间
对XGBoost来说,我们可以大致如下排列各个参数对算法的影响:
影响力 | 参数 |
---|---|
⭐⭐⭐⭐⭐ 几乎总是具有巨大影响力 |
num_boost_round(整体学习能力) eta(整体学习速率) |
⭐⭐⭐⭐ 大部分时候具有影响力 |
booster(整体学习能力) colsample_by*(随机性) gamma(结构风险 + 精剪枝) lambda(结构风险 + 间接剪枝) min_child_weight(精剪枝) |
⭐⭐ 可能有大影响力 大部分时候影响力不明显 |
max_depth(粗剪枝) alpha(结构风险 + 精剪枝) subsamples(随机性) objective(整体学习能力) scale_pos_weight(样本不均衡) |
⭐ 当数据量足够大时,几乎无影响 |
seed base_score(初始化) |
比起其他树的集成算法,XGBoost有大量通过影响建树过程而影响整体模型的参数(比如gamma
,lambda
等)。这些参数以较为复杂的方式共同作用、影响模型的最终结果,因此他们的影响力不是线性的,也不总是能在调参过程中明显地展露出来,但调节这些参数大多数时候都能对模型有影响,因此大部分与结构风险相关的参数都被评为4星参数了。相对的,对XGBoost来说总是具有巨大影响力的参数就只有迭代次数与学习率了。
在上述影响力排名当中,需要特别说明以下几点:
-
在随机森林中影响力巨大的
max_depth
在XGBoost中默认值为6,比GBDT中的调参空间略大,但还是没有太多的空间,因此影响力不足。 -
在GBDT中影响力巨大的
max_features
对标XGBoost中的colsample_by*
系列参数,原则上来说影响力应该非常大,但由于三个参数共同作用,调参难度较高,在只有1个参数作用时效果略逊于max_features
。 -
精剪枝参数往往不会对模型有太大的影响,但在XGBoost当中,
min_child_weight
与结构分数的计算略微相关,因此有时候会展现出较大的影响力。故而将这个精剪枝参数设置为4星参数。 -
类似于
objective
这样影响整体学习能力的参数一般都有较大的影响力,但XGBoost当中每种任务可选的损失函数不多,因此一般损失函数不在调参范围之内,故认为该参数的影响力不明显。 -
XGBoost的初始化分数只能是数字,因此当迭代次数足够多、数据量足够大时,起点的影响会越来越小。因此我们一般不会对base_score进行调参。
那在调参的时候,我们应该选择哪些参数呢?与其他树模型一样,我们首先会考虑所有影响力巨大的参数(5星参数),当算力足够/优化算法运行较快的时候,我们可以考虑将大部分时候具有影响力的参数(4星)也都加入参数空间。一般来说,只要样本量足够,我们还是愿意尝试subsample
以及max_depth
,如果算力充足,我们还可以加入obejctive
这样或许会有效的参数。
需要说明的是,一般不会同时使用三个colsample_by*
参数、更不会同时调试三个colsample_by*
参数。首先,参数colsample_bylevel
较为不稳定,不容易把握,因此当训练资源充足时,会同时调整colsample_bytree
和colsample_bynode
。如果计算资源不足,或者优先考虑节约计算时间,则会先选择其中一个参数、尝试将特征量控制在一定范围内来建树,并观察模型的结果。在这三个参数中,使用bynode
在分枝前随机,比使用bytree
建树前随机更能带来多样性、更能对抗过拟合,但同时也可能严重地伤害模型的学习能力。在这里,我将尝试同时使用两个参数进行调参。
在这样的基本思想下,再结合硬件与运行时间因素,我将选择如下参数进行调整,并使用基于TPE贝叶斯优化(HyperOpt)对XGBoost进行优化——
参数 |
---|
num_boost_round |
eta |
booster |
colsample_bynode |
colsample_bytree |
gamma |
lambda |
min_child_weight |
max_depth |
subsamples |
objective |
在此基础上,我们需要进一步确认参数空间:
- 对于有界的参数(比如
colsample_bynode
,subsamples
等),或者有固定选项的参数(比如booster
,objective
),无需确认参数空间。 - 对取值较小的参数(例如学习率
eta
,一般树模型的min_impurity_decrease
等),或者通常会向下调整的参数(比如max_depth
),一般是围绕默认值向两边展开构建参数空间。 - 对于取值可大可小,且原则上可取到无穷值的参数(
num_boost_round
,gamma
、lambda
、min_child_weight
等),一般需要绘制学习曲线进行提前探索,或者也可以设置广而稀的参数空间,来一步步缩小范围。
在之前的课程当中,我们已经对gamma
和lambda
的范围进行过探索,其中lambda
范围[1,2]之间对模型有影响,而gamma
在[1e6,1e7]之间才对模型有影响。因此我们可以先规定lambda
的参数空间为np.arange(0,3,0.2),并规定gamma
的参数空间为np.arange(1e6,1e7,1e6)。现在我们对剩下2个参数绘制学习曲线进行轻度探索。如下所示:
1 | import xgboost as xgb |
- num_boost_round
一般迭代次数默认为100次,因此我们通常会主动在300或者500以内进行尝试:
1 | train = [] |
1 | plt.plot(option,train); |
1 | plt.plot(option,overfit); |
可以看到,num_boost_round
大约增长到50左右就不再对模型有显著影响了,我们可以进一步来查看分数到30000以下之后的情况:
1 | plt.plot(option,test) |
100棵树之后损失几乎没有再下降,因此num_boost_round
的范围可以定到range(50,200,10)。
- min_child_weight
作为值之和,min_child_weight
的真实值是可以计算出来的,但精确的计算需要跟随xgboost建树的过程运行,因此比较麻烦。遗憾的是,xgboost官方并未提供调用树结构以及值的接口,因此最佳方案其实是对每个叶子上的样本量进行估计。
1 | X.shape |
(1460, 80)
现在总共有样本1460个,在五折交叉验证中训练集共有1460*0.8 = 1168个样本。由于CART树是二叉树,我们规定的最大深度为5,因此最多有个叶子节点,平均每个叶子结点上的样本量大概为1168/32 = 36.5个。粗略估计,如果min_child_weight
是一个小于36.5的值,就可能对模型造成巨大影响。当然,不排除有大量样本集中在一片叶子上的情况,因此我们可以设置备选范围稍微放大,例如设置为[0,100]来观察模型的结果。
1 | train = [] |
1 | plt.plot(option,train); |
1 | plt.plot(option,overfit); |
很明显,min_child_weight
在0~40的范围之内对测试集上的交叉验证损失有较好的抑制作用,因此我们可以将min_child_weight
的调参空间设置为range(0,50,2)来进行调参。
如此,全部参数的参数空间就确定了,如下所示:
参数 | 范围 |
---|---|
num_boost_round |
学习曲线探索,最后定为 (50,200,10) |
eta |
以0.3为中心向两边延展,最后定为 (0.05,2.05,0.05) |
booster |
两种选项 [“gbtree”,“dart”] |
colsample_bytree |
设置为(0,1]之间的值,但由于还有参数bynode ,因此整体不宜定得太小,因此定为(0.3,1,0.1) |
colsample_bynode |
设置为(0,1]之间的值,定为 (0.1,1,0.1) |
gamma |
学习曲线探索,有较大可能需要改变,定为 (1e6,1e7,1e6) |
lambda |
学习曲线探索,定为 (0,3,0.2) |
min_child_weight |
学习曲线探索,定为 (0,50,2) |
max_depth |
以6为中心向两边延展,右侧范围定得更大 (2,30,2) |
subsample |
设置为(0,1]之间的值,定为 (0.1,1,0.1) |
objective |
两种回归类模型的评估指标 [“reg:squarederror”, “reg:squaredlogerror”] |
rate_drop |
如果选择"dart"树所需要补充的参数,设置为(0,1]之间的值 (0.1,1,0.1) |
一般在初次搜索时,我们会设置范围较大、较为稀疏的参数空间,然后在多次搜索中逐渐缩小范围、降低参数空间的维度。不过这一次设置的参数空间都较为密集,参数也较多,大家在实际进行设置的时候可以选择与我设置的不同的范围或密度。
2 基于TEP对XGBoost进行优化
1 | #日常使用库与算法 |
1 | data = pd.read_csv(r"D:\Pythonwork\2021ML\PART 2 Ensembles\datasets\House Price\train_encode.csv",index_col=0) |
1 | X = data.iloc[:,:-1] |
1 | X.shape |
(1460, 80)
Step 1.建立benchmark
算法 | RF (TPE) |
AdaBoost (TPE) |
GBDT (TPE) |
---|---|---|---|
5折验证 运行时间 |
0.22s | 0.27s | 1.54s(↑) |
测试最优分数 (RMSE) |
28346.673 | 35169.730 | 26415.835(↓) |
Step 2.定义目标函数、参数空间、优化函数、验证函数
目标函数
1 | def hyperopt_objective(params): |
参数空间
1 | param_grid_simple = {'num_boost_round': hp.quniform("num_boost_round",50,200,10) |
优化函数
1 | def param_hyperopt(max_evals=100): |
Step 3.训练贝叶斯优化器
XGBoost中涉及到前所未有多的随机性,因此模型可能表现得极度不稳定,我们需要多尝试几次贝叶斯优化来观察模型的稳定性。因此在这里我们完成了5次贝叶斯优化,查看如下的结果:
1 | params_best, trials = param_hyperopt(100) #由于参数空间巨大,给与100次迭代的空间 |
38%|██████████████████▏ | 38/100 [05:16<08:36, 8.33s/trial, best loss: 26753.27408833333]
best params: {'booster': 1, 'colsample_bynode': 0.8, 'colsample_bytree': 0.8, 'eta': 1.55, 'gamma': 2000000.0, 'lambda': 0.2, 'max_depth': 2, 'min_child_weight': 0.0, 'num_boost_round': 200.0, 'objective': 0, 'rate_drop': 0.2, 'subsample': 0.9}
在迭代的早期就因提前停止而停下了,修改提前停止的容忍次数为30次,继续尝试:
1 | params_best, trials = param_hyperopt(100) |
57%|██████████████████████████▊ | 57/100 [05:43<04:18, 6.02s/trial, best loss: 26775.553385333333]
best params: {'booster': 1, 'colsample_bynode': 0.5, 'colsample_bytree': 1.0, 'eta': 0.5, 'gamma': 10000000.0, 'lambda': 1.6, 'max_depth': 2, 'min_child_weight': 0.0, 'num_boost_round': 110.0, 'objective': 0, 'rate_drop': 0.1, 'subsample': 0.7000000000000001}
1 | params_best, trials = param_hyperopt(100) |
41%|███████████████████▎ | 41/100 [02:07<03:03, 3.11s/trial, best loss: 27363.138020666665]
best params: {'booster': 0, 'colsample_bynode': 1.0, 'colsample_bytree': 0.5, 'eta': 0.05, 'gamma': 7000000.0, 'lambda': 2.4000000000000004, 'max_depth': 9, 'min_child_weight': 0.0, 'num_boost_round': 170.0, 'objective': 0, 'rate_drop': 0.7000000000000001, 'subsample': 0.6000000000000001}
1 | params_best, trials = param_hyperopt(100) |
52%|████████████████████████▍ | 52/100 [05:26<05:01, 6.29s/trial, best loss: 27745.835937666667]
best params: {'booster': 1, 'colsample_bynode': 0.30000000000000004, 'colsample_bytree': 1.0, 'eta': 2.0, 'gamma': 7000000.0, 'lambda': 0.0, 'max_depth': 8, 'min_child_weight': 2.0, 'num_boost_round': 110.0, 'objective': 0, 'rate_drop': 0.4, 'subsample': 0.7000000000000001}
1 | params_best, trials = param_hyperopt(100) |
32%|███████████████ | 32/100 [05:21<11:23, 10.05s/trial, best loss: 26803.143880333333]
best params: {'booster': 1, 'colsample_bynode': 0.9, 'colsample_bytree': 0.4, 'eta': 1.3, 'gamma': 9000000.0, 'lambda': 1.2000000000000002, 'max_depth': 8, 'min_child_weight': 4.0, 'num_boost_round': 180.0, 'objective': 0, 'rate_drop': 0.1, 'subsample': 1.0}
这个结果差强人意,比起GBDT跑出的最佳分数还有距离,但我们可以在此基础上继续调整参数空间。以下是5次调参的结果总结:
1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|
num_boost_round | 200 | 110 | 170 | 110 | 180 |
booster | 1 | 1 | 0 | 1 | 1 |
objective | 0 | 0 | 0 | 0 | 0 |
colsample_bynode | 0.8 | 0.5 | 1.0 | 0.3 | 0.9 |
colsample_bytree | 0.8 | 1.0 | 0.5 | 1.0 | 0.4 |
eta | 1.55 | 0.5 | 0.05 | 1.0 | 1.3 |
gamma | 2e6 | 1e7 | 7e6 | 7e6 | 9e6 |
lambda | 0.2 | 1.6 | 2.4 | 0 | 1.2 |
max_depth | 2 | 2 | 9 | 8 | 8 |
min_child_weight | 0 | 0 | 0 | 2 | 4 |
rate_drop | 0.2 | 0.7 | 0.7 | 0.4 | 0.1 |
subsample | 0.9 | 0.7 | 0.6 | 0.7 | 1 |
首先,objective
在所有迭代中都被选为"reg:squarederror",这也是xgboost的默认值,因此不再对该参数进行搜索。同样的。booster
参数在5次运行中有4次被选为"dart",因此基本可以确认对目前的数据使用DART树是更好的选择。同时在参考结果时我们就可以不太考虑第三次搜索的结果,因为第三次搜索是给予普通gbtree给出的结果。
对于其他参数,我们则根据搜索结果修改空间范围、增加空间密度,一般让范围向选中更多的一边倾斜,并且减小步长。例如num_boost_round
从来没有选到100以下的值,还有一次触顶,两次接近上限,因此可以将原本的范围(50,200,10)修改为(100,300,10)。colsample_bynode
的结果均匀地分布在0.3~1之间,可以考虑不更换范围,但缩小步长。colsample_bytree
的结果更多偏向于1.0,因此可以考虑提升下限。其他的参数也以此类推:
1 | param_grid_simple = {'num_boost_round': hp.quniform("num_boost_round",100,300,10) |
1 | def hyperopt_objective(params): |
Step 4.在修改后的参数空间上,继续训练贝叶斯优化器
1 | params_best, trials = param_hyperopt(100) #提前停止的容忍次数提高为50次 |
29%|█████████████▉ | 29/100 [00:53<02:00, 1.69s/trial, best loss: 27279.83528666667]
D:\ProgramData\Anaconda3\lib\site-packages\numpy\core\_methods.py:230: RuntimeWarning: invalid value encountered in subtract
x = asanyarray(arr - arrmean)
100%|██████████████████████████████████████████████| 100/100 [02:33<00:00, 1.53s/trial, best loss: 25662.024739666667]
best params: {'colsample_bynode': 0.45, 'colsample_bytree': 1.0, 'eta': 0.05, 'gamma': 13000000.0, 'lambda': 0.5, 'max_depth': 6, 'min_child_weight': 0.5, 'num_boost_round': 150.0, 'rate_drop': 0.65, 'subsample': 0.8500000000000001}
1 | params_best, trials = param_hyperopt(100) |
100%|██████████████████████████████████████████████| 100/100 [02:38<00:00, 1.59s/trial, best loss: 25711.822916666668]
best params: {'colsample_bynode': 0.7000000000000001, 'colsample_bytree': 0.75, 'eta': 0.1, 'gamma': 13500000.0, 'lambda': 1.8, 'max_depth': 3, 'min_child_weight': 1.5, 'num_boost_round': 210.0, 'rate_drop': 0.7000000000000001, 'subsample': 1.0}
1 | params_best, trials = param_hyperopt(100) |
58%|███████████████████████████▎ | 58/100 [01:33<01:07, 1.62s/trial, best loss: 25737.109375333337]
best params: {'colsample_bynode': 0.7000000000000001, 'colsample_bytree': 0.9500000000000001, 'eta': 0.05, 'gamma': 8000000.0, 'lambda': 0.8, 'max_depth': 9, 'min_child_weight': 1.0, 'num_boost_round': 160.0, 'rate_drop': 0.30000000000000004, 'subsample': 0.8}
1 | params_best, trials = param_hyperopt(100) |
33%|███████████████▌ | 33/100 [01:01<02:04, 1.85s/trial, best loss: 26307.809244666667]
best params: {'colsample_bynode': 0.7000000000000001, 'colsample_bytree': 0.7000000000000001, 'eta': 0.05, 'gamma': 8000000.0, 'lambda': 1.8, 'max_depth': 3, 'min_child_weight': 1.5, 'num_boost_round': 270.0, 'rate_drop': 0.6000000000000001, 'subsample': 0.8500000000000001}
1 | params_best, trials = param_hyperopt(100) |
100%|████████████████████████████████████████████████████| 100/100 [02:49<00:00, 1.69s/trial, best loss: 26049.842448]
best params: {'colsample_bynode': 0.5, 'colsample_bytree': 0.65, 'eta': 0.05, 'gamma': 12500000.0, 'lambda': 0.2, 'max_depth': 4, 'min_child_weight': 1.0, 'num_boost_round': 220.0, 'rate_drop': 0.5, 'subsample': 0.55}
我们在经过调整后的参数空间上进行了5次搜索,其中得到的最糟糕的成绩是26307.809,但这已经是一个超越GBDT的分数。在5次搜索当中,我们得到的最佳分数是25662.024。现在我们可以尝试在验证函数上验证这一组参数:
Step 5.验证参数
验证函数
1 | def hyperopt_validation(params): |
1 | bestparams = {'colsample_bynode': 0.45 |
1 | start = time.time() |
25368.487630333333
1 | end = (time.time() - start) |
1.1478571891784668
算法 | RF (TPE) |
AdaBoost (TPE) |
GBDT (TPE) |
XGB (TPE) |
---|---|---|---|---|
5折验证 运行时间 |
0.22s | 0.27s | 1.54s(↑) | 1.14s(↓) |
测试最优分数 (RMSE) |
28346.673 | 35169.730 | 26415.835(↓) | 25368.487(↓) |
从参数的密度来看,我们还可以继续提升我们的分数,但课时有限,我们就不再继续调整了,大家可以顺着相似的思路继续往下调整,查看XGBoost是否还有更大的空间。