简介这是一份面向本科毕业设计场景的Python数据分析实践资料围绕多项式拟合中的正则化方法系统演示了从最小二乘拟合到L1/L2正则化、优化算法以及主成分分析等经典内容。压缩包包含31个文件其中10个Python脚本为主要实现代码另有多个文档用于说明实验原理与过程还含少量MATLAB脚本及临时文件整体大小943KB。资源覆盖NumPy/SciPy数值计算、EM算法求解高斯混合模型、逻辑斯蒂回归实验、PCA降维及多项式函数拟合等多个进阶主题并以可视化方式辅助结果分析适合需要理解算法细节、动手复现实验或完成毕业设计的学生参考。目前已有37人学习下载兼具代码与配套文档便于对照查阅和扩展训练。1. 用 Python 做正则化多项式拟合这个压缩包比标题多装了三件套拿到这个标题为「基于 python 基本算法实现正则化的多项式拟合」的压缩包时我原以为里面只有一个拟合脚本解开才发现它是一套完整的机器学习课程设计/毕设实验包除了regulazation.py和regulazationl2.py两份正则化多项式拟合代码还附带了逻辑斯蒂回归lr.py/lrmap.py、PCA 主成分分析pca.py/pca_final.py、EM 算法求解高斯混合模型em.py及两个变体以及四份对应的 docx 实验报告。对正在赶毕设、需要「算法 实验 报告」三件套的本科生来说这份资源最实在的价值是你不用从零搭四套实验先把它跑通、读透关键公式再把数据和参数换成自己的工作量就成了。本文按我拆包的实际顺序把每个模块怎么读、怎么跑、坑在哪一条条讲清楚。2. 正则化多项式拟合实操闭式解、Vandermonde 矩阵与 L1/L2 选择2.1 最小二乘加正则项的闭式解为什么能同时解决「求逆」和「过拟合」多项式拟合的本质是把一维特征x映射到高维特征空间[1, x, x², ..., x^degree]。这列特征构成一个矩阵通常叫 Vandermonde 矩阵代码里最常见的是用np.vander(x, degree1, increasingTrue)或者手写列表推导。有了特征矩阵 Φ拟合问题就变成一个线性回归问题找一组系数 w让Φw尽量接近观测值 y。不加正则的最小二乘目标是||Φw - y||²直接把导数置零得到w (ΦᵀΦ)⁻¹Φᵀy。但这个式子有两个隐患一是当 degree 很高、特征列接近线性相关时ΦᵀΦ可能是奇异矩阵求逆直接报LinAlgError二是模型会拼命穿过每一个数据点曲线剧烈摆动也就是过拟合。正则化的做法是在目标函数里加一个惩罚项L2 形式写成||Φw - y||² λ||w||²闭式解变成import numpy as np def make_features(x, degree): # 把一维 x 扩展成 [1, x, x^2, ..., x^degree] 的多项式特征矩阵 cols [x ** i for i in range(degree 1)] return np.column_stack(cols) def fit_ridge(x, y, degree5, lam1e-3): # L2 正则化多项式拟合闭式解: w (Φ^T Φ λI)^-1 Φ^T y Phi make_features(x, degree) n_features Phi.shape[1] # 对角阵上每个位置都乘 λ但第一个列是常数项通常不惩罚截距 reg lam * np.eye(n_features) reg[0, 0] 0 # solve 比 inv 数值更稳因为 (Φ^T Φ λI) 通常是对称正定的 theta np.linalg.solve(Phi.T Phi reg, Phi.T y) return theta这段代码有两个参数值得细说。degree控制模型复杂度5 就是最多拟合 5 次项9 以上曲线开始具备「蛇形走位」的能力lam是正则化系数也是热搜里常说的那个「惩罚力度」——lam1e-6约等于没惩罚lam1e3会把所有系数压到接近 0曲线退化成接近一条平线。reg[0, 0] 0这一行的意思是常数项截距不参与惩罚因为截距只负责上下平移不贡献弯曲程度罚它没有意义。2.2 regulazation.py 与 regulazationl2.py两份代码的差异和验证方法压缩包里有两份拟合代码regulazation.py和regulazationl2.py。从命名习惯看前者应该是一个带通用正则项的实现内部可能预留了切换l1/l2的开关后者是 L2 特化版本也就是上面这种岭回归写法。拆这种老项目有个规律同一份实验会保留「草稿版」和「整理版」草稿版往往注释少、变量名短、还带调试输出整理版结构更干净。你拿到手之后最稳的做法是先跑一遍对比两份代码打印的拟合系数是否一致。跑之前先确认 Python 环境。这份代码年代可能较早建议用 Python 3.8 左右的解释器pip install numpy matplotlib就够了。如果运行报SyntaxError: invalid syntax基本可以断定是 Python 2 的老写法比如print w没有括号这类问题后面避坑章专门讲。跑通之后你会看到一条拟合曲线和一组系数改degree和lam重新运行观察曲线从「过拟合的剧烈摆动」到「平滑」再到「欠拟合的平直」的渐变过程这就是多项式拟合实验最核心的肉眼结论。L1 和 L2 怎么选是这份毕设里必被问到的点两者差别用一张表说清对比项L1 正则化LassoL2 正则化Ridge目标函数闭式解不存在需迭代求解存在(ΦᵀΦλI)⁻¹Φᵀy系数效果部分系数精确置 0产生稀疏解系数整体压缩但不归零适合场景特征多、想自动筛选特征特征相关性高、抑制过拟合包内对应未单独提供完整版本regulazationl2.py热搜里那句「软阈值算子为什么是 L1 正则化的解」指的就是 L1 没有解析解只能靠迭代逼近而每一步迭代的核心就是软阈值算子w soft_threshold(z, λ)把绝对值小于 λ 的系数直接清零。如果你在毕设里想对比 L1/L2需要自己补一个 ISTA 迭代实现这个包只覆盖了 L2。另外务必区分「正则化」和「归一化」归一化改的是输入特征的尺度比如缩放到 0-1正则化罚的是模型参数的范数两者解决的是完全不同的问题答辩时被问到别混为一谈。3. 逻辑斯蒂回归与 PCAlr.py 的梯度下降、lrmap 特征映射和 pca_final.py 的降维链3.1 lr.py 的梯度下降从 sigmoid 到参数更新的完整实现逻辑斯蒂回归在压缩包里对应lr.py和lrmap.py。它解决的是分类问题核心是把线性回归的输出塞进 sigmoid 函数压到 0 到 1 之间当概率用。损失函数选交叉熵而不是均方误差一个直接原因是交叉熵配合 sigmoid 的梯度表达式非常干净梯度 Xᵀ(p - y) / n也就是预测概率和真实标签的残差直接乘上特征矩阵转置实现起来只有几行def sigmoid(z): # 防止 exp 溢出数值稳定处理 z np.clip(z, -500, 500) return 1.0 / (1.0 np.exp(-z)) def train_lr(X, y, lr0.05, epochs5000): # 第一列补 1作为截距对应的伪特征 X np.column_stack([np.ones(X.shape[0]), X]) w np.zeros(X.shape[1]) for i in range(epochs): pred sigmoid(X w) grad X.T (pred - y) / X.shape[0] w - lr * grad if i % 500 0: # 交叉熵损失加一个极小量防止 log(0) loss -np.mean(y * np.log(pred 1e-12) (1 - y) * np.log(1 - pred 1e-12)) print(fepoch {i}: loss {loss:.4f}) return w三个参数决定训练成败。lr学习率设 0.05 是一个比较稳的起点太大会发散、loss 变成 nan太小要跑几万轮才收敛epochs设 5000 够小数据集用pred 1e-12这种「加极小量防 log 爆炸」是数值计算里的常规操作老代码里没有它遇到loss nan自己要能补上。训练结束后预测阶段就是pred sigmoid(X w)然后以 0.5 为阈值判类别这属于「训练-预测」的标准三段式。3.2 lrmap.py 特征映射让线性分类器画出非线性边界lr.py单独用只能画直线决策边界lrmap.py的存在就是打破这个限制。它的思路和多项式拟合如出一辙把原始两列特征(x1, x2)映射成多项式组合特征比如x1²、x2²、x1*x2等等在高维特征空间里再做线性分类映射回原始空间就是一条弯曲的决策边界。典型的实现def map_features(x1, x2, degree6): # 生成多项式组合特征模仿课程作业里的 mapFeature out [np.ones_like(x1)] for i in range(1, degree 1): for j in range(0, i 1): out.append((x1 ** (i - j)) * (x2 ** j)) return np.column_stack(out)这个函数有个特点特征数量随 degree 平方级增长degree6 时特征数已经到 28 列degree 再往上很容易过拟合。所以 lrmap 通常和正则化搭配使用——这也解释了为什么这个毕设包里多项式拟合和逻辑斯蒂回归会出现在一起它们共用同一套「高维映射 正则化防过拟合」的思想。如果lr.py里给梯度下降也加了 L2 惩罚项你会在更新公式里看到w - lr * (grad lam * w)这种写法这是把正则化从多项式拟合延伸到分类问题的直接证据。3.3 pca_final.py 降维链中心化、协方差矩阵、特征值分解PCA 在这个包里负责把高维特征投影到低维做可视化。比如lrmap生成的 28 维特征没法直接画图用 PCA 降到 2 维就能看分布。完整流程是四步对数据做中心化减均值、算协方差矩阵、对协方差矩阵做特征值分解、按特征值大小取前 k 个特征向量做投影。pca_final.py里的核心逻辑对应如下def pca(X, k): # 1. 中心化每个特征减去均值让数据中心落在原点 X_mean np.mean(X, axis0) X_centered X - X_mean # 2. 协方差矩阵描述特征两两之间的线性相关性 cov np.cov(X_centered.T) # 3. 特征值分解eigh 专门处理对称矩阵比 eig 数值更稳 eigvals, eigvecs np.linalg.eigh(cov) # 4. 特征值从大到小排序取前 k 个特征向量构成投影矩阵 idx np.argsort(eigvals)[::-1][:k] W eigvecs[:, idx] return X_centered W, eigvals[idx]这里最容易踩的坑是顺序必须先中心化再算协方差顺序反了协方差矩阵里会混入均值项投影结果完全不对。np.linalg.eigh只适用于对称矩阵但协方差矩阵天然对称半正定用 eigh 比通用 eig 快且稳。降维后保留的维度 k 不是拍脑袋定的要看方差解释率前 k 个特征值之和除以全部特征值之和保留到累计解释率超过 95% 就够用了。pca.py和pca_final.py的差异大概率就在这——前者只做投影后者补了方差解释率和可视化散点图读的时候以 final 版为主。4. EM 算法求解高斯混合em.py 的 E 步 M 步与两个变体文件的差异4.1 E 步计算每个样本属于每个高斯分量的后验概率高斯混合模型GMM解决的是无监督聚类问题假设数据由 K 个高斯分布混合生成每个样本不知道自己是哪个分量生成的EM 算法就是在这种「数据不完整」的情况下交替估计参数。em.py里的实现分 E 步和 M 步两段。E 步做的是固定当前参数计算每个样本属于第 k 个高斯分量的后验概率概率密度那一步绕不开高斯分布公式代码大概长这样def gaussian_pdf(X, mu, sigma): # 多元高斯概率密度sigma 是协方差矩阵 d X.shape[1] diff X - mu inv_sigma np.linalg.inv(sigma) norm_const 1.0 / ((2 * np.pi) ** (d / 2) * np.sqrt(np.linalg.det(sigma))) return norm_const * np.exp(-0.5 * np.sum(diff inv_sigma * diff, axis1)) def e_step(X, mu, sigma, pi, K): # 计算 N x K 的后验概率矩阵每行代表一个样本属于各分量的概率 n X.shape[0] gamma np.zeros((n, K)) for k in range(K): gamma[:, k] pi[k] * gaussian_pdf(X, mu[k], sigma[k]) # 按行归一化保证每个样本的后验概率之和为 1 gamma gamma / gamma.sum(axis1, keepdimsTrue) return gammaE 步两个细节值得注意。gamma矩阵是 EM 的灵魂第(i, k)个元素就是第 i 个样本属于第 k 个分量的概率M 步所有更新都用它做权重最后一行按行归一化必须加keepdimsTrue否则广播维度不匹配会直接 shape 报错。如果这批数据在 E 步里算概率密度时sigma是奇异矩阵说明这个分量的协方差矩阵退化比如某个分量只有两三个样本后面避坑章会专门讲。4.2 M 步用后验概率做加权平均更新均值、协方差和混合系数M 步的逻辑相当直观把后验概率当权重对每个分量做加权平均。均值是后验加权的样本均值协方差是后验加权的样本散度混合系数是每个分量分到的「有效样本数」占比。K 个分量的循环里这种形式化更新很机械但每个量都必须理解后验概率如何参与def m_step(X, gamma, K): n, d X.shape # Nk 是每个分量的有效样本数后验概率之和 Nk gamma.sum(axis0) # 更新均值后验加权的样本均值 mu_new (gamma.T X) / Nk[:, None] # 更新混合系数有效样本数占总样本数的比例 pi_new Nk / n # 更新协方差每个样本对协方差的贡献以后验概率为权重 sigma_new [] for k in range(K): diff X - mu_new[k] sigma_k (gamma[:, k][:, None] * diff).T diff / Nk[k] sigma_new.append(sigma_k) return mu_new, np.array(sigma_new), pi_new参数这里有个天然约束混合系数pi更新后必须满足「和为 1」因为Nk之和等于样本总数 npi_new Nk / n自动满足这个条件。协方差更新里gamma[:, k][:, None]把一维概率转成列向量是为了让广播乘法作用到每一维特征上少了这步维度会错。EM 的终止条件通常看对数似然的变化量连续两轮变化小于阈值就停em.py里如果画了迭代曲线你会看到典型的前几轮提升大、后面趋平的特征。4.3 emsolute.py 与 emsamesigma.py变体改了什么、什么时候用包里有三个 EM 相关文件em.py、emsolute.py、emsamesigma.py。从命名习惯推测这类实验包里常见的套路是「基础版 变体版」emsamesigma.py应该是「same sigma」的缩写即假设所有 K 个高斯分量共享同一个协方差矩阵把参数数量从 K 个协方差压缩到 1 个模型更简单、收敛更稳定特别适合分量形状差异不大的数据emsolute.py从名字看带有「absolute」的痕迹很可能是改用了绝对值形式的误差度量或者对协方差做了某种绝对值约束这类变体通常是为对比实验准备的。验证变体差异有一个通用方法分别运行三个脚本打印每一轮的 log-likelihood 或 loss 值拉到一起画曲线。如果三者收敛速度不同、最终似然不同差异点自然就暴露了。读这份代码时不用纠结每个变量名抓住「E 步算后验概率、M 步更新参数」这个框架变体只是改其中一步的公式。gonge.m是一个 MATLAB 脚本文件名疑似「共轭」相关和 Python 主体无关运行环境不一致就直接忽略不影响流程。5. 常见问题与排查从文件噪声到矩阵奇异的实操记录5.1 解压后一堆~和.tmp结尾的文件不知道跑哪个现象解压出来lr.py~、em.py~、~EDF9F.tmp、test~混在一起README也没有直接懵了。原因~结尾是 vim 编辑器自动生成的备份文件.tmp是 Word 或系统临时文件它们不是项目内容只是使用痕迹解压工具把这些噪声文件全带出来了。解决只认以.py结尾且后面没有~的脚本以及四份.docx实验报告其他全部删除。动手之前先整个目录复制一份留底毕竟老项目里删错代码是家常便饭。gonge.m是 MATLAB 文件跟 Python 环境无关可以单独放一边。5.2LinAlgError: Singular matrix求逆报错或系数爆炸现象跑多项式拟合时np.linalg.inv(Phi.T Phi)这一行直接抛异常有时不报错但拟合系数达到 10 的 8 次方这种离谱量级。原因degree设得过高比如 15 以上x的幂次之间相关性极强ΦᵀΦ接近奇异矩阵行列式趋近 0求逆数值上崩了。解决把inv换成np.linalg.solve求解线性方程组比显式求逆更稳定更根本的方案是加正则项ΦᵀΦ λI这个加进去之后矩阵变成严格正定奇异性问题直接消失。这也是为什么正则化代码能顺带解决求逆数值问题——你看到的λ不只是防过拟合它还在数学上给矩阵打了「补丁」。5.3overflow encountered in expsigmoid 或高斯概率密度爆掉现象训练逻辑斯蒂回归时控制台飘红warning之后 loss 打印成nan训练直接废掉。原因特征值很大时z可能到几百上千np.exp(-z)在 z 为负数时正好溢出或者np.exp(z)在 z 为正数时上溢。老代码里经常没有防护这是典型的教学代码盲区。解决给z做 clip截断到[-500, 500]就够稳定更优雅的写法是sigmoid里对正负 z 分别处理但这个包里用 clip 已经足够。改完重启训练观察 loss 是否单调下降且最终收敛到 0.1~0.5 量级二分类交叉熵的合理区间。5.4 直接跑.py报语法错误print后面没括号现象python regulazation.py报SyntaxError: Missing parentheses in call to print或者除法结果明显不对。原因代码是 Python 2 时代写的print w是语句不是函数5 / 2默认整数除法结果是 2 而不是 2.5。解决确认本机解释器版本建议用 Python 3.8 以上跑如果不想改代码可以在文件头部加from __future__ import print_function和from __future__ import division但最稳妥的做法还是把老式print x改成print(x)一处一处替换花不了几分钟。遇到/除法问题检查是不是需要改成//或保留其中一操作数为浮点。5.5 中文文档打开乱码或编码报错现象四份.docx报告双击打开正常但如果有人用脚本解析文档或者.py文件顶部有中文注释在 Windows 命令行直接跑会报UnicodeDecodeError。原因Python 2 时代源码默认 ASCII 编码含中文字符串时必须声明# -*- coding: utf-8 -*-Python 3 默认 UTF-8老代码的声明没删也没事但如果脚本被转成了 GBK 编码再读就会冲突。解决.py文件用 VS Code 打开后检查右下角编码统一改成 UTF-8命令行运行前设置PYTHONIOENCODINGutf-8Windows 下有效。docx报告本身是文档不具备可执行问题但建议保存一份 PDF 版毕设提交时防止格式漂移——这是我带实验课攒下的经验。6. 毕设验证技巧把正则化系数从手填参数变成一条实验曲线毕设答辩时验证部分最有说服力的不是「我调了一个不错的 λ」而是「我系统性地扫了 λ找到了泛化误差最小的区间」。具体做法是固定多项式阶数degree9让正则化系数 λ 在对数坐标上取 30 个值比如np.logspace(-6, 3, 30)对每个 λ 分别做训练记录训练集和验证集上的均方误差然后画两条误差随 λ 变化的曲线。训练误差单调下降、验证误差先降后升两条线的交叉点附近就是 λ 的最佳取值区间。这个交叉点不是巧合它恰恰对应模型从过拟合λ 太小、验证误差高到欠拟合λ 太大、训练验证都差的临界状态。def evaluate_lambda(x_train, y_train, x_val, y_val, degree9): lams np.logspace(-6, 3, 30) train_err, val_err [], [] for lam in lams: theta fit_ridge(x_train, y_train, degreedegree, lamlam) train_err.append(mse(x_train, y_train, theta)) val_err.append(mse(x_val, y_val, theta)) # lams 取对数坐标画图观察训练/验证误差交叉点附近的 λ 区间 return lams, train_err, val_err把这个函数跑通后你手里就多了一张「正则化系数-误差曲线」图配合原有的拟合曲线对比图实验部分就有了三层递进第一层是不同 degree 下拟合曲线的过拟合现象第二层是同一 degree 下不同 λ 的平滑效果第三层是 λ 的系统性扫描和最佳区间。这套逻辑多项式拟合、逻辑斯蒂回归、EM 三个实验可以复用答辩时主问你「为什么选这个 λ」就不再是被动解释而是主动展示曲线拐点。数据少的话用交叉验证代替固定验证集思路一样。从那以后我每次跑这类拟合实验都会强制把 λ 扫一遍而不是拍脑袋定参数这个习惯救了我好多次「系数爆炸」的翻车现场。希望帮到你。本文还有配套的精品资源点击获取