1. 先说清楚:RFE 到底在干什么
如果你玩过 sklearn 的特征选择模块,大概率见过这行代码:
from sklearn.feature_selection import RFE rfe = RFE(estimator, n_features_to_select=5, step=1)n_features_to_select是“最后想留下几个特征”,step是“每一轮淘汰几个特征”。我见过太多教程把前一个参数讲得很细,对step却一句话带过,导致很多人一直用默认值,等特征一多、训练时间飙上去,才开始怀疑哪里出了问题。这篇小白笔记就是专门把step这一个参数掰开揉碎讲清楚:它是什么、整数和浮点数怎么选、调错了会发生什么、用的时候有哪些坑。适合正在学特征工程、手动调参、或者第一次用 RFE 做建模的人。
1.1 从“淘汰制比赛”理解 RFE 核心流程
RFE 全称 Recursive Feature Elimination,递归特征消除。它做的事情很像选秀节目的淘汰赛:第一轮所有特征都上场,模型给每个特征打分,比如逻辑回归的系数绝对值、随机森林的特征重要性,末位的几个直接淘汰。剩下的人进入下一轮,用剩余特征重新建模、重新排名、再淘汰,直到队伍人数变成n_features_to_select。
这个流程之所以叫“递归”,是因为每轮淘汰之后都会用剩余特征重新训练一次评估器,重新计算一遍重要性,而不是一开始就按同一张榜单从头删到尾。这样做的核心价值在于:一个特征单独看可能不重要,但和其他特征组合在一起时可能很关键。如果只用一轮排序就删掉全部低分特征,很容易误杀这些“组合型选手”。RFE 通过一轮一轮地淘汰,让特征之间的相互作用慢慢浮出水面,最后留下的一组特征,往往是更稳、更可解释的组合。
从算法结构上看,RFE 是一个标准的前向包裹式搜索里反过来做的版本。主流实现里,评估器每个循环都要 fit 一次,所以时间是“迭代轮数 × 单次模型训练时间”。理解了这一条,你就会明白为什么step这么重要:它直接决定迭代轮数,进而决定整个特征选择要跑多久。
1.2 step 参数在每一次淘汰中的位置
step控制的是上面淘汰赛里“每一轮赶走几个人”。默认step=1,也就是每一轮只淘汰排名最后的那一个特征。100 个特征,目标留下 10 个,它大概要循环 90 次。每循环一次都要重新 fit 一次模型,如果模型本身比较复杂,这个时间成本会非常可观。
所以step的本质是一个“效率与精度”的旋钮。step调大,每轮淘汰更多人,迭代次数减少,训练时间缩短;但代价是,一次删掉过多特征,可能把某个真正有用的特征和一群垃圾特征一起扫地出门,而且后面再也不会给它翻身的机会。反过来,step太小,尤其是默认的 1,虽然搜索得最细致,但计算开销可能让人难以接受。
这里要额外说一句:RFE 是递归流程,每一步都必须等上一步结束才能继续,天然不适合并行。也就是说,想靠多进程把 RFE 加速并不容易。相比之下,调大step是最直接、成本最低的加速手段。所以,想要用好 RFE,就得理解step的两种形态,以及它们到底怎么影响每一步的计算。
2. step 的两种形态:整数与比例,差别比你想的大
在 sklearn 的 RFE 实现中,step可以传整数,也可以传(0,1)之间的浮点数。很多人不知道浮点数也能传,导致实际使用的时候只会写step=1,然后抱怨训练太慢。其实step的设计是有讲究的,两种形态对应完全不同的使用场景。
2.1 整数 step:每次固定揪掉几个
当step是大于等于 1 的整数时,含义很直白:每一轮从当前特征集合中移除step个特征。
举个具体例子。假设当前还剩 80 个特征,step=10,模型训练完成后按重要性排序,最后 10 个被淘汰,剩下 70 个进入下一轮。下一轮继续移 10 个,直到剩余特征数等于n_features_to_select为止。如果用 100 个特征、目标 10 个、step=20来做,特征数量的变化过程是这样的:
| 轮次 | 开始时的特征数 | 本轮移除数 | 结束时的特征数 |
|---|---|---|---|
| 1 | 100 | 20 | 80 |
| 2 | 80 | 20 | 60 |
| 3 | 60 | 20 | 40 |
| 4 | 40 | 20 | 20 |
| 5 | 20 | 10 | 10 |
注意第 5 轮,只剩 20 个特征,目标 10 个,step=20看似要一次删 20 个,但 sklearn 不会删过头,而是用max(20 - 20, 10)把剩余特征数钳制在 10。这个“截断”逻辑用公式表示就是:
n_features_remaining = max(n_features_remaining - step, n_features_to_select)很多人会忽略这个细节,以为step=10就一定要一轮删 10 个,实际上 RFE 永远以n_features_to_select为下限,不会删过头。这也是为什么你在代码里设置n_features_to_select=10,最终结果一定是 10 个特征,除非模型本身拟合有问题。
整数step的好处是可控、容易理解,但坏处也明显:在特征数量相差特别大的时候,固定删除数目不太均匀。一开始有 1000 个特征,step=10,前面删得很爽,但到了最后 50 个特征时,每一步仍然只删 10 个,迭代次数还是很多。这种情况下,你可能更希望“按比例删除”。
2.2 浮点数 step:按比例淘汰的“四舍五入陷阱”
当step是浮点数时,必须满足0 < step < 1,含义是每轮移除当前特征集合的step比例。
举例:当前有 100 个特征,step=0.2,那么这一轮会移除int(100 * 0.2) = 20个,剩下 80 个。下一轮有 80 个特征,移除int(80 * 0.2) = 16个,剩下 64 个。下一轮移除int(64 * 0.2) = 12个,因为12.8被向下取整成 12,剩下 52 个。我把这个过程列成一张表:
| 轮次 | 开始时的特征数 | 按 0.2 比例计算的移除数 | 结束时的特征数 |
|---|---|---|---|
| 1 | 100 | 20 | 80 |
| 2 | 80 | 16 | 64 |
| 3 | 64 | 12 | 52 |
| 4 | 52 | 10 | 42 |
| 5 | 42 | 8 | 34 |
可以看到,越往后每轮实际移除的数量越少,这符合“先粗后细”的直觉:特征还很多时,多删点不心疼;剩下二三十个时,每一步都要慢一点、稳一点。
这里有个容易踩的坑:sklearn 在计算这一步时使用的是int(step * n_features),不是四舍五入,而是向下取整。如果step * n_features算出来小于 1,代码内部还会保证至少移除 1 个特征。这个细节很关键,否则当特征少到一定程度,比例算出来是 0,RFE 就永远不会收敛了。
还有一个容易混淆的点:step=1.0不是“一次删除 100%”,而是被当成整数1来用。sklearn 的判定规则是“如果0 < step < 1,按比例;否则按整数”。所以你想用比例删除,必须写0.1、0.2这种严格小于 1 的小数,写1.0得到的效果和1完全相同。
浮点step最典型的取值是 0.1 到 0.3。我自己在跑高维数据时,习惯先设 0.2 或 0.3 快筛一遍,再对筛出来的候选特征做step=1的精细搜索。这样既能享受比例淘汰带来的加速,又能保留最终结果的可控性。
2.3 为什么默认值是 1,而不是更大的数
从算法原教旨主义的角度看,RFE 是一种贪心搜索方法:每一轮只移除当前“最不应该留下”的一个特征,以此逼近最优子集。step=1时,每一步的决策粒度最细,最终结果最接近理论上能搜到的好特征组合。
如果step调得太大,贪心路径很容易被带偏。举个例子,有 100 个特征,里面真正有用的有 8 个,其余全是噪声。假设某一次排序中,一个真正有用的特征排在第 15 名,而step=20,那么这一轮它会和后面 19 个噪声特征一起被删掉。后面无论怎么精修,它都不会再有机会回来。这就是一次性删太多带来的“不可逆误杀”。
所以 sklearn 把默认值设计成 1,不是为了让你训练慢,而是为了保住准确率。理解这一点之后,你就不会盲目把step调大成 50,然后奇怪为什么交叉验证分数掉了。当然,在特征非常多、计算资源紧张的情况下,大step也是合理的,但你要清楚代价是什么。
3. 实操:拿 sklearn 跑一遍,让 step 自己说话
理论讲得再多,不如动手跑一个对照实验。这里我写了一个非常简单的小实验,用合成数据对比step=1、step=10、step=20的行为。你可以直接复制到 Jupyter 里跑。
3.1 一个可以直接复制的对照实验
import time import numpy as np from sklearn.datasets import make_classification from sklearn.feature_selection import RFE from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score from sklearn.pipeline import Pipeline X, y = make_classification( n_samples=800, n_features=100, n_informative=10, n_redundant=40, random_state=42 ) for step in [1, 10, 20]: clf = LogisticRegression(max_iter=1000) rfe = RFE(clf, n_features_to_select=10, step=step) start = time.time() rfe.fit(X, y) cost = time.time() - start selected = np.flatnonzero(rfe.get_support(indices=True)) print(f"step={step:<3d} 耗时={cost:.3f}s 选中特征数={len(selected)}") selector = RFE(clf, n_features_to_select=10, step=step) pipe = Pipeline([("rfe", selector), ("clf", LogisticRegression(max_iter=1000))]) scores = cross_val_score(pipe, X, y, cv=5) print(f" CV准确率={scores.mean():.4f} (+/- {scores.std():.4f})")这段代码里,我特意把RFE放进Pipeline再交叉验证,是为了避免一个经典错误:在全部数据上先用 RFE 选特征,再用同一批数据评估模型,会导致结果虚高。放进 Pipeline 后,每一折训练都会在训练集内部重新做特征选择,测试集完全不参与选择过程,分数才是可信的。
我本地跑过的结果大致是:step=1耗时明显最长,CV 分数最高;step=20速度快很多,但 CV 分数低一点点。不同数据集上幅度不一样,但趋势基本稳定。更重要的是,你可能会发现三种step选出来的 10 个特征并不是同一批,有些特征只在step=1时被选中,在step=20时被早早淘汰了。这就是前面说的“一次删太多会误杀”的直接证据。
如果你想更直观地看每轮特征数量,可以自己写一个循环来模拟。RFE 本身不会把中间过程存下来,但你可以用ranking_和support_查看最终结果:
import pandas as pd rfe = RFE(LogisticRegression(max_iter=1000), n_features_to_select=10, step=10) rfe.fit(X, y) result = pd.DataFrame({ "feature": range(X.shape[1]), "rank": rfe.ranking_, "support": rfe.support_ }) print(result.sort_values("rank").head(15))support_是布尔数组,True 表示最终被选中;ranking_是特征排名,1 表示保留,数值越大表示越早被淘汰。组合起来看,能快速判断哪些特征留到了最后。
3.2 从迭代次数和模型分数反推 step 是否合理
实际工作中,我们没法直接拿到 RFE 的“迭代历史”,但可以从两个角度反推:一是估算法,二是看最终效果。
估算迭代次数可以这样想。总特征数和最终保留数之间的差值,就是整个过程中需要淘汰的总数。假设总共有 1000 个特征,目标是 10 个,需要淘汰 990 个。如果你希望整个流程控制在 20 轮以内,那么整数step至少要设为ceil(990 / 20) = 50。不过凭感觉设置不太稳,我更喜欢直接模拟一下比例step的收敛速度:
import math def estimate_rounds(n_features, n_to_select, step): rounds = 0 n = n_features while n > n_to_select: remove = int(step * n) if step < 1 else step remove = max(remove, 1) n = max(n - remove, n_to_select) rounds += 1 return rounds print(estimate_rounds(1000, 10, 0.1)) print(estimate_rounds(1000, 10, 0.2))这种估算方法很好用。跑代码之前先算一轮,你心里就有底了:step=0.1大概多少轮,step=0.2大概多少轮,然后再决定用哪个。
看最终效果则更简单。如果你发现step调大之后,选出的特征集合和step=1差异很大,而且交叉验证分数明显下降,说明当前step太大,已经偏离了精细搜索的路径。此时最简单的做法是:先把step调回 1,或者先用大step做一次“粗筛”,比如从 1000 个特征选到 100 个,然后再在 100 个特征上用step=1继续选到最终目标。这种“先粗后细”的策略,远比从头到尾用一个固定step更稳妥。
4. 遇到问题怎么办:step 相关的常见坑与排查
说了这么多,实际用的时候总会遇到一些奇奇怪怪的情况。这里我把常见的几类问题整理成一张速查表,再讲几个我真实遇到过的例子。
4.1 高频问题与解决速查表
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
设置了step=0或负数直接报错 | sklearn 要求step是 >=1 的整数或(0,1)的浮点数 | 改成 1、10 这类正整数,或 0.1、0.2 这类小数 |
step调大后,CV 分数明显变差 | 一次删除特征过多,误杀了有组合价值的特征 | 减小step,或先用大step粗筛,再小step精修 |
| 特征数量很多但耗时太长 | 一直用默认的step=1,迭代次数过多 | 改成 0.1 或 0.2 的比例step,先快速缩小特征集 |
程序报错说 estimator 没有feature_importances_或coef_ | RFE 要求评估器能输出特征重要性,但不是所有模型都可以 | 换成逻辑回归、线性 SVM、树模型等支持特征重要性的模型 |
| 特征数量变少后运行变慢 | 比例step越到后面删得越少 | 如果已经接近目标,可以换整数step或直接停手 |
和RFECV一起用时,不同折结果差异巨大 | 每折训练集不同,特征重要性排名不稳定 | 适当减小step,固定随机种子,必要时减少折数 |
想用比例删除但写了step=1.0 | 1.0 被当成整数 1,不是删除 100% | 改成 0.9、0.5 等严格小于 1 的小数 |
这张表里,第二个问题最值得关注。很多人看到step=10能明显加速,就直接把 1000 个特征的数据跑完了,结果模型分数比step=1低了一大截,最后还找不到原因。其实不是 RFE 坏了,而是step太大导致搜索太粗。
4.2 我实际踩过、也帮别人排查过的 3 个典型报错
先说我踩过的一个坑。有一回我用 RFE 配合随机森林做特征选择,数据有 5000 多个特征,我图快把step设成 0.5,结果不但没有更快,最后选出来的特征集合和业务方提供的“经验特征”几乎完全对不上。后来把step降到 0.2,再配合RFECV重新跑,才找回了原来几个重要的业务字段。这个经历让我彻底记住:step是效率开关,不是可以随意拉满的油门。
另一个常见误区是,有人以为step传浮点数时,每一轮删除的数量是按“原始特征总数”的固定比例计算。比如总共 100 个特征,step=0.1,他以为每一轮都固定删 10 个。实际上不是,sklearn 是按“当前剩余特征数”的比例来算的,越到后面删得越少。如果你没意识到这一点,可能会惊讶为什么迭代次数比自己预估的多很多。
还有一次是报错:ValueError: step must be greater than 0 or in (0, 1)。我检查代码才发现,某个循环变量传进来的是一个 numpy 浮点数,值刚好是 0.0。原因是数据预处理阶段把某个特征集过滤成了空集,导致比例计算为 0.0。排查思路很简单:在 fit 之前打印一下step的值和X.shape,确认特征数量不是 0。
除了报错,还有一个业务上的坑。做特征选择时,不同step选出的特征集合可能不稳定,这在跨部门协作时很容易引发信任问题。我以前遇到过算法组和业务组对不上的情况:算法组说step=20选出了 12 个关键特征,业务组用同样的数据跑step=1,选出的特征名字对不上几个。后来我们把step统一成 0.1,并且固定了随机种子,最终结果才稳定下来。这也提醒我:在正式项目中,step这种参数最好显式写清楚,不要依赖默认值。
5. 如果让我重设,我会怎么选 step
聊了这么多原理和排错,最后给一点我自己的经验。如果你也是“刚开始用 RFE,不知道 step 填什么”,可以按下面的思路来。
5.1 我的个人经验设置法
第一步,看特征总量。特征在 50 个以内,我无脑用step=1,因为迭代次数最多也就几十次,训练一个线性模型很快,完全没有必要牺牲精度。特征在几百到几千这个区间,我会先用step=0.1或0.2跑一轮,看选出的特征集合和业务直觉是否一致;如果一致,再用step=1或者小step的 RFECV 精修。特征过万时,我会直接用 0.2 到 0.3,先把特征集砍到几百,再用小step做第二轮。
第二步,看模型复杂度。如果你 RFE 里包的是一个深度神经网络或者非常大的树模型,每次 fit 都很贵,那么就算特征只有 200 个,step=1也可能让你等到怀疑人生。这种情况下,与其勉强用大step,不如先把n_features_to_select提高,或者换一个更轻量的线性评估器来做初筛。不要在一棵复杂的树上死磕。
第三步,看评估指标。如果你追求的是最终模型的预测能力,我会建议在 RFE 外面套RFECV,并刻意把min_features_to_select设得比目标稍微小一点,让交叉验证自己决定到底留下几个。此时step不需要太小,0.1 通常是个不错的平衡点。如果你追求的是特征可解释性,那么step=1带来的稳定特征集合更重要,计算慢一点可以接受。
5.2 一个小提醒:特征量级决定策略
最后提醒一句:不要在网上看到别人“step=1 永远最好”就照搬。这句话只在特征量级可控、模型轻量时才成立。真实业务里特征动辄几千上万,RFE 本身又要求反复训练模型,如果不把step调大,计算资源会被白白烧掉。反过来,也不要为了省时间疯狂调大step,牺牲掉对特征组合的精细探索。
我的做法一向是“先粗后细”:先用比例step快速缩范围,再用step=1精确收尾。比如一份 5000 特征的数据,我先step=0.2选到 200 个,再对 200 个特征做step=1的精细搜索。这套组合拳,既保住了 RFE 该有的效果,也没让训练时间失控。
希望这篇小白笔记能帮你把step这个参数彻底搞明白。下次再看到RFE(estimator, n_features_to_select=10, step=0.2),你至少能一眼看出:这大概是在用 1000 个特征做初筛,而且后面十有八九还要接一轮更精细的特征选择。