使用XGBoost理解您的数据集
介绍
本小册子的目的是向您展示如何使用 XGBoost 更好地发现和理解您自己的数据集。
这个小插图不是关于预测任何东西的(参见 XGBoost 演示)。我们将解释如何使用 XGBoost 来突出显示数据的 特征 与 结果 之间的 联系。
包加载:
require(xgboost)
require(Matrix)
require(data.table)
if (!require('vcd')) {
install.packages('vcd')
}
VCD 包仅用于其嵌入的数据集之一。
数据集的准备
数值变量 vs. 分类变量
XGBoost 仅管理 数值
向量。
当你有分类数据时该怎么办?
一个 分类 变量有固定数量的不同值。例如,如果一个名为 Colour 的变量只能取这三个值之一,red、blue 或 green,那么 Colour 就是一个 分类 变量。
在 R 中,一个 分类 变量被称为
因子
。在控制台中输入
?factor
以获取更多信息。
为了回答上述问题,我们将把 分类 变量转换为 数值
变量。
从分类变量到数值变量的转换
查看原始数据
+在本Vignette中,我们将看到如何将一个密集的data.frame
(密集 = 矩阵中的大多数元素是非零的)与分类变量转换为一个非常稀疏的numeric
特征矩阵(稀疏 = 矩阵中有许多零元素)。
我们将要看到的方法通常被称为 one-hot 编码。
第一步是将 Arthritis
数据集加载到内存中,并使用 data.table
包对其进行包装。
data(Arthritis)
df <- data.table(Arthritis, keep.rownames = FALSE)
我们首先要做的是查看 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
清理数据
我们移除ID,因为从这个特征中没有什么可以学习的(它只会增加一些噪音)。
df[, ID := NULL]
我们将列出列 Treatment
的不同值:
levels(df[, Treatment])
## [1] "Placebo" "Treated"
编码分类特征
下一步,我们将把分类数据转换为虚拟变量。存在几种编码方法,例如,独热编码 是一种常见的方法。我们将使用 虚拟对比编码,这种方法很受欢迎,因为它产生“满秩”编码(另见 Max Kuhn 的这篇博客文章)。
目的是将每个 分类 特征的每个值转换为 二进制 特征 {0, 1}
。
例如,列 Treatment
将被替换为两列,TreatmentPlacebo
和 TreatmentTreated
。它们都将是 二进制的。因此,在转换之前在列 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"
将
Y
向量设置为0
;对于
Improved == Marked
为TRUE
的行,将Y
设置为1
;返回
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)
![](discoverYourData_files/figure-markdown_strict/unnamed-chunk-12-1.png)
运行这行代码,你应该会得到一个条形图,显示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
。所以,包含在 A
和 B
中的信息的重要性(因为它们是完全相关的,所以是相同的)被稀释在 A
和 B
中。因此,你不容易知道这些信息对你的预测目标很重要!当你有 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 用于该软件的商业发布。