下面给出一个专业、系统的解题框架,围绕题干要求设计一个“Funnelling(分层筛选)”的多步特征选择流程,综合滤波(filter)、包装(wrapper)和嵌入(embedded)三类方法,并给出可直接运行的 Python 实现。
一、题干分析与思路提示
目标:在一个二分类或回归任务中,通过三类特征选择方法的多步“漏斗式”组合,逐步筛出对模型最有用、且彼此之间较为独立的特征集合,最终得到一个稳健且可解释的特征子集。
三类方法的角色:
滤波(Filter):快速、无模型依赖地评估每个特征与目标的统计相关性,去除明显无信息的特征,降低维度与噪声,提供初步的候选集合。
包装(Wrapper):在真实预测建模的情境下评估特征子集的组合效果,考虑特征之间的交互与冗余,选出对模型性能贡献最大的子集。
嵌入(Embedded):在模型训练过程中进行特征选择(如正则化导致的系数稀疏化、基于特征重要性的裁剪等),实现对特征在特定模型中的“实际贡献”评估。
融合策略(Funnelling 重点):将三步得到的候选集合进行整合,常用的方法包括取并集/交集、权重投票或简单的交叉验证性能对比。在本题中,推荐使用“按强制交叉一致性(intersection)”的做法:最终特征集为三步都认可的特征的交集,得到稳定性较高的特征子集;如需要,可以在此基础上做一个小的后处理(如在交集基础上再加上部分_wrapper_或_embedded_中表现稳定的特征)。
关键注意点:
避免数据泄露:筛选过程尤其在 wrapper/embedded 阶段应使用交叉验证,且跨步骤尽量使用相同分割策略,避免在同一步骤中对测试数据进行暴露。 。从实现角度,建议把整套流程包装成一个管道(或分阶段管道),并尽量将数据分割、交叉验证和特征评估的设置统一。
处理类型:若数据包含类别型特征,需要在滤波阶段做适当的编码或对数值化处理;本实现主要以数值型特征为主,若有类别特征可在管道中使用 OneHotEncoder 进行编码后再进行筛选。
二、解题步骤(a、b、c)
A. (a) 三类方法的逐步特征选择流程(分步解释)
滤波阶段(Filter)
目的:快速初筛,去除明显无关或冗余的特征,降低后续计算成本。
常用工具与指标:
低方差(Variance Threshold)去除常量/近似常量特征。
互信息(mutual_info_classif / mutual_info_regression)或信息增益,衡量单个特征与目标之间的非线性关系。
相关性过滤:基于与目标的相关性排序,或与其他特征的相关性矩阵,去除高度相关的特征以降低多重共线性。
实施要点:
首先应用 VarianceThreshold 去除零方差特征。
使用 SelectKBest( mutual_info_classif / mutual_info_regression ) 选取前 k 个最相关特征(k 根据数据规模设定,例如 0.6 × n_features)。
计算特征之间的相关性矩阵,按相关性阈值(如 |corr| > 0.95)筛选出高度共线的特征,保留信息量更高的那个特征。
产出:一个“候选特征集”F_filter。
包装阶段(Wrapper)
目的:在一个具体的预测模型框架下评估特征子集对模型性能的贡献,考虑特征之间的交互和冗余。
常用工具与策略:
RFECV(递归特征消除带交叉验证):在每一步去掉性能贡献最小的特征,评估跨越多种子集的模型性能,选出最佳特征子集大小及对应特征。
也可以使用 Sequential Feature Selector(逐步前向/后向选择),但 RFECV 更自动、稳定。
实施要点:
选择一个基础估计器(如 Logistic Regression、Random Forest、SVM 等),通常建议使用线性模型(如 Logistic Regression)作为 Wrapper 的起点,因为其系数直接可解释,计算成本相对较低。
使用 StratifiedKFold(分类)或 KFold(回归)进行交叉验证,确保对不平衡数据的鲁棒性。
产出:一个“Wrapper 选中的特征集”F_wrapper。
嵌入阶段(Embedded)
目的:在模型训练过程中自带的特征选择机制,通常实现稀疏性(如 L1 正则化导致系数为零)或基于特征重要性的裁剪。
常用工具与策略:
L1/L1/L2 正则化的逻辑回归、线性模型等(penalty='l1',solver='liblinear' 等),通过学习到的系数来决定保留的特征(非零系数)。
基于树的模型(如 RandomForest、GradientBoosting)自带的特征重要性排序,保留重要性高的特征。
对于嵌入阶段,通常需要对超参数(如 C)进行交叉验证来选择合适的正则化强度,以获得稳定且可解释的特征集合。
实施要点:
使用 L1 正则化的逻辑回归并通过网格搜索选择最佳 C,使若干系数降为零,保留的特征即为嵌入阶段的输出。
产出:一个“Embedded 选中的特征集”F_embedded。
融合/最终选取
策略:将三步的结果进行交集(F_final = F_filter ∩ F_wrapper ∩ F_embedded),得到在三步中均被认为有意义的特征,用以提升鲁棒性和可解释性。
如若交集为空,可以考虑放宽条件(如允许交集与某一步的特征并集的交叉折中),但优先保持严格的交集以获得稳定特征。
最终输出:最终特征列表 F_final(按原始数据中的特征名称排序)。
B. (b) 选择在各步保留的特征的原因与证据
滤波阶段
原因:快速消除无信息、冗余度高的特征,降低维度,避免后续步骤的过拟合与计算开销。
证据/理由:单变量指标(如互信息)能快速揭示与目标之间的相关性,且与模型无关,具有较高的鲁棒性。
约束/注意:不能完全忽视特征间的交互;因此仍需进入 Wrapper 阶段以捕捉特征组合效应。
包装阶段
原因:考虑特征之间的相互作用与依赖性,评估子集在具体预测任务中的实际表现,能发现单变量关系被忽视的组合性特征。
证据/理由:RFECV 能在多轮交叉验证下选择对模型最有贡献的特征子集,减少冗余度和过拟合风险。
约束/注意:计算开销较大,需合理选择基模型与交叉验证设置,避免数据泄露。
嵌入阶段
原因:在模型训练过程中对特征进行稀疏化或基于树的重要性筛选,是对特征在特定模型中的“实际贡献”的直接量化。
证据/理由:L1 正则化能将不必要的系数压缩至零,从而自然选出稳定、可解释的特征集合;树模型的特征重要性也能揭示对预测有显著贡献的特征。 约束/注意:不同模型的偏好可能不同,最好结合多种嵌入策略并在同一数据集上进行对照分析。
融合策略的合理性
采用交集方式强调三个阶段的一致性,通常能得到鲁棒且可解释的特征,降低因任一阶段偶然性带来的误选风险。
如数据规模较大、噪声较多,也可采用加权投票或分层过滤进行慎重的放宽。
C. (c) 最终选取的特征清单
最终特征应来自 F_final = F_filter ∩ F_wrapper ∩ F_embedded。请注意:
F_final 的具体内容依赖于数据集的特征、目标、以及实施中的参数设置(比如 k 的取值、相关性阈值、C 的取值等)。
在真实作业中,需给出你自己的数据集、执行上述步骤后得到的最终特征清单。
下面给出一个完整的可运行实现(Python),并附带一个示例数据集演示,最终输出最终特征清单。你可以将数据替换成你自己的数据来得到你课程作业中的最终结果。
三、Python 实现(完整代码示例)
说明
该实现以分类任务为例,若数据是回归任务,请将互信息函数与相关指标替换为 mutual_info_classif -> mutual_info_regression,并相应调整评估指标(如 accuracy 替换为 r2、neg_mean_squared_error 等)。
代码包含三个阶段的实现,以及最终的交集输出和一个简要的性能对比示例(全特征 vs 最终特征)以便评估效果。
代码(请直接运行)
"""
Python 3.x
需要的库(请确保已安装):pandas scikit-learn numpy
pip install numpy pandas scikit-learn
"""
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, StratifiedKFold, RFECV, GridSearchCV
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import VarianceThreshold, SelectKBest, mutual_info_classif
from sklearn.metrics import accuracy_score
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import FeatureUnion
--------- 1) 数据加载与分割 ---------
这里用乳腺癌数据集作为示例,特征命名采用 data.feature_names
data = load_breast_cancer(as_frame=True)
X_all = data.data
y = data.target
将特征名作为 DataFrame 的列名,确保后续能跟踪原始特征
X = pd.DataFrame(X_all, columns=data.feature_names)
feature_names = list(X.columns)
划分训练集与测试集(为了演示,后续交叉验证在训练集内部完成)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
--------- 辅助函数 ---------
def prune_by_correlation(X_df, mi_scores, corr_threshold=0.95):
"""
在给定特征集合和对应的MI分数下,去除高度相关的特征(> corr_threshold),保留 MI 较高者。
输入:
X_df: DataFrame,待筛选的特征集合
mi_scores: dict{feature: MI score}
corr_threshold: float,相关性阈值
输出:
保留的特征列表
"""
features = list(X_df.columns)
if len(features) <= 1:
return features
corr_matrix = X_df.corr().abs()
to_drop = set()
# 遍历上三角,找出高度相关的特征对
for i in range(len(features)):
for j in range(i + 1, len(features)):
f1 = features[i]
f2 = features[j]
if corr_matrix.loc[f1, f2] > corr_threshold:
# 保留相关性较高的那个(MI更高者)
if mi_scores.get(f1, 0) >= mi_scores.get(f2, 0):
to_drop.add(f2)
else:
to_drop.add(f1)
kept = [f for f in features if f not in to_drop]
return kept
--------- 2) 滤波阶段(Filter) ---------
def filter_stage(X, y, variance_thresh=0.0, top_k=None, corr_thresh=0.95, seed=0):
"""
滤波阶段:
- VarianceThreshold: 移除低方差特征
- SelectKBest with mutual_info_classif: 选出前 top_k 个特征(若 top_k=None,则保留所有经过方差过滤的特征)
- 基于相关性阈值进行冗余特征剔除:保留 MI 值更高的特征,丢弃相关性高但信息量低的特征
返回:
filter_features:最终在滤波阶段保留的特征名列表
X_filtered:对应的特征子集 DataFrame
mi_scores:每个保留特征的 MI 分数(供后续阶段使用)
"""
# 1) VarianceThreshold
vt = VarianceThreshold(threshold=variance_thresh)
X_var = vt.fit_transform(X)
var_mask = vt.get_support(indices=True)
features_after_var = [X.columns[i] for i in var_mask]
if len(features_after_var) == 0:
return [], pd.DataFrame(), {}
X_after_var = pd.DataFrame(X_var, columns=features_after_var)
# 2) SelectKBest with mutual_info_classif
if top_k is None or top_k >= len(features_after_var):
# 直接计算 MI for all features after variance filter
mi = mutual_info_classif(X_after_var, y, discrete_features=False, random_state=seed)
mi_scores = dict(zip(X_after_var.columns, mi))
# 排序并选取全部作为候选集(仍可后续再过滤)
features_after_mi = list(X_after_var.columns)
scores = mi_scores
else:
selector = SelectKBest(score_func=mutual_info_classif, k=top_k)
_ = selector.fit(X_after_var, y)
idx = selector.get_support(indices=True)
features_after_mi = [X_after_var.columns[i] for i in idx]
scores = dict(zip(X_after_var.columns, selector.scores_))
mi_scores = {f: scores.get(f, 0.0) for f in features_after_mi}
X_after_mi = X_after_var[features_after_mi]
# 3) 相关性过滤(按 MI 分数保留信息量更高的特征,剔除高度相关的特征)
if len(features_after_mi) > 1:
mi_scores_all = {f: mi_scores.get(f, 0.0) for f in features_after_mi}
features_after_corr = prune_by_correlation(X_after_mi, mi_scores_all, corr_thresh)
else:
features_after_corr = features_after_mi
X_filtered = X_after_mi[features_after_corr]
filter_features = features_after_corr
return filter_features, X_filtered, mi_scores_all if len(features_after_mi) > 0 else {}
--------- 3) 包装阶段(Wrapper) ---------
def wrapper_stage(X, y, estimator=None, cv_splits=5):
"""
Wrapper 阶段:
- 使用 RFECV 在给定的估计器下寻找最佳特征子集大小与特征集合
- 返回选中的特征名称列表
"""
if estimator is None:
estimator = LogisticRegression(max_iter=1000, solver='liblinear', random_state=0)
rfecv = RFECV(estimator=estimator,
step=1,
cv= StratifiedKFold(n_splits=cv_splits, shuffle=True, random_state=0),
scoring='accuracy')
rfecv.fit(X, y)
selected_features = list(X.columns[rfecv.support_])
return selected_features
--------- 4) 嵌入阶段(Embedded) ---------
def embedded_stage(X, y, C_values=None, cv_splits=5):
"""
Embedded 阶段:
- 使用 L1 正则化的逻辑回归进行特征选择(非零系数即保留)
- 通过网格搜索选择最佳 C
返回:
embedded_features:在嵌入阶段保留的特征名称列表
"""
if C_values is None:
C_values = [0.01, 0.1, 1, 10]
lr_l1 = LogisticRegression(penalty='l1', solver='liblinear', max_iter=1000, random_state=0)
grid = GridSearchCV(estimator=lr_l1,
param_grid={'C': C_values},
cv=StratifiedKFold(n_splits=cv_splits, shuffle=True, random_state=0),
scoring='accuracy')
grid.fit(X, y)
best_C = grid.best_params_['C']
best_model = grid.best_estimator_
# 取得非零系数的特征
coef = best_model.coef_
# 一般二分类单输出,coef 的形状为 (1, n_features)
non_zero_mask = coef[0] != 0
embedded_features = list(pd.Series(X.columns)[non_zero_mask])
return embedded_features
--------- 5) 融合:最终选取 ---------
def funnel_feature_selection(X, y,
variance_thresh=0.0,
top_k=None,
corr_thresh=0.95,
wrapper_seed=0,
C_values=None):
"""
完整的 Funnelling 流程:
1) 滤波阶段 -> 获取 F_filter 与 X_filtered
2) 包装阶段 -> 获取 F_wrapper
3) 嵌入阶段 -> 获取 F_embedded
4) 最终特征为 F_final = F_filter ∩ F_wrapper ∩ F_embedded
返回:
final_features:最终筛选出的特征名列表
details:包含每一阶段的特征集合供分析
"""
# 1) 滤波阶段
F_filter, X_filter, _ = filter_stage(X, y,
variance_thresh=variance_thresh,
top_k=top_k,
corr_thresh=corr_thresh,
seed=wrapper_seed)
# 2) 包装阶段
if len(F_filter) == 0:
# 如果滤波阶段全部失败,直接用原始特征进行包装
X_for_wrapper = X
else:
X_for_wrapper = X_filter
# 选择一个合适的估计器(默认 Logistic Regression)
estimator = LogisticRegression(max_iter=1000, solver='liblinear', random_state=wrapper_seed)
F_wrapper = wrapper_stage(pd.DataFrame(X_for_wrapper, columns=X_for_wrapper.columns),
y,
estimator=estimator,
cv_splits=5)
# 将 Wrapper 的特征映射回原始特征名称(若 X_for_wrapper 是 X_filter 的子集,需保持一致)
# 这里确保 F_wrapper 使用的是 X_for_wrapper 的列名;直接使用即可
# 3) 嵌入阶段
embedded_features = embedded_stage(X_for_wrapper[F_wrapper] if len(F_wrapper) > 0 else X_for_wrapper, y,
C_values=C_values)
# 4) 最终交集
F_filter_set = set(F_filter)
F_wrapper_set = set(F_wrapper)
F_embedded_set = set(embedded_features)
final_features = sorted(list(F_filter_set & F_wrapper_set & F_embedded_set))
details = {
'F_filter': F_filter,
'F_wrapper': F_wrapper,
'F_embedded': embedded_features,
'F_final': final_features
}
return final_features, details
--------- 6) 演示:在乳腺癌数据集上运行 ---------
设定参数,执行 Funnel
final_features, details = funnel_feature_selection(X, y,
variance_thresh=0.0,
top_k=20, # 滤波阶段选择前 20 个特征作为候选
corr_thresh=0.95,
wrapper_seed=0,
C_values=[0.01, 0.1, 1, 10])
print("最终选取的特征数量:", len(final_features))
print("最终特征列表(按字母序排序):")
print(final_features)
可选:对比全量特征的模型性能与最终特征的模型性能
简单示例:使用 Logistic Regression,在测试集上评估
from sklearn.metrics import accuracy_score
from sklearn.pipeline import make_pipeline
使用最终特征的子集训练一个简单模型
def evaluate_model_with_features(feature_list):
if len(feature_list) == 0:
return None
X_train_sub = X_train[feature_list]
X_test_sub = X_test[feature_list]
model = LogisticRegression(max_iter=1000, solver='liblinear')
model.fit(X_train_sub, y_train)
preds = model.predict(X_test_sub)
acc = accuracy_score(y_test, preds)
return acc
全量特征
acc_full = evaluate_model_with_features(feature_names)
最终特征
acc_final = evaluate_model_with_features(final_features)
print("全量特征的测试集准确率(示例):", acc_full)
print("最终特征集的测试集准确率(示例):", acc_final)
"""
说明
如你需要把这段代码直接运用于你的数据集,请把数据读入 X、y,替换相关参数即可。
输出的 final_features 即为你的最终特征清单,请将其作为你作业的(c)部分的“最终列表”提交。 """
运行结束
若你需要,我还能:
基于你给的具体数据集,替换数据加载部分并输出你数据集上的最终特征清单。
提供一个可选的并集/交集混合策略(如若交集为空时采用并集的稳健备选)。
给出更详细的性能评估(如交叉验证的平均准确率、标准差、特征稳定性分析等)。
简要总结
本解法把 Feature Selection 划分为三步:Filter(快速初筛)、Wrapper(评估子集的实际预测效果)、Embedded(模型内的稀疏化/重要性筛选),最后通过交集融合得到稳定的最终特征集。
该流程兼顾了计算效率、特征之间的相互作用、以及模型在特定任务中的实际贡献,能够提供可解释且鲁棒的特征子集。 将上述内容翻译成英文
最新发布