资讯动态

LASSO与弹性网络实战:高维数据特征筛选与Python实现指南

发布时间:2026/10/21 8:18:31 来源:尧图企业网站定制
最近在折腾一组高维数据特征数量超过了样本量的一半普通线性回归直接跑出个虚高的R方测试集上一塌糊涂。我重新把LASSO回归和弹性网络回归从头到尾捋了一遍顺便把Python代码也做了封装。这篇文章就把原理、实操和踩坑经验一次性讲透适合正在学正则化模型、或者想在项目里做特征筛选但不确定选哪个模型的朋友。1. 为什么普通线性回归和岭回归都满足不了“稀疏筛选”需求1.1 最小二乘在高维场景下的过拟合本质普通线性回归的目标是把残差平方和压到最低。如果特征维度少、样本量大这个目标没有太大问题。可一旦特征数接近甚至超过样本数最小二乘的正规方程解就会出现严重的不稳定性。我打个比方你现在有100个样本却要估计120个系数。这相当于用100个方程去解120个未知数解出来的系数会剧烈波动甚至出现极大正数和极大负数相互抵消的情况。这时候训练集的R方可能高得吓人但换一批样本立刻崩盘。再举一个经典例子两个强相关的特征X1和X2真实关系是y2X10X2。在有限样本下最小二乘可能给出系数组合比如3.5和-1.4整体预测能力还行但单独看任何一个系数都会得出“这个特征很重要”的错误结论。这都是过拟合的表现。要抑制这种不稳定就得给损失函数加上正则化项用小小的偏差换取大大的方差下降。1.2 岭回归的局限收缩了但一个系数都不会变成零岭回归加的是L2惩罚也就是系数平方和。它能让系数整体变小、模型更稳定这是它的好处。但问题在于L2惩罚的几何形状是球形的约束域它会收缩每个系数却不会让任何一个系数恰好归零。这意味着什么如果我有300个特征做完岭回归依然得到300个非零系数。你问它“哪些特征无关”它一个都说不出来。这在某些需要解释性很强的场景下就很难受。比如医疗或风控项目业务方想知道的是“到底哪几个变量在起作用”而不是“这300个变量多多少少都有点用”。退回一步说部署模型时300个特征就意味着要采集、清洗、存储300个特征的生产数据成本不小。1.3 真正需要的是“既收缩又清零”的正则化方式带着这个需求L1正则化就进入了视野。L1惩罚是系数绝对值之和它不仅会把大系数缩小还会把不重要的系数直接压成零。这就实现了自动特征选择帮你从一堆特征里挑出真正有贡献的那部分其余全部忽略。LASSO回归Least Absolute Shrinkage and Selection Operator最小绝对收缩与选择算子就是L1正则化的线性回归。它既解决了过拟合又解决了特征筛选问题尤其适合“特征多、样本不多、大部分特征其实是噪声”的数据场景。当然L1正则化也不是万能药。如果特征高度相关它的表现会不太稳定。这时候就需要弹性网络回归把L1和L2惩罚组合起来用既保留稀疏性又借助L2的稳定效应。接下来我逐个拆解。2. LASSO的目标函数与几何直觉L1惩罚为什么能让系数变零2.1 目标函数形式和数据标准化的必要性LASSO回归的优化目标可以写成[ \min_{w} \frac{1}{2n} | y - Xw |_2^2 \alpha | w |_1 ]第一部分是均方误差第二部分是L1正则项。这里的 (\alpha) 是正则化强度越大惩罚越重系数被压得越狠最终稀疏程度越高。在动手写代码前有一个前置步骤绝不能被省略特征标准化。L1惩罚是对系数绝对值求和而系数的尺度受特征量纲影响。假设特征A的取值范围是0到1特征B的取值范围是0到100000那么同样重要的两个特征在L1惩罚下受到的“待遇”完全不同。B的系数天然会小很多为了压缩B惩罚项可能更容易把它误伤清零。所以实战中我会先把所有特征做StandardScaler标准化让它们都处于同一量纲。这一步不是可选项是必选项。后面专门的避坑章节还会细说。2.2 约束域的几何形状菱形和球体的差别回顾一下L1和L2的数学形式。对于同一目标函数二者可以等价写成带约束的优化问题L1约束(|w|_1 \le t)在二维坐标系里是个旋转45度的菱形。L2约束(|w|_2^2 \le t)在二维坐标系里是个圆形。无约束的普通最小二乘解可能落在约束域外部我们需要在约束域内找到使损失函数最小的点。损失函数的等值线是椭圆形的沿着约束区域边界滑动时和L1菱形的交点更容易出现在“顶点”上也就是某个坐标等于0的位置。而圆形边界光滑切点几乎不会落在坐标轴上。这就解释了为什么Lasso能产生严格为零的系数而岭回归只是把所有系数整体压缩。我在给别人讲的时候喜欢说L1约束是一个“带棱角的框”你在这个框里找最优点往往卡在棱角上L2约束是“光滑的球”怎么滚都很难停在坐标轴上。这个直觉非常有用。2.3 软阈值算子是L1解的核心机制从算法层面看坐标下降法求解Lasso时每一次对单个系数的更新其实就是一个软阈值操作[ w_j \text{sign}(z_j) \cdot \max(|z_j| - \alpha, 0) ]其中 (z_j) 是当前残差与第 (j) 个特征的相关性量。稍微极端地说只要 (|z_j|) 没有超过惩罚强度 (\alpha)这个系数就会被直接置零超过一部分就整体向零收缩一段距离。这个操作非常直觉化先看这个特征和当前残差的相关性强不强弱就一刀切掉强就保留一部分。循环迭代所有坐标直到收敛就得到了最终的稀疏解。注意这个公式的前提是特征正交实际非正交情况下需要加一个分母项但理解它的行为逻辑完全够了。3. Python实现LASSO回归完整流程与代码拆解3.1 生成模拟数据先看稀疏解长什么样为了避免直接使用内部数据集造成干扰我通常先用模拟数据验证逻辑。下面的数据生成中只有3个特征对标签有真实贡献其余50个都是噪声特征。import numpy as np from sklearn.linear_model import Lasso, LinearRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import r2_score, mean_squared_error np.random.seed(42) n_samples 200 n_features 60 X np.random.randn(n_samples, n_features) true_coef np.zeros(n_features) true_coef[:3] [3.5, -2.0, 1.2] y X true_coef np.random.randn(n_samples) * 0.5 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, random_state42)数据里第0、1、2号特征是真正有效的其他57个都是随机噪声。真实系数我设定为 [3.5, -2.0, 1.2]用来验证模型能否把这个稀疏结构找回来。先看不加正则化的普通线性回归会怎样。60个特征在140个训练样本上拟合优度看着还行但求得的部分噪声系数会很大。这里我直接给大家展示结果lr LinearRegression().fit(X_train, y_train) lr_test_r2 r2_score(y_test, lr.predict(X_test)) print(f普通线性回归测试集R2: {lr_test_r2:.4f}) print(f非零系数个数: {np.sum(lr.coef_ ! 0)})输出会显示测试集R2明显低于训练集而且非零系数仍然是60个。虽然在这个模拟里普通回归还不至于完全崩掉但它显然把大量的噪声特征都保留了下来。3.2 先标准化还是先划分数据集顺序错了全盘皆错在训练任何带惩罚的线性模型之前必须对特征做标准化但标准化操作有一个很容易踩的细节先划分训练集和测试集再在训练集上fit标准化器最后用同一个标准化器去transform训练集和测试集。绝对不能先把整个数据集统一fit再划分训练测试。因为那样做会让标准化器“偷看”到测试集的均值和方差造成信息泄露测试集评估自然偏乐观。正确写法scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这里记得是transform而不是fit_transform。在实际项目中我会用sklearn的Pipeline把标准化和模型串起来避免在多步操作中忘记某个环节也方便后续做交叉验证时保持流程一致。3.3 拟合Lasso观察系数稀疏化下面用sklearn的Lasso直接拟合lasso Lasso(alpha0.1) lasso.fit(X_train_scaled, y_train) y_pred lasso.predict(X_test_scaled) n_used np.sum(lasso.coef_ ! 0) print(fLASSO非零系数个数: {n_used}) print(fR2: {r2_score(y_test, y_pred):.4f}) print(fRMSE: {mean_squared_error(y_test, y_pred, squaredFalse):.4f}) print(前10个系数的值:) print(lasso.coef_[:10])输出大致是非零系数个数在10个以内R2在0.9附近RMSE大约0.5。RMSE与噪声标准差0.5接近说明模型已经很好地抓到了真实信号。这里有一个值得说的点(\alpha0.1) 不是拍脑袋选的是我根据特征方差和经验预测出的初值。实际项目中通常会用交叉验证来选择后面专门章节讲。3.4 可视化系数路径不同alpha下模型的动态变化如果想观察alpha从大到小的过程中哪些特征先被选入、哪些最后仍然被放弃可以画一条系数路径图。sklearn的lasso_path函数可以直接返回所有alpha下的系数矩阵from sklearn.linear_model import lasso_path import matplotlib.pyplot as plt alphas_lasso, coefs_lasso, _ lasso_path(X_train_scaled, y_train, alphasnp.logspace(-2, 0.5, 50)) plt.figure(figsize(10, 6)) for i in range(n_features): plt.plot(alphas_lasso, coefs_lasso[i, :], lw1) plt.xscale(log) plt.xlabel(alpha) plt.ylabel(coefficients) plt.title(LASSO coefficient path) plt.show()这张图能很直观地看到alpha很大时所有系数都为零alpha逐渐变小强特征先被选出来再继续降低alpha一些弱噪声特征也开始挤进来。观察哪些特征在较长的一段alpha区间内都保持非零通常说明它们是真正重要的信号。4. 弹性网络回归L1的稀疏性和L2的稳定性如何兼得4.1 LASSO在高度相关特征面前的三个麻烦LASSO的稀疏性很好用但它有两个客观存在的问题。第一当两个特征高度相关时Lasso往往会随机选择其中一个把另一个系数压成零而不是两个都保留。这其实很不稳定换一批样本它可能选出另一个系数的符号也可能会变模型的可解释性大打折扣。第二特征数多于样本数时Lasso最多只能选出 n个非零系数n是样本数这个约束在某些场景下不够用。第三L1单纯做稀疏筛选但缺乏对相似特征“分组”的天然机制。比如一组特征都是同一个来源的派生变量我们更希望它们联动进出模型而不是被随机挑一个代表。弹性网络Elastic Net正是为解决这些问题设计的。它把L1和L2惩罚加在一起用L1做到变量筛选用L2稳定系数、鼓励相关性强的特征一起被选或一起被淘汰。4.2 弹性网络的目标函数和l1_ratio参数含义弹性网络的目标函数是[ \min_{w} \frac{1}{2n} | y - Xw |_2^2 \alpha \cdot l1_ratio \cdot | w |_1 \frac{\alpha}{2} \cdot (1 - l1_ratio) \cdot | w |_2^2 ]其中 (\alpha) 是整体正则化强度(l1_ratio) 控制L1和L2在惩罚里的占比。参数边界很容易记(l1_ratio1)纯LASSO。(l1_ratio0)纯岭回归。(l1_ratio0.5)L1和L2等权重。实战经验是如果数据特征之间相关性较强或者特征数远大于样本数(l1_ratio) 一般设置在0.2到0.6之间比较有效。4.3 ElasticNet代码实操与组效应观察用模拟数据展示弹性网络和Lasso在系数上的差异from sklearn.linear_model import ElasticNet enet ElasticNet(alpha0.1, l1_ratio0.5, random_state42) enet.fit(X_train_scaled, y_train) print(f弹性网络非零系数个数: {np.sum(enet.coef_ ! 0)}) print(fR2: {r2_score(y_test, enet.predict(X_test_scaled)):.4f})如果在此基础上构造两个完全相同的重复特征Lasso会在两个特征之间随机选一个给系数另一个为0弹性网络则倾向于把样本的贡献“分摊”到两个相似特征上两者系数大体接近。这就是组效应的体现相似的变量被当作一个整体对待而不是厚此薄彼。这是我觉得弹性网络在高维场景下比Lasso更稳定的根本原因——它保留了L2的约束抑制了系数在高度相关特征之间的“囚徒困境”式震荡。5. 交叉验证自动选参LassoCV和ElasticNetCV才是实战主力5.1 为什么宁可多花算力也绝不手选alphaalpha太大特征被全删模型欠拟合alpha太小稀疏筛选失效模型接近普通线性回归。手选alpha只能依赖经验猜测而且同一个alpha在不同数据尺度下含义完全不同非常容易翻车。正确做法是使用交叉验证在训练集内部做K折每一折都计算模型在验证子集上的误差选平均误差最小的那组参数。sklearn的LassoCV和ElasticNetCV把这件事封装得很干净。5.2 LassoCV内部路径机制与参数解读LassoCV不会盲目搜索它会在内部构造一条从充分大到充分小的alpha路径针对每个alpha做交叉验证然后选出最优alpha。我们可以指定候选alpha的数量和路径密度。from sklearn.linear_model import LassoCV lasso_cv LassoCV(eps1e-3, n_alphas100, cv5, random_state42) lasso_cv.fit(X_train_scaled, y_train) print(f最优alpha: {lasso_cv.alpha_:.4f}) print(f非零系数个数: {np.sum(lasso_cv.coef_ ! 0)}) print(f测试集R2: {r2_score(y_test, lasso_cv.predict(X_test_scaled)):.4f})输出里会给一个最优alpha的数值。这里的eps控制alpha路径的最小值与最大值之比通常保持默认或设到1e-3。cv5表示5折交叉验证n_alphas越大搜索越细代价是训练时间变长。5.3 ElasticNetCV联合搜索alpha和l1_ratio弹性网络比Lasso多一个超参数自然要多承担一份搜索成本。ElasticNetCV允许传入一组l1_ratio候选值每个候选值内部再搜索alphafrom sklearn.linear_model import ElasticNetCV enet_cv ElasticNetCV( l1_ratio[0.1, 0.3, 0.5, 0.7, 0.9], eps1e-3, n_alphas100, cv5, random_state42 ) enet_cv.fit(X_train_scaled, y_train) print(f最优l1_ratio: {enet_cv.l1_ratio_:.2f}) print(f最优alpha: {enet_cv.alpha_:.4f}) print(f非零系数个数: {np.sum(enet_cv.coef_ ! 0)}) print(f测试集R2: {r2_score(y_test, enet_cv.predict(X_test_scaled)):.4f})一个小的经验是如果你的数据特征数特别多ElasticNetCV的计算量会比较可观。可以考虑把n_alphas调小到50或者把l1_ratio的候选集缩窄到[0.2, 0.5, 0.8]先探出大致方向再细化。5.4 用交叉验证参数重新评估时的一个纪律交叉验证选出参数之后我把最终模型在测试集上评估。但这里有一个很容易犯的“测试集污染”错误反复看测试集结果发现效果不好就回去改参数再跑一遍直到测试集分数漂亮为止。这本质上等于把测试集当训练集用最终得到的分数会虚高泛化能力被高估。正确的节奏是测试集只在最终阶段评估一次中途的参数调整都要依赖交叉验证的验证集分数来完成。6. 我实际项目中踩过的坑与经验总结6.1 忘做标准化模型差点把“量纲错误”当成稀疏信号有一次我在处理一组包含“访问次数”几百和“耗时比例”0到1之间的数据直接对原始特征做Lasso。结果Lasso给出的非零系数几乎全部集中在量纲大的那几个特征上量纲小的特征系数全被压成零。我一开始还以为是这些特征真的不相关后来才发现是标准化的坑L1惩罚作用于系数绝对值量纲大的特征系数天然小因此吃到的惩罚更少更容易被保留量纲小的特征系数巨大反而更容易被惩罚清零。这就是量纲差异造成的虚假稀疏。那之后我给所有Lasso和ElasticNet流程都强制套上Pipelinefrom sklearn.pipeline import Pipeline pipeline Pipeline([ (scaler, StandardScaler()), (model, LassoCV(cv5, random_state42)) ]) pipeline.fit(X_train, y_train)Pipeline的好处是交叉验证时标准化流程自动包含在CV循环里避免在每一折中重复手工操作也杜绝了数据泄露。实际上标准化应该用每一折的训练子集来fit再transform对应的验证子集Pipeline能自动保证这件事。6.2 高度相关特征让Lasso的系数符号来回跳以前跑一个项目有两列特征是从同一份底层数据里按不同窗口算出来的相关性达到0.98。Lasso做出来的结果里这一对特征的系数一正一负而且换一次交叉验证随机种子符号就反转一次。模型预测能力没变但业务逻辑完全讲不通。后来换成弹性网络这组特征系数变得均匀了一些符号也稳定在同一个方向。原因还是那个几何直觉弹性网络的L2惩罚把高度相关的特征“捆”在一起避免它们内部互相打架。6.3 收敛警告的处理不要忽略max_iter和tol样本量一大特征维度一高sklearn的Lasso和ElasticNet有时候会弹出ConvergenceWarning。很多人直接忽略但这样得到的系数还没完全收敛结果解释起来没有意义。处理方式很简单调大max_iter必要时放宽tol。Lasso(alpha0.1, max_iter100000, tol1e-4)如果调大max_iter后依然不收敛通常说明alpha太小正则化惩罚不足以平抑特征共线性带来的震荡可以适当增大alpha或者换用弹性网络。收敛警告不应该被当作无所谓的日志它本身就是模型状态的一种信号。6.4 评价指标不能只看R2Lasso和ElasticNet做出来之后很多人只看R2判断好坏这在高维稀疏场景下不够充分。R2在特征筛选之后必然下降一些因为模型放弃了大量弱信号特征可预测稳定性提升了。我更习惯把RMSE、MAE和R2三个指标都列出来再结合业务对误差的容忍度决定阈值。另外一个好习惯是观察模型选中的系数在业务层面是否说得通。如果某个特征的符号与业务常识相悖先不要急着接受模型结果排查一下是不是数据泄露、标准化顺序或者相关特征干扰导致的。6.5 一个小技巧用LassoCV的alpha结果指导ElasticNetCV搜索范围如果不想让ElasticNetCV在稀薄的网格里大海捞针可以先跑一次LassoCV拿到近似最优alpha然后以这个alpha为中心指数级别地小范围扩展后传给ElasticNetCVlasso_cv LassoCV(cv5, random_state42).fit(X_train_scaled, y_train) alpha_center lasso_cv.alpha_ enet_cv ElasticNetCV( l1_ratio[0.2, 0.4, 0.6, 0.8], alphasnp.linspace(alpha_center * 0.5, alpha_center * 2, 50), cv5, random_state42 ).fit(X_train_scaled, y_train)这样既缩小了搜索空间又能借助Lasso得到的稀疏性强信号让弹性网络在一个合理范围内做精细调整。实际用下来不仅速度更快选出的参数也更稳健。最后说一点个人经验如果任务要求明确说“我要做特征筛选”且特征之间相对独立直接用Lasso如果特征之间关联关系复杂、或者样本不够充裕弹性网络是更好的默认选择。两者在sklearn里都是成熟的实现真正区分模型效果的往往不是算法本身而是数据预处理、参数搜索方式和评估纪律。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑