简介:这是一份面向数据科学与统计学学习者的正则化回归Python算法资源,系统实现L1正则化(Lasso)与L2正则化(Ridge)两种主流模型,解决高维数据下模型过拟合和特征选择问题。资源基于Scikit-Learn搭建,涵盖NumPy数值计算、Pandas数据处理、训练集与测试集划分、均方误差评估及GridSearchCV自动调参,并附有完整可运行的示例代码与预处理脚本,适合初中级Python开发者循序渐进地学习。
压缩包共168个文件,整体约1.28MB,以110个Python脚本和34个RST文档为核心。其中py脚本提供算法实现与工具函数,rst文档与8个Notebook包含原理讲解和案例演示,pyx扩展用于提升计算性能,另有配置、说明文件等,目录结构覆盖基础Lasso、分布式ADMM、分组Lasso及新闻组逻辑回归等多元场景,便于按模块查阅。已有302人学习下载,读者可借助这些代码和数据理解L1、L2的数学原理,掌握交叉验证选择alpha的方法,并将正则化回归灵活应用于实际预测任务。
1. 正则化回归的Python实现:先压住过拟合,再谈系数解释
正则化回归在Python里的出场场景,通常是数据里特征很多、样本不多,或者特征之间互相纠缠,普通线性回归一拟合就“表演式过拟合”——训练集表现惊艳,拿到新数据上立刻翻车。正则化回归做的事情,是在最小二乘的损失函数后面加一道惩罚项,给系数戴上缰绳:允许你拟合数据,但系数不能膨胀到失去解释意义。做量化策略代码里的因子筛选、工业传感器多参数建模,或者任何一份高维表格,只要你想从几十上百个特征里挑出真正有用的几个,这套算法就是最直接的起点。接下来按“选型思路 → 代码实现 → 踩坑记录 → 验证技巧”展开,代码可以直接复制运行。
2. 从最小二乘到正则化:L1、L2和Elastic Net的选型思路
同类型算法一次出现三个,新手容易懵。关键抓住一个核心差异:惩罚项的数学形态决定了模型行为,模型行为决定了适用场景。这一章先把每种正则化的脾气讲透,再给出一张可以抄走的选型表。
2.1 岭回归:L2惩罚解决系数爆炸
普通线性回归的求解目标只是让残差平方和最小。这个目标本身没有问题,但当特征之间存在相关性,或者特征数量接近样本数量时,解会变得非常敏感。想象两个温度传感器放在同一个机柜里,读数高度相关,模型完全可以把系数配成100和-99,照样做出不错的预测。系数绝对值很大,物理上不可解释,数据稍一变化系数就剧烈震荡。
岭回归在原有的残差平方和上追加一项系数的L2范数,也就是所有系数平方和乘以alpha。直观理解是给每个系数都收一道“平方税”,系数越大税越重,于是优化器不再敢把任何一个系数推到离谱的程度。alpha控制税率,税率越高,系数越被压缩,但没有任何系数会被真正压成0,只会趋近于0。
所以我一般把Ridge当作“稳定剂”来用:特征之间有多重共线性,你又没有删特征的硬需求,更关心预测稳定性而不是特征选择,直接上Ridge。比如传感器数据建模,判断所有通道都带一点信息量,就别折腾Lasso,Ridge一次到位。
2.2 Lasso:L1惩罚让不重要的特征归零
Lasso的惩罚项从平方和换成绝对值之和。这一形状上的改变带来了本质不同的几何性质:在约束边界是“菱形”的目标函数中,最优解容易落在坐标轴上,对应某些维度的系数精确等于0。也就是说,Lasso在拟合过程中会主动做特征选择,把冗余特征的系数清零,这是它和Ridge最核心的区别。
Lasso适合所谓“稀疏”问题:几百上千个特征里,真正对目标有解释力的只有一小撮。量化领域用Lasso筛因子,生物统计用Lasso从基因芯片数据里挑选表达相关基因,都是这个套路。一个非常实用的判断标准:如果你把特征全谱排序后发现大量系数非常小、只有少数明显突出,这就是典型的稀疏场景,Lasso会比Ridge出彩得多。
但Lasso也有让人头疼的部分。两个高度相关的特征会让它在两者之间随机二选一,特征选择结果不稳定;另外求解依赖坐标下降迭代,比Ridge的解析解慢,数据量大时训练耗时明显。遇到特征分组相关的情况,Lasso的表现经常不如Elastic Net,这一点在第四章会单独讲坑。
2.3 Elastic Net:Lasso和Ridge的折衷方案
Elastic Net把L1和L2两种惩罚同时放进损失函数,让模型既能利用L1产生稀疏性,又能借L2稳住共线特征。l1_ratio参数控制两者比例,等于1时退化为Lasso,等于0时退化为Ridge,实际项目中取0.5到0.8之间的情况最多。
选择Elastic Net的触发信号主要有两个:特征数量远超样本数量,或者特征之间存在明显的成组相关性。Elastic Net有个很好的性质叫“组效应”:一旦两个特征高度相关,它倾向于把两个系数同时选进去或者同时剔掉,而不是像Lasso那样随机留一个。这会显著提升特征清单在不同次训练之间的稳定性,对要交付特征评审报告的人来说尤其重要。
代价也很明确:调参任务从一维变成二维。alpha和l1_ratio要联合搜索,计算开销随之增加。所以我的建议是把Elastic Net当增强方案:先跑通Ridge和Lasso,确认线性假设成立后,再考虑用Elastic Net冲击更优结果。直接用默认参数跑Elastic Net然后说它不好用,多半是l1_ratio没调对。
2.4 alpha怎么定:网格搜索加交叉验证,别手拍
alpha不是模型里“意思一下”的开关,它的最佳值严重依赖数据本身的尺度。一个在A数据集上表现良好的alpha=1.0,搬到特征方差完全不同的B数据集上可能让模型直接瘫痪。因此我从不手拍alpha,也建议你放弃这个想法。
常见的做法是配合StandardScaler先标准化,再用GridSearchCV在np.logspace(-3, 3, 50)这个范围里按对数等距搜索。用对数均匀而不是线性均匀,是因为alpha的合理取值横跨好几个数量级,只有对数采样才能在小值区间和大值区间都保证足够的搜索密度。评估指标要和业务对齐:关注绝对误差用neg_mean_squared_error,担心异常值拉偏用neg_mean_absolute_error更稳。
需要特别提醒:交叉验证选参和最终评估是两个不同环节。很多人用同一份测试集反复试alpha,选完再拿测试集报R²,这个分数本质上已经不是无偏估计了。正确做法是先用训练集内部交叉验证定alpha,测试集只碰一次。
下表是对三种模型最简的选型对照,适合贴在项目文档里:
| 模型 | 惩罚项 | 是否做特征选择 | 典型场景 |
|---|---|---|---|
| Ridge | L2 | 否,系数只压缩不归零 | 多重共线性强、特征全保留 |
| Lasso | L1 | 是,部分系数精确为0 | 高维稀疏、需要特征清单 |
| ElasticNet | L1+L2 | 是,相关特征成组进出 | 特征数远超样本数、特征成组相关 |
3. 用sklearn跑通正则化回归:最小可复现的完整代码
与其找现成的代码包下载,不如把这份最小复现代码跑通。它能让你看清楚每一步在做什么,并且换到自己的数据上时只改动一行。
3.1 准备环境:只需要四个库
正则化回归在Python里最成熟的实现是scikit-learn,它的linear_model模块同时提供Ridge、Lasso、ElasticNet以及对应的交叉验证版本。安装命令如下:
pip install numpy pandas scikit-learn如果后面要做系数可视化或画学习曲线,再补一个matplotlib。在Windows上如果遇到和sklearn相关的DLL报错,通常是用pip安装的包版本与Python版本不匹配,建议换Python 3.9或3.10的64位版本重试。Linux下一般直接装完就能跑,很少遇到这种玄学问题。
3.2 用模拟数据复现:不需要额外准备数据集
为了让代码开箱即跑,我直接用sklearn内置的make_regression生成一份高维稀疏数据:200个样本、60个特征,但其中只有8个特征真正对y有贡献。这种结构和真实场景里的“特征多、有效特征少”高度一致,是Lasso这类算法最典型的用武之地。
import numpy as np from sklearn.datasets import make_regression # 200个样本,60个特征,其中只有8个真正有用 X, y = make_regression( n_samples=200, n_features=60, n_informative=8, noise=12, random_state=42, ) print("特征矩阵形状:", X.shape, " 目标变量形状:", y.shape)make_regression里的noise参数控制噪声标准差,调大会让过拟合现象更明显,便于观察三种正则化模型的差异。random_state固定为42是为了让每次运行结果可复现,排查问题时能确定变化来自代码而非随机性。把这里的X和y替换成你自己的DataFrame,后续所有代码都不用改结构。
3.3 三种模型一次跑通:核心代码与输出解读
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import ( LinearRegression, Ridge, Lasso, ElasticNet ) from sklearn.metrics import mean_squared_error, r2_score X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) models = { "LinearRegression": LinearRegression(), "Ridge(alpha=1.0)": Ridge(alpha=1.0), "Lasso(alpha=0.5, max_iter=100000)": Lasso(alpha=0.5, max_iter=100000), "ElasticNet(alpha=0.3, l1_ratio=0.7)": ElasticNet( alpha=0.3, l1_ratio=0.7, max_iter=100000 ), } for name, model in models.items(): model.fit(X_train_s, y_train) pred = model.predict(X_test_s) rmse = np.sqrt(mean_squared_error(y_test, pred)) r2 = r2_score(y_test, pred) n_nonzero = np.sum(np.abs(model.coef_) > 1e-8) print(f"{name} -> RMSE: {rmse:.3f}, R²: {r2:.3f}, 非零系数: {n_nonzero}")这段代码的逻辑分四步:先把数据切成训练和测试两半;再用StandardScaler标准化,标准化器必须只在训练集上fit,再对测试集只用transform;然后分别初始化三种正则化模型加一个普通线性回归做对照;最后统一用测试集计算RMSE、R²和非零系数个数。
非零系数个数是理解Lasso的关键输出:如果Lasso的系数个数接近8,说明它基本找回了make_regression里设定的有效特征数。Ridge的RMSE可能也不错,但非零系数一定是60,因为它只会缩小系数不会归零。普通线性回归在这个设置下通常表现出训练集R²虚高、测试集R²明显下滑,这就是不设防的代价。如果跑的时候看到ConvergenceWarning,不用慌,先往后看到4.1节。
3.4 交叉验证自动寻参:把alpha交给数据决定
手动给定alpha总觉得心里没底。常见做法是直接用带内置交叉验证的版本,比如LassoCV和RidgeCV;也可以统一用GridSearchCV,这样三种模型可以走同一套调参流程。
from sklearn.model_selection import GridSearchCV lasso_cv = GridSearchCV( Lasso(max_iter=100000), param_grid={"alpha": np.logspace(-3, 3, 50)}, cv=5, scoring="neg_mean_squared_error", ) lasso_cv.fit(X_train_s, y_train) best_alpha = lasso_cv.best_params_["alpha"] print("Lasso 最佳 alpha:", best_alpha) print("交叉验证最优分数(负MSE):", lasso_cv.best_score_) # 用最优参数重新训练,并在测试集上做最终评估 best_lasso = Lasso(alpha=best_alpha, max_iter=100000) best_lasso.fit(X_train_s, y_train) test_pred = best_lasso.predict(X_test_s) print("测试集 RMSE:", np.sqrt(mean_squared_error(y_test, test_pred)))param_grid里的np.logspace(-3, 3, 50)会在0.001到1000之间取50个对数等距的值。这样设计是因为alpha的合理范围跨越多个数量级,线性等距会在小数区间只分到极少数格子。Lasso在alpha较小时迭代会变慢,max_iter给到100000是安全操作;如果仍然有收敛警告,可以顺手把tol从默认的1e-4放宽到1e-3。
GridSearchCV默认使用5折交叉验证,也就是把训练集切成5份轮流做验证集。scoring="neg_mean_squared_error"表示用负均方误差做评分,这里有个容易误解的点:sklearn的评分口径统一为“越大越好”,所以误差类指标都加了负号,负MSE越接近0说明误差越小。
下表整理了三个最常动的参数,调参时对着改就行:
| 参数 | 含义 | 常用范围或取值 |
|---|---|---|
| alpha | 惩罚强度 | 1e-3 ~ 1e3,推荐对数网格搜索 |
| l1_ratio | ElasticNet中L1惩罚占比 | 0到1,常用0.5 ~ 0.8 |
| max_iter | 坐标下降最大迭代次数 | 50000起步,报错就往上加 |
| tol | 收敛容忍度 | 默认1e-4,不收敛可放宽到1e-3 |
4. 正则化回归常见的五个问题:现象、原因与排查步骤
这一章的内容来自实际项目里的血泪经验。每条都按“现象 → 原因 → 解决”的顺序写,遇到问题直接对号入座。
4.1 Lasso训练时弹出ConvergenceWarning,或系数全部为0
现象:Lasso拟合时终端弹出ConvergenceWarning,提示坐标下降法达到max_iter上限没有收敛;或者不报错,但模型coef_全为0,预测值恒定为一个常数。
原因:第一种常见是alpha设得过大,惩罚压过了拟合信号,模型干脆把所有系数清零。第二种是max_iter太小,训练在未收敛状态下被提前切断。还有一种很隐蔽的情况:数据没标准化,某个特征的绝对数值特别大,L1惩罚把它误伤,导致优化路径异常。
解决:先把alpha往小了调,比如从1.0改到0.01;同时把max_iter设到50000以上,如果还收敛不了,再把tol从默认的1e-4放宽到1e-3。数据标准化是基本功,建议直接用StandardScaler,不要在没标准化的数据上猜alpha。
4.2 没标准化直接跑模型,特征重要性排序一塌糊涂
现象:有人不标准化直接拟合,结果其中一个特征系数高达几百,煞有介事地当作重要特征写进报告;换一批数据再跑,这个特征的排名又完全变了。
原因:L1和L2惩罚对系数大小一视同仁,而特征量纲越大,它的系数天然越小就能产生同等预测效果。没标准化时,惩罚会偏向压制量纲大的特征,让优化问题变成“比量纲”而不是“比重要性”。这是正则化模型里最容易翻车的一步。
解决:把StandardScaler放在流程最前面,并且只在训练集上fit,再用同一个scaler去transform测试集。如果你对全量数据一次性fit,测试集的信息会漏进训练过程,导致测试集评估结果虚高,后面部署时才发现模型实际没那么准。
4.3 alpha拍脑袋设1.0,验证集误差离奇大
现象:照网上某段旧代码把alpha固定成1.0,训练集误差看着还行,一到验证集误差成倍放大;换成alpha=0.01之后结果又恢复正常。
原因:alpha=1.0本身没有绝对意义,惩罚强度是相对数据方差的。如果特征方差很小、信号很强,1.0的惩罚会把系数压得过于保守,模型偏向欠拟合;反过来,特征方差大、噪声多,1.0又可能不够。
解决:别再手拍alpha,用3.4节的网格搜索。也可以直接用sklearn的LassoCV、RidgeCV,这些带CV后缀的模型内置了alpha自动搜索路径,搜索效率比GridSearchCV更高,因为它们用的是交叉验证的解析路径,理论上能逼近最优alpha。
4.4 特征高度相关时,Lasso选中的特征忽A忽B
现象:从同一数据源里抽两份样本,分别跑Lasso,一个重要特征有时被保留、有时被剔除。准备输出特征清单时发现结果没法解释。
原因:Lasso的L1惩罚在高度相关的特征面前没有倾向性,谁留下完全看优化路径走到哪。这是L1稀疏性的固有副产物,不是随机种子的问题,也不是数据质量问题。
解决:先计算特征相关矩阵,把相关系数大于0.8的特征做去重处理,只保留其中代表性最强的一个;或者改用ElasticNet并设l1_ratio在0.7左右,利用组效应让相关特征成组进出。想进一步判断特征稳定性,可以跑bootstrap Lasso:多次重采样计算每个特征被选中的频率,只保留频率高于80%的特征,这个方法在第六章展开。
4.5 把模型套进Pipeline后,网格搜索报参数名不存在
现象:代码逻辑看着没问题,GridSearchCV却报InvalidParameterError,提示参数名不存在;或者搜索出来的结果和手动调的一模一样,参数根本没生效。
原因:Pipeline里每个步骤的参数名要带“步骤名__”前缀。比如步骤名是lasso,参数就要写成lasso__alpha,只写alpha必然报错。更隐蔽的问题是,如果Pipeline里两个估计器都有alpha参数,不写完整前缀时sklearn无法区分到底给谁。
解决:用pipe.get_params()打印出所有可用参数的键名,照着键名写param_grid,不要凭记忆敲。写完先跑一个2×2的小网格做快速验证,确认best_params_确实在你给的网格范围内变化,再放大搜索空间。
5. 把正则化回归用出价值:系数落地、多项式扩展与模型对比
能跑通代码只是第一步。这一章解决的是“跑通之后怎么用”的问题,让模型输出真正进入你的决策流程。
5.1 把系数落成表格:从黑匣子到可解释决策清单
正则化回归最大的优势是系数稳定后可解释。把特征名和系数拼成一个Pandas表格,按绝对值排序后导出,这个习惯在业务侧特别加分。下游同学不需要会Python,拿到表格就能评审。
import pandas as pd feature_names = [f"feature_{i}" for i in range(X_train_s.shape[1])] coef_df = pd.DataFrame({ "feature": feature_names, "coef": best_lasso.coef_, }) coef_df["abs_coef"] = coef_df["coef"].abs() coef_df = coef_df.sort_values("abs_coef", ascending=False) # 只保留被Lasso选中的特征 selected = coef_df[coef_df["coef"] != 0] print(selected.head(15)) # 导出为CSV,方便后续用Excel或BI工具继续加工 selected.to_csv("lasso_selected_features.csv", index=False)这里的coef代表标准化后特征每变动一个标准差,预测值变动多少个单位。这个“标准化系数”口径让不同量纲的特征可以横向比较。如果你习惯用Excel处理,把to_csv换成to_excel前需要先pip install openpyxl。这算是我日常工作里用得最频繁的一段代码,几乎每个特征筛选项目都会走到这一步。
5.2 加多项式特征:什么时候该升维,什么时候该克制
正则化回归本质上还是线性模型,处理不了强非线性关系。常见做法是先用PolynomialFeatures把原始特征升维,再交给Lasso做特征选择。因为Lasso会把不重要的交互项和平方项压成0,相当于自动帮你搜索“哪些非线性组合值得保留”。
from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import Pipeline poly_pipe = Pipeline([ ("scaler", StandardScaler()), ("poly", PolynomialFeatures(degree=2, include_bias=False)), ("scaler2", StandardScaler()), ("lasso", Lasso(max_iter=100000)), ]) param_grid = { "lasso__alpha": np.logspace(-3, 1, 20), "poly__degree": [1, 2], } search = GridSearchCV( poly_pipe, param_grid, cv=5, scoring="neg_mean_squared_error" ) search.fit(X_train, y_train) print("最优参数:", search.best_params_) print("最优分数(负MSE):", search.best_score_)PolynomialFeatures(degree=2)会把60个特征变成约1891个特征,计算量陡增。此时alpha要更小心,特征变多后需要更大的稀疏压力才能压住噪声特征。degree=3千万别轻易尝试,60个特征的三阶多项式会膨胀到接近四万维,普通笔记本内存根本扛不住,这是很多人在高维数据上把机器跑死的直接原因。Pipeline里的scaler2不是可有可无的:多项式会生成高次项,量纲差异比原始特征更夸张,二次标准化能避免Lasso被个别高次项带偏。
5.3 与随机森林回归的对比:什么时候该换树模型
正则化回归和随机森林回归是两条完全不同的路线。随机森林不需要标准化,能建模非线性关系,对异常值也稳健;但它不能外推,给训练集范围之外的特征值做预测时表现很差,而且特征重要性没有正负号,你无法判断某个特征到底是推高还是拉低预测值。
我的判断标准是:如果业务上有明确的线性或半线性关系,而且需要向非技术方解释特征的增减方向,选正则化回归;如果特征与目标的关系完全未知、样本量又足够大,先用随机森林回归跑一个上限参考,看正则化模型差多远。如果两者测试集误差差距在10%以内,果断选正则化回归,因为它的部署和维护成本低得多——一个线性模型只有一组系数,上线之后出了问题也容易排查。
6. 三步验证法:系数稳定性、预测偏差和学习曲线
模型不是跑完就完事,交付前必须验证。我一般只做三件事:bootstrap看系数稳定性,残差看预测偏差,学习曲线看数据量是否够。
from sklearn.utils import resample # 第一步:bootstrap系数稳定性检验 # 100次重采样,统计每个特征被Lasso选中的频率 selected_count = np.zeros(X_train_s.shape[1]) n_bootstrap = 100 for i in range(n_bootstrap): X_boot, y_boot = resample(X_train_s, y_train, random_state=i) lasso_boot = Lasso(alpha=best_alpha, max_iter=100000) lasso_boot.fit(X_boot, y_boot) selected_count += (lasso_boot.coef_ != 0).astype(int) freq = selected_count / n_bootstrap stable_features = np.where(freq > 0.8)[0] print(f"100次重采样后,有 {len(stable_features)} 个特征被Lasso稳定选中") # 第二步:测试集残差检查,看是否有系统性偏差 residuals = y_test - best_lasso.predict(X_test_s) print(f"残差均值: {residuals.mean():.3f}, 残差标准差: {residuals.std():.3f}")bootstrap的思路很简单:用有放回抽样生成100份“新数据集”,每份都重新跑一遍Lasso,统计每个特征被选中的频率。频率高于80%的特征才是真正稳定的信号,那些忽上忽下的特征直接丢进候选区,不要写进结论。残差均值应该接近0,如果明显偏离0,说明模型存在系统性偏差,可能漏掉了某个趋势特征,或者数据生成过程本身就不是对称分布的。
第三步是学习曲线,做法简单描述:固定模型参数,分别用训练集的20%、40%、60%、80%去训练,记录训练集和验证集误差。如果训练集误差一直很低而验证集误差下不来,说明数据量不够,加特征不如加样本。如果两条误差曲线已经收敛到一起且误差都不大,说明当前数据下模型已经吃到上限,继续调参收益很小。我习惯把这三步结果一起打印出来贴进项目文档,比自己说“模型效果不错”有说服力得多。
做正则化回归这几年,最大的教训是别把单次测试集分数当作模型可用的证据。系数稳定性、残差分布和学习曲线,这三样加起来才能说明模型真的靠得住。希望帮到你。
本文还有配套的精品资源,点击获取