用递归消除法优化“特征子集”

机器学习的模型训练中,特征 不是越多越好,而是要找到那个 "刚刚好"的子集

今天介绍的方法 "简单粗暴" ,通过递归的方式,不断淘汰一个个特征,直到找到那个最优的 "特征子集"

核心思想

核心思想用一句话来说,就是用"淘汰赛"选出最强特征组合

递归消除的思路特别像公司搞末位淘汰

plain 复制代码
1. 把所有"员工"(特征)都拉进来干活
2. 让"经理"(模型)给大家打个绩效(特征重要性)
3. 干掉绩效最差的那位
4. 剩下的人继续干活,重新打绩效
5. 重复,直到剩下的人"够用了"为止

但这里有个关键问题:怎么判断"够了"?

答案是:每一步都做一次交叉验证,记录模型得分。最后看哪一步得分最高,那个特征数量就是最优的。

画个图就是这种感觉:

plain 复制代码
特征数:   10    9    8    7    6    5    4    3    2    1
得分:    0.82 0.84 0.87 0.89 0.91 0.93 0.92 0.90 0.86 0.78
                                 ↑
                              最优子集

看到没?随着特征减少,得分先升后降------去掉噪音特征时得分上升,去掉有用特征时得分下降

我们要找的就是那个拐点

设计思路

代码的整体设计上主要分 4 个部分:

初始化参数

python 复制代码
def __init__(self, base_model, remove_count, fold_num, metric, seed, ...):
参数 含义 类比
base_model 用来评估特征重要性的模型 "裁判" --- 由它来判断哪些特征有用
remove_count 每轮淘汰几个特征 每轮换下几名球员
fold_num 交叉验证折数 评估时多试几轮,避免偶然性
metric 评估指标(准确率等) 用什么标准衡量"踢得好不好"
early_stop_rounds 连续几轮变差就提前停止 连输几场就不折腾了

核心流程 --- run()

这是整个算法的主循环,对应下面这张流程图:

plain 复制代码
全部 30 个特征
    │
    ▼
┌──────────────────────────────────┐
│ 第 1 轮:30 个特征                │
│  ① 交叉验证 → 得分 0.85           │  ← 记录下来
│  ② 训练模型 → 获取特征重要性       │
│  ③ 移除最差的 1 个特征            │
└──────────────────────────────────┘
    │
    ▼
┌──────────────────────────────────┐
│ 第 2 轮:29 个特征                │
│  ① 交叉验证 → 得分 0.86           │  ← 更好了!
│  ② 训练模型 → 获取特征重要性       │
│  ③ 移除最差的 1 个特征            │
└──────────────────────────────────┘
    │
    ▼
  ... 重复直到只剩 1 个特征 ...
    │
    ▼
  回顾所有轮次,选得分最高的那轮 → 最优特征子集

关键代码对应关系:

python 复制代码
# 第 3a 步:用当前特征做交叉验证(评估这套阵容的成绩)
eval_result = self._evaluate_subset(model, subset_df, labels)

# 第 3d 步:训练模型,获取特征重要性(让裁判打分)
model.fit(subset_df, labels)
importance_array = self._extract_importance(model, active_features)

# 第 3e 步:移除贡献最低的特征(换下最差的球员)
weakest_indices = np.argsort(importance_array)[:actual_remove]
active_features = [f for f in active_features if f not in weakest_names]

两个重要的内部方法

_evaluate_subset()--- "评委打分"

python 复制代码
scores = cross_validate(clone(model), X_sub, y, cv=splitter, scoring=self.metric)

交叉验证 评估当前特征子集的质量。

为什么要交叉验证而不是直接训练?因为交叉验证把数据分成多份轮流测试,结果更可靠,不容易过拟合。

_extract_importance()--- "判断谁最弱"

python 复制代码
if hasattr(model, "feature_importances_"):    # 树模型(如随机森林)
    raw = model.feature_importances_
elif hasattr(model, "coef_"):                 # 线性模型(如逻辑回归)
    raw = np.abs(model.coef_).ravel()

不同模型衡量特征重要性的方式不同:

  • 树模型 :自带 feature_importances_,直接告诉你每个特征贡献了多少
  • 线性模型 :看系数 coef_ 的绝对值,系数越大说明这个特征越重要

结果输出

  • select():用最优特征子集裁剪数据
  • summary_table():输出每轮迭代的得分汇总表
  • plot_curve():画出"特征数量 vs 得分"的曲线图,直观看出哪个特征数量最优

关键细节

注意 _locate_optimum() 方法:

python 复制代码
def _locate_optimum(self):
    best_record = max(self._tracking_log, key=lambda r: r["mean_score"])

它不是选"最后剩下的特征",而是选整个过程中交叉验证得分最高的那一轮的特征。

这意味着:

有可能 10 个特征时得分最高,继续消除反而变差。算法会自动停在"甜蜜点"。

代码实现

完整的代码和模拟数据的测试用例(心脏病风险预测)共享在:

url11.ctfile.com/d/45455611-... (访问密码: 6872)

相关文件是:recursive_feature_eliminator.pytest_recursive_feature_eliminator.py

感兴趣的朋友欢迎下载了自己运行试试看,运行过程中有什么问题也欢迎交流。

测试代码运行效果如下:

plain 复制代码
----------------------------------------------------------------------
[特征消除] 初始特征数: 30,每轮移除: 1,交叉验证折数: 5,评估指标: accuracy
-----------------------------------------------------------------
  特征数   30  |  得分 0.8625 ± 0.0230
  特征数   29  |  得分 0.8607 ± 0.0385
  特征数   28  |  得分 0.8464 ± 0.0207
  特征数   27  |  得分 0.8643 ± 0.0164
  特征数   26  |  得分 0.8643 ± 0.0311
  特征数   25  |  得分 0.8500 ± 0.0372
  特征数   24  |  得分 0.8625 ± 0.0332
  特征数   23  |  得分 0.8589 ± 0.0273
  特征数   22  |  得分 0.8643 ± 0.0331
  特征数   21  |  得分 0.8768 ± 0.0279
  特征数   20  |  得分 0.8536 ± 0.0332
  特征数   19  |  得分 0.8786 ± 0.0166
  特征数   18  |  得分 0.8768 ± 0.0118
  特征数   17  |  得分 0.8804 ± 0.0256
  特征数   16  |  得分 0.8875 ± 0.0471
  特征数   15  |  得分 0.8750 ± 0.0246
  特征数   14  |  得分 0.8821 ± 0.0273
  特征数   13  |  得分 0.8804 ± 0.0312
  特征数   12  |  得分 0.8875 ± 0.0373
  特征数   11  |  得分 0.8786 ± 0.0443
  特征数   10  |  得分 0.8786 ± 0.0317
  特征数    9  |  得分 0.8946 ± 0.0236
  特征数    8  |  得分 0.8857 ± 0.0345
  特征数    7  |  得分 0.8982 ± 0.0360
  特征数    6  |  得分 0.9000 ± 0.0182
  特征数    5  |  得分 0.8893 ± 0.0337
  特征数    4  |  得分 0.7982 ± 0.0230
  特征数    3  |  得分 0.7821 ± 0.0268
  特征数    2  |  得分 0.7089 ± 0.0184
-----------------------------------------------------------------
[特征消除] 完成!耗时 32.1s
  最优特征数: 6
  最优得分:   0.9000
  选中特征:   ['年龄', '收缩压', '胆固醇', '血糖', '运动频率', '身高']

筛选出的特征: ['年龄', '收缩压', '胆固醇', '血糖', '运动频率', '身高']

测试集准确率: 0.9125

分类报告:
              precision    recall  f1-score   support

         低风险       0.92      0.90      0.91       120
         高风险       0.90      0.93      0.91       120

    accuracy                           0.91       240
   macro avg       0.91      0.91      0.91       240
weighted avg       0.91      0.91      0.91       240


======================================================================
  对比总结
======================================================================
  方案 A(全部 30 项特征)准确率: 0.8750
  方案 B(筛选后 6 项特征)准确率: 0.9125
  准确率变化: +0.0375
  特征数量减少: 30 -> 6
======================================================================

注意事项

每轮不重新训练模型

一开始我图省事,只在第一轮训练一次模型,后面全用同一个模型的重要性来删特征。

问题:特征之间的交互效应被忽略了。A和B单独看都不重要,但组合起来可能很厉害。你第一轮就把A删了,后面B也跟着"失宠"。

正解每删一批特征,必须重新训练模型,重新计算重要性。

忘了做交叉验证

直接用训练集上的得分来评判。

问题:过拟合到训练集上了,选出来的"最优子集"在测试集上表现很差。

正解一定要用交叉验证的得分来做判断,这样选出来的子集泛化能力更强。

没 clone 模型

交叉验证的时候,直接传了同一个模型实例进去。

问题cross_validate 内部会 fit 模型,如果不 clone,每折之间会互相污染。

正解 :用 sklearn.base.clone() 确保每次评估都是一个全新的模型。

特征太多的时候太慢

有次我拿了200多个特征去跑,remove_count=1,跑了大半天......

正解

  • 特征多时先把 remove_count 调大,比如 remove_count=5
  • 或者先用过滤法(比如相关系数阈值)粗筛一轮,再用 RFE 精筛
  • 打开 n_jobs=-1,让交叉验证多核并行

分类不平衡时用了 accuracy

有个二分类数据正样本只占 5%,我用 accuracy 做评估。

问题:模型全预测负类就有 95% 的 accuracy,但毫无意义。

正解 :不平衡数据一定要换 f1roc_auc 或者自定义的评分函数。

总结

RFE(recursive featureeliminator 递归消除法) 核心就三句话:

  • 特征在精不在多------去掉噪声特征,模型反而更好
  • RFE 是"用模型选特征"------让模型自己告诉我们谁重要谁不重要
  • 交叉验证是裁判------不能光看训练集,要用 CV 得分来判断子集好坏

同时,我们也要清醒认识到 RFE 的局限:

  • 贪心策略:每次只删当前最弱的,不保证全局最优
  • 计算开销大:特征数 × 交叉验证折数 = 要训练的模型总数
  • 依赖基模型:基模型的特征重要性计算方式直接影响结果。换不同的基模型,可能选出不同的子集
相关推荐
CAE二次开发工程师1 小时前
【C语言入门到精通】-基础篇
c语言·开发语言·python
liulilittle2 小时前
归一化:激活函数
人工智能·算法·机器学习·llm
森G2 小时前
comfyui安装ComfyUI-Manager失败问题-解决办法
python
过期的秋刀鱼!2 小时前
倾斜数据集的错误指标-混淆矩阵
人工智能·python·深度学习·机器学习·概率论·模型评估
大鹏的NLP博客3 小时前
大模型 Tokenizer:从字符到 Byte,再到大词表
深度学习·机器学习·大模型·分词
能年玲奈喝榴莲牛奶3 小时前
使用AI编写-资产和漏洞管理系统
人工智能·python·网络安全·安全服务
DeepVisionary4 小时前
再观察小星火:当大厂分成“冰封“,国产轻模式创作者平台如何跑出加速度
python·自动化
gb42152874 小时前
python中unstructured库和langchain-unstructured库在解析pdf文件的时候的区别?
python·langchain·pdf
vx-程序开发4 小时前
django医院预约挂号系统---附源码23353
java·javascript·spring boot·python·eclipse·django·php