哈喽,大家好~
今儿和大家系统地聊聊 L1 正则(Lasso)与 L2 正则(Ridge)的核心区别、各自侧重点与典型使用场景。
首先,回顾一下 无正则的最小二乘(OLS)。
给定数据集 、目标 ,标准线性模型假设为
OLS 的优化目标是最小化平方损失:
当 满秩且 ,通常的解析解为
但当存在多重共线性(病态或不可逆)、高维()时,OLS 不稳定或不可求,需要正则化。
L2 正则(Ridge):二次惩罚、闭式解与稳定性:
Ridge 在线性回归的优化目标为
其中 $ |w|2^2 = \sum{j=1}^p w_j^2$。该目标仍是严格凸函数,有唯一解。令梯度为零可得闭式解:
性质:
稳定性:对病态矩阵加上 的“抬升”,改善条件数,降低方差。 均匀收缩:在正交设计下(),可得
即所有系数按同一比例缩小,几乎不产生稀疏(除非 )。
对相关特征:权重更分散,减少过拟合风险。
L1 正则(Lasso):稀疏惩罚、软阈值与特征选择:
Lasso 的优化目标是
其中 $ |w|1 = \sum{j=1}^p |w_j|$。
由于 在 处不可导,需用次梯度或近端方法。
在正交设计()的特例下,可以得到一维分解为 个独立的子问题。记 ,则 Lasso 的解具有经典软阈值(soft-thresholding)形式:
推导思路(正交情形):
目标可分解为对每个 的
对 ,一阶条件给出 ,解得
结合 的符号与零点条件(若 ,则最优为 ),得到软阈值。
这解释了稀疏:当信号强度不够(不超过阈值 ),直接被置零。
几何视角与稀疏性的原因:
Ridge 的约束集合是 球:
Lasso 的约束集合是 多面体:
当损失函数的等高线(椭圆)与约束集合相交,L1 的多面体“顶点”(坐标轴对齐)更容易成为最优点,导致许多系数恰为 0;而 L2 的圆滑边界则不容易把系数推到正好为 0。
偏差-方差权衡与泛化:
L2 增加偏差但显著降低方差,尤其在多重共线性时泛化更好。
L1 增加偏差并强烈降低方差,同时还减少模型维度(特征数),在高维稀疏真值下泛化最佳。
实践中常通过交叉验证选择 (或 ),寻找最佳偏差-方差折中点。
贝叶斯视角(先验解释)
Ridge 等价于在高斯噪声下添加高斯先验 ,MAP 解对应 Ridge 解。形式上:
Lasso 对应拉普拉斯(双指数)先验:
拉普拉斯分布有尖峰重尾,鼓励稀疏。
与相关特征成组性、非唯一性与可解释性:
强相关特征块:
Ridge 倾向于把权重在相关特征中“平均分配”,给出更稳定的估计。 Lasso 倾向于择一入模(或小部分),提高可解释性但可能不稳定。
非唯一性:
Ridge 解唯一。 Lasso 在 或强相关时可能存在多组最优解(不同特征集合但拟合效果相近),需要再加约束或用 Elastic Net。
可解释性:
Lasso 由于稀疏性,便于解释“选择了哪些特征”。 Ridge 更适合“所有特征都有贡献”的场景。
优化算法差异(可微 vs 次梯度)
Ridge 可微,有闭式解、梯度下降都非常稳定。
Lasso 不可微,常用方法:
坐标下降(coordinate descent):逐坐标交替软阈值更新。
近端梯度(ISTA/FISTA):使用软阈值近端算子
ADMM 等。
完整案例
咱们通过一个虚拟回归数据集,展示 L1 vs L2 的差异,包括稀疏选择、正则路径、交叉验证性能曲线、以及在强相关特征下的稳定性。
数据:
样本数 n=600,特征数 p=20。
存在两个相关块:
block1(特征 0-4):强相关(约 0.95)。 block2(特征 5-9):中等相关(约 0.5)。
其他特征独立。
真值稀疏:仅少数特征为非零,例如 0、2、5、11。
高斯噪声;训练-测试拆分;标准化(非常重要:L1/L2 对量纲敏感)。
代码中,使用 StandardScaler 保证不同特征量纲一致。
CV使用 RepeatedKFold,路径使用 logspace 的 alpha 网格。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import Lasso, Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split, RepeatedKFold
import warnings
warnings.filterwarnings("ignore")
# 1) 构造虚拟数据集
def make_synthetic_data(n=1600, p=20, seed=42):
rng = np.random.RandomState(seed)
X = np.zeros((n, p))
# Block1: 强相关,特征 0-4
z1 = rng.normal(size=n)
for j in range(5):
X[:, j] = z1 + 0.05 * rng.normal(size=n) # 高相关: 0.95 左右
# Block2: 中度相关,特征 5-9
z2 = rng.normal(size=n)
for j in range(5, 10):
X[:, j] = z2 + 0.5 * rng.normal(size=n) # 相关: 0.5 左右
# 其他特征独立
for j in range(10, p):
X[:, j] = rng.normal(size=n)
# 真值系数(稀疏)
w_true = np.zeros(p)
w_true[0] = 3.0 # 来自强相关块
w_true[2] = -2.5 # 来自强相关块
w_true[5] = 2.0 # 来自中等相关块
w_true[11] = -3.0 # 独立特征之一
# 可加入少量微弱信号以更真实
w_true[15] = 0.5
noise = rng.normal(scale=2.0, size=n) # 噪声
y = X @ w_true + noise
return X, y, w_true
X, y, w_true = make_synthetic_data()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.35, random_state=123)
# 工具函数:从 pipeline 中还原到原始尺度的系数与截距
def recover_original_coef(pipeline, X_train):
scaler = pipeline.named_steps['standardscaler']
model = pipeline.named_steps['lasso'] if 'lasso' in pipeline.named_steps else pipeline.named_steps['ridge']
coef_std = model.coef_
# 原始尺度系数
coef_orig = coef_std / scaler.scale_
# 原始尺度截距
intercept_orig = model.intercept_ - np.sum(coef_std * scaler.mean_ / scaler.scale_)
return coef_orig, intercept_orig
# 2) 构造 alpha 网格 + 交叉验证
alphas = np.logspace(-3, 1, 40) # 1e-3 到 10
cv = RepeatedKFold(n_splits=5, n_repeats=2, random_state=42)
def cv_curve(model_type='lasso'):
mses = []
for alpha in alphas:
fold_mse = []
for tr_idx, val_idx in cv.split(X_train):
X_tr, X_val = X_train[tr_idx], X_train[val_idx]
y_tr, y_val = y_train[tr_idx], y_train[val_idx]
if model_type == 'lasso':
pipe = make_pipeline(StandardScaler(), Lasso(alpha=alpha, max_iter=10000, random_state=42))
else:
pipe = make_pipeline(StandardScaler(), Ridge(alpha=alpha, random_state=42))
pipe.fit(X_tr, y_tr)
y_pred = pipe.predict(X_val)
fold_mse.append(mean_squared_error(y_val, y_pred))
mses.append(np.mean(fold_mse))
return np.array(mses)
mse_lasso = cv_curve('lasso')
mse_ridge = cv_curve('ridge')
# 选择最佳 alpha
best_alpha_lasso = alphas[np.argmin(mse_lasso)]
best_alpha_ridge = alphas[np.argmin(mse_ridge)]
# 3) 拟合最终模型
pipe_lasso = make_pipeline(StandardScaler(), Lasso(alpha=best_alpha_lasso, max_iter=10000, random_state=42))
pipe_ridge = make_pipeline(StandardScaler(), Ridge(alpha=best_alpha_ridge, random_state=42))
pipe_lasso.fit(X_train, y_train)
pipe_ridge.fit(X_train, y_train)
coef_lasso, intercept_lasso = recover_original_coef(pipe_lasso, X_train)
coef_ridge, intercept_ridge = recover_original_coef(pipe_ridge, X_train)
y_pred_lasso = pipe_lasso.predict(X_test)
y_pred_ridge = pipe_ridge.predict(X_test)
test_mse_lasso = mean_squared_error(y_test, y_pred_lasso)
test_mse_ridge = mean_squared_error(y_test, y_pred_ridge)
# 4) 正则化路径:追踪关键特征 (0, 2, 5, 11) 在不同 alpha 下的系数
key_feats = [0, 2, 5, 11]
path_lasso = {j: [] for j in key_feats}
path_ridge = {j: [] for j in key_feats}
for alpha in alphas:
pipe_l = make_pipeline(StandardScaler(), Lasso(alpha=alpha, max_iter=10000, random_state=42))
pipe_r = make_pipeline(StandardScaler(), Ridge(alpha=alpha, random_state=42))
pipe_l.fit(X_train, y_train)
pipe_r.fit(X_train, y_train)
coef_l, _ = recover_original_coef(pipe_l, X_train)
coef_r, _ = recover_original_coef(pipe_r, X_train)
for j in key_feats:
path_lasso[j].append(coef_l[j])
path_ridge[j].append(coef_r[j])
# 5) Bootstrap 稳定性分析:对相关特征 (0, 2) 做 100 次抽样,观察系数分布
rng = np.random.RandomState(123)
B = 100
coef_boot_lasso = {j: [] for j in [0, 2]}
coef_boot_ridge = {j: [] for j in [0, 2]}
for b in range(B):
# 有放回抽样
idx = rng.choice(np.arange(X_train.shape[0]), size=X_train.shape[0], replace=True)
X_b, y_b = X_train[idx], y_train[idx]
pipe_l = make_pipeline(StandardScaler(), Lasso(alpha=best_alpha_lasso, max_iter=10000, random_state=42))
pipe_r = make_pipeline(StandardScaler(), Ridge(alpha=best_alpha_ridge, random_state=42))
pipe_l.fit(X_b, y_b)
pipe_r.fit(X_b, y_b)
coef_l, _ = recover_original_coef(pipe_l, X_train)
coef_r, _ = recover_original_coef(pipe_r, X_train)
for j in [0, 2]:
coef_boot_lasso[j].append(coef_l[j])
coef_boot_ridge[j].append(coef_r[j])
# 6) 可视化
plt.figure(figsize=(16, 12))
# 图1:系数对比条形图
ax1 = plt.subplot(2, 2, 1)
width = 0.25
indices = np.arange(len(w_true))
ax1.bar(indices - width, w_true, width, color='', label='True', alpha=0.8)
ax1.bar(indices, coef_lasso, width, color='', label='Lasso', alpha=0.8)
ax1.bar(indices + width, coef_ridge, width, color='', label='Ridge', alpha=0.8)
ax1.set_title('图1:系数对比(True vs Lasso vs Ridge)', fontsize=14)
ax1.set_xlabel('特征索引')
ax1.set_ylabel('系数值(原始尺度)')
ax1.legend(loc='upper right')
ax1.grid(True, alpha=0.3)
# 图2:正则化路径(关键特征)
ax2 = plt.subplot(2, 2, 2)
colors = {0: '', 2: '', 5: '', 11: ''}
for j in key_feats:
ax2.plot(alphas, np.abs(path_lasso[j]), color=colors[j], linestyle='-', linewidth=2, label=f'Lasso |w_{j}|')
ax2.plot(alphas, np.abs(path_ridge[j]), color=colors[j], linestyle='--', linewidth=2, label=f'Ridge |w_{j}|')
ax2.set_xscale('log')
ax2.set_title('图2:正则化路径(关键特征系数的绝对值)', fontsize=14)
ax2.set_xlabel('alpha(对数尺度)')
ax2.set_ylabel('|系数|(原始尺度)')
ax2.legend(loc='upper right', ncol=2)
ax2.grid(True, alpha=0.3)
# 图3:交叉验证 MSE vs alpha
ax3 = plt.subplot(2, 2, 3)
ax3.plot(alphas, mse_lasso, color='', marker='o', label=f'Lasso CV MSE (best={best_alpha_lasso:.4f})')
ax3.plot(alphas, mse_ridge, color='', marker='s', label=f'Ridge CV MSE (best={best_alpha_ridge:.4f})')
ax3.set_xscale('log')
ax3.set_title('图3:交叉验证曲线 MSE vs alpha', fontsize=14)
ax3.set_xlabel('alpha(对数尺度)')
ax3.set_ylabel('CV 平均 MSE')
ax3.legend(loc='upper right')
ax3.grid(True, alpha=0.3)
# 在图上标注测试集 MSE
ax3.text(0.0012, max(mse_lasso)*0.9, f'Test MSE Lasso={test_mse_lasso:.3f}', color='', fontsize=12)
ax3.text(0.0012, max(mse_lasso)*0.85, f'Test MSE Ridge={test_mse_ridge:.3f}', color='', fontsize=12)
# 图4:Bootstrap 稳定性箱线图(相关特征 0 和 2)
ax4 = plt.subplot(2, 2, 4)
data_l0 = coef_boot_lasso[0]
data_l2 = coef_boot_lasso[2]
data_r0 = coef_boot_ridge[0]
data_r2 = coef_boot_ridge[2]
box_data = [data_l0, data_l2, data_r0, data_r2]
labels = ['Lasso w0', 'Lasso w2', 'Ridge w0', 'Ridge w2']
box = ax4.boxplot(box_data, patch_artist=True, labels=labels)
colors_box = ['', '', '', '']
for patch, color in zip(box['boxes'], colors_box):
patch.set_facecolor(color)
patch.set_alpha(0.7)
for whisker in box['whiskers']:
whisker.set_color('')
for cap in box['caps']:
cap.set_color('')
for median in box['medians']:
median.set_color('')
ax4.set_title('图4:Bootstrap 稳定性(强相关特征系数分布)', fontsize=14)
ax4.set_ylabel('系数值(原始尺度)')
ax4.grid(True, alpha=0.3)
ax4.text(1.2, np.mean(data_l0), 'Lasso 稀疏/择一', color='', fontsize=12)
ax4.text(3.2, np.mean(data_r0), 'Ridge 稳定/均匀', color='', fontsize=12)
plt.tight_layout()
plt.show()

图1:系数对比
可以看到,Lasso 明显将若干无关特征系数压到 0,体现稀疏与特征选择。
在相关块(特征 0-4、5-9),Lasso 往往只保留其中的一两个(例如 0、2、5),而 Ridge 则将权重分布到多个相关特征上,显示“共享”效果。
Ridge 的系数普遍更小,体现均匀收缩;Lasso 的非零系数则靠近真值(当 alpha 合理)但会因稀疏而可能略偏。
我们通过直接对比两种正则化的估计特征与幅度,便于理解“稀疏 vs 均匀收缩”的核心区别。
图2:正则化路径
在不同 alpha 下,绘制关键特征(0、2、5、11)的系数绝对值曲线,实线为 Lasso,虚线为 Ridge。
可以看到,Lasso 的曲线存在“阈值”行为:某些特征在 alpha 增大后迅速跌落到 0(软阈值),出现明显断崖。
Ridge 的曲线更为平滑,随着 alpha 增加逐渐收缩,但不轻易变成 0。
对强相关特征(如 0、2),两者的动态非常不同:Lasso 可能只保留一个,另一个很快为零;Ridge 则均匀缓慢收缩。
可视化“软阈值 vs 平滑收缩”,理解为何 Lasso 更适用于特征选择而 Ridge 更适用于稳定估计。
图3:交叉验证曲线
两条曲线对比 Lasso 与 Ridge 在不同 alpha 下的 CV 平均 MSE,标注各自的最优 alpha,同时在图内给出测试集 MSE。
可以看到,不同数据结构下最佳 alpha 可能不同;在存在强相关块且真值稀疏时,常见情况是 Lasso 与 Ridge 的最优点不相同。
若噪声较大且相关结构强,Ridge 的曲线可能更平稳;Lasso 的曲线在较小 alpha 时波动可能更大。
可以帮助理解偏差-方差权衡,利用 CV 合理地自动选择正则强度;也说明 Lasso 的稀疏优势并不总是带来更低的 MSE(尤其在真值非稀疏或相关特征较多的场景)。
图4:Bootstrap 稳定性箱线图
对强相关的两特征(如 0、2),分别对 Lasso 与 Ridge 的系数进行 100 次 bootstrap 拟合后绘制分布。
可以看到,Lasso 的分布可能更分散甚至呈现“分裂”(某次选择特征 0,某次选择特征 2,导致两者系数分布较宽),体现选择不稳定性。
Ridge 的分布相对集中稳定,且两特征会形成“共同承担”的权重分配。
说明在相关特征集下,Lasso 的变量选择存在不稳定风险;Ridge 更适用于稳定预测与在高相关情况下保持鲁棒。
结合实验的进一步讨论
选择标准化:在 L1/L2 中,特征量纲差异会造成不公平惩罚;必须进行标准化。 选择 alpha:通过图3可见,CV 是关键步骤。某些数据下 Lasso 在最佳点的测试 MSE 未必优于 Ridge;这取决于真值稀疏程度与相关结构。 稀疏与解释性:图1与图2演示了 Lasso 在特征选择上的优势,能够得到更简洁的解释。但图4提醒:在强相关特征下,选择可能不稳定,Elastic Net 是常用折衷方案。 强相关块:图1与图4联合说明,与其强行做稀疏,不如在高度相关的场景中使用 Ridge 或 Elastic Net;或者做特征工程(聚合/降维)再用 Lasso。
大家通过以上理论与实验,基本可以形成系统的认知框架,帮助在具体项目中根据数据结构与任务目标选择合适的正则化策略,达到更加有效的效果。
最后

