数学基础这个系列我断断续续写了快两年,每次写到范数这一节,评论区就会冒出一堆问题:为什么L1能做出稀疏解而L2不能?为什么Lasso的坐标下降要那么写?距离度量和正则化到底是怎么扯上关系的?说实话,我一开始学的时候也在这块卡了很久,教材上把“范数”和“正则化”分成两章讲,中间那层窗户纸没人帮你捅破,你就总觉得这是两个不相干的东西。这篇就把这层窗户纸彻底撕开,从曼哈顿距离和欧氏距离讲起,一路推到Lasso和Ridge的求解细节、几何直觉、参数选择,以及我在实际项目里踩过的坑。不管你是刚学完线性代数想找个落脚点,还是已经用过sklearn但没搞明白背后在算什么,这篇应该都能让你有收获。
1. 从距离度量到正则项:一条被忽视的逻辑线
1.1 范数到底是什么,为什么它同时出现在两个地方
很多人第一次见到“范数”是在距离公式里,第二次见到是在损失函数后面那个加项里,然后就开始困惑:这俩是同一个东西吗?答案是同一个东西,只是用法不同。范数本质上是一个函数,它把向量映射成一个非负实数,衡量的是这个向量“有多大”。L1范数就是把所有分量的绝对值加起来,L2范数就是把所有分量的平方加起来再开根号。就这么简单。
那为什么它既能当距离又能当惩罚项?关键在于理解“向量之差”这个概念。两个点之间的距离,本质上就是它们坐标向量之差的范数。你算曼哈顿距离,就是算差值向量的L1范数;你算欧氏距离,就是算差值向量的L2范数。而正则化做的事情,是把模型参数向量本身当作一个“点”,惩罚它离原点的距离。所以L1正则就是惩罚参数向量的L1范数,L2正则就是惩罚参数向量的L2范数。距离度量衡量的是“两个样本差多少”,正则化衡量的是“参数离零有多远”,数学形式完全一样,只是作用对象换了。
这个统一视角非常重要,因为后面所有的几何直觉都建立在这上面。你把参数向量想象成空间里的一个点,L1正则的等值线是菱形(二维情况下),L2正则的等值线是圆。损失函数的等值线和这些约束形状相切的位置,就决定了最优解落在哪里。L1的菱形有尖角,尖角正好落在坐标轴上,所以最优解容易出现在某些分量为零的位置,这就是稀疏性的几何来源。L2的圆没有尖角,切点一般不会正好落在坐标轴上,所以参数会被压缩但不会变成零。
提示:如果你只记一句话,就记这个——L1的菱形有角,L2的圆没角,角在坐标轴上,所以L1出稀疏解。这是理解Lasso和Ridge全部差异的钥匙。
1.2 为什么距离的名字叫曼哈顿和欧氏
这两个名字其实挺形象的。曼哈顿距离得名于曼哈顿的街道网格,你从一个路口到另一个路口,只能沿着横平竖直的街道走,不能穿墙,所以走的距离是横向差加纵向差。欧氏距离就是两点之间直线最短,拿尺子量出来的距离。放到高维空间里,这个直觉依然成立:L1范数假设你只能沿着坐标轴方向移动,L2范数允许你走对角线。
这个直觉对理解正则化的行为很有帮助。L1正则像是在说“你只能沿着坐标轴方向调整参数”,所以它倾向于把某些参数直接调到零,然后只在剩下的参数上做调整。L2正则像是在说“你可以沿任意方向调整,但整体别跑太远”,所以它会把所有参数都均匀地压小一点,但很少压到正好为零。我在给团队新人讲这块的时候,就用这个街道网格的比喻,基本没人听不懂。
1.3 从距离到回归:正则化到底在防什么
要理解Lasso和Ridge为什么存在,得先理解它们防的是什么问题。线性回归的最小二乘解是让残差平方和最小,这个解在数据量充足、特征之间不相关的时候表现很好。但现实往往不这么理想:特征之间高度相关(多重共线性),或者特征数比样本数还多(高维小样本),这时候最小二乘解会变得极不稳定,参数的方差会爆炸,模型在新数据上表现很差。这就是过拟合的一种表现形式。
正则化的思路是给损失函数加一个惩罚项,限制参数的大小,从而降低方差、换取一点偏差。这是一笔交易:你牺牲了在训练集上的拟合精度,换来了在测试集上的稳定性。L2正则(Ridge)把参数往零的方向均匀压缩,能有效缓解共线性带来的方差膨胀;L1正则(Lasso)则更进一步,它能把一些参数直接压到零,相当于做了一次自动的特征选择。两者的惩罚力度都由一个超参数控制,通常叫alpha或lambda,这个参数的选择直接决定了正则化的效果,后面会专门讲怎么调。
2. L1与L2的核心差异:几何、代数与概率三条线索
2.1 几何视角:菱形尖角与圆形切点
几何直觉是最直观的入口。把二维参数空间画出来,损失函数的等值线是一圈一圈的椭圆(以最小二乘为例),正则项的等值线是约束形状。优化问题可以理解为:在正则项约束的范围内,找到让损失函数最小的点。这个点就是损失等值线和约束形状的切点。
L1的约束形状是菱形,四个顶点在坐标轴上。当损失椭圆的中心不在坐标轴上时,椭圆往外扩张的过程中,最先碰到的往往是菱形的某个顶点,而顶点意味着其中一个参数为零。这就是L1产生稀疏解的几何解释。L2的约束形状是圆,圆上每个点的“光滑程度”一样,切点落在坐标轴上的概率很低,所以L2解通常所有分量都非零,只是整体被缩小了。
这个解释在二维下很清晰,高维下菱形变成菱形多面体,尖角更多,稀疏性更明显。我建议每个学这块的人都亲手画一次这个图,比看十遍公式都管用。
2.2 代数视角:软阈值与比例收缩
从求解的角度看,两者的差异更具体。对于正交设计矩阵这种简化情形,Lasso的解可以写成软阈值函数的形式:参数估计值等于最小二乘估计值减去一个阈值,如果结果小于零就取零。这个“减去阈值再截断”的操作,就是稀疏性的代数来源——小于阈值的系数直接被清零了。
Ridge的解则是把最小二乘估计值乘以一个小于1的缩放因子,所有系数按比例缩小,但不会变成零。这个差异用一句话概括:Lasso做的是“减法加截断”,Ridge做的是“乘法加缩放”。减法是硬性的,能把小系数直接干掉;乘法是柔性的,只能让系数变小。
这个代数差异也解释了为什么Lasso的求解比Ridge麻烦。Ridge有闭式解,一个矩阵求逆就完事了;Lasso没有闭式解(除非设计矩阵正交),必须用迭代算法,比如坐标下降、近端梯度法或者LARS。这也是为什么早期Ridge比Lasso流行得多,直到坐标下降的高效实现出来,Lasso才真正普及。
2.3 概率视角:高斯先验与拉普拉斯先验
从贝叶斯的角度看,正则化等价于给参数加先验分布。Ridge对应高斯先验,Lasso对应拉普拉斯先验。高斯分布在零附近是光滑的,所以先验把参数往零拉但不会强制为零;拉普拉斯分布在零处有一个尖峰,这个尖峰对应着“参数很可能正好是零”的先验信念,所以后验估计容易产生稀疏解。
这个视角的价值在于,它告诉你正则化不是随便加的,背后有概率模型的解释。你在选择L1还是L2的时候,其实是在选择你对参数分布的假设:你认为参数是稠密的小值(高斯),还是稀疏的、大部分为零(拉普拉斯)。这个假设应该由领域知识来支撑,而不是盲目试。
| 对比维度 | L1范数 / Lasso | L2范数 / Ridge |
|---|---|---|
| 距离名称 | 曼哈顿距离 | 欧氏距离 |
| 约束形状(二维) | 菱形,顶点在坐标轴 | 圆形,处处光滑 |
| 解的特征 | 稀疏,部分系数为零 | 稠密,系数整体缩小 |
| 求解方式 | 无闭式解,需迭代 | 有闭式解,矩阵求逆 |
| 贝叶斯先验 | 拉普拉斯分布 | 高斯分布 |
| 主要用途 | 特征选择、高维稀疏 | 缓解共线性、稳定估计 |
| 对异常值 | 较稳健 | 较敏感 |
3. 实操:用坐标下降手写一个Lasso求解器
3.1 为什么选坐标下降而不是梯度下降
Lasso的目标函数是凸的但不可微(在零点处L1项没有导数),标准梯度下降直接用会有问题。坐标下降的思路是每次只优化一个坐标,固定其他坐标不变,把多维问题拆成一维问题。一维的Lasso子问题有解析解,就是软阈值,所以每一轮更新都很便宜。这个算法收敛性有保证,实现也简单,是工业界Lasso求解的主流方法之一。
我选择手写而不是直接调库,是因为只有自己写一遍,你才会真正理解软阈值是怎么起作用的,也才能在调参出问题时知道该往哪看。下面这段代码是我简化过的版本,去掉了加速和稀疏矩阵优化,保留核心逻辑,方便对照理解。
import numpy as np def soft_threshold(z, gamma): if z > gamma: return z - gamma elif z < -gamma: return z + gamma else: return 0.0 def lasso_coordinate_descent(X, y, alpha, max_iter=1000, tol=1e-6): n_samples, n_features = X.shape w = np.zeros(n_features) residual = y - X @ w col_sq_norms = np.sum(X ** 2, axis=0) for iteration in range(max_iter): w_old = w.copy() for j in range(n_features): # 去掉第j个特征当前的贡献,得到偏残差 residual += X[:, j] * w[j] rho = X[:, j] @ residual z = rho / col_sq_norms[j] w[j] = soft_threshold(z, alpha / col_sq_norms[j]) # 把更新后的贡献加回残差 residual -= X[:, j] * w[j] if np.max(np.abs(w - w_old)) < tol: break return w3.2 逐行拆解:偏残差与软阈值
这段代码的核心在循环内部那几行。residual维护的是当前参数下的残差向量。对第j个特征做更新时,先把该特征当前的贡献从残差里去掉,得到“偏残差”——也就是假设第j个特征不存在时,模型还差多少。然后用这个偏残差和第j列做内积,得到rho,它衡量的是第j个特征和当前残差的相关性。除以该列的平方范数做归一化,得到z,这就是不考虑正则时第j个系数的最优值。
接下来就是软阈值登场:alpha / col_sq_norms[j]是阈值,如果z的绝对值小于这个阈值,系数直接置零;否则向零的方向收缩相应的量。这个操作就是L1正则产生稀疏性的直接原因。更新完系数后,把新的贡献加回残差,进入下一个特征。
注意:
col_sq_norms必须提前算好,不能在循环里重复算,否则复杂度会翻倍。另外,如果某一列的平方范数为零(常数零列),要跳过或者做特殊处理,否则会除零。
3.3 一个可复现的验证实验
光看代码不够,得跑个实验验证行为。我构造一个简单的场景:样本数50,特征数20,其中只有5个特征真正有贡献,其余是噪声。用上面的求解器跑一遍,看系数分布。
np.random.seed(42) n, p = 50, 20 X = np.random.randn(n, p) true_w = np.zeros(p) true_w[:5] = [3.0, -2.0, 1.5, -1.0, 0.8] y = X @ true_w + 0.1 * np.random.randn(n) w_lasso = lasso_coordinate_descent(X, y, alpha=0.1) print("非零系数个数:", np.sum(np.abs(w_lasso) > 1e-8)) print("系数:", np.round(w_lasso, 3))跑下来你会发现,非零系数个数明显少于20,而且集中在真正有贡献的那几个特征上。把alpha调大,非零个数会进一步减少;调小则逐渐逼近最小二乘解。这个实验我建议每个人都跑一遍,改改alpha看看系数怎么变,比看任何理论解释都直观。
3.4 Ridge的闭式解与对比实验
Ridge就简单多了,直接解一个线性方程组。目标函数对参数求导令其为零,得到w = (X^T X + alpha I)^{-1} X^T y。注意这里通常不对截距做正则,所以实际实现时要么先中心化数据,要么在矩阵里把截距对应的对角元设为零。
def ridge_closed_form(X, y, alpha): n_features = X.shape[1] A = X.T @ X + alpha * np.eye(n_features) return np.linalg.solve(A, X.T @ y)拿同样的数据跑Ridge,你会看到所有系数都非零,只是整体比最小二乘解小了一圈。把两个结果并排打印出来,Lasso的稀疏和Ridge的稠密对比一目了然。这个对比实验是我每次讲这块必做的,效果比任何PPT都好。
4. 参数选择与工程实践中的坑
4.1 alpha怎么调:交叉验证的正确姿势
alpha是控制正则化强度的关键超参数,调大了欠拟合,调小了过拟合。标准做法是K折交叉验证,在一组候选alpha上分别训练和验证,选验证误差最小的那个。sklearn里的LassoCV和RidgeCV就是干这个的,内部用高效的路径算法一次性算出整条正则化路径上的解,比暴力网格搜索快得多。
但这里有个坑:交叉验证的折数和alpha网格的粒度要匹配。折数太少(比如3折),验证误差的估计方差大,选出来的alpha不稳定;网格太粗,可能错过最优点。我的经验是5折起步,alpha用对数刻度,从1e-4到1e2取50到100个点。另外,如果特征量纲差异大,一定要先标准化,否则惩罚项对不同特征的力度不一样,结果没有意义。
提示:标准化要在交叉验证的每一折内部做,用训练折的均值和方差去变换验证折,不能在全量数据上先标准化再切分,否则会有信息泄露。sklearn的Pipeline可以帮你自动处理这个顺序。
4.2 多重共线性下Lasso的“任性”行为
这是我在实际项目里踩过的最大的坑。当一组特征高度相关时,Lasso倾向于从这组里随便挑一个保留,把其他的压到零,而且挑哪个对数据的微小扰动很敏感。你换个随机种子重新采样,可能挑中的特征就变了。这导致模型的可解释性打折扣——你以为Lasso帮你选出了“重要特征”,其实它只是从一组等价特征里随机挑了一个代表。
Ridge在这种情况下表现更稳定,它会把相关特征的系数分摊,虽然不能做选择,但估计的方差小。所以如果你的目标是预测精度而非特征选择,或者特征之间相关性很强,Ridge往往是更稳妥的选择。如果确实需要稀疏性,可以考虑Elastic Net,它是L1和L2的结合,既能选择又能处理相关性,代价是多一个超参数要调。
4.3 高维小样本场景的注意事项
特征数远大于样本数时(比如基因数据、文本数据),Lasso最多只能选出与样本数相当的个非零特征,这是L1正则的一个理论限制。这时候如果真实的重要特征很多,Lasso会漏掉一部分。应对办法包括:先用其他方法降维再上Lasso,或者用Elastic Net,或者用稳定性选择(多次子采样取交集)来提高选择的可靠性。
另外,高维场景下坐标下降的收敛可能变慢,需要适当增加最大迭代次数。我遇到过迭代没收敛但代码不报错的情况,结果系数还是半成品,排查了半天才发现是max_iter设小了。建议在求解后检查一下是否真的收敛,或者把max_iter设得宽松一些。
| 常见问题 | 现象 | 排查与解决 |
|---|---|---|
| 系数全为零 | alpha过大 | 减小alpha,检查特征是否已标准化 |
| 结果不稳定 | 特征高度相关 | 改用Ridge或Elastic Net,或做稳定性选择 |
| 迭代不收敛 | 高维或共线性强 | 增大max_iter,检查数据标准化 |
| 验证误差异常 | 标准化信息泄露 | 用Pipeline在折内标准化 |
| 选出的特征不合理 | Lasso随机挑选相关特征 | 结合领域知识,用稳定性选择交叉验证 |
4.4 一个容易被忽略的细节:截距的处理
正则化通常只惩罚斜率系数,不惩罚截距。原因很直白:截距反映的是目标变量的整体水平,惩罚它相当于强行把预测往零拉,没有道理。实现时如果直接对所有参数加惩罚,截距会被不必要地压缩,导致模型有偏。sklearn默认fit_intercept=True并且不对截距正则,但如果你自己手写求解器,记得先对数据做中心化处理,或者把截距对应的惩罚系数设为零。这个小细节在教科书里经常一笔带过,但实际写代码时忘了它,结果会差不少。
5. 从范数到模型选择:我的经验判断
5.1 什么时候用Lasso,什么时候用Ridge
这个问题没有标准答案,但有一些实用的判断规则。特征数量大且你相信只有少数特征真正重要时,用Lasso,它的稀疏性帮你做特征选择,模型也更容易解释。特征之间相关性高、或者你更看重预测稳定性时,用Ridge。两者都拿不准时,Elastic Net是安全的折中,代价是多调一个参数。
还有一个维度是样本量。样本量充足时,两者的差异没那么大;样本量小、特征多时,正则化的选择对结果影响显著,这时候交叉验证的可靠性也更值得关注。我个人的习惯是先把Ridge跑一遍作为基线,再试Lasso看稀疏化能带来多少解释性收益,如果收益不明显就留在Ridge。
5.2 范数思维的其他用武之地
L1和L2的这套思维不只用在回归上。L2正则几乎出现在所有神经网络的训练里(weight decay),作用同样是控制模型复杂度。L1正则用在需要稀疏性的场景,比如压缩感知、稀疏编码。甚至距离度量本身也有广泛应用:L1距离在图像匹配、推荐系统的相似度计算里都有用,因为它对异常值不那么敏感;L2距离则是最常用的相似度度量。
理解范数的统一视角,能让你在遇到新问题时快速判断该用哪种。核心判断标准就一条:你需要的是“挑出少数重要的”(L1),还是“整体压小保持稳定”(L2)。这个判断在回归、分类、降维、神经网络里都通用。
5.3 最后分享一个调试小技巧
如果你怀疑正则化没起作用或者起反作用,做一个简单的诊断:把alpha从很小扫到很大,画出训练误差和验证误差随alpha变化的曲线。正常的曲线应该是训练误差单调上升、验证误差先降后升的U形。如果验证误差一直上升,说明你的基线模型已经欠拟合,问题不在正则化;如果训练误差几乎不变,说明alpha的影响被其他因素掩盖了,检查一下特征尺度或者求解器是否收敛。这个曲线我每次调参都会画,五分钟的事,能省掉大量瞎猜的时间。
这套从距离到正则化的逻辑线,我从第一次接触到现在用了好几年,每次回头看都有新的体会。最开始只是记住公式,后来理解了几何,再后来在项目里被共线性坑过、被标准化顺序坑过、被迭代不收敛坑过,才真正把这些知识串起来。数学基础这东西,光看是看不明白的,得动手跑、动手改、动手踩坑,才能变成自己的东西。