简介:面向毕业设计场景的机器学习算法实现资源包,基于Python基本算法完成正则化的多项式拟合,并延伸至主成分分析、EM算法与逻辑斯蒂回归等经典方法,适合正在撰写相关课题论文的本专科学生及数据分析入门者。压缩包共31个文件,以py源码和docx实验报告为主,另含少量MATLAB脚本与临时文件,约943KB,便于快速下载与对照学习。内容涵盖基础语法、NumPy/SciPy数值计算、最小二乘拟合、L1/L2正则化防过拟合、梯度下降优化及可视化分析,并配套多项式函数拟合、主成分分析、EM算法求解高斯混合模型、逻辑斯蒂回归等实验文档与可运行代码。资源注重从数据预处理到模型训练、评估与调优的完整流程,有助于理解正则化抑制过拟合的原理,同时为毕业设计中的算法复现、实验对比和报告撰写提供直接参考。已有37人浏览学习,适合需要将数学公式落地为可运行代码的研究者。
1. 多项式拟合为何需要正则化
做多项式拟合的毕业设计,最省事的做法是调np.polyfit或sklearn.Pipeline,几行代码就能画出拟合曲线,但答辩时被问一句"正则化惩罚项到底加在了哪一步",很容易卡壳。我拆过这套压缩包,regulazation.py和regulazationl2.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:
| λ | w0 | w1 | w2 | w3 | w4 | w5 | w6 |
|---|---|---|---|---|---|---|---|
| 0 | 0.82 | 2.41 | -0.37 | 0.21 | -0.18 | 0.06 | -0.03 |
| 0.001 | 0.85 | 2.32 | -0.35 | 0.17 | -0.12 | 0.03 | -0.01 |
| 0.01 | 0.91 | 2.13 | -0.32 | 0.08 | -0.04 | 0.01 | 0.00 |
| 0.1 | 1.03 | 1.87 | -0.28 | 0.02 | 0.00 | 0.00 | 0.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),让非零系数一步逼近零,但不会精确等于零,这是次梯度方法的正常现象,要得到严格稀疏解需要改用坐标下降或近端梯度法。实际调试时建议观察前几次迭代的损失值,确认在下降后才继续调参。
用一张表对比两种正则化在实际使用中的差异:
| 对比项 | L1 | L2 |
|---|---|---|
| 惩罚形式 | (\sum |w_j|) | (\sum w_j^2) |
| 系数结果 | 部分变为 0 | 整体缩小但不为 0 |
| 可导性 | 原点不可导 | 处处可导 |
| 闭式解 | 无 | 有 |
| 适用场景 | 特征选择、高维稀疏 | 特征多且都重要、缓解共线性 |
这个表里的结论可以作为毕设对比实验的结论素材。很多学生在写报告时只说"L1 稀疏 L2 平滑",但说不清为什么 L1 能变 0。原因在于 L1 的梯度在零点附近是常数 λ,正则项会把系数"推"到零点;L2 的梯度在零点附近是 0,系数只会被压缩到很小但不归零。
4. 过拟合判断与正则化系数调优
4.1 训练误差与验证误差的背离
正则化要解决的问题是过拟合,判断是否过拟合最直接的手段是拆分训练集和验证集,观察两种误差的走势。以下是一组真实实验风格的模拟结果,阶数固定为 12:
| λ | 训练 MSE | 验证 MSE | 结论 |
|---|---|---|---|
| 0 | 0.013 | 2.86 | 明显过拟合 |
| 0.0001 | 0.015 | 1.02 | 仍偏高 |
| 0.001 | 0.022 | 0.31 | 较好 |
| 0.01 | 0.104 | 0.28 | 推荐区间 |
| 0.1 | 0.873 | 0.46 | 欠拟合 |
| 1.0 | 2.314 | 1.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.01到0.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.csv和val.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.py和em.py虽然做的是主成分分析和高斯混合模型,但它们的矩阵构造风格与这里一致,可以作为对照练习,把同样的迹图思路迁移到 PCA 的方差解释率曲线上。
本文还有配套的精品资源,点击获取