查看全课大纲(第 23 / 35 节)

包裹式特征选择方法

约 4 分钟

小象实战讲义 · 特征工程精讲与实践

在上一节我们学习了过滤式特征选择方法,它基于统计指标评估单个特征的重要性。但过滤法有一个明显的局限性:没有考虑特征之间的相互作用。本节我们将介绍包裹式特征选择方法,它会将特征选择看作一个特征子集搜索问题,直接使用机器学习模型来评估不同特征子集的效果,从而找到最优的特征组合。

💡 核心导读

  • 包裹式特征选择的核心思想:将特征选择过程”包裹”在机器学习模型训练中,通过模型性能来评估特征子集的好坏
  • 三种经典搜索策略:前向选择(逐步添加特征)、后向删除(逐步删除特征)、穷举搜索(遍历所有可能组合)
  • 包裹法的优缺点:考虑特征交互但计算成本较高,适合特征数适中的场景

包裹式特征选择的基本原理

包裹式特征选择(Wrapper Methods)的核心思想是将特征选择过程”包裹”在机器学习模型的训练过程中。与过滤法不同,包裹法不是单独评估每个特征,而是评估不同特征子集组合的整体效果。

工作流程:

  1. 定义一个搜索策略(如前向选择、后向删除等)
  2. 生成一个候选特征子集
  3. 使用该特征子集训练模型
  4. 评估模型性能(如准确率、AUC等)
  5. 根据性能决定是否接受该特征子集
  6. 重复步骤2-5,直到满足停止条件

这种方法的优势在于它考虑了特征之间的相互作用,因为模型是在特征子集上整体训练的。但缺点也很明显:计算成本高,因为需要反复训练模型。

经典包裹式方法实现

我们将使用乳腺癌数据集来演示三种经典的包裹式特征选择方法。首先准备数据:

import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier

# 加载乳腺癌数据集
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"原始特征数: {X_train.shape[1]}")
print(f"训练集样本数: {X_train.shape[0]}, 测试集样本数: {X_test.shape[0]}")
原始特征数: 30
训练集样本数: 455, 测试集样本数: 114

1. 前向选择(Forward Selection)

前向选择是一种贪心算法,从空特征集开始,逐步添加对模型性能提升最大的特征。原课程使用 mlxtend 库的 SequentialFeatureSelector 实现,下面我们手写它的核心逻辑——这恰恰是把机制看得最清楚的方式:

import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 手写前向选择(演示用单次划分评估,工程实践建议交叉验证)
def forward_selection(X_train, y_train, X_val, y_val, k):
    selected = []
    remaining = list(X_train.columns)
    while len(selected) < k:
        best_feat, best_acc = None, -1.0
        for feat in remaining:
            cols = selected + [feat]
            model = RandomForestClassifier(n_estimators=10, random_state=42)
            model.fit(X_train[cols], y_train)
            acc = accuracy_score(y_val, model.predict(X_val[cols]))
            if acc > best_acc:
                best_acc, best_feat = acc, feat
        selected.append(best_feat)
        remaining.remove(best_feat)
    return selected

selected_forward = forward_selection(X_train, y_train, X_val, y_val, k=5)
print("前向选择选中的特征:")
for i, feat in enumerate(selected_forward, 1):
    print(f"{i}. {feat}")
前向选择选中的特征:
1. worst area
2. mean smoothness
3. mean concavity
4. mean compactness
5. area error

前向选择的工作原理:

  1. 从空特征集开始
  2. 依次尝试添加每个未选中的特征
  3. 评估添加该特征后的模型性能
  4. 选择性能提升最大的特征加入特征集
  5. 重复步骤2-4,直到达到预设的特征数量或性能不再提升

2. 后向删除(Backward Elimination)

后向删除与前向选择相反,从全量特征开始,逐步删除对模型性能影响最小的特征:

import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 手写后向删除(每轮尝试删除一个特征,保留使验证性能最高的组合)
def backward_elimination(X_train, y_train, X_val, y_val, k):
    remaining = list(X_train.columns)
    while len(remaining) > k:
        worst_feat, worst_acc = None, -1.0
        for feat in remaining:
            cols = [c for c in remaining if c != feat]
            model = RandomForestClassifier(n_estimators=10, random_state=42)
            model.fit(X_train[cols], y_train)
            acc = accuracy_score(y_val, model.predict(X_val[cols]))
            if acc > worst_acc:
                worst_acc, worst_feat = acc, feat
        remaining.remove(worst_feat)
    return remaining

selected_backward = backward_elimination(X_train, y_train, X_val, y_val, k=10)
print(f"后向删除保留的{len(selected_backward)}个特征:")
for i, feat in enumerate(selected_backward, 1):
    print(f"{i}. {feat}")
后向删除保留的10个特征:
1. mean radius
2. mean symmetry
3. radius error
4. texture error
5. perimeter error
6. smoothness error
7. concave points error
8. symmetry error
9. worst area
10. worst concavity

后向删除的工作原理:

  1. 从所有特征开始
  2. 依次尝试删除每个特征
  3. 评估删除该特征后的模型性能
  4. 删除对性能影响最小的特征
  5. 重复步骤2-4,直到达到预设的特征数量或性能开始显著下降

穷举搜索会尝试所有可能的特征组合,找到性能最优的特征子集。这种方法计算成本极高,只适用于特征数很少的情况。原课程使用 mlxtend 的 ExhaustiveFeatureSelector,用标准库 itertools.combinations 就能等价实现:

import pandas as pd
from itertools import combinations
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 穷举搜索:只取前 6 个特征、组合规模 2~4,控制计算量
X_small = X_train.iloc[:, :6]
cols_small = list(X_small.columns)
best_combo, best_acc = None, -1.0
for r in (2, 3, 4):
    for combo in combinations(cols_small, r):
        model = RandomForestClassifier(n_estimators=10, random_state=42)
        model.fit(X_train[list(combo)], y_train)
        acc = accuracy_score(y_val, model.predict(X_val[list(combo)]))
        if acc > best_acc:
            best_acc, best_combo = acc, list(combo)

print(f"穷举搜索找到的最佳特征组合(从{len(cols_small)}个特征中):")
for i, feat in enumerate(best_combo, 1):
    print(f"{i}. {feat}")
print(f"最佳准确率: {best_acc:.4f}")
穷举搜索找到的最佳特征组合(从6个特征中):
1. mean radius
2. mean texture
3. mean perimeter
4. mean compactness
最佳准确率: 0.9649

除了上述三种经典的搜索策略,包裹式家族中还有一种非常直观的实验性方法——通过打乱单个特征的值来观察模型性能的下降幅度(特征乱序法,也即课程后文的「混序法」),我们将在本章最后一节结合代码详细学习它。

包裹式方法的优缺点与适用场景

优点

  1. 考虑特征交互:直接使用模型评估,能捕捉特征之间的相互作用
  2. 找到最优组合:通过搜索策略找到性能最优的特征子集
  3. 与模型强相关:选择出的特征对特定模型是最优的

缺点

  1. 计算成本高:需要反复训练模型,特征数多时计算量巨大
  2. 容易过拟合:在训练集上搜索最优特征子集,可能在测试集上表现不佳
  3. 随机性影响:模型训练本身的随机性会影响特征选择结果

适用场景

  • 特征数量适中(通常少于50个)
  • 计算资源充足
  • 对模型性能要求极高,愿意为小幅提升付出计算代价
  • 特征之间存在复杂的相互作用

📝 动手练一练

  1. 概念辨析:过滤式特征选择和包裹式特征选择的核心区别是什么?在什么情况下你会选择使用包裹式方法?
👉 点击查看参考答案

核心区别:

  • 过滤式:基于统计指标(如方差、相关系数、卡方检验等)单独评估每个特征,不考虑特征之间的相互作用,计算效率高
  • 包裹式:使用机器学习模型评估特征子集的整体性能,考虑特征交互,但计算成本高

选择包裹式的情况:

  • 特征数量适中(少于50个),计算资源充足
  • 特征之间存在复杂的相互作用,过滤法可能漏掉重要组合
  • 对模型性能有极致要求,愿意为小幅性能提升付出计算代价
  • 需要为特定模型(如随机森林、神经网络)找到最优特征子集
  1. 代码实践:修改前向选择的代码,尝试选择不同数量的特征(如3个、8个、15个),观察选中的特征有何变化,并思考如何确定最优的特征数量?
👉 点击查看参考答案
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

def forward_selection(X_train, y_train, X_val, y_val, k):
    selected = []
    remaining = list(X_train.columns)
    while len(selected) < k:
        best_feat, best_acc = None, -1.0
        for feat in remaining:
            cols = selected + [feat]
            model = RandomForestClassifier(n_estimators=10, random_state=42)
            model.fit(X_train[cols], y_train)
            acc = accuracy_score(y_val, model.predict(X_val[cols]))
            if acc > best_acc:
                best_acc, best_feat = acc, feat
        selected.append(best_feat)
        remaining.remove(best_feat)
    return selected

# 以 k=3 为例(读者可自行把 k 改成 8、15 观察选中的特征有何变化)
selected_3 = forward_selection(X_train, y_train, X_val, y_val, k=3)
print("选择3个特征时的结果:")
for i, feat in enumerate(selected_3, 1):
    print(f"  {i}. {feat}")

# 确定最优特征数量的方法:
# 1. 绘制特征数量 vs 模型性能曲线,找到性能拐点
# 2. 使用验证集或交叉验证评估不同特征数量的泛化性能
# 3. 考虑业务约束(如计算成本、可解释性要求)
选择3个特征时的结果:
  1. worst area
  2. mean smoothness
  3. mean concavity

通过实验可以发现:

  • 选择较少的特征(如3个)时,选中的通常是最重要的核心特征
  • 随着特征数量增加,会逐步加入次重要的特征
  • 存在一个”拐点”,超过该点后增加特征带来的性能提升很小
  • 最优特征数量需要平衡性能、计算成本和模型复杂度

本章小结

本节我们深入学习了包裹式特征选择方法,这是特征工程中考虑特征相互作用的重要技术。包裹法通过将特征选择过程”包裹”在模型训练中,能够找到对特定模型最优的特征子集。

📋 行动清单

  • 在本地运行包裹式特征选择的代码,理解前向选择、后向删除和穷举搜索的工作原理
  • 思考在你们当前的项目中,哪些场景适合使用包裹式特征选择,并设计实验验证
  • 记录不同特征选择方法(过滤式、包裹式)的计算时间和性能差异,建立选择方法论

包裹式特征选择虽然计算成本较高,但在特征数量适中、特征交互复杂、对性能要求极高的场景下,它往往能带来显著的性能提升。下一节我们将学习嵌入式特征选择方法,它通过模型正则化来自动完成特征选择。

—— 小象教研组