原文:
annas-archive.org/md5/8042b1d609c03cc86db1c68794ab294c
译者:飞龙
第四章:第四章:从梯度提升到 XGBoost
XGBoost 是一种独特的梯度提升形式,具有多个显著的优势,这些优势将在第五章,《XGBoost 揭示》中进行解释。为了理解 XGBoost 相较于传统梯度提升的优势,您必须首先了解传统梯度提升是如何工作的。XGBoost 融入了传统梯度提升的结构和超参数。在本章中,您将发现梯度提升的强大能力,而这正是 XGBoost 的核心所在。
在本章中,您将从零开始构建梯度提升模型,并与之前的结果对比梯度提升模型和错误。特别地,您将专注于学习率超参数,构建强大的梯度提升模型,其中包括 XGBoost。最后,您将预览一个关于外行星的案例研究,强调对更快算法的需求,这种需求在大数据领域中至关重要,而 XGBoost 正好满足了这一需求。
在本章中,我们将覆盖以下主要主题:
-
从袋装法到提升法
-
梯度提升的工作原理
-
修改梯度提升的超参数
-
面对大数据的挑战——梯度提升与 XGBoost 的对比
技术要求
从袋装法到提升法
在第三章,《随机森林的袋装法》中,您学习了为什么像随机森林这样的集成机器学习算法通过将多个机器学习模型结合成一个,从而做出更好的预测。随机森林被归类为袋装算法,因为它们使用自助法样本的聚合(决策树)。
相比之下,提升方法通过学习每棵树的错误来进行优化。其一般思路是基于前一棵树的错误来调整新树。
在提升方法(boosting)中,每棵新树的错误修正是与袋装法(bagging)不同的。在袋装模型中,新树不会关注之前的树。此外,新树是通过自助法(bootstrapping)从零开始构建的,最终的模型将所有单独的树进行聚合。然而,在提升方法中,每棵新树都是基于前一棵树构建的。这些树并不是孤立运作的,而是相互叠加构建的。
介绍 AdaBoost
AdaBoost是最早且最受欢迎的提升模型之一。在 AdaBoost 中,每一棵新树都会根据前一棵树的错误来调整权重。通过调整权重,更多地关注预测错误的样本,给予这些样本更高的权重。通过从错误中学习,AdaBoost 能够将弱学习者转变为强学习者。弱学习者是指那些表现几乎与随机猜测一样的机器学习算法。相比之下,强学习者则从数据中学习到了大量信息,表现优异。
提升算法背后的基本思想是将弱学习者转变为强学习者。弱学习者几乎不比随机猜测更好。但这种弱开始是有目的的。基于这一基本思想,提升通过集中精力进行迭代的错误修正来工作,而不是建立一个强大的基准模型。如果基准模型太强,学习过程就会受到限制,从而削弱提升模型背后的整体策略。
通过数百次迭代,弱学习者被转化为强学习者。从这个意义上来说,微小的优势可以带来很大不同。事实上,在过去几十年里,提升方法一直是产生最佳结果的机器学习策略之一。
本书不深入研究 AdaBoost 的细节。像许多 scikit-learn 模型一样,在实践中实现 AdaBoost 非常简单。AdaBoostRegressor
和AdaBoostClassifier
算法可以从sklearn.ensemble
库中下载,并适用于任何训练集。最重要的 AdaBoost 超参数是n_estimators
,即创建强学习者所需的树的数量(迭代次数)。
注意
有关 AdaBoost 的更多信息,请查阅官方文档:scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostClassifier.html
(分类器)和scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostRegressor.html
(回归器)。
我们现在将介绍梯度提升,它是 AdaBoost 的强有力替代方案,在性能上略有优势。
区分梯度提升
梯度提升采用与 AdaBoost 不同的方法。虽然梯度提升也基于错误的预测进行调整,但它更进一步:梯度提升根据前一棵树的预测错误完全拟合每一棵新树。也就是说,对于每一棵新树,梯度提升首先查看错误,然后围绕这些错误完全构建一棵新树。新树不会关心那些已经正确的预测。
构建一个仅关注错误的机器学习算法需要一种全面的方法,累加错误以做出准确的最终预测。这种方法利用了残差,即模型预测值与实际值之间的差异。其基本思想如下:
梯度提升计算每棵树预测的残差,并将所有残差加总来评估模型。
理解计算和累加残差至关重要,因为这个思想是 XGBoost(梯度提升的高级版本)核心原理之一。当你构建自己的梯度提升版本时,计算和累加残差的过程会变得更加清晰。在下一节中,你将构建自己的梯度提升模型。首先,让我们详细了解梯度提升的工作原理。
梯度提升的工作原理
在本节中,我们将深入了解梯度提升的内部原理,通过对前一棵树的错误训练新树,从零开始构建一个梯度提升模型。这里的核心数学思想是残差。接下来,我们将使用 scikit-learn 的梯度提升算法获得相同的结果。
残差
残差是给定模型的预测与实际值之间的差异。在统计学中,残差常常被分析,以判断线性回归模型与数据的拟合程度。
请考虑以下示例:
-
自行车租赁
a) 预测: 759
b) 结果: 799
c) 残差: 799 - 759 = 40
-
收入
a) 预测: 100,000
b) 结果: 88,000
c) 残差: 88,000 – 100,000 = -12,000
正如你所看到的,残差告诉你模型的预测与实际之间的偏差,残差可能是正的,也可能是负的。
下面是一个展示线性回归线的残差的可视化示例:
图 4.1 – 线性回归线的残差
线性回归的目标是最小化残差的平方。正如图表所示,残差的可视化展示了线性拟合数据的效果。在统计学中,线性回归分析通常通过绘制残差图来深入了解数据。
为了从零开始构建一个梯度提升算法,我们将计算每棵树的残差,并对残差拟合一个新模型。现在我们开始吧。
学习如何从零开始构建梯度提升模型
从零开始构建梯度提升模型将帮助你更深入理解梯度提升在代码中的工作原理。在构建模型之前,我们需要访问数据并为机器学习做准备。
处理自行车租赁数据集
我们继续使用自行车租赁数据集,比较新模型与旧模型的表现:
-
我们将从导入
pandas
和numpy
开始,并添加一行代码来关闭任何警告:import pandas as pd import numpy as np import warnings warnings.filterwarnings('ignore')
-
现在,加载
bike_rentals_cleaned
数据集并查看前五行:df_bikes = pd.read_csv('bike_rentals_cleaned.csv') df_bikes.head()
你的输出应如下所示:
图 4.2 – 自行车租赁数据集的前五行
-
现在,将数据拆分为
X
和y
。然后,将X
和y
拆分为训练集和测试集:X_bikes = df_bikes.iloc[:,:-1] y_bikes = df_bikes.iloc[:,-1] from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X_bikes, y_bikes, random_state=2)
现在是时候从头开始构建梯度提升模型了!
从头开始构建梯度提升模型
以下是从头开始构建梯度提升机器学习模型的步骤:
-
将数据拟合到决策树:你可以使用决策树桩,其
max_depth
值为1
,或者使用深度为2
或3
的决策树。初始决策树被称为max_depth=2
,并将其拟合到训练集上,作为tree_1
,因为它是我们集成中的第一棵树:from sklearn.tree import DecisionTreeRegressor tree_1 = DecisionTreeRegressor(max_depth=2, random_state=2) tree_1.fit(X_train, y_train)
-
使用训练集进行预测:与使用测试集进行预测不同,梯度提升法中的预测最初是使用训练集进行的。为什么?因为要计算残差,我们需要在训练阶段比较预测结果。模型构建的测试阶段是在所有树构建完毕后才会进行。第一轮的训练集预测结果是通过将
predict
方法应用到tree_1
,并使用X_train
作为输入来得到的:y_train_pred = tree_1.predict(X_train)
-
计算残差:残差是预测值与目标列之间的差异。将
X_train
的预测值,定义为y_train_pred
,从y_train
目标列中减去,来计算残差:y2_train = y_train - y_train_pred
注意
残差定义为
y2_train
,因为它是下一棵树的目标列。 -
将新树拟合到残差上:将新树拟合到残差上与将模型拟合到训练集上有所不同。主要的区别在于预测。对于自行车租赁数据集,在将新树拟合到残差上时,我们应逐渐得到更小的数字。
初始化一棵新树,并将其拟合到
X_train
和残差y2_train
上:tree_2 = DecisionTreeRegressor(max_depth=2, random_state=2) tree_2.fit(X_train, y2_train)
-
重复步骤 2-4:随着过程的进行,残差应逐渐从正向和负向逼近
0
。迭代会持续进行,直到达到估计器的数量n_estimators
。让我们重复第三棵树的过程,如下所示:
y2_train_pred = tree_2.predict(X_train) y3_train = y2_train - y2_train_pred tree_3 = DecisionTreeRegressor(max_depth=2, random_state=2) tree_3.fit(X_train, y3_train)
这个过程可能会持续几十棵、几百棵甚至几千棵树。在正常情况下,你当然会继续进行。要将一个弱学习器转变为强学习器,肯定需要更多的树。然而,由于我们的目标是理解梯度提升背后的工作原理,因此在一般概念已经覆盖的情况下,我们将继续前进。
-
汇总结果:汇总结果需要为每棵树使用测试集进行预测,如下所示:
y1_pred = tree_1.predict(X_test) y2_pred = tree_2.predict(X_test) y3_pred = tree_3.predict(X_test)
由于预测值是正负差异,将预测值进行汇总应能得到更接近目标列的预测结果,如下所示:
y_pred = y1_pred + y2_pred + y3_pred
-
最后,我们计算均方误差(MSE)来获得结果,如下所示:
from sklearn.metrics import mean_squared_error as MSE MSE(y_test, y_pred)**0.5
以下是预期的输出:
911.0479538776444
对于一个尚未强大的弱学习器来说,这样的表现还不错!现在,让我们尝试使用 scikit-learn 获得相同的结果。
在 scikit-learn 中构建梯度提升模型
让我们看看能否通过调整一些超参数,使用 scikit-learn 的GradientBoostingRegressor
得到与前一节相同的结果。使用GradientBoostingRegressor
的好处是,它构建得更快,且实现起来更简单:
-
首先,从
sklearn.ensemble
库中导入回归器:from sklearn.ensemble import GradientBoostingRegressor
-
在初始化
GradientBoostingRegressor
时,有几个重要的超参数。为了获得相同的结果,必须将max_depth=2
和random_state=2
匹配。此外,由于只有三棵树,我们必须设置n_estimators=3
。最后,我们还必须设置learning_rate=1.0
超参数。稍后我们会详细讨论learning_rate
:gbr = GradientBoostingRegressor(max_depth=2, n_estimators=3, random_state=2, learning_rate=1.0)
-
现在模型已经初始化,可以在训练数据上进行拟合,并在测试数据上进行评分:
gbr.fit(X_train, y_train) y_pred = gbr.predict(X_test) MSE(y_test, y_pred)**0.5
结果如下:
911.0479538776439
结果在小数点后 11 位都是相同的!
回顾一下,梯度提升的关键是构建一个足够多的树的模型,将弱学习器转变为强学习器。通过将
n_estimators
(迭代次数)设置为一个更大的数字,这可以很容易地实现。 -
让我们构建并评分一个拥有 30 个估算器的梯度提升回归器:
gbr = GradientBoostingRegressor(max_depth=2, n_estimators=30, random_state=2, learning_rate=1.0) gbr.fit(X_train, y_train) y_pred = gbr.predict(X_test) MSE(y_test, y_pred)**0.5
结果如下:
857.1072323426944
得分有所提升。现在,让我们看看 300 个估算器的情况:
gbr = GradientBoostingRegressor(max_depth=2, n_estimators=300, random_state=2, learning_rate=1.0) gbr.fit(X_train, y_train) y_pred = gbr.predict(X_test) MSE(y_test, y_pred)**0.5
结果是这样的:
936.3617413678853
这真是个惊讶!得分变差了!我们是不是被误导了?梯度提升真如人们所说的那样有效吗?
每当得到一个意外的结果时,都值得仔细检查代码。现在,我们更改了learning_rate
,却没有详细说明。那么,如果我们移除learning_rate=1.0
,并使用 scikit-learn 的默认值会怎样呢?
让我们来看看:
gbr = GradientBoostingRegressor(max_depth=2, n_estimators=300, random_state=2)
gbr.fit(X_train, y_train)
y_pred = gbr.predict(X_test)
MSE(y_test, y_pred)**0.5
结果是这样的:
653.7456840231495
难以置信!通过使用 scikit-learn 对learning_rate
超参数的默认值,得分从936
变为654
。
在接下来的章节中,我们将深入学习不同的梯度提升超参数,重点关注learning_rate
超参数。
修改梯度提升超参数
在本节中,我们将重点关注learning_rate
,这是最重要的梯度提升超参数,可能唯一需要注意的是n_estimators
,即模型中的迭代次数或树的数量。我们还将调查一些树的超参数,以及subsample
,这会导致RandomizedSearchCV
,并将结果与 XGBoost 进行比较。
learning_rate
在上一节中,将GradientBoostingRegressor
的learning_rate
值从1.0
更改为 scikit-learn 的默认值0.1
,得到了巨大的提升。
learning_rate
,也称为收缩率,会缩小单棵树的贡献,以确保在构建模型时没有一棵树的影响过大。如果整个集成模型是通过一个基础学习器的误差构建的,而没有仔细调整超参数,模型中的早期树可能会对后续的发展产生过大的影响。learning_rate
限制了单棵树的影响。通常来说,随着n_estimators
(树的数量)的增加,learning_rate
应该减小。
确定最优的learning_rate
值需要调整n_estimators
。首先,让我们保持n_estimators
不变,看看learning_rate
单独的表现。learning_rate
的取值范围是从0
到1
。learning_rate
值为1
意味着不做任何调整。默认值0.1
表示树的影响权重为 10%。
这里是一个合理的起始范围:
learning_rate_values = [0.001, 0.01, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5, 1.0]
接下来,我们将通过构建和评分新的GradientBoostingRegressor
来遍历这些值,看看得分如何比较:
for value in learning_rate_values:
gbr = GradientBoostingRegressor(max_depth=2, n_estimators=300, random_state=2, learning_rate=value)
gbr.fit(X_train, y_train)
y_pred = gbr.predict(X_test)
rmse = MSE(y_test, y_pred)**0.5
print('Learning Rate:', value, ', Score:', rmse)
学习率的值和得分如下:
Learning Rate: 0.001 , Score: 1633.0261400367258
Learning Rate: 0.01 , Score: 831.5430182728547
Learning Rate: 0.05 , Score: 685.0192988749717
Learning Rate: 0.1 , Score: 653.7456840231495
Learning Rate: 0.15 , Score: 687.666134269379
Learning Rate: 0.2 , Score: 664.312804425697
Learning Rate: 0.3 , Score: 689.4190385930236
Learning Rate: 0.5 , Score: 693.8856905068778
Learning Rate: 1.0 , Score: 936.3617413678853
从输出中可以看出,默认的learning_rate
值为0.1
时,300 棵树的得分最好。
现在让我们调整n_estimators
。使用前面的代码,我们可以生成learning_rate
图,其中n_estimators
为 30、300 和 3,000 棵树,如下图所示:
图 4.3 – 30 棵树的 learning_rate 图
如您所见,使用 30 棵树时,learning_rate
值在大约0.3
时达到峰值。
现在,让我们看一下 3,000 棵树的learning_rate
图:
图 4.4 – 3,000 棵树的 learning_rate 图
使用 3,000 棵树时,learning_rate
值在第二个值,即0.05
时达到峰值。
这些图表突显了调优learning_rate
和n_estimators
的重要性。
基础学习器
梯度提升回归器中的初始决策树被称为基础学习器,因为它是集成模型的基础。它是过程中的第一个学习器。这里的学习器一词表示一个弱学习器正在转变为强学习器。
尽管基础学习器不需要为准确性进行微调,正如在第二章《决策树深入解析》中所述,当然也可以通过调优基础学习器来提高准确性。
例如,我们可以选择max_depth
值为1
、2
、3
或4
,并比较结果如下:
depths = [None, 1, 2, 3, 4]
for depth in depths:
gbr = GradientBoostingRegressor(max_depth=depth, n_estimators=300, random_state=2)
gbr.fit(X_train, y_train)
y_pred = gbr.predict(X_test)
rmse = MSE(y_test, y_pred)**0.5
print('Max Depth:', depth, ', Score:', rmse)
结果如下:
Max Depth: None , Score: 867.9366621617327
Max Depth: 1 , Score: 707.8261886858736
Max Depth: 2 , Score: 653.7456840231495
Max Depth: 3 , Score: 646.4045923317708
Max Depth: 4 , Score: 663.048387855927
max_depth
值为3
时,得到最佳结果。
其他基础学习器的超参数,如在第二章《决策树深入解析》中所述,也可以采用类似的方式进行调整。
子样本(subsample)
subsample
是样本的一个子集。由于样本是行,子集意味着在构建每棵树时可能并不是所有的行都会被包含。当将subsample
从1.0
改为更小的小数时,树在构建阶段只会选择该百分比的样本。例如,subsample=0.8
会为每棵树选择 80%的样本。
继续使用max_depth=3
,我们尝试不同的subsample
百分比,以改善结果:
samples = [1, 0.9, 0.8, 0.7, 0.6, 0.5]
for sample in samples:
gbr = GradientBoostingRegressor(max_depth=3, n_estimators=300, subsample=sample, random_state=2)
gbr.fit(X_train, y_train)
y_pred = gbr.predict(X_test)
rmse = MSE(y_test, y_pred)**0.5
print('Subsample:', sample, ', Score:', rmse)
结果如下:
Subsample: 1 , Score: 646.4045923317708
Subsample: 0.9 , Score: 620.1819001443569
Subsample: 0.8 , Score: 617.2355650565677
Subsample: 0.7 , Score: 612.9879156983139
Subsample: 0.6 , Score: 622.6385116402317
Subsample: 0.5 , Score: 626.9974073227554
使用subsample
值为0.7
,300 棵树和max_depth
为3
时,获得了目前为止最佳的得分。
当subsample
不等于1.0
时,模型被归类为随机梯度下降,其中随机意味着模型中存在某些随机性。
随机搜索交叉验证(RandomizedSearchCV)
我们有一个良好的工作模型,但尚未进行网格搜索,如第二章《决策树深入分析》中所述。我们的初步分析表明,以max_depth=3
、subsample=0.7
、n_estimators=300
和learning_rate = 0.1
为中心进行网格搜索是一个不错的起点。我们已经展示了,当n_estimators
增加时,learning_rate
应当减少:
-
这是一个可能的起点:
params={'subsample':[0.65, 0.7, 0.75], 'n_estimators':[300, 500, 1000], 'learning_rate':[0.05, 0.075, 0.1]}
由于
n_estimators
从初始值 300 增加,learning_rate
从初始值0.1
减少。我们保持max_depth=3
以限制方差。在 27 种可能的超参数组合中,我们使用
RandomizedSearchCV
尝试其中的 10 种组合,希望找到一个好的模型。注意
尽管
GridSearchCV
可以实现 27 种组合,但最终你会遇到可能性过多的情况,RandomizedSearchCV
在此时变得至关重要。我们在这里使用RandomizedSearchCV
进行实践并加速计算。 -
让我们导入
RandomizedSearchCV
并初始化一个梯度提升模型:from sklearn.model_selection import RandomizedSearchCV gbr = GradientBoostingRegressor(max_depth=3, random_state=2)
-
接下来,初始化
RandomizedSearchCV
,以gbr
和params
作为输入,除了迭代次数、评分标准和折叠数。请记住,n_jobs=-1
可能加速计算,而random_state=2
确保结果的一致性:rand_reg = RandomizedSearchCV(gbr, params, n_iter=10, scoring='neg_mean_squared_error', cv=5, n_jobs=-1, random_state=2)
-
现在在训练集上拟合模型,并获取最佳参数和分数:
rand_reg.fit(X_train, y_train) best_model = rand_reg.best_estimator_ best_params = rand_reg.best_params_ print("Best params:", best_params) best_score = np.sqrt(-rand_reg.best_score_) print("Training score: {:.3f}".format(best_score)) y_pred = best_model.predict(X_test) rmse_test = MSE(y_test, y_pred)**0.5 print('Test set score: {:.3f}'.format(rmse_test))
结果如下:
Best params: {'learning_rate': 0.05, 'n_estimators': 300, 'subsample': 0.65} Training score: 636.200 Test set score: 625.985
从这里开始,值得逐个或成对地调整参数进行实验。尽管当前最好的模型有
n_estimators=300
,但通过谨慎调整learning_rate
,提高该超参数可能会得到更好的结果。subsample
也可以进行实验。 -
经过几轮实验后,我们得到了以下模型:
gbr = GradientBoostingRegressor(max_depth=3, n_estimators=1600, subsample=0.75, learning_rate=0.02, random_state=2) gbr.fit(X_train, y_train) y_pred = gbr.predict(X_test) MSE(y_test, y_pred)**0.5
结果如下:
596.9544588974487
在n_estimators
为1600
、learning_rate
为0.02
、subsample
为0.75
、max_depth
为3
时,我们得到了最佳得分597
。
可能还有更好的方法。我们鼓励你尝试!
现在,让我们看看 XGBoost 与梯度提升在使用迄今为止所涉及的相同超参数时有何不同。
XGBoost
XGBoost 是梯度提升的高级版本,具有相同的一般结构,这意味着它通过将树的残差求和,将弱学习器转化为强学习器。
上一节中的超参数唯一的不同之处是,XGBoost 将learning_rate
称为eta
。
让我们用相同的超参数构建一个 XGBoost 回归模型来比较结果。
从xgboost
导入XGBRegressor
,然后初始化并评分模型,代码如下:
from xgboost import XGBRegressor
xg_reg = XGBRegressor(max_depth=3, n_estimators=1600, eta=0.02, subsample=0.75, random_state=2)
xg_reg.fit(X_train, y_train)
y_pred = xg_reg.predict(X_test)
MSE(y_test, y_pred)**0.5
结果如下:
584.339544309016
分数更好。为什么分数更好将在下一章中揭示,第五章,XGBoost 揭秘。
准确性和速度是构建机器学习模型时最重要的两个概念,我们已经多次证明 XGBoost 非常准确。XGBoost 通常比梯度提升更受欢迎,因为它始终提供更好的结果,并且因为它更快,以下案例研究对此做出了证明。
面对大数据——梯度提升与 XGBoost 的对比
在现实世界中,数据集可能庞大,包含万亿个数据点。仅依靠一台计算机可能会因为资源有限而不利于工作。处理大数据时,通常使用云计算来利用并行计算机。
数据集之所以被认为是“大”,是因为它们突破了计算的极限。在本书至此为止,数据集的行数限制在数万行,列数不超过一百列,应该没有显著的时间延迟,除非你遇到错误(每个人都会发生)。
在本节中,我们将随时间考察系外行星。数据集包含 5,087 行和 3,189 列,记录了恒星生命周期不同阶段的光通量。将列和行相乘得到 150 万数据点。以 100 棵树为基准,我们需要 1.5 亿个数据点来构建模型。
在本节中,我的 2013 款 MacBook Air 等待时间大约为 5 分钟。新电脑应该会更快。我选择了系外行星数据集,以便等待时间对计算有显著影响,同时不会让你的计算机长时间占用。
介绍系外行星数据集
系外行星数据集来自 Kaggle,数据大约来自 2017 年:www.kaggle.com/keplersmachines/kepler-labelled-time-series-data
。该数据集包含关于恒星光的资料。每一行是一个单独的恒星,列展示了随时间变化的不同光模式。除了光模式外,如果恒星有系外行星,系外行星列标记为2
;否则标记为1
。
数据集记录了成千上万颗恒星的光通量。光通量,通常被称为光亮通量,是恒星的感知亮度。
注意
感知亮度与实际亮度不同。例如,一颗非常明亮但距离遥远的恒星,其光通量可能很小(看起来很暗),而像太阳这样距离很近的中等亮度恒星,其光通量可能很大(看起来很亮)。
当单颗恒星的光通量周期性变化时,可能是由于该恒星被外行星所绕。假设是外行星在恒星前方运行时,它会阻挡一小部分光线,导致观测到的亮度略微减弱。
小贴士
寻找外行星是非常罕见的。预测列,关于恒星是否拥有外行星,正例非常少,导致数据集不平衡。不平衡的数据集需要额外的注意。在第七章《使用 XGBoost 发现外行星》中,我们将进一步探讨该数据集的不平衡问题。
接下来,让我们访问外行星数据集并为机器学习做准备。
对外行星数据集进行预处理
外行星数据集已经上传到我们的 GitHub 页面:github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter04
。
以下是加载和预处理外行星数据集以供机器学习使用的步骤:
-
下载
exoplanets.csv
文件,并将其与 Jupyter Notebook 放在同一文件夹下。然后,打开该文件查看内容:df = pd.read_csv('exoplanets.csv') df.head()
DataFrame 会如下所示:
图 4.5 – 外行星数据框
由于空间限制,并非所有列都会显示。光通量列为浮动数值类型,而
Label
列对于外行星恒星是2
,对于非外行星恒星是1
。 -
让我们使用
df.info()
来确认所有列都是数值型的:df.info()
结果如下:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 5087 entries, 0 to 5086 Columns: 3198 entries, LABEL to FLUX.3197 dtypes: float64(3197), int64(1) memory usage: 124.1 MB
从输出结果可以看出,
3197
列是浮动数值类型,1
列是int
类型,所以所有列都是数值型的。 -
现在,让我们用以下代码确认空值的数量:
df.isnull().sum().sum()
输出结果如下:
0
输出结果显示没有空值。
-
由于所有列都是数值型且没有空值,我们可以将数据拆分为训练集和测试集。请注意,第 0 列是目标列
y
,其余列是预测列X
:X = df.iloc[:,1:] y = df.iloc[:,0] X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
现在是时候构建梯度提升分类器来预测恒星是否拥有外行星了。
构建梯度提升分类器
梯度提升分类器的工作原理与梯度提升回归器相同,主要的区别在于评分方式。
让我们开始导入GradientBoostingClassifer
和XGBClassifier
,并导入accuracy_score
,以便我们可以比较这两个模型:
from sklearn.ensemble import GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score
接下来,我们需要一种方法来使用定时器比较模型。
模型计时
Python 自带了一个time
库,可以用来标记时间。一般的做法是在计算前后标记时间,时间差告诉我们计算花费的时间。
time
库的导入方法如下:
import time
在time
库中,.time()
方法以秒为单位标记时间。
作为一个例子,查看通过使用time.time()
在计算前后标记开始和结束时间,df.info()
运行需要多长时间:
start = time.time()
df.info()
end = time.time()
elapsed = end - start
print('\nRun Time: ' + str(elapsed) + ' seconds.')
输出如下:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5087 entries, 0 to 5086
Columns: 3198 entries, LABEL to FLUX.3197
dtypes: float64(3197), int64(1)
memory usage: 124.1 MB
运行时间如下:
Run Time: 0.0525362491607666 seconds.
你的结果可能会与我们的不同,但希望在同一数量级范围内。
现在让我们使用前面的代码标记时间,比较GradientBoostingClassifier
和XGBoostClassifier
在外行星数据集上的速度。
提示
Jupyter Notebooks 配有魔法函数,用%
符号标记在命令前。%timeit
就是这样一个魔法函数。与计算运行一次代码所需的时间不同,%timeit
会计算多次运行代码所需的时间。有关魔法函数的更多信息,请参见ipython.readthedocs.io/en/stable/interactive/magics.html。
比较速度
是时候用外行星数据集对GradientBoostingClassifier
和XGBoostClassifier
进行速度对比了。我们设置了max_depth=2
和n_estimators=100
来限制模型的大小。我们从GradientBoostingClassifier
开始:
-
首先,我们将标记开始时间。在构建并评分模型后,我们将标记结束时间。以下代码可能需要大约 5 分钟来运行,具体时间取决于你的计算机速度:
start = time.time() gbr = GradientBoostingClassifier(n_estimators=100, max_depth=2, random_state=2) gbr.fit(X_train, y_train) y_pred = gbr.predict(X_test) score = accuracy_score(y_pred, y_test) print('Score: ' + str(score)) end = time.time() elapsed = end - start print('\nRun Time: ' + str(elapsed) + ' seconds')
结果如下:
Score: 0.9874213836477987 Run Time: 317.6318619251251 seconds
GradientBoostingRegressor
在我的 2013 款 MacBook Air 上运行了超过 5 分钟。对于一台旧电脑来说,在 150 百万数据点的情况下,表现还不错。注意
尽管 98.7%的得分通常在准确性上非常出色,但对于不平衡数据集而言情况并非如此,正如你将在第七章中看到的那样,使用 XGBoost 发现外行星。
-
接下来,我们将构建一个具有相同超参数的
XGBClassifier
模型,并以相同的方式标记时间:start = time.time() xg_reg = XGBClassifier(n_estimators=100, max_depth=2, random_state=2) xg_reg.fit(X_train, y_train) y_pred = xg_reg.predict(X_test) score = accuracy_score(y_pred, y_test) print('Score: ' + str(score)) end = time.time() elapsed = end - start print('Run Time: ' + str(elapsed) + ' seconds')
结果如下:
Score: 0.9913522012578616 Run Time: 118.90568995475769 seconds
在我的 2013 款 MacBook Air 上,XGBoost 运行时间不到 2 分钟,速度是原来的两倍以上。它的准确性也提高了半个百分点。
在大数据的领域中,一个速度是原来两倍的算法可以节省数周甚至数月的计算时间和资源。这一优势在大数据领域中是巨大的。
在提升法的世界里,XGBoost 因其无与伦比的速度和出色的准确性而成为首选模型。
至于外行星数据集,它将在第七章中重新讨论,在一个重要的案例研究中揭示了处理不平衡数据集时的挑战,以及针对这些挑战的多种潜在解决方案。
注意
我最近购买了一台 2020 款的 MacBook Pro 并更新了所有软件。使用相同代码时,时间差异惊人:
梯度提升运行时间:197.38 秒
XGBoost 运行时间:8.66 秒
超过 10 倍的差异!
总结
在本章中,你了解了 bagging 与 boosting 的区别。你通过从零开始构建一个梯度提升回归器,学习了梯度提升是如何工作的。你实现了各种梯度提升超参数,包括learning_rate
、n_estimators
、max_depth
和subsample
,这导致了随机梯度提升。最后,你利用大数据,通过比较GradientBoostingClassifier
和XGBoostClassifier
的运行时间,预测星星是否拥有系外行星,结果表明XGBoostClassifier
的速度是前者的两倍,甚至超过十倍,同时更为准确。
学习这些技能的优势在于,你现在能明白何时使用 XGBoost,而不是像梯度提升这样的类似机器学习算法。你现在可以通过正确利用核心超参数,包括n_estimators
和learning_rate
,来构建更强大的 XGBoost 和梯度提升模型。此外,你也已经具备了为所有计算设定时间的能力,而不再依赖直觉。
恭喜!你已经完成了所有初步的 XGBoost 章节。到目前为止,目的在于让你了解机器学习和数据分析在更广泛的 XGBoost 叙事中的背景。目的是展示 XGBoost 如何从集成方法、提升、梯度提升以及大数据的需求中应运而生。
下一章将为我们的旅程开启新的一篇,带来 XGBoost 的高级介绍。在这里,你将学习 XGBoost 算法背后的数学细节,并了解 XGBoost 如何通过硬件修改来提高速度。此外,你还将通过一个关于希格斯玻色子发现的历史性案例,使用原始的 Python API 构建 XGBoost 模型。接下来的章节将重点介绍如何构建快速、高效、强大且适合行业应用的 XGBoost 模型,其中涵盖了令人兴奋的细节、优势、技巧和窍门,这些模型将在未来多年内为你所用。
第二部分:XGBoost
通过回顾 XGBoost 的总体框架,包括基础模型、速度提升、数学推导以及原始 Python API,重新介绍并深入分析 XGBoost。对 XGBoost 的超参数进行了详细分析、总结和微调。科学相关的案例研究为构建和微调强大的 XGBoost 模型提供了丰富的实践,旨在修正权重不平衡和不足的得分问题。
本节包括以下章节:
-
第五章*, XGBoost 揭秘*
-
第六章*, XGBoost 超参数*
-
第七章*, 使用 XGBoost 发现系外行星*
第五章:第五章:XGBoost 揭示
在这一章中,你将最终看到极限梯度提升(Extreme Gradient Boosting),或称为XGBoost。XGBoost 是在我们构建的机器学习叙事框架中呈现的,从决策树到梯度提升。章节的前半部分聚焦于 XGBoost 带给树集成算法的独特进展背后的理论。后半部分则聚焦于在Higgs 博士 Kaggle 竞赛中构建 XGBoost 模型,正是这个竞赛让 XGBoost 向全世界展示了它的强大。
具体而言,你将识别出使 XGBoost 更加快速的速度增强,了解 XGBoost 如何处理缺失值,并学习 XGBoost 的正则化参数选择背后的数学推导。你将建立构建 XGBoost 分类器和回归器的模型模板。最后,你将了解大型强子对撞机(Large Hadron Collider),即希格斯玻色子发现的地方,在那里你将使用原始的 XGBoost Python API 来加权数据并进行预测。
本章涉及以下主要内容:
-
设计 XGBoost
-
分析 XGBoost 参数
-
构建 XGBoost 模型
-
寻找希格斯玻色子 – 案例研究
设计 XGBoost
XGBoost 是相较于梯度提升算法的重大升级。在本节中,你将识别 XGBoost 的关键特性,这些特性使它与梯度提升和其他树集成算法区分开来。
历史叙事
随着大数据的加速发展,寻找能产生准确、最优预测的优秀机器学习算法的探索开始了。决策树生成的机器学习模型过于精确,无法很好地泛化到新数据。集成方法通过集成和提升组合多棵决策树,证明了更加有效。梯度提升是从树集成算法轨迹中出现的领先算法。
梯度提升的一致性、强大功能和出色结果使得华盛顿大学的陈天奇(Tianqi Chen)决定增强其能力。他将这一新算法命名为 XGBoost,代表极限梯度提升(Extreme Gradient Boosting)。陈天奇的新型梯度提升算法包含内建的正则化,并在速度上取得了显著提升。
在 Kaggle 竞赛中取得初步成功后,2016 年,陈天奇和卡洛斯·格斯特林(Carlos Guestrin)共同撰写了 XGBoost: A Scalable Tree Boosting System,向更广泛的机器学习社区介绍了他们的算法。你可以在arxiv.org/pdf/1603.02754.pdf
上查看原文。以下部分总结了其中的关键要点。
设计特性
如第四章所示,从梯度提升到 XGBoost,在处理大数据时,对更快算法的需求显而易见。极限(Extreme)在极限梯度提升(Extreme Gradient Boosting)中意味着将计算极限推向极致。推动计算极限不仅需要构建模型的知识,还需要了解磁盘读取、压缩、缓存和核心等方面的知识。
尽管本书的重点仍然是构建 XGBoost 模型,但我们将窥探一下 XGBoost 算法的内部机制,以区分其关键进展,如处理缺失值、提升速度和提高准确度,这些因素使 XGBoost 更快、更准确、并且更具吸引力。接下来,让我们看看这些关键进展。
处理缺失值
你在第一章《机器学习概述》中花费了大量时间,练习了不同的方法来处理空值。这是所有机器学习从业者必须掌握的基本技能。
然而,XGBoost 能够为你处理缺失值。它有一个名为 missing
的超参数,可以设置为任何值。遇到缺失数据时,XGBoost 会对不同的分割选项进行评分,并选择结果最好的那个。
提升速度
XGBoost 是专门为速度设计的。速度的提升使得机器学习模型能够更快速地构建,这在处理百万、十亿或万亿行数据时尤为重要。在大数据的世界里,这并不罕见,因为每天,工业和科学领域都会积累比以往更多的数据。以下新的设计功能使 XGBoost 在速度上相比同类集成算法具有显著优势:
-
近似分割查找算法
-
稀疏感知分割查找
-
并行计算
-
缓存感知访问
-
块压缩与分片
让我们更详细地了解这些功能。
近似分割查找算法
决策树需要最优的分割才能产生最优结果。贪心算法在每一步选择最佳的分割,并且不会回溯查看之前的分支。请注意,决策树分割通常以贪心的方式进行。
XGBoost 提出了一个精确的贪心算法,并且增加了一种新的近似分割查找算法。分割查找算法使用分位数(用于拆分数据的百分比)来提出候选的分割点。在全局提议中,整个训练过程中使用相同的分位数;而在局部提议中,每一轮分割都会提供新的分位数。
一个已知的算法,分位数草图,在数据集权重相等的情况下表现良好。XGBoost 提出了一个新型的加权分位数草图,基于合并和修剪,并提供了理论保证。虽然该算法的数学细节超出了本书的范围,但你可以参考 XGBoost 原始论文的附录,链接请见arxiv.org/pdf/1603.02754.pdf
。
稀疏感知分割查找
使用pd.get_dummies
将类别列转换为数值列。这会导致数据集增大,并产生许多值为 0 的条目。将类别列转换为数值列,其中 1 表示存在,0 表示不存在,这种方法通常称为独热编码。你将在第十章中练习独热编码,XGBoost 模型部署。
稀疏矩阵旨在仅存储具有非零且非空值的数据点,这样可以节省宝贵的空间。稀疏感知的分裂意味着,在寻找分裂时,XGBoost 更快,因为它的矩阵是稀疏的。
根据原始论文《XGBoost: A Scalable Tree Boosting System》,稀疏感知的分裂查找算法在All-State-10K数据集上比标准方法快 50 倍。
并行计算
增强方法并不适合并行计算,因为每棵树都依赖于前一棵树的结果。然而,仍然有一些并行化的机会。
并行计算发生在多个计算单元同时协同处理同一个问题时。XGBoost 将数据排序并压缩为块。这些块可以分发到多个机器,或者分发到外部内存(即核心外存储)。
使用块排序数据速度更快。分裂查找算法利用了块,因而查找分位数的速度也更快。在这些情况下,XGBoost 提供了并行计算,以加速模型构建过程。
缓存感知访问
计算机上的数据被分为缓存和主内存。缓存是你最常使用的部分,保留给高速内存。较少使用的数据则保留在较低速的内存中。不同的缓存级别有不同的延迟量级,详细信息可以参见:gist.github.com/jboner/2841832
。
在梯度统计方面,XGBoost 使用缓存感知预取。XGBoost 分配一个内部缓冲区,提取梯度统计数据,并通过小批量进行累积。根据《XGBoost: A Scalable Tree Boosting System》中的描述,预取延长了读写依赖,并将大规模数据集的运行时间减少了大约 50%。
块压缩与分片
XGBoost 通过块压缩和块分片提供额外的速度提升。
块压缩通过压缩列来帮助计算密集型磁盘读取。块分片通过将数据分片到多个磁盘,并在读取数据时交替使用,减少了读取时间。
准确性提升
XGBoost 增加了内建正则化,以在梯度提升之外实现准确性提升。正则化是通过增加信息来减少方差并防止过拟合的过程。
尽管数据可以通过超参数微调进行正则化,但也可以尝试使用正则化算法。例如,Ridge
和 Lasso
是 LinearRegression
的正则化机器学习替代方法。
XGBoost 将正则化作为学习目标的一部分,与梯度提升和随机森林不同。正则化参数通过惩罚复杂性并平滑最终权重来防止过拟合。XGBoost 是一种正则化版本的梯度提升。
在下一节中,你将接触到 XGBoost 学习目标背后的数学原理,它将正则化与损失函数结合在一起。虽然你不需要知道这些数学内容来有效地使用 XGBoost,但掌握数学知识可能会让你对其有更深的理解。如果愿意,你可以跳过下一节。
分析 XGBoost 参数
在本节中,我们将通过数学推导分析 XGBoost 用于创建最先进机器学习模型的参数。
我们将保持与第二章《决策树深入解析》中的参数和超参数的区分。超参数是在模型训练之前选择的,而参数则是在模型训练过程中选择的。换句话说,参数是模型从数据中学习到的内容。
以下推导来自 XGBoost 官方文档,Boosted Trees 介绍,网址为 xgboost.readthedocs.io/en/latest/tutorials/model.html
。
学习目标
机器学习模型的学习目标决定了模型与数据的拟合程度。对于 XGBoost,学习目标包括两个部分:损失函数 和 正则化项。
数学上,XGBoost 的学习目标可以定义如下:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_002.png 是损失函数,表示回归的均方误差(MSE),或分类的对数损失,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_003.png 是正则化函数,一个用于防止过拟合的惩罚项。将正则化项作为目标函数的一部分使 XGBoost 与大多数树集成方法有所区别。
让我们通过考虑回归的均方误差(MSE)更详细地看一下目标函数。
损失函数
定义为回归的均方误差(MSE)的损失函数可以用求和符号表示,如下所示:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_005.png 是第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_006.png 行的目标值,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_007.png 是机器学习模型为第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_008.png 行预测的值。求和符号 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_009.png 表示从 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_010.png 开始到 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_011.png 结束,所有行的求和。
对于给定的树,预测值 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_012.png 需要一个从树根开始,到叶子结束的函数。数学上可以表达为:
这里,xi 是一个向量,其条目为第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_014.png 行的列,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_015.png 表示函数 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_016.png 是 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_017.png 的成员,后者是所有可能的 CART 函数集合。CART 是 分类与回归树(Classification And Regression Trees)的缩写。CART 为所有叶子节点提供了一个实值,即使对于分类算法也是如此。
在梯度提升中,决定第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_006.png 行预测的函数包括所有之前函数的和,如第四章《从梯度提升到 XGBoost》中所述。因此,可以写出以下公式:
这里,T 是提升树的数量。换句话说,为了获得第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_020.png 棵树的预测结果,需要将之前所有树的预测结果与新树的预测结果相加。符号 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_021.png 表明这些函数属于 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_022.png,即所有可能的 CART 函数集合。
第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_023.png 棵提升树的学习目标现在可以重写如下:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_025.png 是第 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_026.png 棵提升树的总损失函数,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_027.png 是正则化项。
由于提升树会将之前树的预测结果与新树的预测结果相加,因此必须满足 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_028.png。这就是加法训练的思想。
将此代入之前的学习目标,我们得到以下公式:
对于最小二乘回归情况,可以将其重写如下:
展开多项式后,我们得到以下公式:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_032.png 是一个常数项,不依赖于 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_033.png。从多项式的角度来看,这是一个关于变量 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_034.png 的二次方程。请记住,目标是找到一个最优值 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_035.png,即最优函数,它将根节点(样本)映射到叶节点(预测值)。
任何足够光滑的函数,例如二次多项式(quadratic),都可以通过泰勒多项式来逼近。XGBoost 使用牛顿法与二次泰勒多项式来得到以下公式:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_037.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_038.png 可以写作以下偏导数:
若要了解 XGBoost 如何使用 泰勒展开,请查阅 stats.stackexchange.com/questions/202858/xgboost-loss-function-approximation-with-taylor-expansion
。
XGBoost 通过使用仅需要 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_041.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_038.png 作为输入的求解器来实现这一学习目标函数。由于损失函数是通用的,相同的输入可以用于回归和分类。
正则化函数
设 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_044.png 为叶子的向量空间。那么,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_045.png,即将树根映射到叶子的函数,可以用 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_044.png 来重新表示,形式如下:
这里,q 是将数据点分配给叶子的函数,T 是叶子的数量。
经过实践和实验,XGBoost 确定了以下作为正则化函数,其中 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_048.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_049.png 是用于减少过拟合的惩罚常数:
目标函数
将损失函数与正则化函数结合,学习目标函数变为以下形式:
我们可以定义分配给 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_052.png 叶子的数据显示点的索引集,如下所示:
目标函数可以写成如下形式:
最后,通过设置 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_055.png 和 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_056.png,在重新排列索引并合并相似项后,我们得到目标函数的最终形式,即:
通过对目标函数求导并关于 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_058.png 令左侧为零,我们得到如下结果:
这可以代回到目标函数中,得到以下结果:
这是 XGBoost 用来确定模型与数据拟合程度的结果。
恭喜你完成了一个漫长且具有挑战性的推导过程!
构建 XGBoost 模型
在前两部分中,你学习了 XGBoost 如何在底层工作,包括参数推导、正则化、速度增强以及新的特性,如 missing
参数用于补偿空值。
本书中,我们主要使用 scikit-learn 构建 XGBoost 模型。scikit-learn 的 XGBoost 封装器在 2019 年发布。在完全使用 scikit-learn 之前,构建 XGBoost 模型需要更陡峭的学习曲线。例如,必须将 NumPy 数组转换为 dmatrices
,才能利用 XGBoost 框架。
然而,在 scikit-learn 中,这些转换是后台自动完成的。在 scikit-learn 中构建 XGBoost 模型与构建其他机器学习模型非常相似,正如你在本书中所经历的那样。所有标准的 scikit-learn 方法,如 .fit
和 .predict
都可以使用,此外还包括如 train_test_split
、cross_val_score
、GridSearchCV
和 RandomizedSearchCV
等重要工具。
在这一部分,你将开发用于构建 XGBoost 模型的模板。以后,这些模板可以作为构建 XGBoost 分类器和回归器的起点。
我们将为两个经典数据集构建模板:用于分类的 Iris 数据集 和用于回归的 Diabetes 数据集。这两个数据集都很小,内置于 scikit-learn,并且在机器学习社区中经常被测试。作为模型构建过程的一部分,你将显式定义默认的超参数,这些超参数使 XGBoost 模型得分优秀。这些超参数被显式定义,以便你了解它们是什么,并为将来调整它们做好准备。
Iris 数据集
Iris 数据集,机器学习领域的一个重要数据集,由统计学家 Robert Fischer 于 1936 年引入。它易于访问、数据量小、数据干净,且具有对称的数值,这些特点使其成为测试分类算法的热门选择。
我们将通过使用 datasets
库中的 load_iris()
方法直接从 scikit-learn 下载 Iris 数据集,如下所示:
import pandas as pd
import numpy as np
from sklearn import datasets
iris = datasets.load_iris()
Scikit-learn 数据集以 pandas
DataFrame 的形式存储,后者更多用于数据分析和数据可视化。将 NumPy 数组视为 DataFrame 需要使用 pandas
的 DataFrame
方法。这个 scikit-learn 数据集在预先划分为预测列和目标列后,合并它们需要用 np.c_
来连接 NumPy 数组,然后再进行转换。列名也会被添加,如下所示:
df = pd.DataFrame(data= np.c_[iris['data'], iris['target']],columns= iris['feature_names'] + ['target'])
你可以使用 df.head()
查看 DataFrame 的前五行:
df.head()
结果 DataFrame 将如下所示:
图 5.1 – Iris 数据集
预测列的含义不言自明,分别衡量萼片和花瓣的长度与宽度。目标列根据 scikit-learn 文档, scikit-learn.org/stable/auto_examples/datasets/plot_iris_dataset.html
,包含三种不同的鸢尾花:setosa、versicolor 和 virginica。数据集包含 150 行。
为了准备机器学习所需的数据,导入 train_test_split
,然后相应地划分数据。你可以使用原始的 NumPy 数组 iris['data']
和 iris['target']
作为 train_test_split
的输入:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris['data'], iris['target'], random_state=2)
现在我们已经划分了数据,接下来让我们构建分类模板。
XGBoost 分类模板
以下模板用于构建 XGBoost 分类器,假设数据集已经分为 X_train
、X_test
、y_train
和 y_test
:
-
从
xgboost
库中导入XGBClassifier
:from xgboost import XGBClassifier
-
根据需要导入分类评分方法。
虽然
accuracy_score
是标准评分方法,但其他评分方法,如auc
(曲线下的面积),将在后面讨论:from sklearn.metrics import accuracy_score
-
使用超参数初始化 XGBoost 分类器。
超参数微调是 第六章 的重点,XGBoost 超参数。在本章中,最重要的默认超参数已明确列出:
xgb = XGBClassifier(booster='gbtree', objective='multi:softprob', max_depth=6, learning_rate=0.1, n_estimators=100, random_state=2, n_jobs=-1)
前述超参数的简要描述如下:
a)
booster='gbtree'
:booster
的'gbtree'
代表梯度提升树,是 XGBoost 的默认基础学习器。尽管不常见,但也可以使用其他基础学习器,这是一种我们在 第八章 中使用的策略,XGBoost 替代基础学习器。b)
objective='multi:softprob'
:标准的目标选项可以在 XGBoost 官方文档中查看,xgboost.readthedocs.io/en/latest/parameter.html
,在 学习任务参数 部分。multi:softprob
目标是当数据集包含多个类别时,作为binary:logistic
的标准替代方案。它计算分类的概率,并选择最高的一个。如果没有明确指定,XGBoost 通常会为你找到合适的目标。c)
max_depth=6
:树的max_depth
决定了每棵树的分支数量。它是做出平衡预测时最重要的超参数之一。XGBoost 默认值为6
,不同于随机森林,后者不会提供值,除非明确编程。d)
learning_rate=0.1
:在 XGBoost 中,这个超参数通常被称为eta
。该超参数通过减少每棵树的权重来限制方差,达到给定的百分比。learning_rate
超参数在 第四章 中进行了详细探讨,从梯度提升到 XGBoost。e)
n_estimators=100
:在集成方法中非常流行,n_estimators
是模型中的提升树数量。增加这个数量并降低learning_rate
可以得到更稳健的结果。 -
将分类器拟合到数据上。
这就是魔法发生的地方。整个 XGBoost 系统,包括前两节中探讨的细节,最佳参数选择,包括正则化约束,以及速度增强,例如近似分裂查找算法,以及阻塞和分片,都会在这行强大的 scikit-learn 代码中发生:
xgb.fit(X_train, y_train)
-
将 y 值预测为
y_pred
:y_pred = xgb.predict(X_test)
-
通过将
y_pred
与y_test
进行比较来对模型进行评分:score = accuracy_score(y_pred, y_test)
-
显示你的结果:
print('Score: ' + str(score)) Score: 0.9736842105263158
不幸的是,Iris 数据集没有官方的得分列表,得分太多无法在一个地方汇总。使用默认超参数,在 Iris 数据集上的初始得分为 97.4
百分比,非常不错(参见 www.kaggle.com/c/serpro-iris/leaderboard
)。
前面段落中提供的 XGBoost 分类器模板并非最终版本,而是未来构建模型的起点。
糖尿病数据集
现在你已经熟悉了 scikit-learn 和 XGBoost,你正逐步培养起快速构建和评分 XGBoost 模型的能力。在本节中,提供了一个使用 cross_val_score
的 XGBoost 回归器模板,并应用于 scikit-learn 的糖尿病数据集。
在构建模板之前,请导入预测列为 X
,目标列为 y
,如下所示:
X,y = datasets.load_diabetes(return_X_y=True)
现在我们已导入预测列和目标列,让我们开始构建模板。
XGBoost 回归器模板(交叉验证)
以下是在 scikit-learn 中使用交叉验证构建 XGBoost 回归模型的基本步骤,假设已定义预测列 X
和目标列 y
:
-
导入
XGBRegressor
和cross_val_score
:from sklearn.model_selection import cross_val_score from xgboost import XGBRegressor
-
初始化
XGBRegressor
。在这里,我们初始化
XGBRegressor
,并设置objective='reg:squarederror'
,即均方误差(MSE)。最重要的超参数默认值已明确给出:xgb = XGBRegressor(booster='gbtree', objective='reg:squarederror', max_depth=6, learning_rate=0.1, n_estimators=100, random_state=2, n_jobs=-1)
-
使用
cross_val_score
进行回归器的拟合和评分。使用
cross_val_score
,拟合和评分在一个步骤中完成,输入包括模型、预测列、目标列和评分:scores = cross_val_score(xgb, X, y, scoring='neg_mean_squared_error', cv=5)
-
显示结果。
回归得分通常以均方根误差 (RMSE) 展示,以保持单位一致:
rmse = np.sqrt(-scores) print('RMSE:', np.round(rmse, 3)) print('RMSE mean: %0.3f' % (rmse.mean()))
结果如下:
RMSE: [63.033 59.689 64.538 63.699 64.661] RMSE mean: 63.124
没有比较基准,我们无法理解该得分的意义。将目标列 y
转换为 pandas
DataFrame 并使用 .describe()
方法将显示预测列的四分位数和一般统计数据,如下所示:
pd.DataFrame(y).describe()
这是预期的输出:
图 5.2 – 描述 y,糖尿病目标列的统计数据
得分为 63.124
小于 1 个标准差,这是一个可敬的结果。
现在你拥有了可以用于构建模型的 XGBoost 分类器和回归器模板。
现在你已经习惯在 scikit-learn 中构建 XGBoost 模型,是时候深入探索高能物理学了。
寻找希格斯玻色子 – 案例研究
在本节中,我们将回顾希格斯玻色子 Kaggle 竞赛,该竞赛使 XGBoost 成为机器学习的焦点。为了铺垫背景,在进入模型开发之前,首先提供历史背景。我们构建的模型包括当时 XGBoost 提供的默认模型和 Gabor Melis 提供的获胜解决方案的参考。本节内容无需 Kaggle 账户,因此我们不会花时间展示如何提交结果。如果你感兴趣,我们已提供了相关指南。
物理背景
在大众文化中,希格斯玻色子被称为上帝粒子。该粒子由彼得·希格斯在 1964 年提出,用于解释为什么粒子具有质量。
寻找希格斯玻色子的过程最终在 2012 年通过大型强子对撞机在瑞士日内瓦的 CERN 发现达到了高潮。诺贝尔奖颁发了,物理学的标准模型,即解释所有已知物理力(除了重力之外)的模型,变得比以往任何时候都更加重要。
希格斯玻色子是通过以极高速度碰撞质子并观察结果来发现的。观测数据来自ATLAS探测器,该探测器记录了每秒数亿次质子-质子碰撞所产生的数据,具体内容可以参考竞赛的技术文档《Learning to discover: the Higgs boson machine learning challenge》,higgsml.lal.in2p3.fr/files/2014/04/documentation_v1.8.pdf
。
在发现希格斯玻色子后,下一步是精确测量其衰变特性。ATLAS 实验通过从背景噪声中提取的数据发现希格斯玻色子衰变成两个tau粒子。为了更好地理解数据,ATLAS 寻求了机器学习社区的帮助。
Kaggle 竞赛
Kaggle 竞赛是一种旨在解决特定问题的机器学习竞赛。机器学习竞赛在 2006 年变得有名,当时 Netflix 提供了 100 万美元奖励给任何能够改进其电影推荐系统 10%的人。2009 年,100 万美元奖金被颁发给了BellKor的Pragmatic Chaos团队 (www.wired.com/2009/09/bellkors-pragmatic-chaos-wins-1-million-netflix-prize/
)。
许多企业、计算机科学家、数学家和学生开始意识到机器学习在社会中的日益重要性。机器学习竞赛逐渐火热,企业主和机器学习从业者都从中获得了互利的好处。从 2010 年开始,许多早期采用者前往 Kaggle 参与机器学习竞赛。
2014 年,Kaggle 宣布了希格斯玻色子机器学习挑战赛与 ATLAS 合作(www.kaggle.com/c/higgs-boson
)。比赛奖金池为 13,000 美元,共有 1,875 支队伍参加了比赛。
在 Kaggle 比赛中,提供了训练数据以及所需的评分方法。团队在训练数据上构建机器学习模型,然后提交结果。测试数据的目标列不会提供。然而,允许多次提交,参赛者可以在最终日期之前不断优化自己的模型。
Kaggle 比赛是测试机器学习算法的沃土。与工业界不同,Kaggle 比赛吸引了成千上万的参赛者,这使得获奖的机器学习模型经过了非常充分的测试。
XGBoost 与希格斯挑战
XGBoost 于 2014 年 3 月 27 日公开发布,早于希格斯挑战赛 6 个月。在比赛中,XGBoost 大放异彩,帮助参赛者在 Kaggle 排行榜上攀升,同时节省了宝贵的时间。
让我们访问数据,看看参赛者们在使用什么数据。
数据
我们将使用源自 CERN 开放数据门户的原始数据,而不是 Kaggle 提供的数据:opendata.cern.ch/record/328
。CERN 数据与 Kaggle 数据的区别在于,CERN 数据集要大得多。我们将选择前 250,000 行,并进行一些修改以匹配 Kaggle 数据。
你可以直接从github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter05
下载 CERN 希格斯玻色子数据集。
将atlas-higgs-challenge-2014-v2.csv.gz
文件读取到pandas
数据框中。请注意,我们仅选择前 250,000 行,并且使用compression=gzip
参数,因为数据集是以csv.gz
文件形式压缩的。读取数据后,查看前五行,如下所示:
df = pd.read_csv('atlas-higgs-challenge-2014-v2.csv.gz', nrows=250000, compression='gzip')
df.head()
输出的最右边几列应与以下截图所示相同:
图 5.3 – CERN 希格斯玻色子数据 – 包含 Kaggle 列
请注意Kaggleset
和KaggleWeight
列。由于 Kaggle 数据集较小,Kaggle 在其权重列中使用了不同的数字,该列在前面的图中表示为KaggleWeight
。Kaggleset
下的t
值表示它是 Kaggle 数据集的训练集的一部分。换句话说,这两列,Kaggleset
和KaggleWeight
,是 CERN 数据集中的列,用于包含将被用于 Kaggle 数据集的信息。在本章中,我们将限制 CERN 数据的子集为 Kaggle 训练集。
为了匹配 Kaggle 训练数据,我们将删除Kaggleset
和Weight
列,将KaggleWeight
转换为'Weight'
,并将'Label'
列移到最后一列,如下所示:
del df[‹Weight›]
del df[‹KaggleSet›]
df = df.rename(columns={«KaggleWeight»: «Weight»})
一种移动Label
列的方法是将其存储为一个变量,删除该列,然后通过将其分配给新变量来添加新列。每当将新列分配给 DataFrame 时,新列会出现在末尾:
label_col = df['Label']
del df['Label']
df['Label'] = label_col
现在所有更改已经完成,CERN 数据与 Kaggle 数据一致。接下来,查看前五行数据:
df.head()
这是期望输出的左侧部分:
图 5.4 – CERN 希格斯玻色子数据 – 物理列
许多列没有显示,并且出现了-999.00
这一不寻常的值。
EventId
之后的列包含以PRI
为前缀的变量,PRI
代表原始值,即在碰撞过程中由探测器直接测量的值。相比之下,标记为DER
的列是从这些测量值中得出的数值推导。
所有列名和类型可以通过df.info()
查看:
df.info()
这是输出的一个示例,中间的列已被截断以节省空间:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 250000 entries, 0 to 249999
Data columns (total 33 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 EventId 250000 non-null int64
1 DER_mass_MMC 250000 non-null float64
2 DER_mass_transverse_met_lep 250000 non-null float64
3 DER_mass_vis 250000 non-null float64
4 DER_pt_h 250000 non-null float64
…
28 PRI_jet_subleading_eta 250000 non-null float64
29 PRI_jet_subleading_phi 250000 non-null float64
30 PRI_jet_all_pt 250000 non-null float64
31 Weight 250000 non-null float64
32 Label 250000 non-null object
dtypes: float64(30), int64(3)
memory usage: 62.9 MB
所有列都有非空值,只有最后一列Label
是非数字类型。列可以按如下方式分组:
-
列
0
:EventId
– 对于机器学习模型无关。 -
列
1-30
:来自 LHC 碰撞的物理列。这些列的详细信息可以在higgsml.lal.in2p3.fr/documentation
中的技术文档链接中找到。这些是机器学习的预测列。 -
列
31
:Weight
– 该列用于对数据进行缩放。问题在于希格斯玻色子事件非常稀有,因此一个 99.9%准确率的机器学习模型可能无法找到它们。权重弥补了这一不平衡,但测试数据中没有权重。处理权重的策略将在本章后续部分讨论,并在第七章中讨论,使用 XGBoost 发现外星行星。 -
列
32
:Label
– 这是目标列,标记为s
表示信号,b
表示背景。训练数据是从实际数据模拟而来,因此信号比实际情况下更多。信号是指希格斯玻色子衰变的发生。
数据的唯一问题是目标列Label
不是数字类型。通过将s
值替换为1
,将b
值替换为0
,将Label
列转换为数字列,如下所示:
df['Label'].replace(('s', 'b'), (1, 0), inplace=True)
现在所有列都变为数字类型并且没有空值,你可以将数据拆分为预测列和目标列。回顾一下,预测列的索引是 1–30,目标列是最后一列,索引为32
(或 -1)。注意,Weight
列不应包含在内,因为测试数据中没有该列:
X = df.iloc[:,1:31]
y = df.iloc[:,-1]
打分
Higgs 挑战不是普通的 Kaggle 竞赛。除了理解高能物理以进行特征工程(这不是我们将要追求的路径)的难度外,评分方法也不是标准的。Higgs 挑战要求优化 近似中位数显著性 (AMS)。
AMS 的定义如下:
这里,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_062.png 是真阳性率,https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_063.png 是假阳性率,而 https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_05_064.png 是一个常数正则化项,其值为 10
。
幸运的是,XGBoost 为比赛提供了一种 AMS 评分方法,因此不需要正式定义。高 AMS 的结果来自许多真阳性和很少的假阴性。关于为何选择 AMS 而不是其他评分方法的理由在技术文档中说明,地址为 higgsml.lal.in2p3.fr/documentation
。
提示
可以构建自己的评分方法,但通常不需要。在极少数需要构建自定义评分方法的情况下,你可以查看 scikit-learn.org/stable/modules/model_evaluation.html
获取更多信息。
权重
在构建 Higgs 玻色子的机器学习模型之前,了解并利用权重非常重要。
在机器学习中,权重可以用来提高不平衡数据集的准确性。考虑 Higgs 挑战中的 s
(信号)和 b
(背景)列。实际上,s
<< b
,所以信号在背景噪声中非常稀少。例如,信号比背景噪声少 1,000 倍。你可以创建一个权重列,其中 b
= 1,s
= 1/1000 以补偿这种不平衡。
根据比赛的技术文档,权重列定义为 s
(信号)事件。
首先应将权重按比例缩放以匹配测试数据,因为测试数据提供了测试集生成的信号和背景事件的预期数量。测试数据有 550,000 行,比训练数据提供的 250,000 行(len(y)
)多两倍以上。将权重按比例缩放以匹配测试数据可以通过将权重列乘以增加百分比来实现,如下所示:
df['test_Weight'] = df['Weight'] * 550000 / len(y)
接下来,XGBoost 提供了一个超参数 scale_pos_weight
,它考虑了缩放因子。缩放因子是背景噪声权重之和除以信号权重之和。可以使用 pandas
的条件符号来计算缩放因子,如下所示:
s = np.sum(df[df['Label']==1]['test_Weight'])
b = np.sum(df[df['Label']==0]['test_Weight'])
在上述代码中,df[df['Label']==1]
缩小了 DataFrame 到 Label
列等于 1
的行,然后 np.sum
使用 test_Weight
列的值加总这些行的值。
最后,要查看实际比率,将 b
除以 s
:
b/s
593.9401931492318
总结来说,权重代表数据生成的预期信号和背景事件的数量。我们将权重缩放以匹配测试数据的大小,然后将背景权重之和除以信号权重之和,以建立 scale_pos_weight=b/s
超参数。
提示
要了解有关权重的更多详细讨论,请查看 KDnuggets 提供的精彩介绍:www.kdnuggets.com/2019/11/machine-learning-what-why-how-weighting.html
。
模型
现在是时候构建一个 XGBoost 模型来预测信号——即模拟的希格斯玻色子衰变事件。
在比赛开始时,XGBoost 是一种新工具,且 scikit-learn 的封装还没有发布。即便到了今天(2020 年),关于在 Python 中实现 XGBoost 的大多数信息仍然是在 scikit-learn 发布之前的版本。由于你可能会在线遇到 pre-scikit-learn 版本的 XGBoost Python API,而且这正是所有参与者在 Higgs Challenge 中使用的版本,因此本章只展示了使用原始 Python API 的代码。
以下是为 Higgs Challenge 构建 XGBoost 模型的步骤:
-
导入
xgboost
为xgb
:import xgboost as xgb
-
初始化 XGBoost 模型时,将
-999.0
设置为未知值。在 XGBoost 中,未知值可以通过missing
超参数来设置,而不是将这些值转换为中位数、均值、众数或其他空值替代。在模型构建阶段,XGBoost 会自动选择最佳的拆分值。 -
weight
超参数可以等于新列df['test_Weight']
,如weight
部分所定义:xgb_clf = xgb.DMatrix(X, y, missing=-999.0, weight=df['test_Weight'])
-
设置其他超参数。
以下超参数是 XGBoost 为竞赛提供的默认值:
a) 初始化一个名为
param
的空字典:param = {}
b) 将目标定义为
'binary:logitraw'
。这意味着一个二分类模型将从逻辑回归概率中创建。这个目标将模型定义为分类器,并允许对目标列进行排序,这是此特定 Kaggle 比赛提交所要求的:
param['objective'] = 'binary:logitraw'
c) 使用背景权重除以信号权重对正样本进行缩放。这有助于模型在测试集上表现得更好:
param['scale_pos_weight'] = b/s
d) 学习率
eta
设置为0.1
:param['eta'] = 0.1
e)
max_depth
设置为6
:param['max_depth'] = 6
f) 将评分方法设置为
'auc'
,以便显示:param['eval_metric'] = 'auc'
虽然会打印 AMS 分数,但评估指标设置为
auc
,即auc
是描述真正例与假正例的曲线,当其值为1
时是完美的。与准确率类似,auc
是分类问题的标准评分指标,尽管它通常优于准确率,因为准确率对于不平衡数据集来说有局限性,正如在第七章《使用 XGBoost 发现系外行星》中所讨论的那样,难以遗忘。 -
创建一个参数列表,包含前面提到的内容,并加上评估指标(
auc
)和ams@0.15
,XGBoost 实现的使用 15%阈值的 AMS 分数:plst = list(param.items())+[('eval_metric', 'ams@0.15')]
-
创建一个观察列表,包含初始化的分类器和
'train'
,这样你就可以在树继续提升的过程中查看分数:watchlist = [ (xg_clf, 'train') ]
-
将提升轮次设置为
120
:num_round = 120
-
训练并保存模型。通过将参数列表、分类器、轮次和观察列表作为输入来训练模型。使用
save_model
方法保存模型,这样你就不必再经过耗时的训练过程。然后,运行代码并观察随着树的提升,分数如何提高:print ('loading data end, start to boost trees') bst = xgb.train( plst, xgmat, num_round, watchlist ) bst.save_model('higgs.model') print ('finish training')
你的结果应该包括以下输出:
[110] train-auc:0.94505 train-ams@0.15:5.84830 [111] train-auc:0.94507 train-ams@0.15:5.85186 [112] train-auc:0.94519 train-ams@0.15:5.84451 [113] train-auc:0.94523 train-ams@0.15:5.84007 [114] train-auc:0.94532 train-ams@0.15:5.85800 [115] train-auc:0.94536 train-ams@0.15:5.86228 [116] train-auc:0.94550 train-ams@0.15:5.91160 [117] train-auc:0.94554 train-ams@0.15:5.91842 [118] train-auc:0.94565 train-ams@0.15:5.93729 [119] train-auc:0.94580 train-ams@0.15:5.93562 finish training
恭喜你构建了一个能够预测希格斯玻色子衰变的 XGBoost 分类器!
该模型的auc
为94.58
百分比,AMS 为5.9
。就 AMS 而言,竞赛的最高值在三分之三的上方。该模型在提交测试数据时,AMS 大约为3.6
。
你刚刚构建的模型是由 Tanqi Chen 为 XGBoost 用户在竞赛期间提供的基准模型。竞赛的获胜者,Gabor Melis,使用这个基准模型来构建他的模型。从查看获胜解决方案github.com/melisgl/higgsml
并点击xgboost-scripts可以看出,对基准模型所做的修改并不显著。Melis 和大多数 Kaggle 参赛者一样,也进行了特征工程,向数据中添加了更多相关列,这是我们将在第九章中讨论的内容,XGBoost Kaggle 大师。
在截止日期之后,你是可以自己构建和训练模型,并通过 Kaggle 提交的。对于 Kaggle 竞赛,提交必须经过排名、正确索引,并且必须使用 Kaggle API 主题进行进一步说明。如果你希望提交实际竞赛的模型,XGBoost 排名代码对你可能有所帮助,可以在github.com/dmlc/xgboost/blob/master/demo/kaggle-higgs/higgs-pred.py
找到。
总结
在这一章中,你学习了 XGBoost 是如何通过处理缺失值、稀疏矩阵、并行计算、分片和块等技术,来提高梯度提升的准确性和速度。你学习了 XGBoost 目标函数背后的数学推导,该函数决定了梯度下降和正则化的参数。你使用经典的 scikit-learn 数据集构建了XGBClassifier
和XGBRegressor
模板,获得了非常好的分数。最后,你构建了 XGBoost 为希格斯挑战提供的基准模型,这个模型最终引领了获胜解决方案,并将 XGBoost 推到了聚光灯下。
既然你已经对 XGBoost 的整体叙述、设计、参数选择和模型构建模板有了扎实的理解,在下一章中,你将微调 XGBoost 的超参数,以达到最佳评分。
第六章:第六章:XGBoost 超参数
XGBoost 有许多超参数。XGBoost 的基础学习器超参数包含所有决策树的超参数作为起点。由于 XGBoost 是梯度提升的增强版,因此也有梯度提升的超参数。XGBoost 特有的超参数旨在提升准确性和速度。然而,一次性尝试解决所有 XGBoost 超参数可能会让人感到头晕。
在第二章中,决策树深入剖析,我们回顾并应用了基础学习器超参数,如max_depth
,而在第四章中,从梯度提升到 XGBoost,我们应用了重要的 XGBoost 超参数,包括n_estimators
和learning_rate
。我们将在本章中再次回顾这些超参数,并介绍一些新的 XGBoost 超参数,如gamma
,以及一种叫做早停法的技术。
在本章中,为了提高 XGBoost 超参数微调的熟练度,我们将讨论以下主要主题:
-
准备数据和基础模型
-
调整核心 XGBoost 超参数
-
应用早停法
-
将所有内容整合在一起
技术要求
准备数据和基础模型
在介绍和应用 XGBoost 超参数之前,让我们做好以下准备:
-
获取心脏病数据集
-
构建
XGBClassifier
模型 -
实现
StratifiedKFold
-
对基准 XGBoost 模型进行评分
-
将
GridSearchCV
与RandomizedSearchCV
结合,形成一个强大的函数
良好的准备对于在微调超参数时获得准确性、一致性和速度至关重要。
心脏病数据集
本章使用的数据集是最初在第二章中提出的心脏病数据集,决策树深入剖析。我们选择相同的数据集,以最大化超参数微调的时间,并最小化数据分析的时间。让我们开始这个过程:
-
访问
github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter06
,加载heart_disease.csv
到 DataFrame 中,并显示前五行。以下是代码:import pandas as pd df = pd.read_csv('heart_disease.csv') df.head()
结果应如下所示:
图 6.1 - 前五行数据
最后一列,target,是目标列,1表示存在,表示患者患有心脏病,2表示不存在。有关其他列的详细信息,请访问
archive.ics.uci.edu/ml/datasets/Heart+Disease
上的 UCI 机器学习库,或参见第二章《决策树深入剖析》。 -
现在,检查
df.info()
以确保数据全部为数值型且没有空值:df.info()
这是输出结果:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 303 entries, 0 to 302 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 age 303 non-null int64 1 sex 303 non-null int64 2 cp 303 non-null int64 3 trestbps 303 non-null int64 4 chol 303 non-null int64 5 fbs 303 non-null int64 6 restecg 303 non-null int64 7 thalach 303 non-null int64 8 exang 303 non-null int64 9 oldpeak 303 non-null float64 10 slope 303 non-null int64 11 ca 303 non-null int64 12 thal 303 non-null int64 13 target 303 non-null int64 dtypes: float64(1), int64(13) memory usage: 33.3 KB
由于所有数据点都是非空且数值型的,数据已经准备好用于机器学习。现在是时候构建一个分类器了。
XGBClassifier
在调整超参数之前,我们先构建一个分类器,以便获得一个基准评分作为起点。
构建 XGBoost 分类器的步骤如下:
-
从各自的库中下载
XGBClassifier
和accuracy_score
。代码如下:from xgboost import XGBClassifier from sklearn.metrics import accuracy_score
-
声明
X
为预测列,y
为目标列,其中最后一行是目标列:X = df.iloc[:, :-1] y = df.iloc[:, -1]
-
使用
booster='gbtree'
和objective='binary:logistic'
的默认设置初始化XGBClassifier
,并将random_state=2
:model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
'gbtree'
提升器,即基本学习器,是一个梯度提升树。'binary:logistic'
目标是二分类中常用的标准目标,用于确定损失函数。虽然XGBClassifier
默认包含这些值,但我们在此明确指出,以便熟悉这些设置,并为后续章节的修改做准备。 -
为了评估基准模型,导入
cross_val_score
和numpy
,以便进行拟合、评分并显示结果:from sklearn.model_selection import cross_val_score import numpy as np scores = cross_val_score(model, X, y, cv=5) print('Accuracy:', np.round(scores, 2)) print('Accuracy mean: %0.2f' % (scores.mean()))
准确率如下:
Accuracy: [0.85 0.85 0.77 0.78 0.77] Accuracy mean: 0.81
81%的准确率是一个非常好的起点,远高于在第二章《决策树深入剖析》中,DecisionTreeClassifier
通过交叉验证获得的 76%。
我们在这里使用了cross_val_score
,并将使用GridSearchCV
来调整超参数。接下来,让我们找到一种方法,确保使用StratifiedKFold
时测试折叠保持一致。
StratifiedKFold
在调整超参数时,GridSearchCV
和RandomizedSearchCV
是标准选项。来自第二章《决策树深入剖析》中的一个问题是,cross_val_score
和GridSearchCV
/RandomizedSearchCV
在划分数据时方式不同。
一种解决方案是在使用交叉验证时使用StratifiedKFold
。
分层折叠法在每一折中都包含相同百分比的目标值。如果数据集中目标列中包含 60%的 1 和 40%的 0,则每个分层测试集都包含 60%的 1 和 40%的 0。当折叠是随机的时,可能会出现一个测试集包含 70%-30%的划分,而另一个测试集包含 50%-50%的目标值划分。
提示
使用train_test_split
时,shuffle 和 stratify 参数使用默认设置,以帮助你对数据进行分层抽样。有关一般信息,请参见scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html
。
使用StratifiedKFold
时,执行以下操作:
-
从
sklearn.model_selection
中实现StratifiedKFold
:from sklearn.model_selection import StratifiedKFold
-
接下来,通过选择
n_splits=5
、shuffle=True
和random_state=2
作为StratifiedKFold
的参数来定义折叠数为kfold
。请注意,random_state
提供一致的索引排序,而shuffle=True
允许初始时对行进行随机排序:kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=2)
现在可以在
cross_val_score
、GridSearchCV
和RandomizedSearchCV
中使用kfold
变量,以确保结果的一致性。
现在,让我们回到cross_val_score
,使用kfold
,这样我们就有了一个适当的基准进行比较。
基准模型
现在我们有了一种获取一致折叠的方法,是时候在cross_val_score
中使用cv=kfold
来评分一个正式的基准模型。代码如下:
scores = cross_val_score(model, X, y, cv=kfold)
print('Accuracy:', np.round(scores, 2))
print('Accuracy mean: %0.2f' % (scores.mean()))
准确率如下:
Accuracy: [0.72 0.82 0.75 0.8 0.82]
Accuracy mean: 0.78
分数下降了。这意味着什么?
重要的是不要过于执着于获得最高的评分。在这种情况下,我们在不同的折叠上训练了相同的XGBClassifier
模型,并获得了不同的分数。这显示了在训练模型时,保持测试折叠的一致性的重要性,也说明了为什么分数不一定是最重要的。虽然在选择模型时,获得最好的评分是一个最佳策略,但这里的分数差异表明模型并不一定更好。在这种情况下,两个模型的超参数相同,分数差异归因于不同的折叠。
这里的关键是,当使用GridSearchCV
和RandomizedSearchCV
微调超参数时,使用相同的折叠来获得新的分数,以确保分数的比较是公平的。
结合GridSearchCV
和RandomizedSearchCV
GridSearchCV
在超参数网格中搜索所有可能的组合以找到最佳结果。RandomizedSearchCV
默认选择 10 个随机超参数组合。通常,当GridSearchCV
变得繁琐,无法逐一检查所有超参数组合时,RandomizedSearchCV
就会被使用。
我们将GridSearchCV
和RandomizedSearchCV
合并成一个精简的函数,而不是为它们写两个单独的函数,步骤如下:
-
从
sklearn.model_selection
导入GridSearchCV
和RandomizedSearchCV
:from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
-
定义一个
grid_search
函数,以params
字典作为输入,random=False
:def grid_search(params, random=False):
-
使用标准默认值初始化 XGBoost 分类器:
xgb = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
-
如果
random=True
,则用xgb
和params
字典初始化RandomizedSearchCV
。设置n_iter=20
,以允许 20 个随机组合,而不是 10 个。否则,用相同的输入初始化GridSearchCV
。确保设置cv=kfold
以确保结果的一致性:if random: grid = RandomizedSearchCV(xgb, params, cv=kfold, n_iter=20, n_jobs=-1) else: grid = GridSearchCV(xgb, params, cv=kfold, n_jobs=-1)
-
将
X
和y
拟合到grid
模型:grid.fit(X, y)
-
获取并打印
best_params_
:best_params = grid.best_params_ print("Best params:", best_params)
-
获取并打印
best_score_
:best_score = grid.best_score_ print("Training score: {:.3f}".format(best_score))
现在可以使用grid_search
函数来微调所有超参数。
调整 XGBoost 超参数
XGBoost 有许多超参数,其中一些在前几章已经介绍。下表总结了关键的 XGBoost 超参数,我们在本书中大部分进行了讨论。
注意
此处展示的 XGBoost 超参数并非详尽无遗,而是力求全面。要查看完整的超参数列表,请阅读官方文档中的XGBoost 参数,xgboost.readthedocs.io/en/latest/parameter.html
。
紧随表格后,提供了进一步的解释和示例:
图 6.2 – XGBoost 超参数表
既然已经展示了关键的 XGBoost 超参数,让我们逐一调优它们,进一步了解它们的作用。
应用 XGBoost 超参数
本节中展示的 XGBoost 超参数通常由机器学习从业者进行微调。每个超参数简要解释后,我们将使用前面定义的grid_search
函数测试标准的变动。
n_estimators
回顾一下,n_estimators
表示集成中树的数量。在 XGBoost 中,n_estimators
是训练残差的树的数量。
使用默认的100
初始化n_estimators
的网格搜索,然后将树的数量翻倍至800
,如下所示:
grid_search(params={'n_estimators':[100, 200, 400, 800]})
输出如下:
Best params: {'n_estimators': 100}
Best score: 0.78235
由于我们的数据集较小,增加n_estimators
并没有带来更好的结果。在本章的应用早期停止部分中讨论了寻找理想n_estimators
值的策略。
learning_rate
learning_rate
会缩小每一轮提升中树的权重。通过降低learning_rate
,需要更多的树来获得更好的得分。降低learning_rate
能防止过拟合,因为传递下来的权重较小。
默认值为0.3
,尽管以前版本的 scikit-learn 使用的是0.1
。这里是learning_rate
的一个起始范围,已放入我们的grid_search
函数中:
grid_search(params={'learning_rate':[0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5]})
输出如下:
Best params: {'learning_rate': 0.05}
Best score: 0.79585
改变学习率导致了轻微的增加。如在第四章中所述,从梯度提升到 XGBoost,当n_estimators
增大时,降低learning_rate
可能会带来好处。
max_depth
max_depth
决定了树的长度,相当于分裂的轮次。限制max_depth
可以防止过拟合,因为单棵树的生长受到max_depth
的限制。XGBoost 默认的max_depth
值为六:
grid_search(params={'max_depth':[2, 3, 5, 6, 8]})
输出如下:
Best params: {'max_depth': 2}
Best score: 0.79902
将max_depth
从6
调整到2
后得到了更好的分数。较低的max_depth
值意味着方差已被减少。
gamma
被称为gamma
的值为节点提供了一个阈值,只有超过这个阈值后,节点才会根据损失函数进行进一步的分裂。gamma
没有上限,默认值为0
,而大于10
的值通常认为非常高。增大gamma
会使模型变得更加保守:
grid_search(params={'gamma':[0, 0.1, 0.5, 1, 2, 5]})
输出如下:
Best params: {'gamma': 0.5}
Best score: 0.79574
将gamma
从0
调整到0.5
带来了轻微的改善。
min_child_weight
min_child_weight
表示一个节点进行分裂成子节点所需的最小权重和。如果权重和小于min_child_weight
的值,则不会进行进一步的分裂。通过增大min_child_weight
的值,可以减少过拟合:
grid_search(params={'min_child_weight':[1, 2, 3, 4, 5]})
输出如下:
Best params: {'min_child_weight': 5}
Best score: 0.81219
对min_child_weight
的微调给出了最佳结果。
subsample
subsample
超参数限制了每次提升轮次的训练实例(行)百分比。将subsample
从 100%降低有助于减少过拟合:
grid_search(params={'subsample':[0.5, 0.7, 0.8, 0.9, 1]})
输出如下:
Best params: {'subsample': 0.8}
Best score: 0.79579
分数再次略有提高,表明存在轻微的过拟合现象。
colsample_bytree
类似于subsample
,colsample_bytree
根据给定的百分比随机选择特定的列。colsample_bytree
有助于限制列的影响并减少方差。请注意,colsample_bytree
接受的是百分比作为输入,而不是列的数量:
grid_search(params={'colsample_bytree':[0.5, 0.7, 0.8, 0.9, 1]})
输出如下:
Best params: {'colsample_bytree': 0.7}
Best score: 0.79902
在这里的增益最多也只是微乎其微。建议你尝试自行使用colsample_bylevel
和colsample_bynode
。colsample_bylevel
在每一层深度随机选择列,而colsample_bynode
则在评估每个树的分裂时随机选择列。
微调超参数既是一门艺术,也是一门科学。与这两种学科一样,采取不同的策略都会有效。接下来,我们将探讨早停法作为微调n_estimators
的一种特定策略。
应用早停法
早停法是一种通用方法,用于限制迭代机器学习算法的训练轮次。在本节中,我们将探讨如何通过eval_set
、eval_metric
和early_stopping_rounds
来应用早停法。
什么是早停法?
早停法为迭代机器学习算法的训练轮次提供了限制。与预定义训练轮次不同,早停法允许训练继续,直到n次连续的训练轮次未能带来任何增益,其中n是由用户决定的数字。
仅选择n_estimators
的 100 的倍数是没有意义的。也许最佳值是 737 而不是 700。手动找到这么精确的值可能会很累人,特别是当超参数调整可能需要在后续进行更改时。
在 XGBoost 中,每轮增强后可能会确定一个得分。尽管得分会上下波动,但最终得分将趋于稳定或朝错误方向移动。
当所有后续得分未提供任何增益时,达到峰值分数。在经过 10、20 或 100 个训练轮次未能改进得分后,您会确定峰值。您可以选择轮次数。
在早停策略中,给予模型足够的失败时间是很重要的。如果模型停止得太早,比如在连续五轮没有改进后停止,那么模型可能会错过稍后可以捕捉到的一般模式。与深度学习类似,早停策略经常被使用,梯度提升需要足够的时间来在数据中找到复杂的模式。
对于 XGBoost,early_stopping_rounds
是应用早停策略的关键参数。如果early_stopping_rounds=10
,则模型将在连续 10 个训练轮次未能改进模型后停止训练。类似地,如果early_stopping_rounds=100
,则训练将持续直到连续 100 轮未能改进模型。
现在您了解了什么是早停策略后,让我们来看看eval_set
和eval_metric
。
eval_set 和 eval_metric
early_stopping_rounds
不是一个超参数,而是优化n_estimators
超参数的策略。
通常在选择超参数时,会在所有增强轮次完成后给出测试分数。要使用早停策略,我们需要在每一轮后得到一个测试分数。
可以将eval_metric
和eval_set
用作.fit
的参数,以生成每个训练轮次的测试分数。eval_metric
提供评分方法,通常为分类时的'error'
和回归时的'rmse'
。eval_set
提供要评估的测试集,通常为X_test
和y_test
。
以下六个步骤显示了每轮训练的评估指标,其中默认n_estimators=100
:
-
将数据分为训练集和测试集:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
-
初始化模型:
model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
-
声明
eval_set
:eval_set = [(X_test, y_test)]
-
声明
eval_metric
:eval_metric = 'error'
-
使用
eval_metric
和eval_set
拟合模型:model.fit(X_train, y_train, eval_metric=eval_metric, eval_set=eval_set)
-
检查最终得分:
y_pred = model.predict(X_test) accuracy = accuracy_score(y_test, y_pred) print("Accuracy: %.2f%%" % (accuracy * 100.0))
这里是截断的输出:
[0] validation_0-error:0.15790 [1] validation_0-error:0.10526 [2] validation_0-error:0.11842 [3] validation_0-error:0.13158 [4] validation_0-error:0.11842 … [96] validation_0-error:0.17105 [97] validation_0-error:0.17105 [98] validation_0-error:0.17105 [99] validation_0-error:0.17105 Accuracy: 82.89%
不要对得分过于激动,因为我们尚未使用交叉验证。事实上,我们知道当n_estimators=100
时,StratifiedKFold
交叉验证给出的平均准确率为 78%。得分差异来自于测试集的不同。
early_stopping_rounds
early_stopping_rounds
是在拟合模型时与eval_metric
和eval_set
一起使用的可选参数。
让我们尝试early_stopping_rounds=10
。
以添加early_stopping_rounds=10
的方式重复了上述代码:
model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)
eval_set = [(X_test, y_test)]
eval_metric='error'
model.fit(X_train, y_train, eval_metric="error", eval_set=eval_set, early_stopping_rounds=10, verbose=True)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy: %.2f%%" % (accuracy * 100.0))
输出如下所示:
[0] validation_0-error:0.15790
Will train until validation_0-error hasn't improved in 10 rounds.
[1] validation_0-error:0.10526
[2] validation_0-error:0.11842
[3] validation_0-error:0.13158
[4] validation_0-error:0.11842
[5] validation_0-error:0.14474
[6] validation_0-error:0.14474
[7] validation_0-error:0.14474
[8] validation_0-error:0.14474
[9] validation_0-error:0.14474
[10] validation_0-error:0.14474
[11] validation_0-error:0.15790
Stopping. Best iteration:
[1] validation_0-error:0.10526
Accuracy: 89.47%
结果可能会让人惊讶。早停止显示n_estimators=2
给出了最佳结果,这可能是测试折叠的原因。
为什么只有两棵树?只给模型 10 轮来提高准确性,可能数据中的模式尚未被发现。然而,数据集非常小,因此两轮提升可能给出了最佳结果。
一个更彻底的方法是使用更大的值,比如n_estimators = 5000
和early_stopping_rounds=100
。
通过设置early_stopping_rounds=100
,您将确保达到 XGBoost 提供的默认100
个提升树。
这是一段代码,最多生成 5,000 棵树,并在连续 100 轮未找到任何改进时停止:
model = XGBClassifier(random_state=2, n_estimators=5000)
eval_set = [(X_test, y_test)]
eval_metric="error"
model.fit(X_train, y_train, eval_metric=eval_metric, eval_set=eval_set, early_stopping_rounds=100)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print("Accuracy: %.2f%%" % (accuracy * 100.0))
这里是截断的输出:
[0] validation_0-error:0.15790
Will train until validation_0-error hasn't improved in 100 rounds.
[1] validation_0-error:0.10526
[2] validation_0-error:0.11842
[3] validation_0-error:0.13158
[4] validation_0-error:0.11842
...
[98] validation_0-error:0.17105
[99] validation_0-error:0.17105
[100] validation_0-error:0.17105
[101] validation_0-error:0.17105
Stopping. Best iteration:
[1] validation_0-error:0.10526
Accuracy: 89.47%
在 100 轮提升后,两棵树提供的分数仍然是最佳的。
最后一点要注意的是,早停止在大型数据集中特别有用,当不清楚应该瞄准多高时。
现在,让我们使用早停止的结果,以及之前调整的所有超参数来生成最佳模型。
结合超参数
是时候将本章的所有组件结合起来,以提高通过交叉验证获得的 78%分数。
正如您所知,没有一种适合所有情况的超参数微调方法。一种方法是使用RandomizedSearchCV
输入所有超参数范围。更系统化的方法是逐个处理超参数,使用最佳结果进行后续迭代。所有方法都有优势和局限性。无论采取何种策略,尝试多种变化并在数据到手时进行调整是至关重要的。
一次调整一个超参数
使用系统化的方法,我们一次添加一个超参数,并在途中聚合结果。
n_estimators
尽管n_estimators
值为2
给出了最佳结果,但值得在grid_search
函数上尝试一系列值,该函数使用交叉验证:
grid_search(params={'n_estimators':[2, 25, 50, 75, 100]})
输出如下:
Best params: {'n_estimators': 50}
Best score: 0.78907
没有什么奇怪的,n_estimators=50
,介于先前最佳值 2 和默认值 100 之间,给出了最佳结果。由于早停止没有使用交叉验证,这里的结果是不同的。
最大深度
max_depth
超参数确定每棵树的长度。这里是一个不错的范围:
grid_search(params={'max_depth':[1, 2, 3, 4, 5, 6, 7, 8], 'n_estimators':[50]})
输出如下:
Best params: {'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869
这是一个非常可观的收益。深度为 1 的树称为决策树桩。通过调整只有两个超参数,我们从基线模型中获得了四个百分点的增长。
保留顶级值的方法的局限性在于我们可能会错过更好的组合。也许n_estimators=2
或n_estimators=100
与max_depth
结合会给出更好的结果。让我们看看:
grid_search(params={'max_depth':[1, 2, 3, 4, 6, 7, 8], 'n_estimators':[2, 50, 100]})
输出如下:
Best params: {'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869
n_estimators=50
和max_depth=1
仍然给出了最佳结果,因此我们将继续使用它们,并稍后回到我们的早停止分析。
学习率
由于n_estimators
合理较低,调整learning_rate
可能会改善结果。以下是一个标准范围:
grid_search(params={'learning_rate':[0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5], 'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'learning_rate': 0.3, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869
这是与之前获得的得分相同的结果。请注意,learning_rate
值为 0.3 是 XGBoost 提供的默认值。
min_child_weight
让我们看看调整分裂为子节点所需的权重总和是否能提高得分:
grid_search(params={'min_child_weight':[1, 2, 3, 4, 5], 'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'max_depth': 1, 'min_child_weight': 1, 'n_estimators': 50}
Best score: 0.83869
在这种情况下,最佳得分保持不变。请注意,min_child_weight
的默认值是 1。
subsample
如果减少方差是有益的,subsample
可能通过限制样本的百分比来起作用。然而,在此情况下,初始只有 303 个样本,样本量较少使得调整超参数以提高得分变得困难。以下是代码:
grid_search(params={'subsample':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'max_depth': 1, 'n_estimators': 50, 'subsample': 1}
Best score: 0.83869
仍然没有提升。此时,你可能会想,若使用n_estimators=2
,是否会继续带来新的提升。
让我们通过使用到目前为止所使用的值进行全面的网格搜索,找出结果。
grid_search(params={'subsample':[0.5, 0.6, 0.7, 0.8, 0.9, 1],
'min_child_weight':[1, 2, 3, 4, 5],
'learning_rate':[0.1, 0.2, 0.3, 0.4, 0.5],
'max_depth':[1, 2, 3, 4, 5],
'n_estimators':[2]})
输出结果如下:
Best params: {'learning_rate': 0.5, 'max_depth': 2, 'min_child_weight': 4, 'n_estimators': 2, 'subsample': 0.9}
Best score: 0.81224
一台仅有两棵树的分类器表现较差并不令人意外。尽管初始得分较高,但它没有经过足够的迭代,导致超参数没有得到显著调整。
超参数调整
当调整超参数方向时,RandomizedSearchCV
非常有用,因为它涵盖了广泛的输入范围。
这是一个将新输入与先前知识结合的超参数值范围。通过RandomizedSearchCV
限制范围增加了找到最佳组合的机会。回想一下,RandomizedSearchCV
在总组合数量过多以至于网格搜索太耗时时特别有用。以下选项共有 4,500 种可能的组合:
grid_search(params={'subsample':[0.5, 0.6, 0.7, 0.8, 0.9, 1],
'min_child_weight':[1, 2, 3, 4, 5],
'learning_rate':[0.1, 0.2, 0.3, 0.4, 0.5],
'max_depth':[1, 2, 3, 4, 5, None],
'n_estimators':[2, 25, 50, 75, 100]},
random=True)
输出结果如下:
Best params: {'subsample': 0.6, 'n_estimators': 25, 'min_child_weight': 4, 'max_depth': 4, 'learning_rate': 0.5}
Best score: 0.82208
这很有趣。不同的值取得了不错的结果。
我们将使用最佳得分对应的超参数继续调整。
Colsample
现在,我们按顺序尝试colsample_bytree
、colsample_bylevel
和colsample_bynode
。
colsample_bytree
让我们从colsample_bytree
开始:
grid_search(params={'colsample_bytree':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'colsample_bytree': 1, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869
得分没有改善。接下来,尝试colsample_bylevel
。
colsample_bylevel
使用以下代码尝试colsample_bylevel
:
grid_search(params={'colsample_bylevel':[0.5, 0.6, 0.7, 0.8, 0.9, 1],'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'colsample_bylevel': 1, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.83869
仍然没有提升。
看来我们在浅层数据集上已经达到峰值。让我们尝试不同的方法。与其单独使用colsample_bynode
,不如一起调整所有 colsamples。
colsample_bynode
尝试以下代码:
grid_search(params={'colsample_bynode':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'colsample_bylevel':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'colsample_bytree':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'colsample_bylevel': 0.9, 'colsample_bynode': 0.5, 'colsample_bytree': 0.8, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.84852
非常好。通过共同调整,colsamples 组合起来取得了目前为止的最高得分,比原始得分高出了 5 个百分点。
gamma
我们将尝试微调的最后一个超参数是gamma
。这里是一个旨在减少过拟合的gamma
值范围:
grid_search(params={'gamma':[0, 0.01, 0.05, 0.1, 0.5, 1, 2, 3], 'colsample_bylevel':[0.9], 'colsample_bytree':[0.8], 'colsample_bynode':[0.5], 'max_depth':[1], 'n_estimators':[50]})
输出结果如下:
Best params: {'colsample_bylevel': 0.9, 'colsample_bynode': 0.5, 'colsample_bytree': 0.8, 'gamma': 0, 'max_depth': 1, 'n_estimators': 50}
Best score: 0.84852
gamma
仍然保持在默认值0
。
由于我们的最佳得分比原始得分高出超过五个百分点,这在 XGBoost 中是一个不小的成就,我们将在这里停止。
总结
在这一章节中,你通过使用 StratifiedKFold
建立了一个基准的 XGBoost 模型,为超参数调优做了准备。接着,你将 GridSearchCV
和 RandomizedSearchCV
结合成一个强大的功能。你学习了 XGBoost 关键超参数的标准定义、范围和应用,并且掌握了一种名为早停(early stopping)的新技巧。你将所有功能、超参数和技巧进行了综合应用,成功地对心脏病数据集进行了调优,从默认的 XGBoost 分类器中提升了令人印象深刻的五个百分点。
XGBoost 超参数调优需要时间来掌握,而你已经走在了正确的道路上。调优超参数是一项关键技能,它将机器学习专家与初学者区分开来。了解 XGBoost 的超参数不仅有用,而且对于最大化机器学习模型的性能至关重要。
恭喜你完成了这一重要章节。
接下来,我们展示了一个 XGBoost 回归的案例研究,从头到尾,突出展示了 XGBClassifier
的强大功能、适用范围和应用。
第七章:第七章:利用 XGBoost 发现外星行星
在本章中,你将穿越星际,尝试使用XGBClassifier
来发现外星行星。
本章的目的有两个。首先,掌握从头到尾使用 XGBoost 进行分析的实践经验非常重要,因为在实际应用中,这正是你通常需要做的事情。尽管你可能无法凭借 XGBoost 独立发现外星行星,但本章中你所实施的策略,包括选择正确的评分指标并根据该指标精心调整超参数,适用于 XGBoost 的任何实际应用。第二个原因是,本案例研究非常重要,因为所有机器学习从业者必须熟练处理不平衡数据集,这是本章的关键主题。
具体来说,你将掌握使用scale_pos_weight
等技能。要从XGBClassifier
中获得最佳结果,需要仔细分析数据的不平衡性,并明确手头的目标。在本章中,XGBClassifier
是贯穿始终的核心工具,用来分析光数据并预测宇宙中的外星行星。
本章将涵盖以下主要内容:
-
寻找外星行星
-
分析混淆矩阵
-
重采样不平衡数据
-
调优和缩放 XGBClassifier
技术要求
寻找外星行星
在本节中,我们将通过分析外星行星数据集来开始寻找外星行星。我们将在尝试通过绘制和观察光图来探测外星行星之前,提供外星行星发现的历史背景。绘制时间序列是一个有价值的机器学习技能,可以用来洞察任何时间序列数据集。最后,在揭示一个明显的缺陷之前,我们将利用机器学习做出初步预测。
历史背景
自古以来,天文学家就一直在从光线中收集信息。随着望远镜的出现,天文学知识在 17 世纪迎来了飞跃。望远镜与数学模型的结合使得 18 世纪的天文学家能够精确预测我们太阳系内的行星位置和日食现象。
在 20 世纪,天文学研究随着技术的进步和数学的复杂化不断发展。围绕其他恒星运转的行星——外星行星——被发现位于宜居区。位于宜居区的行星意味着该外星行星的位置和大小与地球相当,因此它可能存在液态水和生命。
这些外行星不是通过望远镜直接观测的,而是通过恒星光的周期性变化来推测的。周期性围绕一颗恒星旋转、足够大以阻挡可检测的恒光的一部分的物体,按定义是行星。从恒光中发现外行星需要在较长时间内测量光的波动。由于光的变化通常非常微小,因此很难判断是否确实存在外行星。
本章我们将使用 XGBoost 预测恒星是否有外行星。
外行星数据集
你在 第四章《从梯度提升到 XGBoost》中预览了外行星数据集,揭示了 XGBoost 在处理大数据集时相较于其他集成方法的时间优势。本章将更深入地了解外行星数据集。
这个外行星数据集来自于 NASA Kepler 太空望远镜,第 3 次任务,2016 年夏季。关于数据源的信息可以在 Kaggle 上找到,链接为 www.kaggle.com/keplersmachines/kepler-labelled-time-series-data
。在数据集中的所有恒星中,5,050 颗没有外行星,而 37 颗有外行星。
超过 300 列和 5000 多行数据,总共有超过 150 万条数据点。当乘以 100 棵 XGBoost 树时,总共有 1.5 亿多个数据点。为了加速处理,我们从数据的一个子集开始。使用子集是处理大数据集时的常见做法,以节省时间。
pd.read_csv
包含一个 nrows
参数,用于限制行数。请注意,nrows=n
会选择数据集中的前 n 行。根据数据结构,可能需要额外的代码来确保子集能够代表整个数据集。我们开始吧。
导入 pandas
,然后用 nrows=400
加载 exoplanets.csv
。然后查看数据:
import pandas as pd
df = pd.read_csv('exoplanets.csv', nrows=400)
df.head()
输出应如下所示:
图 7.1 – 外行星数据框
数据框下列出的大量列(3198列)是有道理的。在寻找光的周期性变化时,需要足够的数据点来发现周期性。我们太阳系内的行星公转周期从 88 天(水星)到 165 年(海王星)不等。如果要检测外行星,必须频繁检查数据点,以便不会错过行星在恒星前面经过的瞬间。
由于只有 37 颗外行星恒星,因此了解子集中包含了多少颗外行星恒星是很重要的。
.value_counts()
方法用于确定特定列中每个值的数量。由于我们关注的是 LABEL
列,可以使用以下代码查找外行星恒星的数量:
df['LABEL'].value_counts()
输出如下所示:
1 363 2 37 Name: LABEL, dtype: int64
我们的子集包含了所有的外行星恒星。如 .head()
所示,外行星恒星位于数据的开头。
绘制数据图表
期望的是,当外行星遮挡了恒星的光时,光通量会下降。如果光通量下降是周期性的,那么很可能是外行星在起作用,因为根据定义,行星是绕恒星运行的大型天体。
让我们通过绘图来可视化数据:
-
导入
matplotlib
、numpy
和seaborn
,然后将seaborn
设置为暗网格,如下所示:import matplotlib.pyplot as plt import numpy as np import seaborn as sns sns.set()
在绘制光变曲线时,
LABEL
列不感兴趣。LABEL
列将作为我们机器学习的目标列。提示
推荐使用
seaborn
来改进你的matplotlib
图表。sns.set()
默认设置提供了一个漂亮的浅灰色背景和白色网格。此外,许多标准图表,如plt.hist()
,在应用 Seaborn 默认设置后看起来更加美观。有关 Seaborn 的更多信息,请访问seaborn.pydata.org/
。 -
现在,让我们将数据拆分为
X
(预测列,我们将绘制它们)和y
(目标列)。请注意,对于外行星数据集,目标列是第一列,而不是最后一列:X = df.iloc[:,1:] y = df.iloc[:,0]
-
现在编写一个名为
light_plot
的函数,该函数以数据的索引(行号)为输入,将所有数据点绘制为y坐标(光通量),并将观测次数作为x坐标。图表应使用以下标签:def light_plot(index): y_vals = X.iloc[index] x_vals = np.arange(len(y_vals)) plt.figure(figsize=(15,8)) plt.xlabel('Number of Observations') plt.ylabel('Light Flux') plt.title('Light Plot ' + str(index), size=15) plt.plot(x_vals, y_vals) plt.show()
-
现在,调用函数绘制第一个索引。这颗恒星已被分类为外行星恒星:
light_plot(0)
这是我们第一个光曲线图的预期图表:
图 7.2 – 光曲线 0. 存在周期性光通量下降
数据中存在明显的周期性下降。然而,仅凭这张图表,无法明确得出有外行星存在的结论。
-
做个对比,将这个图与第 37 个索引的图进行比较,后者是数据集中第一个非外行星恒星:
light_plot(37)
这是第 37 个索引的预期图表:
图 7.3 – 光曲线 37
存在光强度的增加和减少,但不是贯穿整个范围。
数据中确实存在明显的下降,但它们在整个图表中并不是周期性的。下降的频率并没有一致地重复。仅凭这些证据,还不足以确定是否存在外行星。
-
这是外行星恒星的第二个光曲线图:
light_plot(1)
这是第一个索引的预期图表:
图 7.4 – 明显的周期性下降表明存在外行星
图表显示出明显的周期性,且光通量有大幅下降,这使得外行星的存在极为可能!如果所有图表都如此清晰,机器学习就不再必要。正如其他图表所示,得出外行星存在的结论通常没有这么明确。
这里的目的是突出数据的特点以及仅凭视觉图表分类系外行星的难度。天文学家使用不同的方法来分类系外行星,而机器学习就是其中的一种方法。
尽管这个数据集是一个时间序列,但目标不是预测下一个时间单位的光通量,而是基于所有数据来分类恒星。在这方面,机器学习分类器可以用来预测给定的恒星是否有系外行星。这个思路是用提供的数据来训练分类器,进而用它来预测新数据中的系外行星。在本章中,我们尝试使用XGBClassifier
来对数据中的系外行星进行分类。在开始分类数据之前,我们必须先准备数据。
准备数据
我们在前一节中已经看到,并非所有图表都足够清晰,无法仅凭图表来确定系外行星的存在。这正是机器学习可以大有帮助的地方。首先,让我们为机器学习准备数据:
-
首先,我们需要确保数据集是数值型的且没有空值。使用
df.info()
来检查数据类型和空值:df.info()
这是预期的输出:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 400 entries, 0 to 399 Columns: 3198 entries, LABEL to FLUX.3197 dtypes: float64(3197), int64(1) memory usage: 9.8 MB
子集包含 3,197 个浮点数和 1 个整数,因此所有列都是数值型的。由于列数较多,因此没有提供关于空值的信息。
-
我们可以对
.null()
方法使用.sum()
两次,第一次是对每一列的空值求和,第二次是对所有列的空值求和:df.isnull().sum().sum()
预期的输出如下:
0
由于数据中没有空值,并且数据是数值型的,我们将继续进行机器学习。
初始的 XGBClassifier
要开始构建初始的 XGBClassifier,请按照以下步骤操作:
-
导入
XGBClassifier
和accuracy_score
:from xgboost import XGBClassifier from sklearn.metrics import accuracy_score
-
将模型拆分为训练集和测试集:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
-
使用
booster='gbtree'
、objective='binary:logistic'
和random_state=2
作为参数构建并评分模型:model = XGBClassifier(booster='gbtree', objective='binary:logistic', random_state=2)model.fit(X_train, y_train)y_pred = model.predict(X_test)score = accuracy_score(y_pred, y_test)print('Score: ' + str(score))
评分如下:
Score: 0.89
正确分类 89%的恒星看起来是一个不错的起点,但有一个明显的问题。
你能弄明白吗?
假设你向天文学教授展示了你的模型。假设你的教授在数据分析方面受过良好训练,教授可能会回应:“我看到你得到了 89%的准确率,但系外行星仅占数据的 10%,那么你怎么知道你的结果不是比一个总是预测没有系外行星的模型更好呢?”
这就是问题所在。如果模型判断没有恒星包含系外行星,它的准确率将约为 90%,因为 10 颗恒星中有 9 颗不包含系外行星。
对于不平衡的数据,准确度并不够。
分析混淆矩阵
混淆矩阵是一个表格,用来总结分类模型的正确预测和错误预测。混淆矩阵非常适合分析不平衡数据,因为它提供了哪些预测正确,哪些预测错误的更多信息。
对于外行星子集,以下是完美混淆矩阵的预期输出:
array([[88, 0],
[ 0, 12]])
当所有正例条目都位于左对角线时,模型的准确度为 100%。在此情况下,完美的混淆矩阵预测了 88 个非外行星恒星和 12 个外行星恒星。请注意,混淆矩阵不提供标签,但在这种情况下,可以根据大小推断标签。
在深入细节之前,让我们使用 scikit-learn 查看实际的混淆矩阵。
confusion_matrix
从 sklearn.metrics
导入 confusion_matrix
,代码如下:
from sklearn.metrics import confusion_matrix
使用 y_test
和 y_pred
作为输入运行 confusion_matrix
(这些变量在上一部分中获得),确保将 y_test
放在前面:
confusion_matrix(y_test, y_pred)
输出如下:
array([[86, 2],
[9, 3]])
混淆矩阵对角线上的数字揭示了 86
个正确的非外行星恒星预测,以及仅 3
个正确的外行星恒星预测。
在矩阵的右上角,数字 2
显示有两个非外行星恒星被误分类为外行星恒星。同样,在矩阵的左下角,数字 9
显示有 9
个外行星恒星被误分类为非外行星恒星。
横向分析时,88 个非外行星恒星中有 86 个被正确分类,而 12 个外行星恒星中只有 3 个被正确分类。
如你所见,混淆矩阵揭示了模型预测的重要细节,而准确度得分无法捕捉到这些细节。
classification_report
在上一部分中混淆矩阵所揭示的各种百分比数值包含在分类报告(classification report)中。让我们查看分类报告:
-
从
sklearn.metrics
导入classification_report
:from sklearn.metrics import classification_report
-
将
y_test
和y_pred
放入classification_report
中,确保将y_test
放在前面。然后将classification_report
放入全局打印函数中,以确保输出对齐且易于阅读:print(classification_report(y_test, y_pred))
这是预期的输出:
precision recall f1-score support 1 0.91 0.98 0.94 88 2 0.60 0.25 0.35 12 accuracy 0.89 100 macro avg 0.75 0.61 0.65 100 weighted avg 0.87 0.89 0.87 100
了解上述得分的含义很重要,让我们逐一回顾它们。
精确度(Precision)
精确度给出了正类预测(2s)中实际上是正确的预测。它在技术上是通过真正例和假正例来定义的。
真正例(True Positives)
以下是关于真正例的定义和示例:
-
定义 – 正确预测为正类的标签数。
-
示例 – 2 被正确预测为 2。
假正例(False Positives)
以下是关于假正例的定义和示例:
-
定义 – 错误地预测为负类的正标签数。
-
示例 – 对于外行星恒星,2 被错误地预测为 1。
精确度的定义通常以其数学形式表示如下:
这里,TP 代表真正例(True Positive),FP 代表假正例(False Positive)。
在外行星数据集中,我们有以下两种数学形式:
和
精确率给出了每个目标类的正确预测百分比。接下来,让我们回顾分类报告中揭示的其他关键评分指标。
召回率
召回率给出了你的预测发现的正样本的百分比。召回率是正确预测的正样本数量除以真正例加上假负例的总和。
虚假负例
这里是虚假负例的定义和示例:
-
定义 – 错误预测为负类的标签数量。
-
示例 – 对于外行星星的预测,2 类被错误地预测为 1 类。
数学形式如下所示:
这里 TP 代表真正例(True Positive),FN 代表假负例(False Negative)。
在外行星数据集中,我们有以下内容:
和
召回率告诉你找到了多少正样本。在外行星的例子中,只有 25%的外行星被找到了。
F1 分数
F1 分数是精确率和召回率的调和平均值。使用调和平均值是因为精确率和召回率基于不同的分母,调和平均值将它们统一起来。当精确率和召回率同等重要时,F1 分数是最优的。请注意,F1 分数的范围从 0 到 1,1 为最高分。
替代评分方法
精确率、召回率和 F1 分数是 scikit-learn 提供的替代评分方法。标准评分方法的列表可以在官方文档中找到:scikit-learn.org/stable/modules/model_evaluation.html
。
提示
对于分类数据集,准确率通常不是最佳选择。另一种常见的评分方法是roc_auc_score
,即接收者操作特征曲线下面积。与大多数分类评分方法一样,越接近 1,结果越好。更多信息请参见scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html#sklearn.metrics.roc_auc_score
。
选择评分方法时,了解目标至关重要。外行星数据集的目标是找到外行星。这一点是显而易见的。但并不明显的是,如何选择最佳评分方法以实现期望的结果。
想象两种不同的情境:
-
场景 1:机器学习模型预测的 4 颗外行星星中,实际为外行星的有 3 颗:3/4 = 75% 精确率。
-
场景 2:在 12 颗外行星星中,模型正确预测了 8 颗外行星星(8/12 = 66% 召回率)。
哪种情况更为理想?
答案是这取决于情况。召回率适合用于标记潜在的正样本(如外行星),目的是尽可能找到所有的正样本。精确率则适用于确保预测的正样本(外行星)确实是正样本。
天文学家不太可能仅仅因为机器学习模型说发现了外星行星就宣布这一发现。他们更可能在确认或否定这一发现之前,仔细检查潜在的外星行星,并根据额外的证据作出判断。
假设机器学习模型的目标是尽可能多地找到外星行星,召回率是一个极好的选择。为什么?召回率告诉我们找到了多少颗外星行星(例如:2/12、5/12、12/12)。让我们尝试找到所有的外星行星。
精确率说明
更高的精确率并不意味着更多的外星行星。例如,1/1 的召回率是 100%,但只发现了一颗外星行星。
recall_score
如前一节所述,我们将使用召回率作为评分方法,针对外星行星数据集寻找尽可能多的外星行星。让我们开始吧:
-
从
sklearn.metrics
导入recall_score
:from sklearn.metrics import recall_score
默认情况下,
recall_score
报告的是正类的召回率,通常标记为1
。在外星行星数据集中,正类标记为2
,负类标记为1
,这比较少见。 -
为了获得外星行星的
recall_score
值,输入y_test
和y_pred
作为recall_score
的参数,并设置pos_label=2
:recall_score(y_test, y_pred, pos_label=2)
外星行星的评分如下:
0.25
这是由分类报告中召回率为2
时给出的相同百分比,即外星行星。接下来,我们将不再使用accuracy_score
,而是使用recall_score
及其前述参数作为我们的评分指标。
接下来,让我们了解一下重新采样,它是改善失衡数据集得分的重要策略。
重新采样失衡数据
现在我们有了一个适当的评分方法来发现外星行星,接下来是探索如重新采样、欠采样和过采样等策略,以纠正导致低召回率的失衡数据。
重新采样
应对失衡数据的一种策略是重新采样数据。可以通过减少多数类的行数来进行欠采样,或通过重复少数类的行数来进行过采样。
欠采样
我们的探索从从 5,087 行中选取了 400 行开始。这是一个欠采样的例子,因为子集包含的行数比原始数据少。
我们来编写一个函数,使其能够按任意行数对数据进行欠采样。这个函数应该返回召回率评分,这样我们就能看到欠采样如何改变结果。我们将从评分函数开始。
评分函数
以下函数接收 XGBClassifier 和行数作为输入,输出外星行星的混淆矩阵、分类报告和召回率。
以下是步骤:
-
定义一个函数
xgb_clf
,它接收model
(机器学习模型)和nrows
(行数)作为输入:def xgb_clf(model, nrows):
-
使用
nrows
加载 DataFrame,然后将数据分成X
和y
,并划分训练集和测试集:df = pd.read_csv('exoplanets.csv', nrows=nrows) X = df.iloc[:,1:] y = df.iloc[:,0] X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
-
初始化模型,将模型拟合到训练集,并使用
y_test
、y_pred
和pos_label=2
作为recall_score
的输入对测试集进行评分:model.fit(X_train, y_train) y_pred = xg_clf.predict(X_test) score = recall_score(y_test, y_pred, pos_label=2)
-
打印混淆矩阵和分类报告,并返回评分:
print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) return score
现在,我们可以通过欠采样减少行数,并观察评分的变化。
欠采样 nrows
让我们先将 nrows
加倍至 800
。这仍然是欠采样,因为原始数据集有 5087
行:
xgb_clf(XGBClassifier(random_state=2), nrows=800)
这是预期的输出:
[[189 1]
[ 9 1]]
precision recall f1-score support
1 0.95 0.99 0.97 190
2 0.50 0.10 0.17 10
accuracy 0.95 200
macro avg 0.73 0.55 0.57 200
weighted avg 0.93 0.95 0.93 200
0.1
尽管非外行星星体的召回率几乎完美,但混淆矩阵显示只有 10 个外行星星体中的 1 个被召回。
接下来,将 nrows
从 400
减少到 200
:
xgb_clf(XGBClassifier(random_state=2), nrows=200)
这是预期的输出:
[[37 0]
[ 8 5]]
precision recall f1-score support
1 0.82 1.00 0.90 37
2 1.00 0.38 0.56 13
accuracy 0.84 50
macro avg 0.91 0.69 0.73 50
weighted avg 0.87 0.84 0.81 50
这个结果稍微好一些。通过减少 nrows
,召回率有所提高。
让我们看看如果我们精确平衡类会发生什么。由于有 37 个外行星星体,37 个非外行星星体就能平衡数据。
使用 nrows=74
运行 xgb_clf
函数:
xgb_clf(XGBClassifier(random_state=2), nrows=74)
这是预期的输出:
[[6 2]
[5 6]]
precision recall f1-score support
1 0.55 0.75 0.63 8
2 0.75 0.55 0.63 11
accuracy 0.63 19
macro avg 0.65 0.65 0.63 19
weighted avg 0.66 0.63 0.63 19
0.5454545454545454
尽管子集要小得多,但这些结果仍然令人满意。
接下来,让我们看看当我们应用过采样策略时会发生什么。
过采样
另一种重采样技术是过采样。与其删除行,过采样通过复制和重新分配正类样本来增加行数。
尽管原始数据集有超过 5000 行,但我们仍然使用 nrows=400
作为起点,以加快过程。
当 nrows=400
时,正类与负类样本的比例为 10:1。为了获得平衡,我们需要 10 倍数量的正类样本。
我们的策略如下:
-
创建一个新的 DataFrame,复制正类样本九次。
-
将新的 DataFrame 与原始数据框连接,得到 10:10 的比例。
在继续之前,需要做一个警告。如果在拆分数据集成训练集和测试集之前进行重采样,召回评分将会被夸大。你能看出为什么吗?
在重采样时,将对正类样本进行九次复制。将数据拆分为训练集和测试集后,复制的样本可能会同时出现在两个数据集中。因此,测试集将包含大多数与训练集相同的数据点。
合适的策略是先将数据拆分为训练集和测试集,然后再进行重采样。如前所述,我们可以使用 X_train
、X_test
、y_train
和 y_test
。让我们开始:
-
使用
pd.merge
按照左索引和右索引合并X_train
和y_train
,如下所示:df_train = pd.merge(y_train, X_train, left_index=True, right_index=True)
-
使用
np.repeat
创建一个包含以下内容的 DataFrame,new_df
:a) 正类样本的值:
df_train[df_train['LABEL']==2.values
。b) 复制的次数——在本例中为
9
c)
axis=0
参数指定我们正在处理列:new_df = pd.DataFrame(np.repeat(df_train[df_train['LABEL']==2].values,9,axis=0))
-
复制列名:
new_df.columns = df_train.columns
-
合并 DataFrame:
df_train_resample = pd.concat([df_train, new_df])
-
验证
value_counts
是否如预期:df_train_resample['LABEL'].value_counts()
预期的输出如下:
1.0 275 2.0 250 Name: LABEL, dtype: int64
-
使用重采样后的 DataFrame 拆分
X
和y
:X_train_resample = df_train_resample.iloc[:,1:] y_train_resample = df_train_resample.iloc[:,0]
-
在重采样后的训练集上拟合模型:
model = XGBClassifier(random_state=2) model.fit(X_train_resample, y_train_resample)
-
使用
X_test
和y_test
对模型进行评分。将混淆矩阵和分类报告包括在结果中:y_pred = model.predict(X_test) score = recall_score(y_test, y_pred, pos_label=2) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) print(score)
得分如下:
[[86 2] [ 8 4]] precision recall f1-score support 1 0.91 0.98 0.95 88 2 0.67 0.33 0.44 12 accuracy 0.90 100 macro avg 0.79 0.66 0.69 100 weighted avg 0.89 0.90 0.88 100 0.3333333333333333
通过适当地留出测试集,过采样达到了 33.3%的召回率,这个得分是之前 17%的一倍,尽管仍然太低。
提示
imblearn
,必须下载才能使用。我通过前面的重采样代码实现了与 SMOTE 相同的结果。
由于重采样的效果最多只能带来适度的提升,是时候调整 XGBoost 的超参数了。
调整和缩放 XGBClassifier
在本节中,我们将微调并缩放 XGBClassifier,以获得外星行星数据集的最佳recall_score
值。首先,您将使用scale_pos_weight
调整权重,然后运行网格搜索以找到最佳的超参数组合。此外,您将为不同的数据子集评分,然后整合并分析结果。
调整权重
在第五章,XGBoost 揭秘中,你使用了scale_pos_weight
超参数来解决 Higgs 玻色子数据集中的不平衡问题。scale_pos_weight
是一个用来调整正类权重的超参数。这里强调的正是非常重要的,因为 XGBoost 假设目标值为1
的是正类,目标值为0
的是负类。
在外星行星数据集中,我们一直使用数据集提供的默认值1
为负类,2
为正类。现在,我们将使用.replace()
方法将其改为0
为负类,1
为正类。
replace
.replace()
方法可以用来重新分配值。以下代码在LABEL
列中将1
替换为0
,将2
替换为1
:
df['LABEL'] = df['LABEL'].replace(1, 0)
df['LABEL'] = df['LABEL'].replace(2, 1)
如果两行代码顺序颠倒,所有列值都会变成 0,因为所有的 2 都会变成 1,然后所有的 1 会变成 0。在编程中,顺序非常重要!
使用value_counts
方法验证计数:
df['LABEL'].value_counts()
这里是预期的输出:
0 363
1 37
Name: LABEL, dtype: int64
正类现在标记为1
,负类标记为0
。
scale_pos_weight
现在是时候构建一个新的XGBClassifier
,并设置scale_pos_weight=10
,以解决数据中的不平衡问题:
-
将新的 DataFrame 拆分为
X
,即预测列和y
,即目标列:X = df.iloc[:,1:] y = df.iloc[:,0]
-
将数据拆分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
-
构建、拟合、预测并评分
XGBClassifier
,设置scale_pos_weight=10
。打印出混淆矩阵和分类报告以查看完整结果:model = XGBClassifier(scale_pos_weight=10, random_state=2) model.fit(X_train, y_train) y_pred = model.predict(X_test) score = recall_score(y_test, y_pred) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred)) print(score)
这里是预期的输出:
[[86 2] [ 8 4]] precision recall f1-score support 0 0.91 0.98 0.95 88 1 0.67 0.33 0.44 12 accuracy 0.90 100 macro avg 0.79 0.66 0.69 100 weighted avg 0.89 0.90 0.88 100 0.3333333333333333
结果与上一节的重采样方法相同。
我们从头开始实现的过采样方法给出的预测结果与scale_pos_weight
的XGBClassifier
一致。
调整 XGBClassifier
现在是时候看看超参数微调是否能够提高精度了。
在微调超参数时,标准做法是使用 GridSearchCV
和 RandomizedSearchCV
。两者都需要进行两折或更多折的交叉验证。由于我们的初始模型效果不佳,并且在大型数据集上进行多折交叉验证计算成本高昂,因此我们尚未实施交叉验证。
一种平衡的方法是使用 GridSearchCV
和 RandomizedSearchCV
,并采用两个折叠来节省时间。为了确保结果一致,推荐使用 StratifiedKFold
(第六章, XGBoost 超参数)。我们将从基准模型开始。
基准模型
以下是构建基准模型的步骤,该模型实现了与网格搜索相同的 k 折交叉验证:
-
导入
GridSearchCV
、RandomizedSearchCV
、StratifiedKFold
和cross_val_score
:from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, StratifiedKFold, cross_val_score
-
将
StratifiedKFold
初始化为kfold
,参数为n_splits=2
和shuffle=True
:kfold = StratifiedKFold(n_splits=2, shuffle=True, random_state=2)
-
使用
scale_pos_weight=10
初始化XGBClassifier
,因为负类样本是正类样本的 10 倍:model = XGBClassifier(scale_pos_weight=10, random_state=2)
-
使用
cross_val_score
对模型进行评分,参数为cv=kfold
和score='recall'
,然后显示得分:scores = cross_val_score(model, X, y, cv=kfold, scoring='recall') print('Recall: ', scores) print('Recall mean: ', scores.mean())
分数如下:
Recall: [0.10526316 0.27777778] Recall mean: 0.1915204678362573
使用交叉验证后,得分稍微差一些。当正例非常少时,训练集和测试集中的行的选择会产生差异。StratifiedKFold
和 train_test_split
的不同实现可能导致不同的结果。
网格搜索
我们将实现来自 第六章 的 grid_search
函数的一个变体, XGBoost 超参数,以便微调超参数:
-
新函数将参数字典作为输入,同时还提供一个使用
RandomizedSearchCV
的随机选项。此外,X
和y
被作为默认参数提供,用于其他子集,并且评分方法为召回率,具体如下:def grid_search(params, random=False, X=X, y=y, model=XGBClassifier(random_state=2)): xgb = model if random: grid = RandomizedSearchCV(xgb, params, cv=kfold, n_jobs=-1, random_state=2, scoring='recall') else: grid = GridSearchCV(xgb, params, cv=kfold, n_jobs=-1, scoring='recall') grid.fit(X, y) best_params = grid.best_params_ print("Best params:", best_params) best_score = grid.best_score_ print("Best score: {:.5f}".format(best_score))
-
让我们运行不使用默认设置的网格搜索,试图提高得分。以下是一些初始的网格搜索及其结果:
a) 网格搜索 1:
grid_search(params={'n_estimators':[50, 200, 400, 800]})
结果:
Best params: {'n_estimators': 50}Best score: 0.19152
b) 网格搜索 2:
grid_search(params={'learning_rate':[0.01, 0.05, 0.2, 0.3]})
结果:
Best params: {'learning_rate': 0.01} Best score: 0.40351
c) 网格搜索 3:
grid_search(params={'max_depth':[1, 2, 4, 8]})
结果:
Best params: {'max_depth': 2} Best score: 0.24415
d) 网格搜索 4:
grid_search(params={'subsample':[0.3, 0.5, 0.7, 0.9]})
结果:
Best params: {'subsample': 0.5} Best score: 0.21637
e) 网格搜索 5:
grid_search(params={'gamma':[0.05, 0.1, 0.5, 1]})
结果:
Best params: {'gamma': 0.05} Best score: 0.24415
-
改变
learning_rate
、max_depth
和gamma
取得了提升。让我们通过缩小范围来尝试将它们组合起来:grid_search(params={'learning_rate':[0.001, 0.01, 0.03], 'max_depth':[1, 2], 'gamma':[0.025, 0.05, 0.5]})
分数如下:
Best params: {'gamma': 0.025, 'learning_rate': 0.001, 'max_depth': 2} Best score: 0.53509
-
还值得尝试
max_delta_step
,XGBoost 仅建议在不平衡数据集上使用。默认值为 0,增加步骤会导致模型更加保守:grid_search(params={'max_delta_step':[1, 3, 5, 7]})
分数如下:
Best params: {'max_delta_step': 1} Best score: 0.24415
-
作为最终策略,我们通过在随机搜索中结合
subsample
和所有列样本:grid_search(params={'subsample':[0.3, 0.5, 0.7, 0.9, 1], 'colsample_bylevel':[0.3, 0.5, 0.7, 0.9, 1], 'colsample_bynode':[0.3, 0.5, 0.7, 0.9, 1], 'colsample_bytree':[0.3, 0.5, 0.7, 0.9, 1]}, random=True)
分数如下:
Best params: {'subsample': 0.3, 'colsample_bytree': 0.7, 'colsample_bynode': 0.7, 'colsample_bylevel': 1} Best score: 0.35380
不继续使用包含 400
行数据的这个数据子集,而是切换到包含 74
行数据的平衡子集(欠采样),以比较结果。
平衡子集
包含 74
行数据的平衡子集数据点最少,它也是测试最快的。
由于X
和y
最后一次是在函数内为平衡子集定义的,因此需要显式地定义它们。X_short
和y_short
的新定义如下:
X_short = X.iloc[:74, :]
y_short = y.iloc[:74]
经过几次网格搜索后,结合max_depth
和colsample_bynode
给出了以下结果:
grid_search(params={'max_depth':[1, 2, 3], 'colsample_bynode':[0.5, 0.75, 1]}, X=X_short, y=y_short, model=XGBClassifier(random_state=2))
分数如下:
Best params: {'colsample_bynode': 0.5, 'max_depth': 2}
Best score: 0.65058
这是一个改进。
现在是时候在所有数据上尝试超参数微调了。
微调所有数据
在所有数据上实现grid_search
函数的问题是时间。现在我们已经接近尾声,到了运行代码并在计算机“出汗”时休息的时刻:
-
将所有数据读入一个新的 DataFrame,
df_all
:df_all = pd.read_csv('exoplanets.csv')
-
将 1 替换为 0,将 2 替换为 1:
df_all['LABEL'] = df_all['LABEL'].replace(1, 0)df_all['LABEL'] = df_all['LABEL'].replace(2, 1)
-
将数据分为
X
和y
:X_all = df_all.iloc[:,1:]y_all = df_all.iloc[:,0]
-
验证
'LABEL'
列的value_counts
:df_all['LABEL'].value_counts()
输出如下:
0 5050 1 37 Name: LABEL, dtype: int64
-
通过将负类除以正类来缩放权重:
weight = int(5050/37)
-
使用
XGBClassifier
和scale_pos_weight=weight
对所有数据进行基准模型评分:model = XGBClassifier(scale_pos_weight=weight, random_state=2) scores = cross_val_score(model, X_all, y_all, cv=kfold, scoring='recall') print('Recall:', scores) print('Recall mean:', scores.mean())
输出如下:
Recall: [0.10526316 0. ] Recall mean: 0.05263157894736842
这个分数很糟糕。可能是分类器在准确率上得分很高,尽管召回率很低。
-
让我们尝试基于迄今为止最成功的结果优化超参数:
grid_search(params={'learning_rate':[0.001, 0.01]}, X=X_all, y=y_all, model=XGBClassifier(scale_pos_weight=weight, random_state=2))
分数如下:
Best params: {'learning_rate': 0.001} Best score: 0.26316
这比使用所有数据时的初始分数要好得多。
让我们尝试结合超参数:
grid_search(params={'max_depth':[1, 2],'learning_rate':[0.001]}, X=X_all, y=y_all, model=XGBClassifier(scale_pos_weight=weight, random_state=2))
分数如下:
Best params: {'learning_rate': 0.001, 'max_depth': 2} Best score: 0.53509
这已经有所改善,但不如之前对欠采样数据集的得分强。
由于在所有数据上的分数起始较低且需要更多时间,自然而然会产生一个问题。对于系外行星数据集,机器学习模型在较小的子集上是否表现更好?
让我们来看看。
整合结果
将不同的数据集进行结果整合是很棘手的。我们一直在处理以下子集:
-
5,050 行 – 大约 54%的召回率
-
400 行 – 大约 54%的召回率
-
74 行 – 大约 68%的召回率
获得的最佳结果包括learning_rate=0.001
,max_depth=2
和colsample_bynode=0.5
。
让我们在所有 37 个系外行星恒星上训练一个模型。这意味着测试结果将来自模型已经训练过的数据点。通常,这不是一个好主意。然而,在这种情况下,正例非常少,看看模型如何在它以前没有见过的正例上进行测试,可能会很有启发。
以下函数以X
、y
和机器学习模型为输入。模型在提供的数据上进行拟合,然后对整个数据集进行预测。最后,打印出recall_score
、confusion matrix
和classification report
:
def final_model(X, y, model):
model.fit(X, y)
y_pred = model.predict(X_all)
score = recall_score(y_all, y_pred,)
print(score)
print(confusion_matrix(y_all, y_pred,))
print(classification_report(y_all, y_pred))
让我们为我们的三个子集运行函数。在三种最强的超参数中,事实证明colsample_bynode
和max_depth
给出了最佳结果。
从行数最少的地方开始,其中系外行星恒星和非系外行星恒星的数量相匹配。
74 行
让我们从 74 行开始:
final_model(X_short, y_short, XGBClassifier(max_depth=2, colsample_by_node=0.5, random_state=2))
输出如下:
1.0
[[3588 1462]
[ 0 37]]
precision recall f1-score support
0 1.00 0.71 0.83 5050
1 0.02 1.00 0.05 37
accuracy 0.71 5087
macro avg 0.51 0.86 0.44 5087
weighted avg 0.99 0.71 0.83 5087
所有 37 颗外行星恒星都被正确识别,但 1462 颗非外行星恒星被错误分类!尽管召回率达到了 100%,但精确度只有 2%,F1 得分为 5%。仅仅调优召回率会带来低精度和低 F1 得分的风险。实际上,天文学家需要筛选出 1462 颗潜在的外行星恒星,才能找到这 37 颗。这是不可接受的。
现在让我们看看在 400 行数据上训练时会发生什么。
400 行
在 400 行数据的情况下,我们使用scale_pos_weight=10
超参数来平衡数据:
final_model(X, y, XGBClassifier(max_depth=2, colsample_bynode=0.5, scale_pos_weight=10, random_state=2))
输出结果如下:
1.0
[[4901 149]
[ 0 37]]
precision recall f1-score support
0 1.00 0.97 0.99 5050
1 0.20 1.00 0.33 37
accuracy 0.97 5087
macro avg 0.60 0.99 0.66 5087
weighted avg 0.99 0.97 0.98 5087
再次,所有 37 颗外行星恒星都被正确分类,达到了 100%的召回率,但 149 颗非外行星恒星被错误分类,精确度为 20%。在这种情况下,天文学家需要筛选出 186 颗恒星,才能找到这 37 颗外行星恒星。
最后,让我们在所有数据上进行训练。
5050 行
在所有数据的情况下,将scale_pos_weight
设置为与先前定义的weight
变量相等:
final_model(X_all, y_all, XGBClassifier(max_depth=2, colsample_bynode=0.5, scale_pos_weight=weight, random_state=2))
输出结果如下:
1.0
[[5050 0]
[ 0 37]]
precision recall f1-score support
0 1.00 1.00 1.00 5050
1 1.00 1.00 1.00 37
accuracy 1.00 5087
macro avg 1.00 1.00 1.00 5087
weighted avg 1.00 1.00 1.00 5087
惊人。所有预测、召回率和精确度都完美达到了 100%。在这种高度理想的情况下,天文学家无需筛选不良数据,就能找到所有的外行星恒星。
但请记住,这些得分是基于训练数据,而非未见过的测试数据,而后者是构建强大模型的必要条件。换句话说,尽管模型完美地拟合了训练数据,但它不太可能对新数据进行良好的泛化。然而,这些数字仍然有价值。
根据这个结果,由于机器学习模型在训练集上表现出色,但在测试集上的表现最多只是适中,方差可能过高。此外,可能需要更多的树和更多轮次的精调,以便捕捉数据中的细微模式。
分析结果
在训练集上评分时,经过调优的模型提供了完美的召回率,但精确度差异较大。以下是关键要点:
-
仅使用精确度而不考虑召回率或 F1 得分,可能会导致次优模型。通过使用分类报告,能揭示更多细节。
-
不建议过度强调小子集的高得分。
-
当测试得分较低,而训练得分较高时,建议在不平衡数据集上使用更深的模型并进行广泛的超参数调优。
Kaggle 用户发布的公开笔记本中对内核的调查,位于www.kaggle.com/keplersmachines/kepler-labelled-time-series-data/kernels
,展示了以下内容:
-
许多用户未能理解,尽管高准确率得分容易获得,但在高度不平衡的数据下,它几乎没有意义。
-
发布精确度的用户通常发布的是 50%到 70%之间的数据,而发布召回率的用户通常发布的是 60%到 100%之间(一个 100%召回率的用户精确度为 55%),这表明了该数据集的挑战和局限性。
当您向天文学教授展示您的结果时,您已经更加了解不平衡数据的局限性,您得出结论,您的模型最佳的召回率为 70%,而 37 颗外行星恒星不足以构建一个强大的机器学习模型来寻找其他行星上的生命。然而,您的 XGBClassifier 将使天文学家和其他经过数据分析训练的人能够使用机器学习来决定在宇宙中应集中关注哪些恒星,以发现下一个处于轨道上的外行星。
总结
在这一章中,您使用外行星数据集对宇宙进行了调查,旨在发现新的行星,甚至可能发现新的生命。您构建了多个 XGBClassifier 来预测外行星恒星是否由光的周期性变化所引起。在仅有 37 颗外行星恒星和 5,050 颗非外行星恒星的情况下,您通过欠采样、过采样和调整 XGBoost 超参数(包括 scale_pos_weight
)来纠正数据的不平衡。
您使用混淆矩阵和分类报告分析了结果。您学习了各种分类评分指标之间的关键差异,并且理解了为什么在外行星数据集中,准确率几乎没有价值,而高召回率是理想的,尤其是当与高精度结合时,能够得到一个好的 F1 分数。最后,您意识到,当数据极其多样化且不平衡时,机器学习模型的局限性。
通过这个案例研究,您已具备了使用 XGBoost 完整分析不平衡数据集所需的背景知识和技能,掌握了 scale_pos_weight
、超参数微调和替代分类评分指标的使用。
在下一章中,您将通过应用不同于梯度提升树的其他 XGBoost 基学习器,大大扩展您对 XGBoost 的应用范围。尽管梯度提升树通常是最佳选择,但 XGBoost 配备了线性基学习器、DART 基学习器,甚至是随机森林,接下来都会介绍!