分类数据
自 1.5 版本以来,XGBoost 支持分类数据。对于数值数据,分割条件定义为 \(value < threshold\),而对于分类数据,分割根据是使用分区还是独热编码来定义。对于基于分区的分割,分割指定为 \(value \in categories\),其中 categories
是一个特征中的类别集。如果改用独热编码,则分割定义为 \(value == category\)。未来版本计划推出更高级的分类分割策略,本教程详细介绍了如何将数据类型告知 XGBoost。
使用 scikit-learn 接口进行训练
将分类数据传入 XGBoost 最简单的方法是使用数据帧和 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 中找到。
版本 3.0 新增: 支持使用 factor
的 R 包。
最优分区
版本 1.6 中新增。
最优分区是一种对每个节点分割的分类预测器进行分区的技术,数值输出的最优性证明最初由 [1] 提出。该算法用于决策树 [2],后来 LightGBM [3] 将其引入梯度提升树的上下文,现在 XGBoost 也将其作为处理分类分割的可选功能。更具体地说,Fisher [1] 的证明指出,当试图根据这些值的度量之间的距离将一组离散值划分为组时,只需查看排序的分区,而不是枚举所有可能的排列。在决策树的上下文中,离散值是类别,度量是输出叶值。直观地,我们希望将输出相似叶值的类别分组。在寻找分割时,我们首先对梯度直方图进行排序以准备连续分区,然后根据这些排序值枚举分割。XGBoost 的相关参数之一是 max_cat_to_onehot
,它控制是否应对每个特征使用独热编码或分区,详情请参阅 分类特征的参数。
使用原生接口
scikit-learn
接口用户友好,但缺少一些仅在原生接口中可用的功能。例如,用户无法直接计算 SHAP 值。此外,原生接口支持更多数据类型。要将原生接口与分类数据一起使用,我们需要将类似的参数传递给 DMatrix
或 QuantileDMatrix
和 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 array
,我们可以通过在 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
具有相同的参数。
自动重编码(数据一致性)
版本 3.1 中更改:从 XGBoost 3.1 开始,Python 接口可以对新输入执行自动重编码。
XGBoost 接受参数来指示哪些特征被认为是分类的,无论是通过数据帧的 dtypes
还是通过 feature_types
参数。但是,除了 Python 接口之外,XGBoost 不会存储关于类别最初如何编码的信息。例如,给定一个将音乐流派映射到整数代码的编码方案:
{"acoustic": 0, "indie": 1, "blues": 2, "country": 3}
除了 Python 接口(R/Java/C 等),XGBoost 不知道来自输入的这个映射,因此不能将其存储在模型中。映射通常发生在用户的数据工程管道中。为了确保 XGBoost 的正确结果,用户需要保持训练和测试数据之间数据转换管道的一致性。
从 3.1 版开始,如果输入是数据帧(pandas、cuDF、polars、pyarrow、modin),Python 接口可以记住编码并在推断和训练继续期间执行重编码。该功能支持侧重于基本用法。它对可接受的输入类型有一些限制。首先,类别名称必须具有以下类型之一:
字符串
整数,从 8 位到 64 位,支持有符号和无符号。
32 位或 64 位浮点数
其他类别类型不受支持。其次,输入类型必须严格一致。例如,如果训练集中的分类列是无符号整数,而测试数据集有有符号整数列,XGBoost 将引发错误。如果您有不属于支持类型之一的类别,则需要使用预处理数据转换器(例如 sklearn.preprocessing.OrdinalEncoder
)执行重编码。请参阅 分类数据的特征工程管道,了解使用序数编码器的示例。为了澄清,这里的类型是指类别名称的类型(在 pandas 中称为 Index
)
# string type
{"acoustic": 0, "indie": 1, "blues": 2, "country": 3}
# integer type
{-1: 0, 1: 1, 3: 2, 7: 3}
# depending on the dataframe implementation, it can be signed or unsigned.
{5: 0, 1: 1, 3: 2, 7: 3}
# floating point type, both 32-bit and 64-bit are supported.
{-1.0: 0, 1.0: 1, 3.0: 2, 7.0: 3}
在内部,XGBoost 尝试从数据帧输入中提取类别。对于推断(预测),重编码是动态发生的,并且没有数据复制(除了数据帧本身执行的一些内部转换)。然而,对于训练继续,如果您使用原生接口,重编码需要一些额外的步骤。sklearn 接口和 Dask 接口可以自动处理训练继续。最后,请注意,将重编码器与原生接口一起使用仍处于实验阶段。它已准备好进行测试,但我们希望观察该功能的使用情况一段时间,并在必要时可能会进行一些突破性更改。以下是使用原生接口的片段:
import pandas as pd
X = pd.DataFrame()
Xy = xgboost.QuantileDMatrix(X, y, enable_categorical=True)
booster = xgboost.train({}, Xy)
# XGBoost can handle re-coding for inference without user intervention
X_new = pd.DataFrame()
booster.inplace_predict(X_new)
# Get categories saved in the model for training continuation
categories = booster.get_categories()
# Use saved categories as a reference for re-coding.
# Training continuation requires a re-coded DMatrix, pass the categories as feature_types
Xy_new = xgboost.QuantileDMatrix(
X_new, y_new, feature_types=categories, enable_categorical=True, ref=Xy
)
booster_1 = xgboost.train({}, Xy_new, xgb_model=booster)
只要输入是数据帧,使用 scikit-learn 接口就不需要额外的步骤。在训练继续期间,XGBoost 将从以前的模型中提取类别,或者如果输入模型没有这些信息,则使用新训练数据集中的类别。
对于 R,截至 3.1 版本,尚不支持自动重编码。举个例子:
> f0 = factor(c("a", "b", "c"))
> as.numeric(f0)
[1] 1 2 3
> f0
[1] a b c
Levels: a b c
在上面的片段中,我们有映射:a -> 1, b -> 2, c -> 3
。假设上述是训练数据,下一个片段是测试数据:
> f1 = factor(c("a", "c"))
> as.numeric(f1)
[1] 1 2
> f1
[1] a c
Levels: a c
现在,我们有 a -> 1, c -> 2
,因为 b
缺失,并且 R 因子以不同的方式编码数据,导致测试时编码无效。XGBoost 无法记住 R 包的原始编码。您必须在推断期间显式编码数据:
> f1 = factor(c("a", "c"), levels = c("a", "b", "c"))
> f1
[1] a c
Levels: a b c
> as.numeric(f1)
[1] 1 3
杂项
默认情况下,XGBoost 假设输入类别代码是整数,从 0 到类别数 \([0, n\_categories)\)。但是,用户可能会由于训练数据集中的错误或缺失值而提供无效值。这可能是负值,无法由 32 位浮点数精确表示的整数值,或者大于实际唯一类别数的值。在训练期间会验证这一点,但出于性能原因,对于预测,它被视为与未选择类别相同。
参考文献
[1] Walter D. Fisher。“关于分组以获得最大同质性”。美国统计协会杂志。第 53 卷,第 284 期(1958 年 12 月),第 789-798 页。
[2] Trevor Hastie, Robert Tibshirani, Jerome Friedman。“统计学习基础”。统计学 Springer New York Inc. 系列 Springer(2001 年)。
[3] Guolin Ke, Qi Meng, Thomas Finley, Taifeng Wang, Wei Chen, Weidong Ma, Qiwei Ye, Tie-Yan Liu。“LightGBM:一种高效的梯度提升决策树。”神经信息处理系统进展 30 (NIPS 2017),第 3149-3157 页。