简介:这份PDF资料面向统计学与机器学习入门者,系统讲解岭回归与Lasso回归两类常用正则化模型及其Python实现。内容从线性回归的多重共线性短板切入,说明岭回归通过L2惩罚项、Lasso通过L1惩罚项提升模型泛化能力,并给出sklearn中Ridge与Lasso的建模代码。资料还延伸至交叉验证调参、F检验与T检验的假设检验流程,配合糖尿病数据集案例演示RidgeCV、LassoCV筛选最优lambda值及RMSE效果验证,帮助读者掌握从建模到评估的完整链路。资源包为1个PDF文件,约907KB,结构紧凑便于随时查阅。目前已有218人学习下载,适合希望补齐回归正则化知识、动手复现代码并理解模型诊断思路的读者参考。
1. 岭回归与 Lasso:从共线性翻车到正则化救场
做特征工程时,最让人头疼的不是数据量不够,而是特征之间高度相关。比如房价预测里,「房间数」和「居住面积」几乎同步变化,普通最小二乘回归(OLS)的系数估计会剧烈震荡——今天跑出来面积系数是 3.2,明天加一条样本就变成 -1.8,符号都反了。这不是代码写错了,是多重共线性让设计矩阵接近奇异,OLS 的解不稳定。
岭回归(Ridge)和 Lasso 就是来解决这个问题的。它们都在损失函数里加了正则化项:岭回归加 L2 范数,把系数往零压但不归零;Lasso 加 L1 范数,能把不重要的特征系数直接压成零,顺带做特征选择。Python 里用 scikit-learn 几行就能跑通,但参数怎么设、标准化要不要做、什么时候选岭回归什么时候选 Lasso,这些才是真正决定结果好坏的地方。这篇笔记面向已经会用 Python 做回归、但被共线性或过拟合卡住的从业者,从原理到代码到踩坑,一步步把这两个模型落地。
2. 正则化到底在惩罚什么:岭回归与 Lasso 的数学直觉
2.1 从 OLS 的不稳定说起
普通最小二乘的损失函数是残差平方和:
Loss_OLS = Σ(y_i - ŷ_i)²当特征矩阵 X 存在共线性时,XᵀX 接近奇异,求逆会放大微小扰动。表现就是系数方差极大,换一批训练数据系数就大变。岭回归在损失里加了一项:
Loss_Ridge = Σ(y_i - ŷ_i)² + α·Σβ_j²α 是惩罚强度,β 是回归系数。加了这个 L2 惩罚后,XᵀX 变成 XᵀX + αI,对角线加了 α,矩阵一定可逆,数值稳定性直接解决。Lasso 换了个惩罚项:
Loss_Lasso = Σ(y_i - ŷ_i)² + α·Σ|β_j|L1 惩罚的几何性质导致最优解出现在坐标轴上,也就是某些 β_j 恰好为零。这就是 Lasso 能做特征选择的原因。
2.2 岭回归和 Lasso 的选型判断
不是所有场景都无脑上 Lasso。我一般按这个逻辑判断:
| 场景 | 推荐 | 理由 |
|---|---|---|
| 特征少且都有业务含义,共线性中等 | 岭回归 | 保留所有特征,只压缩系数 |
| 特征上百个,怀疑大量冗余 | Lasso | 自动稀疏化,输出可解释 |
| 特征高度相关且想保留组效应 | 岭回归 | Lasso 会在相关特征中随机选一个 |
| 需要同时做选择和压缩 | ElasticNet | L1+L2 混合,但调参更复杂 |
提示:如果特征组内相关性很高(比如 one-hot 编码产生的哑变量),Lasso 倾向于只保留其中一个,模型解释会变得不稳定。这时候岭回归更稳。
2.3 标准化是前置条件,不是可选项
正则化项对所有系数一视同仁地惩罚,但如果特征量纲不同,惩罚就不公平。比如「面积」单位是平方米,数值在 50-200;「房间数」数值在 1-5。同样大小的系数,对面积的影响远大于房间数。不做标准化,正则化会优先压缩小量纲特征的系数,结果完全失真。
from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge, Lasso from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error import numpy as np # 假设 X 是特征矩阵,y 是目标变量 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 标准化:fit 只在训练集上做,避免数据泄漏 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 岭回归,alpha 先给一个默认值 ridge = Ridge(alpha=1.0) ridge.fit(X_train_scaled, y_train) y_pred_ridge = ridge.predict(X_test_scaled) print("Ridge MSE:", mean_squared_error(y_test, y_pred_ridge)) # Lasso,alpha 同样先给默认值 lasso = Lasso(alpha=0.1, max_iter=10000) lasso.fit(X_train_scaled, y_train) y_pred_lasso = lasso.predict(X_test_scaled) print("Lasso MSE:", mean_squared_error(y_test, y_pred_lasso)) print("Lasso 非零系数个数:", np.sum(lasso.coef_ != 0))这段代码的关键点:fit_transform只在训练集调用,测试集用transform,否则标准化参数里混入了测试集信息,评估结果会偏乐观。Lasso 的max_iter默认是 1000,特征多的时候经常不收敛,调到 10000 是常规操作。alpha这里只是占位,实际要用交叉验证选。
3. 用交叉验证把 alpha 调出来:RidgeCV 与 LassoCV 实操
3.1 为什么不能手动试 alpha
alpha 控制惩罚强度,太小等于没正则化,太大所有系数被压到接近零,模型欠拟合。手动试几个值靠运气,正确做法是交叉验证。scikit-learn 提供了RidgeCV和LassoCV,内部用 K 折交叉验证遍历 alpha 候选值,选均方误差最小的那个。
from sklearn.linear_model import RidgeCV, LassoCV # 岭回归交叉验证:alpha 候选值用对数刻度 alphas = np.logspace(-3, 3, 100) ridge_cv = RidgeCV(alphas=alphas, cv=5, scoring='neg_mean_squared_error') ridge_cv.fit(X_train_scaled, y_train) print("Ridge 最优 alpha:", ridge_cv.alpha_) print("Ridge 最优 CV MSE:", -ridge_cv.best_score_) # Lasso 交叉验证 lasso_cv = LassoCV( alphas=None, # None 表示自动生成候选值 cv=5, max_iter=10000, random_state=42, n_jobs=-1 ) lasso_cv.fit(X_train_scaled, y_train) print("Lasso 最优 alpha:", lasso_cv.alpha_) print("Lasso 非零系数个数:", np.sum(lasso_cv.coef_ != 0))RidgeCV的alphas参数如果不指定,默认只试[0.1, 1.0, 10.0]三个值,粒度太粗。用np.logspace(-3, 3, 100)生成 100 个对数均匀分布的值,覆盖从 0.001 到 1000 的范围,基本能命中最优区间。LassoCV的alphas=None会让它自动根据数据生成候选序列,通常够用。n_jobs=-1用满所有 CPU 核并行计算,特征多的时候能省不少时间。
3.2 交叉验证的折数怎么选
cv=5是默认值,但数据量小的时候(比如样本少于 200),5 折会导致每折验证集太小,评估方差大。这时候用cv=10或者LeaveOneOut。数据量大且训练成本高时,cv=3也能接受。我一般先看样本量:样本数除以 5 如果小于 30,就加到 10 折。
from sklearn.model_selection import KFold # 样本量小的时候用 10 折 kf = KFold(n_splits=10, shuffle=True, random_state=42) ridge_cv_10 = RidgeCV(alphas=alphas, cv=kf, scoring='neg_mean_squared_error') ridge_cv_10.fit(X_train_scaled, y_train) print("10 折 Ridge 最优 alpha:", ridge_cv_10.alpha_)注意shuffle=True在数据有顺序时很重要,比如按时间采集的样本,不 shuffle 会导致每折分布不一致。random_state固定住保证结果可复现。
3.3 用验证曲线看 alpha 的影响
交叉验证选出的 alpha 是一个点,但了解 alpha 变化对误差的影响趋势更有价值。validation_curve可以画出不同 alpha 下的训练误差和验证误差。
from sklearn.model_selection import validation_curve import matplotlib.pyplot as plt train_scores, test_scores = validation_curve( Ridge(), X_train_scaled, y_train, param_name="alpha", param_range=alphas, cv=5, scoring='neg_mean_squared_error', n_jobs=-1 ) train_mean = -train_scores.mean(axis=1) test_mean = -test_scores.mean(axis=1) plt.figure(figsize=(8, 5)) plt.semilogx(alphas, train_mean, label="训练误差") plt.semilogx(alphas, test_mean, label="验证误差") plt.xlabel("alpha") plt.ylabel("MSE") plt.legend() plt.title("Ridge 验证曲线") plt.show()典型的验证曲线是 U 形:alpha 很小时验证误差高(过拟合),alpha 很大时验证误差也高(欠拟合),最低点在中间。如果曲线最低点出现在 alpha 候选范围的最左端或最右端,说明候选范围没覆盖到最优值,需要调整np.logspace的区间。
4. 避坑与排查:正则化回归最常见的 5 个翻车现场
4.1 标准化做在划分训练测试集之前
现象:交叉验证得分很高,但上线后预测效果差一截。
原因:先对全量数据做StandardScaler().fit_transform(X),再划分训练测试集。这样测试集的均值和方差信息泄漏到了训练过程中,评估结果偏乐观。
解决:永远先train_test_split,再在训练集上fit_transform,测试集只transform。如果用了 Pipeline,把 scaler 和模型串在一起,交叉验证时自动处理。
from sklearn.pipeline import Pipeline pipe = Pipeline([ ("scaler", StandardScaler()), ("ridge", RidgeCV(alphas=np.logspace(-3, 3, 100), cv=5)) ]) pipe.fit(X_train, y_train) # 直接传原始 X_train,Pipeline 内部处理标准化 print("Pipeline 最优 alpha:", pipe.named_steps["ridge"].alpha_)4.2 Lasso 不收敛却不知道
现象:Lasso或LassoCV跑完提示ConvergenceWarning,系数结果不可信。
原因:默认max_iter=1000对高维数据不够,坐标下降法还没收敛就停了。
解决:把max_iter调到 10000 甚至 50000,同时关注tol参数(默认 1e-4),如果数据尺度大可以适当放宽到 1e-3。跑完后检查n_iter_属性,如果等于max_iter,说明还没收敛。
lasso = Lasso(alpha=0.01, max_iter=50000, tol=1e-3) lasso.fit(X_train_scaled, y_train) print("实际迭代次数:", lasso.n_iter_) if lasso.n_iter_ == lasso.max_iter: print("警告:仍未收敛,需要继续增大 max_iter 或检查数据")4.3 对分类特征做了标准化
现象:Lasso 把某些有业务含义的类别特征系数压成零,但业务上这些特征明明很重要。
原因:对 one-hot 编码后的 0/1 特征做了标准化,原本稀疏的类别特征被转换成连续值,正则化惩罚失去了稀疏选择的意义。
解决:只对连续特征做标准化,类别特征保持 0/1 编码。用ColumnTransformer分别处理。
from sklearn.compose import ColumnTransformer numeric_features = ["面积", "房龄", "距离地铁"] categorical_features = ["区域", "楼层类型"] preprocessor = ColumnTransformer([ ("num", StandardScaler(), numeric_features), ("cat", "passthrough", categorical_features) ]) pipe = Pipeline([ ("prep", preprocessor), ("lasso", LassoCV(cv=5, max_iter=10000, random_state=42)) ]) pipe.fit(X_train, y_train)4.4 alpha 候选范围没覆盖最优值
现象:RidgeCV选出的 alpha 等于候选列表的最大值或最小值。
原因:真实最优 alpha 在候选范围之外。比如数据噪声极大,需要很强的正则化,但候选最大只到 1000。
解决:看ridge_cv.alpha_是否落在alphas的两端。如果是,把np.logspace的范围扩大,比如改成np.logspace(-5, 5, 200)。同时用验证曲线确认最低点确实在范围内部。
4.5 用 Lasso 处理高度相关的特征组
现象:同一组相关特征(比如「GDP」「人均收入」「消费水平」),Lasso 每次跑保留的特征不一样,模型解释不稳定。
原因:L1 正则化在相关特征中随机选一个保留,其余压零。数据微小变动就会换一个特征。
解决:如果业务上需要保留整组特征,改用岭回归或 ElasticNet。ElasticNet 的 L2 部分能让相关特征系数趋于一致,L1 部分仍然做稀疏化。
from sklearn.linear_model import ElasticNetCV enet_cv = ElasticNetCV( l1_ratio=[0.1, 0.3, 0.5, 0.7, 0.9], cv=5, max_iter=10000, random_state=42, n_jobs=-1 ) enet_cv.fit(X_train_scaled, y_train) print("最优 l1_ratio:", enet_cv.l1_ratio_) print("最优 alpha:", enet_cv.alpha_)l1_ratio控制 L1 和 L2 的混合比例,0 是纯岭回归,1 是纯 Lasso。中间值兼顾特征选择和组效应保留。
5. 用系数路径图和弹性网络把模型解释做扎实
调完参、跑通模型只是第一步。真正让业务方信服的,是能说清楚「哪些特征重要、重要多少、为什么」。系数路径图(coefficient path)是 Lasso 最直观的解释工具,它展示每个特征的系数随 alpha 增大如何变化。
from sklearn.linear_model import lasso_path import matplotlib.pyplot as plt alphas_path, coefs, _ = lasso_path( X_train_scaled, y_train, alphas=np.logspace(-4, 1, 100) ) plt.figure(figsize=(10, 6)) for i in range(coefs.shape[0]): plt.plot(np.log10(alphas_path), coefs[i], label=f"特征 {i}") plt.xlabel("log10(alpha)") plt.ylabel("系数值") plt.title("Lasso 系数路径") plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left") plt.tight_layout() plt.show()图里每条线是一个特征。alpha 很小时所有系数都非零,随着 alpha 增大,系数逐个被压到零。哪个特征最后归零,说明它对目标变量的贡献最稳定。这个图可以直接拿给业务方看,比一堆数字有说服力。
如果 Lasso 的稀疏化太激进,把一些业务上认为重要的特征也压没了,ElasticNet 是折中方案。我一般先跑 LassoCV 看非零系数个数,如果比预期少太多(比如 50 个特征只剩 3 个),就换 ElasticNetCV,l1_ratio从 0.5 开始试。
还有一个容易忽略的点:正则化回归的系数是在标准化后的尺度上的,解释时要还原回原始尺度。还原公式是β_original = β_scaled / σ_j,其中 σ_j 是第 j 个特征的标准差。scikit-learn 的Ridge和Lasso在fit时如果没做标准化,coef_就是原始尺度;如果用了 Pipeline 里的 StandardScaler,需要手动还原才能和业务口径对齐。
# 从 Pipeline 中提取标准化后的系数并还原 scaler = pipe.named_steps["prep"].named_transformers_["num"] lasso_model = pipe.named_steps["lasso"] # 连续特征的均值和标准差 means = scaler.mean_ stds = scaler.scale_ # 还原系数(只针对连续特征部分) coef_scaled = lasso_model.coef_[:len(means)] coef_original = coef_scaled / stds for name, coef in zip(numeric_features, coef_original): print(f"{name}: 原始尺度系数 = {coef:.4f}")最后说个我自己的习惯:每次跑完正则化回归,一定把alpha_、非零系数个数、测试集 MSE 这三个数记下来。换数据版本或调整特征工程后,对比这三个数的变化,比看单个指标更能发现模型行为是否异常。岭回归和 Lasso 不是银弹,但在共线性和高维特征场景下,它们是最稳的基线。希望帮到你。
本文还有配套的精品资源,点击获取