特征工程

 

特征选择 feature_selection

  • 特征选择(feature extraction):从文字、图像、声音等其他非结构化数据中提取新信息作为特征
  • 特征创造(feature creation):把现有特征进行组合,或相互计算,得到新特征
  • 特征选择(feature selection):从所有特征中,选择出有意义、对模型有帮助的特征,以避免必须将所有特征都导入模型去训练的情况。
import pandas as pd

file_path = '/content/drive/MyDrive/Colab Notebooks/data/digit recognizor.csv'

data = pd.read_csv(file_path)
data.head()

X = data.iloc[:, 1:]
y = data.iloc[:, 0]

X.shape		# (42000, 784)

Filter 过滤法

过滤方法通常用作预处理步骤,特征选择完全独立于任何机器学习算法。它是根据各种统计验证中的分数以及相关性的各项指标来选择特征。

方差过滤

VarianceThreshold

如果一个特征本身的方差很小,就表示样本在这个特征上基本没有差异,,可能能特征中的大多数值都一样,尤其是整个特征的取值都一样,那么这个特征对于样本区分没有任何作用。

from sklearn.feature_selection import VarianceThreshold

X_var0 = VarianceThreshold().fit_transform(X)  # 阈值默认为0
X_var0.shape  # (42000, 708)

将特征方差的中位数作为阈值,可以让特征数量减半。

import numpy as np

X_fsvar = VarianceThreshold(np.median(X.var().values)).fit_transform(X)
X_fsvar.shape  # (42000, 392)
方差过滤对模型的影响

分别使用 X 和 X_fsvar 运行 KNN 模型

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

cross_val_score(KNeighborsClassifier(), X, y, cv=5).mean()	
# 0.965857142857143

%%timeit
cross_val_score(KNeighborsClassifier(), X, y, cv=5).mean()
# 1 loop, best of 5: 1min 33s per loop
cross_val_score(KNeighborsClassifier(), X_fsvar, y, cv=5).mean()
# 0.966

%%timeit
cross_val_score(KNeighborsClassifier(), X_fsvar, y, cv=5).mean()
# 1 loop, best of 5: 59.4 s per loop

可以看到,方差过滤后,准确率稍有提升,但运行效率显著提升,运行时间减少大约1/3。

分别使用 X 和 X_fsvar 运行随机森林

from sklearn.neighbors import RandomForestClassifier
from sklearn.model_selection import cross_val_score

cross_val_score(RandomForestClassifier(n_estimators=10, random_state=0), X, y, cv=5).mean()
# 0.9373571428571429

%%timeit
cross_val_score(RandomForestClassifier(n_estimators=10, random_state=0), X, y, cv=5).mean()
# 1 loop, best of 5: 13.6 s per loop
cross_val_score(RandomForestClassifier(n_estimators=10, random_state=0), X_fsvar, y, cv=5).mean()
# 0.9390476190476191

%%timeit
cross_val_score(RandomForestClassifier(n_estimators=10, random_state=0), X_fsvar, y, cv=5).mean()
# 1 loop, best of 5: 13.3 s per loop

可以看到,方差过滤后,随机森林的准确率有所上升,但运行时间几乎没有变化。

为什么随机森林运行如此之快?为什么方差过滤对随机森林没有很大的影响?

这是由于两种算法的原理中涉及到的计算量不同。 最近邻算法KNN、单棵决策树、支持向量机SVM、神经网络、回归算法,都需要遍历特征或升维来进行运算,所以他们本身的运算量就很大,需要的时间就很长,因此方差过滤这样的特征选择对他们来说就尤为重要。但对于不需要遍历特征的算法,比如随机森林,它随机选取特征进行分枝,本身运算就非常快速,因此特征选择对它来说效果平平。这其实很容易理解,无论过滤法如何降低特征的数量,随机森林也只会选取固定数量的特征来建模;而最近邻算法就不同了,特征越少,距离计算的维度就越少,模型明显会随着特征的减少变得轻量。 因此,过滤法的主要对象是:需要遍历特征或升维的算法们,而过滤法的主要目的是:在维持算法表现的前提下,帮助算法们降低计算成本

超参 threshold

现实中,我们只会使用阈值为0或者阈值很小的方差过滤,来为我们优先消除一些明显用不到的特征,然后我们会选择更优的特征选择方法继续减少特征数量。

相关性过滤

选出与标签有关且有意义的特征

卡方过滤

卡方过滤是专门针对离散型标签(即分类问题)的相关性过滤。

from sklearn.feature_selection import chi2
from sklearn.feature_selection import SelectKBest

X_fschi = SelectKBest(chi2, k=300).fit_transform(X_fsvar, y) # 保留300个特征
X_fschi.shape  #(42000, 300)

cross_val_score(KNeighborsClassifier(), X_fschi, y, cv=5).mean()
# 0.9606190476190477

可以看出,模型的准确率下降了,这说明 k=300 设置的过小了,删除了与模型有关且有效的特征。如果模型的表现提升,则说明我们的相关性过滤是有效的,过滤掉了模型的噪声,这是应该保留过滤后的结果。

选取超参 k

学习曲线

import matplotlib.pyplot as plt

scores = []

for i in range(200, 390, 10):
	X_fschi = SelectKBest(chi2, k=i).fit_transform(X_fsvar, y)
	score = cross_val_score(KNeighborsClassifier(), X_fschi, y, cv=5).mean()
	scores.append(score)

plt.plot(range(200, 390, 10), scores)
plt.show()
过滤法-卡方过滤

可以看到,随着 K 值的增加,模型效果不断增加,这说明,所有特征对模型都是有效的。

但是运行学习曲线的时间也是非常长,接下来我们介绍一种更好的方法:看 p 值选 k

卡方检验的本质是推测两组数据之间的差异,其检验的原假设是“两组数据是相互独立的”。卡方检验返回卡方值和 p 值两个统计量,其中卡方值很难界定有效的范围,而 p 值,我们一般使用0.01或0.05作为显著性水平。

p值 <=0.05或0.01 >0.05或0.01
数据差异 差异不是自然形成的 差异是自然的样本误差
相关性 两组数据是相关的 两组数据是相互独立的
原假设 拒绝原假设 接受原假设
chivalue, pvalues_chi = chi2(X_fsvar, y)

chivalue

pvalues_chi

# k取值
k = pvalues_chi.shape[0] - (pvalues_chi > 0.05).sum()  # 392

可以看到,所有特征的 p 值都是0,这说明所有特征都与标签相关。在这种情况下,舍弃任何一个特征,都会舍弃对模型有用的信息,而使模型表现下降。

F 检验

F 检验,又称 ANOVA,方差齐性检验,是用来捕捉每个特征与标签之间的线性关系的过滤方法,它即可以做回归(feature_selection.f_regression)也可以做分类(feature_selection.f_classif)。

F 检验的本质是寻找两组数据之间的线性关系,其原假设是”数据不存在显著的线性关系“。它返回 F 值和 p 值两个统计量。

from sklearn.feature_selection import f_classif

F, pvalues_f = f_classif(X_fsvar, y)

F

pvalues_f

# k取值
k = pvalues_f.shape[0] - (pvalues_f > 0.05).sum()  # 392

得到的结论和[[#卡方过滤]]的一样:没有任何特征的 p 值大于0.01,所有的特征都是和标签相关的,因此我们不需要相关性过滤。

互信息法

互信息法是用来捕捉每个特性与标签之间的任意关系(包括线性和非线性关系)的过滤方法。和 F 检验相似,它既可以做回归(feature_selection.mutual_info_regression)也可以做分类(feature_selection.mutual_info_classif)。

互信息法返回“每个特征与目标之间的互信息量的估计”,这个估计量在 [0,1] 之间,0表示两个变量独立,1表示两个变量完全相关。

from sklearn.feature_selection import mutual_info_classif

result = mutual_info_classif(X_fsvar, y)
result

k = result.shape[0] - (result <= 0).sum()  # 392

可以看到,所有特征的互信息量估计都大于0,因此所有特征都与标签相关。

过滤法总结

到这里我们学习了常用的基于过滤法的特征选择,包括方差过滤,基于卡方、F 检验和互信息的相关性过滤,总结如下表:

说明 超参选择
VarianceThreshold 方差过滤,删除小于阈值的特征 看具体情况
SelectKBest 用来选取K个统计量结果最佳的特征 看配合使用的统计量
chi2 卡方检验,专用于分类算法,捕捉相关性 p值小于显著性水平的特征
f_classif F检验分类,只能捕捉线性相关性,要求数据服从正态分布 p值小于显著性水平的特征
f_regression F检验回归,只能捕捉线性相关性,要求数据服从正态分布 p值小于显著性水平的特征
mutual_info_classif 互信息分类,可以捕捉任何相关性,要求数据服从正态分布 互信息估计大于0的特征
mutual_info_regression 互信息回归,可以捕捉任何相关性,要求数据服从正态分布 互信息估计大于0的特征

Embedded 嵌入法

嵌入法是一种让算法自己决定使用哪些特征的方法,即特征选择和算法训练同时进行。在使用嵌入法时,我们先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,权值系数往往代表了特征对于模型的贡献度或重要性。因此无关的特征(需要相关性过滤的特征)和无区分度的特征(需要方差过滤的特征)都会因为缺乏对模型的贡献而被删除掉,可谓是过滤法的进化版。

嵌入法引入了算法来挑选特征,因此其计算速度也会和算法有很大的关系。如果采用计算量很大、计算缓慢的算法,嵌入法本身也会非常耗时耗力,并且,在选择完毕之后,我们还是需要自己来评估模型。

feature_selection.SelectFromModel

class sklearn.feature_selection.SelectFromModel(estimator, *, threshold=None, prefit=False, norm_order=1, max_features=None, importance_getter=’auto’)

SelectFromModel 是一个元变换器,可以与任何在拟合后具有 coef_feature_importances_ 属性或参数中可选惩罚项的学习器一起使用(比如随机森林和树模型具有属性feature_importances_,逻辑回归带有 L1 和 L2 惩罚项,线性支持向量机也支持 L2 惩罚项)。

参数 说明
estimator 带有coef_、feature_importances_属性,或带有L1或L2惩罚项的学习器
threshold 重要性低于阈值的特征都将被删除
prefit 判断是否将实例化的模型直接传递给构造函数,默认为False
norm_order k可输入非零整数,正无穷,负无穷,默认为1
max_features 在阈值设定下,要选择的最大特征数。要禁用阈值并仅根据max_features选择,请设置threshold=-np.inf

以随机森林为例,使用学习曲线来寻找最佳阈值。

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)

可以看出,这里阈值设置为0.005,相对来说是比较高的。

import numpy as np
from sklearn.model_selection import cross_val_score
import matplotlib.pyplot as plt

max_threshold = (RFC_.fit(X, y).feature_importances_).max()
threshold = np.linspace(0, max_threshold, 20)
scores = []

for i in threshold:
	X_embedded = SelectFromModel(RFC_, threshold=i).fit_transform(X, y)
	score = cross_val_score(RFC_, X_embedded, y, cv=5).mean()
	scores.append(score)

plt.figure(figsize=(20,5))
plt.plot(threshold, scores)
plt.xticks(threshold)
plt.show()
嵌入法

从图像上看到,阈值越大,被删除的特征越多,信息损失也越多,模型的效果也越差。但是在0.00134之前,模型的效果可以维持在0.93之上。接下来在对随机森林进行调参,准确率应该还可以在升高不少。

在嵌入法下,我们很容易就能够实现特征选择的目标:减少计算量,提升模型表现。然而,在算法本身很复杂的时候,过滤法的计算远远比嵌入法要快,所以大型数据中,我们还是会优先考虑过滤法

Wrapper 包装法

包装法也是一个特征选择和算法训练同时进行的方法,这点与嵌入法相似,也是依赖于算法自身的选择。但不同的是,我们往往使用一个目标函数作为黑盒来帮助我们选择特征,而不是自己输入某个评估指标或统计量的阈值。区别在于,过滤法和嵌入法一次训练解决所有问题,包装法要使用特征子集进行多次训练,因此它所需要的计算成本是最高的。

注意,包装法中的“算法”,指的不是我们最终训练的分类或回归算法,而是专业的数据挖掘算法,即我们的目标函数。这些数据挖掘算法的核心功能就是选取最佳特征子集。

最典型的目标函数是递归特征消除法(Recursive feature elimination,简写 RFE),它是一种贪婪优化算法,旨在找到性能最佳的特征子集。它反复创建模型,并在每次迭代时保留最佳特征或剔除最差特征,如此反复,直到所有特征都耗尽为止。然后,它会根据自己保留或剔除特征的顺序来对特征进行排名,最终选出一个最佳子集。包装法的效果是所有特征选择方法中最利于提升模型表现的,它可以使用很少的特征达到很优秀的效果。除此之外,在特征数据相同时,包装法和嵌入法的效果能够匹敌,不过它比嵌入法算得更缓慢,所以也不太适用于太大量的数据。相比之下,包装法是最能保证模型效果的特征选择方法。

feature_selection.RFE

class sklearn.feature_selection.RFE(estimator, *, n_features_to_select=None, step=1, verbose=0, importance_getter=’auto’)

  • 参数
    • estimator:实例化的学习器
    • n_features_to_select:保留的特征个数
    • step:每次迭代要移除的特征的个数
  • 属性
    • .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)

selector.support_.sum() # 筛选后的特征个数  # 340

selector.ranking_

X_wrapper = selector.transform(X)
cross_val_score(RFC_, X_wrapper, y, cv=5).mean()
# 0.9379761904761905

绘制包装法的学习曲线:

scores = []

for i in range(1,501,50):
	X_wrapper = RFE(RFC_, n_features_to_select=i, step=50).fit_transform(X, y)
	score = cross_val_score(RFC_, X_wrapper, y, cv=5).mean()
	scores.append(score)
	
plt.figure(figsize=(20,5))
plt.plot(range(1,501,50), scores)
plt.xticks(range(1,501,50))
plt.show()
包装法1

可以看出,在包装法下,仅使用51个特征,模型的表现就能达到90%以上,比嵌入法和过滤法都高效很多。接下来,进一步细化学习曲线。

包装法2

特征选择总结

这些方法的代码都不难,但是每种方法的原理都不同,并且都涉及不同调整方法的超参数。经验来说,过滤法更快速,但更粗糙;包装法和嵌入法更精确,比较适合具体到算法去调整,但计算量比较大,运行时间长。当数据量很大时,优先使用方差过滤和互信息法调整,再上其他特征选择方法。使用逻辑回归时,优先使用嵌入法。使用支持向量机时,优先使用包装法。迷茫的时候,从过滤法走起,看具体数据具体分析。