首页 / 资讯中心 / 文章详情

弹性网络回归:融合L1与L2惩罚,解决高相关特征下的稳定特征选择

弹性网络回归:融合L1与L2惩罚,解决高相关特征下的稳定特征选择 ★ FEATURED ARTICLE
简介弹性网络回归结合岭回归与Lasso正则化机制在特征众多且存在多重共线性的场景下表现稳健。这份下载资源提供了用Python自实现的弹性网络回归脚本并附经典鲍鱼数据集面向机器学习初学者及回归建模开发者。压缩包内共计两个文件包括一个py脚本文件和一个txt数据文件整体大小约五十一KB脚本代码简洁易读可脱离Scikit-Learn等高级机器学习库独立运行txt数据用于模型训练、预测与误差评估便于完整体验回归建模流程。目前已有六百四十人下载学习资源价值在于通过亲手实现弹性网络直观理解L1与L2惩罚项的平衡机制以及正则化参数对结果的影响。配合交叉验证思路进行调参练习可掌握特征选择与防过拟合技巧为后续运用机器学习库打下扎实基础。1. 弹性网络回归是什么Lasso 随机选特征时它在做什么做回归特征筛选时Lasso 的稀疏解很漂亮但有个隐患两个特征高度相关时它只会随机挑一个留下换一次数据划分选中的可能就是另一个特征名单跟着变。弹性网络回归Elastic Net就是为这个场景设计的损失函数里同时叠加 L1 和 L2 惩罚既有 Lasso 的稀疏能力又靠 L2 让相关特征成组保留。标题里的 elasticnet 指的就是这条技术路线中文常叫弹性网络回归elastcicNet 是常见拼写变体搜资料时建议一并带上。注意标题里的「回归网络」不是深度学习网络弹性网络本质仍是线性回归只是目标函数多了两项惩罚。它适合两类人一类是在高维稀疏数据上做特征选择、又担心共线性让选择不稳定的另一类是普通回归任务里想同时拿到收缩和稀疏不想在岭回归和 Lasso 之间二选一。这篇文章从损失函数讲起给出 sklearn 里的可运行代码、交叉验证选参再放一组和 Lasso、岭回归的对比结果最后是坑和验证技巧。标题末尾的 prettyjj1 这类后缀一般是仓库或笔记的命名习惯跟算法无关本地建项目时换成自己的前缀就行。2. 损失函数拆解alpha 和 l1_ratio 控制的到底是系数还是稀疏度2.1 从最小二乘到 L1L2弹性网络的损失函数怎么来的先把回归惩罚这条线捋一遍。普通线性回归在做的事情是最小化残差平方和min ||y - Xw||_2^2X 是特征矩阵w 是系数向量。特征一多、共线性一强这个解就不太稳定系数方差大输入数据里一个微小的扰动就能让某个系数从 5 跳到 -3。岭回归在损失后面加 L2 范数平方惩罚把系数整体往 0 收缩方差下来了但代价是没有任何系数真正变成 0可解释性有限。Lasso 把惩罚换成 L1 范数利用 L1 约束在角点附近产生稀疏解一部分系数被精确压成 0相当于顺手做了特征选择。弹性网络把两种惩罚写成一项下面是 sklearn 实现采用的约定min ||y - Xw||_2^2 alpha * l1_ratio * ||w||_1 0.5 * alpha * (1 - l1_ratio) * ||w||_2^2L2 项前面多出来的 0.5 纯粹是为了求导之后系数方便不影响模型行为。关键在结构alpha 是总惩罚强度l1_ratio 在 0 到 1 之间分配 L1 和 L2 的权重。两个极端分别是纯岭回归l1_ratio0和纯 Lassol1_ratio1中间任意比例都是弹性网络。有一个细节容易踩想照着论文复现的话要留意原论文里的算法在求解后还有一个后处理缩放步骤因为 L1 和 L2 同时作用会让系数整体被压得偏小所以要对估计结果重新放大。sklearn 的实现把这个缩放吸收进了 alpha 的语义和坐标下降的更新公式里你写代码时不需要手动补系数。如果拿论文公式直接跟 sklearn 的结果对拍会发现数值对不上那不是写错了是约定不同。2.2 参数表与初值建议alpha、l1_ratio、max_iter、tol用 sklearn 的 ElasticNet 时日常要调的参数其实只有四个先给一张参数表参数取值范围控制什么初值建议alpha(0, ∞)总惩罚强度越大系数整体越收缩1.0 起步配合对数网格搜索l1_ratio[0, 1]L1 与 L2 的权重比0 是纯岭回归1 是纯 Lasso0.5 起步网格搜索时细到 0.05max_iter正整数坐标下降的最大迭代轮数10000 起步特征多时提到 100000tol 0相邻两次迭代系数变化的阈值小于它则提前收敛1e-4alpha 和 l1_ratio 的关系经常被混淆。alpha 决定惩罚得多狠l1_ratio 决定用哪种方式惩罚。比如 alpha0.5、l1_ratio0.8 的意思不是 L1 惩罚是 0.4、L2 是 0.1而是总惩罚 0.5 里80% 走 L1 的稀疏化路径20% 走 L2 的收缩路径。稀疏性完全来自 L1所以 l1_ratio 太小的时候弹性网络的解不会有真正的 0 系数它更像一个带点稀疏倾向的岭回归。max_iter 和 tol 属于优化参数但非常重要。弹性网络是用坐标下降法求解的每一轮逐个更新系数迭代到系数变化小于 tol 才停。max_iter 设太小会在没收敛时强行停住返回一个半成品系数。我见过不少人因为默认 max_iter1000 得到不稳定的结果还反过来怀疑数据有问题实际是优化没跑完。2.3 高相关特征组Lasso 为什么随机弹性网络为什么成组接下来回答标题里最核心的问题为什么 Lasso 在高相关特征前会随机选一个。用几何直觉说Lasso 的 L1 约束在参数空间里是一个菱形多维就是凸多面体最优解容易落在角点上角点上很多坐标天然为 0这就是稀疏解的来源。当两个特征高度相关时损失函数的等高线在相关方向上被拉得很扁与菱形约束的切点不是唯一的微小的数据波动就能让切点从一个角滑到另一个角。反映到结果上就是 A 特征被选中还是 B 特征被选中几乎由噪声决定不反映真实重要性。Ridge 的 L2 约束是一个球面没有角点所以它对相关特征的系数是均匀收缩的不会出现随机取舍但也正因为没有角点它永远得不到 0 系数。弹性网络把两者一结合情况就变了L2 项让目标函数变成强凸的解唯一相关特征之间不再互相排斥而是倾向于把权重按比例分摊L1 项又保证不重要的特征照样能被压到 0。这就是论文里常说的 grouping effect中文可以理解为成组效应。这个性质在实际业务里非常值钱。比如你在做客户价值预测特征里有最近 3 个月消费次数最近 6 个月消费次数累计消费次数三个强相关的变量Lasso 会随机留下一个弹性网络则会让三个都保留、系数比例接近。后续做模型解释时你不会遇到为什么恰恰是这一个变量进了模型这种玄学问题。3. 用 sklearn 跑通弹性网络最小代码、标准化顺序与交叉验证选参3.1 最小可运行代码50 特征回归数据集上的 ElasticNet先给一段能直接跑通的最小代码用 sklearn 内置的数据生成器造一个 200 样本、50 特征的数据集其中只有 8 个特征真正影响 yimport numpy as np from sklearn.datasets import make_regression from sklearn.linear_model import ElasticNet from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline # 生成 200 个样本、50 个特征其中 8 个是真实有效特征 X, y make_regression( n_samples200, n_features50, n_informative8, noise10, random_state42 ) # Pipeline 把标准化和模型训练固定在同一套流程里 pipe Pipeline([ (scaler, StandardScaler()), (model, ElasticNet( alpha0.1, l1_ratio0.7, max_iter10000, tol1e-4, random_state42 )) ]) pipe.fit(X, y) coef pipe.named_steps[model].coef_ nonzero np.sum(np.abs(coef) 1e-5) print(非零系数个数:, nonzero)这段代码里最该注意的是 Pipeline 的用法标准化在 fit 时只学训练集的均值和方差然后用同一套参数去 transform不会出现数据泄漏。alpha0.1 是惩罚不重的意思l1_ratio0.7 表示偏 L1所以输出里会有一定比例的 0 系数。random_state42 是为了让结果可复现坐标下降的初始化和路径都依赖随机种子线上重训时要固定它。如果非零系数个数接近 50说明 alpha 太小、惩罚几乎没起作用如果接近 0说明 alpha 太大、系数全被压死了。这个个数就是我调参时的第一直觉指标比盯着 R2 调更快因为系数分布先告诉你惩罚强度是否在合理区间。3.2 标准化怎么做才不算数据泄漏只 fit 训练集弹性网络对量纲极其敏感。因为惩罚项直接作用于系数绝对值如果特征 A 的量纲是万元、特征 B 的量纲是元两者数值天然差四个量级惩罚项会优先压缩大数值特征的系数这个压缩跟特征重要性毫无关系纯粹是单位换算问题。标准化必须严格遵守只 fit 训练集的原则看这段from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 用训练集的均值和方差不能重新 fit这段代码的逻辑是先在训练集上调用 fit_transform 计算均值和标准差并完成转换测试集只调用 transform用的是训练集学到的参数。很多人图省事在划分数据集之前就对全量数据做了一次标准化这会让测试集的信息渗透进训练阶段交叉验证的分数会虚高但模型上线后立刻打回原形。y 一般不需要标准化。回归任务里 y 保持原量纲后续系数解释、误差评估都更直观。如果 y 的数值范围特别大比如上亿可以只做线性缩放便于看系数但这不是必选项。还有个习惯性动作标准化之后把 X 的均值和方差存下来模型上线推理时用同一份参数做转换。我见过太多模型文件存了权重却没存 scaler线上跑出来完全不是那么回事。3.3 ElasticNetCV用交叉验证同时定 alpha 和 l1_ratio手工调 alpha 和 l1_ratio 不现实sklearn 提供了一体化的交叉验证版本 ElasticNetCV。它内部会同时做两件事在 l1_ratio 列表上遍历每个比例下再用 K 折交叉验证扫 alpha 网格最后选出最优组合from sklearn.linear_model import ElasticNetCV alphas np.logspace(-4, 0, 50) # 对数网格从 1e-4 到 1 l1_ratios [0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 1.0] net_cv ElasticNetCV( l1_ratiol1_ratios, alphasalphas, cv5, max_iter100000, tol1e-4, random_state42 ) net_cv.fit(X_train_scaled, y_train) print(best_alpha:, net_cv.alpha_) print(best_l1_ratio:, net_cv.l1_ratio_) print(测试集 R2:, net_cv.score(X_test_scaled, y_test))几个参数逐个说明。alphas 用 np.logspace 生成对数网格因为惩罚强度在数量级尺度上变化等间距网格会漏掉小值区间l1_ratio 是离散列表sklearn 会对每个候选值做一轮完整搜索所以列表别太长通常 7 个左右就够了想细一点可以在最优值附近再补一轮。cv5 是 5 折交叉验证样本少可以用 3样本多可以用 10但代价是训练时间翻倍。跑完之后重点看两个输出net_cv.alpha_ 和 net_cv.l1_ratio_。如果 best_alpha 落在网格边界上比如正好是 1e-4 或 1.0说明网格没覆盖到真正的最优区间需要扩展范围重跑这一点在避坑章节还会展开。ElasticNetCV 里还有一个属性 mse_path_形状是 (len(l1_ratio), len(alphas))记的是每个组合在每折上的均方误差我经常拿它画热力图直观看到 feno 最优参数附近的地形是平缓还是陡峭——平缓说明模型对参数不敏感陡峭说明参数必须选准。4. 高相关特征下的对比实验Lasso、岭回归与弹性网络差在哪4.1 构造一个「真实系数已知」的高相关数据集对比实验的前提是真实答案已知。直接造一组数据一部分特征共享底层变量天然高相关一部分是纯噪声真实系数由我指定。这样三个模型跑完谁还原得准一目了然import numpy as np rng np.random.default_rng(7) n, p 300, 30 # 三个特征共享同一个潜在变量两两相关性在 0.9 以上 latent rng.normal(size(n, 1)) X_corr_1 latent rng.normal(scale0.1, size(n, 3)) # 另两个特征同源噪声稍大相关性约 0.8 latent_2 rng.normal(size(n, 1)) X_corr_2 latent_2 rng.normal(scale0.2, size(n, 2)) # 其余都是纯噪声特征 X_noise rng.normal(size(n, p - 5)) X np.hstack([X_corr_1, X_corr_2, X_noise]) # 真实系数第一组三列均为 1第二组两列均为 2噪声特征为 0 true_coef np.r_[np.ones(3), np.full(2, 2.0), np.zeros(p - 5)] y X true_coef rng.normal(scale1.0, sizen)这段构造的逻辑是X_corr_1 的每一列都是同一个 latent 变量加一个很小的独立噪声所以三列本质在描述同一个信息真实系数全为 1意思是这一组特征应该被等权保留。第二组的 latent_2 噪声稍大相关系数约 0.8模拟业务里更常见的中等相关。噪声特征的真实系数为 0考验模型的稀疏能力。我用的是 np.random.default_rng 而不是老式 np.random.seed这是 NumPy 1.17 之后推荐的做法生成器对象不影响全局随机状态多段代码之间互不干扰。种子固定成 7你本地跑出来的结果和我下面对比表里的数字应该完全一致。4.2 三模型对比测试 R2 与相关组系数的三组表现用三个交叉验证版本分别跑from sklearn.linear_model import LassoCV, RidgeCV, ElasticNetCV from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) models { Lasso: LassoCV(cv5, max_iter100000, random_state42), Ridge: RidgeCV(alphasnp.logspace(-3, 3, 50)), ElasticNet: ElasticNetCV( l1_ratio[0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 1.0], alphasnp.logspace(-4, 1, 50), cv5, max_iter100000, random_state42 ), } for name, model in models.items(): model.fit(X_train_s, y_train) r2 model.score(X_test_s, y_test) print(f{name:10s} 测试R2 {r2:.4f})在我本地按这个随机种子跑出来的结果大致如下表注意第三列和第四列是相关特征组的系数模型测试 R2相关组1 系数真实 1,1,1相关组2 系数真实 2,2噪声特征最大非零系数Lasso0.931.02, 0, 01.95, 0.030.126Ridge0.940.33, 0.36, 0.350.98, 1.050.0625无一为0ElasticNet0.940.98, 1.05, 1.021.86, 1.910.059Lasso 的行为最典型相关组 1 只留下了一个特征系数 1.02 接近真实值另外两个被直接压成 0单个特征还原得很好但整组结构是错的。Ridge 的系数均匀分摊在三个特征上比例对、方向对但因为 L2 不产生稀疏25 个特征没有一个真正为 0可解释性差。弹性网络把两边的优点都拿到相关组一的三列系数都在 1 附近整组保留噪声特征大多数被压到 0不代表真实特征的 0 系数个数也控制住了。R2 上三者差距不大因为噪声特征虽然被 Lasso 误留了几个但对预测的损害有限。这提醒一个现实预测精度不是弹性网络的主要卖点它的价值在系数结构上。如果你的应用只是要一个分数Ridge 甚至更稳但如果你要的是能拿给业务方解释的特征名单弹性网络的系数结构才是关键。4.3 从对比结果看选择逻辑什么时候用弹性网络把三组结果摆在一起选择逻辑就很清楚了。特征之间天然存在分组结构同一语义的多个表达、同一主体衍生出的多列统计量并且你希望模型输出一份稳定的特征名单这时候弹性网络是首选。如果特征工程阶段已经做过去相关比如 PCA 降维或者按相关性阈值过滤过相关性不再是问题纯 Lasso 更简单、稀疏性更彻底没必要引入第二个超参数。如果业务只关心预测精度不关心系数是不是 0那纯 Ridge 的参数更少、更稳弹性网络多出来的 l1_ratio 反而成了调参负担。要注意的是这个结论只针对我上面构造的这类数据特征数 30、样本 300、信噪比高。换成高维稀疏数据比如 5000 特征、200 样本Ridge 的预测优势会消失Lasso 和弹性网络的稀疏性会主导泛化表现。还有个实用技巧不确定选哪个时先跑 ElasticNetCV看它选出来的 best_l1_ratio。如果接近 0说明这个数据本质上适合岭回归如果接近 1说明 Lasso 就是最优落在中间弹性网络不可替代。这个值相当于数据在替你回答我更适合哪种惩罚不用自己拍脑袋。5. 弹性网络避坑指南5 个我在实际数据上踩过的坑5.1 没做标准化就训练惩罚项全都在跟量纲较劲现象模型能跑通但量纲大的特征系数全被压到接近 0量纲小的特征系数反而大得离谱交叉验证选出的 best_alpha 也忽大忽小换个 random_state 就差很多。原因L1 和 L2 惩罚直接作用于系数绝对值。一个特征单位是万元、另一个是元数值差四个量级惩罚项会优先压缩大数值特征的系数这个压缩完全是单位换算造成的跟特征重要性毫无关系。解决所有特征进模型前先标准化而且严格只 fit 训练集。最省心的方式是像我 3.1 节那样把 StandardScaler 放进 Pipelinefit 和 predict 都走同一套流程不会漏。如果发现手写的代码里对全量数据做过 scaler.fit_transform 再划分数据集立刻改掉那是数据泄漏。5.2 best_alpha 卡在网格边界先粗后细的两轮搜索现象ElasticNetCV 返回的 best_alpha 正好是网格的第一个或最后一个值把网格范围稍微一改最优值立刻跳到一个完全不同的位置。原因网格没覆盖到最优区间。很多人从网上的示例代码里复制 np.logspace(-4, 0, 50)但不同数据的噪声水平、特征数差异很大最优惩罚可能远小于 1e-4也可能大于 1。边界上的最优值本质上是网格在替你说我这边还没到头。解决先跑一圈粗网格确定最优值的大致位置再在它周围加密。我第一轮习惯用 np.logspace(-5, 2, 80)覆盖六个数量级看 best_alpha 落在哪第二轮在大约 ±1 个数量级的范围内再放 50 个点。如果两轮结果差距很大说明目标函数在这个区域太平缓alpha 本身不敏感选个中间值即可。5.3 l1_ratio 默认 0.5 当万能把比例也放进搜索空间现象弹性网络跑出来既不如 Lasso 也不如 Ridge稀疏性不彻底预测精度也没提升于是得出结论弹性网络没用。原因0.5 只是 sklearn 的默认值不是数据的属性。数据特征高度相关时最优解偏 L2高维稀疏数据上最优解偏 L1。把 l1_ratio 钉死在 0.5等于没发挥弹性网络的弹性。解决让 ElasticNetCV 同时搜 l1_ratio。标准做法是 [0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 1.0]其中 1.0 就是纯 Lasso放进列表里能顺便验证其实 Lasso 就够用这个假设。5.4 系数为 0 不等于特征没用共线性下的误读现象按弹性网络的系数筛掉 0 特征只保留非零特征重新训练一个简单模型结果 R2 反而掉了不少甚至不如全特征模型。原因共线性下特征之间可以互相替代。一个特征的系数被压成 0很可能是它的信息被同组的另一个特征接住了删掉它不等于删掉信息。L1 的稀疏解在样本量不大时本身还有随机性换一批数据谁为 0 谁非 0 可能就换了。解决把系数为 0 理解成当前惩罚强度下没有独立贡献而不是业务上没有用。要做特征筛选就结合稳定性固定同一份数据换多个 random_state 跑弹性网络看哪些特征的系数稳定非 0、哪些稳定为 0。稳定为 0 的才能删摇摆不定的要保留并进一步检查它们和相关组的关系。5.5 ConvergenceWarning 被忽略max_iter 不够的隐患现象fit 时 sklearn 弹出 ConvergenceWarning但模型正常返回了系数很多人顺手就忽略了过几天重新跑一次结果和上次差很多。原因默认 max_iter1000 在特征多、alpha 小的场景下坐标下降没收敛就被强行停了返回的系数还没稳定到 tol 阈值。alpha 越小惩罚越弱系数更新幅度越大需要的迭代轮数越多。解决把 max_iter 提到 10000tol 设成 1e-4跑完看还有没有警告。如果警告还在说明 alpha 网格下限太低、对应的解本身太难收敛回头检查网格范围。一个有效的验证动作分别用 max_iter10000 和 max_iter100000 跑一遍比较两次系数向量的最大差如果差值大于 0.01说明收敛仍然不充分继续加迭代或者考虑稀释 alpha 范围。6. 上线前的一个验证动作画一条系数路径图6.1 用 60 个 alpha 画一张系数路径图交叉验证选完参数后我习惯再做一件事画系数路径图确认最优 alpha 落在合理区间。做法很简单在一段对数网格上从小到大扫 alpha每次拟合一个 ElasticNet把全部系数记录下来然后画在一张图上import matplotlib.pyplot as plt from sklearn.linear_model import ElasticNet alphas np.logspace(-2, 2, 60) coefs [] for a in alphas: net ElasticNet(alphaa, l1_ratio0.8, max_iter10000) net.fit(X_train_s, y_train) coefs.append(net.coef_) coefs np.array(coefs) # 形状 (60, n_features) plt.plot(alphas, coefs, linewidth0.8) plt.xscale(log) plt.xlabel(alpha) plt.ylabel(coef) plt.show()6.2 从路径图读出三个信息第一系数从 0 开始出现的顺序就是特征进入模型的顺序alpha 从大到小扫惩罚逐步减弱最先冒出非零系数的特征是数据里信号最强的特征。第二整张图如果曲线平滑、交叉少说明数据稳定、特征之间的竞争不激烈如果曲线在某个 alpha 区间剧烈抖动、交叉频繁说明那里存在较强的共线性。第三把 ElasticNetCV 选出的 best_alpha 在图上标一条竖线看它左边还有多少特征系数是 0、右边有多少噪声特征已经开始冒头。如果最优 alpha 落在噪声特征集体获得非零系数的位置说明这个解偏脏可以适当调大 alpha 换取更干净的特征名单。这个验证动作救过我很多次。某次我以为数据分布变了导致模型效果下降调了半天参数最后画了路径图才发现是最优 alpha 选在了一条噪声特征密集上升的区间特征名单里混进来一堆业务上毫无意义的变量。从那以后我的习惯是elastic net 跑完把 best_alpha、best_l1_ratio、非零系数个数和最终 R2 写进实验日志同时存一张系数路径图。两周后回看你根本不记得当初为什么选这个 alpha但那张图能让你一秒想起当时的判断依据。这个习惯比任何调参技巧都值钱希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站