在上一篇文章中我们已经了解到GBDT 是一种强大的集成学习方法,具有高准确性、良好的泛化能力和处理非线性关系的优势。
但是其仍存在训练时间长、难以并行化和超参数调优复杂等缺点。而XGBT(Extreme Gradient Boosting) 是一种改进的 GBDT,旨在解决上述问题。
1. XGBT相对GBDT的优化
-
正则化:
XGBoost 在目标函数中引入了正则化项,以控制模型的复杂度,防止过拟合。 -
二阶导数信息:
XGBoost 利用损失函数的一阶和二阶导数信息来构建树,优化效率更高。 -
Shrinkage(缩减):
在每一轮迭代后,缩减新树的贡献,防止过拟合。 -
列抽样:
类似于随机森林的思想,XGBoost 在构建每棵树时,随机抽样一部分特征,从而增加了模型的多样性,减少了过拟合。 -
并行化:
XGBoost 通过并行化实现了更高效的训练过程。这里的并行化体现在两大部分:- 特征分裂点搜索的并行化
在每次选择最佳分裂点时,XGBoost会对所有特征进行遍历,计算每个特征的最佳分裂点增益。这一过程是可以并行化的,即在不同的线程或进程中同时计算不同特征的分裂增益。最终,选择所有计算结果中增益最大的分裂点。 - 数据块结构(Block Structure)
XGBoost引入了特定的数据块结构,称为“块化并行”(Block-wise Parallelism)。数据被分成多个块,每个块包含一部分样本和所有特征。块与块之间是独立的,可以并行处理。这种结构使得在分布式系统中,每个节点可以独立处理自己的数据块,然后在全局上合并结果。 - 并行化的梯度计算
在每次迭代中,XGBoost需要计算所有样本的梯度和Hessian。这一过程可以在多个处理单元上并行计算,然后将结果汇总。梯度和Hessian的计算是独立的,不同样本之间没有依赖关系,因此可以高效并行化。
- 特征分裂点搜索的并行化
-
分布式计算:
对于特别大的数据集,单机内存可能不足以容纳所有数据。XGBoost支持分布式训练,将数据分布在多个计算节点上,每个节点独立训练局部模型,然后在全局上合并结果。这种方法利用了集群计算的强大处理能力,显著提高了训练效率。
将这些概念代入数学,我们可以得到对应推理公式。
2. 推理公式
-
初始化模型:
对于回归问题,初始模型是一个常数预测,即所有样本的均值。\hat{y}_i^{(0)} = \frac{1}{N} \sum_{i=1}^{N} y_i -
定义目标函数:
XGBoost的目标是最小化一个带有正则化项的目标函数。L(\phi) = \sum_{i=1}^{N} l(y_i, \hat{y}_i^{(t)}) + \sum_{k=1}^{t} \Omega(f_k)- \sum_{i=1}^{N} l(y_i, \hat{y}_i) 是损失函数项,评估所有样本的预测误差。(在回归中为均方MSE,在分类中为交叉熵Log Loss)
- \sum_{k=1}^{t} \Omega(f_k) 是正则化项,评估模型复杂度。
\Omega(f) = \gamma T + \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2- \gamma T 是树的叶节点数量的惩罚项,T表示一棵树叶子的数量。
- \frac{1}{2} \lambda \sum_{j=1}^{T} w_j^2 是叶节点权重的L2正则化,控制叶节点权重的大小,\lambda是调节系数,w是向量的模。
-
梯度提升:
每一步中,添加一个新的决策树来拟合当前模型的残差。\hat{y}_i^{(t)} = \hat{y}_i^{(t-1)} + f_t(x_i)其中,f_t 是在第t步添加的决策树。
-
优化目标函数:
经过多次迭代后,目标函数愈发复杂,直接求解比较困难,且基于避免过拟合,我们可以使用二阶泰勒展开,将损失函数近似为:L^{(t)} \approx \sum_{i=1}^{N} \left[ l(y_i, \hat{y}_i^{(t-1)}) + g_i f_t(x_i) + \frac{1}{2} h_i f_t^2(x_i) \right] + \Omega(f_t)其中,g_i = \frac{\partial l(y_i, \hat{y}_i)}{\partial \hat{y}_i} 是一阶导数,h_i = \frac{\partial^2 l(y_i, \hat{y}_i)}{\partial \hat{y}_i^2} 是二阶导数。
-
树的结构得分:
对于每个候选分裂点,计算增益(Gain),选择增益最大的分裂。Gain = \frac{1}{2} \left[ \frac{G_L^2}{H_L + \lambda} + \frac{G_R^2}{H_R + \lambda} - \frac{(G_L + G_R)^2}{H_L + H_R + \lambda} \right] - \gamma其中,G_L 和 G_R 是左子节点和右子节点的梯度和,H_L 和 H_R 是左子节点和右子节点的Hessian和,\lambda 是正则化参数,\gamma 是分裂的惩罚项。
- 增益可以用于迭代时优化,选择增益最大的分裂点。
- 增益还可以用于剪枝,选择增益最大的分裂点
- 如果设定了阈值,增益小于阈值,则停止迭代。
- 计算分裂前后分数,如果分裂前分数更好或者相同,停止分裂。
3. 构建过程
我们仍然以上文提到的回归问题且相同为例,构建XGBT的构建过程。
3.1 目标函数和初始预测
初始数据如下:
X | y |
---|---|
1 | 2 |
2 | 3 |
3 | 6 |
4 | 8 |
我们使用均方误差(MSE)作为损失函数:
初始模型预测值为所有目标值的均值:
3.2 计算残差和梯度
一阶导数(梯度):
二阶导数(Hessian):
计算每个样本的梯度和 Hessian:
X | y | \hat{y}_i^{(0)} | g_i | h_i |
---|---|---|---|---|
1 | 2 | 4.75 | 2.75 | 1 |
2 | 3 | 4.75 | 1.75 | 1 |
3 | 6 | 4.75 | -1.25 | 1 |
4 | 8 | 4.75 | -3.25 | 1 |
3.3 选择最佳分裂点
计算在每个分裂点的增益,选择增益最大的分裂点。设 \lambda = 1 和 \gamma = 0(假设值)。
分裂点 X=1.5:
左节点:
右节点:
增益:
分裂点 X=2.5:
左节点:
右节点:
增益:
分裂点 X=3.5:
左节点:
右节点:
增益:
最佳分裂点是 X=2.5。
更新叶节点值
更新左子节点和右子节点的预测值:
左子节点:
右子节点:
3.4 更新模型
假设学习率 \eta = 1,则:
X | \hat{y}_i^{(0)} | f_1(x_i) | \hat{y}_i^{(1)} |
---|---|---|---|
1 | 4.75 | -1.5 | 4.75 - 1.5*1 = 3.75 |
2 | 4.75 | -1.5 | 4.75 - 1.5*1 = 3.75 |
3 | 4.75 | 1.5 | 4.75 + 1.5*1 = 6.25 |
4 | 4.75 | 1.5 | 4.75 + 1.5*1 = 6.5 |
3.5 反复迭代
这里和其它boosting一样,我们需要根据上一学习器的残差来计算新的残差和梯度。为了简化流程,这里仅举例一次迭代。
使用第一轮迭代更新后的预测值:
X | y | \hat{y}_i^{(1)} | 残差g_i | 二阶导数h_i |
---|---|---|---|---|
1 | 2 | 3.25 | 1.25 | 1 |
2 | 3 | 3.25 | 0.25 | 1 |
3 | 6 | 6.25 | 0.25 | 1 |
4 | 8 | 6.25 | -1.75 | 1 |
选择最佳分裂点
计算在每个分裂点的增益,选择增益最大的分裂点。设 \lambda = 1 和 \gamma = 0。
- 分裂点 X=1.5:
左节点:
右节点:
增益:
- 分裂点 X=2.5:
左节点:
右节点:
增益:
- 分裂点 X=3.5:
左节点:
右节点:
增益:
最佳分裂点是 X=3.5。
更新叶节点值
更新左子节点和右子节点的预测值:
左子节点:
右子节点:
更新模型
假设学习率 \eta = 1,则:
X | \hat{y}_i^{(1)} | f_2(x_i) | \hat{y}_i^{(2)} |
---|---|---|---|
1 | 3.25 | -0.4375 | 3.25 - 0.4375 = 2.8125 |
2 | 3.25 | -0.4375 | 3.25 - 0.4375 = 2.8125 |
3 | 6.25 | 0.875 | 6.25 + 0.875 = 7.125 |
4 | 6.25 | 0.875 | 6.25 + 0.875 = 7.125 |
3.6 模型验证
我们继续使用上一篇文章中的数据,进行模型验证。
X | y |
---|---|
1 | 2 |
2 | 3 |
3 | 6 |
4 | 8 |
详细预测步骤 |
-
x = 1.5:
- 初始预测值:4.75
- 第一轮调整值:-1.5
- 第一轮后预测值:4.75 - 1.5 = 3.25
- 第二轮调整值:-0.4375
- 最终预测值:3.25 - 0.4375 = 2.8125
-
x = 2.5:
- 初始预测值:4.75
- 第一轮调整值:-1.5
- 第一轮后预测值:4.75 - 1.5 = 3.25
- 第二轮调整值:-0.4375
- 最终预测值:3.25 - 0.4375 = 2.8125
-
x = 3.5:
- 初始预测值:4.75
- 第一轮调整值:1.5
- 第一轮后预测值:4.75 + 1.5 = 6.25
- 第二轮调整值:0.875
- 最终预测值:6.25 + 0.875 = 7.125
-
x = 4.5:
- 初始预测值:4.75
- 第一轮调整值:1.5
- 第一轮后预测值:4.75 + 1.5 = 6.25
- 第二轮调整值:0.875
- 最终预测值:6.25 + 0.875 = 7.125
汇总可得下表:
X | 初始预测值\hat{y}_i^{(0)} | 第一轮调整值f_1(x_i) | 第二轮调整值f_2(x_i) | 最终预测值\hat{y}_i^{(final)} |
---|---|---|---|---|
1.5 | 4.75 | -1.5 | -0.4375 | 4.75 - 1.5 - 0.4375 = 2.8125 |
2.5 | 4.75 | -1.5 | -0.4375 | 4.75 - 1.5 - 0.4375 = 2.8125 |
3.5 | 4.75 | 1.5 | 0.875 | 4.75 + 1.5 + 0.875 = 7.125 |
4.5 | 4.75 | 1.5 | 0.875 | 4.75 + 1.5 + 0.875 = 7.125 |
可以看到,随着迭代次数的增加,模型预测值逐渐接近真实值。当然,由于我们的迭代次数极少且模型参数设置极简单,所以模型效果并不是很好。
4. 代码实现
4.1 常用python函数
需要注意的是,xgboost不再是sklearn的模型,而是单独的一个包。
import xgboost
import sklearn.model_selection
""" xgboost 类 """
clf = xgboost.XGBClassifier(
# 树的数量,即迭代次数。
# 默认值100。调整范围(50-500)。常用值100-300。
# 增加提高模型拟合能力,但也会增加训练时间和过拟合风险。
# 一般与 learning_rate 结合调整。
n_estimators=100,
# 学习率,控制每棵树对最终模型的贡献。
# 默认值0.3。调整范围(0.01-0.3)。常用值0.1。
# 较小的学习率可以让模型更加稳健,但需要更多的树。
learning_rate=0.1,
# 树的最大深度。
# 默认值6。调整范围(3-10)。常用值3-8。
# 较深的树可以拟合更加复杂的关系,但也容易过拟合。
max_depth=6,
# L2正则化项的权重。
# 默认值1。调整范围(0-10)。常用值0-3。
# 增加此参数可以减少过拟合。
reg_lambda=1,
# L1正则化项的权重。
# 默认值0。调整范围(0-10)。常用值0-1。
# 增加此参数可以减少过拟合。
reg_alpha=0,
# 树的子样本比例。
# 默认值1。调整范围(0.5-1)。常用值0.7-1。
# 设置为小于1时,算法会随机抽样部分数据来训练树,增加模型的随机性。
subsample=1,
# 特征的子样本比例。
# 默认值1。调整范围(0.5-1)。常用值0.7-1。
# 设置为小于1时,算法会随机抽样部分特征来训练树,增加模型的随机性。
colsample_bytree=1,
# 最小的叶子节点样本权重和。
# 默认值1。调整范围(1-10)。常用值1-5。
# 增加此参数可以减少过拟合。
min_child_weight=1,
# 每次树分裂的最小损失减少。
# 默认值0。调整范围(0-1)。常用值0-0.1。
# 增加此参数可以减少过拟合。
gamma=0,
# 树的生长策略。
# 可选值为 'auto', 'exact', 'approx', 'hist', 'gpu_hist'。默认值为 'auto'。
#'exact' 贪心算法,找到最优分裂点(小数据集,高精度要求),
#'approx' 近似搜索,提高速度,保持较高精度(中数据集)
#'hist' 直方图搜索,大幅提高速度,降低内存使用(大规模数据)
#'gpu_hist' GPU直方图搜索。(gpu加速)
# 商业应用中一般选择 'hist' 或 'gpu_hist' 以提高训练速度。
tree_method='auto',
# 随机种子。
# 默认值0。调整范围(0-1000)。常用值0-42。
# 设置随机种子以便结果可重现。
random_state=0
)
""" 将数据转换为 DMatrix
DMatrix 是 XGBoost 的内部数据结构,专门用于高效存储和处理数据。它在内存利用率和计算效率上进行了优化,
尤其适合于大规模数据集。创建 DMatrix 的过程包括将数据和标签传递给 DMatrix 类,然后可以使用这个结构进行训练和预测。
"""
X_train = [[1, 2], [3, 4], [5, 6], [7, 8]]
y_train = [0, 1, 0, 1]
X_test = [[2, 3], [4, 5], [6, 7]]
y_test = [0, 1, 0]
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
""" xgboost 训练器,提供细粒度控制,并支持 DMatrix"""
params = {
# 树的最大深度。
# 默认值6。调整范围(3-10)。常用值3-8。
# 较大值增加模型的复杂度和训练时间,但可能提高准确性。
'max_depth': 6,
# 学习率,控制每棵树对最终模型的贡献。
# 默认值0.3。调整范围(0.01-0.3)。常用值0.1。
# 较小的学习率需要更多的树,较大的学习率可能导致过拟合。
'eta': 0.1,
# 树的生长策略。
# 默认值 'auto'。可选值包括 'exact', 'approx', 'hist', 'gpu_hist'。
# 商业应用中常用 'hist' 或 'gpu_hist' 以提高训练速度。
'tree_method': 'hist',
# 目标函数。
# 根据任务选择,回归任务一般为 'reg:squarederror',分类任务为 'binary:logistic'。
# 其他目标函数根据具体需求调整。
'objective': 'binary:logistic',
# 评估指标。
# 默认值无。可以设置为 'auc', 'logloss' 等。
# 商业应用中常用 'auc' 评估分类任务的性能。
'eval_metric': 'auc',
# L2正则化项权重。
# 默认值1。调整范围(0-10)。常用值0-3。
# 增加此参数可以减少过拟合。
'lambda': 1,
# L1正则化项权重。
# 默认值0。调整范围(0-10)。常用值0-1。
# 增加此参数可以减少过拟合。
'alpha': 0,
# 样本子集采样率。
# 默认值1。调整范围(0.5-1)。常用值0.7-1。
# 设置为小于1时,可以防止过拟合。
'subsample': 1,
# 特征子集采样率。
# 默认值1。调整范围(0.5-1)。常用值0.7-1。
# 设置为小于1时,可以防止过拟合。
'colsample_bytree': 1,
}
bst = xgb.train(
params,
dtrain,
num_boost_round=100, # 树的数量,即迭代次数。默认值10。调整范围(50-500)。常用值100-300。
evals=[(dtrain, 'train'), (dtest, 'eval')], # 评估数据,用于监控模型性能。
early_stopping_rounds=10, # 提前停止的轮数。如果在指定轮数内评估指标没有提升,则停止训练。
evals_result={}, # 用于存储评估结果的字典。
verbose_eval=True # 是否输出训练过程中的评估结果。
)
""" 展示单颗树结构 """
xgboost.plot_tree(clf, num_trees=0)
""" xgboost长伴随网格搜索适用,用于超参调优 """
sklearn.model_selection.GridSearchCV.param_grid = {
'n_estimators': [100, 200, 300],
'learning_rate': [0.01, 0.1, 0.2],
'max_depth': [3, 5, 7],
'subsample': [0.7, 0.8, 0.9],
'colsample_bytree': [0.7, 0.8, 0.9]
}
grid_search = sklearn.model_selection.GridSearchCV(clf, param_grid, scoring='roc_auc', cv=3)
4.2 代码应用
我们将使用Kaggle的一个客户流失数据集,该数据集包含客户的基本信息以及他们是否流失的标记。
数据集
我们将使用Kaggle的Telco Customer Churn数据集,下载链接:Telco Customer Churn
1. 数据预处理
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
import time
# 读取数据
data = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
# 数据预处理
data = data.drop(columns=['customerID'])
data['TotalCharges'] = pd.to_numeric(data['TotalCharges'], errors='coerce')
data = data.dropna()
# 编码分类变量
label_encoders = {}
for column in data.select_dtypes(include=['object']).columns:
le = LabelEncoder()
data[column] = le.fit_transform(data[column])
label_encoders[column] = le
# 特征和标签
X = data.drop(columns=['Churn'])
y = data['Churn']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
2. 使用XGBoost进行训练和预测
import xgboost as xgb
from sklearn.metrics import classification_report, confusion_matrix
# 创建DMatrix
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
# 设置参数
params = {
'max_depth': 3,
'eta': 0.1,
'objective': 'binary:logistic',
'eval_metric': 'auc',
'tree_method': 'gpu_hist' # 使用并行处理的树构建方法
}
# 训练模型
start_time = time.time()
bst = xgb.train(params, dtrain, num_boost_round=100, evals=[(dtest, 'test')], early_stopping_rounds=10)
xgb_train_time = time.time() - start_time
# 预测
start_time = time.time()
y_pred_prob = bst.predict(dtest)
xgb_predict_time = time.time() - start_time
y_pred = np.round(y_pred_prob).astype(int)
# 评估模型
print("XGBoost Classification Report:\n", classification_report(y_test, y_pred))
print("XGBoost Confusion Matrix:\n", confusion_matrix(y_test, y_pred))
print("XGBoost Training Time:", xgb_train_time)
print("XGBoost Prediction Time:", xgb_predict_time)
3. 使用GBDT进行训练和预测
from sklearn.ensemble import GradientBoostingClassifier
# 创建GBDT模型
gbdt = GradientBoostingClassifier(
n_estimators=100,
learning_rate=0.1,
max_depth=3,
random_state=42
)
# 训练模型
start_time = time.time()
gbdt.fit(X_train, y_train)
gbdt_train_time = time.time() - start_time
# 预测
start_time = time.time()
y_pred_gbdt = gbdt.predict(X_test)
gbdt_predict_time = time.time() - start_time
# 评估模型
print("GBDT Classification Report:\n", classification_report(y_test, y_pred_gbdt))
print("GBDT Confusion Matrix:\n", confusion_matrix(y_test, y_pred_gbdt))
print("GBDT Training Time:", gbdt_train_time)
print("GBDT Prediction Time:", gbdt_predict_time)
评估和对比结果
我们将在控制台输出中包括训练时间和预测时间的指标。
综合示例
完整的代码如下:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.ensemble import GradientBoostingClassifier
import xgboost as xgb
import time
# 读取数据
data = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
# 数据预处理
data = data.drop(columns=['customerID'])
data['TotalCharges'] = pd.to_numeric(data['TotalCharges'], errors='coerce')
data = data.dropna()
# 编码分类变量
label_encoders = {}
for column in data.select_dtypes(include=['object']).columns:
le = LabelEncoder()
data[column] = le.fit_transform(data[column])
label_encoders[column] = le
# 特征和标签
X = data.drop(columns=['Churn'])
y = data['Churn']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 创建DMatrix
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)
# 设置XGBoost参数
params = {
'max_depth': 3,
'eta': 0.1,
'objective': 'binary:logistic',
'eval_metric': 'auc',
'tree_method': 'hist' # 使用并行处理的树构建方法
}
# 训练XGBoost模型
start_time = time.time()
bst = xgb.train(params, dtrain, num_boost_round=100, evals=[(dtest, 'test')], early_stopping_rounds=10)
xgb_train_time = time.time() - start_time
# XGBoost预测
start_time = time.time()
y_pred_prob = bst.predict(dtest)
xgb_predict_time = time.time() - start_time
y_pred = np.round(y_pred_prob).astype(int)
# 评估XGBoost模型
print("XGBoost Classification Report:\n", classification_report(y_test, y_pred))
print("XGBoost Confusion Matrix:\n", confusion_matrix(y_test, y_pred))
print("XGBoost Training Time:", xgb_train_time)
print("XGBoost Prediction Time:", xgb_predict_time)
# 创建GBDT模型
gbdt = GradientBoostingClassifier(
n_estimators=100,
learning_rate=0.1,
max_depth=3,
random_state=42
)
# 训练GBDT模型
start_time = time.time()
gbdt.fit(X_train, y_train)
gbdt_train_time = time.time() - start_time
# GBDT预测
start_time = time.time()
y_pred_gbdt = gbdt.predict(X_test)
gbdt_predict_time = time.time() - start_time
# 评估GBDT模型
print("GBDT Classification Report:\n", classification_report(y_test, y_pred_gbdt))
print("GBDT Confusion Matrix:\n", confusion_matrix(y_test, y_pred_gbdt))
print("GBDT Training Time:", gbdt_train_time)
print("GBDT Prediction Time:", gbdt_predict_time)
输出示例
xgboost train time=0.4810643196105957,predict time=0.0029926300048828125
GDBT train time=1.752678394317627,predict time=0.004985332489013672
this follow is report of xgboost:
precision recall f1-score support
0 0.83 0.89 0.86 1023
1 0.64 0.53 0.58 386
accuracy 0.79 1409
macro avg 0.74 0.71 0.72 1409
weighted avg 0.78 0.79 0.78 1409
this follow is report of GDBT:
precision recall f1-score support
0 0.83 0.89 0.86 1023
1 0.65 0.52 0.57 386
accuracy 0.79 1409
macro avg 0.74 0.70 0.72 1409
weighted avg 0.78 0.79 0.78 1409
xgboost matrix = [
[910 113]
[183 203]
]
GDBT matrix = [
[914 109]
[187 199]
]
对比可见:
- 速度: XGBoost显著快于GBDT,而且XGBoost还可以拓展使用gpu更进一步加速。
- 性能: 两个模型在准确率上几乎相同,在处理不平衡数据时,XGBoost略有优势。
- 使用场景: 如果计算资源有限且对训练速度有要求,XGBoost是更好的选择;如果需要略微更好的正类预测,XGBoost也是更优选择。