不知各位读者是否遇到这种情形:在一项机器学习工作中,千方百计地变换模型并调整参数,但收效甚微;后经高人指点,对进入模型的特征做了一些修改后,即使用了最基础的模型,其效果也有如脱胎换骨。这似乎在暗示我们,在机器学习任务中,“使用什么样的特征”往往要比“使用什么样的模型”更重要。
以大数据的普遍流行为分割线,无论是在之前的数据因稀少而珍贵的时代,还是现在的数据「泛滥」时代,选择特征一直都是机器学习研究者所关注的重要内容。数据少的时候,人们希望通过增加特征来找到目标变量的变化趋势;数据多的时候,人们希望剔除冗余和无关因素的影响来加速模型收敛,提升模型精度。
下面,我将以scikit-learn
中的相关方法为例,梳理特征选择的基本理论。
特征选择:概述与方法
特征选择,顾名思义,就是从所有给定的特征中选出一个或多个用于构建模型。为了评估选出的特征是否让模型具有了更好的表现效果,还需要定义评价指标来对特征子集进行打分。
进行特征选择的一个最直观的想法是,穷举特征的所有组合,分别进行评价。虽然这种方法在理论上能够获得最优的组合,但对于高维数据而言,会面临组合爆炸的问题,不太可行。为了提高可行性,我们需要采用其他的方式来选择特征。
基于方差的特征选择
方差是衡量数据离散程度的指标,值越小说明数据变化的程度越小。举一个极端的例子,如果数据集中的某个特征的方差为0,说明该特征的取值在所有样本中都是一致的,那么它对于我们区分目标变量毫无帮助,是可以剔除的。
一般的,我们可以设置一个方差阈值,然后将每个特征的方差与阈值进行比较。如果某特征的方差低于该阈值,则认为该特征的数据变化不大,并推断其所包含的信息不足以对目标变量进行区分,因而予以剔除。
下面是一个sklearn中的例子:
>>> 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)))
>>> sel.fit_transform(X)
array([[0, 1],
[1, 0],
[0, 0],
[1, 1],
[1, 0],
[1, 1]])
易知,X
的第一个维度的方差不满足阈值要求,该特征被剔除。
读者应该已经发现,这种做法是有「问题」的:在上面的例子中,如果目标变量的取值刚好与第一个特征的取值变化相同,换句话说,它们具有很高的相关性,这时剔除该特征是不合适的。
这种问题的本质,在于判定哪些变量需要剔除时没有考虑目标变量的取值。因此,它是一种相对粗糙的方法,多用于特征初筛,剔除那些在我们看来几乎绝对不可能提升模型效果的特征。
相关系数法
以回归问题为例对本方法及下一个方法进行说明。
要衡量某个特征对于目标的预测是否重要,一个直观的想法是计算特征与目标的相关性。例如,可以计算特征列与目标列的皮尔逊相关系数:
from sklearn.datasets import load_boston # 波士顿房价数据集,在1.2版本后将不再可用
X, y = load_boston(return_X_y=True)
print(X.shape)
# 输出:(506, 13)
# 样本数为506,特征数为13
from sklearn.feature_selection import r_regression
print(r_regression(X, y))
# 输出:
# [-0.38830461 0.36044534 -0.48372516 0.17526018 -0.42732077 0.69535995
# -0.37695457 0.24992873 -0.38162623 -0.46853593 -0.50778669 0.33346082
# -0.73766273]
r_regression
实际上计算的是各个特征与y
的相关系数,可以验证如下:import numpy as np; print(np.corrcoef(X[:,0], y)[0][1]) # 得到第一个特征与y的相关系数,可以类似地得到其他特征的相关系数
接下来,可以将相关系数从大到小进行排序,从而选择前k
个最相关的特征用于建模。sklearn
已经帮我们实现了这个过程:
from sklearn.feature_selection import SelectKBest
k = 3 # 这里选择前3个最相关的特征
selector = SelectKBest(r_regression, k=3) # r_regression就是评分函数
X_new = selector.fit_transform(X, y)
print(selector.get_feature_names_out())
# 输出:['x1', 'x5', 'x11']
可以看出,经过r_regression
评分,选择出的三个最可能影响目标的特别分别是x1
、x5
和x11
,即原始X
的第2、6和12列。
由于X是numpy数组,没有列名,因此sklearn自动生成了列名。如果传入的X是pandas.DataFrame,那么
get_feature_names_out
将会输出X的列名。
R 2 R^2 R2与 F F F统计量
在使用相关系数进行特征选择时,某些特征与目标的负相关程度很高,也可以用于建模预测,但有可能会被遗漏;此外,多重共线性问题也是相关系数法所无法避免的。为此,需要一些改进的方法。
我们知道,判定系数
R
2
R^2
R2可以用来衡量所建立的回归模型的“好坏”,其计算公式如下:
R
2
=
S
S
E
/
S
S
T
=
1
−
S
S
R
S
S
T
R^2=SSE/SST=1-\frac{SSR}{SST}
R2=SSE/SST=1−SSTSSR
其中,称SST为总平方和(total sum of squares)、SSE为解释平方和(explained sum of squares)、SSR为残差平方和(residual sum of squares)。它们各自的定义如下:
S S T = ∑ i = 1 n ( y i − y ˉ ) 2 SST=\sum_{i=1}^{n}(y_i-\bar y)^2 SST=∑i=1n(yi−yˉ)2
S S E = ∑ i = 1 n ( y ^ i − y ˉ ) 2 SSE=\sum_{i=1}^{n}(\hat y_i-\bar y)^2 SSE=∑i=1n(y^i−yˉ)2
S S R = ∑ i = 1 n ( y i − y ^ i ) 2 SSR=\sum_{i=1}^{n}(y_i-\hat y_i)^2 SSR=∑i=1n(yi−y^i)2
下图有助于理解上述三个概念:
SST度量了 y i y_i yi的总样本波动,反映了样本观测值总体离差的大小;SSE度量了 y i ^ \hat {y_i} yi^的样本波动,反映由模型中解释变量所解释的那部分离差的大小;SSR度量了残差的样本波动,反映观测值与估计值偏离的程度,也是解释变量未解释的那部分离差的大小。
它们之间有如下关系: S S T = S S R + S S E SST=SSR+SSE SST=SSR+SSE(证明略)。因此可以知道,SSE不可能大于SST,所以 R 2 R^2 R2的取值范围为 [ 0 , 1 ] [0,1] [0,1]。越接近于1,说明模型的拟合得越好。
还有一个困扰人的问题:判定系数表示为 R 2 R^2 R2,那么是否说明它就等于相关系数 r r r的平方呢?
对于简单线性回归而言,上述结论是正确的;否则,就是不正确的。
我们知道,两个卡方分布分别除以其自由度后构造的新的统计量就服从F分布。这里先说结论:在线性回归分析中,有
S
S
R
∼
χ
(
1
)
SSR \sim \chi(1)
SSR∼χ(1),
S
S
E
∼
χ
(
n
−
2
)
SSE \sim \chi(n-2)
SSE∼χ(n−2)。因此:
F
(
1
,
n
−
2
)
=
S
S
R
S
S
E
/
(
n
−
2
)
=
S
S
R
/
S
S
T
S
S
E
/
S
S
T
∗
(
n
−
2
)
=
R
2
1
−
R
2
∗
(
n
−
2
)
F(1,n-2)=\frac{SSR}{SSE/(n-2)}=\frac{SSR/SST}{SSE/SST}*(n-2)=\frac{R^2}{1-R^2}*(n-2)
F(1,n−2)=SSE/(n−2)SSR=SSE/SSTSSR/SST∗(n−2)=1−R2R2∗(n−2)
在简单线性回归中,又有
R
2
=
r
2
R^2=r^2
R2=r2,所以:
F
(
1
,
n
−
2
)
=
r
2
1
−
r
2
∗
(
n
−
2
)
F(1,n-2)=\frac{r^2}{1-r^2}*(n-2)
F(1,n−2)=1−r2r2∗(n−2)
这也就意味着,如果知道了自变量和因变量的相关系数,就可以求出其F统计量的值,从而判定该自变量在回归模型中是否具有解释力。
在sklearn中,可以用f_regression
来作为评价函数进行特征选择:
from sklearn.feature_selection import f_regression
selector = SelectKBest(f_regression, k=3)
X_new = selector.fit_transform(X, y)
print(selector.get_feature_names_out())
# 输出:['x5', 'x10', 'x12']
可以看出,经过f_regression
选择后,得出的三个最能解释因变量的为第5、10和12个特征。
执行以下代码,以获得各个特征对因变量的解释力的F统计量值:
print(f_regression(X, y))
# array([ 89.48611476, 75.2576423 , 153.95488314, 15.97151242,
# 112.59148028, 471.84673988, 83.47745922, 33.57957033,
# 85.91427767, 141.76135658, 175.10554288, 63.05422911,
# 601.61787111]),
# array([1.17398708e-19, 5.71358415e-17, 4.90025998e-31, 7.39062317e-05,
# 7.06504159e-24, 2.48722887e-74, 1.56998221e-18, 1.20661173e-08,
# 5.46593257e-19, 5.63773363e-29, 1.60950948e-34, 1.31811273e-14,
# 5.08110339e-88])
第一个array为各个因变量的F统计量值,可以看出,SelectKBest
实际上是选出了具有最大F值的前3个特征;第二个array为对应的p值。读者可以利用相关系数自行验证F统计量的值是否满足
F
=
r
2
/
(
1
−
r
2
)
∗
(
n
−
2
)
F=r^2/(1-r^2)*(n-2)
F=r2/(1−r2)∗(n−2)。
参考内容:
- https://stats.stackexchange.com/questions/56881/whats-the-relationship-between-r2-and-f-test
- 特征选择
- R 2 R^2 R2:Calculation & Interpretation
- F检验
- 残差平方和SSE为什么服从卡方分布?