跳到主要内容

特征选择

特征选择是机器学习流程中至关重要的一步。它从原始特征中选择最相关的子集,目的是提高模型性能、减少过拟合、加快训练速度并增强模型可解释性。在高维数据场景下,特征选择尤为重要——并非所有特征都对预测任务有帮助,冗余或无关的特征不仅会增加计算成本,还可能引入噪声,降低模型的泛化能力。

为什么需要特征选择?

维度问题

在实际项目中,数据往往包含大量特征,但这些特征并非都有价值。特征过多会带来一系列问题:

维度灾难:随着特征维度增加,数据在空间中变得稀疏,模型需要更多的样本才能学到有效的模式。对于固定数量的样本,特征越多,模型越容易过拟合。

计算成本:更多特征意味着更长的训练时间和更多的内存消耗。对于大规模数据集,这种影响尤为明显。

可解释性下降:特征过多使得模型难以解释,难以识别哪些因素真正影响预测结果。

特征选择 vs 特征提取

特征选择和特征提取都是降维的方法,但本质不同:

方面特征选择特征提取
方式从原始特征中选择子集创建新的特征组合
特征含义保持原始特征含义新特征难以解释
方法过滤、包装、嵌入PCA、LDA等
可解释性

特征选择保留了原始特征的可解释性,这在很多业务场景中非常重要。例如,在信用评分模型中,了解"收入"、"负债率"等具体特征的影响,比解释一个抽象的主成分更有意义。

特征选择方法分类

特征选择方法通常分为三类:

过滤法(Filter Methods):在训练模型之前,基于统计指标对特征进行评分和筛选。计算效率高,但不考虑特征与模型之间的交互。

包装法(Wrapper Methods):将特征选择视为搜索问题,通过训练模型来评估特征子集的质量。效果通常更好,但计算成本高。

嵌入法(Embedded Methods):特征选择嵌入到模型训练过程中,模型自动学习特征的重要性。结合了前两者的优点。

移除低方差特征

这是最简单、最快速的特征选择方法。它基于一个直观假设:如果一个特征在所有样本中几乎取相同的值,那么它对区分不同类别或预测目标几乎没有帮助。

原理

VarianceThreshold 移除所有方差低于阈值的特征。默认情况下,它会移除所有零方差特征——即在所有样本中取值完全相同的特征。

对于布尔型特征(取值为0或1),其方差为:

Var(X)=p(1p)\text{Var}(X) = p(1 - p)

其中 pp 是特征取值为1的概率。

基本用法

from sklearn.feature_selection import VarianceThreshold
import numpy as np

# 示例数据:6个样本,3个特征
X = np.array([
[0, 0, 1],
[0, 1, 0],
[1, 0, 0],
[0, 1, 1],
[0, 1, 0],
[0, 1, 1]
])

# 第一个特征:5/6 是0,方差很低
# 第二个特征:4/6 是1,有一定变化
# 第三个特征:分布较均匀

# 移除方差低于阈值的特征
selector = VarianceThreshold(threshold=0.2) # 阈值可调整
X_selected = selector.fit_transform(X)

print(f"原始特征数: {X.shape[1]}")
print(f"选择后特征数: {X_selected.shape[1]}")
print(f"保留的特征索引: {selector.get_support(indices=True)}")

阈值选择

对于布尔型特征,如果要移除在80%以上样本中取相同值的特征:

# 布尔特征的方差公式:Var = p(1-p)
# 当 p = 0.8 时,Var = 0.8 * 0.2 = 0.16
selector = VarianceThreshold(threshold=0.8 * (1 - 0.8))

对于连续特征,需要根据数据特点选择合适的阈值:

# 查看各特征的方差
from sklearn.datasets import load_iris

iris = load_iris()
X = iris.data

# 计算各特征方差
variances = X.var(axis=0)
print("各特征方差:", variances)

# 移除方差低于某个值的特征
selector = VarianceThreshold(threshold=0.5)
X_selected = selector.fit_transform(X)

适用场景

  • 快速预处理:作为特征选择的第一步,快速移除明显无用的特征
  • 高维稀疏数据:许多特征可能取值单一
  • 布尔特征数据:自然适合方差阈值法

局限性

  • 只考虑单个特征的方差,不考虑特征与目标变量的关系
  • 可能错误移除与目标高度相关的特征(如果目标本身就稀有)
  • 对连续特征的阈值选择缺乏指导原则

单变量特征选择

单变量特征选择基于统计检验评估每个特征与目标变量之间的关系,选择得分最高的特征。这种方法考虑了特征与目标的相关性,比方差阈值法更合理。

核心思想

对每个特征独立进行统计检验,计算其与目标变量的关联程度,然后选择关联程度最高的k个特征或前p%的特征。

选择方法

SelectKBest:选择得分最高的k个特征

from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 选择最好的2个特征
selector = SelectKBest(score_func=f_classif, k=2)
X_selected = selector.fit_transform(X, y)

print(f"选择后形状: {X_selected.shape}")
print(f"各特征得分: {selector.scores_}")
print(f"保留的特征索引: {selector.get_support(indices=True)}")

SelectPercentile:选择得分最高的前p%特征

from sklearn.feature_selection import SelectPercentile, f_classif

# 选择最好的50%特征
selector = SelectPercentile(score_func=f_classif, percentile=50)
X_selected = selector.fit_transform(X, y)

print(f"选择后形状: {X_selected.shape}")

SelectFpr、SelectFdr、SelectFwe:基于p值进行选择

  • SelectFpr:控制假阳性率(False Positive Rate)
  • SelectFdr:控制假发现率(False Discovery Rate)
  • SelectFwe:控制族错误率(Family-Wise Error)
from sklearn.feature_selection import SelectFpr, SelectFdr

# 基于假阳性率选择
selector_fpr = SelectFpr(score_func=f_classif, alpha=0.05)
X_selected = selector_fpr.fit_transform(X, y)

# 基于假发现率选择
selector_fdr = SelectFdr(score_func=f_classif, alpha=0.05)
X_selected = selector_fdr.fit_transform(X, y)

评分函数选择

不同的任务类型需要不同的评分函数:

分类任务

评分函数说明适用场景
f_classif方差分析F值线性关系,默认选择
chi2卡方检验非负特征(如词频)
mutual_info_classif互信息任意关系(非线性)

回归任务

评分函数说明适用场景
f_regressionF值线性关系
mutual_info_regression互信息任意关系
r_regression相关系数线性关系

不同评分函数的对比

from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif, chi2
from sklearn.datasets import load_iris
import matplotlib.pyplot as plt

iris = load_iris()
X, y = iris.data, iris.target

# 计算不同评分函数的得分
selector_f = SelectKBest(score_func=f_classif, k='all')
selector_f.fit(X, y)

selector_mi = SelectKBest(score_func=mutual_info_classif, k='all')
selector_mi.fit(X, y)

# 可视化对比
plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.bar(range(X.shape[1]), selector_f.scores_)
plt.xlabel('特征索引')
plt.ylabel('F值')
plt.title('方差分析F值')

plt.subplot(1, 2, 2)
plt.bar(range(X.shape[1]), selector_mi.scores_)
plt.xlabel('特征索引')
plt.ylabel('互信息')
plt.title('互信息得分')

plt.tight_layout()
plt.show()

F检验 vs 互信息

F检验评估特征与目标之间的线性依赖程度。如果关系是非线性的,F检验可能低估特征的重要性。

互信息可以捕获任意类型的统计依赖关系,包括非线性关系。但由于是非参数方法,需要更多样本才能准确估计。

选择建议:

  • 如果怀疑特征与目标之间存在非线性关系,优先使用互信息
  • 如果特征与目标之间主要是线性关系,F检验更快且效果相当
  • 卡方检验只适用于非负特征(如词频计数)

递归特征消除(RFE)

递归特征消除是一种包装法,它通过递归地训练模型并移除最不重要的特征来进行特征选择。

原理

RFE 的工作流程如下:

  1. 用所有特征训练模型
  2. 获取各特征的重要性权重(如线性模型的系数、树模型的特征重要性)
  3. 移除重要性最低的特征
  4. 用剩余特征重复步骤1-3,直到达到目标特征数量

这个过程不断缩小特征集合,每次迭代都会移除最不重要的特征,最终得到最优特征子集。

基本用法

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 使用逻辑回归作为基模型
estimator = LogisticRegression(max_iter=200)

# RFE 选择2个特征
rfe = RFE(estimator, n_features_to_select=2)
X_selected = rfe.fit_transform(X, y)

print(f"选择后形状: {X_selected.shape}")
print(f"特征排名: {rfe.ranking_}") # 1表示被选中
print(f"是否被选中: {rfe.support_}")

排名说明

  • 排名为1的特征是被选中的
  • 排名为2的特征是在最后一轮被淘汰的
  • 排名为3的特征是在倒数第二轮被淘汰的,依此类推

RFECV:自动选择最优特征数

RFE 需要预先指定保留的特征数量,但最优数量通常未知。RFECV 通过交叉验证自动确定最优特征数:

from sklearn.feature_selection import RFECV
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
import matplotlib.pyplot as plt

iris = load_iris()
X, y = iris.data, iris.target

estimator = LogisticRegression(max_iter=200)

# RFECV 自动选择最优特征数
rfecv = RFECV(
estimator=estimator,
step=1, # 每次移除1个特征
cv=StratifiedKFold(5),
scoring='accuracy'
)
rfecv.fit(X, y)

print(f"最优特征数: {rfecv.n_features_}")
print(f"被选中的特征: {rfecv.support_}")

# 可视化交叉验证结果
plt.figure(figsize=(8, 5))
plt.plot(range(1, len(rfecv.cv_results_['mean_test_score']) + 1),
rfecv.cv_results_['mean_test_score'])
plt.xlabel('特征数量')
plt.ylabel('交叉验证准确率')
plt.title('RFECV 特征数量选择')
plt.axvline(x=rfecv.n_features_, color='r', linestyle='--',
label=f'最优: {rfecv.n_features_}个')
plt.legend()
plt.grid(True)
plt.show()

RFE 的优缺点

优点

  • 考虑特征之间的交互作用
  • 可以与任何能输出特征重要性的模型结合
  • 通常能找到比过滤法更好的特征子集

缺点

  • 计算成本高,需要多次训练模型
  • 结果依赖于基模型的选择
  • 对于大规模特征集可能很慢

适用场景

  • 特征数量适中(几十到几百个)
  • 追求较高的特征选择质量
  • 有足够的时间进行模型训练

使用 SelectFromModel 选择特征

SelectFromModel 是一种嵌入法,它从训练好的模型中提取特征重要性,然后根据阈值选择特征。

原理

  1. 训练一个能输出特征重要性的模型
  2. 获取每个特征的重要性分数
  3. 根据阈值选择重要性高于阈值的特征

与 RFE 不同,SelectFromModel 只训练一次模型,不需要迭代。

基于树模型的特征选择

树模型天然支持特征重要性评估:

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 训练随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

print(f"特征重要性: {rf.feature_importances_}")

# 基于特征重要性选择
selector = SelectFromModel(rf, threshold='mean') # 选择重要性高于均值的特征
X_selected = selector.fit_transform(X, y)

print(f"\n选择后形状: {X_selected.shape}")
print(f"被选中的特征: {selector.get_support()}")

阈值选择

SelectFromModel 支持多种阈值设置方式:

# 方式1:字符串阈值
selector = SelectFromModel(rf, threshold='mean') # 特征重要性的均值
selector = SelectFromModel(rf, threshold='median') # 特征重要性的中位数
selector = SelectFromModel(rf, threshold='1.25*mean') # 均值的1.25倍

# 方式2:数值阈值
selector = SelectFromModel(rf, threshold=0.1) # 重要性大于0.1

# 方式3:限制最大特征数
selector = SelectFromModel(rf, max_features=3) # 最多选3个特征

基于 L1 正则化的特征选择

L1 正则化会产生稀疏解,许多系数会变为零:

from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import SelectFromModel
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# L1 正则化的逻辑回归
l1_lr = LogisticRegression(penalty='l1', solver='saga', max_iter=500, C=0.1)
l1_lr.fit(X, y)

print(f"系数: {l1_lr.coef_}")
print(f"非零系数数量: {(l1_lr.coef_ != 0).sum()}")

# 选择非零系数对应的特征
selector = SelectFromModel(l1_lr, threshold=1e-5)
X_selected = selector.fit_transform(X, y)

print(f"\n选择后形状: {X_selected.shape}")

正则化强度的影响

import numpy as np

C_values = [0.01, 0.1, 1.0, 10.0]

for C in C_values:
lr = LogisticRegression(penalty='l1', solver='saga', max_iter=500, C=C)
selector = SelectFromModel(lr)
X_selected = selector.fit_transform(X, y)
print(f"C={C}: 选择了 {X_selected.shape[1]} 个特征")
  • C 越小(正则化越强),选择的特征越少
  • C 越大(正则化越弱),选择的特征越多

SelectFromModel vs RFE

方面SelectFromModelRFE
训练次数1次多次迭代
计算速度
选择质量依赖于单次训练递归优化,可能更好
适用模型需要输出特征重要性需要输出特征重要性

顺序特征选择(SFS)

顺序特征选择是另一种包装法,它不依赖于模型的特征重要性输出,而是通过实际训练模型来评估特征子集的质量。

原理

前向选择(Forward Selection)

  1. 从空特征集开始
  2. 尝试添加每个未被选择的特征,训练模型并评估性能
  3. 选择使性能提升最大的特征加入特征集
  4. 重复步骤2-3,直到达到目标特征数或性能不再提升

后向选择(Backward Selection)

  1. 从所有特征开始
  2. 尝试移除每个特征,训练模型并评估性能
  3. 移除使性能下降最小的特征
  4. 重复步骤2-3,直到达到目标特征数

基本用法

from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 使用KNN作为基模型
knn = KNeighborsClassifier(n_neighbors=3)

# 前向选择
sfs_forward = SequentialFeatureSelector(
knn,
n_features_to_select=2,
direction='forward',
cv=5
)
X_selected = sfs_forward.fit_transform(X, y)

print(f"前向选择结果形状: {X_selected.shape}")
print(f"被选中的特征: {sfs_forward.get_support()}")

# 后向选择
sfs_backward = SequentialFeatureSelector(
knn,
n_features_to_select=2,
direction='backward',
cv=5
)
X_selected = sfs_backward.fit_transform(X, y)

print(f"后向选择结果形状: {X_selected.shape}")
print(f"被选中的特征: {sfs_backward.get_support()}")

前向 vs 后向选择

from sklearn.datasets import make_classification
import time

# 创建数据集
X, y = make_classification(n_samples=500, n_features=20, n_informative=10, random_state=42)

knn = KNeighborsClassifier(n_neighbors=5)

# 前向选择
start = time.time()
sfs_forward = SequentialFeatureSelector(knn, n_features_to_select=10, direction='forward', cv=5)
sfs_forward.fit(X, y)
time_forward = time.time() - start

# 后向选择
start = time.time()
sfs_backward = SequentialFeatureSelector(knn, n_features_to_select=10, direction='backward', cv=5)
sfs_backward.fit(X, y)
time_backward = time.time() - start

print(f"前向选择时间: {time_forward:.2f}秒")
print(f"后向选择时间: {time_backward:.2f}秒")
print(f"前向选择特征数: {sfs_forward.get_support().sum()}")
print(f"后向选择特征数: {sfs_backward.get_support().sum()}")

选择建议

  • 如果目标特征数远小于总特征数,前向选择更快
  • 如果目标特征数接近总特征数,后向选择更快
  • 前向和后向选择的结果通常不同,可以都尝试

SFS 的特点

优点

  • 不需要模型输出特征重要性
  • 可以与任何模型配合使用
  • 通过交叉验证评估特征子集

缺点

  • 计算成本最高(每次迭代都要训练多个模型)
  • 贪心策略可能错过全局最优

特征选择作为管道的一部分

特征选择应该作为预处理流程的一部分,确保训练集和测试集使用相同的特征选择结果。使用 Pipeline 可以避免数据泄露。

为什么需要 Pipeline?

如果在整个数据集上进行特征选择,然后再划分训练集和测试集,测试集的信息会泄露到特征选择过程中。正确的做法是:

  1. 划分训练集和测试集
  2. 在训练集上进行特征选择
  3. 将相同的特征选择应用到测试集

Pipeline 会自动处理这个过程:

from sklearn.pipeline import Pipeline
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建管道
pipeline = Pipeline([
('selector', SelectKBest(f_classif, k=2)),
('classifier', LogisticRegression(max_iter=200))
])

# 训练(特征选择自动只在训练集上进行)
pipeline.fit(X_train, y_train)

# 预测(使用相同的特征选择)
accuracy = pipeline.score(X_test, y_test)
print(f"测试集准确率: {accuracy:.3f}")

# 交叉验证(每次fold都会重新进行特征选择)
cv_scores = cross_val_score(pipeline, X, y, cv=5)
print(f"交叉验证准确率: {cv_scores.mean():.3f}")

结合网格搜索选择最优特征数

from sklearn.model_selection import GridSearchCV

pipeline = Pipeline([
('selector', SelectKBest(f_classif)),
('classifier', LogisticRegression(max_iter=200))
])

# 搜索最优特征数
param_grid = {
'selector__k': [1, 2, 3, 4],
'classifier__C': [0.1, 1.0, 10.0]
}

grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)

print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.3f}")

完整的特征选择管道示例

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
import pandas as pd
import numpy as np

# 创建模拟数据
np.random.seed(42)
n_samples = 1000

df = pd.DataFrame({
'age': np.random.randint(18, 70, n_samples),
'income': np.random.randint(20000, 150000, n_samples),
'education': np.random.choice(['高中', '本科', '硕士', '博士'], n_samples),
'city': np.random.choice(['北京', '上海', '广州', '深圳'], n_samples),
# 添加一些噪声特征
'noise1': np.random.randn(n_samples),
'noise2': np.random.randn(n_samples),
'noise3': np.random.randn(n_samples)
})
y = np.random.randint(0, 2, n_samples)

# 定义数值列和类别列
numeric_features = ['age', 'income', 'noise1', 'noise2', 'noise3']
categorical_features = ['education', 'city']

# 预处理器
preprocessor = ColumnTransformer([
('num', StandardScaler(), numeric_features),
('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)
])

# 完整管道
pipeline = Pipeline([
('preprocessor', preprocessor),
('selector', SelectFromModel(RandomForestClassifier(n_estimators=50, random_state=42), threshold='median')),
('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# 交叉验证评估
cv_scores = cross_val_score(pipeline, df, y, cv=5)
print(f"交叉验证准确率: {cv_scores.mean():.3f} (+/- {cv_scores.std()*2:.3f})")

特征选择方法对比

方法类型速度效果适用场景
VarianceThreshold过滤法最快一般快速移除无用特征
SelectKBest过滤法中等一般场景,特征较少
RFE包装法追求高质量选择
RFECV包装法最慢最好自动选择特征数
SelectFromModel嵌入法有预训练模型时
SFS包装法很慢模型不输出特征重要性时

最佳实践

选择建议

  1. 特征数很多(>1000)

    • 先用 VarianceThreshold 快速过滤
    • 再用 SelectKBest 或 SelectFromModel 进一步筛选
  2. 特征数适中(10-1000)

    • 用 SelectFromModel 配合树模型或 L1 正则化
    • 追求最优效果可用 RFE 或 RFECV
  3. 模型不输出特征重要性

    • 使用 SFS,但注意计算成本

避免常见错误

错误1:在整个数据集上选择特征

# 错误做法
selector = SelectKBest(k=10)
X_selected = selector.fit_transform(X, y) # 用了全部数据
X_train, X_test, y_train, y_test = train_test_split(X_selected, y, ...)

# 正确做法
X_train, X_test, y_train, y_test = train_test_split(X, y, ...)
selector = SelectKBest(k=10)
X_train_selected = selector.fit_transform(X_train, y_train)
X_test_selected = selector.transform(X_test)

错误2:过度选择特征

特征选择的目标是去除噪声,但如果选择过多特征,可能仍然包含冗余信息。建议:

  • 使用 RFECV 自动确定最优特征数
  • 交叉验证评估不同特征数的效果

错误3:忽视特征工程

特征选择不能替代特征工程。好的特征比选择特征更重要。建议:

  • 先进行特征工程,创建有意义的特征
  • 再进行特征选择,去除冗余

小结

特征选择是机器学习流程中的重要环节:

  1. 理解方法类型:过滤法快但简单,包装法慢但效果好,嵌入法介于两者之间
  2. 结合 Pipeline 使用:避免数据泄露,确保流程一致性
  3. 没有万能方法:根据数据特点、特征数量和计算资源选择合适的方法
  4. 交叉验证评估:通过交叉验证确定最优特征数和选择策略
  5. 配合特征工程:特征选择是锦上添花,好的特征才是根本

在下一章中,我们将学习分类算法,了解如何使用选择好的特征进行模型训练。