1. L1正则化为什么会火
做深度学习这些年,我见过不少模型训练完一看权重,几千个参数没几个是真正有用的,整个网络又大又慢,还特别容易过拟合。早期我习惯无脑加L2正则化,效果有,但总觉得差了点什么。直到一个文本分类项目里,需要把特征维度从几万压缩到几千,我才认认真真把L1正则化从原理到落地研究了一遍。这篇文章就是我自己反复踩坑后梳理出来的总结,适合已经会搭建基础网络、想进一步控制模型复杂度或者做特征选择的同学,也适合那些听说过L1能产生稀疏解但一直没搞懂“为什么”的人。
L1正则化,也叫Lasso正则化,核心动作是在损失函数里加上所有权重绝对值之和。它的最大卖点就是让一部分权重变成精确的零,从而让模型自动做特征选择,而不是像L2那样只把权重压小但保留全部维度。这个“自动置零”的特性,在很多场景里非常值钱:模型文件更小、推理更快、可解释性更强,甚至能帮我们理解数据里到底哪些信号在起作用。
不过,L1正则化不是随便加个惩罚项就完事。它涉及梯度求解的特殊处理、超参数lambda的精细调节、优化器的选择,甚至和BatchNorm放在一起还会闹别扭。这些坑我基本都踩过,所以这篇文章打算讲透三件事:L1为什么能产生稀疏解、实践中怎么用才能发挥它的威力、以及遇到问题时怎么排查。
2. 核心原理:稀疏性从哪儿来的
2.1 从几何视角理解L1和L2的本质差异
要理解L1为什么能把权重归零,最直观的方式是看约束空间的几何形状。假设只有两个权重w1和w2,没有正则化时,损失函数的最优解在某个点上。加上L2正则化,等价于在w1²+w2²≤r的圆内找最优解;加上L1正则化,则是在|w1|+|w2|≤r的菱形内找最优解。
问题就出在菱形的角上。当损失函数的等高线往约束边界靠的时候,圆形边界是光滑的,切点一般落在坐标轴之外,两个权重都不为零;但菱形边界在坐标轴上有尖锐的顶点,等高线很容易跟顶点相切,这时其中一个权重就是零。从高维角度看,L1的约束空间是个超多面体,角点非常多,所以稀疏解出现的概率天然就高。
我刚开始学时有个疑惑:既然L2也有压缩效果,为什么不能把权重压到非常接近零的值然后手动剪枝?理论上可以,但要设置一个阈值去截断,而这个阈值选多少是个新问题;L1的好处是零就是零,不需要额外处理。在实际项目中,比如构建CTR预估模型,几千万维的特征经过L1筛选后留下来可能只有几万维,这个稀疏性对线上内存和计算延迟的改善是数量级的。
2.2 统计学视角下的拉普拉斯先验
从贝叶斯角度理解会更有味道。L2正则化等价于给权重加上高斯先验,L1正则化等价于给权重加上拉普拉斯先验。高斯分布在0附近密度很高,但两端也有明显尾巴,所以估计值会偏向0却很少精确等于0。拉普拉斯分布在0处有一个尖峰,概率密度集中在原点附近,后验最大化的结果就更容易直接落在0上。
我印象很深的是一次特征筛选实验:同一个数据集,用L2训练后权重分布像一条平滑的山脉,数值从负到正连绵不断;用L1训练后,权重的直方图在0处形成一根高耸的针,两侧迅速衰减,数值精确为0的参数占了七成。那种分布形状一下就把两个正则化的差异摆在了眼前,比任何抽象的数学描述都有说服力。
不过这里有个容易忽略的点:拉普拉斯先验假设权重独立同分布,现实中特征之间往往存在相关性。如果两个特征高度相关,L1会倾向于随便选一个,另一个直接置零,这也意味着L1的特征选择结果不一定语义上最优,但它确实把冗余信息剪掉了。
3. 数学推导与工程实现的细节
3.1 损失函数与次梯度的坑
L1正则化的损失函数长这样:
L = Loss(y_pred, y_true) + λ * sum(|w_i|)
听起来简单,难点在于|w|在w=0处不可导。深度学习框架的反向传播要求每个节点都有导数,于是就有了次梯度的概念。常规做法是在w=0时把梯度定义为0,PyTorch的sign函数的默认行为就是这样。
这个细节影响很大。次梯度为0意味着在训练过程中,一旦某个权重变成0,它可能就永远停在0了,梯度更新不再把它推离0。这看起来是好事,但实际训练中如果学习率太大,权重的符号会频繁翻转,出现一种“抖来抖去”的现象,最终稀疏率反而不理想。我自己遇到过的典型情况是:用了Adam优化器,学习率设成1e-3,L1惩罚系数设成0.01,训练到一半发现很多权重在0附近来回振荡。后来把学习率降到1e-4,设置warmup让模型先拟合数据再把惩罚项加进去,振荡问题才缓解。
所以引入一个经验法则:L1正则化对学习率比L2更敏感。L2惩罚项是平缓的线性压缩,L1在0附近的梯度突变,容易和优化器的动量机制起冲突。
3.2 不依赖框架符号微分的实现方案
除了直接依赖框架的自动求导,还有一种更可控的实现方式是近端梯度法。它的核心思路是先把损失函数和正则项分开处理:先做正常的梯度下降更新权重,再做一步“软阈值”操作:
prox(w_i) = sign(w_i) * max(|w_i| - λ*η, 0)
这里η是学习率。软阈值操作直接将绝对值小于阈值λ*η的权重归零,其余的按比例缩小。这个方法的好处是规避了w=0处的次梯度问题,而且稀疏化效果非常干净。
我实际测试过,在同样的任务上,近端梯度的稀疏率比直接对|w|求导的朴素实现要高5到10个百分点,而且训练过程更平稳。不过要注意,PyTorch里要实现近端梯度,不能只靠loss.backward(),需要手动控制参数更新逻辑,比如在optimizer.step()之后遍历参数做软阈值操作。这样做的代价是失去了自适应学习率的联动,可能需要更精细地调整baseline学习率。对小项目或者教学演示来说,直接加惩罚项省事;但做大规模特征筛选时,软阈值方案更可靠。
3.3 优化器选型对L1效果的实际影响
优化器对L1的影响常常被低估。我做过一组对照实验,固定λ和网络结构,分别用SGD动量、Adam和AdamW训练带L1的模型,结果稀疏率差别不大,但最终的精度和稀疏性的平衡差异明显。SGD动量配合L1更容易获得真正的零权重,因为它的更新方式比较简单,软阈值效果能直观体现;Adam会按梯度平方的累积对学习率做缩放,等价于在某种尺度上改变了L1惩罚项的权重,所以同样的λ在Adam下的实际惩罚强度更弱。
如果用了AdamW又叠加L1惩罚,会出现重复惩罚的问题。AdamW本身已经把权重衰减从梯度里分离出来,这时再把L1加进loss里,相当于两种正则化在不同环节起作用,很可能惩罚过重导致模型欠拟合。我的建议是:L1惩罚项放在loss里是最直观的,适合大多数场景;如果项目对稀疏性要求极高,可以试试单独写一个L1惩罚的优化器钩子,在每个step后直接对参数做软阈值处理,绕开优化器内部的复杂交互。
4. 实操过程与选参策略
4.1 从目标出发选λ
λ是L1正则化里最重要的超参数,没有之一。它直接控制稀疏率和拟合程度。我调节λ的流程一般是三步走:先在log尺度上用几个值快速跑通网络,观察损失曲线和验证集指标;再在表现最好的区间细化搜索;最后结合稀疏率做权衡,看业务到底是要精度高一点还是压缩多一点。
具体数值很难直接抄,跟模型大小、数据量、损失函数量级都有关系。如果损失是MSE均方差,数据都在0到1之间,模型有几千个参数,λ从1e-5到1e-3之间通常就是有效区间;如果是交叉熵损失,量级偏小,λ就要相应往小调,有时1e-6才开始起效。最稳妥的办法不是盯着λ本身,而是看每个epoch后权重中“精确为0”的比例,如果这个比例在训练初期就疯狂上升,说明λ偏大,模型还来不及学就快被清零了;如果训练到后期稀疏率还在低位数徘徊,说明λ偏小。
我习惯把稀疏率曲线和loss曲线画在同一张图上,一眼就能看出谁抢了谁的地盘。有一次我用二元分类任务测试,λ从1e-4调到1e-2,精度只下降了不到1个百分点,稀疏率从5%涨到45%,这个交换比非常划算;继续调到5e-2,稀疏率到65%,但精度掉了一大截,那就是过犹不及了。
4.2 特征维度的状态观察
不要只盯着权重数值,要把特征维度的状态可视化出来。训练完成后,我会把每一维度的权重绝对值从小到大排序,画成一个阶梯图。L1训练出来的图会有一个明显的“悬崖”:前面一堆权重是0,后面忽然出现一串非零权重。L2则是平滑的斜坡。看到悬崖,就知道稀疏化成功了。
另外一个很实用的工具是记录特征被清零的“顺序”。在训练早期就把某些特征清零,说明这些特征对损失函数的贡献很小;而那些坚持到最后的特征,往往就是模型真正的决策依据。我在一个用户画像项目里,通过这种方式筛掉了十几个相关性极低的字段,不仅压缩了模型体积,还帮业务部门理解了哪些数据值得继续采集。
需要注意,稀疏解不等同于“重要特征排序”。由于特征之间存在共线性,L1可能随机挑选相关特征组里的某一个,把另一个清零。所以在结论汇报时,最好通过多次随机采样训练观察特征被保留的稳定性,而不是直接断言“这个特征没用”。
4.3 与BN、Dropout的相处方式
L1正则化和BatchNorm放在一起会有微妙的化学反应。BatchNorm本身自带一定的正则化效果,因为它让每个batch的统计量变化提供了噪声,可以理解为一种隐式正则。加上BN之后,模型的拟合能力变强,L1的作用也会被稀释一些,需要稍微调大λ才能达到同等稀疏率。
我踩过的一个坑是:网络结构里先加Dropout,又在loss后加L1,结果稀疏率涨得很慢。原因是Dropout在训练时随机丢弃神经元,相当于改变了有效网络结构,L1建模的那批“固定”参数被干扰了。后来我把Dropout比例从0.5降到0.2,同时把λ上调50%,才看到预期的稀疏效果。另一个常见问题是,L1加在卷积层上,卷积核内部本来就有空间结构,稀疏化会让某些通道变成全零矩阵,从而等价于剪枝,但这需要额外的通道判断逻辑,如果框架不自动处理,推理时这部分计算照样会被执行,性能提升就打折扣。
- 经验:L1在BatchNorm之前或之后的参数上作用效果差异很大。对全连接层加L1一般比较温和;对卷积核加L1更容易产生结构性稀疏,但需要配合剪枝代码。
- 经验:BN的gamma参数加L1是一种变体做法,用它做通道剪枝是近几年很常见的方案,比直接对权重加L1更容易控制稀疏粒度。
5. 常见问题与排查技巧
5.1 稀疏率上不去怎么办
最基础也是最高频的问题:加了L1,为什么训练完稀疏率还是很低?我遇到过的原因主要有四个。第一个是λ太小,惩罚力度不够,权重虽然有收缩但还没到0,这种情况把λ调大一个数量级通常就能改善,但要注意伴随着K值增大,肉眼可见地,损失函数里的正则占比也升高了;对应地,因为正则项偏大,数据损失被压制,训练早期loss下降会变慢。第二个是优化器交互问题,尤其是Adam会让L1的实际惩罚打折扣,可以改用SGD动量或者做软阈值更新。第三个是学习率太大,权重在0附近来回跳动,没有稳定落在0上,调整学习率衰减策略,让后期学习率小到能让权重“锁死”。第四个是权重初始化范围过大,比如用标准差0.1初始化而数据本身很小,L1的惩罚相对于初始权重就不值一提,把初始化范围缩小会有帮助。
5.2 加了L1之后训练不稳定
训练损失曲线一开始还正常,训练到一半突然出现尖峰,或者验证集指标剧烈抖动,多半是L1惩罚的梯度震荡和模型当前的动力学发生了冲突。这时我通常会做两件事。第一,把L1惩罚系数从训练一开始就全局生效改成warmup后生效:前若干个epoch正常训练,让模型先找到一个大致的解,再把L1惩罚项加进来。这样做的好处是避免模型在冷启动阶段被正则强压到错误区域。第二,对L1的梯度做clip,限制每次更新的最大梯度范数,让别人跳变被限制住。
还有一个小技巧:把L1的损失单独记录,不混在总loss里观察。总loss下降不代表L1惩罚项已经被满足,分开记录正则损失和数据损失,能更清楚看到两者是否平衡。如果发现数据损失已经收敛但正则损失还在缓慢下降,说明此时提升稀疏率仍然有空间;如果正则损失已经很小但数据损失在上升,说明λ偏大,正在牺牲拟合能力。
5.3 L1和L2到底选谁
这个问题没有标准答案,但我有一个决策框架。如果目标偏重于防止过拟合、提高泛化能力且不关心参数是否稀疏,优先选L2。如果项目需要特征选择、压缩模型体积、提升推理效率,或者希望模型可解释性更强,选L1。如果条件允许,组合使用L1加L2(即弹性网)也是一种成熟方案,它能兼顾稀疏性和稳定性,尤其在特征高度相关的场景下效果比单独L1更稳。
我实际用过一次弹性网,λ1控制稀疏、λ2控制整体收缩,调节起来比单一L1多了一个维度,初始时会更耗时,但最终那个项目在压缩模型一半参数的同时,精度还略高于单独用L1或L2的方案。为了不让参数调节太复杂,可以把λ1和λ2的比例固定为1:1或2:1,先只调一个缩放系数,等整体趋势摸清后再分开微调。
6. 一个完整的迷你演示思路
说了这么多,最后给一个能快速上手验证L1效果的小实验设计:构造一个线性回归任务,生成1000个样本、50个特征,其中只有切片为7到10的四个特征真正跟目标值线性相关,其余特征都是纯噪声。分别训练无正则、L2正则、L1正则三个模型,对比它们学到的权重向量。
无正则模型会把噪声特征的权重学成随机的小数值,L2模型会让所有权重均匀变小但都不为零,L1模型则能精准地把绝大多数噪声特征的权重置零。这个实验十几分钟就能跑完,是我做过的最直观的稀疏性演示。框架用PyTorch或者TensorFlow的Keras接口都行,重点是看清训练后weights里非零项的分布。建议把随机种子固定住,方便复现;λ建议从0.001开始调,因为线性回归的数据比较简单,太大会把有效特征的权重也压没了。
7. 写在最后的一点体会
做了这么多次稀疏化实验之后,我现在对L1正则化的态度是:它是特征筛选和数据洞察的一把好刀,但它不会自动帮你把一切变得更好。稀疏性来得很容易,代价却是你需要花更多时间调试λ和学习率之间的配合。没有固定参数可以直接抄,每次换数据、换网络都要重新做一轮标定。个人建议是,在项目起步阶段就把L1纳入实验体系,因为它能透露的信息远超它在loss上增加的那点数字。权重清零的过程本身,就是模型在告诉你它对哪些输入从不关心。养成定期观察权重分布的习惯之后,你会逐渐把L1当成一个理解数据的工具,而不仅仅是压缩模型的技巧。