特征选择三剑客:前向、后向、全子集,哪种更适合你?

特征选择三剑客:前向、后向、全子集,哪种更适合你?

本文面向机器学习入门者,用大白话 + 代码 + 可视化,带你搞清楚三种经典特征选择方法的原理与区别。


为什么要做特征选择?

假设你要预测房价,手里有 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 怎么选?

相关推荐
起个破名想半天了2 小时前
算法与数据结构之Floyd算法
数据结构·算法
千寻girling2 小时前
机器学习 | 无监督学习算法(了解) | 尚硅谷学习
学习·算法·机器学习
小七在进步2 小时前
数据结构:线性表之顺序表
c语言·数据结构·算法
张小九992 小时前
【酶改造】如何利用进化信息快速定位蛋白质突变热点?EVcouplings 保姆级教程
算法
Never_love_MCI!2 小时前
洛谷P15799 [GESP202603 五级] 找数 题解
数据结构·c++·算法
仍然.2 小时前
算法题目---BFS解决FloodFill算法问题
算法·宽度优先
Sirius Wu3 小时前
MoE与Fengyu-Dense_架构对比及训练方案
人工智能·深度学习·算法·机器学习·语言模型·架构
却道天凉_好个秋3 小时前
HEVC(一):环路滤波
人工智能·算法·计算机视觉·环路滤波
8Qi83 小时前
LeetCode 300 & 674:最长递增子序列 vs 最长连续递增子序列
算法·leetcode·职场和发展·动态规划