使用XGBoost理解您的数据集

介绍

本小册子的目的是向您展示如何使用 XGBoost 更好地发现和理解您自己的数据集。

这个小插图不是关于预测任何东西的(参见 XGBoost 演示)。我们将解释如何使用 XGBoost 来突出显示数据的 特征结果 之间的 联系

包加载:

require(xgboost)
require(Matrix)
require(data.table)
if (!require('vcd')) {
  install.packages('vcd')
}

VCD 包仅用于其嵌入的数据集之一。

数据集的准备

数值变量 vs. 分类变量

XGBoost 仅管理 数值 向量。

当你有分类数据时该怎么办?

一个 分类 变量有固定数量的不同值。例如,如果一个名为 Colour 的变量只能取这三个值之一,redbluegreen,那么 Colour 就是一个 分类 变量。

R 中,一个 分类 变量被称为 因子

在控制台中输入 ?factor 以获取更多信息。

为了回答上述问题,我们将把 分类 变量转换为 数值 变量。

从分类变量到数值变量的转换

查看原始数据

+在本Vignette中,我们将看到如何将一个密集data.frame密集 = 矩阵中的大多数元素是非零的)与分类变量转换为一个非常稀疏numeric特征矩阵(稀疏 = 矩阵中有许多零元素)。

我们将要看到的方法通常被称为 one-hot 编码

第一步是将 Arthritis 数据集加载到内存中,并使用 data.table 包对其进行包装。

data(Arthritis)
df <- data.table(Arthritis, keep.rownames = FALSE)

data.table 完全兼容 Rdata.frame,但其语法更加一致,并且在大数据集上的性能是 同类最佳(包括 RdplyrPythonPandas 在内)。XGBoostR 包中的一些部分使用了 data.table

我们首先要做的是查看 data.table 的前几行:

head(df)

##    ID Treatment  Sex Age Improved
## 1: 57   Treated Male  27     Some
## 2: 46   Treated Male  29     None
## 3: 77   Treated Male  30     None
## 4: 17   Treated Male  32   Marked
## 5: 36   Treated Male  46   Marked
## 6: 23   Treated Male  58   Marked

现在我们将检查每一列的格式。

str(df)

## Classes 'data.table' and 'data.frame':   84 obs. of  5 variables:
##  $ ID       : int  57 46 77 17 36 23 75 39 33 55 ...
##  $ Treatment: Factor w/ 2 levels "Placebo","Treated": 2 2 2 2 2 2 2 2 2 2 ...
##  $ Sex      : Factor w/ 2 levels "Female","Male": 2 2 2 2 2 2 2 2 2 2 ...
##  $ Age      : int  27 29 30 32 46 58 59 59 63 63 ...
##  $ Improved : Ord.factor w/ 3 levels "None"<"Some"<..: 2 1 1 3 3 3 1 3 1 1 ...
##  - attr(*, ".internal.selfref")=<externalptr>

2 列具有 factor 类型,一列具有 ordinal 类型。

ordinal 变量 :

  • 可以取有限数量的值(如 factor);

  • 这些值是有序的(与 factor 不同)。这里的有序值是:Marked > Some > None

基于旧功能创建新功能

我们将添加一些新的 分类 特征,看看是否有帮助。

按10年分组

对于第一个特性,我们通过四舍五入实际年龄来创建年龄组。

注意,我们将其转换为 factor,以便算法将这些年龄组视为独立值。

因此,20 并不比 60 更接近 30。换句话说,在这种转换中,年龄之间的距离被忽略了。

head(df[, AgeDiscret := as.factor(round(Age / 10, 0))])

##    ID Treatment  Sex Age Improved AgeDiscret
## 1: 57   Treated Male  27     Some          3
## 2: 46   Treated Male  29     None          3
## 3: 77   Treated Male  30     None          3
## 4: 17   Treated Male  32   Marked          3
## 5: 36   Treated Male  46   Marked          5
## 6: 23   Treated Male  58   Marked          6
随机分成两组

以下是对真实年龄的进一步简化,以30岁为任意分割点。我选择这个值没有任何依据。我们稍后会看到,基于任意值简化信息是否是一个好的策略(你可能已经对它的效果有了一些想法……)。

head(df[, AgeCat := as.factor(ifelse(Age > 30, "Old", "Young"))])

##    ID Treatment  Sex Age Improved AgeDiscret AgeCat
## 1: 57   Treated Male  27     Some          3  Young
## 2: 46   Treated Male  29     None          3  Young
## 3: 77   Treated Male  30     None          3  Young
## 4: 17   Treated Male  32   Marked          3    Old
## 5: 36   Treated Male  46   Marked          5    Old
## 6: 23   Treated Male  58   Marked          6    Old
添加相关特征的风险

这些新特性与 Age 特性高度相关,因为它们是该特性的简单变换。

对于许多机器学习算法来说,使用相关特征并不是一个好主意。它有时可能会降低预测的准确性,而且在大多数情况下会使模型的解释变得几乎不可能。例如,GLM 假设特征是不相关的。

幸运的是,决策树算法(包括提升树)对这些特征非常鲁棒。因此我们不需要做任何事情来管理这种情况。

清理数据

我们移除ID,因为从这个特征中没有什么可以学习的(它只会增加一些噪音)。

df[, ID := NULL]

我们将列出列 Treatment 的不同值:

levels(df[, Treatment])

## [1] "Placebo" "Treated"

编码分类特征

下一步,我们将把分类数据转换为虚拟变量。存在几种编码方法,例如,独热编码 是一种常见的方法。我们将使用 虚拟对比编码,这种方法很受欢迎,因为它产生“满秩”编码(另见 Max Kuhn 的这篇博客文章)。

目的是将每个 分类 特征的每个值转换为 二进制 特征 {0, 1}

例如,列 Treatment 将被替换为两列,TreatmentPlaceboTreatmentTreated。它们都将是 二进制的。因此,在转换之前在列 Treatment 中具有值 Placebo 的观察值,在转换后将在新列 TreatmentPlacebo 中具有值 1,在列 TreatmentTreated 中具有值 0。列 TreatmentPlacebo 将在对比编码过程中消失,因为它将被吸收到一个共同的常数截距列中。

Improved 被排除,因为它将是我们的 标签 列,即我们想要预测的那一列。

sparse_matrix <- sparse.model.matrix(Improved ~ ., data = df)[, -1]
head(sparse_matrix)

## 6 x 9 sparse Matrix of class "dgCMatrix"
##   TreatmentTreated SexMale Age AgeDiscret3 AgeDiscret4 AgeDiscret5 AgeDiscret6
## 1                1       1  27           1           .           .           .
## 2                1       1  29           1           .           .           .
## 3                1       1  30           1           .           .           .
## 4                1       1  32           1           .           .           .
## 5                1       1  46           .           .           1           .
## 6                1       1  58           .           .           .           1
##   AgeDiscret7 AgeCatYoung
## 1           .           1
## 2           .           1
## 3           .           1
## 4           .           .
## 5           .           .
## 6           .           .

上述公式 Improved ~ . 表示将所有 分类 特征转换为二进制值,但 Improved 列除外。-1 列选择移除了全为 1 的截距列(该列由转换生成)。更多信息,您可以在控制台中输入 ?sparse.model.matrix

创建输出 numeric 向量(不是作为稀疏 Matrix):

output_vector <- df[, Improved] == "Marked"
  1. Y 向量设置为 0

  2. 对于 Improved == MarkedTRUE 的行,将 Y 设置为 1

  3. 返回 Y 向量。

构建模型

下面的代码非常常见。更多信息,你可以查看 xgboost 函数的文档(或在 vignette XGBoost 介绍 中查看)。

bst <- xgboost(data = sparse_matrix, label = output_vector, max_depth = 4,
               eta = 1, nthread = 2, nrounds = 10, objective = "binary:logistic")

## [1]  train-logloss:0.485466 
## [2]  train-logloss:0.438534 
## [3]  train-logloss:0.412250 
## [4]  train-logloss:0.395828 
## [5]  train-logloss:0.384264 
## [6]  train-logloss:0.374028 
## [7]  train-logloss:0.365005 
## [8]  train-logloss:0.351233 
## [9]  train-logloss:0.341678 
## [10] train-logloss:0.334465

你可以看到一些 train-logloss: 0.XXXXX 行,后面跟着一个数字。它会减少。每一行显示了模型对数据的解释程度。越低越好。

训练误差的小值可能是 过拟合 的症状,这意味着模型将无法准确预测未见过的值。

特征重要性

衡量特征重要性

构建特征重要性数据表。

记住,每个二进制列对应于一个 分类 特征的单个值。

importance <- xgb.importance(feature_names = colnames(sparse_matrix), model = bst)
head(importance)

##             Feature        Gain      Cover  Frequency
## 1:              Age 0.622031769 0.67251696 0.67241379
## 2: TreatmentTreated 0.285750540 0.11916651 0.10344828
## 3:          SexMale 0.048744022 0.04522028 0.08620690
## 4:      AgeDiscret6 0.016604639 0.04784639 0.05172414
## 5:      AgeDiscret3 0.016373781 0.08028951 0.05172414
## 6:      AgeDiscret4 0.009270557 0.02858801 0.01724138

Gain 提供了我们正在寻找的信息。

如你所见,功能是按 增益 分类的。

Gain 是特征为其所在分支带来的准确性提升。其思想是,在分支上添加特征 X 的新分割之前,存在一些错误分类的元素;在添加该特征的分割后,会产生两个新分支,每个分支的准确性都更高(一个分支表示如果观察结果在该分支上,则应分类为 1,另一个分支则表示完全相反的情况)。

Cover 与损失函数相对于某一特定变量的二阶导数(或 Hessian 矩阵)相关;因此,较大的值表示该变量对损失函数有较大的潜在影响,因此是重要的。

Frequency 是衡量 Gain 的一种更简单的方式。它只是计算一个特征在所有生成的树中被使用的次数。你不应该使用它(除非你知道为什么要使用它)。

绘制特征重要性

所有这些都很不错,但如果能绘制结果就更好了。

xgb.plot.importance(importance_matrix = importance)

运行这行代码,你应该会得到一个条形图,显示6个特征的重要性(包含与我们之前看到的输出相同的数据,但以视觉形式展示,以便更容易理解)。请注意,xgb.ggplot.importance 也适用于所有 ggplot2 的粉丝!

根据数据集和学习参数,您可能会有两个以上的簇。默认值是将其限制为 10,但您可以增加此限制。查看函数文档以获取更多信息。

根据上图,在这个数据集中,预测治疗是否有效的最重要特征是:

  • 一个人的年龄;

  • 是否接受了安慰剂;

  • 性别;

  • 我们生成的特征 AgeDiscret。我们可以看到它的贡献非常低。

这些结果有意义吗?

让我们检查这些特征与标签之间的 Chi2 值。

更高的 Chi2 意味着更好的相关性。

c2 <- chisq.test(df$Age, output_vector)
print(c2)

## 
##  Pearson's Chi-squared test
## 
## data:  df$Age and output_vector
## X-squared = 35.475, df = 35, p-value = 0.4458

年龄与疾病消失之间的皮尔逊相关系数为 35.47

c2 <- chisq.test(df$AgeDiscret, output_vector)
print(c2)

## 
##  Pearson's Chi-squared test
## 
## data:  df$AgeDiscret and output_vector
## X-squared = 8.2554, df = 5, p-value = 0.1427

我们对年龄的第一次简化给出了 8.26 的皮尔逊相关系数。

c2 <- chisq.test(df$AgeCat, output_vector)
print(c2)

## 
##  Pearson's Chi-squared test with Yates' continuity correction
## 
## data:  df$AgeCat and output_vector
## X-squared = 2.3571, df = 1, p-value = 0.1247

我们在30岁时对年轻和年老的完全随机划分具有较低的相关性 2.36。这表明,对于我们正在研究的特定疾病,某人易受此疾病影响的年龄可能与30岁有很大不同。

故事的寓意:不要让你的 直觉 降低你模型的质量。

数据科学 中,有一个词 科学 :-)

结论

如你所见,通常情况下 通过简化信息来破坏信息并不会改善你的模型Chi2 只是证明了这一点。

但在更复杂的情况下,从现有功能创建新功能可能有助于算法并改进模型。

+这里研究的案例并不复杂,不足以展示这一点。请查看 Kaggle 网站 以获取一些具有挑战性的数据集。

此外,你可以看到,即使我们添加了一些不太有用/与其他特征高度相关的新特征,提升树算法仍然能够选择最佳的特征(在这种情况下是年龄)。

线性模型可能表现不佳。

特别说明:随机森林™ 怎么样?

如你所知,随机森林算法与提升算法是近亲,两者都属于集成学习家族。

两者都为同一个数据集训练多个决策树。主要区别在于,在随机森林中,树是独立的,而在提升方法中,第 N+1 棵树专注于学习第 N 棵树未能很好建模的部分(即损失)。

这种差异可能会对特征重要性分析中的一个边缘案例产生影响:相关特征

想象两个完全相关的特征,特征 A 和特征 B。对于某一个特定的树,如果算法需要其中一个特征,它将随机选择(这在提升和随机森林中都是正确的)。

然而,在随机森林中,这种随机选择将在每棵树上进行,因为每棵树都是相互独立的。因此,大约(并取决于你的参数)50% 的树会选择特征 A,而另外 50% 的树会选择特征 B。所以,包含在 AB 中的信息的重要性(因为它们是完全相关的,所以是相同的)被稀释在 AB 中。因此,你不容易知道这些信息对你的预测目标很重要!当你有 10 个相关特征时,情况甚至更糟……

在提升算法中,当算法学习到特征与结果之间的特定联系后,理论上它会尝试不再关注这一联系(现实情况并不总是如此简单)。因此,所有的重点将放在特征 A 或特征 B 上(但不会同时关注两者)。你会知道某个特征在观察值与标签之间的联系中起着重要作用。如果你需要知道所有相关的特征,仍然需要你自己去寻找与检测到的重要特征相关的其他特征。

如果你想尝试随机森林算法,你可以调整XGBoost的参数!

例如,要计算一个包含1000棵树的模型,行和列的采样因子为0.5:

data(agaricus.train, package = 'xgboost')
data(agaricus.test, package = 'xgboost')
train <- agaricus.train
test <- agaricus.test

#Random Forest - 1000 trees
bst <- xgboost(
    data = train$data
    , label = train$label
    , max_depth = 4
    , num_parallel_tree = 1000
    , subsample = 0.5
    , colsample_bytree = 0.5
    , nrounds = 1
    , objective = "binary:logistic"
)

## [1]  train-logloss:0.456201

#Boosting - 3 rounds
bst <- xgboost(
    data = train$data
    , label = train$label
    , max_depth = 4
    , nrounds = 3
    , objective = "binary:logistic"
)

## [1]  train-logloss:0.444882 
## [2]  train-logloss:0.302428 
## [3]  train-logloss:0.212847

注意参数 round 被设置为 1

随机森林 是 Leo Breiman 和 Adele Cutler 的商标,并且独家授权给 Salford Systems 用于该软件的商业发布。