特征选择是特征工程里的一个重要问题,其目标是寻找最优特征子集。特征选择能剔除不相关(irrelevant)或冗余(redundant )的特征,从而达到减少特征个数,提高模型精确度,减少运行时间的目的。另一方面,选取出真正相关的特征简化模型,协助理解数据产生的过程。并且常能听到“数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已”,由此可见其重要性。但是它几乎很少出现于机器学习书本里面的某一章。然而在机器学习方面的成功很大程度上在于如果使用特征工程。
特征选择的过程 ( M. Dash and H. Liu 1997 )
主要分为产生过程,评估过程,停止条件和验证过程。
但是, 当特征数量很大的时候, 这个搜索空间会很大,如何找最优特征还是需要一些经验结论。
根据特征选择的形式可以将特征选择方法分为三大类:
1、过滤(filter)特征选择
过滤特征选择法的想法是针对每个特征
,
从
到
,计算
相对于类别标签
的信息量
,得到
个结果,然后将
个
按照从大到小排序,输出前
个特征。显然,这样复杂度大大降低。那么关键的问题就是使用什么样的方法来度量
,我们的目标是选取与
关联最密切的一些 特征
。
皮尔森相关系数是一种最简单的,能帮助理解特征和响应变量之间关系的方法,该方法衡量的是变量之间的线性相关性,结果的取值区间为
,
表示完全的负相关(这个变量下降,那个就会上升),
表示完全的正相关,
表示没有线性相关。Pearson Correlation速度快、易于计算,经常在拿到数据(经过清洗和特征提取之后的)之后第一时间就执行。Scipy的pearsonr方法能够同时计算相关系数和p-value,
import numpy as np
from scipy.stats import pearsonr
np.random.seed(0)
size = 300
x = np.random.normal(0, 1, size)
print("Lower noise", pearsonr(x, x + np.random.normal(0, 1, size)))
print("Higher noise", pearsonr(x, x + np.random.normal(0, 10, size)))
Pearson相关系数的一个明显缺陷是,作为特征排序机制,他只对线性关系敏感。如果关系是非线性的,即便两个变量具有一一对应的关系,Pearson相关性也可能会接近
。
经典的卡方检验是检验定性自变量对定性因变量的相关性。假设自变量有N种取值,因变量有M种取值,考虑自变量等于i且因变量等于j的样本频数的观察值与期望的差距,构建统计量:
不难发现,这个统计量的含义简而言之就是自变量对因变量的相关性。用sklearn中feature_selection库的SelectKBest类结合卡方检验来选择特征的代码如下:
from sklearn.datasets import load_iris
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
iris = load_iris()
X, y = iris.data, iris.target
#选择K个最好的特征,返回选择特征后的数据
X_new = SelectKBest(chi2, k=2).fit_transform(X, y)
sklearn.feature_selection模块中的类可以用于样本集中的特征选择/维数降低,以提高估计器的准确度分数或提高其在非常高维数据集上的性能
经典的互信息也是评价定性自变量对定性因变量的相关性的,互信息公式如下:
当
是0/1离散值的时候,这个公式如上。很容易推广到
是多个离散值的情况。这里的
,
和
都是从训练集上得到的。若问这个
公式如何得来,请看它的
距离(Kullback-Leibler)表述:
也就是说,
衡量的是
和
的独立性。如果它俩独立
,那么
距离值为0,也就是
和
不相关了,可以去除
。相反,如果两者密切相关,那么
值会很大。在对
进行排名后,最后剩余的问题就是如何选择
个值(前
个
)。(后面将会提到此方法)我们继续使用交叉验证的方法,将
从
扫描到
,取最大的
。 不过这次复杂度是线性的了。比如,在使用朴素贝叶斯分类文本的时候,词表长度
很大。 使用filiter特征选择方法,能够增加分类器精度。
想把互信息直接用于特征选择其实不是太方便:1、它不属于度量方式,也没有办法归一化,在不同数据及上的结果无法做比较;2、对于连续变量的计算不是很方便(
和
都是集合,
,
都是离散的取值),通常变量需要先离散化,而互信息的结果对离散化的方式很敏感。
最大信息系数克服了这两个问题。它首先寻找一种最优的离散化方式,然后把互信息取值转换成一种度量方式,取值区间在
。minepy提供了MIC功能。
下面我们来看下
这个例子,MIC算出来的互信息值为1(最大的取值)。代码如下:
from minepy import MINE
m = MINE()
x = np.random.uniform(-1, 1, 10000)
m.compute_score(x, x**2)
print(m.mic())
距离相关系数是为了克服Pearson相关系数的弱点而生的。在
和
这个例子中,即便Pearson相关系数是
,我们也不能断定这两个变量是独立的(有可能是非线性相关);但如果距离相关系数是
,那么我们就可以说这两个变量是独立的。
方差选择法
过滤特征选择法还有一种方法不需要度量特征
和类别标签
的信息量。这种方法先要计算各个特征的方差,然后根据阈值,选择方差大于阈值的特征。
例如,假设我们有一个具有布尔特征的数据集,并且我们要删除超过80%的样本中的一个或零(开或关)的所有特征。布尔特征是伯努利随机变量,这些变量的方差由下式给出:
VarianceThreshold是特征选择的简单基线方法。它删除方差不符合某个阈值的所有特征。默认情况下,它会删除所有零差异特征,即所有样本中具有相同值的特征。代码如下:
from sklearn.feature_selection import VarianceThreshold
X = [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 1], [0, 1, 0], [0, 1, 1]]
sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
print(sel.fit_transform(X))
输出结果:
array([[0, 1],
[1, 0],
[0, 0],
[1, 1],
[1, 0],
[1, 1]])
如预期的那样,VarianceThreshold
已经删除了第一列,其具有
包含零的概率。
2、包装(wrapper)特征选择
Wrapper这里指不断地使用不同的特征组合来测试学习算法进行特征选择。先选定特定算法, 一般会选用普遍效果较好的算法, 例如Random Forest, SVM, kNN等等。
前向搜索说白了就是每次增量地从剩余未选中的特征选出一个加入特征集中,待达到阈值或者
时,从所有的
中选出错误率最小的。过程如下:
为空。
从
到
如果第
个特征不在
中,那么特征
和
放在一起作为
(即
)。 在只使用
中特征的情况下,利用交叉验证来得到
的错误率。
个
中选出错误率最小的
,更新
为
。
中的特征数达到了
或者预定的阈值(如果有的话), 那么输出整个搜索过程中最好的 ;若没达到,则转到 2,继续扫描。
既然有增量加,那么也会有增量减,后者称为后向搜索。先将
设置为
,然后每次删除一个特征,并评价,直到达到阈值或者为空,然后选择最佳的
。
这两种算法都可以工作,但是计算复杂度比较大。时间复杂度为
递归消除特征法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练。使用feature_selection库的RFE类来选择特征的代码如下:
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
#递归特征消除法,返回特征选择后的数据
#参数estimator为基模型
#参数n_features_to_select为选择的特征个数
RFE(estimator=LogisticRegression(), n_features_to_select=2).fit_transform(iris.data, iris.target)
3、嵌入(Embedded)特征选择
通过L1正则项来选择特征:L1正则方法具有稀疏解的特性,因此天然具备特征选择的特性,但是要注意,L1没有选到的特征不代表不重要,原因是两个具有高相关性的特征可能只保留了一个,如果要确定哪个特征重要应再通过L2正则方法交叉检验。
这种方法的思路是直接使用你要用的机器学习算法,针对每个单独的特征和响应变量建立预测模型。假如某个特征和响应变量之间的关系是非线性的,可以用基于树的方法(决策树、随机森林)、或者扩展的线性模型等。基于树的方法比较易于使用,因为他们对非线性关系的建模比较好,并且不需要太多的调试。但要注意过拟合问题,因此树的深度最好不要太大,再就是运用交叉验证。通过这种训练对特征进行打分获得相关性后再训练最终模型。
在波士顿房价数据集上使用sklearn的随机森林回归给出一个单变量选择的例子:
from sklearn.cross_validation import cross_val_score, ShuffleSplit
from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
#加载波士顿房价作为数据集
boston = load_boston()
X = boston["data"]
Y = boston["target"]
names = boston["feature_names"]
#n_estimators为森林中树木数量,max_depth树的最大深度
rf = RandomForestRegressor(n_estimators=20, max_depth=4)
scores = []
for i in range(X.shape[1]):
#每次选择一个特征,进行交叉验证,训练集和测试集为7:3的比例进行分配,
#ShuffleSplit()函数用于随机抽样(数据集总数,迭代次数,test所占比例)
score = cross_val_score(rf, X[:, i:i+1], Y, scoring="r2",
cv=ShuffleSplit(len(X), 3, .3))
scores.append((round(np.mean(score), 3), names[i]))
#打印出各个特征所对应的得分
print(sorted(scores, reverse=True))
输出结果:
[(0.64300000000000002, 'LSTAT'), (0.625, 'RM'), (0.46200000000000002, 'NOX'),
(0.373, 'INDUS'), (0.30299999999999999, 'TAX'), (0.29799999999999999, 'PTRATIO'),
(0.20399999999999999, 'RAD'), (0.159, 'CRIM'), (0.14499999999999999, 'AGE'),
(0.097000000000000003, 'B'), (0.079000000000000001, 'ZN'), (0.019, 'CHAS'),
(0.017999999999999999, 'DIS')]