资讯详情

权重衰退:深度学习最简正则化技术解析与PyTorch实战

📅 2026/10/20 6:12:51 | 华诺云谱 👁 阅读
权重衰退:深度学习最简正则化技术解析与PyTorch实战
权重衰退是我在深度学习里学过最简单、见效却最稳定的正则化手段没有之一。说它简单是因为实现起来往往只需要在损失函数后面加一项L2范数惩罚说它见效是因为哪怕你什么都不懂把权重衰减系数从0调到0.01过拟合问题立刻就能缓解很多。这篇是动手学深度学习系列的第14篇我会抛开教科书式的推导顺序从一个我反复踩过的场景讲起——训练误差一路下降、验证误差却在疯狂上涨然后用代码把权重衰退一步步实现出来最后再聊聊调参时那些文档上不会写的细节。1. 从多项式回归过拟合说起越大越灵活的模型为何必然失控1.1 一个会过拟合的玩具实验我在给初学者讲线性回归时最喜欢用这个例子数据来自一个平平无奇的三次多项式加了一点高斯噪声然后我们偏要用一个20阶多项式去拟合它。为什么要故意把模型建这么复杂因为神经网络的一个典型特点就是参数量远多于实际需要的数据量。一个只有几千个样本的任务网络参数却有上百万这种情况和用20阶多项式去拟合几个点没有本质区别——模型容量远大于数据信息量。数据生成的代码很直接import torch import torch.nn as nn n_train, n_test 100, 100 num_inputs 20 # x^0 到 x^19 # 真实关系y 5 1.2x - 3.4x^2 5.6x^3 def true_func(X): return 5 1.2 * X - 3.4 * X**2 5.6 * X**3 X torch.rand(n_train n_test, 1) * 2 - 1 # 范围 [-1, 1] y true_func(X) torch.normal(0, 0.05, size(n_train n_test, 1)) def make_features(X, degree19): X_poly torch.zeros(len(X), degree 1) for i in range(degree 1): X_poly[:, i] X[:, 0] ** i return X_poly features make_features(X)然后我们把这个20维特征送进一个最简单的线性模型。先不着急加惩罚项跑一遍你看损失曲线训练误差稳步下降最终降到噪声水平以下几乎贴着0.002测试误差跌到某个低位后转头一路飙升最高时可以比训练误差大出上百倍。这是过拟合最经典的症状也是判断模型是否过拟合的第一指标训练和测试之间的gap越拉越大。这不是测试集出了bug而是模型已经开始背答案了。1.2 过拟合的本质是模型容量失控为什么20阶多项式特别容易过拟合你想想看一个高阶多项式的输出是由x^19、x^18这些高阶项控制的这些项在x接近1时会急剧放大在x接近-1时又会剧烈振荡。如果模型想精确拟合每一个带噪声的点它最省力的做法就是把高阶项的系数调得非常大用剧烈波动去强行穿过每个数据点。结果就是训练点之间的空隙里模型在疯狂画波浪线。这个波浪线在训练样本处和真实值贴得很近但一离开样本点立刻和真实函数分道扬镳。权重越大波浪越剧烈泛化越差。从本质上看模型容量实际上是使用参数的灵活程度而参数的绝对值大小直接决定灵活程度的上限。如果我们能限制权重不要太大模型的表达能力就被锁在了一个球内——球越大越灵活球越小越保守。正则化干的事情就是拿模型在训练集上的友好度去换模型在未见数据上的稳健度。这也是理解权重衰退最重要的铺垫你不需要设计特别花哨的约束只需要盯住权重的范数就能把失控的模型拉回正轨。2. 权重衰退的数学直觉惩罚项如何悄悄改变梯度方向2.1 L2范数惩罚项怎么加原始损失函数是loss MSE(y_hat, y)加上惩罚之后L MSE(y_hat, y) (λ/2) · ||w||²其中||w||²是所有权重元素的平方和λ是控制正则化强度的超参数。这里有两个细节值得说清楚。第一个细节为什么只惩罚w不惩罚偏置b。因为偏置只会让整个拟合曲线整体上下平移一个足够大的b不会产生波浪式的剧烈变化也改变不了模型对输入扰动的敏感程度。我们真正要限制的是那些能改变曲线形状的权重所以动手实现的时候惩罚项只会作用在w上。第二个细节为什么惩罚项前面是λ/2而不是λ。纯粹是为了让求导后的梯度变成λ·w而不是2λ·w公式写起来更清爽。数值上你写成λ也行只是很多教材和框架默认这个1/2系数保持一致可以减少沟通成本。2.2 从梯度更新看权重衰退这个名字惩罚项真正影响模型的地方在梯度下降这一步。对w求梯度∂L/∂w ∂MSE/∂w λ·w用SGD更新时w ← w - η·(∂MSE/∂w λ·w) ← (1 - ηλ)·w - η·∂MSE/∂w和普通SGD相比唯一的差别是第一项权重w先被乘以一个略小于1的系数(1-ηλ)。这个系数每一轮都会把w向0的方向拉回一步就像每天磨损一点点权重衰退这个名字就是从这里来的。我打个比方普通训练像一个只顾往某个方向发力的人肌肉越练越紧最后变得僵硬权重衰退是每天训练完以后做拉伸放松让肌肉保持弹性不至于僵硬到无法做其它动作。当然模型训练不是真的长肌肉但按住了往回收这个动作确实让权重始终保持在合理范围内。从这个更新公式里还能看出一个东西权重衰退的作用强度和学习率是耦合的。η越大每一轮被衰退掉的比例越大。这也是后面调参时要注意的坑先记住这个结论。2.3 贝叶斯视角与岭回归的关系在统计学习里L2惩罚对应岭回归Ridge Regression。这个名词你可能在传统机器学习书里见过它和权重衰退说的是同一个数学对象。在贝叶斯框架里给权重加一个均值为0的高斯先验再做最大后验估计推导出来正好就是L2范数。换句话说权重衰退背后有一个隐含假设在没有足够数据证据时我们倾向于认为权重在0附近是合理的。理解这个视角λ就不再只是一个随便调的数字它代表的是我们对权重应该很小这个先验有多强的信念。λ越大先验方差越小模型越偏保守λ越小越依赖数据本身先验几乎不起作用。为了让理论不悬空建议记住两个后面实验会反复验证的结论λ增大时训练误差会略微上升但验证误差会先降后升最优λ一定出现在验证误差的谷底附近而不是训练误差的最低点。3. 从零手写L2惩罚项亲眼看着权重被压回原点3.1 训练循环里到底发生了什么在动手写代码之前先说明这一节要做什么。我们沿用第一节生成的20维多项式特征训练一个线性回归模型并在损失函数中手动加入惩罚项。手动实现的目的不是重复造轮子而是为了看清训练循环里每一步到底发生了什么梯度从哪儿来、惩罚项怎么进loss、权重在每次更新时被怎么拉。完整训练函数如下def train_with_penalty(X_train, y_train, X_test, y_test, lambda_val, lr0.01, num_epochs1000): w torch.normal(0, 0.01, size(X_train.shape[1], 1), requires_gradTrue) b torch.zeros(1, requires_gradTrue) mse nn.MSELoss(reductionmean) train_losses, test_losses [], [] for epoch in range(num_epochs): y_hat X_train w b loss mse(y_hat, y_train) lambda_val * (w ** 2).sum() / 2 loss.backward() with torch.no_grad(): w - lr * w.grad b - lr * b.grad w.grad.zero_() b.grad.zero_() if epoch % 100 0: with torch.no_grad(): y_hat_train X_train w b y_hat_test X_test w b train_losses.append(mse(y_hat_train, y_train).item()) test_losses.append(mse(y_hat_test, y_test).item()) w_norm torch.sqrt((w ** 2).sum()).item() return w, b, train_losses, test_losses, w_norm三个必须注意的细节惩罚项加在loss里面是训练阶段的行为。计算测试误差时依然只用原始MSE不能再加惩罚项否则测试误差会被污染失去评估意义。loss.backward()之后必须用w.grad.zero_()清空梯度。如果不清空下一次backward会在旧梯度上累加梯度数值会越滚越大训练直接发散。这个错误我第一次写的时候犯过后来养成了每次更新完立刻清零的习惯。手写更新等价于(1-ηλ)w - η·grad。你完全可以先乘一个0.999之类的系数再减梯度结果一样但把惩罚项写进loss更直观、更容易排查逻辑错误。3.2 用数据说话不同λ的对比把λ分别设成0、0.01、0.1跑一遍记录训练误差、测试误差和最终权重范数得到下面这张典型结果数值在不同随机种子下会有小幅波动但趋势非常稳定λ训练误差测试误差权重范数‖w‖00.00212.5814.320.010.00430.153.870.10.0120.061.26两个现象值得展开。第一没有惩罚时测试误差高得离谱权重范数被模型撑到了14左右。这个十几的量级说明高阶项系数非常大拟合曲线上全是剧烈的波浪线。第二加上惩罚之后测试误差骤降权重范数也随之缩水。λ0.1时权重范数已经压到接近1但训练误差也从0.002升到了0.012。这恰恰是正则化该有的效果模型不再追求把每个噪声点都背下来而是把精力放在学习真实的三次函数上。训练误差小幅度牺牲换来的是测试误差几十倍的改善。3.3 权重范数是一个非常值得跟踪的监控指标我在实操里有一个习惯训练的时候除了看loss曲线还会定期打印权重的L2范数。原因很简单权重范数的变化是判断正则化有没有生效的最直接信号。如果λ调大了权重范数纹丝不动大概率是梯度流出了问题或者优化器里的weight_decay配置根本没起作用如果权重范数过小比如λ直接设成10模型很可能已经退化成近似常数预测此时要往回调λ。这个习惯在排查loss掉不下去的问题时特别好用。有些训练不收敛的情况光看loss看不出所以然但把权重范数和梯度范数打印出来立刻能定位到是爆炸还是塌缩。强烈建议所有人在训练脚本里都加这么一行监控成本极低收益很高。4. 一行代码接入weight_decayPyTorch简洁实现的关键差异4.1 简洁实现到底简洁在哪手动实现是为了理解原理但工程项目里很少自己拼接优化器更新步骤。在PyTorch里最简单的做法是在构造优化器时指定weight_decay参数net nn.Sequential(nn.Linear(num_inputs, 1)) optimizer torch.optim.SGD(net.parameters(), lr0.01, weight_decay0.01) for epoch in range(num_epochs): y_hat net(X_train) loss mse(y_hat, y_train) optimizer.zero_grad() loss.backward() optimizer.step()注意简洁实现里我们并没有在loss中手工加惩罚项weight_decay参数会在优化器内部完成这件事。以SGD为例PyTorch对weight_decay的实现方式是在梯度上直接加上λ·w这跟损失函数里加L2惩罚项再求导是数学等价的。这里有一个高频错误用了优化器的weight_decay又在loss里手动加一遍L2惩罚项结果等于施加了双倍正则化验证误差会莫名其妙地比预期高。我见过不止一个新手这么干排查半天最后发现是双重惩罚。4.2 偏置到底应不应该被惩罚前面说过理论上偏置b不应该被惩罚因为它不能引发波浪抖动。从零实现时我们是把b单独拿出来的天然只惩罚了w。但PyTorch的weight_decay参数默认会施加到所有传入优化器的参数上包括nn.Linear的bias。如果你比较较真可以把权重和偏置分成两组分别用不同的配置param_groups [ {params: net[0].weight, weight_decay: 0.01}, {params: net[0].bias, weight_decay: 0} ] optimizer torch.optim.SGD(param_groups, lr0.01)不过说实话在大多数现代网络里bias带来的偏差惩罚影响很小。偏置参数数量通常很少而卷积和全连接的权重动辄百万、千万量级多惩罚几个bias不会让模型欠拟合。很多开源训练脚本就是统一weight_decay实践效果也没问题。所以这个区别知道就行不必为了理论完美而强行分组。4.3 weight_decay在Adam里其实不是L2正则化这一点很多人没意识到我一开始也被坑过。PyTorch里SGD的weight_decay等价于L2正则化但Adam完全不同。Adam的更新规则里有一个自适应学习率机制会针对每个参数单独缩放梯度。如果直接把L2梯度项加进去权重大的维度虽然梯度大但会被分母里的平方梯度累积项放大掉导致正则化效果被削弱。也就是说在Adam里简单加L2惩罚λ的实际作用会变得不可控。学界为此提出了解耦权重衰减decoupled weight decay也就是AdamW把权重衰退从梯度计算中抽出来在更新完参数后单独乘上一个小于1的衰减系数而不是混进梯度里。PyTorch里的torch.optim.AdamW就是这个思路。在做大规模预训练和微调时几乎所有开源脚本用的都是AdamW这不是偶然。补充一句对于普通小模型SGDweight_decay和AdamW的最终结果差别不会特别大。但理解这个区别能让你在复杂的Transformer类模型里少走弯路尤其是当你想调整λ的时候至少知道自己在调什么东西。5. 调参实战笔记λ怎么选、和其他正则策略怎么配合5.1 λ选择的经验法则权重衰退虽然简单λ怎么定却没有统一公式。我的常规做法是网格搜索从λ0开始先确认模型确实过拟合训练误差远小于验证误差按数量级往上试1e-4、1e-3、1e-2、1e-1、1记录每一档的训练误差和验证误差画在同一张图上看验证误差在哪一档达到最低点。选λ的时候不要看训练误差它只会单调上升。验证误差走的是U型过小正则化不足过大模型被压得太死连真实信号都学不动欠拟合了。以我的经验λ很少超过0.1常见区间是[1e-4, 1e-2]。但这不是绝对的它还和权重初始化的尺度、学习率大小直接相关。如果你用大学习率训练大模型权重本身涨得快λ可以适当调大如果模型参数本来就受限λ需要调小否则表达能力会被额外削弱。5.2 权重衰退作用在哪一层权重衰退可以施加在所有可学习参数上但实际操作中往往要区分对待embedding层通常设置较小的weight_decay因为这类参数承担了大量信息存储作用压得太狠会让表示空间退化归一化层的γ、β参数实践中一般不设置weight_decay因为这些参数的本质是让每一层的分布灵活可变约束它们没有意义还可能带来数值问题偏置通常不设或设置一个很小的weight_decay。很多开源Transformer训练脚本的参数分组里你会看到对bias和归一化层参数设置weight_decay0.0这不是玄学而是有明确依据的。归一化层的缩放和平移本来就是为了保留模型表达能力再去正则化它等于自相矛盾。5.3 与Dropout、早停、数据增强的搭配权重衰退是参数层面的约束和结构层面的Dropout、流程层面的早停是正交的可以叠着用互不冲突。我通常的顺序是先单独加权重衰退看验证误差的回落如果还不够再加Dropout、再做早停、再做数据增强每一步都用验证误差单独评估而不是一次性全堆上去。为什么要这样因为正则化手段叠加后各自的效果并不总是112。有时它们会互相干扰比如Dropout增大时模型的有效容量已经降低此时再把λ调大很容易直接跨到欠拟合区。一个稳妥的策略是固定学习率先扫λ再扫Dropout比例最后做早停。5.4 实际操作中容易踩的三个坑坑一特征没有标准化就加权重惩罚。如果特征X的量纲差异很大比如一个特征在0~1另一个在0~10000那么同样大小的权重在这两个特征上对输出的影响完全不同L2范数会不公平地惩罚量纲大的特征对应的权重。所以在结构化特征场景下用权重衰退前一定要先标准化。图像深度网络里问题不大因为像素值天然在0~255之间。坑二重复施加正则化。就是前面说的用了weight_decay又在loss里手动加L2惩罚双重惩罚会异常放大正则强度。排查方法很简单取出一批参数手算一下加惩罚前后的梯度看看是不是差了一倍。坑三用学习率调度器时没考虑weight_decay的耦合。从梯度更新公式可以看出学习率变小后每一步参数更新幅度变小但权重衰退的系数(1-ηλ)却依然在起作用。如果η降到非常低而λ又开得比较大训练后期权重会被持续往0压出现前100轮正常、后100轮权重范数突然塌缩的现象。我在一个特征工程实验里就遇到过这种诡异情况前面的loss曲线正常下降最后几百轮突然训练误差和验证误差都开始上升。一开始怀疑数据出错了后来把权重范数打出来才发现是学习率余弦退火降得过低同时λ值又偏大两个因素叠加直接把模型拉到了欠拟合区。把λ降到原来的四分之一后问题立刻消失。最后分享一个我自己的习惯在新任务上哪怕一开始不觉得会过拟合我也会先开一个小的权重衰退λ1e-4左右训练基线。因为权重衰退几乎不增加训练成本却能明显提高结果稳定性。我见过很多训练曲线抖动剧烈的情况答案往往不是学习率问题而是正则化缺失加上权重衰退之后曲线立刻平滑不少。真正的问题从来不是权重衰退没有用而是你常常忘了用它。下一篇继续聊聊动量法到时见。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑