1 引言
特征提取和特征选择作为机器学习的重点内容,可以将原始数据转换为更能代表预测模型的潜在问题和特征的过程,可以通过挑选最相关的特征,提取特征和创造特征来实现。要想学习特征选择必然要了解什么是特征提取和特征创造,得到数据的特征之后对特征进行精炼,这时候就要用到特征选择。本文主要介绍特征选择的三种方法:过滤法(filter)、包装法(wrapper)和嵌入法(embedded)。
特征提取(Feature Extraction):从文字,图像,声音等其他非结构化数据中提取新信息作为特征。比如说,从淘宝宝贝的名称中提取出产品类别,产品颜色,是否是网红产品等等。
特征创造(Feature Creation):把现有特征进行组合,或互相计算,得到新的特征。比如说,我们有一列特征是速度,一列特征是距离,我们就可以通过让两列相处,创造新的特征:通过距离所花的时间。
特征选择(Feature Selection):从所有的特征中,选择出有意义,对模型有帮助的特征,以避免必须将所有特征都导入模型去训练的情况。
2 Filter过滤法
过滤法可以理解为在机器学习算法之前的预处理,过滤法特征选择的过程完全独立与任何机器学习算法。根据对特征经过统计检验之后得到的分数,来筛选掉一些相对来说无用的特征,从而优化特征集。
过滤法适用场景:在需要遍历特征或升维的算法之前,对特征进行过滤。
过滤法的目的:在维持算法表现的前提下,帮助算法降低计算成本。
2.1 方差过滤
Variance Threshold是通过特征本身方差来筛选特征的类。比如一个特征本身的方差很小,就表示样本在这个特征上基本没有差异,可能特征中的大多数值都一样,甚至整个特征的取值都相同,那这个特征对于样本区分没有什么作用。所以无论接下来的特征工程要做什么,都要优先消除方差为0的特征。VarianceThreshold有重要参数threshold,表示方差的阈值,表示舍弃所有方差小于threshold的特征,不填默认为0,即删除所有的记录都相同的特征。下面代码简单的实现了方差过滤:
- import pandas as pd
- from sklearn.feature_selection import VarianceThreshold
- data = pd.read_csv(r"./train.csv")
- x = data.iloc[:,1:]
- y = data.iloc[:,0]
- print(x.shape)
- selector = VarianceThreshold()
- x_var0 = selector.fit_transform(x)
- print(x_var0.shape)
从结果中可以看到原本数据中有784个特征,经过阈值为 0 的方差过滤之后,剩下708个特征,也就是说之前有76个特征的方差都为0。剩下的708个特征还是比较多的,并不能满足我们的需求,此时我们还需要进一步的特征选择。由于单纯调整阈值比较抽象,我们并不知道特定阈值下会留下多少个特征,留下特征过多或者过少都对我们的结果不利,所以我们可以留下指定数量的特征,比如留下一半的特征,找到特征方差的中位数,再将这个中位数作为 threshold 的值就可以让特征总数减半,代码如下:
- import pandas as pd
- import numpy as np
- from sklearn.feature_selection import VarianceThreshold
- data = pd.read_csv(r"./train.csv")
- x = data.iloc[:,1:]
- y = data.iloc[:,0]
- print(x.shape)
- selector = VarianceThreshold(np.median(x.var().values))
- x_feature_selection = selector.fit_transform(x)
- print(x_feature_selection.shape)
如果特征是二分类,特征的取值就是伯努利随机变量,这些变量的方差计算公式为:Var[X] = p (1 - p),其中 X 为特征矩阵,p为二分类特征中的一类在这个特征中所占的概率。那么假设 p = 0.8,即二分类中某种分类占到80%以上的时候删除特征。代码如下
- import pandas as pd
- import numpy as np
- from sklearn.feature_selection import VarianceThreshold
- data = pd.read_csv(r"./train.csv")
- x = data.iloc[:,1:]
- y = data.iloc[:,0]
- print(x.shape)
- selector = VarianceThreshold(0.8 *(1-0.8))
- x_feature_selection = selector.fit_transform(x)
- print(x_feature_selection.shape)
K-近邻算法(KNN)是一种比较简单的分类算法,其原理是利用每个样本到其他样本点的距离来判断每个样本点的相似度,然后对样本进行分类。KNN必须遍历每个特征和样本,因而特征越多,KNN所需要的计算力也就越大。
随机森林或随机决策森林是用于分类,回归和其他任务的集成学习方法,其通过在训练时构建多个决策树并输出作为类的模式(分类)或平均预测(回归)的类来操作。个别树木。随机决策森林纠正决策树过度拟合其训练集的习惯。随机森林随机的选取特征进行分值,本身的运算非常迅速。
实验证明,对特征进行方差过滤之后,KNN的准确率稍有提升,运行时间降低了三分之一。随机森林的准确率略低于KNN,但是花费的算力非常少,不到KNN计算时间的百分之 1 。另外随机森林的准确率略微上升,运行时间并没与什么变化。因此方差过滤并不是适用于所有的算法,因为过滤之后模型可能变好也可能变性能下降。我们就需要针对数据集去进行尝试,也就是调参,选出最优的参数,画学习曲线就可以找到比较好的参数点。但是现实中一般不会花费太多时间在方差过滤的调参上,而是使用阈值为 0 或者阈值很小的方差进行过滤,消除一些明显用不到的特征然后选取其他的特征选择方法继续削减特征数量。
2.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
- #留下300个特征
- X_fschi = SelectKBest(chi2, k=300).fit_transform(X_fsvar, y)
- X_fschi.shape
- #验证模型效果
- cross_val_score(RFC(n_estimators=10,random_state=0),X_fschi,y,cv=5).mean()
K值变化时,模型的评分也会跟着变化,手动去调整一个一个参数的话效率非常低,我们可以使用学习曲线来获得一个最优的超参数K。代码如下:
- import pandas as pd
- import numpy as np
- from sklearn.feature_selection import VarianceThreshold
- 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
- import matplotlib.pyplot as plt
- data = pd.read_csv(r"./train.csv")
- x = data.iloc[:,1:]
- y = data.iloc[:,0]
- # print(x.shape)
- selector = VarianceThreshold(np.median(x.var().values))
- x_fsvar = selector.fit_transform(x)
- score = []
- for i in range(390,200,-10):
- x_fschi = SelectKBest(chi2,k = i).fit_transform(x_fsvar,y)
- once = cross_val_score(RFC(n_estimators=10,random_state=0),x_fschi,y,cv=5).mean()
- score.append(once)
- plt.plot(range(390,200,-10),score)
- plt.show()
从曲线中我们可以看到,随着K值不断的在呢个价模型的表现不断上升,这说明K越大越好,数据中所有的特征都是和标签相关的。但是这个程序运行时间比较长,我们可以用另一种更好的方法选择 K :看 p 值选K。
卡方阿金艳的本质是推测数据之间的差异,卡方检验返回卡方值和 P 值两个统计量,其中卡方值很难界定有效的范围,而 p 值我们一般使用 0.01 或 0.05 作为显著性水平,即p值判断的边界。
p值 | <=0.05 或0.01 | >0.05 或 0.01 |
数据差异 | 差异不是自然形成的 | 这些差异是很自然的样本误差 |
相关性 | 两组数据是相关的 | 两组数据是相互独立的 |
原假设 | 拒绝原假设,接受备择假设 | 接受原假设 |
卡方值大,p值小于0.05的特征是和标签相关联的特征。调用 SelectKBest,可以直接从chi实例化后的模型中获取各个特征所对应的卡方值和 p 值。我们只需要算出来p值大于0.05 的特征有几个,这个个数就是我们想要得到的K值。这里得到的 p 值全为 0,也就是说对于该数据集,方差过滤已经把所有和标签无关的特征都剔除了。
- chivalue, pvalues_chi = chi2(X_fsvar,y)
- print(chivalue)
- print(pvalues_chi)
- #k取多少?我们想要消除所有p值大于设定值,比如0.05或0.01的特征:
- k = chivalue.shape[0] - (pvalues_chi > 0.05).sum()
- #X_fschi = SelectKBest(chi2, k=填写具体的k).fit_transform(X_fsvar, y)
- #cross_val_score(RFC(n_estimators=10,random_state=0),X_fschi,y,cv=5).mean()
F检验
F检验,又称ANOVA,方差齐性检验,是用来捕捉每个特征与标签之间的线性关系的过滤方法。它即可以做回归也可以做分类,因此包含feature_selection.f_classif(F检验分类)和feature_selection.f_regression(F检验回归)两个类。其中F检验分类用于标签是离散型变量的数据,而F检验回归用于标签是连续型变量的数据。
和卡方检验一样,这两个类需要和类SelectKBest连用,并且我们也可以直接通过输出的统计量来判断我们到底要设置一个什么样的K。需要注意的是,F检验在数据服从正态分布时效果会非常稳定,因此如果使用F检验过滤,我们会先将数据转换成服从正态分布的方式。
F检验的本质是寻找两组数据之间的线性关系,其原假设是”数据不存在显著的线性关系“。它返回F值和p值两个统计量。和卡方过滤一样,我们希望选取p值小于 0.05 或 0.01 的特征,这些特征与标签时显著线性相关的,而p值大于0.05或0.01的特征则被我们认为是和标签没有显著线性关系的特征,应该被删除。以F检验的分类为例,我们继续在数字数据集上来进行特征选择:
- chivalue, pvalues_chi = chi2(X_fsvar,y)
- chivalue
- pvalues_chi
- #k取多少?我们想要消除所有p值大于设定值,比如0.05或0.01的特征:
- k = chivalue.shape[0] - (pvalues_chi > 0.05).sum()
- #X_fschi = SelectKBest(chi2, k=填写具体的k).fit_transform(X_fsvar, y)
- #cross_val_score(RFC(n_estimators=10,random_state=0),X_fschi,y,cv=5).mean()
- from sklearn.feature_selection import f_classif
- F, pvalues_f = f_classif(X_fsvar,y)
- F
- pvalues_f
- k = F.shape[0] - (pvalues_f > 0.05).sum()
- #X_fsF = SelectKBest(f_classif, k=填写具体的k).fit_transform(X_fsvar, y)
- #cross_val_score(RFC(n_estimators=10,random_state=0),X_fsF,y,cv=5).mean()
得到的结论和我们用卡方过滤得到的结论一模一样:没有任何特征的p值大于0.01,所有的特征都是和标签相关,因此不需要进行相关性过滤。
互信息法
互信息法是用来捕捉每个特征与标签之间的任意关系(包括线性和非线性关系)的过滤方法。和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)
- #X_fsmic = SelectKBest(MIC, k=填写具体的k).fit_transform(X_fsvar, y)
- #cross_val_score(RFC(n_estimators=10,random_state=0),X_fsmic,y,cv=5).mean()
最终得到的互信息量都大于 0,表明所有特征都和标签相关。
3 Embedded嵌入法
嵌入法是一种算法自己决定使用哪些特征的方法,即特征选择和算法训练同时进行。在使用嵌入法时,我们先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据权值系数从大到小选择特征。这些权值系数往往代表了特征对于模型的某种贡献或某种重要性,比如决策树和树的集成模型中的feature_importances_属性,可以列出各个特征对树的建立的贡献,我们就可以基于这种贡献的评估,找出对模型建立最有用的特征。因此相比于过滤法,嵌入法的结果会更加精确到模型的效用本身,对于提高模型效力有更好的效果。并且,由于考虑特征对模型的贡献,因此无关的特征(需要相关性过滤的特征)和无区分度的特征(需要方差过滤的特征)都会因为缺乏对模型的贡献而被删除掉,可谓是过滤法的进化版。
由于嵌入法时算法自身决定哪些特征可以使用,很难去界定一个标准来判断特征是否是有效的,所以并不会像过滤法那样根据统计知识和指标来对特征进行衡量(如p值应当低于显著水平 0.05)。针对不同的算法,模型的权值系数也会不同,所以不同模型的嵌入法使用起来也是不一样的。此外,由于嵌入法选取特征时是根据算法来决定的,也就是说特征选择的过程伴随着算法的训练过程,那么整个过程非常耗时耗力,这也是嵌入法的一个缺陷。下面我们会以随机森林和决策树模型来学习一下嵌入法。
feature_selection.SelectFromModel
- class sklearn.feature_selection.SelectFromModel (estimator, threshold=None, prefit=False, norm_order=1,max_features=None)
参数 | 说明 |
estimator | 使用的模型评估器,只要是带feature_importances或者coef_属性,或带有 l1 和 l2 惩罚项的模型都可以使用。 |
threshold | 特征重要性的阈值,重要性低于这个阈值的特征都将被删除 |
prefit | 默认False,后的模型直接传递给构造函数。如果为True,则必须直接调用 fit和transform,不能使用fit_transform,并且SelectFromModel不能与 cross_val_score,GridSearchCV和克隆估计器的类似实用程序一起使用。 |
norm_order | K可输入非零整数,正无穷,负无穷,默认值为1。载频鼓起的coef_属性高 于一维的情况下,用于过滤低于阈值的系数的向量的番薯的阶数。 |
max_features | 在阈值设定下,要选择的最大特征数。要禁用阈值并仅根据max_features 选择,请设置threshold = -np.inf |
前两个参数 estimator 和 threshold在实际应用过程中比较重要,需要我们重点学习。以随机森林为例,借助学习曲线帮助我们寻找最佳特征值。
- from sklearn.feature_selection import SelectFromModel
- from sklearn.ensemble import RandomForestClassifier as RFC
- import numpy as np
- import matplotlib.pyplot as plt
- RFC_ = RFC(n_estimators =10,random_state=0)
- print(X_embedded.shape)
- #模型的维度明显被降低了
- #画学习曲线来找最佳阈值
- RFC_.fit(X,y).feature_importances_
- 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()
- X_embedded = SelectFromModel(RFC_,threshold=0.00067).fit_transform(X,y)
- X_embedded.shape
- print(cross_val_score(RFC_,X_embedded,y,cv=5).mean())
通过学习曲线我们可以得到一个最佳的阈值,使得模型的分数达到 96%以上。因此嵌入法可以实现特征的选择。
4 Wrapper包装法
包装法也是一个特征选择和算法训练同时进行的方法,与嵌入法十分相似,它也是依赖于算法自身的选择,比如coef_属性或feature_importances_属性来完成特征选择。但不同的是,我们往往使用一个目标函数作为黑盒来帮助我们选取特征,而不是自己输入某个评估指标或统计量的阈值。包装法在初始特征集上训练评估器,并且通过coef_属性或通过feature_importances_属性获得每个特征的重要性。然后,从当前的一组特征中修剪最不重要的特征。在修剪的集合上递归地重复该过程,直到最终到达所需数量的要选择的特征。区别于过滤法和嵌入法的一次训练解决所有问题,包装法要使用特征子集进行多次训练,因此它所需要的计算成本是最高的。
图中的算法值得并不是我们最终涌过来导入数据的分类和回归算法(即不是随机森林),而是专业的数据挖掘算法,即我们的目标函数。这些数据挖掘算法的核心功能就是选取最佳特征子集。
最典型的目标函数是递归特征消除法(Recursive feature elimination,简写为RFE)。它是一种贪婪的优化算法,旨在找到性能最佳的特征子集。 它反复创建模型,并在每次迭代时保留最佳特征或剔除最差特征,下一次迭代时,它会使用上一次建模中没有被选中的特征来构建下一个模型,直到所有特征都耗尽为止。 然后,它根据自己保留或剔除特征的顺序来对特征进行排名,最终选出一个最佳子集。包装法的效果是所有特征选择方法中最利于提升模型表现的,它可以使用很少的特征达到很优秀的效果。除此之外,在特征数目相同时,包装法和嵌入法的效果能够匹敌,不过它比嵌入法算得更见缓慢,所以也不适用于太大型的数据。相比之下,包装法是最能保证模型效果的特征选择方法。
feature_selection.RFE
class sklearn.feature_selection.RFE (estimator, n_features_to_select=None, step=1, verbose=0)
参数 | 说明 |
estimator | 使用的模型评估器。 |
n_feature_to_select | 所需特征数 |
step | 每次迭代中希望移除的特征数 |
RFE类中有两个比较重要的属性,.support_:返回所有的特征的是否最后被选中的布尔矩阵,以及.ranking_返回特征的按数次迭代中综合重要性的排名。类feature_selection.RFECV会在交叉验证循环中执行RFE以找到最佳数量的特征,增加参数cv,其他用法都和RFE一模一样。
- import pandas as pd
- import numpy as np
- import matplotlib.pyplot as plt
- from sklearn.ensemble import RandomForestClassifier as RFC
- from sklearn.model_selection import cross_val_score
- from sklearn.feature_selection import RFE
- data = pd.read_csv(r"./train.csv")
- x = data.iloc[:,1:]
- y = data.iloc[:,0]
- # print(x.shape)
- RFC_ = RFC(n_estimators=10,random_state=0)
- score = []
- for i in range(1,751,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,751,50),score)
- plt.xticks(range(1,751,50))
- plt.show()
结果可以看到,使用包装法之后,只需要 50 个特征,模型的表现就已经达到了 90% 以上,比嵌入法和过滤法得到的特征子集要好很多。
5 总结
本文讲了过滤法、嵌入法和包装法三种特征选择方法。三种方法中过滤法最为简单快速,需要的计算时间也最短,但是也较为粗略,实际应用过程中,通常只作为数据的预处理,剔除掉部分明显不需要的特征,然后使用其他方法进一步特征选择。嵌入式和包装法更为精确,更适合具体到算法中去调整。计算量也较大,相应的运行时间也比较长。当数据量比较大时,优先使用方差过滤和互信息法对数据进行预处理,然后在使用其他的特征选择方法。使用逻辑回归时,优先使用嵌入法。使用支持向量机时,优先使用包装法。