简介:基于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 里用Ctrl+Shift+P打开命令面板,选择Python: Select Interpreter后指向pr_env;PyCharm 则在Settings -> Project -> Python Interpreter里添加。这一步解决的是“代码明明没错但一 import 就红”的问题,从免费 python 源码大全里拉下来的代码,翻车点十有八九在解释器选错。Linux 服务器上如果同时存在python3和python两个命令,优先用python3 -m venv pr_env,避免后续 pip 装到了另一个解释器下。
依赖版本可以参考下表:
| 依赖库 | 最小版本 | 用途 |
|---|---|---|
| numpy | 1.21+ | 数组运算与广播 |
| scikit-learn | 1.0+ | 数据集、模型、评估接口 |
| matplotlib | 3.5+ | 混淆矩阵、PCA、谱系图 |
| scipy | 1.7+ | 层次聚类与 linkage |
2.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_samples=600, n_features=8, n_informative=4, n_redundant=2, n_classes=3, n_clusters_per_class=1, random_state=42 ) # 30% 作测试集,stratify 保证测试集类别比例与原始数据一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) print(X_train.shape, np.bincount(y_train))random_state=42决定了数据生成和划分的全过程,报告里所有指标都来自这个种子,换机器跑结果不会变。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, k=5) evaluate.report(y_test, y_pred, save_path="confusion_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, k=5): 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, axis=1)[:, :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(axis=0)) # 加 1e-6 防止特征方差为 0 导致除零 self.vars.append(X_c.var(axis=0) + 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, axis=1)]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 KNN,p=2 表示欧氏距离 knn = KNeighborsClassifier(n_neighbors=5, p=2) 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是可运行代码中最常被改的两个参数。p=2是欧氏距离,p=1是曼哈顿距离;对维度较高的特征,曼哈顿距离有时反而更稳定。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_path="confusion_matrix.png"): print(classification_report(y_test, y_pred, digits=4)) 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(ax=ax, cmap="Blues") plt.tight_layout() plt.savefig(save_path, dpi=150) plt.close(fig) # 关掉当前图,避免和后面的 PCA 图叠加classification_report里的 precision、recall、f1-score 三列,在实验报告里可以直接摘录成表。digits=4控制小数位数,默认 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_components=2, random_state=42) # 在训练集上 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], c=y_test, cmap="viridis", s=30, alpha=0.8 ) plt.xlabel("PC1") plt.ylabel("PC2") # 横坐标刻度太密时,限制刻度数量并旋转 plt.locator_params(axis="x", nbins=6) plt.xticks(rotation=45, fontsize=8) plt.colorbar(scatter) plt.tight_layout() plt.savefig("pca_scatter.png", dpi=150)pca.explained_variance_ratio_可以输出每个主成分的方差占比,报告里写成“前两主成分累计解释 xx%”会比只贴图更有说服力。plt.locator_params(axis='x', nbins=6)解决的就是 python 画图横坐标太密集的问题;如果 x 轴是样本编号,几百个刻度默认全画出来会糊成一团,限制 nbins 后只显示 6 个刻度点,再旋转 45 度就清楚了。
如果想看 KNN 在二维平面上的决策边界,用X_train_pca重新训练一个 KNN,然后在网格上预测并填充颜色:
from sklearn.neighbors import KNeighborsClassifier import numpy as np model_2d = KNeighborsClassifier(n_neighbors=5) 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, alpha=0.3, cmap="viridis") plt.scatter(X_test_pca[:, 0], X_test_pca[:, 1], c=y_test, cmap="viridis", s=20) plt.xlabel("PC1") plt.ylabel("PC2") plt.tight_layout() plt.savefig("knn_boundary.png", dpi=150)决策边界图是实验报告里最容易拿分的一张图,因为它直观展示了 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, method="ward") plt.figure(figsize=(8, 4)) dendrogram(Z, truncate_mode="level", p=5, no_labels=True) plt.ylabel("distance") plt.tight_layout() plt.savefig("dendrogram.png", dpi=150) # 从谱系图中切出 3 个簇 clusters = fcluster(Z, t=3, criterion="maxclust") counts = np.unique(clusters, return_counts=True) print("cluster sizes:", dict(zip(*counts)))linkage的第二个参数method决定簇间距离怎么算,常见的四个取值适合不同数据形状。dendrogram画谱系图时,truncate_mode='level'配合p=5表示只显示最后 5 层合并过程,样本太多时不至于把叶子标签挤满。fcluster里的t=3配合maxclust表示从树顶往下切出 3 个簇,正好和前面三分类实验对应。
| method | 合并规则 | 建议场景 |
|---|---|---|
| ward | 合并后簇内方差增量最小 | 样本较均衡的连续特征 |
| complete | 两个簇间最远样本距离 | 簇边界清晰时更好用 |
| average | 两个簇间样本平均距离 | 一般数据默认选择 |
| single | 两个簇间最近样本距离 | 条状或链状数据,但容易链式效应 |
注意聚类结果和y_train不一致是正常现象,无监督任务本来就不保证对齐类别标签。如果一定要量化对比,可以用调整兰德指数 ARI 评估聚类与真实标签的匹配程度。
5. 模式识别实验可运行代码的调参、复现与排错
5.1 多随机种子取均值,写进报告更稳
实验报告里如果只报一个random_state=42下的准确率,换台机器可能就对不上了。更稳的写法是固定模型和超参数,让数据划分的随机种子跑多次,输出均值和标准差。
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_neighbors=5, n_runs=5): scores = [] for seed in range(n_runs): X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=seed ) model = KNeighborsClassifier(n_neighbors=n_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(f"acc = {mean_acc:.4f} ± {std_acc:.4f}")n_runs设为 5 或 10,一般 5 次足够说明稳定性。写报告时把acc = 0.9640 ± 0.0051放进结论,比单独一个 92.7% 有信息量。n_neighbors也值得跑一个循环:对 k=1, 3, 5, 7, 9 分别用这个函数算出均值,画一条折线图,实验结果部分就有了第一个图表。
5.2 四个高频报错和两条环境检查命令
我经常遇到的运行错误主要有四类:
ModuleNotFoundError: No module named 'sklearn':虚拟环境没激活或依赖没装,运行python -m pip install numpy scikit-learn matplotlib scipy。ValueError: n_features=5, 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 defined:notebook 或脚本里代码执行顺序乱了,把数据生成、模型训练、评估三个部分从上到下重新跑一遍;用python main.py单文件执行则不会出现这个问题。
排查环境问题时,下面两条命令比翻报错日志更直接:
# 当前解释器路径,确认没有选错环境 python -c "import sys; print(sys.executable)" # 核心包版本,确认和报告里的版本一致 python -m pip list | grep -Ei "scikit-learn|numpy|matplotlib|scipy"第一次跑通代码后,把这两条命令的原始输出直接贴进实验报告的“运行环境”一节。以后任何人拿到这份代码,先用这两行确认环境,再运行python main.py,就不会再出现“在我机器上明明是好的”这类问题。
本文还有配套的精品资源,点击获取