ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

武大机器学习与模式识别实验课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, lr0.01, n_iters1000): 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_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) clf LogisticRegression(max_iter5000, C1.0, solverlbfgs) 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))这里有几个参数值得说清楚。stratifyy保证训练集和测试集的类别比例一致不然随机切分可能让某一类在测试集里特别少准确率波动很大。max_iter5000是因为 lbfgs 求解器默认迭代次数在特征多的时候不够用你会看到 ConvergenceWarning不是代码错了是没收敛完。C是正则化强度的倒数C 越小正则化越强模型越保守C 越大越容易过拟合。实验里一般会让你试 C0.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_components2) X_pca pca.fit_transform(X_train) kmeans KMeans(n_clusters3, initk-means, n_init10, random_state42) labels kmeans.fit_predict(X_pca) plt.scatter(X_pca[:, 0], X_pca[:, 1], clabels, cmapviridis, alpha0.6) plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], markerx, s200, linewidths3, colorred) plt.xlabel(PC1) plt.ylabel(PC2) plt.show() print(pca.explained_variance_ratio_)n_components2表示降到二维explained_variance_ratio_告诉你每个主成分保留了多少原始方差。如果前两个主成分加起来不到 60%说明二维投影丢失了太多信息散点图可能看不出明显聚类这时候要么增加维度到三维要么换非线性降维方法比如 t-SNE。n_init10表示用不同初始中心跑 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, std1e-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, axis1, keepdimsTrue)) self.probs exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) return self.probs def backward(self, X, y, reg1e-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, axis0) 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, axis0) return dW1, db1, dW2, db2std1e-2是权重初始化标准差设太大梯度会爆炸设太小梯度会消失。reg1e-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, version1, as_frameFalse) X, y mnist.data / 255.0, mnist.target.astype(int) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) net TwoLayerNet(input_size784, hidden_size128, output_size10) 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), axis1) y_train) test_acc np.mean(np.argmax(net.forward(X_test), axis1) y_test) print(fEpoch {epoch}: train_acc{train_acc:.4f}, test_acc{test_acc:.4f})hidden_size128是隐藏层神经元数量设太小欠拟合设太大过拟合且训练慢。batch_size64是每次更新权重用的样本数设太小梯度噪声大设太大内存吃紧且收敛慢。epochs20是遍历整个训练集的次数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 的数几层之后梯度就接近零了。解决办法是把隐藏层激活函数换成 ReLUReLU 在正区间的导数是 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_iter5000), param_grid, cv5, scoringf1, n_jobs-1 ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)cv5是五折交叉验证scoringf1表示用 F1 分数评估而不是准确率。n_jobs-1表示用所有 CPU 核心并行跑。best_params_是交叉验证下平均 F1 最高的参数组合。注意交叉验证是在训练集内部做的测试集始终不参与这样选出来的参数在测试集上的表现才是可信的。我自己的习惯是先用网格搜索粗调确定参数的大致范围再在这个范围里用随机搜索细调。网格搜索适合参数少且范围小的情况参数一多组合爆炸随机搜索更划算。另外交叉验证的折数不是越多越好5 折或 10 折是常见选择折数太多会导致每折训练集太小评估方差反而变大。还有一个容易被忽略的点交叉验证的切分也要分层。如果类别不平衡普通 K 折可能让某一折里某个类别样本极少评估结果波动大。用StratifiedKFold替代默认的KFold保证每折的类别比例和整体一致。这个细节在实验报告里通常不写但实际做项目时是必须的。最后说一个我踩过的坑不要用测试集反复调参。有些人调完参数在测试集上看一眼不满意再调再看一眼来回几次之后测试集就被「用旧了」你看到的准确率是过拟合到测试集上的。正确做法是测试集只在最后用一次中间所有调参都在验证集或交叉验证上完成。希望帮到你。本文还有配套的精品资源点击获取
返回列表