
简介这份资源是面向高校学生与初学者的模式识别实验代码包围绕课程实验与报告需求整理帮助读者在较短时间内完成性别分类、人脸识别等典型实验任务。包内共466个文件以400个bmp人脸图像、16个Python脚本、13个txt数据文件为主另含xml配置、docx实验报告与iml工程文件压缩包约5.7MB结构清晰便于按实验模块查阅。内容覆盖基于FAMALE与MALE数据建立Bayes分类器并测试以及调整特征与分类器参数观察性能变化同时包含单特征下最大似然估计概率密度、最小错误率决策规则推导以及PCA降维结合k近邻的人脸识别与N-交叉准确率比较并涉及Fisher判别。已有1632人学习下载适合需要完整代码、实验报告与排错思路的读者参考复用。1. 从一份“九块九实验课”说起这套 Python 模式识别代码到底能跑出什么如果你正在上模式识别、机器学习导论这类课大概率会遇到一个尴尬局面理论课讲贝叶斯决策、Fisher 判别、PCA 降维公式推得飞起但一到实验课老师甩过来一句“用 Python 实现一下”然后就没了。教材上的伪代码和能跑通的工程代码之间隔着数据读取、维度对齐、交叉验证、可视化这一堆琐事。这套资源就是冲着这个场景来的——它把模式识别实验课里最常出现的四个任务打包成了可运行代码基于身高体重的贝叶斯性别分类、Fisher 线性判别、KNN 分类、PCA 人脸识别附带实验报告。样本数据是 FAMALE.TXT 和 MALE.TXT 两个性别特征文件人脸部分用的是 ORL 风格的 BMP 灰度图s1_1.bmp 到 s40_10.bmp 这种命名。适合谁正在赶实验报告 deadline 的本科生、想快速验证经典算法行为的研究生新生以及需要一套干净基线代码来做对比实验的从业者。它不教你 Python 语法也不讲 sklearn 的 API 大全它解决的是“给我一份能直接 python xxx.py 跑出结果和图的代码”这个具体诉求。2. 贝叶斯性别分类器从最大似然估计到最小错误率决策2.1 为什么先拿身高体重开刀模式识别实验里贝叶斯分类器通常是第一个要啃的骨头因为它把“先验概率 类条件概率密度 后验决策”这条链路完整暴露出来。这套代码选择身高和体重作为特征原因很实际这两个特征在性别上的分布差异肉眼可见且近似服从正态分布正好用来演示正态分布假设下的最大似然估计。FAMALE.TXT 和 MALE.TXT 里每行就是一个样本的身高体重值格式简单到不需要任何解析技巧。用单特征做实验时你可以只取身高或只取体重观察决策边界怎么随特征变化用双特征时决策面从一维点变成二维直线。这种从一维到二维的过渡是理解“特征维度如何影响分类器性能”最直观的方式。2.2 最大似然估计的代码落地先看单特征下的参数估计和决策规则实现。假设我们只取身高且两类都服从正态分布那么需要估计的参数就是均值 μ 和方差 σ²。最大似然估计的结果很简洁μ 就是样本均值σ² 是样本方差注意是有偏还是无偏代码里一般用 np.var 默认的有偏版本但实验报告里要说明。下面这段代码展示了从文件读取到参数估计再到分类的核心逻辑import numpy as np def load_data(filename): 读取TXT文件每行两个浮点数身高 体重 data [] with open(filename, r) as f: for line in f: parts line.strip().split() if len(parts) 2: data.append([float(parts[0]), float(parts[1])]) return np.array(data) def mle_gaussian(x): 最大似然估计正态分布的均值和方差 mu np.mean(x) sigma2 np.var(x) # 有偏估计除以N return mu, sigma2 def gaussian_pdf(x, mu, sigma2): 正态分布概率密度函数 return (1.0 / np.sqrt(2 * np.pi * sigma2)) * np.exp(-0.5 * (x - mu)**2 / sigma2) # 加载训练数据 female load_data(FAMALE.TXT) male load_data(MALE.TXT) # 取身高作为单特征第0列 h_f female[:, 0] h_m male[:, 0] # 估计两类的身高分布参数 mu_f, var_f mle_gaussian(h_f) mu_m, var_m mle_gaussian(h_m) # 先验概率按样本数比例 prior_f len(h_f) / (len(h_f) len(h_m)) prior_m 1 - prior_f def classify_bayes(x, mu_f, var_f, mu_m, var_m, prior_f, prior_m): 最小错误率贝叶斯决策比较后验概率 post_f gaussian_pdf(x, mu_f, var_f) * prior_f post_m gaussian_pdf(x, mu_m, var_m) * prior_m return Female if post_f post_m else Male # 对测试样本分类 test_heights [160, 170, 180] for h in test_heights: print(h, classify_bayes(h, mu_f, var_f, mu_m, var_m, prior_f, prior_m))这段代码的逻辑链条是读取数据 → 按类别分离 → 对每个类别做最大似然估计 → 用估计出的参数构造类条件概率密度 → 乘以先验得到后验 → 取后验大的类作为决策。参数方面np.var默认除以 N如果你想让实验报告更严谨可以改成np.var(x, ddof1)用无偏估计但要注意决策规则里的方差项要同步改。先验概率这里用样本比例估计如果训练集两类样本数差很多先验会明显偏向多数类这是实验报告里可以讨论的点。2.3 双特征下的决策面与错误率计算把体重也加进来后特征变成二维类条件概率密度变成二维正态分布需要估计均值向量和协方差矩阵。如果假设两类协方差矩阵相等决策面是直线如果不等决策面是二次曲线。代码里通常先实现协方差相等的情况因为计算简单且能画出直线边界。下面是对双特征版本的关键补充def mle_gaussian_2d(X): 二维正态分布的最大似然估计 mu np.mean(X, axis0) cov np.cov(X, rowvarFalse, biasTrue) # biasTrue对应有偏估计 return mu, cov def gaussian_pdf_2d(x, mu, cov): 二维正态分布概率密度 d len(mu) diff x - mu inv_cov np.linalg.inv(cov) det_cov np.linalg.det(cov) coef 1.0 / ((2 * np.pi) ** (d / 2) * np.sqrt(det_cov)) exponent -0.5 * diff inv_cov diff return coef * np.exp(exponent) # 双特征训练 X_f female[:, :2] X_m male[:, :2] mu_f2, cov_f2 mle_gaussian_2d(X_f) mu_m2, cov_m2 mle_gaussian_2d(X_m) # 在二维网格上计算决策边界 xx, yy np.meshgrid(np.linspace(140, 200, 200), np.linspace(30, 100, 200)) Z np.zeros_like(xx) for i in range(xx.shape[0]): for j in range(xx.shape[1]): point np.array([xx[i, j], yy[i, j]]) pf gaussian_pdf_2d(point, mu_f2, cov_f2) * prior_f pm gaussian_pdf_2d(point, mu_m2, cov_m2) * prior_m Z[i, j] 1 if pf pm else 0这段代码用网格法画出决策区域Z为 1 表示判为女性0 表示男性。实际跑的时候你会看到一条近似直线的分界但如果两类协方差差异大边界会弯曲。错误率计算就是拿测试样本跑一遍分类统计判错的个数除以总数。实验报告里要求“调整特征、分类器等方面因素考察性能影响”你可以做的对比包括只用身高 vs 只用体重 vs 双特征先验用样本比例 vs 等先验协方差相等 vs 不等。这些对比不需要改太多代码但能撑起实验报告的讨论部分。3. Fisher 线性判别与 KNN从降维投影到近邻投票3.1 Fisher 判别为什么值得单独实现一遍Fisher 线性判别的核心思想是找一个投影方向让两类投影后的均值差尽量大、类内方差尽量小。这个准则函数最终化成一个广义特征值问题解出来就是投影向量 w。很多人直接用 sklearn 的LinearDiscriminantAnalysis但实验课要求你手写一遍目的是理解“类内散度矩阵”和“类间散度矩阵”怎么构造。这套代码里 Fisher 部分通常和贝叶斯部分共用数据加载函数但计算流程完全不同。下面给出核心实现def fisher_lda(X_f, X_m): Fisher线性判别返回投影向量w和阈值 mu_f np.mean(X_f, axis0) mu_m np.mean(X_m, axis0) # 类内散度矩阵 Sw np.zeros((X_f.shape[1], X_f.shape[1])) for x in X_f: diff (x - mu_f).reshape(-1, 1) Sw diff diff.T for x in X_m: diff (x - mu_m).reshape(-1, 1) Sw diff diff.T # 类间散度矩阵 mu_diff (mu_f - mu_m).reshape(-1, 1) Sb mu_diff mu_diff.T # 求解 Sw^{-1} Sb 的特征值 eig_vals, eig_vecs np.linalg.eig(np.linalg.inv(Sw) Sb) w eig_vecs[:, np.argmax(eig_vals)].real # 投影后计算阈值 proj_f X_f w proj_m X_m w threshold (np.mean(proj_f) np.mean(proj_m)) / 2 return w, threshold w, thresh fisher_lda(X_f, X_m) print(投影向量:, w) print(分类阈值:, thresh) # 对测试样本分类 def fisher_classify(x, w, thresh): proj x w return Female if proj thresh else Male这里有几个容易翻车的点。第一np.linalg.eig返回的特征向量可能是复数取实部用.real是常见做法但更稳妥的是用np.linalg.eigh针对对称矩阵。第二Sw 可能奇异尤其当特征维度接近样本数时需要加正则项或者先做 PCA。第三阈值取两类投影均值的中点这是假设两类先验相等且方差相近时的简化如果先验差很多阈值要按先验调整。实验报告里可以对比 Fisher 和贝叶斯在同样数据上的错误率通常 Fisher 在正态且协方差相等时接近贝叶斯但协方差不等时会有差距。3.2 KNN 的参数选择与交叉验证KNN 看起来简单但 k 取多少、距离怎么定义、要不要加权这些都会影响结果。这套代码里 KNN 主要用在人脸识别部分但也可以先在身高体重数据上跑通流程。核心逻辑是对每个测试样本计算它到所有训练样本的距离取最近的 k 个看哪个类多就判哪个。下面是一个可复用的 KNN 实现def knn_classify(X_train, y_train, X_test, k3): KNN分类返回预测标签 predictions [] for x in X_test: distances np.linalg.norm(X_train - x, axis1) nearest_idx np.argsort(distances)[:k] nearest_labels y_train[nearest_idx] # 多数投票 counts np.bincount(nearest_labels) predictions.append(np.argmax(counts)) return np.array(predictions) # 构造训练集和测试集 X_all np.vstack([X_f, X_m]) y_all np.array([0]*len(X_f) [1]*len(X_m)) # 简单划分 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split(X_all, y_all, test_size0.3, random_state42) for k in [1, 3, 5, 7]: y_pred knn_classify(X_train, y_train, X_test, kk) acc np.mean(y_pred y_test) print(fk{k}, 准确率{acc:.4f})参数方面k 取奇数可以避免平票但这不是硬性要求因为多数投票遇到平票时可以随机选或按距离加权。距离默认用欧氏距离如果特征量纲差异大比如身高厘米和体重公斤必须先标准化否则体重的数值范围会主导距离计算。交叉验证部分代码里通常用KFold或StratifiedKFold把数据分成 N 折轮流做测试集。实验报告要求“用 N-交叉法估计准确率”你可以直接调 sklearn 的cross_val_score也可以手写循环。手写的好处是能控制每一折的随机种子方便复现。4. PCA 人脸识别降维维数与 k 值的联合调参4.1 人脸数据读取与预处理人脸识别部分的数据是 BMP 灰度图命名如 s1_1.bmp、s1_2.bmp……s40_10.bmp表示 40 个人每人 10 张不同表情或角度的照片。读取时用 PIL 或 OpenCV 都行关键是转成灰度并拉平成向量。每张图的大小通常是 92x112 或类似尺寸拉平后维度上万直接算距离会非常慢所以必须先做 PCA 降维。下面是从文件读取到构建数据矩阵的代码import os from PIL import Image import numpy as np def load_faces(data_dir): 读取ORL人脸库返回数据矩阵和标签 images [] labels [] for i in range(1, 41): for j in range(1, 11): filename fs{i}_{j}.bmp filepath os.path.join(data_dir, filename) if os.path.exists(filepath): img Image.open(filepath).convert(L) img_array np.array(img, dtypenp.float64).flatten() images.append(img_array) labels.append(i) return np.array(images), np.array(labels) X_faces, y_faces load_faces(./faces) print(数据矩阵形状:, X_faces.shape) # 应为 (400, 10304) 左右这里有个血泪经验BMP 文件如果路径不对os.path.exists返回 False代码会静默跳过最后数据量对不上。建议加一个计数打印确认读到了 400 张。另外图像拉平后数值范围是 0-255PCA 对量纲敏感但人脸像素本身同量纲所以不标准化也能跑不过中心化是必须的——PCA 内部会做但如果你手写 PCA记得先减去均值。4.2 PCA 降维与 KNN 分类的联合实验PCA 的核心是求协方差矩阵的特征向量取前 d 个最大特征值对应的向量组成投影矩阵。人脸数据维度上万直接算协方差矩阵是 10304x10304内存扛不住所以实际用 SVD 或者对样本矩阵做特征分解。下面用 sklearn 的 PCA 做演示但实验报告里通常要求手写一遍from sklearn.decomposition import PCA from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import cross_val_score, StratifiedKFold # 中心化 X_centered X_faces - np.mean(X_faces, axis0) # 对不同降维维数做实验 dimensions [10, 20, 30, 50, 80, 100, 150, 200] k_values [1, 3, 5, 7] results [] for d in dimensions: pca PCA(n_componentsd) X_pca pca.fit_transform(X_centered) for k in k_values: knn KNeighborsClassifier(n_neighborsk) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(knn, X_pca, y_faces, cvcv, scoringaccuracy) results.append((d, k, scores.mean())) print(fd{d}, k{k}, 准确率{scores.mean():.4f}) # 找最佳组合 best max(results, keylambda x: x[2]) print(f最佳降维维数{best[0]}, 最佳k{best[1]}, 最高准确率{best[2]:.4f})这段代码的逻辑是对每个降维维数 d先用 PCA 把数据投影到 d 维然后用 KNN 做 5 折交叉验证记录平均准确率。参数方面n_components如果设为 0.95 表示保留 95% 方差但实验要求对比不同维数所以直接指定整数。StratifiedKFold保证每折中每个类别的比例一致这对人脸识别很重要因为每人 10 张如果随机分可能导致某折里某人一张都没有。k 值的选择上k1 容易受噪声影响k 太大又会模糊类别边界通常 3 到 7 之间效果较好但具体要看降维后的数据分布。跑完这个实验你会得到一张二维表行是降维维数列是 k 值单元格是准确率。通常准确率随 d 增加先升后降因为太小的 d 丢失了判别信息太大的 d 引入了噪声和冗余。k 的影响相对平缓但 k1 往往偏低。实验报告里可以画热力图或者折线图来展示这个趋势。注意PCA 是无监督降维它不考虑类别标签所以降维后的前几个主成分未必对分类最有利。如果想让降维更有判别性可以换 LDA但那是另一个实验了。5. 避坑与排查跑这套代码时最容易翻车的五个地方5.1 数据文件路径和编码问题现象代码运行后报FileNotFoundError或者读到的数据为空。原因通常是 FAMALE.TXT 和 MALE.TXT 不在当前工作目录或者文件里有 BOM 头导致第一行解析失败。解决用绝对路径或者os.path.dirname(__file__)定位脚本所在目录读取时加encodingutf-8-sig处理 BOM。人脸图片同理确认 s1_1.bmp 到 s40_10.bmp 都在同一个文件夹里且文件名大小写一致。5.2 协方差矩阵奇异导致求逆失败现象贝叶斯双特征版本报LinAlgError: Singular matrix。原因当特征维度接近或超过样本数时协方差矩阵不满秩或者某类样本太少导致方差估计为 0。解决加一个很小的正则项比如cov np.eye(cov.shape[0]) * 1e-6或者先做 PCA 降维再估计协方差。Fisher 判别里 Sw 奇异也是同样处理。5.3 人脸数据维度顺序搞反现象PCA 跑完准确率极低接近随机猜。原因读取图片时把行和列搞反了或者拉平顺序不一致导致同一张图在不同步骤里向量表示不同。解决统一用img_array.flatten()不要混用flatten()和reshape(-1)虽然结果通常一样但万一中间有转置就全乱了。另外确认所有图片尺寸一致ORL 库是统一的但如果混入了其他来源的图尺寸不同会直接报错。5.4 交叉验证的随机种子没固定现象每次运行准确率都不一样实验报告里的数字对不上。原因train_test_split或KFold没有设random_state每次划分不同。解决所有涉及随机划分的地方都加random_state42这样别人复现你的结果时能得到同样的数字。如果要求更严格的复现连 numpy 的全局种子也设上np.random.seed(42)。5.5 KNN 距离计算未标准化导致特征主导现象在身高体重数据上KNN 几乎只按体重分类身高影响被淹没。原因体重数值范围可能是 40-100身高是 150-190欧氏距离里体重的差异被放大。解决用StandardScaler对特征做标准化或者手动(X - mean) / std。注意标准化参数只能从训练集计算然后应用到测试集否则会引入数据泄露。人脸数据因为像素同量纲可以不做标准化但中心化必须做。6. 把实验报告写成可复现的工程笔记我的参数扫描习惯这套代码最值钱的地方不是某个算法实现而是它把四个实验串成了一条线从低维特征到高维图像从参数估计到降维调参。我带人做实验时最怕看到的是“跑出结果就完事”报告里只贴一个准确率数字没有对比、没有分析。我的习惯是每跑完一个实验强制自己填一张参数扫描表。比如 PCA 人脸识别我会把降维维数和 k 值的组合全部跑一遍记录准确率然后找三个点最高点、次高点、明显下降的点分别分析原因。最高点说明该组合下判别信息和噪声达到了平衡次高点可能和最高点差不到 1%但降维维数少很多计算更快明显下降的点通常对应维数太低丢失了关键特征或者 k 太大导致欠拟合。另一个习惯是所有随机过程都固定种子并且把种子值写进报告。这样别人拿到你的代码跑出来的数字和你完全一致讨论才有意义。如果用了 sklearn 的交叉验证记得把每一折的索引也打印出来万一某折准确率异常低可以单独看那一折的测试样本是不是包含了难分类的个体。人脸识别里有些人戴眼镜、有些人光照不同这些因素会在某些折里集中出现导致准确率波动。把这些观察写进报告比单纯列数字有说服力得多。最后说一个我踩过的坑早期做 Fisher 判别时我直接用np.linalg.eig求特征向量结果投影方向偶尔会反向导致分类标签全反。后来每次求完特征向量都检查一下投影后两类的均值差是否为正如果为负就把 w 取反。这个检查只需要一行代码但能省掉半小时的调试。从那以后我每次做线性判别都强制走一遍符号校验希望帮到你。本文还有配套的精品资源点击获取