特征选择
特征选择是机器学习流程中至关重要的一步。它从原始特征中选择最相关的子集,目的是提高模型性能、减少过拟合、加快训练速度并增强模型可解释性。在高维数据场景下,特征选择尤为重要——并非所有特征都对预测任务有帮助,冗余或无关的特征不仅会增加计算成本,还可能引入噪声,降低模型的泛化能力。
为什么需要特征选择?
维度问题
在实际项目中,数据往往包含大量特征,但这些特征并非都有价值。特征过多会带来一系列问题:
维度灾难:随着特征维度增加,数据在空间中变得稀疏,模型需要更多的样本才能学到有效的模式。对于固定数量的样本,特征越多,模型越容易过拟合。
计算成本:更多特征意味着更长的训练时间和更多的内存消耗。对于大规模数据集,这种影响尤为明显。
可解释性下降:特征过多使得模型难以解释,难以识别哪些因素真正影响预测结果。
特征选择 vs 特征提取
特征选择和特征提取都是降维的方法,但本质不同:
| 方面 | 特征选择 | 特征提取 |
|---|---|---|
| 方式 | 从原始特征中选择子集 | 创建新的特征组合 |
| 特征含义 | 保持原始特征含义 | 新特征难以解释 |
| 方法 | 过滤、包装、嵌入 | PCA、LDA等 |
| 可解释性 | 高 | 低 |
特征选择保留了原始特征的可解释性,这在很多业务场景中非常重要。例如,在信用评分模型中,了解"收入"、"负债率"等具体特征的影响,比解释一个抽象的主成分更有意义。
特征选择方法分类
特征选择方法通常分为三类:
过滤法(Filter Methods):在训练模型之前,基于统计指标对特征进行评分和筛选。计算效率高,但不考虑特征与模型之间的交互。
包装法(Wrapper Methods):将特征选择视为搜索问题,通过训练模型来评估特征子集的质量。效果通常更好,但计算成本高。
嵌入法(Embedded Methods):特征选择嵌入到模型训练过程中,模型自动学习特征的重要性。结合了前两者的优点。
移除低方差特征
这是最简单、最快速的特征选择方法。它基于一个直观假设:如果一个特征在所有样本中几乎取相同的值,那么它对区分不同类别或预测目标几乎没有帮助。
原理
VarianceThreshold 移除所有方差低于阈值的特征。默认情况下,它会移除所有零方差特征——即在所有样本中取值完全相同的特征。
对于布尔型特征(取值为0或1),其方差为:
其中 是特征取值为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_regression | F值 | 线性关系 |
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-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 是一种嵌入法,它从训练好的模型中提取特征重要性,然后根据阈值选择特征。
原理
- 训练一个能输出特征重要性的模型
- 获取每个特征的重要性分数
- 根据阈值选择重要性高于阈值的特征
与 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
| 方面 | SelectFromModel | RFE |
|---|---|---|
| 训练次数 | 1次 | 多次迭代 |
| 计算速度 | 快 | 慢 |
| 选择质量 | 依赖于单次训练 | 递归优化,可能更好 |
| 适用模型 | 需要输出特征重要性 | 需要输出特征重要性 |
顺序特征选择(SFS)
顺序特征选择是另一种包装法,它不依赖于模型的特征重要性输出,而是通过实际训练模型来评估特征子集的质量。
原理
前向选择(Forward Selection):
- 从空特征集开始
- 尝试添加每个未被选择的特征,训练模型并评估性能
- 选择使性能提升最大的特征加入特征集
- 重复步骤2-3,直到达到目标特征数或性能不再提升
后向选择(Backward Selection):
- 从所有特征开始
- 尝试移除每个特征,训练模型并评估性能
- 移除使性能下降最小的特征
- 重复步骤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?
如果在整个数据集上进行特征选择,然后再划分训练集和测试集,测试集的信息会泄露到特征选择过程中。正确的做法是:
- 划分训练集和测试集
- 在训练集上进行特征选择
- 将相同的特征选择应用到测试集
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 | 包装法 | 很慢 | 好 | 模型不输出特征重要性时 |
最佳实践
选择建议
-
特征数很多(>1000):
- 先用 VarianceThreshold 快速过滤
- 再用 SelectKBest 或 SelectFromModel 进一步筛选
-
特征数适中(10-1000):
- 用 SelectFromModel 配合树模型或 L1 正则化
- 追求最优效果可用 RFE 或 RFECV
-
模型不输出特征重要性:
- 使用 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:忽视特征工程
特征选择不能替代特征工程。好的特征比选择特征更重要。建议:
- 先进行特征工程,创建有意义的特征
- 再进行特征选择,去除冗余
小结
特征选择是机器学习流程中的重要环节:
- 理解方法类型:过滤法快但简单,包装法慢但效果好,嵌入法介于两者之间
- 结合 Pipeline 使用:避免数据泄露,确保流程一致性
- 没有万能方法:根据数据特点、特征数量和计算资源选择合适的方法
- 交叉验证评估:通过交叉验证确定最优特征数和选择策略
- 配合特征工程:特征选择是锦上添花,好的特征才是根本
在下一章中,我们将学习分类算法,了解如何使用选择好的特征进行模型训练。