ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

模式识别实验可运行代码:KNN、朴素贝叶斯与层次聚类实战

模式识别实验可运行代码:KNN、朴素贝叶斯与层次聚类实战 简介基于Python的模式识别实验代码包面向高校学生完成性别分类、人脸识别等课程实验。涵盖贝叶斯分类器身高/体重最大似然估计与最小错误率决策、Fisher判别、KNN及PCA人脸识别附带实验报告文档可直接对照运行。包体共466个文件仅5.7MB包含16个py核心代码、5个docx实验报告、400个bmp图像数据、20个xml和13个txt标注/训练样本结构清晰。目前已有1631人学习下载体积小巧便于快速部署。实验设计覆盖单特征分类、交叉验证不同降维维数与k值等能帮助深入理解模式识别原理是一份实用且性价比高的课程配套资料。1. 从可运行代码理解模式识别实验模式识别实验的代码真正难住人的地方往往不在算法而在“第一次跑通”。网上能搜到很多基于 Python 的 KNN、贝叶斯、聚类代码但下载下来要么数据路径写死要么 sklearn 版本接口对不上要么没有主入口最后只能贴一段自己都说不清的代码。我给出的路线是从环境检查开始用合成数据、KNN、朴素贝叶斯、层次聚类和可视化评估搭出一份能直接运行的代码骨架。跑通python main.py之后再按实验要求换数据集或算法几分钟就能复现全部结果。适合准备交模式识别实验报告的学生也适合需要快速验证算法效果的工程师。2. 准备模式识别实验环境虚拟环境、数据集与最小代码结构2.1 先固定 Python 解释器和依赖版本可运行代码的第一道门槛是解释器环境。同一个项目里numpy 1.24 和 1.26 对某些矩阵运算的警告处理不同matplotlib 3.5 和 3.8 的绘图后端也有差异所以我不建议直接在全局 Python 里装包。常见做法是创建虚拟环境把依赖版本固定下来这样代码换到另一台机器上也能复现。python -m venv pr_env source pr_env/bin/activate # Windows 下换用 pr_env\Scripts\activate python -c import sklearn, numpy, matplotlib, scipy; print(sklearn.__version__, numpy.__version__, matplotlib.__version__, scipy.__version__)最后一条命令如果报ModuleNotFoundError说明虚拟环境已经激活但包没装补一句pip install numpy scikit-learn matplotlib scipy即可。VSCode 里用CtrlShiftP打开命令面板选择Python: Select Interpreter后指向pr_envPyCharm 则在Settings - Project - Python Interpreter里添加。这一步解决的是“代码明明没错但一 import 就红”的问题从免费 python 源码大全里拉下来的代码翻车点十有八九在解释器选错。Linux 服务器上如果同时存在python3和python两个命令优先用python3 -m venv pr_env避免后续 pip 装到了另一个解释器下。依赖版本可以参考下表依赖库最小版本用途numpy1.21数组运算与广播scikit-learn1.0数据集、模型、评估接口matplotlib3.5混淆矩阵、PCA、谱系图scipy1.7层次聚类与 linkage2.2 用 make_classification 生成可复现的数据集数据生成我一般不用手写坐标数组而是用sklearn.datasets.make_classification。手写数据虽然直观但特征数量和类别一改就要重写合成数据生成器可以一次性控制样本量、维度、冗余特征和类别数做对比实验时只需要改一个数字。from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split import numpy as np # 600 个样本8 个特征其中 4 个信息特征、2 个冗余特征3 个类别 X, y make_classification( n_samples600, n_features8, n_informative4, n_redundant2, n_classes3, n_clusters_per_class1, random_state42 ) # 30% 作测试集stratify 保证测试集类别比例与原始数据一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) print(X_train.shape, np.bincount(y_train))random_state42决定了数据生成和划分的全过程报告里所有指标都来自这个种子换机器跑结果不会变。n_informative控制真正对分类有贡献的特征数n_redundant会由信息特征线性组合生成二者之和不能超过n_features否则make_classification会直接报错。把这段放进data.py并封装成load_data()函数后面的模型和评估都从这一个入口取数据。如果希望数据更接近真实任务也可以换成load_iris或load_wine接口基本一致。2.3 按数据、模型、评估拆分代码文件不建议把所有代码堆进一个 notebook 或者单个main.py。模式识别实验通常要交数据处理、模型、评估三部分我会在本地拆成四个文件每个文件只做一件事pattern_recognition_experiment/ ├── data.py # 数据生成、划分、保存 ├── models.py # 手写与 sklearn 模型封装 ├── evaluate.py # 评估指标、混淆矩阵、可视化 └── main.py # 串联整个实验流程这样拆不是为了形式主义。当你想把 KNN 换成 SVM 时只需要改models.py评估和画图流程完全不动。main.py的骨架长这样# main.py只做流程编排不写算法细节 import data import evaluate from models import knn_predict X_train, X_test, y_train, y_test data.load_data() y_pred knn_predict(X_train, y_train, X_test, k5) evaluate.report(y_test, y_pred, save_pathconfusion_matrix.png)data.load_data()在 2.2 节已经定义好knn_predict接收训练集、测试集和 k 值返回预测结果evaluate.report输出分类报告并保存图片。把这个结构定下来后面加朴素贝叶斯、SVM 或层次聚类时main.py不会越改越长。3. KNN 与朴素贝叶斯用可运行代码实现模式识别核心3.1 自己实现 KNN向量化距离计算KNN 是最适合手写的模式识别算法它没有显式训练参数预测阶段计算待测样本与全部训练样本的距离取最近的 K 个类别投票。核心难点不在算法思想而在距离计算不能写成两层 for 循环否则 600 个样本还撑得住换成 6000 个样本会明显变慢。下面的实现用 NumPy 广播一次性算出距离矩阵。import numpy as np from collections import Counter # 向量化 KNN返回和 X_test 等长的预测标签 def knn_predict(X_train, y_train, X_test, k5): X_test np.atleast_2d(X_test) # 广播计算欧氏距离结果形状为 (n_test, n_train) distances np.sqrt( ((X_test[:, np.newaxis, :] - X_train[np.newaxis, :, :]) ** 2).sum(axis-1) ) # 取每个测试样本最近的 k 个训练样本 neighbor_indices np.argsort(distances, axis1)[:, :k] neighbor_labels y_train[neighbor_indices] # 对每个样本的邻居标签取众数 predictions [ Counter(neighbor_labels[i]).most_common(1)[0][0] for i in range(neighbor_labels.shape[0]) ] return np.array(predictions)X_test[:, np.newaxis, :]把形状从(n_test, n_features)变成(n_test, 1, n_features)和X_train[np.newaxis, :, :]广播后相减、平方、求和、开方得到二维距离矩阵。argsort只返回下标不修改原数组[:, :k]取前 k 个。Counter的most_common(1)在平票时返回先出现的类别为了避免平票争议k 一般取奇数。k 值特征1容易受单点噪声影响边界很不平滑3常见默认值噪声影响开始下降5实验中最常用的起点7决策边界更平滑但局部细节丢失在可运行代码层面手写版和sklearn.KNeighborsClassifier应给出几乎一致的准确率如果差别超过一个百分点先检查距离公式再确认是否对特征做了不同的标准化。3.2 自己实现高斯朴素贝叶斯对数似然避免下溢朴素贝叶斯的可运行版本要处理两个问题概率连乘会下溢方差可能为 0。连续特征一般假设服从高斯分布用每类样本的均值和方差估计概率密度实际计算时取对数把乘法变成加法。# 高斯朴素贝叶斯适合连续特征接口与 sklearn 类似 class GaussianNB: def fit(self, X, y): self.classes np.unique(y) self.means [] self.vars [] for c in self.classes: X_c X[y c] self.means.append(X_c.mean(axis0)) # 加 1e-6 防止特征方差为 0 导致除零 self.vars.append(X_c.var(axis0) 1e-6) self.means np.array(self.means) self.vars np.array(self.vars) self.priors np.array([len(X[y c]) / len(X) for c in self.classes]) return self def predict(self, X): log_prior np.log(self.priors) log_lik ( -0.5 * np.log(2 * np.pi * self.vars) - (X[:, None, :] - self.means[None, :, :]) ** 2 / (2 * self.vars) ) log_posterior log_lik.sum(axis-1) log_prior return self.classes[np.argmax(log_posterior, axis1)]log_lik的形状是(n_samples, n_classes, n_features)sum(axis-1)把每个特征的对数似然加起来相当于在高斯假设下计算联合对数概率。方差加1e-6的作用是当某个类里所有样本在某个特征上取值完全相同时程序不会出现除零或 NaN。最后一个argmax返回后验概率最大的类别这一步等价于比较各类别的对数后验。如果实验数据是离散特征比如文本词频高斯假设不适用应该换成多项式朴素贝叶斯连续特征里如果有明显偏态分布建议先取对数或标准化再建模。3.3 用 sklearn 做对照实验并输出准确率自己实现的版本用来讲原理正式跑实验时我一般保留 sklearn 版本做基线因为KNeighborsClassifier和GaussianNB在边界条件处理上更成熟。两者放在同一份报告里也能体现你既理解实现又熟悉工具链。from sklearn.neighbors import KNeighborsClassifier from sklearn.naive_bayes import GaussianNB from sklearn.metrics import accuracy_score import data # 统一从 data 模块取数据 X_train, X_test, y_train, y_test data.load_data() # sklearn KNNp2 表示欧氏距离 knn KNeighborsClassifier(n_neighbors5, p2) knn.fit(X_train, y_train) y_pred_knn knn.predict(X_test) print(sklearn KNN acc:, accuracy_score(y_test, y_pred_knn)) # sklearn 高斯朴素贝叶斯 nb GaussianNB() nb.fit(X_train, y_train) y_pred_nb nb.predict(X_test) print(sklearn GaussianNB acc:, accuracy_score(y_test, y_pred_nb))n_neighbors和p是可运行代码中最常被改的两个参数。p2是欧氏距离p1是曼哈顿距离对维度较高的特征曼哈顿距离有时反而更稳定。GaussianNB没有需要手动调的超参数可以直接把准确率当作后面层次聚类实验的参考。4. 评估、可视化与层次聚类让模式识别实验结论可验证4.1 用混淆矩阵和分类报告代替单一准确率准确率不能直接用来写实验报告的结论三分类场景下随便猜也有约 33% 的准确率。输出混淆矩阵和分类报告才能看出错误集中在哪两个类别之间。from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt # 评估并保存混淆矩阵save_path 可以写相对路径 def report(y_test, y_pred, save_pathconfusion_matrix.png): print(classification_report(y_test, y_pred, digits4)) cm confusion_matrix(y_test, y_pred) disp ConfusionMatrixDisplay( cm, display_labels[class-0, class-1, class-2] ) fig, ax plt.subplots(figsize(4.5, 4)) disp.plot(axax, cmapBlues) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close(fig) # 关掉当前图避免和后面的 PCA 图叠加classification_report里的 precision、recall、f1-score 三列在实验报告里可以直接摘录成表。digits4控制小数位数默认 2 位会让 0.977 和 0.982 看起来一样。plt.close(fig)是很多人会漏掉的细节不关掉当前 figure后面画 PCA 散点图时颜色和坐标可能混乱。4.2 PCA 降维与决策边界横坐标刻度太密时这样处理特征维度高于 2 时散点图没法直接画。常见做法是先标准化再 PCA 降到二维但需要注意scaler和pca都必须用训练集拟合再用同一个变换去处理测试集。如果对训练集和测试集分别调用fit_transform就相当于测试时看见了训练集分布属于数据泄漏画出来的决策边界会偏乐观。import data from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler import matplotlib.pyplot as plt X_train, X_test, y_train, y_test data.load_data() scaler StandardScaler() pca PCA(n_components2, random_state42) # 在训练集上 fit再 transform 测试集 X_train_scaled scaler.fit_transform(X_train) X_train_pca pca.fit_transform(X_train_scaled) X_test_scaled scaler.transform(X_test) X_test_pca pca.transform(X_test_scaled) # 画测试集样本的 PCA 散点图 plt.figure(figsize(6, 5)) scatter plt.scatter( X_test_pca[:, 0], X_test_pca[:, 1], cy_test, cmapviridis, s30, alpha0.8 ) plt.xlabel(PC1) plt.ylabel(PC2) # 横坐标刻度太密时限制刻度数量并旋转 plt.locator_params(axisx, nbins6) plt.xticks(rotation45, fontsize8) plt.colorbar(scatter) plt.tight_layout() plt.savefig(pca_scatter.png, dpi150)pca.explained_variance_ratio_可以输出每个主成分的方差占比报告里写成“前两主成分累计解释 xx%”会比只贴图更有说服力。plt.locator_params(axisx, nbins6)解决的就是 python 画图横坐标太密集的问题如果 x 轴是样本编号几百个刻度默认全画出来会糊成一团限制 nbins 后只显示 6 个刻度点再旋转 45 度就清楚了。如果想看 KNN 在二维平面上的决策边界用X_train_pca重新训练一个 KNN然后在网格上预测并填充颜色from sklearn.neighbors import KNeighborsClassifier import numpy as np model_2d KNeighborsClassifier(n_neighbors5) model_2d.fit(X_train_pca, y_train) x_min, x_max X_train_pca[:, 0].min() - 1, X_train_pca[:, 0].max() 1 y_min, y_max X_train_pca[:, 1].min() - 1, X_train_pca[:, 1].max() 1 xx, yy np.meshgrid( np.linspace(x_min, x_max, 300), np.linspace(y_min, y_max, 300) ) Z model_2d.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape) plt.figure(figsize(6, 5)) plt.contourf(xx, yy, Z, alpha0.3, cmapviridis) plt.scatter(X_test_pca[:, 0], X_test_pca[:, 1], cy_test, cmapviridis, s20) plt.xlabel(PC1) plt.ylabel(PC2) plt.tight_layout() plt.savefig(knn_boundary.png, dpi150)决策边界图是实验报告里最容易拿分的一张图因为它直观展示了 KNN 的非线性边界。注意边界模型只由训练集 PCA 拟合测试集散点只做叠加不能用测试集参与边界训练。4.3 层次聚类谱系图与簇切分分类实验做完模式识别作业里一般还会有一个无监督部分。层次聚类是比较好写的一段代码它不要求提前指定初始簇数在谱系图上可以直接观察不同距离下的聚合行为。import data from sklearn.preprocessing import StandardScaler from scipy.cluster.hierarchy import dendrogram, linkage, fcluster import matplotlib.pyplot as plt import numpy as np X_train, _, _, _ data.load_data() X_train_scaled StandardScaler().fit_transform(X_train) # ward 方法最小化合并后的簇内方差 Z linkage(X_train_scaled, methodward) plt.figure(figsize(8, 4)) dendrogram(Z, truncate_modelevel, p5, no_labelsTrue) plt.ylabel(distance) plt.tight_layout() plt.savefig(dendrogram.png, dpi150) # 从谱系图中切出 3 个簇 clusters fcluster(Z, t3, criterionmaxclust) counts np.unique(clusters, return_countsTrue) print(cluster sizes:, dict(zip(*counts)))linkage的第二个参数method决定簇间距离怎么算常见的四个取值适合不同数据形状。dendrogram画谱系图时truncate_modelevel配合p5表示只显示最后 5 层合并过程样本太多时不至于把叶子标签挤满。fcluster里的t3配合maxclust表示从树顶往下切出 3 个簇正好和前面三分类实验对应。method合并规则建议场景ward合并后簇内方差增量最小样本较均衡的连续特征complete两个簇间最远样本距离簇边界清晰时更好用average两个簇间样本平均距离一般数据默认选择single两个簇间最近样本距离条状或链状数据但容易链式效应注意聚类结果和y_train不一致是正常现象无监督任务本来就不保证对齐类别标签。如果一定要量化对比可以用调整兰德指数 ARI 评估聚类与真实标签的匹配程度。5. 模式识别实验可运行代码的调参、复现与排错5.1 多随机种子取均值写进报告更稳实验报告里如果只报一个random_state42下的准确率换台机器可能就对不上了。更稳的写法是固定模型和超参数让数据划分的随机种子跑多次输出均值和标准差。import numpy as np from sklearn.model_selection import train_test_split from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score def evaluate_knn_across_seeds(X, y, n_neighbors5, n_runs5): scores [] for seed in range(n_runs): X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_stateseed ) model KNeighborsClassifier(n_neighborsn_neighbors) model.fit(X_train, y_train) scores.append(accuracy_score(y_test, model.predict(X_test))) return np.mean(scores), np.std(scores) mean_acc, std_acc evaluate_knn_across_seeds(X, y) print(facc {mean_acc:.4f} ± {std_acc:.4f})n_runs设为 5 或 10一般 5 次足够说明稳定性。写报告时把acc 0.9640 ± 0.0051放进结论比单独一个 92.7% 有信息量。n_neighbors也值得跑一个循环对 k1, 3, 5, 7, 9 分别用这个函数算出均值画一条折线图实验结果部分就有了第一个图表。5.2 四个高频报错和两条环境检查命令我经常遇到的运行错误主要有四类ModuleNotFoundError: No module named sklearn虚拟环境没激活或依赖没装运行python -m pip install numpy scikit-learn matplotlib scipy。ValueError: n_features5, n_samples... mismatch训练和测试特征维度不一致多数是在做 PCA 或标准化时分别调用了fit_transform检查是否用了同一个scaler和pca实例。matplotlib 中文显示为方框把图中的标题、轴标签统一改成英文或者设置plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans]Linux 服务器没有 SimHei 时建议直接用英文标签。NameError: name X_train is not definednotebook 或脚本里代码执行顺序乱了把数据生成、模型训练、评估三个部分从上到下重新跑一遍用python main.py单文件执行则不会出现这个问题。排查环境问题时下面两条命令比翻报错日志更直接# 当前解释器路径确认没有选错环境 python -c import sys; print(sys.executable) # 核心包版本确认和报告里的版本一致 python -m pip list | grep -Ei scikit-learn|numpy|matplotlib|scipy第一次跑通代码后把这两条命令的原始输出直接贴进实验报告的“运行环境”一节。以后任何人拿到这份代码先用这两行确认环境再运行python main.py就不会再出现“在我机器上明明是好的”这类问题。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进