1. 嵌入法到底在解决什么问题
做特征选择绕不开三条路:过滤式、包裹式、嵌入式。前几篇已经把过滤式和包裹式的实操讲得差不多了,今天专门拆嵌入法(Embedded)。
先记住一句话:嵌入法把“特征筛选”直接塞进模型训练过程里。模型一边拟合目标函数,一边给每个特征分配权重或打重要性分,训练一结束,特征的重要性排名也就出来了。整个过程不需要像包裹式那样反复训练特征子集,也不需要像过滤式那样把特征和模型割裂开单独算统计量。这也是嵌入法叫“嵌入”的原因——选择动作内嵌于训练本身。
适合看这篇的人有三类:一是手里特征动辄上百个,想快速找到对预测真正有用的那一小部分;二是被包裹式方法(比如递归特征消除)的计算量劝退,想找更省时的筛选方式;三是你已经在用线性模型或树模型,但对Lasso系数和feature_importances_到底能不能信、怎么用来选特征这件事缺乏系统理解。
1.1 过滤式、包裹式、嵌入式的本质区别
先放一张总表,方便理解嵌入法在整个特征选择体系中的位置。
| 方法 | 评估粒度 | 是否依赖具体模型 | 计算成本 | 典型代表 |
|---|---|---|---|---|
| 过滤式 | 单个特征与标签的关系 | 否 | 极低 | 方差、卡方、互信息、相关系数 |
| 包裹式 | 特征子集的整体预测能力 | 是,模型当黑盒反复试探 | 高 | RFE、递归特征消除、前向选择 |
| 嵌入式 | 特征在模型目标函数中的贡献 | 是,参与训练过程 | 中 | Lasso、树模型重要性 |
为什么嵌入式要放到过滤式和包裹式之后才讲?因为它对模型有硬性要求:你手上必须有一个能产出权重或重要性的模型。同时,前面做过的数据清洗、编码、分箱、缺失值处理这些前置工作一样也不能少。换句话说,嵌入法是站在前两种方法肩膀上的精细操作,前置工作不到位,嵌入式的输出就会失真。
用生活化类比来说明三者的差异:过滤式像相亲只看简历上的身高学历,不相处不知道合不合拍;包裹式像反复约会试错,成本高但最贴近真实;嵌入式像一边相处一边打分,合适就让对方留下,不合适直接淘汰,效率和效果都在线。
1.2 嵌入式“嵌入”的到底是什么
我习惯把嵌入法理解成:把“特征选择函数”做进了损失函数里。
以线性模型为例,普通最小二乘只优化误差项,所有特征都会分到或大或小的系数,没有一个真正的0。加了L1惩罚后,目标函数多了一项“所有系数绝对值之和”,优化器在误差和惩罚之间权衡,对预测贡献微弱的特征,系数会直接被压到0。这个“压到0”的动作,就是选择动作本身。
树模型那边同理。随机森林、XGBoost、LightGBM训练时不断寻找最优分裂特征,每次分裂都会让节点纯度提升,这种“提升量”被累计到对应特征头上,就成了我们常见的特征重要性。训练结束,重要性的排序就是筛选依据。
所以嵌入式的核心答案是:不需要额外设计一套筛选流程,模型的训练过程本身就包含了特征评估。这是它效率高的根本原因。
1.3 单特征视角和组合视角的差异
过滤式的主要问题是只看单特征与标签的关系,完全看不到特征之间的组合效应。包裹式虽然看组合,但搜索空间是指数级增长的,特征一多就完全跑不动。嵌入法则站在中间:它考虑的是“在当前模型假设下,某个特征对整体目标函数的最优解有多大贡献”。
它能看到特征在模型结构里的作用。树模型天然能捕捉特征交互,某个特征单独看与标签关系很弱,但和另一个特征组合起来价值很大,过滤式会漏掉,树模型的增益机制能抓住。Lasso则把相关系数高的特征视为冗余,自动挑一个留下。这种“组合视角”让嵌入式在精度和效率之间找到了一个平衡点。
2. 嵌入式的两大技术流派
嵌入式具体落地,基本分裂成两个流派:正则化流派和树模型流派。两者原理不同、产出物不同、适用场景也不同,搞清楚它们的差异,才能在不同数据集上选对工具。
2.1 正则化流派:L1为什么能把系数压成0
回归问题的优化目标可以写成误差项加惩罚项。L1惩罚是所有系数绝对值之和,L2惩罚是所有系数平方和。两者只有一个字母之差,行为却天差地别。
图解能看得很清楚:L1的约束区域是菱形,顶点正好落在坐标轴上;误差项的等值线是椭圆,优化时等值线碰到约束边界,极大概率先碰到菱形的顶点,交点处某个系数正好为0。L2的约束区域是圆形,等值线只会沿着圆周压缩,系数整体变小但不会归零。这就是为什么L1天然适合做特征选择,L2天然适合处理共线性。选择L1还是L2,本质上是选择“稀疏”还是“收缩”。
正则化强度由alpha(也叫lambda)控制。alpha越大,惩罚越狠,系数越稀疏,保留的特征越少;alpha越小,越接近普通最小二乘,特征几乎全留下。实操中千万不要手动试alpha,用LassoCV直接在交叉验证里搜最优值,又快又稳。
还有一个容易踩的点:当特征之间强相关时,L1会“随机”挑一个代表性特征,另一个直接被归零,而且挑谁不稳定。这时候可以用ElasticNet,也就是L1加L2混合惩罚。它在L1稀疏的基础上多了一层L2收缩,相关特征组的权重会被平摊,稳定性好很多。我的经验是,一旦发现特征相关度明显,直接上ElasticNet,别再纠结Lasso。
2.2 树模型流派:重要性分数怎么算出来的
树模型的特征重要性有三种常见口径,很多人混着用,但它们的内涵完全不同。
第一种是最常用也最容易出问题的“基于分裂增益”的重要性。每次分裂都会降低节点不纯度,分类用基尼或信息熵,回归用MSE。特征被选为分裂点的次数越多、带来的不纯度下降越大,累计重要性越高。XGBoost和LightGBM在此基础上还细分出weight(被选中分裂的次数)、gain(每次分裂的平均增益)、cover(覆盖的样本量)。默认输出的是weight,但这只能说明“这个特征经常被用到”,不能完全反映“贡献多大”。想评估真实贡献,看gain更有意义。
XGBoost和LightGBM里取gain型重要性的方式不太一样,以LightGBM为例:
import pandas as pd import lightgbm as lgb model = lgb.LGBMRegressor(n_estimators=200, random_state=42) model.fit(X, y) importance = pd.Series( model.booster_.feature_importance(importance_type='gain'), index=[f'feature_{i}' for i in range(X.shape[1])] ).sort_values(ascending=False) print(importance.head(20))第二种是排列重要性(permutation importance),做法是训练完模型后,把某个特征列随机打乱,再看模型效果下降多少,下降越多说明该特征越重要。严格来说它不属于嵌入法,因为发生在训练之后,是后验评估。我在这里专门提它,是提醒你别把模型自带的重要性和排列重要性混为一谈,两者统计口径不同,排列重要性更接近“该特征对预测能力的直接影响”。
树模型重要性有个天生缺陷:数值型特征和高基数特征容易被高估。因为可切分点多,偶然获得的“伪增益”也多。所以比较重要性要在同类型特征内部比,不要拿一个连续变量和一个二分类变量的重要性绝对值直接对比,那是没有意义的。
2.3 根据业务场景选流派
两个流派谁更强,取决于数据和业务目标,我从经验出发给一个选型方向:
- 线性关系为主、特征全是数值型、希望得到稀疏系数方便出业务规则,选L1或ElasticNet。
- 特征之间存在交互、类型混杂、有大量类别变量,选树模型。
- 特征维度上万甚至更高,L1迭代快,树模型的增益计算虽然也扛得住,但重要性解释成本更高。
- 业务上要求可解释性,Lasso系数的正负号有直接含义;树模型只能给出相对排名。
实操里我喜欢“两步走”:先用Lasso做一次粗筛把维度降下来,比如500个特征压到50个,再把筛选后的特征喂给树模型做精细调参。这样既吃到Lasso的稀疏优点,又不浪费树模型对非线性关系的捕捉能力。这种“嵌入式加嵌入式”的组合,实际用下来比单用一种效果好。
3. 实操:把嵌入法落地
理论讲完,直接上代码。下面以scikit-learn为主,讲清楚LassoCV、SelectFromModel和Pipeline三个核心工具的用法和背后的原理。
3.1 LassoCV自动选alpha
第一步是标准化。L1惩罚项直接加在系数绝对值上,如果特征量纲不同,数值大的特征系数天然就小,惩罚力度完全不一致。标准化后所有特征在同一尺度上,Lasso的系数才是可比的。
第二步是用LassoCV自动搜alpha,以回归任务为例:
import numpy as np import pandas as pd from sklearn.datasets import make_regression from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LassoCV X, y = make_regression(n_samples=500, n_features=80, n_informative=20, noise=0.5, random_state=42) scaler = StandardScaler() X_scaled = scaler.fit_transform(X) lasso = LassoCV(cv=5, random_state=42, max_iter=10000, n_alphas=200) lasso.fit(X_scaled, y) coef = pd.Series(lasso.coef_, index=[f'feature_{i}' for i in range(X.shape[1])]) selected = coef[coef != 0] print('保留特征数:', len(selected)) print('最优alpha:', lasso.alpha_)这里有几个值得说透的细节。LassoCV内部会用交叉验证搜索最优alpha,n_alphas=200意味着alpha搜索路径划分得很细,找到的最优alpha更精确。代价是训练时间稍长,但Lasso本身是线性复杂度,完全可接受。
max_iter是我踩过坑的地方。LassoCV默认迭代上限是1000,特征维度稍高就报坐标下降不收敛警告。直接把max_iter拉到10000甚至50000,问题基本消失。
筛选出来的特征就是coef不为0的那些,不需要额外设定阈值。如果你想要更严格的筛选,也可以对非零系数再按绝对值排序,只取前N个,这适合业务上有特征数量上限的场景。
3.2 树模型加SelectFromModel
树模型做特征选择时,scikit-learn的SelectFromModel是官方推荐的高效封装。它的原理很简单:对模型fit一次,读取model.feature_importances_(或coef_),再按阈值决定保留哪些特征。
from sklearn.ensemble import RandomForestRegressor from sklearn.feature_selection import SelectFromModel rf = RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1) sfm = SelectFromModel(rf, threshold="median", max_features=30) sfm.fit(X, y) selected_mask = sfm.get_support() print('保留特征数:', selected_mask.sum()) selected_features = [f'feature_{i}' for i in range(X.shape[1]) if selected_mask[i]] print(selected_features)threshold参数控制保留标准。默认值是"mean",也就是保留重要性高于平均值的特征。但机器学习里的重要性分布通常呈长尾,少数特征占据大部分重要性,此时mean会把大部分特征过掉,筛选过于激进。threshold="median"保留重要性高于中位数的特征,约等于保留一半,更温和一些。
max_features参数用来设置保留数的上限。当你对特征数量有明确约束,比如只准100个特征入模,它在threshold之上再卡一道闸,非常实用。
实际使用前,我强烈建议先看一眼重要性的分布,打印排序后的重要性数值,再定threshold。很多教程直接告诉你用median,但它默认删掉一半特征,可能过于保守也可能过于激进,还是要看具体数据。
3.3 把选择器放进Pipeline的正确姿势
嵌入法最容易被忽视的坑:先在完整数据集上跑Lasso选特征,再对选出来的特征子集切训练测试集、训练下游模型。这样做的结果是模型评估分数虚高,因为特征选择环节已经看到了全量标签信息,属于典型的信息泄露。
解决办法是把特征选择器放进Pipeline,让交叉验证把“选择加建模”整条链路一起评估:
from sklearn.pipeline import Pipeline from sklearn.linear_model import Ridge from sklearn.model_selection import cross_val_score pipe = Pipeline([ ('scale', StandardScaler()), ('select', SelectFromModel(LassoCV(cv=3, random_state=42, max_iter=5000), threshold="median")), ('model', Ridge(alpha=1.0)) ]) scores = cross_val_score(pipe, X, y, cv=5, scoring='r2') print('CV R2:', scores.mean())这段代码里,select环节在每一折交叉验证内部重新执行,选择依据只来自训练折,不会碰到验证折的信息。这个原则同样适用于过滤式特征选择——只要选择过程使用了标签信息,就必须放在CV内部。这个点我会反复强调,因为它直接影响整个模型评估体系的真实性。
4. 常见坑与排查实录
嵌入法用久了,会遇到一些重复出现的坑。下面这些场景基本覆盖了我在多个项目里踩过的典型问题,遇到相似情况可以先对号入座。
4.1 特征重要性不等于真实因果
嵌入法筛出来的特征,只能说明“在当前模型和数据下,该特征对预测有用”,不能说明“改变这个特征就能改变结果”。我在一个信贷评分场景里遇到过,模型里“年龄”的重要性很高,业务同学觉得年龄是关键因子,差点据此设计差异化策略。但事实上年龄反映的是其他社会经济变量的复合影响,直接拿去做业务归因,很容易得出误导性结论。
还有一类偏差来自样本分布。正负样本比例严重失衡时,少数类样本对应的特征往往被低估,因为树模型分裂主要被多数类主导。处理方式是训练时设置class_weight,或者对样本加权后再计算重要性。样本不均衡问题不处理,嵌入式选出来的特征集可能只对多数类有效。
4.2 高相关性特征会把重要性摊薄
两个特征强相关时,树模型每次分裂会随机选择其中一个,导致两者重要性都被压低。这不是模型坏了,而是模型在“节俭”——它只要一个特征就够用了。Lasso的表现更直接,L1会挑一个特征强制归零另一个。
我常用的规避手段有三个:
- 先用相关性热图或聚类把强相关特征归成组,从每组里保留一个代表特征。
- 树模型下把强相关特征的重要性直接相加,看“组重要性”而不是单特征重要性。
- 用ElasticNet替代Lasso,让相关特征组的权重被L2平摊,选特征更稳定。
如果你发现重要性的排名在两次训练之间剧烈变动,第一件事就去查特征相关矩阵,大概率能找到一群高度相关的特征在那里“打架”。
4.3 特征数大于样本数时L1的表现
当特征维度远大于样本量,也就是常说的p远大于n场景,Lasso选出非零系数的特征数大约最多不超过样本量n,而且选谁相当不稳定。alpha稍微变一点,选出的特征集会剧烈变化。这不是代码写错了,是L1在超高维下的天然特性。
解决思路是分层处理:先用过滤式把明显没用的特征清掉,比如方差过滤和相关性分析,把维度降到样本量的可接受范围,再上嵌入法做精细筛选。如果必须处理超高维,用ElasticNet会比Lasso稳定得多,唯一代价是选出来的特征没那么“硬核稀疏”。
4.4 阈值到底怎么定
很多教程说threshold="median"简单方便,但它默认删掉一半特征,这个“一半”可能太保守也可能太激进。我的做法是画重要性排序曲线,看前多少特征贡献了80%以上的累计重要性,找肘部位置作为自然阈值点。操作不复杂,把importance排序后算累计占比画个折线图就行。
如果业务上对特征数量有硬性约束,比如上线流程只允许10个特征,那就没有讨论空间,阈值直接按数量倒推。如果没有约束,我建议在交叉验证框架下对threshold做网格搜索,对比不同阈值下模型效果,而不是拍脑袋定“一半”。
4.5 类别特征和缺失值的影响
独热编码后的类别特征喂给Lasso,会让特征数量暴涨,大部分虚拟变量系数为0,符合预期但特征空间很大、训练变慢。我一般先对类别做目标编码或频次编码,压缩维度后再上Lasso。树模型对类别特征的原生支持更好,但连续编码的排序方式会影响分裂逻辑,需要多留意。
缺失值方面,Lasso完全不能容忍缺失,必须先填补;部分梯度提升树框架内部可以处理缺失值,默认策略是把它分到增益最大的方向,所以保留缺失值本身也是一种选择,要不要用取决于业务含义。比如“收入缺失”可能本身就含有“群体标签”的信息,直接填补反而丢掉信息。
这里给一个常见问题速查表,遇到类似情况可以先对照排查:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| Lasso报警不收敛 | max_iter太小或特征未标准化 | 标准化、max_iter拉到10000以上 |
| 选中特征全是数值型 | 树模型对类别特征有偏 | 同类型特征内比较,或调大min_samples_leaf |
| 两次运行保留特征集差异大 | 随机性加特征强相关 | 固定random_state、用ElasticNet、多次跑取交集 |
| CV分数诡异偏高 | 特征选择在CV外面做了 | 把选择器放进Pipeline再评估 |
| 重要性排名和业务认知相悖 | 相关性不等于因果 | 检查相关矩阵和样本分布,勿直接下结论 |
5. 关于嵌入法,我最后想说的话
前面把原理和代码都过了一遍,最后分享几个我在项目里沉淀下来的操作习惯,这些细节教科书里很少写,但对实际选特征帮助很大。
5.1 稳定性优先,三跑取交集
嵌入法选出来的特征不一定稳定。我自己的标准操作是:固定random_state,把LassoCV跑三遍,每次换不同的交叉验证折法,取三次非零系数的交集作为最终特征集。交集里的特征才是“无论如何都重要”的特征。这个做法偏保守,但换来的是后续建模时特征集不会反复变动,省掉大量返工时间。
树模型也一样。临时跑出来的重要性排名可能有运气成分,多跑几次取稳定出现的头部特征,比单跑一次的结果可信得多。
5.2 树模型要看gain而不是weight
用梯度提升树时,默认输出往往是weight(被选中分裂的次数)。一个特征如果经常被选中做分裂,很可能是因为它在某个局部数据子群里有效,对全局贡献有限。gain,也就是平均增益,更能反映“每次分裂带来的实际质量”。
我习惯把gain排名前20和weight排名前20拉出来对比,差异大的特征单独检查。曾经有一个特征在weight里排第三,但gain排到五十几名,最后确认它是被频繁使用却每分裂贡献很小的“工具人”特征,对整体预测帮助有限,去掉之后模型效果不降反升。
5.3 嵌入法不是一劳永逸
特征选择结果绑定在训练时的数据分布上。业务环境里数据分布会漂移,上个月重要度很高的特征,这个月可能完全不重要。我的习惯是给特征重要性设一个“保质期”,模型上线后定期重算重要性,一旦发现老特征集体失效、新特征大量浮现,就考虑重训加重选。这个动作听起来麻烦,但比模型悄悄变差再抢救要省事得多。
嵌入法是我做特征选择时最常用的工具箱,它不像过滤式那样单薄,也不像包裹式那样昂贵。但记住一点:它给出的是“当前模型视角下的重要性”,不是“客观世界里的重要性”。带着这个认知去用,嵌入法能帮你把特征工程做得又快又稳。