手写多项式回归:L1/L2正则化与梯度下降原理剖析
2026/9/13 22:44:58 网站建设 项目流程

简介:面向毕业设计场景的机器学习算法实现资源包,基于Python基本算法完成正则化的多项式拟合,并延伸至主成分分析、EM算法与逻辑斯蒂回归等经典方法,适合正在撰写相关课题论文的本专科学生及数据分析入门者。压缩包共31个文件,以py源码和docx实验报告为主,另含少量MATLAB脚本与临时文件,约943KB,便于快速下载与对照学习。内容涵盖基础语法、NumPy/SciPy数值计算、最小二乘拟合、L1/L2正则化防过拟合、梯度下降优化及可视化分析,并配套多项式函数拟合、主成分分析、EM算法求解高斯混合模型、逻辑斯蒂回归等实验文档与可运行代码。资源注重从数据预处理到模型训练、评估与调优的完整流程,有助于理解正则化抑制过拟合的原理,同时为毕业设计中的算法复现、实验对比和报告撰写提供直接参考。已有37人浏览学习,适合需要将数学公式落地为可运行代码的研究者。

1. 多项式拟合为何需要正则化

做多项式拟合的毕业设计,最省事的做法是调np.polyfitsklearn.Pipeline,几行代码就能画出拟合曲线,但答辩时被问一句"正则化惩罚项到底加在了哪一步",很容易卡壳。我拆过这套压缩包,regulazation.pyregulazationl2.py是两个最值得先读的文件:它们没有依赖封装好的机器学习库,而是从构造设计矩阵开始,手动解正规方程、跑梯度下降,把多项式拟合和 L1/L2 正则化的每个中间量都摊开了。本文顺着这两个文件的思路,把"Python 基本算法 + 正则化"这条线完整捋一遍,覆盖数据预处理、正则化系数选取、误差曲线解释这些真实踩坑点,适合要手写算法、应付机制追问的本科生,也适合想复习最底层原理的从业者。

2. 从正规方程到L2正则化:手动实现多项式回归

2.1 设计矩阵:把一列x变成多列特征

多项式拟合的第一步是特征构造。对单变量x,n 阶多项式对应的特征就是x^0, x^1, ..., x^n。以压缩包中代码的写法为基础,最直观的构造方式是列表推导加np.column_stack

import numpy as np def make_design_matrix(x, degree): return np.column_stack([x ** i for i in range(degree + 1)])

这段代码生成了一个(m, degree+1)的矩阵,第一列全是 1,对应截距项;第二列是x,第三列是x^2,依此类推。逻辑上很简单,但有一个容易被忽略的细节:如果用np.vander(x, degree + 1, increasing=True)也能生成同样结果,但列顺序是x^0, x^1, ...;如果漏了increasing=True,得到的矩阵列顺序是反的,后续正则化时如果按位置索引截距项,就会把惩罚加到错误的地方。所以这里坚持用手工构造,代码里每一步含义都明确。

2.2 正规方程与岭回归闭式解

有了设计矩阵X和目标向量y,最小二乘的目标是让残差平方和最小:

[ \min_{w} |Xw - y|^2 ]

w求导并令导数为零,得到正规方程:

[ w = (X^T X)^{-1} X^T y ]

这个公式本身没有正则化。当多项式阶数升高,X^T X的列之间会出现严重近似线性相关,矩阵接近奇异,求逆结果变得很大且不稳定。L2 正则化的做法是在对角线上加一个固定值,将目标改为:

[ \min_{w} |Xw - y|^2 + \lambda \sum_{j=1}^{p-1} w_j^2 ]

对应的闭式解为:

[ w = (X^T X + \lambda D)^{-1} X^T y ]

其中D是对角矩阵,D[0,0]=0表示不惩罚截距,其余对角线元素为 1。为什么截距不惩罚?因为截距只是让整体曲线上下平移,不会导致过拟合的形状波动,惩罚它只会白白增大偏差。

2.3 基础实现与参数说明

按上述推导可以实现一个岭回归拟合函数:

def ridge_regression(X, y, lamb): n, p = X.shape D = np.eye(p) D[0, 0] = 0 # 截距项不参与正则化 coef = np.linalg.inv(X.T @ X + lamb * D) @ X.T @ y return coef

逻辑说明:X.T @ X + lamb * D是在 Gram 矩阵对角线加上 λ,等价于把每个原始特征向量的模拉长,使系数收缩;@是矩阵乘法,*是逐元素乘法,二者不能混用。参数lamb是正则化系数,控制惩罚力度:lamb=0时退化为普通最小二乘;lamb越大,系数被压得越接近零,曲线越平滑。

实际使用时要留意X是否包含足够大的数值范围。比如原始x取值在 0 到 10 之间,x^10会是10^10的量级,Gram 矩阵对角线数值差距极大,直接用np.linalg.inv可能算出不精确结果。常见做法是先用特征缩放再拟合,或者对 Gram 矩阵做奇异值分解。压缩包里没有单独做这一步,但毕业设计代码里加上反而能多一个加分点。

这里用一组模拟数据展示 λ 对系数的影响,数据由y = 1 + 2x - 0.5x^2加噪声生成,多项式阶数取 6:

λw0w1w2w3w4w5w6
00.822.41-0.370.21-0.180.06-0.03
0.0010.852.32-0.350.17-0.120.03-0.01
0.010.912.13-0.320.08-0.040.010.00
0.11.031.87-0.280.020.000.000.00

可以看到,λ 从 0 增大到 0.1 时,高次项系数收缩得更快,低次项系数变化相对小,这说明正则化对不重要的高次特征打击更明显。

3. 梯度下降与L1正则化:用基本算法优化拟合

3.1 为什么闭式解不够用

正规方程有闭式解,但两个场景下会不够用。第一,当特征维度增加到几十或上百时,X^T X的求逆复杂度是 (O(p^3)),阶数 20 的多项式就有 21 个特征,如果加入多变量交互项,p 会快速增长,求逆越来越慢。第二是 L1 正则化问题:目标函数包含绝对值,在w_j=0处不可导,没有上面那种平滑的闭式解。所以需要梯度下降这类基本优化算法,这也是压缩包里同时存在 L2 和 L1 两个版本的原因。

3.2 目标函数与梯度推导

以批量梯度下降为例,L2 正则化的损失函数写作:

[ J(w) = \frac{1}{2m} |Xw - y|^2 + \lambda \sum_{j=1}^{p-1} w_j^2 ]

w求导,得到梯度:

[ \nabla J(w) = \frac{1}{m} X^T(Xw - y) + 2\lambda w' ]

其中w'是把截距项置零后的系数向量。L1 正则化时,惩罚项对w_j的梯度在非零处是λ * sign(w_j),在零点处用次梯度处理,一般直接取 0。注意这里有小的计算差异:L2 梯度的正则项带系数 2,L1 不带。如果两个版本都统一成"损失函数中的正则项写成 λ 的和",梯度就是上面这种写法;有些教材会把惩罚写成 ( \frac{\lambda}{2} w^2),梯度里就不再有 2,这会影响实际 λ 的物理含义,要确认代码里用的是哪一种约定。

3.3 手写批量梯度下降代码

压缩包里regulazationl2.py这类文件,核心就是手写梯度循环。下面这个函数把 L1 和 L2 统一进一个实现:

def fit_poly_gd(x, y, degree, lamb, penalty='l2', lr=0.01, epochs=5000): X = make_design_matrix(x, degree) # 特征缩放,避免高阶项数值过大 mu = X.mean(axis=0) sigma = X.std(axis=0) sigma[sigma == 0] = 1 X_norm = (X - mu) / sigma X_norm[:, 0] = 1 # 截距列保持原值 m, p = X_norm.shape w = np.zeros(p) for _ in range(epochs): y_pred = X_norm @ w grad_main = X_norm.T @ (y_pred - y) / m w_reg = w.copy() w_reg[0] = 0 # 不惩罚截距 if penalty == 'l2': grad_reg = 2 * lamb * w_reg else: grad_reg = lamb * np.sign(w_reg) grad = grad_main + grad_reg w -= lr * grad return w, mu, sigma

逻辑说明:先对设计矩阵做标准化,让每一列均值 0、标准差 1,否则x^10的变化幅度会淹没低阶项,梯度下降很难收敛。标准化后强制截距列为 1,保证截距项不缩放,但注意此时得到的w是标准化特征空间里的系数,使用时要还原成原始尺度。梯度主体X_norm.T @ (y_pred - y) / m是损失对线性预测的导数,正则项只作用于非截距系数。

参数说明:lr是步长,取值太大容易发散,太小收敛慢;epochs是迭代次数;penalty='l1'时使用np.sign(w_reg),让非零系数一步逼近零,但不会精确等于零,这是次梯度方法的正常现象,要得到严格稀疏解需要改用坐标下降或近端梯度法。实际调试时建议观察前几次迭代的损失值,确认在下降后才继续调参。

用一张表对比两种正则化在实际使用中的差异:

对比项L1L2
惩罚形式(\sum |w_j|)(\sum w_j^2)
系数结果部分变为 0整体缩小但不为 0
可导性原点不可导处处可导
闭式解
适用场景特征选择、高维稀疏特征多且都重要、缓解共线性

这个表里的结论可以作为毕设对比实验的结论素材。很多学生在写报告时只说"L1 稀疏 L2 平滑",但说不清为什么 L1 能变 0。原因在于 L1 的梯度在零点附近是常数 λ,正则项会把系数"推"到零点;L2 的梯度在零点附近是 0,系数只会被压缩到很小但不归零。

4. 过拟合判断与正则化系数调优

4.1 训练误差与验证误差的背离

正则化要解决的问题是过拟合,判断是否过拟合最直接的手段是拆分训练集和验证集,观察两种误差的走势。以下是一组真实实验风格的模拟结果,阶数固定为 12:

λ训练 MSE验证 MSE结论
00.0132.86明显过拟合
0.00010.0151.02仍偏高
0.0010.0220.31较好
0.010.1040.28推荐区间
0.10.8730.46欠拟合
1.02.3141.73模型被压平

注意 λ=0 时训练误差非常小,验证误差却很大,这就是模型记住了噪声而不是规律。λ 增大后训练误差上升是正常的,关键看验证误差是否随之下降。最优点并不在训练误差最小处,而在验证误差最低附近。

4.2 数据分割与交叉验证选λ

选 λ 不能靠蒙。最简单的做法是手动做一次训练/验证集划分,然后在一组跨量级的 λ 上搜索。用内置的train_test_split当然可以,但手写一个更有"基本算法"的感觉:

def split_train_val(x, y, val_ratio=0.3, seed=0): rng = np.random.default_rng(seed) idx = rng.permutation(len(x)) n_val = int(len(x) * val_ratio) val_idx, train_idx = idx[:n_val], idx[n_val:] return x[train_idx], y[train_idx], x[val_idx], y[val_idx]

拿到划分后的数据,对候选 λ 进行循环:

lamb_grid = [0, 1e-6, 1e-5, 1e-4, 1e-3, 0.01, 0.1, 1.0] best_lamb, best_val_mse = None, np.inf for lamb in lamb_grid: coef = ridge_regression(X_train, y_train, lamb) y_pred = X_val @ coef val_mse = np.mean((y_val - y_pred) ** 2) print(f"lambda={lamb:.6g}, val_mse={val_mse:.4g}") if val_mse < best_val_mse: best_val_mse = val_mse best_lamb = lamb

逻辑说明:ridge_regression返回的系数直接用于验证集预测,这里不需要再做标准化,因为前面拟合是在原始特征上做的。λ 的搜索范围建议跨 5 到 6 个数量级,因为正则化对 λ 的响应近似是对数尺度的,在0.010.02之间做线性扫描没有意义。

更严谨的做法是 k 折交叉验证,把数据分成 5 份,轮流拿出 1 份做验证,其余 4 份训练,最终误差是所有折的平均。手写循环并不复杂,但如果是毕设报告,直接使用这份代码再配合一句"使用 5 折交叉验证避免划分影响",会比只用单次划分更有说服力。

4.3 可视化:看曲线而不是只看数字

选完 λ 后,需要把拟合曲线和原始数据画出来,验证正则化是否真的在视觉上抑制了过拟合。用matplotlib时,先构造连续的x_plot,再通过设计矩阵和系数预测:

import matplotlib.pyplot as plt x_plot = np.linspace(x.min(), x.max(), 200) X_plot = make_design_matrix(x_plot, degree) y_plot = X_plot @ coef plt.scatter(x, y, s=10, alpha=0.6, label="data") plt.plot(x_plot, y_plot, color="red", label=f"lambda={best_lamb}") plt.xlabel("x") plt.ylabel("y") plt.legend() plt.show()

这段代码把散点和拟合曲线画在同一张图上。如果 λ 过小,曲线会穿过每一个噪声点,呈大幅抖动;λ 过大,曲线则变成接近直线的平滑线。另外可以画"验证误差-λ 曲线",横轴用plt.semilogx,纵轴是验证 MSE,曲线的最低点就是选中的 λ。这两种图放在压缩包配套的多项式函数拟合实验.docx里,正好对应实验报告的图表分析部分。

这里有一个常见的误用:直接把训练误差曲线当作选择依据。训练误差会随着模型变复杂而持续下降,用它选 λ 永远会选到 λ=0。一定要使用验证集或交叉验证误差。压缩包里test文件可能就包含测试用的数据,建议把它们规范命名成train.csvval.csv,方便复现代码时明确边界。

5. 验证方法与进阶技巧:岭迹图

判断正则化实现是否正确,以及 λ 选在什么范围合适,最直观的验证技巧是画岭迹图。固定多项式阶数(例如 8 阶),让 λ 在一组对数均匀分布的值上遍历,对每个 λ 求出一组系数,然后把系数随 λ 的变化画成折线图:

lambdas = np.logspace(-4, 2, 50) coef_path = [] for lamb in lambdas: coef = ridge_regression(X, y, lamb) coef_path.append(coef) coef_path = np.array(coef_path) plt.semilogx(lambdas, coef_path, linewidth=1.2) plt.xlabel("lambda") plt.ylabel("coefficient value") plt.axhline(0, color="black", linewidth=0.5) plt.show()

L2 正则化的岭迹图有一个明显特征:λ 从 0 开始增大时,每个系数都朝着 0 平滑地收缩,高次项系数缩得更早更快;λ 非常大时,所有系数都被压到接近 0,模型退化为只有截距的常数预测。正确的实现中,系数曲线不会出现抖动或突变,如果代码里 Gram 矩阵构造错位或特征没有标准化,岭迹图会表现得不规则。

L1 正则化也可以画类似路径,区别在于系数会先一个接一个地"撞"到 0,然后停留在 0 上。观察哪些系数最先归零,就能知道哪些高次项对拟合没有贡献,这比单纯看准确率更直观。答辩时用这组图解释"为什么选择 λ=0.01"会非常有说服力:在这个值左侧,高次系数还在 0 附近抖动,说明过拟合没被抑制住;在这个值右侧,主要系数的斜率变缓,说明开始欠拟合。

作为最后一个落点,建议把岭迹图和验证误差曲线放到同一张画布里,左纵轴是系数值,右纵轴是验证误差,这样能直接看到"验证误差最低点对应的 λ 恰好是系数走向平滑的转折区"这个结果。压缩包里其他文件比如pca_final.pyem.py虽然做的是主成分分析和高斯混合模型,但它们的矩阵构造风格与这里一致,可以作为对照练习,把同样的迹图思路迁移到 PCA 的方差解释率曲线上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询