跳至主要内容

使用 Boruta R 包进行 R 中的特征选择

在 R 中进行特征选择:探索围绕随机森林分类算法构建的 Boruta 算法及其实现!
· 10分钟 阅读

使用 AI 探索

在当今的机器学习问题中,高维数据(就特征数量而言)越来越常见。要从海量数据中提取有用信息,您需要使用统计技术来降低噪声或冗余数据。这是因为在训练模型时,往往并不需要使用手头的每一个特征。只输入那些不相关且不冗余的特征,反而能提升模型表现。此时,特征选择就显得尤为重要。它不仅能加快模型训练速度,还能降低模型复杂度、提升可解释性,并在准确率、精确率或召回率(无论您的评估指标是什么)上带来改进。

在本教程中,您将学习以下内容:

  • 首先,进一步了解特征选择:何时使用它、以及有哪些方法可以用来为您的模型挑选最重要的特征!

  • 随后,您将认识Boruta 算法。您将看到如何通过比较原始属性的重要性与其随机置换副本(影子特征)所能达到的重要性,来执行自顶向下的相关特征搜索,并逐步剔除无关特征。

  • 您还将简要了解用于执行特征选择的数据集,并学习如何借助 Amelia 包轻松填补缺失值。

  • 最后,您将进一步了解用于运行该算法的Boruta 包。

特征选择

通常,当您想要降低数据维度时,会想到诸如主成分分析(PCA)、奇异值分解等方法。那么,自然会问:为什么还需要其他特征选择方法?这些技术的问题在于,它们是不依赖监督信息的特征选择方式:以 PCA 为例,它通过数据的方差来寻找主成分。这些方法并不考虑特征取值与目标类别或目标值之间的信息。此外,这类方法还伴随着一些假设(例如正态性),在应用之前往往需要进行某些变换。这些限制并不适用于所有类型的数据。

一般来说,特征选择方法可分为三类:

  • 过滤法(Filter Methods):通常作为预处理步骤使用。特征的选择不依赖任何机器学习算法,而是基于特征在各种统计检验中与结果变量的相关性得分来挑选。一些常见的过滤法包括相关性度量(皮尔逊、斯皮尔曼、距离相关)、卡方检验、方差分析(ANOVA)、Fisher 分数等。

  • 封装法(Wrapper Methods):在封装法中,您尝试使用一个特征子集来训练模型。基于从先前模型得到的推断,决定在子集中添加或移除特征。前向选择、后向消除等是这类方法的例子。

  • 嵌入法(Embedded Methods):这类算法自身带有内置的特征选择机制。LASSO 回归就是一个例子。

在本教程中,您将使用一种封装法,它在 R 中通过名为 Boruta 的包即可直接使用。

Boruta 算法

Boruta 算法是围绕随机森林分类算法构建的封装法。它试图在与结果变量相关的前提下,捕获数据集中所有重要且有信息量的特征。

  • 首先,它会复制数据集,并对每一列的取值进行随机打乱。这些值称为影子特征(shadow features)。* 然后,它在数据集上训练一个分类器,比如随机森林分类器。通过这样做,您可以得到每个特征的重要性评估(例如基于平均准确率降低或平均不纯度降低)。分数越高,表示越好、越重要。
  • 接着,算法会检查每个真实特征是否具有更高的重要性。也就是说,该特征的 Z 分数是否高于其影子特征中的最大 Z 分数(即影子特征中的最佳值)。如果是,就在一个向量中记录一次命中(hit)。随后进入下一次迭代。经过预设次数的迭代后,您将得到一张命中表。请记住:Z 分数表示数据点距离均值有多少个标准差,更多信息请点击这里。
  • 在每次迭代中,算法都会比较特征的随机置换副本与原始特征的 Z 分数,看看后者是否优于前者。如果是,算法会将该特征标记为重要。本质上,算法通过与随机打乱的副本进行比较来验证特征的重要性,从而提高稳健性。这是通过使用二项分布,简单比较某个特征优于影子特征的次数来完成的。

feature selection R boruta

  • 如果某个特征在例如 15 次迭代中都没有被记录为命中,则拒绝它,并且从原始矩阵中移除。达到设定的迭代次数后,或当所有特征都已被确认或拒绝时,算法停止。

R 中的 Boruta 算法

让我们在一个最常见的数据集上使用 Boruta 算法:银行营销数据。该数据代表一家葡萄牙银行机构的直接营销活动(电话)。分类目标是预测客户是否会订阅定期存款。

提示:不要忘了在这里查看不同特征的详细说明。

read_file <- read.csv('./bank_bank.csv',header=TRUE,sep=';',stringsAsFactors = F) #read csv into a dataframe
str(read_file)

## 'data.frame':    4521 obs. of  17 variables:
##  $ age      : int  30 33 35 30 59 35 36 39 41 43 ...
##  $ job      : chr  "unemployed" "services" "management" "management" ...
##  $ marital  : chr  "married" "married" "single" "married" ...
##  $ education: chr  "primary" "secondary" "tertiary" "tertiary" ...
##  $ default  : chr  "no" "no" "no" "no" ...
##  $ balance  : int  1787 4789 1350 1476 0 747 307 147 221 -88 ...
##  $ housing  : chr  "no" "yes" "yes" "yes" ...
##  $ loan     : chr  "no" "yes" "no" "yes" ...
##  $ contact  : chr  "cellular" "cellular" "cellular" "unknown" ...
##  $ day      : int  19 11 16 3 5 23 14 6 14 17 ...
##  $ month    : chr  "oct" "may" "apr" "jun" ...
##  $ duration : int  79 220 185 199 226 141 341 151 57 313 ...
##  $ campaign : int  1 1 1 4 1 2 1 2 2 1 ...
##  $ pdays    : int  -1 339 330 -1 -1 176 330 -1 -1 147 ...
##  $ previous : int  0 4 1 0 0 3 2 0 0 2 ...
##  $ poutcome : chr  "unknown" "failure" "failure" "unknown" ...
##  $ y        : chr  "no" "no" "no" "no" ...

使用 summary() 函数汇总数据集中不同特征的常见描述性统计信息。

summary(read_file)

##       age            job              marital           education        
##  Min.   :19.00   Length:4521        Length:4521        Length:4521       
##  1st Qu.:33.00   Class :character   Class :character   Class :character  
##  Median :39.00   Mode  :character   Mode  :character   Mode  :character  
##  Mean   :41.17                                                           
##  3rd Qu.:49.00                                                           
##  Max.   :87.00                                                           
##    default             balance        housing              loan          
##  Length:4521        Min.   :-3313   Length:4521        Length:4521       
##  Class :character   1st Qu.:   69   Class :character   Class :character  
##  Mode  :character   Median :  444   Mode  :character   Mode  :character  
##                     Mean   : 1423                                        
##                     3rd Qu.: 1480                                        
##                     Max.   :71188                                        
##    contact               day           month              duration   
##  Length:4521        Min.   : 1.00   Length:4521        Min.   :   4  
##  Class :character   1st Qu.: 9.00   Class :character   1st Qu.: 104  
##  Mode  :character   Median :16.00   Mode  :character   Median : 185  
##                     Mean   :15.92                      Mean   : 264  
##                     3rd Qu.:21.00                      3rd Qu.: 329  
##                     Max.   :31.00                      Max.   :3025  
##     campaign          pdays           previous         poutcome        
##  Min.   : 1.000   Min.   : -1.00   Min.   : 0.0000   Length:4521       
##  1st Qu.: 1.000   1st Qu.: -1.00   1st Qu.: 0.0000   Class :character  
##  Median : 2.000   Median : -1.00   Median : 0.0000   Mode  :character  
##  Mean   : 2.794   Mean   : 39.77   Mean   : 0.5426                     
##  3rd Qu.: 3.000   3rd Qu.: -1.00   3rd Qu.: 0.0000                     
##  Max.   :50.000   Max.   :871.00   Max.   :25.0000                     
##       y            
##  Length:4521       
##  Class :character  
##  Mode  :character  
##                    
##                    
##  

summary() 函数会为连续型特征给出集中趋势的度量,如均值、中位数、分位数等。如果数据集中有分类特征,您还会看到这些特征的 Class 和 Mode。

现在将分类特征转换为因子(factor)类型:

convert <- c(2:5, 7:9,11,16:17)
read_file[,convert] <- data.frame(apply(read_file[convert], 2, as.factor))
str(read_file)

## 'data.frame':    4521 obs. of  17 variables:
##  $ age      : int  30 33 35 30 59 35 36 39 41 43 ...
##  $ job      : Factor w/ 12 levels "admin.","blue-collar",..: 11 8 5 5 2 5 7 10 3 8 ...
##  $ marital  : Factor w/ 3 levels "divorced","married",..: 2 2 3 2 2 3 2 2 2 2 ...
##  $ education: Factor w/ 4 levels "primary","secondary",..: 1 2 3 3 2 3 3 2 3 1 ...
##  $ default  : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...
##  $ balance  : int  1787 4789 1350 1476 0 747 307 147 221 -88 ...
##  $ housing  : Factor w/ 2 levels "no","yes": 1 2 2 2 2 1 2 2 2 2 ...
##  $ loan     : Factor w/ 2 levels "no","yes": 1 2 1 2 1 1 1 1 1 2 ...
##  $ contact  : Factor w/ 3 levels "cellular","telephone",..: 1 1 1 3 3 1 1 1 3 1 ...
##  $ day      : int  19 11 16 3 5 23 14 6 14 17 ...
##  $ month    : Factor w/ 12 levels "apr","aug","dec",..: 11 9 1 7 9 4 9 9 9 1 ...
##  $ duration : int  79 220 185 199 226 141 341 151 57 313 ...
##  $ campaign : int  1 1 1 4 1 2 1 2 2 1 ...
##  $ pdays    : int  -1 339 330 -1 -1 176 330 -1 -1 147 ...
##  $ previous : int  0 4 1 0 0 3 2 0 0 2 ...
##  $ poutcome : Factor w/ 4 levels "failure","other",..: 4 1 1 4 4 1 2 4 4 1 ...
##  $ y        : Factor w/ 2 levels "no","yes": 1 1 1 1 1 1 1 1 1 1 ...

由于为了创建影子特征需要对数据点进行打乱,并为每个特征计算 Z 分数,因此在使用 boruta 包之前,务必先处理缺失或空值,否则会抛出错误。

(不)幸的是,该数据集两者都没有。不过,出于教学目的,您将人为引入一些 NA。

使用 prodNA() 函数在数据集中植入缺失值。安装 missForest 包后即可使用该函数。

请记住,如有需要,您可以使用 install.packages() 安装缺失的包!

library(missForest)

# Generate 5% missing values at random
bank.mis <- prodNA(read_file, noNA = 0.05)  

您也可以再次对新的数据框调用 summary() 来查看填补的 NA 数量,但我们不妨更有创意一些!

使用下面的 ggplot2 代码可视化数据中的缺失情况:

library(reshape2)
library(ggplot2)
library(dplyr)
ggplot_missing <- function(x){

  x %>% 
    is.na %>%
    melt %>%
    ggplot(data = .,
           aes(x = Var2,
               y = Var1)) +
    geom_raster(aes(fill = value)) +
    scale_fill_grey(name = "",
                    labels = c("Present","Missing")) +
    theme_minimal() + 
    theme(axis.text.x  = element_text(angle=45, vjust=0.5)) + 
    labs(x = "Variables in Dataset",
         y = "Rows / observations")
}
ggplot_missing(bank.mis)

图中的白色线条直观地显示了您已在每个特征中植入了缺失值。不过,您也看到了为了编写这个 ggplot_missing 函数费了不少功夫?好在接下来将要使用的 R 包 Amelia,提供了一行代码即可绘制类似图形的替代方案:

library(Amelia)
missmap(bank.mis)

动手试试吧!

使用 Amelia 填补缺失值

现在您可以用多种方式来填补缺失值,比如用均值、中位数或众数(针对分类特征)填补,但我们将使用另一个功能强大的缺失值填补包 Amelia。

Amelia 采用 m 次自助法(bootstrap)抽样,并对每个样本应用 EMB(期望最大化 + 自助法)算法。m 组均值和方差估计会有所不同。最后,使用第一组估计通过回归填补第一组缺失值,第二组估计用于第二组,以此类推。多重插补有助于降低偏差并提高效率。此外,它支持使用多核 CPU 进行并行插补。

它有 3 个重要参数:

  • m:要创建的插补数据集数量。
  • idvars:保留所有 ID 变量及其他不希望被插补的变量。
  • noms:在此处指定名义(分类)变量。
library(Amelia)
amelia_bank <- amelia(bank.mis, m=3, parallel = "multicore",noms=c('job','marital','education','default','housing','loan','contact','month','poutcome','y'))

## -- Imputation 1 --
## 
##   1  2  3  4  5  6
## 
## -- Imputation 2 --
## 
##   1  2  3  4  5  6
## 
## -- Imputation 3 --
## 
##   1  2  3  4  5

要访问插补后的数据框,您可以使用如下子集操作:

amelia_bank$imputations[[1]]

要将插补后的数据集导出为 csv 文件,请使用:

write.amelia(amelia_bank, file.stem = "imputed_bank_data_set")

Boruta R 包

现在让我们在其中一个插补数据集上使用 Boruta 算法。您可以借助 Boruta 包来完成:

library(Boruta)
set.seed(111)
boruta.bank_train <- Boruta(y~., data = amelia_bank$imputations[[1]], doTrace = 2)
print(boruta.bank_train)

## Boruta performed 99 iterations in 18.97234 mins.
##  10 attributes confirmed important: age, contact, day, duration,
## housing and 5 more;
##  3 attributes confirmed unimportant: education, job, marital;
##  3 tentative attributes left: balance, campaign, default;

Boruta 会对数据集中各特征的重要性作出判断。许多特征已被直接归类为重要或不重要,但您会看到还有一些被标记为待定(tentative)。

这意味着什么?

待定特征的重要性与其最佳影子特征非常接近,以至于在默认的随机森林运行次数下,Boruta 无法以所需的置信度作出决定。

那么该如何处理?

如果仍有待定特征,您可以考虑增大 maxRuns 参数。不过,注意,您也可以提供 mtry 和 ntree 参数的取值,这些参数会传递给 randomForest() 函数。前者用于指定每次分裂时随机抽样作为候选的变量个数,后者用于指定要生长的树的数量。在指定这些参数后,随机森林分类器会在袋外误差的最小值附近收敛。

请记住,袋外误差是用于衡量采用自助聚合(bagging)对训练样本进行子采样的分类器的预测误差的估计值。它是对每个训练样本 X,仅使用未包含 X 的自助样本中训练出的树来进行预测,并取其平均预测误差。

或者,您也可以将 doTrace 参数设置为 1 或 2,以获取过程进展的报告。

boruta 包还提供了 TentativeRoughFix() 函数,可通过比较特征 Z 分数的中位数与最重要影子特征的 Z 分数中位数,来补全缺失的决策:

#take a call on tentative features
boruta.bank <- TentativeRoughFix(boruta.bank_train)
print(boruta.bank)

## Boruta performed 99 iterations in 18.97234 mins.
## Tentatives roughfixed over the last 99 iterations.
##  12 attributes confirmed important: age, campaign, contact, day,
## default and 7 more;
##  4 attributes confirmed unimportant: balance, education, job,
## marital;

Boruta 到此已完成任务:它已成功将每个特征归类为重要或不重要。

您现在可以通过调用 plot(boruta.bank) 绘制 Boruta 变量重要性图。不过,x 轴标签将是水平的,看起来不太美观。

因此,您将像下面的代码块那样,将特征标签垂直添加到 x 轴上:

plot(boruta.bank, xlab = "", xaxt = "n")
lz<-lapply(1:ncol(boruta.bank$ImpHistory),function(i)
boruta.bank$ImpHistory[is.finite(boruta.bank$ImpHistory[,i]),i])
names(lz) <- colnames(boruta.bank$ImpHistory)
Labels <- sort(sapply(lz,median))
axis(side = 1,las=2,labels = names(Labels),
at = 1:ncol(boruta.bank$ImpHistory), cex.axis = 0.7)

y 轴标签 Importance 表示在打乱后的数据集中,每个特征的 Z 分数。

蓝色箱线图对应影子特征的最小、平均和最大 Z 分数;红色和绿色箱线图分别表示被拒绝和被确认特征的 Z 分数。您可以看到,红色箱线图的 Z 分数低于影子特征的最大 Z 分数,这正是它们被归为不重要类别的原因。

您可以通过输入以下命令来确认特征的重要性:

getSelectedAttributes(boruta.bank, withTentative = F)

##  [1] "age"      "default"  "housing"  "loan"     "contact"  "day"     
##  [7] "month"    "duration" "campaign" "pdays"    "previous" "poutcome"

bank_df <- attStats(boruta.bank)
print(bank_df)

##              meanImp  medianImp     minImp    maxImp   normHits  decision
## age       11.4236197 11.3760979  8.4250222 15.518420 1.00000000 Confirmed
## job        0.0741753  0.3002281 -1.7651336  1.566687 0.01010101  Rejected
## marital    1.8891283  2.0043568 -1.0276720  4.804499 0.22222222  Rejected
## education  1.5969540  1.6188117 -1.6836346  4.629572 0.28282828  Rejected
## default    2.3721979  2.3472820 -0.1434933  5.044653 0.50505051 Confirmed
## balance    2.3349682  2.3214378 -0.8098151  5.567993 0.51515152  Rejected
## housing    8.4147808  8.4384240  4.7392059 10.404609 1.00000000 Confirmed
## loan       4.1872186  4.2797591  2.0325838  6.263155 0.87878788 Confirmed
## contact   18.9482180 18.9757719 16.0937657 22.121461 1.00000000 Confirmed
## day        9.5645192  9.5828766  6.1842233 13.495442 1.00000000 Confirmed
## month     24.1475736 24.2067940 20.0621966 27.200679 1.00000000 Confirmed
## duration  71.5232213 71.1785055 64.3941499 78.249830 1.00000000 Confirmed
## campaign   2.6221456  2.6188180 -0.4144493  4.941482 0.65656566 Confirmed
## pdays     26.5650528 26.7123730 23.7902945 29.067476 1.00000000 Confirmed
## previous  20.9569022 20.9703991 18.7273357 23.117672 1.00000000 Confirmed
## poutcome  28.5166889 28.4885934 25.9855974 31.527154 1.00000000 Confirmed

您可以很容易地验证这一结果:特征 duration 获得了最高的重要性,这一点在数据说明中已明确提到(仔细阅读可点这里)!

结论

大功告成!您仅用几行代码就从数据集中筛选出了最重要的特征。借此,您降低了数据中的噪声,这将有助于任意分类器更准确地为观测赋予标签。基于这些重要特征训练模型,必将提升模型表现——这正是进行特征选择的初衷!

如果您想查看编写本教程所用到的资源,请参阅以下内容:

  • PACKAGE AMELIA: A Program for Missing Data; James Honaker, Gary King, and Matthew Blackwell
  • Feature Selection with the Boruta Package; Miron B. Kursa, Witold R. Rudnicki
  • RDocumentation
  • UCI 机器学习资料库

主题