news 2026/10/3 14:01:44

武大机器学习与模式识别实验课Python源码拆解:从感知机到神经网络

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
武大机器学习与模式识别实验课Python源码拆解:从感知机到神经网络

简介:这份资源是武汉大学机器学习与模式识别课程的实验配套源码与文档,面向计算机、人工智能、通信、自动化等专业的在校学生及自学者,可用于课程实验、课程设计、毕业设计或项目立项演示。内容覆盖监督学习、无监督学习、神经网络等典型算法,以Python实现为主,并配有说明文档,便于理解算法流程与代码结构。压缩包共337个文件,约60.22MB,其中145个py源码文件构成实验主体,72个yaml配置文件用于参数与环境管理,另有18个md说明文档、9个ipynb交互式笔记,以及png、jpg等图像素材和少量cpp、h等底层代码,整体结构清晰,方便按模块检索学习。目前已有154人学习下载。代码均经过运行测试,读者可参考README快速上手,也可在现有实现上修改扩展,用于课程作业或进一步研究,适合作为机器学习入门与进阶的实践参考。

1. 武大机器学习与模式识别实验课:一份能直接跑通的 Python 源码拆解

如果你正在搜「武汉大学 机器学习 模式识别 Python 实验」,大概率是三种人之一:期末要交作业但不知道从哪下手、想自学但被各种公式劝退、或者已经跑过 sklearn 但说不清底层到底发生了什么。这份实验源码加文档的组合,核心价值不在于代码有多长,而在于它把监督学习、无监督学习、神经网络三条线拆成了可以逐个文件跑通的最小单元。你不需要先啃完周志华那本《机器学习》再动手,反过来,先跑通一个感知机或者 K-Means,再回头看公式,理解速度会快很多。这篇文章不打算复述实验报告,而是把这份源码里最值得复现的部分抽出来,告诉你每个实验在做什么、参数怎么调、哪里最容易翻车,以及这套东西到底值不值得你花时间。

2. 监督学习实验:从感知机到逻辑回归的代码骨架

2.1 为什么先跑感知机而不是直接上 sklearn

很多人第一次接触机器学习,上来就from sklearn.linear_model import LogisticRegression,三行代码出结果,然后呢?不知道权重怎么更新的,不知道学习率设大了会怎样,不知道线性不可分时它为什么永远不收敛。武大这套实验里监督学习部分的第一道坎就是手写感知机,目的不是让你以后都用自己写的,而是让你在调试过程中亲眼看到「权重不更新了」这件事是怎么发生的。

感知机的核心逻辑只有一句话:对每个样本,如果预测错了,就按学习率把权重往正确方向推一点。听起来简单,但真正动手时,第一个坑就是数据没有中心化。如果特征值范围差异很大,比如一个特征在 0 到 1 之间,另一个在 0 到 1000 之间,权重更新会被大数值特征主导,小特征几乎不起作用。常见做法是先做标准化,把每个特征减去均值再除以标准差。

import numpy as np class Perceptron: def __init__(self, lr=0.01, n_iters=1000): self.lr = lr self.n_iters = n_iters self.weights = None self.bias = None def fit(self, X, y): n_samples, n_features = X.shape self.weights = np.zeros(n_features) self.bias = 0 y_ = np.array([1 if i > 0 else -1 for i in y]) for _ in range(self.n_iters): for idx, x_i in enumerate(X): linear_out = np.dot(x_i, self.weights) + self.bias if y_[idx] * linear_out <= 0: self.weights += self.lr * y_[idx] * x_i self.bias += self.lr * y_[idx] return self def predict(self, X): linear_out = np.dot(X, self.weights) + self.bias return np.where(linear_out >= 0, 1, -1)

这段代码里lr是学习率,n_iters是遍历整个数据集的轮数。学习率设 0.01 是比较稳的起点,设 1.0 以上容易在最优解附近震荡,设 0.0001 以下收敛慢到你以为代码卡死了。n_iters不是越大越好,线性可分的数据集通常几十轮就收敛,线性不可分的数据集你跑一万轮也不会停,这时候需要加一个早停条件,比如连续若干轮权重变化小于某个阈值就退出。

逻辑回归和感知机的区别在于,它不只看分类对错,而是输出一个概率。实现上就是把感知机的阶跃函数换成 Sigmoid,损失函数从误分类点到超平面的距离换成交叉熵。实验文档里通常会让你对比两者在同一个数据集上的决策边界,你会发现逻辑回归的边界更「居中」,因为它考虑的是所有样本的似然,而不是只盯着分错的那几个。

2.2 用乳腺癌数据集验证分类器:参数怎么设、结果怎么看

跑通手写版本之后,下一步是拿真实数据验证。武大实验里常用的是 sklearn 自带的乳腺癌数据集,569 个样本,30 个特征,二分类,规模适中,不会让你等太久。加载和预处理的代码如下:

from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report data = load_breast_cancer() X, y = data.data, data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) clf = LogisticRegression(max_iter=5000, C=1.0, solver='lbfgs') clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))

这里有几个参数值得说清楚。stratify=y保证训练集和测试集的类别比例一致,不然随机切分可能让某一类在测试集里特别少,准确率波动很大。max_iter=5000是因为 lbfgs 求解器默认迭代次数在特征多的时候不够用,你会看到 ConvergenceWarning,不是代码错了,是没收敛完。C是正则化强度的倒数,C 越小正则化越强,模型越保守,C 越大越容易过拟合。实验里一般会让你试 C=0.01、0.1、1、10 几个值,画一条准确率随 C 变化的曲线,你会发现 C 太大或太小都会掉点。

分类报告里的 precision 和 recall 比准确率更有信息量。乳腺癌数据里恶性样本如果被漏判,代价比误判良性大得多,所以 recall 比 precision 更值得关注。实验文档里通常会让你调整分类阈值,默认是 0.5,你可以改成 0.3,看看 recall 怎么变、precision 怎么变,这就是所谓的 ROC 曲线背后的逻辑。

3. 无监督学习实验:K-Means 和 PCA 的配合打法

3.1 K-Means 的初始化陷阱:为什么你的聚类结果每次都不一样

K-Means 是很多人接触的第一个无监督算法,逻辑简单到可以用一句话概括:随机选 K 个中心,把每个点分给最近的中心,重新计算中心,重复到中心不再移动。但真正跑起来,你会发现同一个数据集跑两次,聚类结果可能完全不同。这不是玄学,是初始化的问题。

标准 K-Means 随机选初始中心,如果两个中心恰好落在同一个簇里,另一个簇就没有中心,最终结果会很差。常见解法是 K-Means++,它让初始中心尽量分散:第一个中心随机选,后续每个中心被选中的概率与它到已有中心距离的平方成正比。sklearn 的KMeans默认就是 K-Means++,但如果你手写实现,这一步不能省。

import numpy as np def kmeans_plus_plus_init(X, k): n_samples, n_features = X.shape centers = np.zeros((k, n_features)) idx = np.random.randint(n_samples) centers[0] = X[idx] for i in range(1, k): dist_sq = np.array([min([np.sum((x - c) ** 2) for c in centers[:i]]) for x in X]) prob = dist_sq / dist_sq.sum() cumulative_prob = np.cumsum(prob) r = np.random.rand() for j, p in enumerate(cumulative_prob): if r < p: centers[i] = X[j] break return centers

这段代码里dist_sq是每个样本到最近已选中心的距离平方,prob是归一化后的概率分布。距离越大的点越容易被选为下一个中心,这样初始中心天然分散。手写版本和 sklearn 版本跑同一个数据,你会发现手写版如果不用 K-Means++,准确率可能差十几个百分点。

另一个关键参数是n_clusters,也就是 K 值。实验里通常用肘部法则来选:对不同的 K 值跑 K-Means,计算簇内平方和,画一条曲线,曲线拐点对应的 K 就是比较合理的。但肘部法则不是万能的,有时候曲线很平滑,没有明显拐点,这时候要结合业务理解,比如你知道数据大概分几类,就直接设几。

3.2 PCA 降维后再聚类:二维可视化与解释方差比

K-Means 跑完,你怎么知道聚类效果好还是不好?如果数据是二维的,直接画散点图。但真实数据往往几十个特征,画不了。这时候 PCA 就派上用场了。PCA 把高维数据投影到低维空间,同时尽量保留原始数据的方差。实验里最常见的做法是先用 PCA 降到二维,画散点图看聚类分布,再决定 K 值。

from sklearn.decomposition import PCA from sklearn.cluster import KMeans import matplotlib.pyplot as plt pca = PCA(n_components=2) X_pca = pca.fit_transform(X_train) kmeans = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42) labels = kmeans.fit_predict(X_pca) plt.scatter(X_pca[:, 0], X_pca[:, 1], c=labels, cmap='viridis', alpha=0.6) plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], marker='x', s=200, linewidths=3, color='red') plt.xlabel('PC1') plt.ylabel('PC2') plt.show() print(pca.explained_variance_ratio_)

n_components=2表示降到二维,explained_variance_ratio_告诉你每个主成分保留了多少原始方差。如果前两个主成分加起来不到 60%,说明二维投影丢失了太多信息,散点图可能看不出明显聚类,这时候要么增加维度到三维,要么换非线性降维方法比如 t-SNE。n_init=10表示用不同初始中心跑 10 次,取最好的结果,这是对抗初始化随机性的另一个手段。

PCA 降维后再聚类有一个容易忽略的点:PCA 是无监督的,它保留的是方差最大的方向,但方差大不一定等于对聚类有用。如果某个方差很小的特征恰好是区分不同簇的关键,PCA 会把它丢掉。所以实验里通常会让你对比「原始特征直接聚类」和「PCA 降维后聚类」的结果,你会发现有时候降维后反而更差,这不是代码问题,是方法本身的局限。

4. 神经网络实验:从 BP 手推到手写前馈网络

4.1 BP 神经网络的正向与反向:用 NumPy 实现一个两层网络

神经网络实验是这套源码里最厚的一部分,也是很多人卡住的地方。卡住的原因通常不是代码写不出来,而是反向传播的链式求导推不明白。武大实验文档里一般会先让你手推一个两层网络的梯度,再用 NumPy 实现。两层网络的结构是:输入层 → 隐藏层(Sigmoid 激活)→ 输出层(Softmax 或 Sigmoid)。

import numpy as np class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, std=1e-2): self.W1 = np.random.randn(input_size, hidden_size) * std self.b1 = np.zeros(hidden_size) self.W2 = np.random.randn(hidden_size, output_size) * std self.b2 = np.zeros(output_size) def forward(self, X): self.z1 = X.dot(self.W1) + self.b1 self.a1 = 1 / (1 + np.exp(-self.z1)) self.z2 = self.a1.dot(self.W2) + self.b2 exp_scores = np.exp(self.z2 - np.max(self.z2, axis=1, keepdims=True)) self.probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) return self.probs def backward(self, X, y, reg=1e-3): n_samples = X.shape[0] dscores = self.probs.copy() dscores[range(n_samples), y] -= 1 dscores /= n_samples dW2 = self.a1.T.dot(dscores) + reg * self.W2 db2 = np.sum(dscores, axis=0) da1 = dscores.dot(self.W2.T) dz1 = da1 * (self.a1 * (1 - self.a1)) dW1 = X.T.dot(dz1) + reg * self.W1 db1 = np.sum(dz1, axis=0) return dW1, db1, dW2, db2

std=1e-2是权重初始化标准差,设太大梯度会爆炸,设太小梯度会消失。reg=1e-3是 L2 正则化系数,防止权重过大导致过拟合。dscores的计算是 Softmax 加交叉熵的梯度,推导结果是probs - 1(对应正确类别),这一步如果推错了,整个反向传播都是错的。dz1是 Sigmoid 的导数乘以从上层传下来的梯度,Sigmoid 导数在输入很大或很小时接近零,这就是梯度消失的来源。

训练循环里通常用随机梯度下降,每次取一个 mini-batch,计算梯度后更新权重。学习率设 0.1 到 0.001 之间,太大 loss 会震荡甚至发散,太小收敛慢。实验里会让你画 loss 曲线,正常的曲线应该是先快速下降然后逐渐平缓,如果 loss 一直不降,检查反向传播的梯度计算;如果 loss 震荡,降低学习率。

4.2 用 MNIST 验证手写网络:训练轮数、批量大小与过拟合判断

手写网络写完之后,拿 MNIST 验证是最直观的。MNIST 有 60000 张训练图,10000 张测试图,每张 28x28 像素,展平后是 784 维。实验里通常不会让你跑全量,取一部分样本就能看出效果。

from sklearn.datasets import fetch_openml from sklearn.model_selection import train_test_split mnist = fetch_openml('mnist_784', version=1, as_frame=False) X, y = mnist.data / 255.0, mnist.target.astype(int) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) net = TwoLayerNet(input_size=784, hidden_size=128, output_size=10) batch_size = 64 learning_rate = 0.1 epochs = 20 for epoch in range(epochs): indices = np.random.permutation(X_train.shape[0]) for start in range(0, X_train.shape[0], batch_size): batch_idx = indices[start:start + batch_size] X_batch, y_batch = X_train[batch_idx], y_train[batch_idx] net.forward(X_batch) dW1, db1, dW2, db2 = net.backward(X_batch, y_batch) net.W1 -= learning_rate * dW1 net.b1 -= learning_rate * db1 net.W2 -= learning_rate * dW2 net.b2 -= learning_rate * db2 train_acc = np.mean(np.argmax(net.forward(X_train), axis=1) == y_train) test_acc = np.mean(np.argmax(net.forward(X_test), axis=1) == y_test) print(f"Epoch {epoch}: train_acc={train_acc:.4f}, test_acc={test_acc:.4f}")

hidden_size=128是隐藏层神经元数量,设太小欠拟合,设太大过拟合且训练慢。batch_size=64是每次更新权重用的样本数,设太小梯度噪声大,设太大内存吃紧且收敛慢。epochs=20是遍历整个训练集的次数,MNIST 上手写两层网络通常 10 到 20 轮就能到 97% 以上的测试准确率。

判断过拟合的方法是看训练准确率和测试准确率的差距。如果训练准确率 99% 但测试准确率只有 90%,说明模型记住了训练集但没学到泛化特征。解决办法是加正则化、减小隐藏层、或者加 Dropout。实验文档里通常会让你对比不同hidden_size和reg组合下的测试准确率,画一张热力图,你会看到存在一个最优区间,不是越大越好。

5. 避坑与排查:跑这套实验源码时最容易翻车的五个地方

5.1 环境配置翻车:numpy 版本和 matplotlib 后端冲突

现象:代码在别人机器上跑得好好的,到你这里import matplotlib.pyplot as plt直接报错,或者画图窗口弹不出来。原因通常是 numpy 版本和 matplotlib 版本不匹配,或者你在没有图形界面的服务器上跑。解决方法是先确认 Python 版本在 3.8 以上,然后用pip install numpy matplotlib scikit-learn统一安装最新稳定版。如果在服务器上,把plt.show()改成plt.savefig('output.png'),并且在使用 matplotlib 之前加import matplotlib; matplotlib.use('Agg')。

5.2 数据泄漏:标准化在切分之前做了

现象:测试集准确率高得离谱,换一组数据就崩。原因是你先对整个数据集做了标准化,再切分训练集和测试集。标准化用到了全局均值和方差,测试集的统计信息泄漏到了训练过程。正确顺序永远是先train_test_split,再用训练集的均值和方差去变换测试集。scaler.fit_transform(X_train)和scaler.transform(X_test)这两步不能合并成一步。

5.3 梯度消失:Sigmoid 隐藏层堆太深

现象:两层网络还能跑,加到三层四层 loss 就不降了。原因是 Sigmoid 的导数最大只有 0.25,反向传播每经过一层就乘一个小于 0.25 的数,几层之后梯度就接近零了。解决办法是把隐藏层激活函数换成 ReLU,ReLU 在正区间的导数是 1,不会衰减。如果非要用 Sigmoid,隐藏层不要超过两层,并且用 Xavier 初始化让每层输出方差保持一致。

5.4 K-Means 空簇:某个中心没有任何样本

现象:聚类结果里某个簇的样本数是 0,或者程序报错说中心无法更新。原因是初始中心选得不好,或者 K 值设得比实际类别数大太多。解决办法是用 K-Means++ 初始化,并且在每次迭代后检查是否有空簇,如果有,把空簇的中心重新随机选一个离其他中心最远的点。sklearn 的 KMeans 内部已经处理了这个问题,但手写版本必须自己加判断。

5.5 学习率设太大:loss 变成 nan

现象:训练刚开始 loss 就变成 nan,或者权重数值溢出。原因是学习率太大,梯度更新一步跨太远,权重变成极大值,下一轮 forward 时指数运算溢出。解决办法是把学习率降低一个数量级,比如从 0.1 降到 0.01,同时加梯度裁剪,把梯度的范数限制在一个阈值内。另一个常见原因是输入数据没有归一化,像素值在 0 到 255 之间,点积结果很大,除以 255 之后就正常了。

6. 进阶技巧:用交叉验证选超参数,而不是靠猜

跑完上面这些实验,你手里已经有一套能工作的代码了。但真正让模型从「能跑」到「好用」的,是超参数的选择。很多人调参靠试,试到哪个算哪个,这不是不行,但效率低且不可复现。交叉验证是更系统的做法:把训练集分成 K 份,每次用 K-1 份训练、1 份验证,重复 K 次取平均。这样每个样本都当过验证集,评估结果更稳定。

from sklearn.model_selection import GridSearchCV from sklearn.linear_model import LogisticRegression param_grid = { 'C': [0.01, 0.1, 1, 10], 'penalty': ['l2'], 'solver': ['lbfgs'] } grid = GridSearchCV( LogisticRegression(max_iter=5000), param_grid, cv=5, scoring='f1', n_jobs=-1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)

cv=5是五折交叉验证,scoring='f1'表示用 F1 分数评估,而不是准确率。n_jobs=-1表示用所有 CPU 核心并行跑。best_params_是交叉验证下平均 F1 最高的参数组合。注意,交叉验证是在训练集内部做的,测试集始终不参与,这样选出来的参数在测试集上的表现才是可信的。

我自己的习惯是,先用网格搜索粗调,确定参数的大致范围,再在这个范围里用随机搜索细调。网格搜索适合参数少且范围小的情况,参数一多组合爆炸,随机搜索更划算。另外,交叉验证的折数不是越多越好,5 折或 10 折是常见选择,折数太多会导致每折训练集太小,评估方差反而变大。

还有一个容易被忽略的点:交叉验证的切分也要分层。如果类别不平衡,普通 K 折可能让某一折里某个类别样本极少,评估结果波动大。用StratifiedKFold替代默认的KFold,保证每折的类别比例和整体一致。这个细节在实验报告里通常不写,但实际做项目时是必须的。

最后说一个我踩过的坑:不要用测试集反复调参。有些人调完参数在测试集上看一眼,不满意再调,再看一眼,来回几次之后测试集就被「用旧了」,你看到的准确率是过拟合到测试集上的。正确做法是测试集只在最后用一次,中间所有调参都在验证集或交叉验证上完成。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 14:01:12

手写PL/0编译器:从词法分析到解释器的编译原理课程实验指南

简介&#xff1a;山东大学SDU编译原理课程PL/0编译器实验的完整实现包&#xff0c;面向高校计算机专业学生及编译原理学习者&#xff0c;适合作为课程实验参考、复习与二次开发基础。资源包含完整的C/C源代码&#xff08;.cpp/.h/.c&#xff09;、CMake构建脚本、测试用例&…

作者头像 李华
网站建设 2026/10/3 14:00:52

基于论文复现的InDuDoNet低剂量CT去噪Python实现源码

简介&#xff1a;本资源为InDuDoNet模型的Python复现源码&#xff0c;面向深度学习研究者与医学图像处理方向的开发者&#xff0c;尤其适合需要复现CT图像分割算法、开展对比实验或二次开发的中高级学习者。项目围绕论文提出的InDuDoNet展开&#xff0c;涵盖训练、推理、数据预…

作者头像 李华
网站建设 2026/10/3 13:59:45

C语言手写词法分析器:从DFA到可运行Lexer的完整实现

简介&#xff1a;本资源是面向计算机专业本科生及编译原理初学者的实践型实验材料&#xff0c;聚焦词法分析器这一编译器前端核心模块的设计与实现&#xff0c;帮助学习者打通从理论&#xff08;如Token分类、正则匹配、状态机建模&#xff09;到C语言编码落地的关键环节。压缩…

作者头像 李华
网站建设 2026/10/3 13:58:14

基于LSTM+SVM的设备故障诊断Python源码实现与调参实战

简介&#xff1a;基于LSTM和SVM的设备故障诊断Python源码项目&#xff0c;融合长短期记忆网络提取时序特征与支持向量机分类判别&#xff0c;面向人工智能、自动化、电子信息等专业的毕业设计、课程设计与项目初期演示&#xff0c;也适合故障诊断方向的学习者作为进阶参考。压缩…

作者头像 李华
网站建设 2026/10/3 13:56:27

2026深度解读:Work Agent如何串联搜索、分析与写作完成完整业务报告

AI的交互形态正在发生根本性转变。早期大模型以单轮问答为核心&#xff0c;用户提出问题&#xff0c;模型即时输出一段文字&#xff0c;对话在单次信息交换后就可以结束。随后多轮对话能力成熟&#xff0c;模型能够记住上下文&#xff0c;在一轮轮问答中持续迭代内容&#xff0…

作者头像 李华
网站建设 2026/10/3 13:55:23

ABAP Cloud 中的 Reuse Services,如何把通用能力沉淀成可复用的企业级服务

在一个真正投入生产的 SAP 业务应用里,开发工作很少只是维护几张业务表、写几个 CDS View、实现一个 RAP Business Object,再通过 OData V4 暴露出去这么简单。销售订单修改以后要留下审计记录,后台批处理需要能够调度,业务异常需要写入统一日志,审批需要进入工作流,业务…

作者头像 李华