原文:
annas-archive.org/md5/8042b1d609c03cc86db1c68794ab294c
译者:飞龙
第三部分:高级 XGBoost
构建高级 XGBoost 模型需要实践、分析和实验。在本节中,你将实验并微调替代基学习器,学习来自 Kaggle 大师的创新技巧——包括堆叠方法和高级特征工程——并实践构建稳健的模型,为行业部署做好准备,使用稀疏矩阵、定制的转换器和管道。
本节包含以下章节:
-
第八章*,XGBoost 替代基学习器*
-
第九章*,XGBoost Kaggle 大师*
-
第十章*,XGBoost 模型部署*
第八章:第八章: XGBoost 替代基础学习器
在本章中,您将分析并应用不同的gbtree
,基础学习器的附加选项包括gblinear
和dart
。此外,XGBoost 还有其自己的随机森林实现,作为基础学习器以及作为树集成算法,您将在本章中进行实验。
通过学习如何应用替代的基础学习器,您将大大扩展 XGBoost 的使用范围。您将能够构建更多的模型,并学习开发线性、基于树的以及随机森林机器学习算法的新方法。本章的目标是让您熟练掌握使用替代基础学习器构建 XGBoost 模型,从而能够利用 XGBoost 的高级选项,在各种情况下找到最优的模型。
在本章中,我们将涵盖以下主要主题:
-
探索替代的基础学习器
-
应用
gblinear
-
比较
dart
-
查找 XGBoost 随机森林
技术要求
本章的代码和数据集可以在github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter08
找到。
探索替代的基础学习器
基础学习器是 XGBoost 用来构建其集成中第一个模型的机器学习模型。使用base这个词是因为它是第一个出现的模型,而使用learner是因为该模型会在从错误中学习后自我迭代。
决策树已成为 XGBoost 首选的基础学习器,因为提升树模型能够 consistently 产生优秀的分数。决策树的受欢迎程度不仅仅局限于 XGBoost,还扩展到其他集成算法,如随机森林以及ExtraTreesClassifier
和ExtraTreesRegressor
(scikit-learn.org/stable/modules/ensemble.html
)。
在 XGBoost 中,默认的基础学习器是gbtree
,它是多种基础学习器之一。还有gblinear
,一种梯度提升线性模型,以及dart
,一种包括基于神经网络的 dropout 技术的决策树变种。此外,XGBoost 还有随机森林的实现。在接下来的部分中,我们将探讨这些基础学习器之间的差异,然后在后续章节中应用它们。
gblinear
决策树对于非线性数据非常适用,因为它们可以通过多次划分数据来轻松访问数据点。由于实际数据通常是非线性的,决策树通常更适合作为基础学习器。
然而,可能会有某些情况,gblinear
作为线性基础学习器的选项。
提升线性模型的基本思想与提升树模型相同。首先构建一个基模型,然后在每个后续模型中训练残差。最后,将所有模型的结果求和得到最终结果。与线性基学习器的主要区别在于,集成中的每个模型都是线性的。
与gblinear
一样,dart
也在线性回归中添加了正则化项。XGBoost 的创始人兼开发者 Tianqi Chin 在 GitHub 上评论称,经过多轮提升后,gblinear
可以用来返回一个单一的 lasso 回归(github.com/dmlc/xgboost/issues/332
)。
gblinear
也可以通过逻辑回归应用于分类问题。这是因为逻辑回归的构建方式与线性回归相同,都是通过找到最优系数(加权输入)并通过sigmoid 方程求和(见第一章,机器学习概览)。
我们将在本章的应用 gblinear部分深入探讨gblinear
的细节和应用。现在,先了解一下dart
。
DART
丢弃法与多重加法回归树,简称DART,由 UC Berkeley 的 K. V. Rashmi 和微软的 Ran Gilad-Bachrach 于 2015 年在以下论文中提出:proceedings.mlr.press/v38/korlakaivinayak15.pdf
。
Rashmi 和 Gilad-Bachrach 指出,多重加法回归树(MART)是一种成功的模型,但其问题在于过度依赖早期的树。为了改进这一点,他们没有侧重于收缩这一标准的惩罚项,而是使用了来自神经网络的丢弃法技术。简单来说,丢弃法技术通过从神经网络的每一层学习中删除节点(数学点),从而减少过拟合。换句话说,丢弃法通过在每一轮中删除信息,减缓了学习过程。
在 DART 中,每一轮新的提升中,DART 并不是通过对所有前一个树的残差求和来建立新模型,而是随机选择部分前一个树的样本,并通过一个缩放因子来归一化叶子节点!,其中https://github.com/OpenDocCN/freelearn-ml-pt2-zh/raw/master/docs/hsn-gdbt-xgb-skl/img/Formula_08_002.png是被丢弃的树的数量。
DART 是决策树的一种变体。XGBoost 中的 DART 实现类似于gbtree
,但是增加了一些超参数来适应丢弃法。
有关 DART 的数学细节,请参考本节第一段中提到的原始论文。
你将在本章稍后的比较 dart部分,练习使用DART
基学习器来构建机器学习模型。
XGBoost 随机森林
本节我们将探索的最后一个选项是 XGBoost 随机森林。通过将 num_parallel_trees
设置为大于 1
的整数,可以将随机森林实现为基础学习器,并作为 XGBoost 中的类选项,定义为 XGBRFRegressor
和 XGBRFClassifier
。
请记住,梯度提升法是为改进相对较弱的基础学习器的错误而设计的,而不是像随机森林这样强大的基础学习器。然而,可能会有一些边缘情况,随机森林基础学习器也可能具有优势,因此它是一个不错的选择。
作为额外奖励,XGBoost 提供了 XGBRFRegressor
和 XGBRFClassifier
作为随机森林机器学习算法,它们不是基础学习器,而是独立的算法。这些算法的工作方式与 scikit-learn 的随机森林类似(参见 第三章,使用随机森林的袋装法)。主要的区别在于,XGBoost 包括默认的超参数来对抗过拟合,并有自己构建单棵树的方法。XGBoost 随机森林虽然还处于实验阶段,但自 2020 年末以来,它们已经开始超越 scikit-learn 的随机森林,如你将在本章中看到的。
在本章的最后部分,我们将分别尝试将 XGBoost 的随机森林作为基础学习器和独立模型进行实验。
现在你已经对 XGBoost 基础学习器有了一个概览,接下来我们将逐一应用它们。
应用 gblinear
很难找到与线性模型最为契合的真实世界数据集。实际数据往往比较杂乱,复杂的模型(如树集成)通常会产生更好的得分。而在其他情况下,线性模型可能会有更好的泛化能力。
机器学习算法的成功依赖于它们在真实世界数据上的表现。在接下来的部分,我们将首先将 gblinear
应用于糖尿病数据集,然后再应用于一个通过构造生成的线性数据集。
将 gblinear 应用于糖尿病数据集
糖尿病数据集是由 scikit-learn 提供的一个回归数据集,包含 442 名糖尿病患者。预测列包括年龄、性别、BMI(体重指数)、BP(血压)和五项血清测量值。目标列是疾病在 1 年后的进展。你可以在原始论文中阅读有关数据集的详细信息,链接在这里:web.stanford.edu/~hastie/Papers/LARS/LeastAngle_2002.pdf
。
Scikit-learn 的数据集已经为你将预测列和目标列拆分好了。它们已预处理,机器学习时 X
是预测列,y
是目标列,分别加载。
下面是你需要用来处理这个数据集以及本章其余部分的完整导入列表:
import pandas as pd
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.model_selection import cross_val_score
from xgboost import XGBRegressor, XGBClassifier, XGBRFRegressor, XGBRFClassifier
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.linear_model import Lasso, Ridge
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import KFold
from sklearn.metrics import mean_squared_error as MSE
让我们开始吧!要使用糖尿病数据集,请执行以下操作:
-
你首先需要使用
load_diabetes
并将return_X_y
参数设置为True
,以定义X
和y
:X, y = load_diabetes(return_X_y=True)
计划是使用
cross_val_score
和GridSearchCV
,所以我们先创建折叠以获得一致的得分。在 第六章,XGBoost 超参数 中,我们使用了StratifiedKFold
,它对目标列进行分层,确保每个测试集包含相同数量的类别。这种方法适用于分类问题,但不适用于回归问题,其中目标列是连续值,不涉及类别。
KFold
通过在数据中创建一致的分割来实现类似的目标,而不进行分层。 -
现在,打乱数据并使用
KFold
对数据进行5
次分割,使用以下参数:kfold = KFold(n_splits=5, shuffle=True, random_state=2)
-
构建一个使用
cross_val_score
的函数,输入一个机器学习模型,并返回5
次折叠的平均得分,确保设置cv=kfold
:def regression_model(model): scores = cross_val_score(model, X, y, scoring='neg_mean_squared_error', cv=kfold) rmse = (-scores)**0.5 return rmse.mean()
-
要使用
gblinear
作为基本模型,只需在回归函数中的XGBRegressor
中设置booster='gblinear'
:regression_model(XGBRegressor(booster='gblinear'))
得分如下:
55.4968907398679
-
让我们检查这个得分与其他线性模型的对比,包括
LinearRegression
,Lasso
,使用Ridge
,使用LinearRegression
如下:regression_model(LinearRegression())
得分如下:
55.50927267834351
b)
Lasso
如下:regression_model(Lasso())
得分如下:
62.64900771743497
c)
Ridge
如下:regression_model(Ridge())
得分如下:
58.83525077919004
如您所见,
XGBRegressor
在使用gblinear
作为基本学习器时表现最佳,与LinearRegression
一同表现突出。 -
现在将
booster='gbtree'
放入XGBRegressor
中,这是默认的基本学习器:regression_model(XGBRegressor(booster='gbtree'))
得分如下:
65.96608419624594
如您所见,在这种情况下,gbtree
基本学习器的表现远不如 gblinear
基本学习器,这表明线性模型更为理想。
让我们看看是否能通过调整超参数使 gblinear
作为基本学习器获得一些提升。
gblinear 超参数
在调整超参数时,理解 gblinear
和 gbtree
之间的区别非常重要。在 第六章*,XGBoost 超参数* 中介绍的许多 XGBoost 超参数是树形超参数,不适用于 gblinear
。例如,max_depth
和 min_child_weight
是专门为树形设计的超参数。
以下是针对线性模型设计的 XGBoost gblinear
超参数总结。
reg_lambda
Scikit-learn 使用 reg_lambda
代替 lambda
,因为 lambda
是 Python 中保留的关键字,用于定义 Lambda 函数。这是 Ridge
使用的标准 L2 正则化。接近 0
的值通常效果最好:
-
默认值:0
-
范围:[0, inf)
-
增大可防止过拟合
-
别名:lambda
reg_alpha
Scikit-learn 接受 reg_alpha
和 alpha
两种方式。这是 Lasso
使用的标准 L1 正则化。接近 0
的值通常效果最好:
-
默认值:0
-
范围:[0, inf)
-
增大可防止过拟合
-
别名:alpha
更新器
这是 XGBoost 在每轮提升过程中用于构建线性模型的算法。shotgun
使用hogwild
并行化与坐标下降法来生成非确定性解。相比之下,coord_descent
是普通的坐标下降法,产生确定性解:
-
默认值:shotgun
-
范围:shotgun, coord_descent
注意
坐标下降法是机器学习术语,定义为通过逐一寻找每个坐标的梯度来最小化误差。
feature_selector
feature_selector
决定了如何选择权重,具体选项如下:
a) cyclic
– 迭代循环通过特征
b) shuffle
– 每轮随机特征重排的循环方式
c) random
– 坐标下降法中的坐标选择是随机的
d) greedy
– 耗时;选择具有最大梯度幅度的坐标
e) thrifty
– 大致是贪婪的,根据权重变化重新排序特征
-
默认值:cyclic
-
范围必须与
updater
一起使用,具体如下:a)
shotgun
:cyclic
,shuffle
b)
coord_descent
:random
,greedy
,thrifty
注意
对于大数据集来说,
greedy
计算开销较大,但通过更改参数top_k
(见下文),可以减少greedy
考虑的特征数量。
top_k
top_k
是greedy
和thrifty
在坐标下降法中选择特征的数量:
-
默认值:0(所有特征)
-
范围:[0,最大特征数]
注意
欲了解更多关于 XGBoost
gblinear
超参数的信息,请查阅 XGBoost 官方文档页面:xgboost.readthedocs.io/en/latest/parameter.html#parameters-for-linear-booster-booster-gblinear
。
gblinear 网格搜索
现在你已经熟悉了gblinear
可能使用的超参数范围,接下来让我们使用GridSearchCV
在自定义的grid_search
函数中找到最佳参数:
-
这是我们从第六章中得到的
grid_search
函数版本,XGBoost 超参数:def grid_search(params, reg=XGBRegressor(booster='gblinear')): grid_reg = GridSearchCV(reg, params, scoring='neg_mean_squared_error', cv=kfold) grid_reg.fit(X, y) best_params = grid_reg.best_params_ print("Best params:", best_params) best_score = np.sqrt(-grid_reg.best_score_) print("Best score:", best_score)
-
让我们从使用标准范围修改
alpha
开始:grid_search(params={'reg_alpha':[0.001, 0.01, 0.1, 0.5, 1, 5]})
输出如下:
Best params: {'reg_alpha': 0.01} Best score: 55.485310447306425
得分大致相同,但略有提升。
-
接下来,让我们使用相同的范围修改
reg_lambda
:grid_search(params={'reg_lambda':[0.001, 0.01, 0.1, 0.5, 1, 5]})
输出如下:
Best params: {'reg_lambda': 0.001} Best score: 56.17163554152289
这里的得分非常相似,但略逊一筹。
-
现在让我们将
feature_selector
与updater
一起使用。默认情况下,updater=shotgun
,feature_selector=cyclic
。当updater=shotgun
时,feature_selector
唯一的另一个选择是shuffle
。让我们看看
shuffle
是否比cyclic
表现更好:grid_search(params={'feature_selector':['shuffle']})
输出如下:
Best params: {'feature_selector': 'shuffle'} Best score: 55.531684115240594
在这种情况下,
shuffle
表现不佳。 -
现在让我们将
updater
更改为coord_descent
。因此,feature_selector
可以选择random
、greedy
或thrifty
。通过输入以下代码,尝试在grid_search
中测试所有feature_selector
选项:grid_search(params={'feature_selector':['random', 'greedy', 'thrifty'], 'updater':['coord_descent'] })
输出如下:
Best params: {'feature_selector': 'thrifty', 'updater': 'coord_descent'} Best score: 55.48798105805444 This is a slight improvement from the base score.
最后一个需要检查的超参数是
top_k
,它定义了在坐标下降过程中,greedy
和thrifty
检查的特征数量。由于总共有 10 个特征,top_k
的范围从2
到9
都是可以接受的。 -
在
grid_search
中为greedy
和thrifty
输入top_k
的范围,以找到最佳选项:grid_search(params={'feature_selector':['greedy', 'thrifty'], 'updater':['coord_descent'], 'top_k':[3, 5, 7, 9]})
输出如下:
Best params: {'feature_selector': 'thrifty', 'top_k': 3, 'updater': 'coord_descent'} Best score: 55.478623763746256
这是迄今为止最好的得分。
在继续之前,请注意,除了树以外,还可以使用其他超参数,比如 n_estimators
和 learning_rate
。
现在让我们看看 gblinear
在一个构建时就是线性的数据集上的表现。
线性数据集
确保数据集是线性的一个方法是通过构建。我们可以选择一系列 X
值,比如从 1
到 99
,然后乘以一个缩放因子,并加入一些随机性。
这是构建线性数据集的步骤:
-
设置
X
值的范围从1
到100
:X = np.arange(1,100)
-
使用 NumPy 声明一个随机种子,以确保结果的一致性:
np.random.seed(2)
-
创建一个空列表,定义为
y
:y = []
-
遍历
X
,将每个条目乘以一个从-0.2
到0.2
的随机数:for i in X: y.append(i * np.random.uniform(-0.2, 0.2))
-
将
y
转换为numpy
数组以用于机器学习:y = np.array(y)
-
重塑
X
和y
,使它们包含与数组成员相同数量的行和一列,因为列是 scikit-learn 期望的机器学习输入:X = X.reshape(X.shape[0], 1) y = y.reshape(y.shape[0], 1)
我们现在有一个线性数据集,其中
X
和y
含有随机性。
让我们再次运行 regression_model
函数,这次使用 gblinear
作为基础学习器:
regression_model(XGBRegressor(booster='gblinear', objective='reg:squarederror'))
得分如下:
6.214946302686011
现在使用 gbtree
作为基础学习器运行 regression_model
函数:
regression_model(XGBRegressor(booster='gbtree', objective='reg:squarederror'))
得分如下:
9.37235946501318
如你所见,gblinear
在我们构建的线性数据集上表现更好。
为了更精确,让我们在同一数据集上尝试 LinearRegression
:
regression_model(LinearRegression())
得分如下:
6.214962315808842
在这种情况下,gblinear
的表现稍好,或许差距微乎其微,得分比 LinearRegression
低了 0.00002
分。
分析 gblinear
gblinear
是一个有吸引力的选择,但只有在你有理由相信线性模型可能比基于树的模型表现更好时,才应使用它。在真实数据集和构建的数据集中,gblinear
比 LinearRegression
的表现高出非常微弱的优势。在 XGBoost 中,gblinear
在数据集庞大且线性时,是一个很强的基础学习器选择。gblinear
也可以用于分类数据集,下一节中你将应用这一方法。
比较 dart
基础学习器 dart
类似于 gbtree
,因为它们都是梯度提升树。主要的区别是,dart
在每一轮提升中移除一些树(称为 dropout)。
在本节中,我们将应用并比较基础学习器 dart
与其他基础学习器在回归和分类问题中的表现。
使用 XGBRegressor 的 DART 方法
让我们看看 dart
在糖尿病数据集上的表现:
-
首先,像之前一样使用
load_diabetes
重新定义X
和y
:X, y = load_diabetes(return_X_y=True)
-
要将
dart
作为 XGBoost 的基础学习器使用,需要在regression_model
函数内将XGBRegressor
参数booster='dart'
设置:regression_model(XGBRegressor(booster='dart', objective='reg:squarederror'))
分数如下:
65.96444746130739
dart
基础学习器与gbtree
基础学习器的结果相同,精确到小数点后两位。结果相似是因为数据集较小,且gbtree
默认的超参数能够有效防止过拟合,无需采用丢弃树技术。
让我们看看dart
与gbtree
在更大数据集上的分类表现如何。
dart 与 XGBClassifier
你在本书的多个章节中使用了 Census 数据集。我们在第一章《机器学习概况》中修改过的清洁版数据集,已经为你预加载,另外还包括了第八章《XGBoost 替代基础学习器》中的代码,数据集可以从github.com/PacktPublishing/Hands-On-Gradient-Boosting-with-XGBoost-and-Scikit-learn/tree/master/Chapter08
获取。现在让我们开始测试dart
在更大数据集上的表现:
-
将 Census 数据集加载到 DataFrame 中,并使用最后一列索引(
-1
)作为目标列,将预测列和目标列拆分为X
和y
:df_census = pd.read_csv('census_cleaned.csv') X_census = df_census.iloc[:, :-1] y_census = df_census.iloc[:, -1]
-
定义一个新的分类函数,使用
cross_val_score
,输入为机器学习模型,输出为均值分数,类似于本章前面定义的回归函数:def classification_model(model): scores = cross_val_score(model, X_census, y_census, scoring='accuracy', cv=kfold) return scores.mean()
-
现在使用
XGBClassifier
分别设置booster='gbtree'
和booster='dart'
调用函数两次进行结果比较。请注意,由于数据集更大,运行时间会更长:a) 首先,让我们调用
XGBClassifier
并设置booster='gbtree'
:classification_model(XGBClassifier(booster='gbtree'))
分数如下:
0.8701208195968675
b) 现在,让我们调用
XGBClassifier
并设置booster='dart'
:classification_model(XGBClassifier(booster='dart')
分数如下:
0.8701208195968675
这令人吃惊。dart
与gbtree
的结果完全相同,精确到所有 16 位小数!目前尚不清楚是否真的丢弃了树,或者丢弃树对结果没有任何影响。
我们可以调整超参数以确保丢弃树,但首先,让我们看看dart
与gblinear
的比较。回想一下,gblinear
通过使用 sigmoid 函数来对权重进行缩放,类似于逻辑回归,从而也能用于分类:
-
使用
XGBClassifier
调用classification_model
函数,并设置booster='gblinear'
:classification_model(XGBClassifier(booster='gblinear'))
分数如下:
0.8501275704120015
这种线性基础学习器的表现不如树型基础学习器。
-
让我们看看
gblinear
与逻辑回归的比较。由于数据集较大,最好将逻辑回归的max_iter
超参数从100
调整为1000
,以便有更多时间进行收敛并消除警告。请注意,在这种情况下,增加max_iter
能提高准确率:classification_model(LogisticRegression(max_iter=1000))
分数如下:
0.8008968643699182
在这种情况下,
gblinear
比逻辑回归具有明显的优势。值得强调的是,XGBoost 的gblinear
选项在分类中提供了一个可行的替代逻辑回归的方案。
现在你已经了解了dart
与gbtree
和gblinear
作为基本学习器的比较,接下来我们来修改dart
的超参数。
DART 超参数
dart
包含所有gbtree
的超参数,并且还包括一组额外的超参数,用于调整丢弃树的百分比、频率和概率。有关详细信息,请参见 XGBoost 文档:xgboost.readthedocs.io/en/latest/parameter.html#additional-parameters-for-dart-booster-booster-dart
。
以下各节是 XGBoost 中专门针对dart
的超参数总结。
sample_type
sample_type
的选项包括uniform
,表示树是均匀丢弃的,和weighted
,表示树按其权重比例丢弃:
-
默认值:“uniform”
-
范围:[“uniform”, “weighted”]
-
决定丢弃树的选择方式
normalize_type
normalize_type
的选项包括tree
,即新树的权重与丢弃的树相同,和forest
,即新树的权重与丢弃的树的总和相同:
-
默认值:“tree”
-
范围:[“tree”, “forest”]
-
计算树的权重,以丢弃的树为单位
rate_drop
rate_drop
允许用户精确设置丢弃树的百分比:
-
默认值:0.0
-
范围:[0.0, 1.0]
-
丢弃树的百分比
one_drop
当设置为1
时,one_drop
确保每次提升轮次中总有一棵树被丢弃:
-
默认值:0
-
范围:[0, 1]
-
用于确保丢弃
skip_drop
skip_drop
给出了完全跳过丢弃的概率。在官方文档中,XGBoost 说 skip_drop
的优先级高于rate_drop
或one_drop
。默认情况下,每棵树被丢弃的概率相同,因此对于某次提升轮次可能没有树被丢弃。skip_drop
允许更新此概率,以控制丢弃轮次的数量:
-
默认值:0.0
-
范围:[0.0, 1.0]
-
跳过丢弃的概率
现在让我们修改dart
的超参数,以区分不同的得分。
修改 dart 超参数
为确保每次提升轮次中至少有一棵树被丢弃,我们可以设置one_drop=1
。现在通过classification_model
函数使用 Census 数据集来实现:
classification_model(XGBClassifier(booster='dart', one_drop=1))
结果如下:
0.8718714338474818
这是一个提高了百分之一点的改进,表明每次提升轮次丢弃至少一棵树可能是有利的。
现在我们正在丢弃树以更改得分,让我们回到更小且更快的糖尿病数据集,修改剩余的超参数:
-
使用
regression_model
函数,将sample_type
从uniform
更改为weighted
:regression_model(XGBRegressor(booster='dart', objective='reg:squarederror', sample_type='weighted'))
得分如下:
65.96444746130739
这个得分比之前
gbtree
模型得分高出 0.002 分。 -
将
normalize_type
更改为forest
,以便在更新权重时包括树的总和:regression_model(XGBRegressor(booster='dart', objective='reg:squarederror', normalize_type='forest'))
得分如下:
65.96444746130739
分数没有变化,这可能发生在数据集较浅的情况下。
-
将
one_drop
更改为1
,确保每次提升回合至少丢弃一棵树:regression_model(XGBRegressor(booster='dart', objective='reg:squarederror', one_drop=1))
得分如下:
61.81275131335009
这是一个明显的改进,得分提高了四个完整点。
对于 rate_drop
,即将被丢弃的树的百分比,可以使用以下 grid_search
函数来设置百分比范围:
grid_search(params={'rate_drop':[0.01, 0.1, 0.2, 0.4]}, reg=XGBRegressor(booster='dart', objective='reg:squarederror', one_drop=1))
结果如下:
Best params: {'rate_drop': 0.2}
Best score: 61.07249602732062
这是迄今为止的最佳结果。
我们可以使用类似的范围来实现 skip_drop
,它给出了某棵树不被丢弃的概率:
grid_search(params={'skip_drop':[0.01, 0.1, 0.2, 0.4]}, reg=XGBRegressor(booster='dart', objective='reg:squarederror'))
结果如下:
Best params: {'skip_drop': 0.1}
Best score: 62.879753748627635
这是一个不错的得分,但 skip_drop
没有带来实质性的提升。
现在你看到 dart
的实际应用,让我们分析一下结果。
分析 dart
dart
在 XGBoost 框架中提供了一个很有吸引力的选项。由于 dart
接受所有 gbtree
超参数,因此在修改超参数时,可以轻松地将基础学习器从 gbtree
改为 dart
。实际上,优点是你可以尝试包括 one_drop
、rate_drop
、normalize
等新的超参数,以查看是否能获得额外的收益。在你的研究和 XGBoost 模型构建中,dart
绝对值得尝试作为基础学习器。
现在你已经对 dart
有了很好的理解,是时候转向随机森林了。
查找 XGBoost 随机森林
在 XGBoost 中实现随机森林有两种策略。第一种是将随机森林作为基础学习器,第二种是使用 XGBoost 的原始随机森林,即 XGBRFRegressor
和 XGBRFClassifier
。我们从原始主题开始,即将随机森林作为替代基础学习器。
随机森林作为基础学习器
没有选项可以将提升器的超参数设置为随机森林。相反,可以将超参数 num_parallel_tree
从其默认值 1
增加,以将 gbtree
(或 dart
)转变为一个提升的随机森林。这里的思路是,每个提升回合将不再是单棵树,而是多个并行的树,这些树共同构成一片森林。
以下是 XGBoost 超参数 num_parallel_tree
的简要总结。
num_parallel_tree
num_parallel_tree
指定了在每次提升回合中构建的树的数量,可能大于 1:
-
默认值:1
-
范围:[1, inf)
-
给出并行提升的树的数量
-
大于 1 的值会将提升器转变为随机森林
通过在每回合中包含多棵树,基础学习器不再是单棵树,而是森林。由于 XGBoost 包含与随机森林相同的超参数,因此当 num_parallel_tree
超过 1 时,基础学习器被适当分类为随机森林。
让我们看看 XGBoost 随机森林基础学习器在实际中的表现:
-
使用
XGBRegressor
调用regression_model
并设置booster='gbtree'
。此外,设置num_parallel_tree=25
,意味着每次提升回合由25
棵树组成:regression_model(XGBRegressor(booster='gbtree', objective='reg:squarederror', num_parallel_tree=25))
评分如下:
65.96604877151103
评分是相当不错的,在这种情况下,几乎和提升一个单独的
gbtree
相同。原因在于梯度提升的设计是通过从前一个树的错误中学习来优化性能。通过从一个强大的随机森林开始,学习的空间有限,因此收益最多是微小的。理解梯度提升算法的核心优势来自于学习过程至关重要。因此,尝试将
num_parallel_tree
设置为一个更小的值(例如5
)是有意义的。 -
在同一个回归模型中设置
num_parallel_tree=5
:regression_model(XGBRegressor(booster='gbtree', objective='reg:squarederror', num_parallel_tree=5))
评分如下:
65.96445649315855
从技术上讲,这个得分比 25 棵树的森林产生的得分高 0.002 分。虽然这个改善不大,但一般来说,在构建 XGBoost 随机森林时,较低的
num_parallel_tree
值会更好。
现在你已经看到如何将随机森林作为 XGBoost 中的基础学习器实现,是时候将随机森林构建为原始的 XGBoost 模型了。
作为 XGBoost 模型的随机森林
除了 XGBRegressor
和 XGBClassifier
,XGBoost
还提供了 XGBRFRegressor
和 XGBRFClassifier
来构建随机森林。
根据官方 XGBoost 文档 xgboost.readthedocs.io/en/latest/tutorials/rf.html
,scikit-learn 的随机森林包装器仍处于实验阶段,默认值可能会随时更改。在编写本文时(2020 年),以下是 XGBRFRegressor
和 XGBRFClassifier
的默认值。
n_estimators
使用 n_estimators
而不是 num_parallel_tree
来构建随机森林时,请使用 XGBRFRegressor
或 XGBRFClassifier
。请记住,在使用 XGBRFRegressor
和 XGBRFClassifier
时,你并不是在做梯度提升,而是在一次回合中对树进行集成,就像传统的随机森林一样:
-
默认值: 100
-
范围: [1, inf)
-
自动转换为 num_parallel_tree 用于随机森林
learning_rate
learning_rate
通常是为学习型模型设计的,包括增强器,而不是 XGBRFRegressor
或 XGBRFClassifier
,因为它们仅由一轮树组成。然而,将 learning_rate
从 1 改变会影响得分,因此通常不建议修改这个超参数:
-
默认值: 1
-
范围: [0, 1]
subsample, colsample_by_node
Scikit-learn 的随机森林将这些默认值保持为 1
,使得默认的 XGBRFRegressor
和 XGBRFClassifier
更不容易过拟合。这是 XGBoost 和 scikit-learn 随机森林默认实现之间的主要区别:
-
默认值: 0.8
-
范围: [0, 1]
-
减少有助于防止过拟合
现在,让我们看看 XGBoost 的随机森林在实践中的工作原理:
-
首先,将
XGBRFRegressor
放入regression_model
函数中:regression_model(XGBRFRegressor(objective='reg:squarederror'))
得分如下:
59.447250741400595
这个得分稍微比之前展示的
gbtree
模型好一些,但比本章中展示的最佳线性模型稍差。 -
为了对比,看看将
RandomForestRegressor
放入相同函数后的表现:regression_model(RandomForestRegressor())
得分如下:
59.46563031802505
这个得分稍微比
XGBRFRegressor
差一些。
现在让我们使用更大的 Census 数据集进行分类,将 XGBoost 随机森林与 scikit-learn 的标准随机森林进行比较:
-
将
XGBRFClassifier
放入classification_model
函数中,看看它在预测用户收入时的表现如何:classification_model(XGBRFClassifier())
得分如下:
0.856085650471878
这个得分很好,比
gbtree
稍低,gbtree
之前的得分是 87%。 -
现在将
RandomForestClassifier
放入相同的函数中,比较结果:classification_model(RandomForestClassifier())
得分如下:
0.8555328202034789
这个得分稍微比 XGBoost 的实现差一些。
由于 XGBoost 的随机森林仍处于开发阶段,我们将在此结束并分析结果。
分析 XGBoost 随机森林
你可以通过将num_parallel_tree
增加到大于1
的值,随时尝试将随机森林作为 XGBoost 的基础学习器。尽管正如你在本节中所见,提升法(boosting)是为了从弱模型中学习,而不是从强模型中学习,因此num_parallel_tree
的值应该保持接近1
。将随机森林作为基础学习器应该谨慎使用。如果单棵树的提升法未能产生最佳分数,随机森林基础学习器是一个可选方案。
或者,XGBoost 的随机森林的XGBRFRegressor
和XGBRFClassifier
可以作为 scikit-learn 随机森林的替代方法来实现。XGBoost 新的XGBRFRegressor
和XGBRFClassifier
表现超过了 scikit-learn 的RandomForestRegressor
和RandomForestClassifier
,尽管这次比较非常接近。鉴于 XGBoost 在机器学习社区中的总体成功,未来使用XGBRFRegressor
和XGBRFClassifier
作为可行的选择绝对值得尝试。
总结
在本章中,你通过将所有 XGBoost 基础学习器(包括gbtree
、dart
、gblinear
和随机森林)应用于回归和分类数据集,极大地扩展了 XGBoost 的使用范围。你预览、应用并调优了独特的基础学习器超参数以提高得分。此外,你还尝试了使用线性构建数据集的gblinear
,以及使用XGBRFRegressor
和XGBRFClassifier
构建没有任何提升法的 XGBoost 随机森林。现在,你已经熟悉了所有基础学习器,你对 XGBoost 的理解已经达到了高级水平。
在下一章中,你将分析 Kaggle 高手的技巧,进一步提升你的 XGBoost 技能!
第九章:第九章:XGBoost Kaggle 大师
在本章中,您将学习从VotingClassifier
和VotingRegressor
中获得的宝贵技巧和窍门,以构建非相关的机器学习集成模型,并了解堆叠最终模型的优势。
在本章中,我们将涵盖以下主要内容:
-
探索 Kaggle 竞赛
-
构建新的数据列
-
构建非相关集成模型
-
堆叠最终模型
技术要求
探索 Kaggle 竞赛
“我只用了 XGBoost(尝试过其他的,但没有一个能达到足够的表现以最终加入我的集成模型中)。”
– Qingchen Wang,Kaggle 获胜者
(www.cnblogs.com/yymn/p/4847130.html
)
在本节中,我们将通过回顾 Kaggle 竞赛的简短历史、它们的结构以及区分验证/测试集与保留/测试集的重要性,来探讨 Kaggle 竞赛。
XGBoost 在 Kaggle 竞赛中的表现
XGBoost 因其在赢得 Kaggle 竞赛中的无与伦比的成功而建立了作为领先机器学习算法的声誉。XGBoost 常常与深度学习模型如神经网络一起出现在获胜的集成模型中,除了单独获胜之外。一个 XGBoost Kaggle 竞赛获胜者的样例列表出现在分布式(深度)机器学习社区的网页上,github.com/dmlc/xgboost/tree/master/demo#machine-learning-challenge-winning-solutions
。要查看更多 XGBoost Kaggle 竞赛获胜者,可以通过Winning solutions of Kaggle competitions (www.kaggle.com/sudalairajkumar/winning-solutions-of-kaggle-competitions
)来研究获胜模型。
注意事项
虽然 XGBoost 经常出现在获胜者中,但其他机器学习模型也有出现。
如第五章《XGBoost 揭秘》中提到的,Kaggle 竞赛是机器学习竞赛,机器学习从业者相互竞争,争取获得最佳分数并赢得现金奖励。当 XGBoost 在 2014 年参加希格斯玻色子机器学习挑战时,它迅速跃升至排行榜顶端,并成为 Kaggle 竞赛中最受欢迎的机器学习算法之一。
在 2014 年到 2018 年间,XGBoost 一直在表格数据(以行和列组织的数据,相对于图像或文本等非结构化数据,神经网络在这些领域占有优势)上表现出色。随着 LightGBM 在 2017 年问世,这款由微软推出的快速梯度提升算法,XGBoost 在表格数据上终于遇到了真正的竞争者。
以下是由八位作者编写的入门论文,LightGBM: A Highly Efficient Gradient Boosting Decision Tree,推荐用作了解 LightGBM 的入门材料:papers.nips.cc/paper/6907-lightgbm-a-highly-efficient-gradient-boosting-decision-tree.pdf
。
在 Kaggle 竞赛中实现一个优秀的机器学习算法,如 XGBoost 或 LightGBM,仅仅做到这一点还不够。同样,调优模型的超参数通常也不足够。尽管单个模型的预测很重要,但工程化新的数据并结合最优模型以获得更高的分数同样至关重要。
Kaggle 竞赛的结构
理解 Kaggle 竞赛的结构是很有价值的,这有助于你理解为什么像无相关的集成构建和堆叠技术在竞赛中如此广泛应用。此外,探索 Kaggle 竞赛的结构也会让你在未来如果选择参与此类竞赛时更有信心。
提示
Kaggle 推荐了 Housing Prices: Advanced Regression Techniques,www.kaggle.com/c/house-prices-advanced-regression-techniques
,适合那些希望从基础过渡到高级竞赛的机器学习学生。这是许多基于知识的竞赛之一,尽管它们不提供现金奖励。
Kaggle 竞赛存在于 Kaggle 网站上。以下是 Avito Context Ad Clicks 竞赛网站的链接,这场比赛于 2015 年由 XGBoost 用户 Owen Zhang 获胜:www.kaggle.com/c/avito-context-ad-clicks/overview
。许多 XGBoost Kaggle 竞赛的获胜者,包括 Owen Zhang,在 2015 年就已获得奖项,这表明 XGBoost 在 Tianqi Chin 2016 年发表的里程碑论文 XGBoost: A Scalable Tree Boosting System 之前就已广泛传播:arxiv.org/pdf/1603.02754.pdf
。
这是 Avito Context Ad Clicks 网站的顶部:
图 9.1 – Avito Context Ad Clicks Kaggle 竞赛网站
该概览页面对竞赛的解释如下:
-
在 概览(以蓝色高亮显示)旁边的附加链接包括 数据,在这里你可以访问竞赛的数据。
-
笔记本,Kagglers 发布解决方案和起始笔记本的地方。
-
讨论区,Kagglers 在这里发布和回答问题。
-
排行榜,展示最高分的地方。
-
规则,解释了竞赛的运作方式。
-
另外,请注意右侧的延迟提交链接,表示即使竞赛已经结束,提交仍然是被接受的,这是 Kaggle 的一项常规政策。
若要下载数据,你需要通过注册一个免费的账户来参加竞赛。数据通常被分为两个数据集,training.csv
是用于构建模型的训练集,test.csv
是用于评估模型的测试集。提交模型后,你会在公共排行榜上获得一个分数。竞赛结束时,最终模型会提交给一个私有测试集,以确定获胜的解决方案。
保留集
在构建机器学习模型时,区分在 Kaggle 竞赛中构建模型与独立构建模型非常重要。到目前为止,我们已经将数据集分为训练集和测试集,以确保我们的模型能够很好地泛化。然而,在 Kaggle 竞赛中,模型必须在竞争环境中进行测试。因此,测试集的数据会保持隐藏。
下面是 Kaggle 的训练集和测试集之间的区别:
-
training.csv
:这是你自己训练和评分模型的地方。这个训练集应该使用train_test_split
或cross_val_score
将其划分为自己的训练集和测试集,从而构建能够很好泛化到新数据的模型。在训练过程中使用的测试集通常被称为验证集,因为它们用来验证模型。 -
test.csv
:这是一个独立的保留集。在模型准备好并可以在它从未见过的数据上进行测试之前,你不会使用测试集。隐藏测试集的目的是保持竞赛的公正性。测试数据对参与者是隐藏的,结果只会在参与者提交模型之后才会公开。
在构建研究或行业模型时,将测试集留存一旁始终是一个良好的做法。当一个模型在已经见过的数据上进行测试时,模型有过拟合测试集的风险,这种情况通常出现在 Kaggle 竞赛中,参赛者通过千分之一的微小差异来过度优化自己的成绩,从而在公共排行榜上提升名次。
Kaggle 竞赛与现实世界在保留集的使用上有所交集。构建机器学习模型的目的是使用未知数据进行准确的预测。例如,如果一个模型在训练集上达到了 100% 的准确率,但在未知数据上只有 50% 的准确率,那么这个模型基本上是没有价值的。
在测试集上验证模型与在保留集上测试模型之间的区别非常重要。
这里是验证和测试机器学习模型的一般方法:
-
将数据划分为训练集和保留集:将保留集隔离开,并抵制查看它的诱惑。
-
将训练集划分为训练集和测试集,或使用交叉验证:在训练集上拟合新模型,并验证模型,一来一回地改进得分。
-
在获得最终模型后,在保留集上进行测试:这是对模型的真正考验。如果得分低于预期,返回到第 2 步并重复。切记——这一点很重要——不要将保留集用作新的验证集,一来一回地调整超参数。这样做会导致模型根据保留集进行调整,这违背了保留集的初衷。
在 Kaggle 竞赛中,过于将机器学习模型与测试集紧密结合是行不通的。Kaggle 通常将测试集拆分为公共和私有两个部分。公共测试集让参赛者有机会评估他们的模型,并进行改进,一来一回地调整并重新提交。私有测试集直到竞赛最后一天才会揭晓。虽然公共测试集的排名会显示,但竞赛的胜者是基于未见测试集的结果宣布的。
赢得 Kaggle 竞赛需要在私有测试集上获得尽可能高的分数。在 Kaggle 竞赛中,每一个百分点都至关重要。业界有时对这种精确度嗤之以鼻,但它促使了创新的机器学习实践来提高得分。理解本章所介绍的这些技术,可以让我们构建更强的模型,并更深入地理解整体机器学习。
开发新列
“几乎总是,我都能找到我想做的事情的开源代码,我的时间应该更多地用在研究和特征工程上。”
– Owen Zhang,Kaggle 冠军
(medium.com/kaggle-blog/profiling-top-kagglers-owen-zhang-currently-1-in-the-world-805b941dbb13
)
许多 Kaggle 参赛者和数据科学家都承认,他们花了相当多的时间在研究和特征工程上。在本节中,我们将使用pandas
来开发新的数据列。
什么是特征工程?
机器学习模型的效果取决于它们训练所用的数据。当数据不足时,构建一个强大的机器学习模型几乎是不可能的。
一个更具启发性的问题是,数据是否可以改进。当从其他列中提取新数据时,这些新列的数据被称为工程化数据。
特征工程是从原始列中开发新数据列的过程。问题不是你是否应该实施特征工程,而是你应该实施多少特征工程。
让我们在预测Uber和Lyft打车费的数据集上进行特征工程实践。
Uber 和 Lyft 数据
除了举办竞赛,Kaggle 还主办了大量数据集,其中包括如下公开数据集,该数据集预测 Uber 和 Lyft 的出租车价格:www.kaggle.com/ravi72munde/uber-lyft-cab-prices
:
-
首先,导入本节所需的所有库和模块,并禁止警告:
import pandas as pd import numpy as np from sklearn.model_selection import cross_val_score from xgboost import XGBClassifier, XGBRFClassifier from sklearn.ensemble import RandomForestClassifier, StackingClassifier from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split, StratifiedKFold from sklearn.metrics import accuracy_score from sklearn.ensemble import VotingClassifier import warnings warnings.filterwarnings('ignore')
-
接下来,加载
'cab_rides.csv'
CSV 文件并查看前五行。限制nrows
为10000
,以加快计算速度。数据总共有超过 60 万行:df = pd.read_csv('cab_rides.csv', nrows=10000) df.head()
以下是预期输出:
图 9.2 – 出租车数据集
此展示显示了多种列,包括类别特征和时间戳。
空值
一如既往,进行任何计算之前要检查空值:
-
记得
df.info()
也提供了关于列类型的信息:df.info()
输出结果如下:
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 distance 10000 non-null float64 1 cab_type 10000 non-null object 2 time_stamp 10000 non-null int64 3 destination 10000 non-null object 4 source 10000 non-null object 5 price 9227 non-null float64 6 surge_multiplier 10000 non-null float64 7 id 10000 non-null object 8 product_id 10000 non-null object 9 name 10000 non-null object dtypes: float64(3), int64(1), object(6) memory usage: 781.4+ KB
从输出结果可以看到,
price
列中存在空值,因为非空浮动数值少于10,000
。 -
检查空值是值得的,以查看是否可以获得更多关于数据的信息:
df[df.isna().any(axis=1)]
以下是输出的前五行:
图 9.3 – 出租车数据集中的空值
如你所见,这些行没有什么特别明显的问题。可能是因为这次乘车的价格从未被记录。
-
由于
price
是目标列,可以使用dropna
删除这些行,并使用inplace=True
参数确保删除操作发生在 DataFrame 中:df.dropna(inplace=True)
你可以使用df.na()
或df.info()
再检查一次,验证没有空值。
特征工程时间列
时间戳列通常表示Unix 时间,即自 1970 年 1 月 1 日以来的毫秒数。可以从时间戳列中提取特定时间数据,帮助预测出租车费用,如月份、小时、是否为高峰时段等:
-
首先,使用
pd.to_datetime
将时间戳列转换为时间对象,然后查看前五行:df['date'] = pd.to_datetime(df['time_stamp']) df.head()
以下是预期输出:
图 9.4 – 时间戳转换后的出租车数据集
这个数据有问题。稍微具备领域知识的人就能知道 Lyft 和 Uber 在 1970 年并不存在。额外的小数位是转换不正确的线索。
-
尝试了几个乘数以进行适当的转换后,我发现
10**6
给出了合适的结果:df['date'] = pd.to_datetime(df['time_stamp']*(10**6)) df.head()
以下是预期输出:
图 9.5 –
'date'
转换后的出租车数据集 -
对于一个日期时间列,你可以在导入
datetime
后提取新列,如month
、hour
和day of week
,如下所示:import datetime as dt df['month'] = df['date'].dt.month df['hour'] = df['date'].dt.hour df['dayofweek'] = df['date'].dt.dayofweek
现在,你可以使用这些列来进行特征工程,创建更多的列,例如判断是否是周末或高峰时段。
-
以下函数通过检查
'dayofweek'
是否等于5
或6
来确定一周中的某天是否为周末,这两个值分别代表星期六或星期天,具体参见官方文档:pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.weekday.html
:def weekend(row): if row['dayofweek'] in [5,6]: return 1 else: return 0
-
接下来,将该函数应用于 DataFrame 作为新列
df['weekend']
,如下所示:df['weekend'] = df.apply(weekend, axis=1)
-
相同的策略可以用来创建一个高峰时段列,通过判断小时是否在早上 6-10 点(小时
6-10
)和下午 3-7 点(小时15-19
)之间:def rush_hour(row): if (row['hour'] in [6,7,8,9,15,16,17,18]) & (row['weekend'] == 0): return 1 else: return 0
-
现在,将该函数应用于新的
'rush_hour'
列:df['rush_hour'] = df.apply(rush_hour, axis=1)
-
最后一行显示了新列的变化,正如
df.tail()
所揭示的:df.tail()
下面是输出的摘录,展示了新列:
图 9.6 – 特征工程后出租车乘车数据集的最后五行
提取和工程化新时间列的过程可以继续进行。
注意
在进行大量新列的工程化时,值得检查是否有新的特征高度相关。数据的相关性将在本章稍后讨论。
现在你理解了时间列特征工程的实践,让我们来进行类别列的特征工程。
类别列特征工程
之前,我们使用 pd.get_dummies
将类别列转换为数值列。Scikit-learn 的 OneHotEncoder
特性是另一种选择,它使用稀疏矩阵将类别数据转换为 0 和 1,这种技术将在 第十章 XGBoost 模型部署 中应用。虽然使用这两种方法将类别数据转换为数值数据是常规做法,但也存在其他的替代方法。
虽然 0 和 1 作为类别列的数值表示是有意义的,因为 0 表示缺失,1 表示存在,但也有可能其他值能提供更好的结果。
一种策略是将类别列转换为它们的频率,这相当于每个类别在给定列中出现的百分比。因此,列中的每个类别都被转换为它在该列中的百分比,而不是一个类别列。
接下来,让我们查看将类别值转换为数值值的步骤。
工程化频率列
要对类别列进行工程化,例如 'cab_type'
,首先查看每个类别的值的数量:
-
使用
.value_counts()
方法查看各类型的频率:df['cab_type'].value_counts()
结果如下:
Uber 4654 Lyft 4573 Name: cab_type, dtype: int64
-
使用
groupby
将计数放入新列。df.groupby(column_name)
是groupby
,而[column_name].transform
指定要转换的列,后面跟着括号内的聚合操作:df['cab_freq'] = df.groupby('cab_type')['cab_type'].transform('count')
-
将新列除以总行数以获得频率:
df['cab_freq'] = df['cab_freq']/len(df)
-
验证更改是否按预期进行:
df.tail()
下面是显示新列的输出摘录:
图 9.7 – 经出租车频率工程处理后的出租车乘车数据集
现在,出租车频率显示出预期的结果。
Kaggle 小贴士 – 均值编码
我们将通过一个经过竞赛验证的特征工程方法来结束这一部分,称为均值编码或目标编码。
均值编码将类别列转换为基于目标变量均值的数值列。例如,如果颜色橙色对应的七个目标值为 1,三个目标值为 0,那么均值编码后的列将是 7/10 = 0.7。由于在使用目标值时存在数据泄漏,因此需要额外的正则化技术。
数据泄漏发生在训练集和测试集之间,或者预测列和目标列之间共享信息时。这里的风险是目标列被直接用来影响预测列,这在机器学习中通常是个坏主意。不过,均值编码已被证明能产生出色的结果。当数据集很深,并且均值分布对于输入数据大致相同时,它仍然有效。正则化是减少过拟合可能性的一项额外预防措施。
幸运的是,scikit-learn 提供了TargetEncoder
来帮助你处理均值转换:
-
首先,从
category_encoders
导入TargetEncoder
。如果无法工作,可以使用以下代码安装category_encoders
:pip install --upgrade category_encoders from category_encoders.target_encoder import TargetEncoder
-
接下来,初始化
encoder
,如下所示:encoder = TargetEncoder()
-
最后,介绍一个新列,并使用编码器的
fit_transform
方法应用均值编码。将要更改的列和目标列作为参数传入:df['cab_type_mean'] = encoder.fit_transform(df['cab_type'], df['price'])
-
现在,验证更改是否按预期进行:
df.tail()
下面是显示新列的输出摘录:
图 9.8 – 经均值编码后的出租车乘车数据集
最右侧的列cab_type_mean
符合预期。
有关均值编码的更多信息,请参考这篇 Kaggle 研究:www.kaggle.com/vprokopev/mean-likelihood-encodings-a-comprehensive-study
。
这里的观点并不是说均值编码比独热编码更好,而是说明均值编码是一种经过验证的技术,在 Kaggle 竞赛中表现优异,可能值得实施来尝试提高得分。
更多特征工程
没有理由止步于此。更多的特征工程可能包括对其他列进行统计度量,使用groupby
和附加编码器。其他类别型列,比如目的地和到达列,可以转换为纬度和经度,然后转换为新的距离度量方式,例如出租车距离或Vincenty距离,它考虑了球面几何。
在 Kaggle 竞赛中,参与者可能会进行数千列新的特征工程,希望能获得几位小数的准确度。如果你有大量的工程化列,可以使用.feature_importances_
选择最重要的列,正如在第二章《决策树深入剖析》中所述。你还可以去除高度相关的列(将在下一节“构建无相关性的集成模型”中解释)。
对于这个特定的出租车乘车数据集,还附带了一个包含天气数据的 CSV 文件。但如果没有天气文件该怎么办呢?你可以自行查找提供日期的天气数据,并将其添加到数据集中。
特征工程是任何数据科学家构建鲁棒模型的必要技能。这里讲解的策略只是现存选项的一部分。特征工程涉及研究、实验、领域专业知识、标准化列、对新列的机器学习性能反馈,并最终缩小最终列的范围。
现在你已经了解了各种特征工程策略,让我们继续讨论构建无相关性的集成模型。
构建无相关性的集成模型
“在我们的最终模型中,我们使用了 XGBoost 作为集成模型,其中包含了 20 个 XGBoost 模型,5 个随机森林,6 个随机化决策树模型,3 个正则化贪婪森林,3 个逻辑回归模型,5 个 ANN 模型,3 个弹性网模型和 1 个 SVM 模型。”
– Song, Kaggle 获胜者
(hunch243.rssing.com/chan-68612493/all_p1.html
)
Kaggle 竞赛的获胜模型很少是单一模型;它们几乎总是集成模型。这里所说的集成模型,并不是指提升(boosting)或袋装(bagging)模型,如随机森林(random forests)或 XGBoost,而是纯粹的集成模型,包含任何不同的模型,包括 XGBoost、随机森林等。
在本节中,我们将结合机器学习模型,构建无相关性的集成模型,以提高准确性并减少过拟合。
模型范围
威斯康星州乳腺癌数据集用于预测患者是否患有乳腺癌,包含 569 行和 30 列数据,可以在scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.html?highlight=load_breast_cancer
查看。
以下是使用几种分类器准备和评分数据集的步骤:
-
从 scikit-learn 导入
load_breast_cancer
数据集,以便我们能快速开始构建模型:from sklearn.datasets import load_breast_cancer
-
通过设置
return_X_y=True
参数,将预测变量列赋值给X
,将目标变量列赋值给y
:X, y = load_breast_cancer(return_X_y=True)
-
使用
StratifiedKFold
准备 5 折交叉验证以确保一致性:kfold = StratifiedKFold(n_splits=5)
-
现在,构建一个简单的分类函数,该函数接收一个模型作为输入,并返回交叉验证的平均得分作为输出:
def classification_model(model): scores = cross_val_score(model, X, y, cv=kfold) return scores.mean()
-
获取几个默认分类器的得分,包括 XGBoost 及其替代基础学习器、随机森林和逻辑回归:
a) 使用 XGBoost 进行评分:
classification_model(XGBClassifier())
得分如下:
0.9771619313771154
b) 使用
gblinear
进行评分:classification_model(XGBClassifier(booster='gblinear'))
得分如下:
0.5782952957615277
c) 使用
dart
进行评分:classification_model(XGBClassifier(booster='dart', one_drop=True))
得分如下:
0.9736376339077782
请注意,对于 dart 增强器,我们设置
one_drop=True
以确保树木确实被丢弃。d) 使用
RandomForestClassifier
进行评分:classification_model(RandomForestClassifier(random_state=2))
得分如下:
0.9666356155876418
e) 使用
LogisticRegression
进行评分:classification_model(LogisticRegression(max_iter=10000))
得分如下:
0.9490451793199813
大多数模型的表现都很不错,其中 XGBoost 分类器获得了最高分。然而,gblinear
基础学习器的表现不太好,因此我们以后将不再使用它。
实际上,应该对每个模型进行调整。由于我们在多个章节中已经介绍了超参数调整,因此在这里不再讨论该选项。然而,了解超参数的知识可以增加尝试快速模型并调整一些参数值的信心。例如,正如以下代码所示,可以尝试将 XGBoost 的 max_depth
降至 2
,将 n_estimators
增加到 500
,并确保将 learning_rate
设置为 0.1
:
classification_model(XGBClassifier(max_depth=2, n_estimators=500, learning_rate=0.1))
得分如下:
0.9701133364384411
这是一个非常不错的得分,尽管它不是最高的,但在我们的集成模型中可能仍然有价值。
现在我们有了多种模型,让我们了解它们之间的相关性。
相关性
本节的目的是选择非相关的模型,而不是选择所有模型进行集成。
首先,让我们理解相关性代表什么。
相关性是一个统计度量,范围从 -1
到 1
,表示两组数据点之间线性关系的强度。相关性为 1
表示完全的直线关系,而相关性为 0
表示没有任何线性关系。
一些关于相关性的可视化图表可以使事情变得更加清晰。以下图表来自维基百科的 Correlation and Dependence 页面,en.wikipedia.org/wiki/Correlation_and_dependence
:
- 显示相关性的散点图如下所示:
图 9.9 – 列出相关性
许可证信息
由 DenisBoigelot 上传,原上传者是 Imagecreator – 自制作品,CC0,commons.wikimedia.org/w/index.php?curid=15165296
。
- Anscombe 四重奏 – 四个相关性为 0.816 的散点图如下所示:
图 9.10 – 相关性为 0.816
许可证信息
由 Anscombe.svg 提供:Schutz(使用下标标记):Avenue – Anscombe.svg,CC BY-SA 3.0,commons.wikimedia.org/w/index.php?curid=9838454
第一个示例表明,相关性越高,点通常越接近直线。第二个示例表明,相同相关性的数据显示点可能会有较大差异。换句话说,相关性提供了有价值的信息,但它不能完全说明问题。
现在你理解了相关性是什么意思,接下来让我们将相关性应用于构建机器学习集成。
机器学习集成中的相关性
现在我们选择要包括在集成学习中的模型。
机器学习模型之间的高相关性在集成学习中是不可取的。那为什么呢?
考虑两个分类器每个有 1,000 个预测的情况。如果这两个分类器做出了相同的预测,那么从第二个分类器中没有获得新信息,使得它变得多余。
使用多数规则实现时,只有在大多数分类器预测错误时,预测才算错误。因此,拥有表现良好但给出不同预测的多样化模型是可取的。如果大多数模型给出了相同的预测,相关性就很高,那么将新模型加入集成学习的价值就不大了。找到模型预测的差异,尤其是强模型可能错误的地方,为集成学习提供了产生更好结果的机会。当模型不相关时,预测结果会有所不同。
要计算机器学习模型之间的相关性,我们首先需要用来比较的数据点。机器学习模型生成的不同数据点是它们的预测结果。获得预测结果后,我们将它们连接成一个数据框,然后应用.corr
方法一次性获取所有相关性。
以下是找到机器学习模型之间相关性的步骤:
-
定义一个函数,返回每个机器学习模型的预测结果:
def y_pred(model): model.fit(X_train, y_train) y_pred = model.predict(X_test) score = accuracy_score(y_pred, y_test) print(score) return y_pred
-
使用
train_test_split
准备数据进行一次预测:X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
-
使用之前定义的函数获取所有分类器候选的预测结果:
a)
XGBClassifier
使用以下方法:y_pred_gbtree = y_pred(XGBClassifier())
准确率得分如下:
0.951048951048951
b)
XGBClassifier
使用dart
,采用以下方法:y_pred_dart = y_pred(XGBClassifier(booster='dart', one_drop=True))
准确率得分如下:
0.951048951048951
c)
RandomForestClassifier
使用以下方法:y_pred_forest = y_pred(RandomForestClassifier())
准确率得分如下:
0.9370629370629371
d)
LogisticRegression
使用以下方法:y_pred_logistic = y_pred(LogisticRegression(max_iter=10000))
准确率得分如下:
0.9370629370629371 y_pred_xgb = y_pred(XGBClassifier(max_depth=2, n_estimators=500, learning_rate=0.1))
准确率得分如下:
0.965034965034965
-
使用
np.c
(c
代表连接)将预测结果连接成一个新的数据框:df_pred = pd.DataFrame(data= np.c_[y_pred_gbtree, y_pred_dart, y_pred_forest, y_pred_logistic, y_pred_xgb], columns=['gbtree', 'dart','forest', 'logistic', 'xgb'])
-
使用
.corr()
方法在数据框上运行相关性计算:df_pred.corr()
你应该看到以下输出:
图 9.11 – 各种机器学习模型之间的相关性
如您所见,所有对角线上的相关性都是1.0
,因为模型与自身之间的相关性必须是完全线性的。其他所有值也相当高。
没有明确的截断值来确定非相关性的阈值。最终选择依赖于相关性值和可选模型的数量。对于这个例子,我们可以选择与最佳模型xgb
相关性最小的下两个模型,分别是随机森林和逻辑回归。
现在我们已经选择了模型,接下来我们将它们组合成一个集成模型,使用VotingClassifier
集成,如下所述。
VotingClassifier 集成
Scikit-learn 的VotingClassifier
集成旨在结合多个分类模型,并使用多数规则选择每次预测的输出。请注意,scikit-learn 还包含VotingRegressor
,它通过取每个回归模型的平均值来结合多个回归模型。
以下是在 scikit-learn 中创建集成模型的步骤:
-
初始化一个空列表:
estimators = []
-
初始化第一个模型:
logistic_model = LogisticRegression(max_iter=10000)
-
将模型作为元组
(model_name, model)
追加到列表中:estimators.append(('logistic', logistic_model))
-
根据需要重复步骤 2和步骤 3:
xgb_model = XGBClassifier(max_depth=2, n_estimators=500, learning_rate=0.1) estimators.append(('xgb', xgb_model)) rf_model = RandomForestClassifier(random_state=2) estimators.append(('rf', rf_model))
-
使用模型列表作为输入初始化
VotingClassifier
(或VotingRegressor
):ensemble = VotingClassifier(estimators)
-
使用
cross_val_score
评分分类器:scores = cross_val_score(ensemble, X, y, cv=kfold) print(scores.mean())
得分如下:
0.9754075454122031
如您所见,得分已经提高。
现在您已经了解了构建无相关性机器学习集成模型的目的和技术,让我们继续探讨一种类似但可能更有优势的技术——堆叠。
堆叠模型
“对于堆叠和提升方法,我使用 xgboost,主要是由于对它的熟悉以及它验证过的优异结果。”
– David Austin, Kaggle 冠军
(www.pyimagesearch.com/2018/03/26/interview-david-austin-1st-place-25000-kaggles-popular-competition/
)
在本节最后,我们将探讨 Kaggle 获奖者经常使用的最强大技巧之一——堆叠。
什么是堆叠?
堆叠将机器学习模型结合在两个不同的层次:基础层,模型对所有数据进行预测;元层,将基础层模型的预测作为输入,并用它们生成最终预测。
换句话说,堆叠中的最终模型并不直接使用原始数据作为输入,而是将基础机器学习模型的预测作为输入。
堆叠模型在 Kaggle 比赛中取得了巨大的成功。大多数 Kaggle 比赛都有合并截止日期,个人和团队可以在此期间合并。通过合并,作为团队而非个人竞争可以获得更大的成功,因为参赛者可以构建更大的集成模型并将其堆叠在一起。
注意,堆叠与标准集成方法不同,因为它有一个在最后进行预测组合的元模型。由于元模型将预测值作为输入,因此通常建议使用一个简单的元模型,比如回归任务中的线性回归和分类任务中的逻辑回归。
现在你对堆叠有所了解,让我们使用 scikit-learn 应用堆叠。
在 scikit-learn 中的堆叠
幸运的是,scikit-learn 提供了一个堆叠回归器和分类器,使得这一过程相当简单。其基本思路与上一节中的集成模型非常相似。选择多种基础模型,然后为元模型选择线性回归或逻辑回归。
以下是在 scikit-learn 中使用堆叠的步骤:
-
创建一个空的基础模型列表:
base_models = []
-
使用语法
(name, model)
将所有基础模型作为元组附加到基础模型列表中:base_models.append(('lr', LogisticRegression())) base_models.append(('xgb', XGBClassifier())) base_models.append(('rf', RandomForestClassifier(random_state=2)))
在堆叠中可以选择更多的模型,因为没有多数规则的限制,并且线性权重能更容易地调整到新数据。一个最佳方法是使用非相关性作为松散的指导原则,并尝试不同的组合。
-
选择一个元模型,最好是回归任务中的线性回归和分类任务中的逻辑回归:
meta_model = LogisticRegression()
-
使用
base_models
作为estimators
,meta_model
作为final_estimator
来初始化StackingClassifier
(或StackingRegressor
):clf = StackingClassifier(estimators=base_models, final_estimator=meta_model)
-
使用
cross_val_score
或任何其他评分方法来验证堆叠模型:scores = cross_val_score(clf, X, y, cv=kfold) print(scores.mean())
得分如下:
0.9789318428815401
这是迄今为止最强的结果。
正如你所看到的,堆叠是一种非常强大的方法,它超越了上一节中的非相关集成模型。
总结
在这一章中,你学习了一些来自 Kaggle 竞赛获胜者的经过验证的技巧和窍门。除了探索 Kaggle 竞赛并理解保留集的重要性外,你还获得了在时间列特征工程、类别列特征工程、均值编码、构建非相关集成模型以及堆叠方面的基本实践。这些高级技术在精英 Kaggler 中广泛使用,它们能在你开发用于研究、竞赛和行业的机器学习模型时,提供优势。
在下一章,也是最后一章,我们将从竞争世界转向技术世界,在这里我们将使用转换器和管道从头到尾构建一个 XGBoost 模型,完成一个适合行业部署的模型。
第十章:第十章: XGBoost 模型部署
在本章关于 XGBoost 的最后一部分,您将把所有内容结合起来,并开发新的技术,构建一个适应行业需求的强大机器学习模型。将模型部署到行业环境中与为研究和竞赛构建模型有所不同。在行业中,自动化非常重要,因为新数据会频繁到达。更多的重视放在流程上,而不是通过微调机器学习模型来获得微小的百分比提升。
本章中,您将获得与独热编码和稀疏矩阵相关的丰富经验。此外,您还将实现并自定义 scikit-learn 转换器,以自动化机器学习管道,对包含分类和数值列的混合数据进行预测。到本章结束时,您的机器学习管道将准备好处理任何新来的数据。
在本章中,我们将讨论以下主题:
-
编码混合数据
-
自定义 scikit-learn 转换器
-
完成 XGBoost 模型
-
构建机器学习管道
技术要求
编码混合数据
想象一下,您正在为一家教育科技公司工作,您的工作是预测学生成绩,以便为弥补技术技能差距的服务提供目标。您的第一步是将包含学生成绩的数据加载到pandas
中。
加载数据
由公司提供的学生表现数据集,可以通过加载已为您导入的student-por.csv
文件来访问。
首先导入pandas
并关闭警告。然后,下载数据集并查看前五行:
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
df = pd.read_csv('student-por.csv')
df.head()
这里是预期的输出:
图 10.1 – 学生表现数据集的原始样子
欢迎来到行业世界,在这里数据并不总是如预期那样呈现。
推荐的选项是查看 CSV 文件。可以在 Jupyter Notebook 中通过定位到本章节的文件夹并点击student-por.csv
文件来实现。
您应该看到如下内容:
图 10.2 – 学生表现 CSV 文件
如前图所示,数据是通过分号分隔的。CSV 代表pandas
,它带有一个sep
参数,表示分隔符,可以将其设置为分号(;),如下所示:
df = pd.read_csv('student-por.csv', sep=';')
df.head()
这里是预期的输出:
图 10.3 – 学生表现数据集
现在,DataFrame 看起来符合预期,包含了分类值和数值的混合数据,我们必须清理空值。
清理空值
你可以通过在df.insull()
上调用.sum()
方法来查看所有包含空值的列。以下是结果的摘录:
df.isnull().sum()
school 0
sex 1
age 1
address 0
…
health 0
absences 0
G1 0
G2 0
G3 0
dtype: int64
你可以通过将df.isna().any(axis=1)
放入括号中与df
一起使用,来查看这些列的行:
df[df.isna().any(axis=1)]
以下是预期的输出:
图 10.4 – 学生成绩数据集的空数据
最好将空列显示在中间,Jupyter 默认会删除它们,因为列数较多。可以通过将max columns
设置为None
来轻松修正,如下所示:
pd.options.display.max_columns = None
现在,再次运行代码会显示所有的列:
df[df.isna().any(axis=1)]
以下是预期的输出摘录:
图 10.5 – 学生成绩数据集中所有行的空数据
如你所见,所有列,包括'guardian'
下隐藏的空值,现已显示。
数值型空值可以设置为-999.0
,或其他值,XGBoost 将使用missing
超参数为你找到最佳替代值,正如在第五章中介绍的那样,XGBoost 揭示。
下面是用-999.0
填充'age'
列的代码:
df['age'].fillna(-999.0)
接下来,类别列可以通过众数进行填充。众数是列中最常见的值。用众数填充类别列可能会扭曲结果分布,但只有在空值数量很大时才会发生。然而,我们只有两个空值,所以我们的分布不会受到影响。另一种选择是将类别空值替换为'unknown'
字符串,经过独热编码后,这个字符串可能会变成一个单独的列。需要注意的是,XGBoost 需要数值型输入,因此从 2020 年起,missing
超参数不能直接应用于类别列。
以下代码将'sex'
和'guardian'
这两列类别数据转换为众数:
df['sex'] = df['sex'].fillna(df['sex'].mode())
df['guardian'] = df['guardian'].fillna(df['guardian'].mode())
由于我们的空值出现在前两行,我们可以通过使用df.head()
来显示它们已经被更改:
df.head()
以下是预期的输出:
图 10.6 – 删除空值后的学生成绩数据集(仅显示前五行)
空值已经按预期被清除。
接下来,我们将使用独热编码将所有类别列转换为数值列。
独热编码
之前,我们使用pd.get_dummies
将所有类别变量转换为0
和1
的数值,其中0
表示缺失,1
表示存在。虽然这种方法是可以接受的,但它也存在一些不足之处。
第一个缺点是pd.get_dummies
在计算上可能非常昂贵,就像你在前几章等待代码运行时所发现的那样。第二个缺点是pd.get_dummies
在转换到 scikit-learn 的管道时效果不太好,这是我们将在下一节中探讨的概念。
一个不错的pd.get_dummies
替代方案是 scikit-learn 的OneHotEncoder
。与pd.get_dummies
类似,独热编码将所有类别值转换为0
和1
,其中0
表示缺失,1
表示存在,但与pd.get_dummies
不同的是,它并不在计算上昂贵。OneHotEncoder
使用稀疏矩阵而不是密集矩阵,从而节省空间和时间。
稀疏矩阵通过只存储不包含零的值来节省空间。通过使用更少的位数,保存了相同的信息量。
此外,OneHotEncoder
是一个 scikit-learn 的转换器,意味着它是专门设计用来在机器学习管道中使用的。
在以前的 scikit-learn 版本中,OneHotEncoder
只接受数值输入。在那时,需要使用LabelEncoder
先将所有类别列转换为数值列,作为中间步骤。
要在特定列上使用OneHotEncoder
,可以按照以下步骤进行:
-
将所有
dtype
为对象的类别列转换为列表:categorical_columns = df.columns[df.dtypes==object].tolist()
-
导入并初始化
OneHotEncoder
:from sklearn.preprocessing import OneHotEncoder ohe = OneHotEncoder()
-
在列上使用
fit_transform
方法:hot = ohe.fit_transform(df[categorical_columns])
-
0
或1
。 -
如果你想查看
hot
稀疏矩阵的实际样子,可以按如下方式打印出来:print(hot)
这是结果的一个摘录:
(0, 0) 1.0 (0, 2) 1.0 (0, 5) 1.0 (0, 6) 1.0 (0, 8) 1.0 … 0 have been skipped. For instance, the 0th row and the 1st column, denoted by (0, 1), has a value of 0.0 in the dense matrix, but it's skipped over in the one-hot matrix.
如果你想获得更多关于稀疏矩阵的信息,只需输入以下变量:
hot
结果如下:
<649x43 sparse matrix of type '<class 'numpy.float64'>'
with 11033 stored elements in Compressed Sparse Row format>
这告诉我们,矩阵是649
行43
列,但只有11033
个值被存储,从而节省了大量空间。请注意,对于包含许多零的文本数据,稀疏矩阵是非常常见的。
合并独热编码矩阵和数值列
现在我们拥有一个独热编码的稀疏矩阵,接下来我们需要将它与原始 DataFrame 的数值列合并。
首先,让我们分离数值列。可以使用df.select_dtypes
的exclude=["object"]
参数来选择特定类型的列,方法如下:
cold_df = df.select_dtypes(exclude=["object"])
cold_df.head()
这是预期的输出:
图 10.8 – 学生表现数据集的数值列
这些就是我们要查找的列。
对于这种规模的数据,我们可以选择将稀疏矩阵转换为常规的 DataFrame,如前面的截图所示,或者将这个 DataFrame 转换为稀疏矩阵。我们将选择后者,因为行业中的 DataFrame 可能非常庞大,节省空间是有利的:
-
要将
cold_df
DataFrame 转换为压缩稀疏矩阵,请从scipy.sparse
导入csr_matrix
并将 DataFrame 放入其中,如下所示:from scipy.sparse import csr_matrix cold = csr_matrix(cold_df)
-
最后,通过导入并使用
hstack
,将热矩阵和冷矩阵堆叠起来,hstack
将稀疏矩阵水平组合:from scipy.sparse import hstack final_sparse_matrix = hstack((hot, cold))
-
通过将稀疏矩阵转换为密集矩阵并像往常一样显示数据框,验证
final_sparse_matrix
是否按预期工作:final_df = pd.DataFrame(final_sparse_matrix.toarray()) final_df.head()
这是预期的输出:
图 10.9 – 最终稀疏矩阵的数据框
输出被向右移动,以显示一热编码和数值列一起呈现。
现在数据已准备好进行机器学习,让我们使用变换器和流水线自动化这个过程。
定制 Scikit-learn 变换器
现在我们有了一个将数据框转换为适合机器学习的稀疏矩阵的过程,将其用变换器进行泛化,以便可以轻松地为新的数据重复这个过程,将会是一个有利的选择。
Scikit-learn 的变换器通过使用fit
方法与机器学习算法一起工作,fit
方法用于找到模型参数,transform
方法则将这些参数应用于数据。这些方法可以合并为一个fit_transform
方法,在一行代码中同时完成拟合和变换数据。
当一起使用时,各种变换器,包括机器学习算法,可以在同一流水线中协同工作,便于使用。然后,数据被放入流水线中,进行拟合和变换,以实现期望的输出。
Scikit-learn 提供了许多优秀的变换器,例如StandardScaler
和Normalizer
分别用于标准化和归一化数据,SimpleImputer
用于转换空值。然而,当数据包含混合类别和数值列时(如本例所示),你需要小心。在某些情况下,Scikit-learn 提供的选项可能不是自动化的最佳选择。在这种情况下,值得创建你自己的变换器来精确地完成你想要的操作。
定制变换器
创建你自己的变换器的关键是使用 Scikit-learn 的TransformerMixin
作为父类。
这是在 Scikit-learn 中创建定制变换器的一般代码框架:
class YourClass(TransformerMixin):
def __init__(self):
None
def fit(self, X, y=None):
return self
def transform(self, X, y=None):
# insert code to transform X
return X
如你所见,你无需初始化任何内容,fit
方法可以始终返回self
。简而言之,你可以将所有用于变换数据的代码放在transform
方法下。
现在你已经了解了定制化的一般操作方式,让我们创建一个定制的变换器,用来处理不同类型的空值。
定制混合空值填充器
让我们通过创建一个定制的混合空值填充器来看看这个如何工作。这里,定制化的原因是为了处理不同类型的列,并采用不同的方式修正空值。
以下是步骤:
-
导入
TransformerMixin
并定义一个新类,以TransformerMixin
作为父类:from sklearn.base import TransformerMixin class NullValueImputer(TransformerMixin):
-
使用
self
作为输入初始化类。如果这个操作没有实际作用也没关系:def __init__(self): None
-
创建一个
fit
方法,接受self
和X
作为输入,y=None
,并返回self
:def fit(self, X, y=None): return self
-
创建一个
transform
方法,该方法接受self
和X
作为输入,y=None
,并通过返回一个新的X
来转换数据,如下所示:def transform(self, X, y=None):
我们需要根据列的不同单独处理空值。
以下是将空值转换为众数或
-999.0
的步骤,具体取决于列的类型:a) 通过将列转换为列表来循环遍历列:
for column in X.columns.tolist():
b) 在循环中,通过检查哪些列是
object
类型来访问字符串列:if column in X.columns[X.dtypes==object].tolist():
c) 将字符串(
object
)列的空值转换为众数:X[column] = X[column].fillna(X[column].mode())
d) 否则,将列填充为
-999.0
:else: X[column]=X[column].fillna(-999.0) return X
在前面的代码中,你可能会想知道为什么使用了y=None
。原因是,当将机器学习算法纳入管道时,y
将作为输入使用。通过将y
设置为None
,只会对预测列进行预期的更改。
现在定制的插补器已经定义完成,可以通过在数据上调用fit_transform
方法来使用它。
通过从 CSV 文件创建一个新的 DataFrame 并使用定制的NullValueImputer
在一行代码中转换空值,来重置数据:
df = pd.read_csv('student-por.csv', sep=';')
nvi = NullValueImputer().fit_transform(df)
nvi.head()
这里是期望的输出:
图 10.10 – 使用 NullValueImputer()后的学生表现 DataFrame
如你所见,所有空值都已清除。
接下来,让我们像之前一样将数据转换为独热编码的稀疏矩阵。
独热编码混合数据
我们将采取与上一节类似的步骤,通过创建一个定制的转换器对分类列进行独热编码,然后将它们与数值列合并为一个稀疏矩阵(对于这个大小的数据集,密集矩阵也可以):
-
定义一个新的类,继承
TransformerMixin
作为父类:class SparseMatrix(TransformerMixin):
-
用
self
作为输入初始化类。如果这没有任何作用也没关系:def __init__(self): None
-
创建一个
fit
方法,该方法接受self
和X
作为输入并返回self
:def fit(self, X, y=None): return self
-
创建一个
transform
方法,该方法接受self
和X
作为输入,转换数据并返回新的X
:def transform(self, X, y=None):
以下是完成转换的步骤;首先访问仅包含
object
类型的分类列,如下所示:a) 将分类列放入一个列表中:
categorical_columns= X.columns[X.dtypes==object].tolist()
b) 初始化
OneHotEncoder
:ohe = OneHotEncoder()
c) 使用
OneHotEncoder
转换分类列:hot = ohe.fit_transform(X[categorical_columns])
d) 通过排除字符串,创建一个仅包含数值列的 DataFrame:
cold_df = X.select_dtypes(exclude=["object"])
e) 将数值型 DataFrame 转换为稀疏矩阵:
cold = csr_matrix(cold_df)
f) 将两个稀疏矩阵合并为一个:
final_sparse_matrix = hstack((hot, cold))
g) 将其转换为压缩稀疏行(CSR)矩阵,以限制错误。请注意,XGBoost 要求使用 CSR 矩阵,并且根据你的 XGBoost 版本,转换可能会自动发生:
final_csr_matrix = final_sparse_matrix.tocsr() return final_csr_matrix
-
现在我们可以使用强大的
fit_transform
方法对没有空值的nvi
数据进行转换:sm = SparseMatrix().fit_transform(nvi) print(sm)
这里给出的期望输出已被截断以节省空间:
(0, 0) 1.0 (0, 2) 1.0 (0, 5) 1.0 (0, 6) 1.0 (0, 8) 1.0 (0, 10) 1.0 : : (648, 53) 4.0 (648, 54) 5.0 (648, 55) 4.0 (648, 56) 10.0 (648, 57) 11.0 (648, 58) 11.0
-
你可以通过将稀疏矩阵转换回密集矩阵来验证数据是否符合预期,方法如下:
sm_df = pd.DataFrame(sm.toarray()) sm_df.head()
以下是预期的密集输出:
图 10.11 – 稀疏矩阵转换为密集矩阵
这看起来是正确的。图中显示第 27 列的值为0.0
,第 28 列的值为1.0
。前面的独热编码输出排除了(0
,27
),并显示(0
,28
)的值为1.0
,与密集输出匹配。
现在数据已经转换,让我们将两个预处理步骤合并到一个单一的管道中。
预处理管道
在构建机器学习模型时,通常会先将数据分为X
和y
。在考虑管道时,合理的做法是转换X
(预测列),而不是y
(目标列)。此外,重要的是保留一个测试集以供后用。
在将数据放入机器学习管道之前,让我们将数据拆分为训练集和测试集,并保留测试集。我们按以下方式从头开始:
-
首先将 CSV 文件读取为一个 DataFrame:
df = pd.read_csv('student-por.csv', sep=';')
在为学生表现数据集选择
X
和y
时,需要注意的是,最后三列都包含学生成绩。这里有两个潜在的研究方向值得关注:a) 包括之前的成绩作为预测列
b) 不包括之前的成绩作为预测列
假设你的 EdTech 公司希望基于社会经济变量进行预测,而不是基于以前获得的成绩,因此忽略前两列成绩,索引为
-2
和-3
。 -
选择最后一列作为
y
,并将最后三列以外的所有列作为X
:y = df.iloc[:, -1] X = df.iloc[:, :-3]
-
现在导入
train_test_split
,并将X
和y
拆分为训练集和测试集:from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=2)
现在让我们按照以下步骤构建管道:
-
首先从
sklearn.pipeline
导入Pipeline
:from sklearn.pipeline import Pipeline
-
接下来,使用语法(name,transformer)将元组分配为
Pipeline
中的参数,按顺序排列:data_pipeline = Pipeline([('null_imputer', NullValueImputer()), ('sparse', SparseMatrix())])
-
最后,通过将
X_train
放入data_pipeline
的fit_transform
方法中来转换我们的预测列X_train
:X_train_transformed = data_pipeline.fit_transform(X_train)
现在你有了一个数值稀疏矩阵,没有空值,可以作为机器学习的预测列。
此外,你有一个管道,可以通过一行代码转换任何传入的数据!现在让我们完成一个 XGBoost 模型来进行预测。
完成 XGBoost 模型的最终调整
现在是时候构建一个稳健的 XGBoost 模型并将其添加到管道中。请按以下方式导入XGBRegressor
、numpy
、GridSearchCV
、cross_val_score
、KFold
和mean_squared_error
:
import numpy as np
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score, KFold
from sklearn.metrics import mean_squared_error as MSE
from xgboost import XGBRegressor
现在让我们构建模型。
第一个 XGBoost 模型
这个学生表现数据集在预测列y_train
方面有一个有趣的值范围,可以如下所示:
y_train.value_counts()
结果如下:
11 82
10 75
13 58
12 53
14 42
15 36
9 29
16 27
8 26
17 24
18 14
0 10
7 7
19 1
6 1
5 1
如你所见,数值范围从5
到19
,并且包括0
。
由于目标列是有序的,意味着其值是按数字顺序排列的,因此回归方法比分类方法更为合适,尽管输出有限。在通过回归训练模型后,最终结果可能会进行四舍五入,以给出最终预测。
下面是对 XGBRegressor
使用此数据集进行评分的步骤:
-
从设置使用
KFold
的交叉验证开始:kfold = KFold(n_splits=5, shuffle=True, random_state=2)
-
现在定义一个交叉验证函数,该函数返回
cross_val_score
:def cross_val(model): scores = cross_val_score(model, X_train_transformed, y_train, scoring='neg_root_mean_squared_error', cv=kfold) rmse = (-scores.mean()) return rmse
-
通过调用
cross_val
以XGBRegressor
为输入并将missing=-999.0
,为 XGBoost 寻找最佳替代值,建立一个基准分数:cross_val(XGBRegressor(missing=-999.0))
分数如下:
2.9702248207546296
这是一个值得尊敬的起始分数。2.97
的均方根误差,基于 19 种可能性,表示分数的准确性相差不到几个点。这几乎是 15%,在美国 A-B-C-D-F 系统中准确到一个字母等级。在工业界,您甚至可以使用统计学方法来包括置信区间,提供预测区间,这是本书范围之外的推荐策略。
现在您已有了基准分数,让我们微调超参数以改进模型。
微调 XGBoost 超参数
我们从检查带有提前停止的 n_estimators
开始。回忆一下,为了使用提前停止,我们可能会检查一个测试折叠。创建测试折叠需要进一步划分 X_train
和 y_train
:
-
这是第二个
train_test_split
,可用于创建一个测试集进行验证,同时确保将真实的测试集保留到后期:X_train_2, X_test_2, y_train_2, y_test_2 = train_test_split(X_train_transformed, y_train, random_state=2)
-
现在定义一个函数,使用提前停止来返回回归器的最佳估计器数量(参见 第六章,XGBoost 超参数):
def n_estimators(model): eval_set = [(X_test_2, y_test_2)] eval_metric="rmse" model.fit(X_train_2, y_train_2, eval_metric=eval_metric, eval_set=eval_set, early_stopping_rounds=100) y_pred = model.predict(X_test_2) rmse = MSE(y_test_2, y_pred)**0.5 return rmse
-
现在运行
n_estimators
函数,将最大值设置为5000
:n_estimators(XGBRegressor(n_estimators=5000, missing=-999.0))
下面是输出的最后五行:
[128] validation_0-rmse:3.10450 [129] validation_0-rmse:3.10450 [130] validation_0-rmse:3.10450 [131] validation_0-rmse:3.10450 Stopping. Best iteration: [31] validation_0-rmse:3.09336
分数如下:
3.0933612343143153
使用我们默认的模型,31 个估计器目前给出最佳估算值。这将是我们的起点。
接下来,这是一个我们多次使用的 grid_search
函数,它搜索超参数网格并显示最佳参数和最佳分数:
def grid_search(params, reg=XGBRegressor(missing=-999.0)):
grid_reg = GridSearchCV(reg, params, scoring='neg_mean_squared_error', cv=kfold)
grid_reg.fit(X_train_transformed, y_train)
best_params = grid_reg.best_params_
print("Best params:", best_params)
best_score = np.sqrt(-grid_reg.best_score_)
print("Best score:", best_score)
以下是一些推荐的模型微调步骤:
-
从将
max_depth
范围设置为1
到8
开始,同时将n_estimators
设置为31
:grid_search(params={'max_depth':[1, 2, 3, 4, 6, 7, 8], 'n_estimators':[31]})
结果如下:
Best params: {'max_depth': 1, 'n_estimators': 31} Best score: 2.6634430373079425
-
将
max_depth
从1
缩小到3
,同时将min_child_weight
范围设定为1
到5
,并保持n_estimators
为31
:grid_search(params={'max_depth':[1, 2, 3], 'min_child_weight':[1,2,3,4,5], 'n_estimators':[31]})
结果如下:
Best params: {'max_depth': 1, 'min_child_weight': 1, 'n_estimators': 31} Best score: 2.6634430373079425
没有改进。
-
通过强制将
min_child_weight
设置为2
或3
,同时包含subsample
范围从0.5
到0.9
,你可能可以保证一些变化。此外,增加n_estimators
可能有助于为模型提供更多学习时间:grid_search(params={'max_depth':[2], 'min_child_weight':[2,3], 'subsample':[0.5, 0.6, 0.7, 0.8, 0.9], 'n_estimators':[31, 50]})
结果如下:
Best params: {'max_depth': 1, 'min_child_weight': 2, 'n_estimators': 50, 'subsample': 0.9} Best score: 2.665209161229433
分数几乎相同,但略有下降。
-
缩小
min_child_weight
和subsample
的范围,同时将colsample_bytree
设置为0.5
到0.9
的范围:grid_search(params={'max_depth':[1], 'min_child_weight':[1, 2, 3], 'subsample':[0.6, 0.7, 0.8], 'colsample_bytree':[0.5, 0.6, 0.7, 0.8, 0.9, 1], 'n_estimators':[50]})
结果如下:
Best params: {'colsample_bytree': 0.9, 'max_depth': 1, 'min_child_weight': 3, 'n_estimators': 50, 'subsample': 0.8} Best score: 2.659649642579931
这是目前为止最好的分数。
-
保持当前最优值,尝试
colsample_bynode
和colsample_bylevel
的范围从0.6
到1.0
:grid_search(params={'max_depth':[1], 'min_child_weight':[3], 'subsample':[.8], 'colsample_bytree':[0.9], 'colsample_bylevel':[0.6, 0.7, 0.8, 0.9, 1], 'colsample_bynode':[0.6, 0.7, 0.8, 0.9, 1], 'n_estimators':[50]})
结果如下:
Best params: {'colsample_bylevel': 0.9, 'colsample_bynode': 0.8, 'colsample_bytree': 0.9, 'max_depth': 1, 'min_child_weight': 3, 'n_estimators': 50, 'subsample': 0.8} Best score: 2.64172735526102
得分再次提高。
进一步尝试使用基础学习器dart
和gamma
,但未获得新收益。
根据项目的时间和范围,可能值得进一步调整超参数,甚至将它们一起放入RandomizedSearch
中尝试。在工业界,你很有可能可以访问到云计算,在这里,廉价的、可抢占的虚拟机(VMs)将允许更多的超参数搜索,以找到更好的结果。只需要注意的是,scikit-learn 目前没有提供一种方法来停止耗时的搜索,以便在代码完成之前保存最佳参数。
现在我们有了一个健壮的模型,可以继续前进并测试该模型。
测试模型
现在你有了一个潜在的最终模型,重要的是在测试集上对它进行测试。
记住,测试集在我们的管道中并没有进行转化。幸运的是,此时只需要一行代码即可将其转化:
X_test_transformed = data_pipeline.fit_transform(X_test)
现在我们可以使用之前部分中选择的最佳超参数初始化一个模型,拟合训练集,并在保留的测试集上进行测试:
model = XGBRegressor(max_depth=2, min_child_weight=3, subsample=0.9, colsample_bytree=0.8, gamma=2, missing=-999.0)
model.fit(X_train_transformed, y_train)
y_pred = model.predict(X_test_transformed)
rmse = MSE(y_pred, y_test)**0.5
rmse
得分如下:
2.7908972630881435
得分稍微高一些,尽管这可能是由于折叠的原因。
如果没有的话,我们的模型可能在验证集上拟合得过于紧密,这在微调超参数并将其精确调整以改进验证集时是可能发生的。模型的泛化能力还不错,但它可以有更好的泛化能力。
对于接下来的步骤,当考虑是否可以改进得分时,以下选项是可用的:
-
返回超参数微调。
-
保持模型不变。
-
根据超参数知识进行快速调整。
快速调整超参数是可行的,因为模型可能会过拟合。例如,增加min_child_weight
并降低subsample
应该有助于模型更好地泛化。
让我们进行最终的调整,得到一个最终模型:
model = XGBRegressor(max_depth=1,
min_child_weight=5,
subsample=0.6,
colsample_bytree=0.9,
colsample_bylevel=0.9,
colsample_bynode=0.8,
n_estimators=50,
missing=-999.0)
model.fit(X_train_transformed, y_train)
y_pred = model.predict(X_test_transformed)
rmse = MSE(y_pred, y_test)**0.5
rmse
结果如下:
2.730601403138633
请注意,得分已经提高。
此外,你绝对不应该反复尝试提高验证集得分。在收到测试得分后进行少许调整是可以接受的;否则,你永远无法在第一次结果的基础上进行改进。
现在剩下的就是完成管道。
构建机器学习管道
完成机器学习管道需要将机器学习模型添加到之前的管道中。你需要在NullValueImputer
和SparseMatrix
之后,得到一个机器学习元组,如下所示:
full_pipeline = Pipeline([('null_imputer', NullValueImputer()), ('sparse', SparseMatrix()),
('xgb', XGBRegressor(max_depth=1, min_child_weight=5, subsample=0.6, colsample_bytree=0.9, colsample_bylevel=0.9, colsample_bynode=0.8, missing=-999.0))])
这个管道现在已经完成了机器学习模型,并且它可以在任何X
,y
组合上进行拟合,如下所示:
full_pipeline.fit(X, y)
现在你可以对任何目标列未知的数据进行预测:
new_data = X_test
full_pipeline.predict(new_data)
这是预期输出的前几行:
array([13.55908 , 8.314051 , 11.078157 , 14.114085 , 12.2938385, 11.374797 , 13.9611025, 12.025812 , 10.80344 , 13.479145 , 13.02319 , 9.428679 , 12.57761 , 12.405045 , 14.284043 , 8.549758 , 10.158956 , 9.972576 , 15.502667 , 10.280028 , ...
为了得到更真实的预测,数据可能需要按如下方式四舍五入:
np.round(full_pipeline.predict(new_data))
期望的输出如下:
array([14., 8., 11., 14., 12., 11., 14., 12., 11., 13., 13., 9., 13., 12., 14., 9., 10., 10., 16., 10., 13., 13., 7., 12., 7., 8., 10., 13., 14., 12., 11., 12., 15., 9., 11., 13., 12., 11., 8.,
...
11., 13., 12., 13., 9., 13., 10., 14., 12., 15., 15., 11., 14., 10., 14., 9., 9., 12., 13., 9., 11., 14., 13., 11., 13., 13., 13., 13., 11., 13., 14., 15., 13., 9., 10., 13., 8., 8., 12., 15., 14., 13., 10., 12., 13., 9.], dtype=float32)
最后,如果新数据到达,可以将其与之前的数据连接,并通过相同的管道处理,从而得到一个更强大的模型,因为新的模型可能会基于更多的数据进行训练,如下所示:
new_df = pd.read_csv('student-por.csv')
new_X = df.iloc[:, :-3]
new_y = df.iloc[:, -1]
new_model = full_pipeline.fit(new_X, new_y)
现在,这个模型可以用于对新数据进行预测,如以下代码所示:
more_new_data = X_test[:25]
np.round(new_model.predict(more_new_data))
期望的输出如下:
array([14., 8., 11., 14., 12., 11., 14., 12., 11., 13., 13., 9., 13., 12., 14., 9., 10., 10., 16., 10., 13., 13., 7., 12., 7.],
dtype=float32)
有一个小小的细节。
如果你只想对一行数据进行预测怎么办?如果你将单行数据通过管道处理,生成的稀疏矩阵将没有正确的列数,因为它只会对单行数据中存在的类别进行独热编码。这将导致数据中的不匹配错误,因为机器学习模型已经适配了一个需要更多数据行的稀疏矩阵。
一个简单的解决方法是将新的一行数据与足够多的行数据连接起来,以确保生成的稀疏矩阵包含所有可能的类别列并进行转换。我们已经看到,这样操作使用X_test
中的 25 行数据是有效的,因为没有出现错误。使用X_test
中的 20 行或更少的数据将导致在这种情况下出现不匹配错误。
因此,如果你想用单行数据进行预测,可以将这一行数据与X_test
的前25
行连接,并按如下方式进行预测:
single_row = X_test[:1]
single_row_plus = pd.concat([single_row, X_test[:25]])
print(np.round(new_model.predict(single_row_plus))[:1])
结果是这样的:
[14.]
你现在知道如何将机器学习模型纳入管道,以对新数据进行转换和预测。
总结
恭喜你完成了本书!这是一次非凡的旅程,从基础的机器学习和pandas
开始,到最终构建自己定制的转换器、管道和函数,以便在工业场景中使用稀疏矩阵部署强大、精细调优的 XGBoost 模型进行新数据预测。
在此过程中,你了解了 XGBoost 的故事,从最初的决策树到随机森林和梯度提升,再到发现使 XGBoost 如此特别的数学细节和复杂性。你一次又一次地看到 XGBoost 超越了其他机器学习算法,并且你获得了调优 XGBoost 广泛超参数的实践经验,包括n_estimators
、max_depth
、gamma
、colsample_bylevel
、missing
和scale_pos_weight
。
你了解了物理学家和天文学家如何通过历史上重要的案例研究获得关于我们宇宙的知识,并且通过不平衡数据集和替代基本学习器的应用,深入了解了 XGBoost 的广泛范围。你甚至通过先进的特征工程、非相关集成和堆叠等技巧,从 Kaggle 竞赛中学到了行业技巧。最后,你了解了工业中的高级自动化过程。
此时,你对 XGBoost 的知识已经达到高级水平。你现在可以高效、迅速、强大地使用 XGBoost 来解决你将面对的机器学习问题。当然,XGBoost 并不完美。如果你处理的是非结构化数据,比如图像或文本,神经网络可能会更适合你。对于大多数机器学习任务,尤其是表格数据的任务,XGBoost 通常会给你带来优势。
如果你有兴趣继续深入研究 XGBoost,我个人的建议是参加 Kaggle 竞赛。原因是 Kaggle 竞赛汇聚了经验丰富的机器学习从业者,与他们竞争会让你变得更强。此外,Kaggle 竞赛提供了一个结构化的机器学习环境,许多从业者都在同一个问题上进行工作,这导致了共享笔记本和论坛讨论,进一步促进了学习过程。这也是 XGBoost 在希格斯玻色子竞赛中首次建立起非凡声誉的地方,如本书所述。
你现在可以自信地使用 XGBoost 进入大数据领域,推动研究、参加竞赛,并构建适合生产的机器学习模型。