简介:一份面向机器学习与数据科学学习者的正则化回归Python算法代码包,聚焦L1(Lasso)与L2(Ridge)两种经典正则化方法,通过向损失函数引入惩罚项有效抑制高维数据下的过拟合,并利用L1稀疏性自动完成特征选择、借助L2平滑参数以控制模型复杂度。资源覆盖从环境搭建、数据预处理、训练集/测试集划分,到模型训练、均方误差(MSE)评估及GridSearchCV交叉验证调参的完整流程,适合需要系统掌握回归建模与超参数调优的中高级读者。包体为zip压缩包,共168个文件,含110个py源码脚本、34个rst说明文档、8个ipynb示例笔记及少量txt、配置与资源文件,整体约1.28MB,目录结构清晰,便于按模块阅读。已有302人学习下载。除基础Lasso/Ridge实现外,资源还收录Fused LASSO、Group LASSO、分布式LASSO(ADMM)等进阶场景的Notebook示例,可帮助读者结合实际数据动手实践,深入理解正则化回归的数学原理与Scikit-Learn工程实现。
1. 正则化回归的 Python 算法包:别只盯着 Lasso,distributed 那本 notebook 才是精华
这份资源是一个名为 regreg-master 的 Python 算法示例包,表面上看它只是把 LASSO demo、Group LASSO、Fused LASSO 这几本 Jupyter notebook 打包在一起,但实际拆开你会发现,它把正则化回归从"调一个 sklearn 的 Lasso 类"推到了"自己控制惩罚项、自己实现分布式 ADMM 求解"这一层。对于正在做特征选择、高维稀疏建模、或者面试前想补一轮正则化回归原理的人来说,这里面的代码比文档更直观。它不是给你一个黑匣子预测接口,而是把损失函数怎么拆、迭代怎么收敛、alpha 怎么扫全摊开在单元格里。下载后我建议你按 INSTALL 文件装好环境,先跑通 LASSO demo,再去看 Distributed LASSO 那本,顺序不对容易劝退。
2. 为什么选 regreg:从 L1/L2 的损失函数到 sklearn 的边界
2.1 L1 与 L2 惩罚的数学直觉和选型理由
正则化回归的核心做法是在原有损失函数后面加一个惩罚项。岭回归加的是参数平方和,对应 L2 范数,它对每个系数都施加一个按比例的收缩,系数被压向零但不会真正变成零。Lasso 加的是参数绝对值之和,对应 L1 范数,由于绝对值函数在零点的不可导特性,优化结果里会有一批系数被精确地置零。这两种惩罚的差异在几何上可以用约束区域的形状解释:L1 的约束区域是带尖角的菱形,损失函数等值线第一次碰到这个尖角的位置通常落在坐标轴上;L2 的约束区域是光滑的球面,切点几乎不会落在坐标轴上。
选型的时候,如果你的目标是从几百个特征里筛出真正有用的少数几个,L1 是首选;如果特征之间相关性高,你希望模型稳定、系数不要来回跳,L2 更合适。实践中更常见的是两者组合使用,比如 elastic net,它把 L1 的稀疏性和 L2 的稳定性按比例混合,alpha 控制整体惩罚强度,l1_ratio 控制 L1 在总惩罚里的占比。这份资源里的 Fused LASSO 则更进一步,它不光惩罚系数本身,还惩罚相邻系数之间的差值,适合信号平滑和趋势提取。
2.2 sklearn 够用,但 regreg 把求解器拆开了
很多人上手正则化回归都从 sklearn 的 Lasso 和 Ridge 开始,这个方向没问题,但 sklearn 把求解过程封装得太干净了。你调一个Lasso(alpha=0.01),它内部用坐标下降帮你迭代完,你能看到的结果只有 coef_ 和 intercept_,中间发生了什么对你来说是个黑匣子。regreg 这一类库的定位不一样,它提供的是惩罚项和求解器的积木:你可以自定义一个惩罚项组合,可以选择 ADMM、FISTA 这类算法去求解,甚至可以自己实现分布式求解逻辑。
资源包里的 Distributed LASSO: ADMM.ipynb 和 Distributed LASSO: ADMM one value.ipynb 就是这一点的直接体现。前者展示了一个完整的 ADMM 求解框架,后者用单个数值演示 ADMM 循环中每一步交换的信息长什么样。普通应用场景你不需要分布式,但当数据量大到单机装不下、或者数据本身分布在多个节点时,ADMM 这种"本地求解、只交换系数"的模式就非常有价值了。此外,资源里还有 Group LASSO 和 Fused LASSO 两本 notebook,这两类惩罚在 sklearn 主线版本里没有直接对应实现,想跑通只能用第三方库或者自己推导近端算子,这份资源恰好把这条路走了一遍。
2.3 装环境并跑通第一个 LASSO demo
拿到压缩包后先看 INSTALL 文件,里面会列出 regreg 以及 notebook 运行所需的依赖。我一般会新建一个干净的虚拟环境,然后安装依赖:
python -m venv regreg-env source regreg-env/bin/activate pip install --upgrade pip pip install numpy scipy scikit-learn jupyter matplotlib pip install regreg jupyter notebook逻辑说明:虚拟环境是为了隔离依赖版本,避免系统 Python 环境里的旧包冲突。regreg 依赖 numpy 和 scipy 做矩阵运算,sklearn 用于交叉验证和数据集加载,matplotlib 用于画正则化路径和收敛曲线。参数说明:regreg 的最新版本支持 Python 3.8 以上,如果你在安装时遇到编译错误,先单独升级 scipy 再装 regreg,这个顺序能解决大部分问题。
环境装好后,打开 LASSO demo.ipynb,从头执行一遍。这个 notebook 会构造一个稀疏系数向量,生成观测数据,然后用 regreg 定义 L1 惩罚并求解。执行完看一下输出的 coef_ 形状,你会发现有相当一部分系数精确等于零。这里有个值得注意的细节:regreg 里惩罚项的权重通常写为l1 = regreg.atoms.nonlinear.l1norm,它返回的是惩罚公式本身,你需要再用regreg.solve去跑优化。这种写法跟 sklearn 的 fit 完全不同,第一次接触要适应一下。
3. 分布式 LASSO 的 ADMM 求解:多节点如何收敛到同一个解
3.1 ADMM 的核心拆分思路
ADMM 解决的核心问题是"一个带惩罚的回归目标怎么拆成多个可并行的小问题"。假设你的数据被切成若干块,每一块分布在一个计算节点上,每个节点手上的数据只够算一个局部的损失。ADMM 的思路是引入一个全局变量 z,让每个节点在求自己的系数时不要离 z 太远,同时用一个对偶变量 u 来记录每轮迭代中节点与全局之间的偏差。
这种拆分方式的好处是每个节点的子问题仍然是一个标准的岭回归形式,可以直接用现成的线性方程求解器处理;而全局变量 z 的更新则是一个软阈值操作,它是 L1 惩罚对应的近端算子,计算量大头在矩阵求逆,但每个节点只对自己那块数据做求逆,通信时只传系数向量不传原始数据,所以在机群环境下扩展性比整体求解好很多。资源包里 Distributed LASSO: ADMM.ipynb 的核心逻辑就是把上述三步循环写成了可执行代码,并配有迭代轮次、残差变化的输出。
3.2 用 numpy 手写一版简化 ADMM 更新式
我按资源里 notebook 的核心逻辑,用 numpy 写了一个单变量场景的简化版本,方便你理解每步在算什么:
import numpy as np def soft_threshold(z, kappa): # 软阈值算子:L1惩罚的近端映射 return np.sign(z) * np.maximum(np.abs(z) - kappa, 0.0) np.random.seed(123) n_samples, n_features = 80, 30 # 模拟两个节点各持一半数据 A1 = np.random.randn(n_samples // 2, n_features) A2 = np.random.randn(n_samples // 2, n_features) true_w = np.zeros(n_features) true_w[:3] = [2.0, -1.5, 1.0] b1 = A1 @ true_w + 0.05 * np.random.randn(n_samples // 2) b2 = A2 @ true_w + 0.05 * np.random.randn(n_samples // 2) lam = 0.1 rho = 1.0 x1 = np.zeros(n_features) x2 = np.zeros(n_features) z = np.zeros(n_features) u1 = np.zeros(n_features) u2 = np.zeros(n_features) # 节点本地子问题的系数矩阵预先分解 C1 = np.linalg.inv(A1.T @ A1 + rho * np.eye(n_features)) C2 = np.linalg.inv(A2.T @ A2 + rho * np.eye(n_features)) for iteration in range(200): # 节点1本地更新:岭回归形式 x1 = C1 @ (A1.T @ b1 + rho * (z - u1)) # 节点2本地更新 x2 = C2 @ (A2.T @ b2 + rho * (z - u2)) # 全局变量取两个节点系数的均值再收缩 z_old = z.copy() x_avg = 0.5 * (x1 + x2) z_avg = x_avg + 0.5 * (u1 + u2) z = soft_threshold(z_avg, lam / rho) # 对偶变量按本轮偏差累加 u1 = u1 + (x1 - z) u2 = u2 + (x2 - z) # 计算原始残差用于收敛判断 r_norm = np.linalg.norm(np.concatenate([x1 - z, x2 - z])) if iteration % 50 == 0: print(f"iter {iteration:3d} | r_norm={r_norm:.4f}") print("z:", z)逻辑说明:每个节点先固定 z 和 u,把带惩罚的目标函数转成一个岭回归子问题,这样求逆矩阵可以在循环外预先算好,省去每轮重复分解。z 的更新等价于对两个节点系数的平均再做一次软阈值收缩,这一步把 L1 的稀疏性注入解中。u 的更新是让对偶变量沿偏差方向累积,保证收敛后 x1、x2 和 z 一致。参数说明:rho 是 ADMM 的步长参数,rho 太大会让收敛变慢但更稳,rho 太小容易震荡;lam 是 L1 惩罚强度,对应 sklearn 里的 alpha,lam 越大 z 中零元素越多。收敛判断用原始残差 r_norm,实际工程中还要同时算对偶残差,两者都低于阈值才停止迭代。
3.3 收敛参数与停止条件的实际操作建议
ADMM 类算法的停止条件不能只看损失函数变化。很多人在 notebook 里跑一遍看到 loss 在降就认为没问题,实际上迭代结束的标准应该是原始残差和对偶残差同时达到阈值。原始残差衡量的是"各节点本地解与全局解的一致性",对偶残差衡量的是"全局解在过去几轮里的变化幅度"。资源里的 one value 那本 notebook 更直观,它固定一个 rho、一个 lambda,然后打印每一步的 z 变化,你能看到 z 从全零逐渐逼近真实稀疏系数。
实际操作时我一般这样设:rho 初始设为 1.0,max_iter 设 500,原始残差阈值 tol_abs 设 1e-4,对偶残差阈值 tol_rel 设 1e-4。如果 r_norm 在前 100 轮降得很快但后面长时间不动,先怀疑 rho 设置,试着把 rho 调大 5 倍再跑一轮对比;如果 z 的值在两个数之间震荡,说明 rho 偏小,需要减小步长或者改用自适应 rho。
4. Group LASSO 和 Fused LASSO:结构先验如何进入惩罚项
4.1 Group LASSO:按组收缩而不是按单系数收缩
普通 Lasso 对每个系数独立施加 L1 惩罚,如果一组特征之间存在强关联,比如 one-hot 编码产生的类别哑变量组,Lasso 可能只挑组里某一个系数而丢弃其余,这在业务解释上非常尴尬。Group LASSO 的惩罚项是按组计算 L2 范数后再求和,它迫使同一组内的系数要么一起保留、要么一起被压缩到零。资源里的 Group LASSO.ipynb 展示了如何用 regreg 构造这种分组惩罚。
regreg 中构造分组惩罚的典型代码模式是:
import regreg.api as rr import numpy as np # 假设12个特征分成3组,每组4个 groups = np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]) penalty = rr.group_lasso(groups, weights=None, lagrange=0.1) # lagrange 就是该组的惩罚强度逻辑说明:group_lasso会按 groups 数组把系数分组,组内用 L2 范数计算大小,组间用 L1 求和,这样求解时某个组只可能整组为零或整组非零。参数说明:weights 如果传 None,则每个组的权重相同;如果某个组样本量小或者你希望它更不容易被压缩成零,可以给它更小的权重数值。lagrange 对应惩罚强度,越大越容易整组清零。
4.2 Fused LASSO:惩罚相邻系数之间的差值
Fused LASSO 的惩罚由两部分组成:第一部分是常规的 L1 惩罚,让单个系数趋向零;第二部分是相邻系数差值的 L1 惩罚,让相邻系数尽可能接近。这个设计天然适合那些本身有平滑结构的场景,比如时间序列的断点检测、基因表达数据中顺次排列的位点分析。
regreg 里实现 fused lasso 惩罚可以用:
import regreg.api as rr n_features = 100 # 一阶差分矩阵:每一行表达相邻两个系数的差 D = (np.eye(n_features) - np.eye(n_features, k=1))[:-1] penalty = rr.l1norm.linear(D, lagrange=0.05) penalty2 = rr.l1norm(n_features, lagrange=0.01) # 最终惩罚是两个惩罚项的加权组合 combined_penalty = rr.smooth_atom.smooth_sum([penalty, penalty2]) # 需要包装为光滑形式逻辑说明:rr.l1norm.linear(D, lagrange=...)构造的是一个对 D @ beta 施加 L1 惩罚的项,D 是差分矩阵,乘以 beta 后得到相邻系数的差值向量,对这个差值向量做 L1 收缩,效果就是让相邻系数尽量相等。lagrange 参数控制这个平滑约束的强度,lagrange 越大,系数曲线越平滑,会抹掉真实的突变点;lagrange 越小,曲线越贴近原始数据,突变保留得越好。单独调 Fused LASSO 时我习惯先跑一版很小的 lagrange 让曲线出现 2~3 个明显台阶,再逐步加大,观察业务上关心的突变点是否稳定存在。
4.3 Newsgroup 逻辑回归示例的实际操作
资源里的 Newsgroup logistic regression.ipynb 用的是 20 新闻组数据集,这是一个经典的文本分类场景。逻辑回归加 L1 惩罚在这里的作用是:特征空间是几万维的词频向量,绝大多数词对分类没有贡献,L1 惩罚可以把词权重精确压成零,留下真正有区分度的关键词。
这份 notebook 的操作步骤一般是这样:先用 sklearn 的fetch_20newsgroups加载数据,用CountVectorizer转成词频矩阵,设置max_features限制特征维度,然后定义一个带 L1 惩罚的逻辑回归目标,跑训练后输出词权重。做完后你会得到一个非零系数只有几十个的稀疏模型,这些词的权重正负方向直接对应类别倾向,业务解释性远好于全连接网络。
这里有一个值得记住的工程经验:文本数据经过向量化后矩阵极其稀疏,用 regreg 求解时默认的密集矩阵计算会浪费大量内存,常见做法是把训练数据转成 scipy 的 csr_matrix,或者直接控制max_features到 1 万以内。如果你的机器只有 8G 内存,建议先把max_features设成 5000 跑通流程,再逐步扩大到全量特征。
5. 正则化回归避坑:alpha、标准化与收敛判断
5.1 没做标准化就让 alpha 失去可比性
现象:同一个数据集,直接套 Lasso 跑出来的系数很奇怪,某个特征的系数比其他特征大好几个数量级,而且改变 alpha 时系数路径图非常不顺滑。
原因:特征量纲差异大时,量级大的特征在损失函数里天然占主导,L1 惩罚在绝对数值上对它们相对宽松,求解器会优先保留大尺度特征,小尺度特征即便真正预测力强也容易被压成零。
解决:建模前对所有特征做标准化,sklearn 的StandardScaler是常见做法。注意先 fit 再 transform,而且 pipeline 要用同一个 scaler 去处理训练集和测试集,否则会有数据泄露。标准化之后 alpha 的取值范围也更好解释:通常从 1e-4 到 1e-1 按对数网格扫描即可。
5.2 alpha 只扫了 5 个值就把结果当最优
现象:用 GridSearchCV 搜 alpha,网格里只有 5 个点,选出来的最优 alpha 刚好落在网格边界,评估指标在边界处还在明显下降。
原因:网格设得不够宽或者不够密,边界处模型还没展现出真实性能就停了。
解决:把 alpha 轴改成对数均匀分布的 20~30 个值,如果最优值仍然在边界,就把网格边界外扩一个数量级重新扫。更稳妥的是用交叉验证曲线来选,画出一条完整路径的 CV 得分曲线,看 U 形的最低点在哪,不要只看几个离散点的结果。资源包 LASSO demo.ipynb 里如果你把 alpha 直接复制剧本,建议改成这种扫描方式再跑。
5.3 稀疏矩阵直接参与 regreg 求解报内存错误
现象:文本特征矩阵非常大,稀疏度 95% 以上,直接交给 regreg 的求解器后内存暴涨,甚至进程被杀。
原因:regreg 默认对输入做密集矩阵处理,稀疏矩阵在内部被转成了 dense 格式,几十万维的特征矩阵瞬间占满内存。
解决:先用 scipy 保持稀疏格式,参与求解前把数据转成 float64 的稀疏矩阵,并在构造惩罚项时确认 regreg 的求解器支持稀疏输入。如果库版本不支持,退而求其次用特征选择把维度压到 1 万以内再跑。这一步属于资源使用过程中的高发翻车点,很多人在文本分类这类场景上第一次接触 regreg 就是在这里卡住。
5.4 迭代没收敛就把中间结果当结论
现象:跑 Distributed ADMM notebook 时只执行了 30 轮迭代,打印出的系数和真实稀疏系数相比多了一堆非零小值,但看起来结果尚可就继续用了。
原因:ADMM 在迭代初期原始残差还很大,本地解和全局解没有对齐,此时的系数不是最优解,只是中间状态。
解决:看残差曲线,等原始残差和对偶残差都降到底部再取结果。工程上至少执行 200 轮以上,并加上max_iter和tol双条件退出。如果你发现 500 轮后残差还在缓慢下降,优先调 rho,而不是无限加大迭代次数。
5.5 只用 MSE 评估模型忽略系数稀疏度
现象:Lasso 和 Ridge 的测试集 MSE 几乎一样,就断定 Lasso 没必要,直接上了 Ridge。
原因:Lasso 的价值不只是预测精度,它同时还做特征选择。如果维度很高、后续要落地到线上的特征存储和计算成本,20 个非零特征和 500 个非零特征完全不是一个量级。
解决:评估正则化模型时同时输出非零系数个数、MSE、以及业务关注的解释性指标。如果一个模型在精度损失 1% 以内的前提下把特征数从 500 降到 30,真实落地价值是非常大的。这也是算法工程师面试里常被追问的细节:Lasso 与 Ridge 的性能对比,不能只看正确率。
6. 从复现到验证:一张正则化路径图判断 alpha 选得对不对
6.1 正则化路径图怎么看
下载资源里 LASSO demo.ipynb 和 Group LASSO.ipynb 跑完后,最有价值的输出不是最终的系数向量,而是不同 alpha 下所有系数的轨迹变化。我建议你画一张正则化路径图:横轴是 log(alpha),纵轴是每个特征的系数值。这张图能直接回答三个问题:哪些特征在 alpha 很小的时候就已经稳定非零;哪些特征在 alpha 变大时最先被压成零;整体稀疏性在哪个 alpha 区间变化最快。
import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import lasso_path alphas, coefs, _ = lasso_path(X_train, y_train, alphas=np.logspace(-3, 0, 50)) for coef in coefs: plt.plot(np.log10(alphas), coef, linewidth=0.8) plt.xlabel("log(alpha)") plt.ylabel("coefficients") plt.axvline(np.log10(best_alpha), color="red", linestyle="--") plt.show()逻辑说明:lasso_path在一次计算里沿整个 alpha 路径求出全部解,比循环调Lasso快很多;每一行 coefs 对应一个特征在不同 alpha 下的系数。图中横线附近的线说明该特征在大多数 alpha 下都被压缩为零,图中有明显分离的上升线才是真正有解释力的特征。best_alpha 的位置可以来自交叉验证,画上去是为了看它是否落在系数快速变化的区间——如果是,说明模型处于敏感区,alpha 略微波动会导致选入的特征集大变,这种模型不够稳健。
参数说明:alphas 用对数网格覆盖 3 个数量级,保证曲线能展示"全稠密"到"全稀疏"的完整过渡。如果 best_alpha 靠近路径末端,说明你的特征几乎全被压零了,要检查特征工程;如果靠近起始端,说明惩罚太弱,模型接近普通最小二乘。
6.2 稀疏度与残差的双指标验证
选定 alpha 后,我还要做一次双指标验证:计算非零系数数量随 alpha 的变化曲线,以及测试集残差随 alpha 的变化曲线,把两者叠加在同一张图里看。你会发现有时候残差只上升一点,但特征数骤降一半,这时候的 alpha 就是性价比极高的操作点。从那以后我每次跑正则化回归,都会强制走一遍这个路径图加双曲线的流程,不再只看一个 CV 分数就收工;尤其在分布式或大规模特征场景下,这个习惯帮我避开了好几次选了过强惩罚导致业务特征全部丢失的性翻车。希望帮到你。
本文还有配套的精品资源,点击获取