特征选择三剑客:前向、后向、全子集,哪种更适合你?
本文面向机器学习入门者,用大白话 + 代码 + 可视化,带你搞清楚三种经典特征选择方法的原理与区别。
为什么要做特征选择?
假设你要预测房价,手里有 50 个特征:面积、楼层、装修年份、附近餐厅数量、业主星座......
把所有特征全塞进模型,真的好吗?不一定。
冗余特征的三大危害:
- 过拟合:特征越多,模型越容易"死记硬背"训练数据
- 训练变慢:计算量随特征数指数级上升
- 可解释性差:50 个特征的模型,谁能看懂?
特征选择就是在回答一个问题:哪些特征真正有用?
三种经典方法一览
| 方法 | 核心思路 | 复杂度 | 适合场景 |
|---|---|---|---|
| 全子集法 | 枚举所有组合,找全局最优 | O(2ᵖ) | 特征数 < 20 |
| 前向筛选 | 从空集出发,逐步加特征 | O(p²) | 特征数较多 |
| 后向消除 | 从全集出发,逐步删特征 | O(p²) | 特征数中等 |
p = 特征总数。特征一多,全子集直接"爆炸",前向/后向就是折中方案。
方法一:全子集法(Best Subset Selection)
原理
枚举所有 2ᵖ 种特征组合,每种组合训练一个模型,选出指标最好的那个。
ini
特征数 p=3,所有组合:
∅
{x₁}, {x₂}, {x₃}
{x₁,x₂}, {x₁,x₃}, {x₂,x₃}
{x₁,x₂,x₃}
共 2³ = 8 种
优缺点
✅ 保证找到全局最优子集
❌ p=30 时有 10 亿种组合,根本跑不完
❌ 大数据集上完全不可行
Python 示例
python
from itertools import combinations
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
import numpy as np
def best_subset_selection(X, y, max_features=None):
n_features = X.shape[1]
if max_features is None:
max_features = n_features
best_score = -np.inf
best_subset = None
for k in range(1, max_features + 1):
for subset in combinations(range(n_features), k):
X_sub = X[:, list(subset)]
model = LinearRegression()
score = cross_val_score(model, X_sub, y, cv=5, scoring='r2').mean()
if score > best_score:
best_score = score
best_subset = subset
print(f"最优子集: 特征 {best_subset},R² = {best_score:.4f}")
return best_subset
# 用法
# best_subset_selection(X_train, y_train, max_features=5)
方法二:前向筛选(Forward Selection)
原理
从空集出发,每次从剩余特征里挑出加进去后模型表现最好的那个,直到满足停止条件。
ini
初始:S = {}
第1轮:
试加 x₁ → R²=0.52
试加 x₂ → R²=0.71 ← 最优
试加 x₃ → R²=0.48
→ 加入 x₂,S = {x₂}
第2轮:
试加 x₁ → R²=0.79 ← 最优
试加 x₃ → R²=0.73
→ 加入 x₁,S = {x₂, x₁}
第3轮:
试加 x₃ → R²=0.80,提升微小,停止
Python 示例
python
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
import numpy as np
def forward_selection(X, y, significance_threshold=0.01):
"""
前向筛选:每轮加入最优特征,直到改善不显著
"""
n_features = X.shape[1]
selected = []
remaining = list(range(n_features))
current_score = -np.inf
while remaining:
best_score = -np.inf
best_feature = None
for feature in remaining:
candidate = selected + [feature]
X_sub = X[:, candidate]
model = LinearRegression()
score = cross_val_score(model, X_sub, y, cv=5, scoring='r2').mean()
if score > best_score:
best_score = score
best_feature = feature
# 改善幅度不足则停止
if best_score - current_score < significance_threshold:
break
selected.append(best_feature)
remaining.remove(best_feature)
current_score = best_score
print(f"加入特征 x{best_feature},当前 R² = {current_score:.4f}")
print(f"\n最终选中特征: {selected}")
return selected
# 用法
# forward_selection(X_train, y_train)
Scikit-learn 一行搞定
ini
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.linear_model import LinearRegression
model = LinearRegression()
sfs = SequentialFeatureSelector(
model,
n_features_to_select=5, # 选 5 个特征
direction='forward', # 前向
scoring='r2',
cv=5
)
sfs.fit(X_train, y_train)
print("选中的特征索引:", sfs.get_support(indices=True))
方法三:后向消除(Backward Elimination)
原理
从全部特征 出发,每次删掉去掉后模型损失最小(或统计上最不显著)的那个特征,直到满足停止条件。
ini
初始:S = {x₁, x₂, x₃, x₄, x₅},R²=0.88
第1轮(删哪个损失最小?):
删 x₁ → R²=0.87,损失 0.01
删 x₂ → R²=0.88,损失 0.00 ← 最小
删 x₃ → R²=0.82,损失 0.06
...
→ 删除 x₂,S = {x₁, x₃, x₄, x₅}
继续直到删除任何特征都会显著降低性能
Python 示例
ini
def backward_elimination(X, y, significance_threshold=0.01):
"""
后向消除:每轮删除影响最小的特征
"""
n_features = X.shape[1]
selected = list(range(n_features))
# 计算初始得分
model = LinearRegression()
current_score = cross_val_score(
model, X[:, selected], y, cv=5, scoring='r2'
).mean()
print(f"初始 R²(全特征)= {current_score:.4f}")
while len(selected) > 1:
worst_score_drop = np.inf
worst_feature = None
for feature in selected:
candidate = [f for f in selected if f != feature]
X_sub = X[:, candidate]
score = cross_val_score(model, X_sub, y, cv=5, scoring='r2').mean()
drop = current_score - score
if drop < worst_score_drop:
worst_score_drop = drop
worst_feature = feature
best_score_without = score
# 删除该特征几乎不影响性能
if worst_score_drop < significance_threshold:
selected.remove(worst_feature)
current_score = best_score_without
print(f"删除特征 x{worst_feature},R² = {current_score:.4f}")
else:
break
print(f"\n最终保留特征: {selected}")
return selected
# 用法
# backward_elimination(X_train, y_train)
Scikit-learn 版
ini
sfs_backward = SequentialFeatureSelector(
LinearRegression(),
n_features_to_select=5,
direction='backward', # 改成 backward 即可
scoring='r2',
cv=5
)
sfs_backward.fit(X_train, y_train)
三种方法的直观对比
markdown
全子集:★★★★★(精度) ★☆☆☆☆(速度)
穷举所有路径,找到最优,但太慢
前向: ★★★☆☆(精度) ★★★★☆(速度)
从空白出发,贪心地往里加
风险:加进去的特征不能撤回
后向: ★★★★☆(精度) ★★★☆☆(速度)
从全集出发,贪心地往外删
优势:一开始看到了特征间的交互
风险:初始特征多时第一轮就很慢
一个记忆口诀:
- 特征少(< 20)→ 全子集,追求最优
- 特征多、从头建模 → 前向,从简到繁
- 特征多、已有基线模型 → 后向,从繁到简
实战:用真实数据集跑一遍
python
import numpy as np
import pandas as pd
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.model_selection import cross_val_score
# 加载糖尿病数据集(10 个特征)
data = load_diabetes()
X, y = data.data, data.target
feature_names = data.feature_names
model = LinearRegression()
# 基线:所有特征
baseline = cross_val_score(model, X, y, cv=5, scoring='r2').mean()
print(f"全特征基线 R²: {baseline:.4f}")
# 前向筛选 5 个特征
sfs_fwd = SequentialFeatureSelector(model, n_features_to_select=5,
direction='forward', cv=5)
sfs_fwd.fit(X, y)
X_fwd = sfs_fwd.transform(X)
score_fwd = cross_val_score(model, X_fwd, y, cv=5, scoring='r2').mean()
print(f"前向 5 特征 R²: {score_fwd:.4f}")
print(f"选中: {[feature_names[i] for i in sfs_fwd.get_support(indices=True)]}")
# 后向消除 5 个特征
sfs_bwd = SequentialFeatureSelector(model, n_features_to_select=5,
direction='backward', cv=5)
sfs_bwd.fit(X, y)
X_bwd = sfs_bwd.transform(X)
score_bwd = cross_val_score(model, X_bwd, y, cv=5, scoring='r2').mean()
print(f"后向 5 特征 R²: {score_bwd:.4f}")
print(f"选中: {[feature_names[i] for i in sfs_bwd.get_support(indices=True)]}")
典型输出:
less
全特征基线 R²: 0.4823
前向 5 特征 R²: 0.4801
后向 5 特征 R²: 0.4812
选中(前向): ['bmi', 's5', 'bp', 's3', 's1']
选中(后向): ['bmi', 's5', 'bp', 's3', 's4']
用一半特征,几乎保住了全部性能------这就是特征选择的价值。
常见坑与注意事项
坑 1:在全数据上选特征,再做交叉验证
这是数据泄露!特征选择必须在每个 CV fold 内部做:
ini
from sklearn.pipeline import Pipeline
# ✅ 正确做法:把特征选择放进 Pipeline
pipe = Pipeline([
('selector', SequentialFeatureSelector(model, n_features_to_select=5,
direction='forward')),
('model', LinearRegression())
])
score = cross_val_score(pipe, X, y, cv=5, scoring='r2').mean()
坑 2:前向/后向不一定给出相同结果
前向和后向是贪心算法,路径不同,结果可能不一样。如果结论差异很大,说明数据中特征间有较强交互,建议用 LASSO 等正则化方法替代。
坑 3:停止条件选不好
n_features_to_select太小:丢失重要特征- 用 p 值作为停止条件时注意多重比较问题(Bonferroni 校正)
总结
| 全子集 | 前向筛选 | 后向消除 | |
|---|---|---|---|
| 全局最优 | ✅ | ❌ | ❌ |
| 速度 | 极慢 | 较快 | 较快 |
| 可用特征数 | < 20 | 几百 | 几十~几百 |
| 能捕捉特征交互 | ✅ | ❌ | ✅(初期) |
| sklearn 支持 | 手写 | ✅ | ✅ |
三种方法各有适用场景,没有"最好的",只有"最合适的"。实际工程中,前向筛选因为速度快、直观,是最常用的起点;全子集适合小数据集的严格研究场景;后向消除在已有完整模型、需要精简时更自然。
如果觉得有帮助,点个赞 👍 欢迎评论区讨论~
下一篇:基于模型的特征重要性:Random Forest、LASSO、SHAP 怎么选?