【机器学习】scikit-learn中的特征选择小结
一.概述
1. 特征工程
特征工程是将原始数据转换为更能代表预测模型的潜在问题的特征的过程,可以通过挑选最相关的特征,提取特征以及创造特征来实现。
可能面对的问题有:特征之间有相关性,特征和标签无关,特征太多或太小,或者干脆就无法表现出应有的数据现象或无法展示数据的真实面貌
特征工程的目的:1) 降低计算成本,2) 提升模型上限 。
2.sklearn中的特征工程
sklearn中包含众多特征工程相关的模块,包括:
- 模块feature_selection:包含特征选择的各种方法的实践
- 模块decomposition:包含降维算法
二三 . 特征选择feature_selection
特征工程的第一步是:理解业务。
一定要抓住给你提供数据的人,尤其是理解业务和数据含义的人,跟他们聊一段时间。技术能够让模型起飞,前提是你和业务人员一样理解数据。所以首先要根据我们的目标,用业务常识来选择特征。
当然了,在真正的数据应用领域,比如金融,医疗,电商,我们的数据特征可能很多,业务重要性也不是这么明显,我们无法依赖对业务的理解来选择特征,该怎么办呢?我们有四种方法可以用来选择特征:过滤法,嵌入法,包装法,和降维算法。
#导入数据,让我们使用digit recognizor数据来一展身手
import pandas as pd
data = pd.read_csv(r"./../菜菜的sklearn课堂课件/03数据预处理和特征工程/digit recognizor.csv")
X = data.iloc[:,1:]
y = data.iloc[:,0]
X.shape
(42000, 784)
1.FIlter过滤法
过滤方法通常用作预处理步骤,它的特征选择完全独立于任何机器学习算法。是根据各种统计检验中的分数以及相关性的各项指标来选择特征。
(1)方差过滤
这是通过特征本身的方差来筛选特征的类。比如一个特征本身的方差很小,就表示样本在这个特征上基本没有差 异,可能特征中的大多数值都一样,甚至整个特征的取值都相同,那这个特征对于样本区分没有什么作用。
VarianceThreshold
- 重要参数threshold,表示方差的阈值,表示舍弃所有方差小于threshold的特征,不填默认为0
from sklearn.feature_selection import VarianceThreshold
import numpy as np
X1 = VarianceThreshold().fit_transform(X)
X_fsvar = VarianceThreshold(np.median(X.var().values)).fit_transform(X)
print(X1.shape)
print(X_fsvar.shape)
(42000, 708)
(42000, 392)
可以看到,0为阈值原来有784个特征,现在剩下708个特征 ;使用方差中位数为阈值,剩下392个特征。
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import cross_val_score
# 使用随机森林模型查看特征选择后的效果
# threshold=0
cross_val_score(RFC(n_estimators=10,random_state=0),X1,y,cv=5).mean()
0.9385004515100317
# threshold =np.median(X.var().values)
cross_val_score(RFC(n_estimators=10,random_state=0),X_fsvar,y,cv=5).mean()
0.9388098166696807
可以看到使用方差中位数过滤后的特征,不仅特征减少了,模型准确率还上升了一些。说明方法有效且过滤的大多是一些噪声,可以直接选择特征过滤力度大的X_fsvar。
方差过滤带来的影响
如果对KNN,随机森林算法做特征选择前后的模型运行时间和准确率做对比,会发现knn在特征选择后,运行速度明显变快,而对随机森林却没有影响。
原因是,最近邻算法KNN,单棵决策树,支持向量机SVM,神经网络,回归算法等,都需要遍历特征或升维来进行运算,所以他们本身的运算量就很大,需要的时间就很长,因此方差过滤这样的特征选择对他们来说就尤为重要。但对于不需要遍历特征的算法,比如随机森林,它本身就是随机选取部分特征进行分枝,本身运算就非常快速,因此特征选择对它来说效果平平。这其实很容易理解,无论过滤法如何降低特征的数量,随机森林也只会选取固定数量的特征 来建模;而最近邻算法就不同了,特征越少,距离计算的维度就越少,模型明显会随着特征的减少变得轻量。
因此,过滤法的主要对象是:需要遍历特征或升维的算法们,主要目的是:在维持算法表现的前提下,帮助算法们降低计算成本。
如果使用过滤法后,模型的准确率不变或者略有上升,说明过滤掉的大多是噪音,使用过滤法效果好。 如果模型准确率下降很多,说明被过滤掉了很多有效特征,我们可能设置的阈值过大。这时可以调小阈值,或者是放弃使用过滤法。
(2)相关性过滤
除了过滤方差小的特征以外,我们还希望选出与标签相关且有意义的特征,因为这样的 特征能够为我们提供大量信息。如果特征与标签无关,那只会白白浪费我们的计算内存,可能还会给模型带来噪音。在sklearn当中,我们有三种常用的方法来评判特征与标签之间的相关性:卡方,F检验,互信息。
卡方检验 (针对分类问题)
卡方过滤是专门针对离散型标签(即分类问题)的相关性过滤。卡方检验类feature_selection.chi2计算每个非负 特征和标签之间的卡方统计量,并依照卡方统计量由高到低为特征排名。再结合feature_selection.SelectKBest 这个可以输入”评分标准“来选出前K个分数最高的特征的类,我们可以借此除去最可能独立于标签,与我们分类目的无关的特征。
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import cross_val_score
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
# 使用卡方检验,选取卡方统计量最高的前k个特征
X_fschi = SelectKBest(chi2, k='all').fit_transform(X_fsvar, y)
X_fschi.shape
(42000, 392)
关于上述k值的选择,可以画一个学习曲线选取最佳的K值,也可以通过卡方检验的p值进行删选。
卡方检验的本质是推测两组数据之间的差异,其检验的原假设是”两组数据是相互独立的”。卡方检验返回卡方值和 P值两个统计量,其中卡方值很难界定有效的范围,而p值,我们一般使用0.01或0.05作为显著性水平,即p值判断的边界。
- p值<=0.05或0.01则说明两组数据是相关的,拒绝原假设,接受备择假设 。p值>.05或0.01则相反。
我们可以直接从chi2实例化后的模型中获得各个特征所对应的卡方值和P值,然后转化为要选的k值
chivalue, pvalue = chi2(X_fsvar,y)
#k取多少?我们想要消除所有p值大于设定值,比如0.05或0.01的特征:
k = chivalue.shape[0] - (pvalue > 0.05).sum()
k
392
可以看到k值没有变,这说明对于digit recognizor这个数据集来说,方差验证已经把所有和标签无关的特征都剔除了,或者这个数据集本身就不含与标签无关的特征。在这种情况下,舍弃任何一个特征,都会舍弃 对模型有用的信息,而使模型表现下降,因此在我们对计算速度感到满意时,我们不需要使用相关性过滤来过滤我们的数据。
F检验
F检验,又称ANOVA,方差齐性检验,是用来捕捉每个特征与标签之间的线性关系的过滤方法。它即可以做回归也 可以做分类,因此包含feature_selection.f_classif(F检验分类)和feature_selection.f_regression(F检验回 归)两个类。其中F检验分类用于标签是离散型变量的数据,而F检验回归用于标签是连续型变量的数据。
和卡方检验一样,这两个类需要和类SelectKBest连用,并且我们也可以直接通过输出的统计量来判断我们到底要 设置一个什么样的K。需要注意的是,F检验在数据服从正态分布时效果会非常稳定,因此如果使用F检验过滤,我 们会先将数据转换成服从正态分布的方式。
from sklearn.feature_selection import f_classif
F, pvalues_f = f_classif(X_fsvar,y)
k = F.shape[0] - (pvalues_f > 0.05).sum()
k
392
得到的结论和我们用卡方过滤得到的结论一模一样:没有任何特征的p值大于0.05,所有的特征都是和标签相关的,因此我们不需要相关性过滤。
互信息法
互信息法是用来捕捉每个特征与标签之间的任意关系(包括线性和非线性关系)的过滤方法。和F检验相似,它既 可以做回归也可以做分类,并且包含两个类feature_selection.mutual_info_classif(互信息分类)和 feature_selection.mutual_info_regression(互信息回归)。这两个类的用法和参数都和F检验一模一样,不过互信息法比F检验更加强大,F检验只能够找出线性关系,而互信息法可以找出任意关系。
互信息法不返回p值或F值类似的统计量,它返回“每个特征与目标之间的互信息量的估计”,这个估计量在[0,1]之间 取值,为0则表示两个变量独立,为1则表示两个变量完全相关。以互信息分类为例的代码如下:
from sklearn.feature_selection import mutual_info_classif as MIC
result = MIC(X_fsvar,y)
k = result.shape[0] - sum(result <= 0)
k
392
所有特征的互信息量估计都大于0,因此所有特征都与标签相关,得到的结论是一样的。
以上总结了比较常见的过滤法,通常来说,可以先使用方差过滤,然后使用互信息法来捕捉相关性。
2.Embedded嵌入法
嵌入法是一种让算法自己决定使用哪些特征的方法,即特征选择和算法训练同时进行。在使用嵌入法时,我们先使 用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据权值系数从大到小选择特征。这些权值系 数往往代表了特征对于模型的某种贡献或某种重要性,比如决策树和树的集成模型中的feature_importances_属 性,可以列出各个特征对树的建立的贡献,我们就可以基于这种贡献的评估,找出对模型建立最有用的特征。因此 相比于过滤法,嵌入法的结果会更加精确到模型的效用本身,对于提高模型效力有更好的效果。并且,由于考虑特 征对模型的贡献,因此无关的特征(需要相关性过滤的特征)和无区分度的特征(需要方差过滤的特征)都会因为 缺乏对模型的贡献而被删除掉,可谓是过滤法的进化版。
但是嵌入法也有缺点,嵌入法会提供每个特征的权值,但是权值的临界值很难界定,是一个超参数。另外,嵌入法引入了算法来挑选特征,因此其计算速度也会和应用的算法有很大的关系。如果采用计算量很大,计 算缓慢的算法,嵌入法本身也会非常耗时耗力。并且,在选择完毕之后,我们还是需要自己来评估模型。
feature_selection.SelectFromModel 用于与评估器一起用于特征选择
SelectFromModel是一个元变换器,可以与任何在拟合后具有coef_,feature_importances_属性或参数中可选惩罚项的评估器一起使用(比如随机森林和树模型就具有属性feature_importances_,逻辑回归就带有l1和l2惩罚项,线性支持向量机也支持l2惩罚项)。
- 对于有feature_importances_的模型来说,若重要性低于提供的阈值参数,则认为这些特征不重要并被移除。
- 而对于使用惩罚项的模型来说,正则化惩罚项越大,特征在模型中对应的系数就会越小。当正则化惩罚项大到 一定的程度的时候,部分特征系数会变成0,这些特征就可以被删除。另外,支持向量机和逻辑回归使用参数C来控制返回的特征矩阵的稀疏性,参数C越小,返回的特征越少。Lasso回归,用alpha参数来控制返回的特征矩阵,alpha的值越大,返回的特征越少。
这里使用随机森林来举例,X_embedded即为特征选择后的特征矩阵
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier as RFC
RFC_ = RFC(n_estimators =10,random_state=0)
X_embedded = SelectFromModel(RFC_,threshold=0.005).fit_transform(X,y)
X_embedded.shape
(42000, 47)
其中threshold的阈值的选择可以用学习曲线去寻找
import numpy as np
import matplotlib.pyplot as plt
threshold = np.linspace(0,(RFC_.fit(X,y).feature_importances_).max(),20)
score = []
for i in threshold:
X_embedded = SelectFromModel(RFC_,threshold=i).fit_transform(X,y)
once = cross_val_score(RFC_,X_embedded,y,cv=5).mean()
score.append(once)
plt.plot(threshold,score)
plt.show()
<Figure size 640x480 with 1 Axes>
从图像上来看,随着阈值越来越高,模型的效果逐渐变差,被删除的特征越来越多,信息损失也逐渐变大。但是在 0.00134之前,模型的效果都可以维持在0.93以上,因此我们可以从中挑选一个数值来验证一下模型的效果。
X_embedded = SelectFromModel(RFC_,threshold=0.00067).fit_transform(X,y)
cross_val_score(RFC_,X_embedded,y,cv=5).mean()
0.939905083368037
3.Wrapper包装法
包装法也是一个特征选择和算法训练同时进行的方法,与嵌入法十分相似,它也是依赖于算法自身的选择,比如 coef_属性或feature_importances_属性来完成特征选择。但不同的是,我们往往使用一个目标函数作为黑盒来帮 助我们选取特征,而不是自己输入某个评估指标或统计量的阈值。包装法在初始特征集上训练评估器,并且通过 coef_属性或通过feature_importances_属性获得每个特征的重要性。然后,从当前的一组特征中修剪最不重要的 特征。在修剪的集合上递归地重复该过程,直到最终到达所需数量的要选择的特征。区别于过滤法和嵌入法的一次 训练解决所有问题,包装法要使用特征子集进行多次训练,因此它所需要的计算成本是最高的。
包装法的效果是所有特征选择方法中最利于提升模型 表现的,它可以使用很少的特征达到很优秀的效果。但是因为计算量较大,所以也不适用于太大型的数据。
feature_selection.RFE 递归特征消除法
最典型的目标函数是递归特征消除法(Recursive feature elimination, 简写为RFE)。它是一种贪婪的优化算法, 旨在找到性能最佳的特征子集。 它反复创建模型,并在每次迭代时保留最佳特征或剔除最差特征,下一次迭代时, 它会使用上一次建模中没有被选中的特征来构建下一个模型,直到所有特征都耗尽为止。 然后,它根据自己保留或 剔除特征的顺序来对特征进行排名,最终选出一个最佳子集。
class sklearn.feature_selection.RFE (estimator, n_features_to_select=None, step=1, verbose=0)
- 参数estimator是需要填写的实例化后的评估器,n_features_to_select是想要选择的特征个数,step表示每次迭代中希望移除的特征个数
- 除此之外,RFE类有两个很重要的属性,.support_:返回所有的特征的是否最后被选 中的布尔矩阵,以及.ranking_返回特征的按数次迭代中综合重要性的排名。
from sklearn.feature_selection import RFE
RFC_ = RFC(n_estimators =10,random_state=0)
selector = RFE(RFC_, n_features_to_select=340, step=50).fit(X, y)
# 特征选择后的特征矩阵
X_wrapper = selector.transform(X)
cross_val_score(RFC_,X_wrapper,y,cv=5).mean()
0.9389522459432109
print(selector.support_.sum())
print(selector.ranking_)
340
[10 9 8 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
6 6 6 6 6 6 7 7 6 6 5 6 5 6 6 6 6 6 6 6 6 6 6 7
6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 5 4 4 5 3 4
4 4 5 4 5 7 6 7 7 7 8 8 8 8 8 8 8 8 6 7 4 3 1 2
3 3 1 1 1 1 1 3 3 4 5 5 5 8 8 9 9 9 9 8 9 9 4 4
3 2 1 1 1 1 1 1 1 1 1 1 2 3 3 4 5 5 9 9 10 10 10 10
7 4 4 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 3 3 5 8 10
10 10 10 9 4 4 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
3 4 10 10 10 10 9 7 4 3 2 2 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 2 4 4 10 9 10 6 6 4 2 3 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 3 5 9 10 8 7 4 5 3 2 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 2 1 2 4 10 10 10 9 7 5 3 3 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3 5 5 9 9 9 7 5
5 3 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 4 5 9 9
9 9 9 5 4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
4 5 7 10 10 9 10 9 4 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 3 5 10 9 10 10 9 7 4 2 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 2 2 4 8 9 10 10 10 5 4 2 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 2 3 5 10 10 10 10 9 5 4 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3 4 5 9 10 10 10 5
3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 3 3 4 8 8
10 10 9 5 3 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 3
3 4 10 10 10 10 8 4 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 2 4 5 8 10 10 10 10 5 2 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 2 4 7 10 10 10 10 8 5 3 2 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 2 3 3 5 5 7 9 9 9 9 5 5 2 2 1
1 1 1 1 1 1 1 1 1 1 1 2 2 2 3 4 5 5 8 9 9 9 9 7
4 4 2 1 1 1 1 1 1 1 1 1 1 1 1 3 3 3 5 5 9 8 9 9
9 9 9 5 4 4 2 2 1 1 1 1 1 2 1 1 1 1 2 2 3 4 5 5
9 8 8 8 8 8 8 7 8 6 4 2 2 1 1 2 2 1 2 2 3 2 2 4
4 5 5 8 8 8 7 7 7 7 7 7 7 5 5 4 5 4 3 3 3 4 3 3
4 3 4 5 5 6 7 7 7 6 7 8 8 8 9 9 9 9 6 8 8 8 7 8
8 8 7 8 8 8 8 8 7 8 8 8 8 9 10 7]
我们也可以对包装法画学习曲线:
score = []
for i in range(1,501,50):
X_wrapper = RFE(RFC_,n_features_to_select=i, step=50).fit_transform(X,y)
once = cross_val_score(RFC_,X_wrapper,y,cv=5).mean()
score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(1,501,50),score)
plt.xticks(range(1,501,50))
plt.show()
4. 特征选择小结
以上我们介绍了很多的特征选择的方法。这些方法的代码都不难,但是每种方法的原理都不同,并且都涉及到不同调整方法的超参数。
经验来说,过滤法更快速,但更粗糙。包装法和嵌入法更精确,比较适合具体到算法去调整,但计算量比较大,运行时间长。
当数据量很大的时候,优先使用方差过滤和互信息法调整,再上其他特征选择方法。使用逻辑回归时,优先使用嵌入法。使用支持向量机时,优先使用包装法。迷茫的时候,从过滤法走起,看具体数据具体分析。
其实特征选择只是特征工程中的第一步。真正的高手,往往使用特征创造或特征提取来寻找高级特征。在Kaggle之类的算法竞赛中,很多高分团队都是在高级特征上做文章,而这是比调参和特征选择更难的,提升算法表现的高深 方法。