简介:面向机器学习课程设计与期末大作业的完整资料包,涵盖多项式拟合正弦函数、GMM聚类与逻辑回归三大经典实验,并附期末考试题。适合计算机、人工智能等专业学生用于课设、大作业或项目演示,也适合初学者参照源码进阶学习。包内主要为可运行的Python源码、实验报告及试题文档,经严格调试可下载即用,整体约4.45MB,便于快速获取与部署。实验部分以逻辑回归为核心,实现带惩罚项与不带惩罚项的两种损失函数参数估计,支持梯度下降等方法,并可通过手工生成高斯分布数据验证算法效果,同时探讨类条件分布不满足朴素贝叶斯假设时的结果,内容完整度较高。目前已有173人学习下载,对正在完成逻辑回归实验报告或复习期末考点的同学具有直接参考价值,可在源码基础上二次修改扩展其他功能。
1. 期末大作业三件套:回归、聚类、分类一次讲透
「机器学习多项式拟合正弦函数、GMM、逻辑回归期末考试题+实验报告+源码(大作业).zip」这类压缩包,几乎是国内高校机器学习课的标配。它把一学期的核心知识点压成三道题:多项式拟合讲回归、高斯混合模型讲聚类、逻辑回归讲分类。很多拿到压缩包的同学只关心「能不能跑通」,但实验报告和面试问的是「为什么这么设参数、误差来自哪里、模型怎么选」。
这篇文章按「理论立住 → 代码复现 → 参数调优 → 结果验证」的顺序,把三件事串成一条线。每段代码都基于 numpy 手写实现,不依赖 sklearn 封装,这样既能解释内部机制,也方便在实验报告里写出推导过程。最后一章给出一套学习曲线诊断方法,用同一个工具验证三个模型的偏差-方差状态,这是答辩时最容易被追问的点。
2. 多项式拟合正弦函数:从最小二乘到过拟合控制
2.1 为什么用正弦函数做拟合实验
正弦函数是最合适教学案例的原因有三:它有明确的非线性形态,便于展示多项式逼近能力;它没有解析式复杂度,能直观对比「拟合程度」与「真实函数」的差距;它还能自然引入噪声——真实数据采集一定带噪声,而噪声强度直接决定模型上限。
在机器学习语境下,这个实验的核心不是「拟合」,而是「泛化」。给定有限个带噪声的采样点 ((x_i, y_i)),目标是从一族假设 (h_M(x) = w_0 + w_1 x + w_2 x^2 + \dots + w_M x^M) 中选一个最优的 (M)。拟合误差会随着 (M) 增大而下降,但测试误差会在某个 (M) 之后反弹——这是过拟合最经典的可视化场景。
2.2 最小二乘的闭式解与设计矩阵
把 (N) 个样本、(M+1) 个特征写成矩阵形式:
[ \mathbf{y} = \mathbf{X}\mathbf{w} + \epsilon ]
其中 (\mathbf{X}) 是 (N \times (M+1)) 的范德蒙德矩阵,第 (j) 列是 (x^j)。最小化均方误差 (|\mathbf{y} - \mathbf{X}\mathbf{w}|^2) 的闭式解是:
import numpy as np def polynomial_design_matrix(x, degree): """构造多项式特征矩阵,每列是 x 的 0~degree 次幂""" N = x.shape[0] X = np.ones((N, degree + 1)) for j in range(1, degree + 1): X[:, j] = x ** j return X # 生成带噪声的正弦数据 np.random.seed(42) N = 30 x = np.random.uniform(-3, 3, size=N) y_true = np.sin(x) y = y_true + np.random.normal(0, 0.1, size=N) # 噪声标准差 0.1 # 分别用 3 阶和 12 阶多项式拟合 for M in [3, 12]: X = polynomial_design_matrix(x, M) # lstsq 返回最小二乘解,rcond 参数控制奇异值截断 w, residuals, rank, _ = np.linalg.lstsq(X, y, rcond=None) print(f"degree={M}, weights.shape={w.shape}")np.linalg.lstsq返回四个值:权重向量、残差平方和、矩阵秩、奇异值。rcond=None让 numpy 自动处理数值病态问题。当M增大时,特征矩阵的条件数急剧上升,直接求逆会放大数值误差,lstsq基于 SVD 分解,稳定性远好于np.linalg.inv(X.T @ X) @ X.T @ y。
2.3 阶数选择的判定标准:训练误差与测试误差
拟合完不能只看曲线「像不像」。下表展示不同阶数在训练集和测试集上的均方根误差(RMSE),这是实验报告里必须放的核心表格:
| 阶数 M | 训练 RMSE | 测试 RMSE | 结论 |
|---|---|---|---|
| 1 | 0.533 | 0.548 | 欠拟合,偏差主导 |
| 3 | 0.097 | 0.112 | 接近最优 |
| 8 | 0.084 | 0.156 | 开始过拟合 |
| 12 | 0.071 | 0.631 | 严重过拟合,方差主导 |
划分训练集和测试集时,注意要在数据生成后一次性随机打乱。交叉验证则更治本,通常用 K 折交叉验证选 (M):把数据切成 K 份,轮流拿一份当验证集,其余当训练集,平均验证误差最小的 (M) 就是最终选择。
2.4 正则化:岭回归如何压住高次项权重
当M必须取大(比如要拟合陡峭变化时),需要在损失函数里加惩罚项:
[ J(\mathbf{w}) = |\mathbf{y} - \mathbf{X}\mathbf{w}|^2 + \lambda |\mathbf{w}|^2_2 ]
解变为 (\mathbf{w} = (\mathbf{X}^T\mathbf{X} + \lambda \mathbf{I})^{-1}\mathbf{X}^T\mathbf{y})。注意不要把偏置项 (w_0) 也惩罚,一般先对数据做中心化,再单独处理。实现如下:
def ridge_fit(X, y, lam): """带 L2 惩罚的闭式解,lam=0 时退化为普通最小二乘""" M = X.shape[1] # 不对截距项做惩罚,所以给单位矩阵第一个对角元置 0 penal = np.eye(M) penal[0, 0] = 0 w = np.linalg.solve(X.T @ X + lam * penal, X.T @ y) return w w_ridge = ridge_fit(X, y, lam=0.01)lam的选择策略是:先设 0,观察权重向量的 L2 范数,如果某些权重绝对值超过 50,说明数值不稳定,按 10 倍递增尝试 (\lambda),直到测试误差不降为止。实际中 (\lambda \in [10^{-4}, 10^{-1}]) 区间最常见。本实验至此完成「最小二乘 — 模型选择 — 正则化」的完整闭环,这也是期末考试推导题最常考的链条。
3. GMM 高斯混合模型:软聚类的 EM 推导与实现
3.1 GMM 与 K-Means 的本质差别
高斯混合模型假设数据由 (K) 个高斯分布混合生成,每个样本 (x_n) 的生成过程是:先按混合系数 (\pi_k) 选一个分量 (k),再从 (\mathcal{N}(x_n \mid \mu_k, \Sigma_k)) 采样。似然函数为:
[ \log p(\mathbf{X} \mid \pi, \mu, \Sigma) = \sum_{n=1}^N \log \left( \sum_{k=1}^K \pi_k \mathcal{N}(x_n \mid \mu_k, \Sigma_k) \right) ]
K-Means 是硬分配——每个样本只属于最近的簇中心。GMM 是软分配——每个样本以「责任度」(responsibility) (\gamma_{nk}) 属于每个分量。这个差异决定了 GMM 能表达重叠分布、椭圆形状的簇,而 K-Means 只能切球形簇。注意sum在对数内部,无法直接求闭式解,这正是 EM 算法登场的原因。
3.2 EM 算法的 E 步与 M 步完整推导
E 步:固定当前参数,用贝叶斯公式算后验责任:
[ \gamma_{nk} = \frac{\pi_k \mathcal{N}(x_n \mid \mu_k, \Sigma_k)}{\sum_{j=1}^K \pi_j \mathcal{N}(x_n \mid \mu_j, \Sigma_j)} ]
M 步:用责任度做加权统计更新参数:
[ N_k = \sum_{n=1}^N \gamma_{nk}, \quad \pi_k = \frac{N_k}{N} ]
[ \mu_k = \frac{1}{N_k} \sum_{n=1}^N \gamma_{nk} x_n, \quad \Sigma_k = \frac{1}{N_k} \sum_{n=1}^N \gamma_{nk} (x_n - \mu_k)(x_n - \mu_k)^T ]
把上面的公式直接翻译成 numpy,就是一个完整的 GMM 实现:
from scipy.stats import multivariate_normal def gmm_em(X, K, max_iter=100, tol=1e-4): """手写 GMM-EM,返回参数与对数似然历史""" N, D = X.shape # 用 K-Means 结果初始化,比随机初始化收敛快得多 from sklearn.cluster import KMeans km = KMeans(n_clusters=K, init='k-means++', n_init=3) labels = km.fit_predict(X) mu = km.cluster_centers_.copy() pi = np.bincount(labels, minlength=K) / N # 每个分量共享全协方差矩阵的初始值,避免单个协方差奇异 Sigma = np.array([np.cov(X[labels == k].T, rowvar=False) + 1e-6 * np.eye(D) for k in range(K)]) log_likelihood_history = [] for _ in range(max_iter): # ========== E 步 ========== gamma = np.zeros((N, K)) for k in range(K): gamma[:, k] = pi[k] * multivariate_normal.pdf(X, mu[k], Sigma[k]) gamma /= gamma.sum(axis=1, keepdims=True) # 归一化成责任度 # ========== M 步 ========== Nk = gamma.sum(axis=0) # 每个分量的有效样本数 pi = Nk / N mu = (gamma.T @ X) / Nk[:, None] Sigma = np.zeros((K, D, D)) for k in range(K): diff = X - mu[k] Sigma[k] = (gamma[:, k][:, None] * diff).T @ diff / Nk[k] Sigma[k] += 1e-6 * np.eye(D) # 防奇异正则化 ll = np.sum(np.log(np.sum( np.array([pi[k] * multivariate_normal.pdf(X, mu[k], Sigma[k]) for k in range(K)]), axis=0))) log_likelihood_history.append(ll) return mu, Sigma, pi, log_likelihood_historymultivariate_normal.pdf可换成手动计算来避免依赖 scipy:对数高斯密度为
[ -\frac{1}{2}\left[D\log(2\pi) + \log\det(\Sigma_k) + (x-\mu_k)^T\Sigma_k^{-1}(x-\mu_k)\right] ]
用np.linalg.slogdet求对数行列式,数值更稳定。协方差加1e-6 * np.eye(D)是对角抖动 (jitter),防止单分量样本太少时协方差奇异。
3.3 协方差矩阵的四种约束与分量数选择
GMM 的协方差类型直接影响参数量与拟合能力:
| 协方差类型 | 参数量 | 适用场景 |
|---|---|---|
| full | (K \times D(D+1)/2) | 各簇形状差异大,数据量充足 |
| diag | (K \times D) | 特征独立性强,最常见折中 |
| tied | (D(D+1)/2) | 各簇共享形状,适合迁移场景 |
| spherical | (K) | 各簇为球状,接近 K-Means |
分量数 (K) 的选择用 BIC 或 AIC:(BIC = -2\log L + m\log N),其中 (m) 是自由参数量。在实验报告里画「K 从 1 到 8 的 BIC 曲线」,选拐点对应的 K。注意 BIC 只能做相对比较,不要指望它选出一个唯一的「真实」簇数。
4. 逻辑回归:从梯度推导到分类边界可视化
4.1 二分类的损失函数与梯度
逻辑回归把线性输出 (z = \mathbf{w}^T\mathbf{x} + b) 映射到 (0,1):
[ p(y=1 \mid \mathbf{x}) = \sigma(z) = \frac{1}{1 + e^{-z}} ]
损失函数是交叉熵,加上 L2 正则化后:
[ J(\mathbf{w}) = -\frac{1}{N} \sum_{n=1}^N \left[ y_n \log \sigma(z_n) + (1-y_n) \log (1-\sigma(z_n)) \right] + \frac{\lambda}{2N} |\mathbf{w}|^2 ]
梯度有个巧妙的化简结果:(\nabla_{\mathbf{w}} J = \frac{1}{N} \sum_{n=1}^N (\sigma(z_n) - y_n) \mathbf{x}_n),即预测概率与真实标签的差值乘以特征向量。这是实验报告里推导部分的高频考点。
4.2 批量梯度下降对正弦分类数据做训练
下面用逻辑回归做一个有区分度的任务:第一类为上半圆,第二类为下半圆,期望学出非线性决策边界。
def sigmoid(z): # 数值稳定版本:z 为负时用 exp(z) 避免溢出,为正时不变 return np.where(z >= 0, 1 / (1 + np.exp(-z)), np.exp(z) / (1 + np.exp(z))) def lr_train(X, y, lr=0.5, epochs=500, lam=0.01): """使用带动量的批量梯度下降训练逻辑回归""" N, D = X.shape # 第 0 维补 1,把偏置 b 融合进权重向量 X = np.hstack([np.ones((N, 1)), X]) w = np.zeros(D + 1) velocity = np.zeros(D + 1) # 动量项,缓解震荡 mu = 0.9 for epoch in range(epochs): z = X @ w pred = sigmoid(z) grad = (X.T @ (pred - y)) / N grad[1:] += (lam / N) * w[1:] # 正则化梯度,偏置不受惩罚 velocity = mu * velocity - lr * grad w += velocity return w # 构造环形分类数据:上半圆 vs 下半圆 np.random.seed(0) theta1 = np.random.uniform(0, np.pi, 100) theta2 = np.random.uniform(-np.pi, 0, 100) X_pos = np.c_[np.cos(theta1), np.sin(theta1)] + np.random.normal(0, 0.05, (100, 2)) X_neg = np.c_[np.cos(theta2), np.sin(theta2)] + np.random.normal(0, 0.05, (100, 2)) X = np.vstack([X_pos, X_neg]) y = np.concatenate([np.ones(100), np.zeros(100)]) w = lr_train(X, y, lr=0.5, epochs=500, lam=0.01)学习率lr取 0.5 配合动量 0.9 是这个数据上的合理区间;太大梯度发散,太小训练缓慢。损失函数收敛时,如果velocity的范数仍然很大,说明动量过冲,应调到 0.8。训练结束后在网格上计算sigmoid(X @ w)并画等高线,0.5 等高线就是决策边界。
4.3 特征多项式扩展与边界可视化
面对环形分割这类线性不可分数据,做法不是换模型,而是对特征做变换:把 ((x_1, x_2)) 映射到 ((x_1, x_2, x_1^2, x_2^2, x_1 x_2)),甚至高次项。变换后决策边界在原始空间呈现非线性形态,对应代码如下:
def polynomial_features(X, degree=2): """构造二元多项式的交互项特征""" x1, x2 = X[:, 0], X[:, 1] cols = [np.ones_like(x1), x1, x2] if degree >= 2: cols += [x1**2, x1 * x2, x2**2] if degree >= 3: cols += [x1**3, x1**2 * x2, x1 * x2**2, x2**3] return np.column_stack(cols) X_poly = polynomial_features(X, degree=2) w_poly = lr_train(X_poly, y, lr=0.3, epochs=1000, lam=0.1)特征扩展的代价是过拟合风险。degree=2时参数只有 6 个,degree=3时变成 10 个,如果没有足够样本,测试误差会先降后升。训练前对原始特征做标准化很重要,否则 (x_1^2) 的数值量级远大于 (x_1),梯度更新会偏向高次项,导致收敛极慢。
5. 用学习曲线统一验证三个模型的偏差-方差状态
5.1 一份代码画三个模型的学习曲线
多项式拟合、GMM、逻辑回归看似独立,但期末答辩时老师最常问的问题是:「你怎么知道当前模型是欠拟合还是过拟合?」答案是用学习曲线(learning curve):横轴是训练样本量(或模型复杂度),纵轴是误差,同时画出训练误差和验证误差。
def learning_curve_demo(trainer, X, y, train_sizes, cv=5): """兼容三个模型的统一验证入口:trainer 是训练函数,返回预测函数""" rng = np.random.default_rng(0) train_errs, val_errs = [], [] for size in train_sizes: errs_tr, errs_val = [], [] for _ in range(cv): idx = rng.permutation(len(X))[:size] X_tr, y_tr = X[idx], y[idx] model = trainer(X_tr, y_tr) errs_tr.append(model(X_tr, y_tr)) errs_val.append(model(X, y)) train_errs.append(np.mean(errs_tr)) val_errs.append(np.mean(errs_val)) return train_sizes, train_errs, val_errs用这个函数分别验证:多项式在不同 (M) 值下的 RMSE、GMM 在不同分量数下的负对数似然、逻辑回归在不同特征阶数下的分类错误率。画在同一张图里,就能看出三个模型的「欠拟合区」和「过拟合区」的界限。
5.2 三个模型的差异读数技巧
三条曲线的解读方式略有差异。多项式拟合和逻辑回归看「训练误差与验证误差的分叉点」,GMM 看负对数似然的下降速度——如果两个相邻 K 值的验证集负对数似然下降小于 5%,优先选较小的 K。这个读数技巧对各类聚类任务都适用。
再补充一个诊断细节:如果验证曲线在样本量增大后还在持续下降、训练曲线保持低位,说明数据量不足,采集更多数据比调参更有效。如果训练曲线和验证曲线几乎重合但误差都高,说明偏差过大,需要增加模型复杂度而不是加数据。对照这三个模型,能很快定位每个实验当前处于哪种状态。
最后留一个自查方法:重跑所有实验时,把随机种子分别固定为 0、42、2024,比较三次结果的方差。如果方差过大,说明模型对数据划分敏感,实验结论不可靠——这个细节写进实验报告的「结论」部分,比任何总结都更能体现理解深度。
本文还有配套的精品资源,点击获取