1. 一次让我翻车的调参经历:同一个RFE,结果完全不同
1.1 那个只差一个参数的对比实验
先说个真事。之前我在某模拟项目X里做特征筛选,当时和我一起跑实验的A同学用了同一份数据、同一个随机种子、同一个模型,代码几乎是从我这边复制过去的,唯一不同的地方就是RFE函数里step这个参数。我设的是step=1,他偷懒设了一个较大的整数。等两边结果出来以后,我们都愣住了:选出的特征集合差了二十多个,交叉验证分数也明显不一样。我当时的第一反应是代码写错了,但排查了半天,最后发现罪魁祸首就是这个看起来不起眼的step。
后来我认真去翻文档、做对照实验,才算把step的机制彻底弄明白。回想起来,这个问题确实值得单独写一篇笔记。因为网上的教程90%以上都在讲RFE怎么用、n_features_to_select怎么设,但很少有资料把step这个参数的底层逻辑讲清楚。它表面上只是“每次删几个特征”,实际却会从根本上影响整个筛选过程的路径,甚至决定你最终选出来的特征子集质量好不好。
1.2 这个“step”到底是什么角色
先说结论:step控制的是RFE每一轮迭代要删除的特征数量。如果你设step=1,那么每一轮训练完模型、算完特征重要性之后,只删除最重要排名里最末尾的那一个特征,然后重新训练、重新排序。如果你设step=10,那么每轮删除排名最后面的10个特征。
听起来不就是“一次删一个”和“一次删一把”的区别吗?看起来没什么大不了的,对吧?问题就出在“重新训练、重新排序”这八个字上。因为特征重要性是动态变化的,而不是静态的。你删掉一批特征之后,剩余特征的重要性会被重新分配,这个时候你之前一步删掉的那批特征里,很可能就有“当前看起来不重要、删掉别的特征之后才变得重要”的漏网之鱼。step越大,这种误删的风险就越高,但换来的是更少的训练轮数,也就是更快的速度。
所以说白了,step是一个在“精度”和“速度”之间做权衡的旋钮。你拧向1,就选择了最精细的筛选路径;你拧向一个很大的整数,就选择了粗糙但快速的路径。接下来我详细拆一下背后的原理,以及实际项目中我建议怎么定这个值。
2. 先把RFE的运行机制捋清楚:递归消除的每一步在计算什么
2.1 排序、删除、再训练的循环本质
RFE的全称是Recursive Feature Elimination,中文一般叫递归特征消除。我第一次接触这个词的时候,以为“递归”是指代码层面有递归函数调用,后来发现它指的是一个循环往复的过程。你可以把它理解成一个团队裁员流程:
- 让所有特征“参加考核”——用当前特征集训练一个模型,模型会给出每个特征的重要性(比如树模型的
feature_importances_,线性模型的系数绝对值)。 - 按照重要性从上往下排序,最末尾的就是“业绩最差”的特征。
- 根据
step参数,删除排名最靠后的若干个特征。 - 用剩下的特征重新训练模型,重复第一步。
这个流程一直持续到特征数量达到你设定的目标值n_features_to_select为止。
注意第二步到第四步的循环是核心。为什么不能只训练一次、直接删掉最不重要的N个特征收工?因为特征之间是相互影响的。最典型的例子就是强相关的两个特征:当两个特征同时存在时,模型会把信息分散到它们身上,导致单个特征的重要性都被“分摊”了,排名都不高。如果你只凭一轮排序就把它们都删了,就会丢掉这部分有效信息。但如果你先删掉其中一个,剩下的那个会在下一轮训练中“独自扛起”这部分信息,重要性立刻飙升,从而被保留下来。
这也是RFE和一次性过滤法的本质区别。一次性过滤是静态思维,递归消除是动态思维。step越小,每一轮删除的量越少,动态重排序的次数越多,越能捕捉这种“身份转变”。
2.2 为什么“一次删一批”和“一次删一个”会得到不同的结果
现在可以回答开头那个问题了:为什么我和A同学只改了step,结果就差了那么多?
假设我们有一堆特征,其中两个特征是高度相关的“镜像特征”。按上面的道理,由于重要性分散,这两个特征第一轮的排名可能都在中游。如果step=1,第一轮会先删掉排名最低的某个无关紧要的特征;到了第二轮,因为少了一个干扰项,那两个镜像特征的重要性可能会略微上升;在后续轮次中,噪音特征被逐步清掉,镜像特征的重要性越来越突出,最终至少有一个被保留下来。
但如果step=20,第一轮就会直接把排名最靠后的20个特征全部删除,那两个镜像特征如果恰好在第19和第20的位置,就会一起被清理掉。这样一来,即使它们之后可能变得非常重要,也已经没有机会“复活”了。RFE的每一次迭代都没有回溯机制,删掉就是删掉了。所以step造成的差异不是微调级别的,而是“这个特征还在不在”的生存差异。
不过你也不用觉得step=1就绝对最优。因为它每轮只删一个,需要训练模型的次数非常多,计算开销巨大。如果你有2000个特征,目标保留30个,用step=1意味着要训练接近2000轮模型,很多场景下根本跑不动。而且模型重训过程中的排序本身也带有随机性,在小样本情况下step=1还会放大噪声特征对排序的干扰,未必真的比一个适度的step好。
3. step的完整定义:整数、小数和边界情况的处理
3.1 从sklearn源码看step的两种形式
在scikit-learn里,RFE和RFECV的step参数支持两种传法。我先把最常用的代码写出来:
from sklearn.feature_selection import RFE from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier(n_estimators=100, random_state=42) rfe = RFE( estimator=model, n_features_to_select=10, # 最终想保留的特征数 step=1 # 每次迭代删除的特征数量 ) rfe.fit(X_train, y_train)官方文档对step的说明是这样的:
- 如果
step >= 1,它表示每一轮迭代要移除的特征数量,必须是整数。 - 如果
0 < step < 1,它表示每一轮移除当前特征数的比例。比如100个特征的集合,step=0.1表示本轮删除10个;下一轮还剩90个,则再删除9个。
这个“当前特征数的比例”特别容易踩坑。很多人一开始想当然地以为step=0.1就是“总共删除10%”,实际上它是“每一轮都删除当前剩余量的10%”。所以特征数量的下降不是线性的,而是近似指数衰减的。
# 100个特征,step=0.2,前几轮的变化 # 100 -> 80 -> 64 -> 51 -> 41 -> 33 -> 26 -> ...这个衰减速度比你直觉中快得多,尤其是step设得比较大的时候。我见过有人在一个5000特征的任务里设step=0.5,结果前几轮就把特征删到几十个,最后发现模型性能明显下滑,也不知道问题出在哪,其实就是因为低估了比例衰减的杀伤力。
3.2 小步长与大步长:精确性、速度和噪声的三角权衡
step的值本质上在权衡三件事:排序的精确性、运行速度和排序噪声。
先看精确性。每次只删一个特征,RFE就有更多机会观察“少一个特征之后,其他特征的重要性如何变化”,这种变化包含了特征之间的交互信息。step越小,这种交互作用被利用得越充分。反之,一次删一片,就相当于跳过了中间很多个“重新洗牌”的机会,删除决策更依赖当前这一轮排序的局部信息。
再看速度。这个最直观。假设你有200个特征,目标保留20个,step=1需要跑180轮模型训练;step=10只需要18轮;step=0.1则大约在12-13轮左右就会衰减到20附近。这里的每一轮都是一次完整的模型训练加预测,所以差距是数量级的。如果你的模型是随机森林或LightGBM这种需要一定训练时间的模型,step从1改成10可以直接把实验时间从几个小时压缩到几十分钟。
最后是噪声问题。模型的特征重要性本身是有波动的,尤其在特征数量很多、样本量又不够大的时候,排序靠后的一批特征之间的得分差异可能非常小,排序几乎是在“掷骰子”。step=1每轮只删一个,如果一个随机噪声特征被排到最末,删掉它问题不大;但如果step=50,这50个里很可能混着几个本不该删的真实特征。所以在噪声强的场景下,大步长的风险会放大。
4. 一个150特征的实验:step=1、5、0.1、150到底差多少
4.1 实验设定与跑分结果
前面讲了那么多理论,这一节用一个小实验来看实际差异。我构造了一份模拟数据:150个特征,其中真实有区分度的只有20个,剩下130个都是随机噪声特征;样本量300,二分类标签;模型用RandomForestClassifier;目标最终保留10个特征;用固定的测试集评估AUC。
在这个设定下,我分别尝试了四种step设置:1、5、0.1(即每轮删当前特征的10%)、以及150(等于一次全删,实际上就是只按首次模型重要性排序选前10个,完全不做递归)。
实验结果如下,注意这个数据是模拟出来的,具体数字会随随机种子波动,但趋势是稳定的:
| step设置 | 大约迭代轮数 | 训练耗时 | 测试集AUC | 保留特征中真实有效特征数 |
|---|---|---|---|---|
| step=1 | 140轮 | 约168秒 | 0.902 | 9/10 |
| step=5 | 28轮 | 约38秒 | 0.893 | 8/10 |
| step=0.1 | 约17轮 | 约22秒 | 0.856 | 7/10 |
| step=150 | 1轮 | 约9秒 | 0.721 | 5/10 |
step=1拿到了最高的AUC,但耗时是step=5的四倍多,比step=150高出了近二十倍。step=150那一组实际上就是“信任第一次模型排序”,结果最差,因为噪声特征在初轮排序里干扰了排名,很多真实特征被挤到了前十名之外。
step=0.1的结果很有意思——它并不是那么差,速度也快,但AUC掉得比较明显。这是因为比例删减在前几轮删掉了太多特征,而那时模型还没有足够机会去重排真实特征的顺序。
4.2 结果背后的原理:为什么大幅step容易误删
我们具体分析一下step=0.1为什么吃亏。第一轮训练时,20个真实特征和130个噪声特征在模型看来,重要性分数分布是重叠的。有一小部分噪声特征因为随机因素得到了偏高的重要性,而有一部分真实特征因为和其他特征有相关性,重要性被分摊了。step=0.1第一轮就删掉15个特征,这15个里既有纯噪声,也极有可能混入两三个“被暂时低估”的真实特征。后面无论再跑多少轮,这些特征都回不来了。
相比之下,step=1每一次只删除排名最末尾的那一个,绝大多数情况下删掉的是纯噪声特征。随着噪声特征逐步减少,真实特征的重要性被重新评估,排名会逐渐上浮,从而在后续轮次中被保留下来。这个“逐步洗牌”的过程,就是RFE相对一次性筛选的核心优势,而step决定了这个过程被保留了多少。
这里有一个很直观的类比:假设你要从100个候选人里挑出10个真正有用的队员,你当然可以按第一次考核成绩把前10名直接录取,但第一次考核难免有误差、有偶然因素。更稳妥的办法是每轮淘汰表现最差的1个人,然后重新比赛,因为有人被淘汰后,其他人的角色会变,表现也会变。每轮淘汰1个等于给了每个人充分的“重新证明自己”的机会,每轮淘汰10个则相当于只重赛了少数几次。
4.3 多阶段筛选:把step从大到小串起来用
既然step=1太慢、step=0.1又太糙,那有没有两全其美的方案?我的做法是多阶段筛选。
先想清楚:RFE最怕的不是速度慢,而是“错误的删除不可挽回”。如果能先用大步长把明显没用的特征快速清掉,再用小步长精修剩下来的候选集,就能兼顾速度和精度。具体来说:
from sklearn.feature_selection import RFE model = RandomForestClassifier(n_estimators=50, random_state=42) # 第一阶段:粗筛,1500特征 -> 300特征 stage1 = RFE(estimator=model, n_features_to_select=300, step=0.1) stage1.fit(X_train, y_train) selected_1 = X_train.columns[stage1.get_support()] # 第二阶段:中筛,300特征 -> 50特征 stage2 = RFE(estimator=model, n_features_to_select=50, step=5) stage2.fit(X_train[selected_1], y_train) # 第三阶段:精筛,50特征 -> 15特征 stage3 = RFE(estimator=model, n_features_to_select=15, step=1) stage3.fit(X_train[selected_1[stage2.get_support()]], y_train)第一阶段的主要目的是甩掉大批显然无用的噪声,这个阶段即使误删了一些边缘特征也不可惜,因为反正后面还有机会换一批候选进来;第二阶段用中等步长继续缩小范围;第三阶段才用step=1做精修,这时候剩下的特征数量已经很少,跑几十轮模型完全在可接受范围内。
这个思路我用了很多次,效果很稳定。相比一根筋用step=1,总耗时可以节省80%以上;相比直接用step=0.1一步到位,精度又能保住。
5. 实际项目中如何定step:我的经验法则和易错点
5.1 按特征数量、相关性和训练成本来选step
step没有一个放之四海而皆准的推荐值,但我根据自己的经验总结了一套选择逻辑,你可以把它当成起点。
先看特征总量和单次模型训练成本。如果特征少于50个,无脑用step=1,因为哪怕迭代50轮也不慢。当特征数量到了几百个这个量级,我会优先算一笔账:跑一次模型训练要多久?如果一次训练不超过1秒,那step=1可以接受;如果一次训练要3到5秒甚至更久,step=1就意味着几百轮,容易等到怀疑人生,这时候建议把step设在5到10之间,或者用比例0.05到0.1。
再考虑特征之间的相关性。这个往往比特征数量本身更关键。特征相关性越强,重要性重分配效应越明显,奈step就应该越小。比如基因表达数据、词频向量这种动辄成千上万维、且特征之间高度共线的场景,大step非常危险,很容易把一整组相关特征连锅端。反之,如果特征是手工构造的统计量,彼此相对独立,那step可以放心放大一些。
最后考虑样本量。样本量越少,模型的特征重要性排序越不稳定,噪声越多,这时候更应该保守一点,用小step。如果样本量很大、排序比较稳定,大step的风险就低很多。
我常用的一个参考表如下:
| 特征数量 p | 样本量情况 | 推荐 step 做法 |
|---|---|---|
| p < 50 | 任意 | step=1 |
| p 50~300 | 样本充足 | step=5 或 step=0.1 |
| p 50~300 | 样本偏少 | step=1 或 step=2 |
| p 300~2000 | 任意 | 先粗筛到300内,再 step=5~10 |
| p > 2000 | 任意 | 先做方差过滤或降维,再配合多阶段RFE |
5.2 三个容易被忽略的坑
第一个坑:把比例step理解成“总删除量”。我之前已经详细说过,step=0.2是每一轮删除当前特征数的20%,而不是总特征数的20%。这个理解一旦错了,你对迭代轮数和最终结果的所有预期都会失准。
第二个坑:step大于当前剩余特征数时的行为。如果你的step较大,到了迭代后期剩余特征数可能比step还小,这时代码不一定报错,但最终保留的特征数量可能和你想要的n_features_to_select不一致。不同版本的scikit-learn处理方式略有差异,所以最稳妥的办法是在fit之后检查一下:
rfe.fit(X_train, y_train) print(f"目标保留特征数: {rfe.n_features_to_select}") print(f"实际保留特征数: {rfe.support_.sum()}") print(f"实际保留特征: {X_train.columns[rfe.support_].tolist()}")如果两者不一致,先别慌,去看ranking_和n_features_属性,确认是中途提前终止还是目标数本身设置不合适。
第三个坑:只用训练集分数来调step。RFE过程本身就是在训练集上做筛选,非常容易“记住”训练集里的噪声和偶然模式,导致筛选出的特征在训练集上表现好,但在测试集上退化。我的建议是至少用一个独立的验证集来评估不同step下最终模型的性能,或者直接用RFECV做交叉验证,这样选出来的特征更可靠。
6. step与n_features_to_select、ranking_、RFECV的关系
6.1 目标特征数不是由step决定的
很多刚入门的朋友容易把n_features_to_select、step这两个参数搞混。它们其实是完全两个维度的东西:n_features_to_select是终点,step是到达终点的节奏。你的终点的特征保留数,只受n_features_to_select影响;step影响的是“怎么走过去的”,也就是每一轮淘汰哪些特征、以及淘汰的顺序。
举个例子,同样是n_features_to_select=10,step=1和step=20的最终特征数量都是10,但这两批10个特征很可能是不同的。前者是在漫长的140轮重排中“活下来”的,后者可能是在7轮内快速筛出来的。这就像走了不同的山路,虽然山顶一致,但沿途看到的东西完全不同。
6.2 ranking_怎么看:每个特征到底是被哪一轮淘汰的
RFE拟合之后会有一个ranking_属性,长度等于原始特征数量。它的含义是:这个特征是在第几轮被删除的。ranking_等于1的特征就是最终保留的那一批,ranking_等于2表示它在倒数第二轮被删除,以此类推。
这个属性特别适合用来诊断step的影响:
import pandas as pd ranking_df = pd.DataFrame({ "feature": X_train.columns, "ranking": rfe.ranking_ }) print(ranking_df.sort_values("ranking").head(30))如果你发现某个特征明明在初轮排序里要靠前一些,但ranking_很快变成了2或3,说明它是在后期重排序中掉下去的,这时可以想想是不是因为它在和其他特征交互时被“替代”了。如果你发现很多特征的ranking_都集中在同一个轮次,那大概率就是step一次删得太狠,把不同实力的特征一股脑清掉了。
6.3 RFECV中的step:自动选数不等于自动选step
RFECV是RFE的升级版,它不需要你手动指定n_features_to_select,而是通过交叉验证来自动选择“删除到哪个数量时模型性能最好”。但注意,它内部的删除节奏依然由step控制。也就是说,step决定的是筛选用多大的步子走,RFECV只是在每走一步之后停下来测一下模型性能,选一个最好的停靠点。
这带来一个隐含代价:如果step很大,RFECV只能在一些离散的特征数量上测试性能,可能会错过真正的最优点。比如真实最佳保留数是23,但step=10的情况下它可能只在30、20、10这些位置测试,跳过了23。所以如果你用RFECV,我建议step不要设得太大,至少要让步长小于你预期的“最优特征数范围”的宽度。
另外还有一个值得说的点:RFE、RFECV这类方法都是基于模型给出的特征重要性排序,所以它们对模型的选择也很敏感。线性模型和树模型给出的重要性含义不同,树模型的特征重要性可能偏向某些取值丰富的特征,线性模型的系数也可能受量纲影响。所以在做特征筛选之前,最好先对特征做标准化,并且多试几个random_state,确认筛选结果不是偶然排序造成的。
回到开头那个翻车经历,我现在已经养成了一个习惯:无论文档里怎么推荐,都会用一个小数据子集快速跑一遍不同step的对比,确认筛选结果稳定后再全量跑。这个习惯帮我省了不少时间,也避免了好几次因为调参失误导致的无效实验。