分类数据

备注

截至 XGBoost 1.6,该功能是实验性的,功能有限。仅完全支持 Python 包。

从版本1.5开始,XGBoost Python包提供了对分类数据进行实验性支持,供公众测试。对于数值数据,分割条件定义为 \(value < threshold\),而对于分类数据,分割的定义取决于是否使用分区或独热编码。对于基于分区的分割,分割条件指定为 \(value \in categories\),其中 categories 是某个特征中的类别集合。如果使用独热编码,则分割定义为 \(value == category\)。未来版本计划支持更高级的分类分割策略,本教程详细介绍了如何告知XGBoost数据类型。

使用 scikit-learn 接口进行训练

将分类数据传递到 XGBoost 的最简单方法是使用 dataframe 和 scikit-learn 接口,如 XGBClassifier。为了准备数据,用户需要将输入预测器的数据类型指定为 category。对于 pandas/cudf Dataframe,这可以通过

X["cat_feature"].astype("category")

对于所有表示分类特征的列。之后,用户可以告诉 XGBoost 启用分类数据的训练。假设您正在使用 XGBClassifier 进行分类问题,请指定参数 enable_categorical:

# Supported tree methods are `approx` and `hist`.
clf = xgb.XGBClassifier(tree_method="hist", enable_categorical=True, device="cuda")
# X is the dataframe we created in previous snippet
clf.fit(X, y)
# Must use JSON/UBJSON for serialization, otherwise the information is lost.
clf.save_model("categorical-model.json")

一旦训练完成,大多数其他功能都可以利用该模型。例如,可以绘制模型并计算全局特征重要性:

# Get a graph
graph = xgb.to_graphviz(clf, num_trees=1)
# Or get a matplotlib axis
ax = xgb.plot_tree(clf, num_trees=1)
# Get feature importances
clf.feature_importances_

来自 dask 的 scikit-learn 接口与单节点版本类似。基本思路是创建带有类别特征类型的数据框,并通过设置 enable_categorical 参数告诉 XGBoost 使用它。有关使用带有独热编码的 scikit-learn 接口处理类别数据的示例,请参见 开始使用分类数据。关于使用独热编码数据与 XGBoost 的类别数据支持之间的比较,请参见 使用 cat_in_the_dat 数据集训练 XGBoost

最佳划分

Added in version 1.6.

最优分区是一种用于对每个节点分割的分类预测因子进行分区的技术,数值输出的最优性证明首先由 [1] 引入。该算法用于决策树 [2],后来 LightGBM [3] 将其引入到梯度提升树的上下文中,现在也被 XGBoost 采用作为处理分类分割的可选功能。更具体地说,Fisher 的证明 [1] 指出,当试图根据这些值之间的距离将一组离散值分组时,只需要查看排序后的分区,而不是枚举所有可能的排列。在决策树的上下文中,离散值是类别,度量是输出叶值。直观地说,我们希望将输出相似叶值的类别分组。在分割查找过程中,我们首先对梯度直方图进行排序以准备连续分区,然后根据这些排序值枚举分割。XGBoost 的相关参数之一是 max_cat_to_onehot,它控制每个特征是使用独热编码还是分区,详情请参阅 分类特征的参数

使用原生接口

scikit-learn 接口对用户友好,但缺少一些仅在原生接口中可用的功能。例如,用户无法直接计算 SHAP 值。此外,原生接口支持更多数据类型。要使用原生接口处理分类数据,我们需要将相似的参数传递给 DMatrixQuantileDMatrix 以及 train 函数。对于数据框输入:

# X is a dataframe we created in previous snippet
Xy = xgb.DMatrix(X, y, enable_categorical=True)
booster = xgb.train({"tree_method": "hist", "max_cat_to_onehot": 5}, Xy)
# Must use JSON for serialization, otherwise the information is lost
booster.save_model("categorical-model.json")

SHAP 值计算:

SHAP = booster.predict(Xy, pred_interactions=True)

# categorical features are listed as "c"
print(booster.feature_types)

对于其他类型的输入,例如 numpy 数组 ,我们可以通过在 DMatrix 中使用 feature_types 参数来告诉 XGBoost 特征类型:

# "q" is numerical feature, while "c" is categorical feature
ft = ["q", "c", "c"]
X: np.ndarray = load_my_data()
assert X.shape[1] == 3
Xy = xgb.DMatrix(X, y, feature_types=ft, enable_categorical=True)

对于数值数据,特征类型可以是 "q""float",而对于分类特征,则指定为 "c"。XGBoost 中的 Dask 模块具有相同的接口,因此 dask.Array 也可以用于分类数据。最后,sklearn 接口 XGBRegressor 具有相同的参数。

数据一致性

XGBoost 接受参数来指示哪些特征被视为分类特征,可以通过数据框的 dtypes 或通过 feature_types 参数来实现。然而,XGBoost 本身并不存储类别最初是如何编码的信息。例如,给定一个将音乐流派映射到整数码的编码方案:

{"acoustic": 0, "indie": 1, "blues": 2, "country": 3}

XGBoost 不知道这个从输入到输出的映射,因此无法将其存储在模型中。映射通常在用户的数据工程管道中通过像 sklearn.preprocessing.OrdinalEncoder 这样的列转换器来实现。为了确保从 XGBoost 获得正确的结果,用户需要确保数据转换管道在训练和测试数据之间保持一致。应该注意避免以下错误:

X_train["genre"] = X_train["genre"].astype("category")
reg = xgb.XGBRegressor(enable_categorical=True).fit(X_train, y_train)

# invalid encoding
X_test["genre"] = X_test["genre"].astype("category")
reg.predict(X_test)

在上面的代码片段中,训练数据和测试数据分别进行编码,导致两种不同的编码模式和无效的预测结果。有关使用序数编码器的示例,请参见 分类数据的特征工程管道

杂项

默认情况下,XGBoost 假设输入的类别是从 0 开始到类别总数减一的整数 \([0, n\_categories)\)。然而,用户可能会由于训练数据集中的错误或缺失值而提供无效的输入。这些无效值可能是负值、无法由 32 位浮点数准确表示的整数值,或者是大于实际唯一类别数的值。在训练过程中会对此进行验证,但在预测时出于性能考虑,这些值会被视为未选择的类别。

参考文献

[1] Walter D. Fisher. “On Grouping for Maximum Homogeneity”. Journal of the American Statistical Association. Vol. 53, No. 284 (Dec., 1958), pp. 789-798.

[2] Trevor Hastie, Robert Tibshirani, Jerome Friedman. 《统计学习基础》. Springer Series in Statistics Springer New York Inc. (2001).

[3] 柯国霖, 孟琦, 托马斯·芬利, 王泰峰, 陈伟, 马伟东, 叶启伟, 刘铁岩. “LightGBM: 一种高效的梯度提升决策树.” 神经信息处理系统进展 30 (NIPS 2017), 页码 3149-3157.