ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

L1与L2正则化详解:Ridge、Lasso与ElasticNet实战对比

L1与L2正则化详解:Ridge、Lasso与ElasticNet实战对比 自己试过 10 多个模型项目之后我越来越觉得正则化这件事不是调参时随便加一加而是模型结构之外真正决定泛化能力的那道闸门。L1 正则化和 L2 正则化放在一起对比表面看只是惩罚项从绝对值换成了平方实际用起来差距极大。很多新手把 Ridge 和 Lasso 当成同一种东西换着用结果要么模型稀疏性完全不对要么特征被砍得莫名其妙。这篇文章我把两者从原理到实操完整拆一遍包含我踩过的一些坑希望能帮你一次性把这个问题理顺。1. 正则化到底在解决什么问题1.1 模型的“记性”与泛化损失先说一个最直观的例子。你训练一个线性模型如果特征维度远大于样本量比如 500 个特征只有 80 条样本最小二乘解往往能把训练误差压到几乎为 0但换一批数据立刻崩盘。这就是典型的过拟合模型太“聪明”把训练集里的噪声也当成了规律来记。正则化的本质是在损失函数里加一条限制逼着模型放弃一部分“自由度”。原来的目标是最小化误差现在变成最小化误差的同时还要控制系数大小。这条限制就是惩罚项。惩罚得越狠模型越不敢把系数放大因为它知道放大系数虽然能在当前训练集上更准但换数据后很容易翻车。L1 正则化对应 Lasso惩罚项是所有系数绝对值之和即 λΣ|w|L2 正则化对应 Ridge惩罚项是所有系数平方和即 λΣw²。这个 λ 就是正则化系数它控制惩罚强度是整篇文章里最需要花时间调试的参数。1.2 偏差与方差的平衡点理解正则化还有一个绕不开的概念偏差方差权衡。没有正则化时模型方差高给一点数据波动就大变样。加上正则化后系数被压缩向 0模型变得迟钝方差降低但代价是训练误差略微上升也就是偏差增加。你真正想要的是总误差最小而不是训练误差最小。测试集上的误差一般可以分解成偏差平方、方差和不可约噪声三部分。正则化正是在偏差和方差之间找平衡。L1 和 L2 在这一点上方向相同但实现平衡的方式截然不同这正是后文要重点展开的内容。2. L1 与 L2 的数学机制和差异根因2.1 从损失函数看两者的本质差别把线性回归的损失函数写出来对比一下一眼就能看出区别。L2 正则化Ridge的目标函数是L Σ(y - Xw)^2 λΣw_j^2L1 正则化Lasso的目标函数是L Σ(y - Xw)^2 λΣ|w_j|看起来只是平方和绝对值之差但导数行为完全不同。先对 L2 求偏导得到 2λw_j你会发现梯度里还带着 w_j 自身。这意味着越大的系数被拉回的力度越强系数越大惩罚越大最终它会平滑地把所有系数都缩小但很少会精确变成 0。L1 的导数是 λ·sign(w_j)无论系数是 1 还是 100惩罚力度都是恒定不变的 λ。这就有意思了。当一个特征对应的系数绝对值小于某个阈值时梯度里的惩罚分量会直接把系数推过零点使其精确等于 0。这就是 L1 产生稀疏解的根因也是它可以直接做特征选择的原因。2.2 几何视角为什么 L1 会把系数压到 0光看公式还不够直观我来解释一个更形象的视角。把带约束的最小化问题看成一个带棱角的“可行域”和一圈圈椭圆等值线的接触过程。L2 的可行域是一个圆形圆边界光滑等值线大概率切在圆周的任意位置这个切点通常不在坐标轴上所以系数只是变小但不等于 0。而 L1 的可行域是一个菱形四个顶点恰好位于坐标轴上等值线碰到菱形的角落时就有一个系数变成 0。特征维度越高这种“落在棱角上”的概率也就越大。这个几何差异说人话就是L2 温和地收缩所有特征L1 则干脆利落地砍掉一部分特征。如果你面临的任务是“从几十个特征里挑出真正有用的几个”L1 这种特性会非常顺手如果你只是怕过拟合并没有删除特征的需求那 L2 更稳妥。2.3 为什么 L2 在相关特征面前更稳定在实际数据里特征之间常常高度相关比如电商数据里的“点击量”和“曝光点击率”这两个变量本身就有强相关关系。这时候用 L1 会出问题它倾向于在相关特征里随机挑一个把另一个系数压成 0而这一个留下哪一个并不稳定换个数据子集留下的那一个可能就变了。L2 不会这么极端。因为平方惩罚是按系数大小成比例收缩的两个相关特征可以同时保留各自分到一半权重模型的稳定性要好得多。这句话是经验之谈也是为什么很多统计学习教材会强调如果特征是分组相关的L1 不是好选择ElasticNet 能做得更好。ElasticNet 的思路后面单独讲。3. 实操详解Lasso、Ridge 与 ElasticNet 完整流程3.1 数据预处理不做标准化就白正则化了正则化对特征尺度极其敏感这一点我见过太多次被忽略。为什么敏感因为惩罚项是直接加在 w 上的如果某一列特征的取值特别大比如取值范围是 0 到 10000而另一列特征的取值范围是 0 到 1那么为了让模型拟合得好那个大尺度特征对应的系数天然会变得非常小。此时 L2 的平方惩罚会对小系数施加非常轻微的惩罚而对大尺度特征几乎不痛不痒后果就是惩罚被不均匀地分摊正则化等于失效。所以任何使用 L1/L2 正则化的线性模型第一步都必须是标准化。用 scikit-learn 做这件事非常直接from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline scaler StandardScaler()把数据拟合到 scaler 上之后再喂给 Ridge 或 Lasso或者直接塞进 make_pipeline让预处理成为流程的一部分省得测试集上忘记做同样的变换。还有一个细节特征标准化用 Z-score 还是 MinMaxScaler对 L1 影响不大L2 下 Z-score 更常见。顺序也别忘了先划分训练测试集再用训练集的数据拟合 scaler再变换测试集不然测试集信息提前泄露到预处理里去你得到的评估结果会偏乐观。3.2 Ridge 回归实操依赖系数的精细调节Ridge 的使用场景很明确特征之间存在相关、没有明确的删特征需求、只想提升泛化能力。我用一个模拟数据集演示完整流程。import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import Ridge, RidgeCV from sklearn.metrics import mean_squared_error from sklearn.preprocessing import StandardScaler # 构造一个有 8 个特征、其中两个特征强相关的数据 rng np.random.default_rng(42) X rng.normal(size(200, 8)) X[:, 5] X[:, 2] rng.normal(scale0.1, size200) # 强相关特征 true_w np.array([1.5, -2.0, 0.8, 0.0, 3.0, 0.0, 0.0, 0.5]) y X true_w rng.normal(scale0.5, size200) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.25, random_state42) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) ridge Ridge(alpha1.0) ridge.fit(X_train_s, y_train) print(Ridge coef:, np.round(ridge.coef_, 3)) print(Ridge RMSE:, np.sqrt(mean_squared_error(y_test, ridge.predict(X_test_s))))执行之后你会看到那两个相关特征的系数被分配到接近 0.4 和 0.4 左右而不是一个保持 0.8 另一个归零。这就是前面说的 L2 在相关特征上的均衡行为。此时如果把 alpha 从 1 调到 10系数会一起缩小模型偏差增大方差减小测试 RMSE 趋向某种先降后升的曲线。alpha 的取值如何确定不要拍脑袋交给交叉验证ridge_cv RidgeCV(alphasnp.logspace(-2, 2, 50)) ridge_cv.fit(X_train_s, y_train) print(Best alpha:, ridge_cv.alpha_)RidgeCV 会通过留一交叉验证遍历候选 alpha选出泛化误差最小的那个。对于中小型数据集这样的成本很低完全够用。3.3 Lasso 实操特征选择与稀疏解Lasso 的特性是直接产出稀疏系数。需要注意sklearn 的 Lasso 默认求解算法在处理大特征集时用的是坐标下降法它会迭代地逐特征更新系数速度尚可但千万不要把 alpha 设成 0那等价于普通最小二乘且在小样本高维度下存在焦油坑问题。先跑一个默认参数版本看看效果from sklearn.linear_model import Lasso, LassoCV lasso Lasso(alpha0.1) lasso.fit(X_train_s, y_train) print(Lasso coef:, np.round(lasso.coef_, 3)) print(Lasso RMSE:, np.sqrt(mean_squared_error(y_test, lasso.predict(X_test_s))))你会发现当 alpha 比较小时系数几乎没被砍掉多少稀疏性不明显把 alpha 调大到 0.5 或 1某些不重要的特征系数被压成精确的 0.0。Lasso 跳过了“逐步缩小”的阶段直接归零这让它非常适合做特征筛选。但你马上会遇到另一个问题alpha 选多少合适Lasso 的系数路径会对 alpha 非常敏感稍微大一点就会把一堆特征一起删掉。正确的做法是使用 LassoCV在数据上跑 k 折交叉验证找到最优 alpha然后重新拟合lasso_cv LassoCV(alphasnp.logspace(-3, 1, 100), cv5, random_state42) lasso_cv.fit(X_train_s, y_train) print(Lasso best alpha:, lasso_cv.alpha_) print(Selected features:, np.where(lasso_cv.coef_ ! 0)[0])这里有个实操经验LassoCV 在特征数超过样本数时默认的迭代求解容易不稳定最好把 max_iter 调大一些比如 10000 或 50000。我在一个基因表达数据集上见过默认 max_iter 不够导致收敛警告调大后结果完全不一样。3.4 ElasticNet把 L1 和 L2 的优点合起来既然 L1 擅长特征选择但相关特征下不稳定L2 稳定但不会归零那能不能折中一下ElasticNet 就是干这个的。它的目标函数是L Σ(y - Xw)^2 α·(l1_ratio·Σ|w_j| (1 - l1_ratio)·Σw_j^2)这里多了一个 l1_ratio 超参表示 L1 惩罚所占的比例。l1_ratio1 等价于纯 Lassol1_ratio0 等价于纯 Ridge。实际中 l1_ratio 取 0.5 左右是比较常见的起点。from sklearn.linear_model import ElasticNetCV elastic_cv ElasticNetCV(l1_ratio[0.1, 0.3, 0.5, 0.7, 0.9], alphasnp.logspace(-3, 1, 50), cv5, random_state42) elastic_cv.fit(X_train_s, y_train) print(Best alpha:, elastic_cv.alpha_) print(Best l1_ratio:, elastic_cv.l1_ratio_)用 ElasticNet 的好处是即使特征强相关它也不会因为特征选择完全丢失分组结构因为它额外保留了 L2 的收缩能力。特征维度高又存在多重共线性的时候ElasticNet 是比纯 Lasso 更稳的方案。3.5 正则化系数与交叉验证的配合正则化系数的选择是整个实操中最核心的一环。alpha 太小惩罚不够过拟合依旧alpha 太大系数被压成接近 0模型退化成几乎只看截距出现严重欠拟合。判断标准只有一个在验证集或交叉验证分数上选。用全量数据训练一个固定 alpha 的模型再评估这种流程容易在 alpha 选择上过拟合验证集最好把 alpha 选择也放入嵌套交叉验证里或者使用内置 CV 的模型。alpha 候选范围建议用对数网格比如 np.logspace(-4, 2, 50)横跨好几个数量级既不遗漏小量级最优解也覆盖大量级情况。如果在候选范围的边界处取到最优值说明范围需要扩大不要直接把边界值当成最终答案。4. 常见问题与参数调优经验4.1 为什么 L1 会把所有系数一起砍没这个新手必踩的坑是alpha 设得太大Lasso 把所有系数全部置零只留下截距。看起来模型很“简洁”但本质上就是“均值预测器”毫无用处。用 LassoCV 选出来的最优 alpha 如果接近候选范围的最大端优先怀疑两种可能数据噪声大或者特征和标签之间本来就没有足够强的线性关系。此时不要一味加惩罚反而要考虑特征工程、增加数据量或者切换到非线性模型。另一个相关问题L1 在小样本高纬度下容易不稳定因为样本一少噪声对参数估计的影响会被放大L1 选出的特征变量在不同随机种子下变动很大。这时候记得跑 10 次以上不同的 random_state 做稳定性验证如果选出来的特征集合每次差异很大说明数据本身就撑不起 L1 的特征选择能力。4.2 标准化遗漏导致系数差异巨大很多人把标准化只用在训练集上测试集上只用训练集的 scaler 变换这个没错。但有人先对整个特征矩阵标准化再切分数据集这个问题很隐蔽因为它不会报错只会让验证结果虚高。原因是 scaler 在拟合时已经看到了测试集数据的均值方差等于把测试集信息提前泄露给了模型。一旦用真实场景的数据做推理均值和方差可能完全不同模型就露馅了。我自己的习惯是始终使用 Pipeline把 StandardScaler 和模型绑在一起一步到位from sklearn.pipeline import make_pipeline pipeline make_pipeline(StandardScaler(), LassoCV(cv5)) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test)这样无论怎么切分预处理逻辑都不会越过数据泄漏这条线。4.3 特征高度相关导致 Lasso 表现差特征高度相关时Lasso 选择特征就像“随机抽签”。我已经在前面提到了这一点实操中有一个快速检验方法跑两次 LassoCV 不同随机种子看选出的特征集合重合度。如果重合度低于五成就不要再硬用纯 Lasso。替代方案有两个轻量的先做相关性分析剔除一组相关特征中与其他特征相关性最高但业务解释力不足的再跑 Lasso重量的直接使用 ElasticNet让 L2 部分把相关特征兜住。后者的调参成本只是多一个 l1_ratio但效果通常更可靠。4.4 高维数据里 L1 与 L2 的计算成本差异特征维度极高时L1 由于产生稀疏解计算上反而有优势因为大量系数为 0后续矩阵运算等效维度大幅下降。L2 不稀疏计算成本一直维持在原始特征维度上。但训练过程本身 L1 使用坐标下降迭代可能要很久设置 max_iter 时不要害怕浪费计算量多迭代几百次换来一个收敛结果远比用默认值跑完得到一个可疑系数好。如果是超高维数据比如上百万特征可以优先考虑稀疏矩阵存储配合 SGDRegressor 加 L1 惩罚来近似 Lasso或者用多轮特征筛选降维后再跑 L1。这个经验适用于文本分类中的词袋特征场景我处理过百万维 n-gram 特征时就是先砍低频词数量到几万维再跑 ElasticNet既保留了稀疏性又让训练时间可控。5. 一些踩坑之后的最终建议5.1 先想清楚你要什么再选正则化类型正则化不是单纯的数学技巧它同时是一种模型决策。如果你的目标是模型可解释性、需要向业务方解释哪几个特征是关键影响因素那 L1 的稀疏性是最有价值的因为它天然把特征量缩减到可审核的规模。如果你只是想让预测更稳特征之间又强相关那 L2 是保守且正确的选择。如果你两头都想要ElasticNet 才是真正的答案。不要为了“别人都在用 L1”就盲目换正则化选择。每个模型背后都对应一整套特征工程和业务逻辑正则化选错后续所有分析都可能跟着错。5.2 正则化系数不是越大越好我在项目里见过很多次“为了稀疏性把 alpha 调很高”的冲动操作结果模型解释性好了预测能力却直线下滑。alpha 的本质是告诉模型“你更相信数据还是更相信先验”。数据量原本就大就没有必要强力正则化系数天然会稳定数据量小正则化才有明显收益。合理的做法始终是把 alpha 当超参用交叉验证去选而不是拍脑袋定。同理l1_ratio 也不要墨守成规地固定在 0.5多跑几组网格看验证分数的变化幅度往往会有意外收获。5.3 把训练结果落回到业务解释上最后分享一个工作习惯拿到 L1 选出的特征列表之后我一定会回到业务上一一核对这些特征是否解释得通。如果有个特征在业务上明显不合理但系数非零这通常不是模型“发现新规律”而是数据采集中存在隐藏的噪声或泄漏。丢掉模型跑出来的“玄学特征”比纠结正则化参数更能提升项目的可靠性。L1 与 L2 的差异不是一道面试题而是每个建模流程都要面对的取舍。理解了它们行为上的本质差异再回到数据上去做实验你会发现自己对模型控制力上了一个台阶。
返回列表