别再死记硬背L1、L2了!用Python和NumPy手把手带你理解范数在机器学习里的真实作用
别再死记硬背L1、L2了!用Python和NumPy手把手带你理解范数在机器学习里的真实作用
在机器学习的世界里,L1和L2范数就像两个性格迥异的老朋友——一个喜欢精简(L1),一个偏爱均衡(L2)。但很多初学者只是机械地记住"L1产生稀疏解,L2防止过拟合",却不知道背后的几何意义。今天,我们将用Python代码和可视化,带你从几何角度真正理解这些概念。
1. 范数究竟是什么?从数学定义到几何直观
范数(Norm)本质上是衡量向量"长度"或"大小"的方式。在数学上,Lp范数定义为:
||x||ₚ = (∑|xᵢ|ᵖ)^(1/p)
这个公式看起来抽象,但如果我们用Python画出不同p值下的"单位球"(所有范数等于1的点组成的图形),一切就会变得直观。
import numpy as np
import matplotlib.pyplot as plt
def plot_unit_ball(p_values):
theta = np.linspace(0, 2*np.pi, 100)
fig, ax = plt.subplots(figsize=(10, 10))
for p in p_values:
if p == 0:
# L0 "范数"(实际上不是真正的范数)
x = [0, 1, 1, 0, 0, -1, -1, 0, 0]
y = [0, 0, 1, 1, 0, 0, -1, -1, 0]
ax.plot(x, y, label=f'p={p}')
elif p == np.inf:
# L无穷范数
x = [1, 1, -1, -1, 1]
y = [1, -1, -1, 1, 1]
ax.plot(x, y, label=f'p=∞')
else:
# 普通Lp范数
r = 1/((np.abs(np.cos(theta))**p + np.abs(np.sin(theta))**p)**(1/p))
x = r * np.cos(theta)
y = r * np.sin(theta)
ax.plot(x, y, label=f'p={p}')
ax.set_aspect('equal')
plt.legend()
plt.title('Unit Balls for Different Lp Norms')
plt.show()
plot_unit_ball([0, 0.5, 1, 2, 5, np.inf])
运行这段代码,你会看到从p=0到p=∞的范数单位球变化:
- p=0 :虽然严格来说不是范数,但表示非零元素个数,图形是离散的点
- p=1 :菱形(曼哈顿距离)
- p=2 :圆形(欧几里得距离)
- p=∞ :正方形(取最大值)
注意:p=0时的"范数"实际上不满足范数的数学定义,但在机器学习中常被用作特征选择的指标。
2. 为什么L1产生稀疏解?几何视角的解释
稀疏解意味着许多系数为零,这在特征选择中非常有用。要理解为什么L1正则化会产生稀疏解,我们需要看优化问题的几何表现。
考虑一个简单的线性回归问题:
from sklearn.linear_model import Lasso, Ridge
# 生成数据
np.random.seed(42)
X = np.random.randn(100, 10)
y = X @ np.array([1.5, 0, 0, 2.0, 0, -1.0, 0, 0, 0.5, 0]) + 0.1 * np.random.randn(100)
# L1和L2回归
lasso = Lasso(alpha=0.1).fit(X, y)
ridge = Ridge(alpha=0.1).fit(X, y)
# 比较系数
print("Lasso coefficients:", lasso.coef_)
print("Ridge coefficients:", ridge.coef_)
典型输出可能显示:
- Lasso系数:[1.4, 0, 0, 1.9, 0, -0.9, 0, 0, 0.4, 0]
- Ridge系数:[1.3, 0.1, 0.1, 1.8, 0.1, -0.8, 0.1, 0.1, 0.4, 0.1]
几何解释 :
- 损失函数的等高线是椭圆
- L1约束区域是有尖角的菱形
- 最优解往往出现在尖角处(即某些维度为零)
def plot_optimization():
# 简化到二维情况便于可视化
X = np.random.randn(100, 2)
y = X @ np.array([1.5, -1.0]) + 0.1 * np.random.randn(100)
# 网格计算损失函数
w1 = np.linspace(-2, 2, 100)
w2 = np.linspace(-2, 2, 100)
W1, W2 = np.meshgrid(w1, w2)
loss = np.array([np.mean((y - X @ np.array([w1_, w2_]))**2)
for w1_, w2_ in zip(W1.ravel(), W2.ravel())]).reshape(W1.shape)
# 绘制
plt.figure(figsize=(12, 5))
# L1情况
plt.subplot(121)
CS = plt.contour(W1, W2, loss, levels=20)
plt.clabel(CS, inline=1, fontsize=10)
plt.plot(w1, 1 - np.abs(w1), 'r') # L1约束
plt.plot(w1, -1 + np.abs(w1), 'r')
plt.title('L1 Constraint')
# L2情况
plt.subplot(122)
CS = plt.contour(W1, W2, loss, levels=20)
plt.clabel(CS, inline=1, fontsize=10)
theta = np.linspace(0, 2*np.pi, 100)
plt.plot(np.cos(theta), np.sin(theta), 'r') # L2约束
plt.title('L2 Constraint')
plt.tight_layout()
plt.show()
plot_optimization()
从图中可以清晰看到:
- L1的尖角更容易与损失函数等高线相切在坐标轴上
- L2的圆形约束往往产生非零解
3. L2范数为什么防止过拟合?从数值稳定到权重衰减
L2正则化(Ridge回归)通过限制权重的大小来防止过拟合。这背后的数学原理是什么?
数值稳定性 : 当特征之间存在高度相关性时,普通最小二乘估计的方差会变得很大。L2正则化通过惩罚大权重来稳定解。
贝叶斯解释 : L2正则对应高斯先验,假设权重应该较小且分布均匀。
def compare_ols_ridge():
# 设计高度相关的特征
X = np.random.randn(100, 2)
X[:, 1] = 0.99 * X[:, 0] + 0.1 * np.random.randn(100)
y = X @ np.array([1.0, -1.0]) + 0.1 * np.random.randn(100)
# 普通最小二乘
w_ols = np.linalg.inv(X.T @ X) @ X.T @ y
# Ridge回归
alpha = 1.0
w_ridge = np.linalg.inv(X.T @ X + alpha * np.eye(2)) @ X.T @ y
print("OLS coefficients:", w_ols)
print("Ridge coefficients:", w_ridge)
compare_ols_ridge()
典型输出:
- OLS系数:[ 20.3, -20.1] (数值极大且不稳定)
- Ridge系数:[ 0.9, -0.8] (合理大小)
权重衰减效应 : L2正则化相当于在每次梯度下降更新时缩小权重:
def l2_penalty_gradient_descent(X, y, learning_rate=0.01, lambda_=0.1, epochs=1000):
n_samples, n_features = X.shape
w = np.zeros(n_features)
for _ in range(epochs):
gradient = (2/n_samples) * X.T @ (X @ w - y) + 2 * lambda_ * w
w -= learning_rate * gradient
return w
4. 实践指南:如何选择L1、L2或Elastic Net
在实际项目中,我们该如何选择正则化方法?下面是一个决策框架:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 特征选择 | L1 (Lasso) | 产生稀疏解,自动选择重要特征 |
| 高度相关特征 | L2 (Ridge) | 更稳定,避免给相关特征分配过大系数 |
| 大型数据集 | L2或None | 数据量大时过拟合风险低 |
| 中等规模数据 | Elastic Net | 结合L1和L2的优点 |
Elastic Net示例 :
from sklearn.linear_model import ElasticNet
# 生成有组结构的数据
X = np.random.randn(100, 20)
# 前5个特征相关且重要
y = X[:, :5] @ np.array([1, 1, 1, 1, 1]) + 0.1 * np.random.randn(100)
# Elastic Net结合L1和L2
en = ElasticNet(alpha=0.1, l1_ratio=0.5).fit(X, y)
print("ElasticNet coefficients:", en.coef_)
调参技巧 :
- 先尝试L2,如果模型仍然复杂再考虑L1
- 使用交叉验证选择正则化强度α
- 对于Elastic Net,l1_ratio=0.5通常是合理的起点
5. 超越L1和L2:其他范数的应用
虽然L1和L2最常用,但其他范数也有特殊用途:
-
L∞范数
(最大绝对值):
- 应用:对抗训练、鲁棒优化
- 特点:关注最显著特征
def linfty_norm(x):
return np.max(np.abs(x))
# 在GAN训练中限制判别器权重
class Discriminator:
def __init__(self):
self.weights = np.random.randn(100)
def clip_weights(self, c=0.01):
self.weights = np.clip(self.weights, -c, c)
-
组Lasso (Group Lasso):
- 对特征组进行选择而非单个特征
- 适用于有自然分组的特征(如one-hot编码的类别变量)
-
核范数 (矩阵的奇异值和,用于低秩近似):
- 应用:推荐系统、矩阵补全
- 实现:通过奇异值阈值(SVT)
def nuclear_norm(X):
return np.sum(np.linalg.svd(X, compute_uv=False))
# 矩阵补全示例
def soft_threshold(X, tau):
U, s, Vh = np.linalg.svd(X, full_matrices=False)
s_thresh = np.maximum(s - tau, 0)
return U @ np.diag(s_thresh) @ Vh
理解这些范数的几何意义和数学性质,能帮助我们在不同场景下做出更明智的选择。下次当你使用正则化时,不妨想想背后的几何图形——是菱形的尖角还是圆形的平滑更适合你的问题?
更多推荐



所有评论(0)