机器学习正则化实战:L1、L2、弹性网与Dropout原理与调参

📅 发布时间:2026/10/5 7:48:59
机器学习正则化实战:L1、L2、弹性网与Dropout原理与调参
先说说我为什么想整理这一篇。做机器学习项目的同学应该都有过这种体验模型在训练集上跑出接近满分的成绩一丢到验证集上立刻原形毕露损失曲线往上翻预测误差大得离谱。这就是典型的过拟合。而机器学习里对付过拟合最直接、最标准的手段之一就是正则化。我打算把 L1、L2、弹性网、Dropout 这些常用正则化手段的原理讲透配套给出可以直接运行的案例代码也把这几年调正则化系数踩过的坑一并写出来。无论你是刚接触正则化的学生还是已经在用 sklearn 和 PyTorch 做项目、想系统补一遍基本功的同学都能从这篇文章里拿到可以直接上手的干货。1. 正则化到底在治什么病先给过拟合把个脉1.1 过拟合的典型症状与判断方法先聊一个很直观的场景。你用多项式去拟合一条 sin 曲线次数从 3 次提到 7 次的时候训练误差会低到让人兴奋可一旦用独立的测试集去算误差反而比 3 次多项式大好几倍。这个现象不是某个模型特有的线性回归、逻辑回归、神经网络、梯度提升树全都逃不掉。本质上模型在训练阶段“记”住了数据本身的噪声而不是真正学到了背后的规律。判断过拟合不需要什么高深工具三个信号就够了第一训练集损失持续下降但验证集损失出现先降后升的反转点第二权重向量的绝对值普遍偏大模型对输入的细微变化极其敏感第三把训练样本稍微加一点扰动预测结果就会发生很大的波动。如果你发现自己训练的模型符合其中任意两条就该考虑上正则化了。我自己的习惯是任何模型训练完第一件事不是看精度而是画一张训练/验证损失曲线。曲线图上如果两条线之间的“沟壑”越来越宽说明方差在主导误差如果两条线都比较高、贴在一起说明偏差在主导。后一种情况加正则化基本没用真正需要的是增加特征、换更强模型或者加深网络。1.2 偏差-方差权衡正则化的理论根基正则化能生效背后靠的是偏差-方差分解这个统计学习里的基本框架。一个模型的泛化误差大致可以拆成三块噪声本身、偏差的平方、方差。写出来就是Err(x) σ² Bias² Variance噪声是数据天生带来的任何模型都躲不掉能控制的就是偏差和方差。偏差衡量的是模型平均下来和真实规律的偏离程度方差衡量的是模型在不同训练集上表现的不稳定性。用打靶来类比偏差是瞄得准不准方差是手抖不抖。一个模型拟合能力太强相当于瞄得很准但手一直在抖打出去的子弹分布很散这对应方差过大模型过于简单则是手稳了但瞄点本身偏了对应偏差过大。正则化做的事情是在原来的经验风险上叠加一个结构风险惩罚项新的目标函数写作J(w) Loss(w) λ·Ω(w)Loss 是原始损失Ω 是惩罚项λ 是控制惩罚强度的正则化系数。加上惩罚项之后模型不再只追求把训练集拟合到位还要兼顾权重不要过于极端。这样做的代价通常是偏差略微上升但换来的是方差大幅下降总误差反而更小。理解这一点非常重要因为它决定了正则化调参的方向当验证集误差的高方差特征明显时加大 λ当训练集本身都拟合不好时反而应该减小 λ 或者换模型。2. L1 与 L2 的数学原理与几何直觉为什么一个选边一个压小2.1 L2 权重衰减的数学逻辑和更新公式正则化家族里最常碰面的就是 L2也叫岭回归在神经网络里它还有个更常见的名字叫权重衰减weight decay。L2 的惩罚项是所有参数的平方和目标函数是J MSE λ·Σ wᵢ²为什么这个惩罚能把权重压小把梯度下降的更新公式展开就一目了然。假设原始损失对 w 的梯度是 ∇L加上 L2 惩罚后整体梯度变成 ∇L 2λw于是梯度下降更新为w ← w − η·(∇L 2λw) w·(1 − 2ηλ) − η·∇L前面这一项 (1 − 2ηλ) 小于 1相当于每一步更新都先把当前的权重等比压缩一圈再沿梯度负方向移动。学习率 η 和惩罚系数 λ 越大压缩比例就越狠。这也是“权重衰减”这个名字的由来权重在每个 iteration 里都在被“缓慢磨损”磨损的方向与梯度无关纯粹向零收缩。从另一个角度看L2 给损失函数增加的是一个二次凸项在最小化时会把原来可能病态的 Hessian 矩阵往对角方向拉一拉。岭回归的闭式解是 (XᵀX λI)⁻¹Xᵀy这里的 λI 相当于给 XᵀX 的对角线加了一个正的扰动即使原矩阵接近奇异求逆也稳定得多。所以 L2 在我看来有双重身份既是过拟合的刹车也是数值稳定性的保护垫。2.2 L1 的特殊之处稀疏解是怎么来的L1 正则化LASSO的惩罚项是权重的绝对值之和λ·Σ|wᵢ|。同样是“罚”它和 L2 的脾气完全不同。L2 的梯度在接近零时会因为 2λw 这一项而变小所以权重会无限趋近零但很少真正等于零L1 的梯度则是常数符号λ·sign(w)。这意味着只要权重不为零惩罚对它的推力就恒定不变不会因为“快接近零了”就温柔起来。而在 w0 这个不可导点上优化过程会用次梯度处理结果就是许多权重被精确地推到零。用几何图形来理解更直观。假设只有两个权重 w₁、w₂L2 对应的约束区域是一个圆形L1 对应的是一个菱形。损失函数的等值线是中心在最优解处的椭圆。不加约束时椭圆中心落在哪里就是哪里加了约束后最优点必须在约束区域上。椭圆向外扩张的过程中最先碰到圆形的点大概率不在坐标轴上但碰到菱形的点却很容易落在菱形的顶点上而顶点恰好就是 w₁0 或 w₂0 所在的坐标轴。因此L1 天然具备稀疏化倾向它真正做到了“不重要的特征直接归零”相当于把特征选择和模型训练一步完成。2.3 L1 与 L2 的对比和选型建议把两者的差别整理成一个表用起来就很清晰对比维度L2/权重衰减L1/LASSO惩罚表达式λ·Σ wᵢ²λ·Σ绝对值 wᵢ梯度作用按比例衰减越接近 0 推力越弱恒定符号推力能精确归零解的形态权重变小但几乎不出现硬 0部分权重精确为 0典型场景特征基本都有用侧重防过拟合高维稀疏、特征筛选、可解释性主要风险不具备特征选择能力强相关特征会被随机选中一个选型上我给一个务实的建议如果你做的是表格型数据特征几十维到几百维且事先不知道哪些特征有用优先试 L1 或弹性网它能帮你完成一轮特征粗筛如果特征之间相关性很高直接用 L1 会有“随机挑选一个代表”的问题这时候 L2 或弹性网表现更稳如果特征数量超过样本数量L1 最多只能选出与样本数相同数量的非零特征这时候就要考虑先做降维或者直接用弹性网。3. 正则化系数 λ 怎么调三步实操法3.1 λ 的两极效应与量纲范围λ 是正则化方法里最关键的旋钮。设成 0正则化失效原模型该怎么过拟合还怎么过拟合设得太大惩罚项压过原始损失所有权重会被压向零模型退化成近似常量预测。这个“中间找平衡”的过程说白了就是偏差和方差的拉锯战。实际操作中λ 的搜索范围我一般不看线性等距而是用对数网格比如从 1e-4 到 1e2 之间均匀取 20 个点。原因很简单λ 对模型效果的影响通常跨越多个数量级线性网格会把大量计算浪费在无效区间。还有一点要特别注意——λ 的合理范围受特征尺度影响。如果特征没有标准化取值范围大的特征对应的系数天然就小惩罚对它不起作用取值范围小的特征系数天然偏大反而更容易被惩罚。所以我在所有需要加 L1/L2 的模型里第一步永远是 StandardScaler 标准化没有例外。3.2 三步调参标准化、粗搜索、看曲线第一步对所有数值特征做标准化让每个维度的方差差不多是 1这样惩罚项对不同特征的约束才是公平的。第二步用带交叉验证的模型直接粗搜 λ。以 sklearn 为例from sklearn.linear_model import RidgeCV, LassoCV, ElasticNetCV import numpy as np # 对数网格取 20 个候选 alphas np.logspace(-3, 2, 20) ridge RidgeCV(alphasalphas, cv5) ridge.fit(X_train, y_train) print(Ridge 最优 alpha:, ridge.alpha_) lasso LassoCV(alphasalphas, cv5, random_state42, max_iter50000) lasso.fit(X_train, y_train) print(Lasso 最优 alpha:, lasso.alpha_) enet ElasticNetCV( alphasalphas, l1_ratio[0.1, 0.5, 0.7, 0.9, 1.0], cv5, random_state42, max_iter50000 ) enet.fit(X_train, y_train) print(ElasticNet 最优 alpha:, enet.alpha_, l1_ratio:, enet.l1_ratio_)代码里l1_ratio是弹性网里 L1 惩罚所占的比重取 1 时退化为 Lasso取 0 时退化为 Ridge。实际运行时这套代码会把四个候选模型的最优 λ 一次跑完数据量不大的情况下几秒就能出结果。粗搜结束后如果你还想精调可以在最优值周围的 0.5 倍到 2 倍区间内再细分一个网格效果提升通常有限但能做到心中有数。第三步也是最容易被跳过的一步观察正则化路径图。把不同 λ 下各特征系数画出来横轴是 λ纵轴是系数L1 会呈现一条条“断崖式”落到零的线L2 则是平滑地向零收缩。画一次路径图你就能直观看到 λ 在什么区间开始真正压制某个特征这比只看一个交叉验证的最优点有用得多。3.3 学习曲线验证别只看训练集网格搜索选出 α 之后我还习惯再用学习曲线复查一遍。学习曲线展示的是训练集误差和验证集误差随训练样本量变化的曲线。如果两条曲线随着样本量增大始终保持着很宽的间距说明模型当前依然是方差主导可以继续增大 λ 验证一下如果训练集本身误差就很高说明偏差主导加正则化意义不大。这里尤其要强调一个容错心态不要追求验证集上那个“理论最优 λ”的精确值。交叉验证本身带有随机性λ 在最优值附近小范围波动对最终泛化效果的影响往往微乎其微。我在实践中经常遇到“α0.013 和 α0.021 差不了多少”的情况与其纠结小数点不如多跑几次不同的随机种子确认结果稳定。4. 正则化家族进阶弹性网、Dropout 与 Early Stopping4.1 弹性网当 L1 和 L2 相遇弹性网ElasticNet是把 L1 和 L2 揉在一起目标函数是J MSE λ₁·Σ|wᵢ| λ₂·Σ wᵢ²它的价值在于解决了两个实际问题。第一个问题是高相关特征下的 L1 不稳定两组强相关特征放在一起Lasso 会随机选其中一个换一组训练数据可能就换一个这导致模型可解释性很差。弹性网里 L2 项的存在会让强相关的特征组“有难同当”系数被同时压缩但不会被单独剔掉。第二个问题是 p n 场景Lasso 最多只能选出 n 个非零特征但业务里常常特征上万、样本只有几千弹性网通过 L2 项的稳定作用可以绕过这个限制。sklearn 中弹性网的主要参数有两个alpha控制总惩罚强度l1_ratio控制 L1 占比。我一般把l1_ratio当作一个连续旋钮来理解0 是纯 L21 是纯 L10.5 就是一半一半。做业务项目时如果对特征是否稀疏没有先验我通常从l1_ratio[0.1, 0.5, 0.7, 0.9]开始让 CV 自己定。4.2 Dropout 与 Early Stopping深度学习的隐式正则化神经网络里权重衰减对应 L2但深度学习最常用的正则化其实是 Dropout 和 Early Stopping。Dropout 的思路特别朴素训练时每个神经元以概率 p 随机“失联”让网络不能过度依赖某几个节点测试时再把所有神经元按保留概率缩放恢复。这个操作迫使模型学到冗余的、分布式的特征表示本质上等价于同时训练了大量不同结构的子网络最后取平均效果方差自然就降下来了。Early Stopping 连参数都不用动做的是“管住训练轮数”。验证集指标连续多轮不涨就停因为模型在训练后期往往已经开始把噪声学进权重多跑一步都是伤害。它算是迭代轮数的隐式正则化我会在所有神经网络训练里都开启配合 10 到 20 轮的 patience稳定效果立竿见影。4.3 一批不太像正则化的正则化手段还有一些手段不叫正则化但实际效果和正则化殊途同归。数据增强是给输入加旋转、裁剪、噪声扰动等于不断提醒模型“这些变化不该改变预测”对决策边界的平滑效果非常明显。标签平滑把 one-hot 的硬标签变成 0.9/0.05 这类软标签能抑制模型对训练样本过强的置信度。批量归一化也有一定的隐式正则化作用因为每个 batch 的统计量都不同等价于往中间层注入了一点噪声只是它的主要作用还是加速收敛不能当作主正则化来用。在实际项目里我不会把正则化手段全部堆上而是按这个优先级走先做 Early Stopping 和随机种子稳定再加 Weight Decay数据量小或类别不平衡时加数据增强最后才考虑 Dropout。原因很简单不加验证的无脑堆叠会掩盖真正起作用的那一个。5. 三种框架的代码实现numpy、sklearn 与 PyTorch5.1 自造轮子用 numpy 实现带 L2 的梯度下降理解正则化最好的方式是自己手写一次带惩罚项的梯度下降。下面这个例子用 10 维模拟数据真实权重只有 3.0、-2.0、1.0、0.5 这 4 个非零剩下的都是噪声特征天然适合验证正则化能不能把无效特征压下去。import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler np.random.seed(42) n, d 500, 10 true_w np.array([3.0, 0.0, 0.0, -2.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.5]) X np.random.randn(n, d) y X true_w np.random.randn(n) * 0.3 scaler StandardScaler() X scaler.fit_transform(X) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42 ) def l2_gradient_descent(X, y, lr0.01, lambd0.0, epochs3000): w np.zeros(X.shape[1]) loss_history [] for _ in range(epochs): pred X w loss np.mean((pred - y) ** 2) lambd * np.sum(w ** 2) grad 2 / len(y) * X.T (pred - y) 2 * lambd * w w - lr * grad loss_history.append(loss) return w, loss_history w_no_reg, _ l2_gradient_descent(X_train, y_train, lambd0.0) w_reg, _ l2_gradient_descent(X_train, y_train, lambd0.1) val_mse_no_reg np.mean((X_val w_no_reg - y_val) ** 2) val_mse_reg np.mean((X_val w_reg - y_val) ** 2) print(不加正则化 验证集 MSE:, round(val_mse_no_reg, 4)) print(加 L2 正则化 验证集 MSE:, round(val_mse_reg, 4))运行之后你会发现不加正则化的权重会去拟合噪声验证集 MSE 通常明显高于加了lambd0.1的版本。把w_reg打印出来权重普遍被压实到真值的附近这就是 L2 的直观效果。这个手写版本虽然不会用在正式项目里但它把公式里的每一项都变成了可看见的数值变化对理解正则化机制帮助很大。5.2 sklearn 标准流程标准化、CV 与模型对比正规的工程项目我一般直接上 sklearn核心是别把标准化落下以及用带 CV 的模型来选 α。完整流程延续前面模拟数据代码把 Ridge、Lasso、ElasticNet 放在一起对比from sklearn.linear_model import Ridge, Lasso, ElasticNet from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import cross_val_score alphas np.logspace(-3, 2, 20) for name, model_class in [(Ridge, Ridge), (Lasso, Lasso), (ElasticNet, ElasticNet)]: # ElasticNet 需要单独给 l1_ratio这里取默认 0.5 if name ElasticNet: model model_class(alpha0.1, l1_ratio0.5, max_iter50000) else: model model_class(alpha0.1, max_iter50000) pipe make_pipeline(StandardScaler(), model) # 或者直接用前面已经标准化的 X 也可以 cv_score cross_val_score(pipe, X_train, y_train, cv5, scoringneg_mean_squared_error).mean() print(f{name} CV R2: {cv_score:.4f})实际跑的时候更推荐直接用RidgeCV、LassoCV、ElasticNetCV这三个内置 CV 的类它们在内部完成 α 的搜索。如果你想知道具体每个模型在 α 变化时表现如何可以把 α 网格传入alphas参数让模型自动输出最优值。这里有一个容易被忽视的体验LassoCV默认的max_iter在高维数据下可能不够训练时如果出现警告就把max_iter提到 50000 或者更大同时加重tol的精度设置。5.3 PyTorch 里的 weight_decay 与手动 L1神经网络里的 L2 正则化不用自己往 loss 里加惩罚项直接在优化器里设weight_decay参数就行。PyTorch 的 Adam 和 SGD 都支持这个参数它在梯度下降内部等价于加入 L2 惩罚import torch import torch.nn as nn X_t torch.tensor(X_train, dtypetorch.float32) y_t torch.tensor(y_train, dtypetorch.float32).view(-1, 1) model nn.Linear(X_t.shape[1], 1) optimizer torch.optim.Adam(model.parameters(), lr0.01, weight_decay0.01) loss_fn nn.MSELoss() for epoch in range(5000): optimizer.zero_grad() pred model(X_t) loss loss_fn(pred, y_t) loss.backward() optimizer.step() with torch.no_grad(): val_pred model(torch.tensor(X_val, dtypetorch.float32)).squeeze() val_loss loss_fn(val_pred, torch.tensor(y_val, dtypetorch.float32)) print(PyTorch weight_decay 验证集 loss:, val_loss.item())很多人第一次用weight_decay会好奇它和手动往 loss 里加lambd * sum(w**2)是否完全等价。严格说PyTorch 是在梯度上直接扣除了衰减项而手动加惩罚是在 loss 里计算后再对整体求梯度两者在标准 SGD 下是等价的在 Adam 这类自适应优化器下会有细微差别。不过在实际调参中这个差别对最终效果的影响通常可以忽略你把weight_decay当作 L2 系数用就行。如果项目里确实需要 L1 稀疏性PyTorch 没有内置的简单开关需要手动把惩罚项加到 loss 里。注意只对权重做惩罚、不对 bias 做因为 bias 没有“特征相关性”的语义# 在每个 step 里loss 单独加上 L1 惩罚 l1_lambd 0.001 l1_penalty l1_lambd * sum(p.abs().sum() for p in model.parameters() if p.dim() 0) loss_to_backward loss_fn(pred, y_t) l1_penalty6. 正则化实战避坑清单与常见问题6.1 高频翻车点七个踩过的坑这七年里我在正则化上踩过的坑比任何一种技巧都多。挑七个最常见的整理成表看到任何一个都建议直接绕道坑点现象正确做法忘了标准化大范围特征被“豁免”小范围特征被罚过度任何 L1/L2 前先 StandardScalerα 搜索范围用线性网格要么全在无效区间要么错过最优数量级用 logspace 覆盖多个数量级用全量数据选 α 再交叉验证验证结果虚高泛化表现差α 选择必须在 CV 内部完成L1 筛完特征不复查强相关特征被随机删除业务解释失真配合相关性分析检查被删特征深度模型用 L1 做稀疏训练损失很难降优化不稳定默认用 weight_decay特殊场景再用 L1正则化惩罚包含 bias截距被无意义压缩模型预测偏差增大手动实现时只惩罚权重维度树模型上照搬 L1/L2没提升还增加参数数量树模型靠深度、叶子数和样本权重控容量其中“用全量数据选 α 再交叉验证”这个错是最隐蔽的。很多人先跑一次LassoCV选出 α然后用这个固定 α 去做常规交叉验证结果验证分数特别漂亮上线后却崩了。原因很简单α 的信息已经从全量数据流进了模型交叉验证形同虚设。正确的做法是把 α 搜索放进 CV 的每一折里面也就是直接使用LassoCV(cv5)这种内置流程它会自动保证每一折都只用自己的训练部分选 α。6.2 正则化无效时的下一步排查顺序有时候加了正则化验证集误差纹丝不动这时候不要死磕。先排查标准化是否做好再确认 α 搜索范围是否覆盖了有效区间如果模型本身容量很小比如只剩一个线性层的欠拟合状态加惩罚只会雪上加霜。此时最该做的是提升原始表达能力增加特征、降低噪声、上更灵活的非线性模型等你把模型容量涨上去了正则化才重新有发挥空间。一批没有正则化的异常数据也可能让惩罚失效比如特征里有大量缺失值、离群点、稀疏假特征。处理这些脏数据效果往往比调 λ 来得更直接。所以我的排查顺序永远是数据先行、损失曲线次之、α 搜索最后。最后分享一个调参时的小体会这几年下来我逐渐养成一个习惯无论哪个项目先把不带正则化的模型跑一遍拿到训练集与验证集的误差差再决定要不要加正则化、加多大。因为正则化不是万能药它只治“方差过大”这一种病。如果训练误差本来就高加再大的 λ 也只是把模型推得更笨而已。正确定位问题比满屏堆技巧重要得多。