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

混序法与混合法特征选择

约 4 分钟

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

在特征选择的工具箱中,除了基于统计的过滤法、基于模型迭代的包裹法和基于正则化的嵌入法,还有两种非常直观且实用的实验性方法:混序法和混合法。本节我们将学习如何通过打乱特征值来评估其重要性,以及如何将多种选择策略组合使用,从而更稳健地筛选出对模型最有价值的特征。

💡 核心导读

  • 混序法思想:通过打乱某个特征的值,观察模型性能(如AUC)的下降幅度,来量化该特征的重要性。
  • 混序法实操:使用自研工具 feature_shuffle_rf 对乳腺癌数据集进行特征重要性评估。
  • 混合法思想:综合运用多种特征选择方法(如模型重要度、递归特征消除/增加),形成更可靠的筛选策略。
  • 混合法实操:使用 RFE 和自研的递归特征消除/增加工具,演示混合选择流程。

5.1 混序法:通过特征打乱评估重要性

混序法(Feature Shuffling)是一种基于实验的特征重要性评估方法。其核心思想非常直观:如果一个特征对模型预测至关重要,那么将其值随机打乱(Shuffle)后,模型性能应该会显著下降。

原理拆解:

  1. 正常建模:使用原始特征训练模型,得到一个基准性能(如AUC)。
  2. 特征打乱:针对待评估的某个特征,将其在训练集和测试集中的值分别进行随机重排。这相当于切断了该特征与样本标签之间的真实关联。
  3. 性能对比:使用打乱后的特征重新训练模型(或仅对测试集进行预测),计算新的性能指标。
  4. 重要性量化:计算性能指标的下降幅度(如 AUC_原始 - AUC_打乱后)。下降幅度越大,说明该特征越重要。

这种方法不依赖于特定模型的内部结构(如决策树的分裂增益或线性模型的系数),是一种与模型相对独立的评估方式,结果易于解释。

5.1.1 混序法代码实践

我们使用课程提供的工具 feature_shuffle_rf(位于 utils 目录)在经典的乳腺癌数据集上进行演示。该工具内部使用随机森林作为评估模型。

import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
# 注意:实际工具函数 feature_shuffle 需要从 utils 导入,此处为演示逻辑,我们模拟其核心过程。

# 加载数据并构造DataFrame
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(df.drop('target', axis=1), 
                                                    df['target'], 
                                                    test_size=0.2, 
                                                    random_state=42)
print(f"训练集形状: {X_train.shape}, 测试集形状: {X_test.shape}")
训练集形状: (455, 30), 测试集形状: (114, 30)

接下来,我们模拟混序法的核心计算逻辑。为了演示清晰,我们使用一个极简的示例数据。

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

# 构造一个极简的演示数据(8个样本,3个特征)
X_demo = pd.DataFrame({
    'feature_A': [5, 1, 8, 2, 9, 3, 4, 7],
    'feature_B': [0.3, 0.1, 0.5, 0.2, 0.6, 0.15, 0.25, 0.45],
    'feature_C': [10, 20, 30, 70, 80, 90, 40, 85]  # 这个特征与标签强相关
})
# 构造一个简单的二分类标签,与 feature_C 强相关
y_demo = pd.Series([0, 0, 0, 1, 1, 1, 0, 1])

# 划分训练测试(前6个作为训练,最后2个作为测试,两部分都包含两个类别)
X_train_demo = X_demo.iloc[:6].copy()
y_train_demo = y_demo.iloc[:6].copy()
X_test_demo = X_demo.iloc[6:].copy()
y_test_demo = y_demo.iloc[6:].copy()

print("演示数据 - 训练特征:")
print(X_train_demo)
print("\n演示数据 - 训练标签:")
print(y_train_demo.values)
print("\n演示数据 - 测试特征:")
print(X_test_demo)
print("\n演示数据 - 测试标签:")
print(y_test_demo.values)
演示数据 - 训练特征:
   feature_A  feature_B  feature_C
0          5       0.30         10
1          1       0.10         20
2          8       0.50         30
3          2       0.20         70
4          9       0.60         80
5          3       0.15         90

演示数据 - 训练标签:
[0 0 0 1 1 1]

演示数据 - 测试特征:
   feature_A  feature_B  feature_C
6          4       0.25         40
7          7       0.45         85

演示数据 - 测试标签:
[0 1]
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

# 构造与上节相同的极简演示数据(8 个样本,3 个特征,feature_C 与标签强相关)
X_demo = pd.DataFrame({
    'feature_A': [5, 1, 8, 2, 9, 3, 4, 7],
    'feature_B': [0.3, 0.1, 0.5, 0.2, 0.6, 0.15, 0.25, 0.45],
    'feature_C': [10, 20, 30, 70, 80, 90, 40, 85]
})
y_demo = pd.Series([0, 0, 0, 1, 1, 1, 0, 1])
X_train_demo = X_demo.iloc[:6].copy()
y_train_demo = y_demo.iloc[:6].copy()
X_test_demo = X_demo.iloc[6:].copy()
y_test_demo = y_demo.iloc[6:].copy()

# 初始化随机森林模型
rf_model = RandomForestClassifier(n_estimators=10, random_state=42)

# 1. 计算基准性能(使用原始特征)
rf_model.fit(X_train_demo, y_train_demo)
y_pred_proba_original = rf_model.predict_proba(X_test_demo)[:, 1]
auc_original = roc_auc_score(y_test_demo, y_pred_proba_original)
print(f"基准模型 AUC: {auc_original:.4f}")

# 2. 对每个特征进行乱序并评估
feature_importance_shuffle = {}
for col in X_train_demo.columns:
    # 复制数据,避免污染原始数据
    X_train_shuffled = X_train_demo.copy()
    X_test_shuffled = X_test_demo.copy()
    
    # 打乱当前特征在训练集和测试集中的值
    # 注意:原课工具 feature_shuffle_rf 的口径是「只训练一次基准模型,
    # 逐个打乱特征列后直接预测(不再重新训练)」,对比 AUC 下降幅度。
    # 本演示为展示完整流程,采用「打乱后重新拟合再预测」的变体,
    # 两种做法的核心判据相同:重要特征被打乱后 AUC 显著下降。
    X_train_shuffled[col] = np.random.RandomState(seed=42).permutation(X_train_shuffled[col].values)
    X_test_shuffled[col] = np.random.RandomState(seed=42).permutation(X_test_shuffled[col].values)
    
    # 使用打乱后的特征重新训练和预测
    rf_model.fit(X_train_shuffled, y_train_demo)
    y_pred_proba_shuffled = rf_model.predict_proba(X_test_shuffled)[:, 1]
    auc_shuffled = roc_auc_score(y_test_demo, y_pred_proba_shuffled)
    
    # 计算AUC下降幅度
    auc_drop = auc_original - auc_shuffled
    feature_importance_shuffle[col] = auc_drop
    print(f"特征 '{col}' 打乱后 AUC: {auc_shuffled:.4f}, AUC下降: {auc_drop:.4f}")

# 3. 按重要性排序
print("\n特征重要性排序(AUC下降幅度越大越重要):")
for feat, imp in sorted(feature_importance_shuffle.items(), key=lambda x: x[1], reverse=True):
    print(f"  {feat}: {imp:.4f}")
基准模型 AUC: 1.0000
特征 'feature_A' 打乱后 AUC: 1.0000, AUC下降: 0.0000
特征 'feature_B' 打乱后 AUC: 1.0000, AUC下降: 0.0000
特征 'feature_C' 打乱后 AUC: 0.0000, AUC下降: 1.0000

特征重要性排序(AUC下降幅度越大越重要):
  feature_C: 1.0000
  feature_A: 0.0000
  feature_B: 0.0000

结果解读:

  • 在我们的演示数据中,feature_C 与标签强相关,打乱其值后模型完全失效(AUC从1.0降至0.0),下降幅度为1.0,表明它非常重要。
  • feature_A 和 feature_B 与标签无关,打乱它们对模型性能没有影响(AUC下降为0),表明它们在当前任务中不重要。

在实际项目中,课程提供的 feature_shuffle_rf 工具会对所有特征执行此过程,并返回每个特征对应的AUC下降值。我们可以设定一个阈值(如 auc_drop > 0),选择那些对模型性能有实质影响的特征。

5.2 混合法:综合策略的威力

混合法(Hybrid Method)不是一种特定的算法,而是一种工程思想:不局限于单一的特征选择方法,而是根据实际情况,组合使用过滤、包裹、嵌入或混序等方法,以期获得更稳定、更可靠的特征子集。

常见的混合策略包括:

  1. 先过滤,后包裹:先用方差过滤或相关系数法快速去掉大量明显无关或冗余的特征,再用递归特征消除(RFE)等包裹法在剩余特征中精细筛选。
  2. 模型重要度 + 实验验证:利用随机森林、XGBoost等模型输出特征重要度排序,再通过混序法验证排名靠前的特征是否真的对性能有显著影响。
  3. 多模型共识:使用不同类型的模型(如线性模型、树模型)分别进行特征选择,取这些模型共同认可的重要特征。

5.2.1 混合法代码实践:递归特征消除与增加

我们结合课程提供的混合法工具,演示两种常见的包裹式策略:递归特征消除(RFE)和递归特征增加(RFA)。这些工具已封装在 utils.hybrid 模块中。

首先,我们使用 sklearn 内置的 RFE 配合随机森林进行特征选择。

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
from sklearn.feature_selection import RFE

# 加载数据
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target

# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(df.drop('target', axis=1), 
                                                    df['target'], 
                                                    test_size=0.2, 
                                                    random_state=42)
print("数据准备完毕。")

# 使用RFE选择10个特征
# n_features_to_select 决定停止条件,这里我们指定保留10个特征
selector = RFE(estimator=RandomForestClassifier(n_estimators=20, random_state=42), 
               n_features_to_select=10)
selector.fit(X_train, y_train)

# 获取被选中的特征
selected_feat_rfe = X_train.columns[selector.support_]
print(f"RFE选出的 {len(selected_feat_rfe)} 个特征:")
print(list(selected_feat_rfe))
数据准备完毕。
RFE选出的 10 个特征:
['mean texture', 'mean concave points', 'area error', 'worst radius', 'worst texture', 'worst perimeter', 'worst area', 'worst smoothness', 'worst concavity', 'worst concave points']

接下来,我们演示课程自研工具 recursive_feature_elimination_rf 的核心逻辑。该工具与标准RFE略有不同,它根据移除特征后模型性能(AUC)的下降是否超过一个容忍度(tol)来决定是否删除该特征。

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score

# 为了清晰演示,我们使用一个更小的自制数据集
np.random.seed(42)
X_small = pd.DataFrame({
    'f1': np.random.randn(8),
    'f2': np.random.randn(8),
    'f3': np.random.randn(8) * 0.1,  # 噪声特征,方差小
    'f4': np.array([1,1,1,2,2,2,1,2]),  # 与标签有一定关联
})
# 构造标签,与 f4 有较强关系(前 6 行做训练集、后 2 行做测试集,两部分都含两个类别)
y_small = pd.Series([0,0,0,1,1,1,0,1])

X_train_small = X_small.iloc[:6].copy()
y_train_small = y_small.iloc[:6].copy()
X_test_small = X_small.iloc[6:].copy()
y_test_small = y_small.iloc[6:].copy()

print("小型演示数据(训练集):")
print(X_train_small)
print("标签:", y_train_small.values)
print("\n小型演示数据(测试集):")
print(X_test_small)
print("标签:", y_test_small.values)

# 演示递归特征消除的核心逻辑(简化版,非完整工具函数)
def demo_recursive_elimination(X_train, y_train, X_test, y_test, tol=0.001, n_estimators=5):
    """
    简化的递归特征消除演示
    tol: 容忍度,如果移除某个特征导致的AUC下降小于tol,则移除它。
    """
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.metrics import roc_auc_score

    features_to_keep = list(X_train.columns)
    print(f"初始特征数: {len(features_to_keep)}")
    
    # 初始模型(使用所有特征)
    model_all = RandomForestClassifier(n_estimators=n_estimators, random_state=42)
    model_all.fit(X_train[features_to_keep], y_train)
    y_pred_all = model_all.predict_proba(X_test[features_to_keep])[:, 1]
    auc_all = roc_auc_score(y_test, y_pred_all)
    print(f"初始模型 AUC: {auc_all:.4f}")
    
    features_to_remove = []
    for feature in list(features_to_keep):  # 遍历当前保留的特征列表副本
        # 尝试移除该特征
        candidate_features = [f for f in features_to_keep if f not in features_to_remove and f != feature]
        
        if not candidate_features:  # 避免特征被删光
            continue
            
        model_reduced = RandomForestClassifier(n_estimators=n_estimators, random_state=42)
        model_reduced.fit(X_train[candidate_features], y_train)
        y_pred_reduced = model_reduced.predict_proba(X_test[candidate_features])[:, 1]
        auc_reduced = roc_auc_score(y_test, y_pred_reduced)
        
        auc_drop = auc_all - auc_reduced
        print(f"  尝试移除 '{feature}',AUC变为 {auc_reduced:.4f},下降 {auc_drop:.4f}")
        
        if auc_drop < tol:  # 下降很小,可以移除
            print(f"  -> 决定移除 '{feature}'")
            features_to_remove.append(feature)
            # 更新基准AUC为移除该特征后的AUC
            auc_all = auc_reduced
        else:
            print(f"  -> 保留 '{feature}'")
    
    final_features = [f for f in features_to_keep if f not in features_to_remove]
    print(f"\n最终保留特征数: {len(final_features)}")
    return final_features

# 运行演示
print("\n=== 容忍度 tol = 0.01 ===")
final_kept_features_001 = demo_recursive_elimination(X_train_small, y_train_small, X_test_small, y_test_small, tol=0.01)
print(f"\n最终保留的特征: {final_kept_features_001}")
小型演示数据(训练集):
         f1        f2        f3  f4
0  0.496714 -0.469474 -0.101283   1
1 -0.138264  0.542560  0.031425   1
2  0.647689 -0.463418 -0.090802   1
3  1.523030 -0.465730 -0.141230   2
4 -0.234153  0.241962  0.146565   2
5 -0.234137 -1.913280 -0.022578   2
标签: [0 0 0 1 1 1]

小型演示数据(测试集):
         f1        f2        f3  f4
6  1.579213 -1.724918  0.006753   1
7  0.767435 -0.562288 -0.142475   2
标签: [0 1]

=== 容忍度 tol = 0.01 ===
初始特征数: 4
初始模型 AUC: 1.0000
  尝试移除 'f1',AUC变为 1.0000,下降 0.0000
  -> 决定移除 'f1'
  尝试移除 'f2',AUC变为 1.0000,下降 0.0000
  -> 决定移除 'f2'
  尝试移除 'f3',AUC变为 1.0000,下降 0.0000
  -> 决定移除 'f3'

最终保留特征数: 1

最终保留的特征: ['f4']

演示总结:

  • 逐一尝试移除特征并观察 AUC 变化:移除噪声特征 f1、f2、f3 后 AUC 均保持 1.0000(下降 0.0000,小于容忍度),它们被逐个删除;最终只保留了与标签强关联的 f4——它承载了这份数据的全部判别信息(若把它也移除,模型将失去所有依据,这正是工具用空集保护避免的操作)。
  • 容忍度 tol 是一个关键超参数,它控制了特征选择的严格程度。tol 越小,越不容易删除特征;tol 越大,特征筛选越激进。

课程提供的 hybrid.recursive_feature_addition_rf 工具则采用相反的思路:从一个特征开始,逐步添加最能提升模型性能的特征,直到添加新特征带来的提升小于容忍度。

📝 动手练一练

练习 1(概念辨析):混序法(shuffle-based)与前向特征选择(逐个添加特征)在”评估一个特征的价值”时,思路有何本质不同?各自的计算代价受什么因素主导?

👉 点击查看参考答案

混序法是”破坏性验证”:训练完成后,把某一列特征值随机打乱再评估模型,性能掉得越多说明该特征贡献越大——它只需要在已训练模型上做预测,不用反复重训。前向选择是”建设性验证”:从零开始逐个尝试加入特征,每加一个都要重新训练模型并评估——训练次数随特征数呈倍数增长。混序法的代价主要在”每个特征一次预测 + 一次基线训练”,前向选择的代价主要在”多轮全量重训”,特征多、模型慢时前向选择明显更贵。

练习 2(动手实算):本节示例中,乱序 feature_C 后 AUC 从基准值明显下降,而乱序 feature_A 影响很小。如果把演示数据中 feature_C 的标签相关性改弱(比如 y_demo 改为 [0, 1, 0, 1]),你预期三个特征的 AUC 降幅排序会怎么变化?为什么?

👉 点击查看参考答案

y_demo 改为 [0, 1, 0, 1] 后,feature_C 的取值 10/20/30/40 对应标签 0/1/0/1,不再单调相关,乱序它带来的信息损失会大幅减小;此时三个特征都几乎没有稳定预测力,各特征的 AUC 降幅都会趋近于零(在 2 个测试样本的极简数据上波动会很大)。这说明混序法度量的是”特征对模型的可预测贡献”,标签与特征的关联强度决定了乱序的代价——也提醒我们:极小样本上的特征重要性结论不可靠,工业实践中要用足够大的数据。

本章小结

本节我们深入学习了两种高级的特征选择方法:

  1. 混序法:一种直观的实验性方法,通过打乱特征值并观察模型性能下降来评估特征重要性。它不依赖于模型内部结构,解释性强。
  2. 混合法:一种工程实践思想,强调灵活组合多种特征选择策略(如过滤、包裹、嵌入、混序),以得到更稳健的特征子集。我们重点演示了递归特征消除(RFE)和递归特征增加(RFA)两种包裹式策略的混合应用。

至此,我们已经完成了特征选择模块所有方法的学习。从基于统计的过滤法,到基于模型迭代的包裹法,再到基于正则化的嵌入法,以及本节学习的混序法与混合法,你已经拥有了一个完整的特征选择工具箱。

📋 行动清单

  • 运行混序法示例:尝试在课程提供的乳腺癌数据集上完整运行 feature_shuffle_rf,观察所有特征的AUC下降排名。
  • 调整混合策略参数:修改递归特征消除/增加演示代码中的容忍度 tol,观察不同严格度下最终保留的特征有何不同。
  • 设计自己的混合流程:针对一个你熟悉的数据集,设计一个两步走的混合选择策略(例如:先用相关系数过滤掉相关性极低的特征,再用混序法对剩余特征进行重要性排序)。

—— 小象教研组