模式识别实验代码:Bayes、Fisher、KNN与PCA人脸识别实战
简介基于Python的模式识别实验代码包直接对应本科模式识别课程常见实验任务适合计算机/人工智能相关专业学生对照学习与完成实验报告。代码包围绕三类典型实验展开基于FAMALE.TXT与MALE.TXT建立Bayes性别分类器并测试不同特征、分类器参数对性能的影响在正态分布假设下利用最大似然法估计概率密度设计最小错误率贝叶斯分类器以及用PCA对人脸数据降维、结合k-近邻法完成人脸识别并通过N-交叉法比较不同降维维数和k值的准确率。代码还涵盖Fisher、KNN等经典方法附带实验报告可直接运行验证。压缩包共466个文件以400个bmp人脸图像数据、16个py源码、13个txt样本数据、5个docx实验报告为主另有xml、iml等项目配置整体大小仅5.7MB轻量易用。已有1631人学习/下载对需要快速搞定模式识别实验的同学来说是一份完整、省心的参考。1. 一门实验课四套能跑的代码省下的时间够你调一周参手里这套基于 Python 的模式识别实验代码覆盖了 Bayes 分类器性别分类、Fisher 线性判别、KNN 和 PCA 人脸识别四个经典实验。每个实验都配了可直接运行的数据文件和实验报告模板其中性别分类部分用FAMALE.TXT和MALE.TXT作为训练集人脸识别部分自带 30 个样本的 BMP 图像。对于正在修《模式识别》课程、被实验报告和代码双重夹击的学生来说这套东西的价值在于你不用从零去啃理论公式再写实现而是先把跑通流程再回头调特征、调参数、观察性能变化——这正是课程要求“考察分类器性能影响因素”的核心训练。即便有五年以上开发经验看这套代码的组织方式也有参考意义它把数据加载、模型训练、评估串成了一条干净的主线没有多余的框架依赖。2. 最小错误率 Bayes 分类器从正态分布假设到性别决策规则2.1 为什么先用最大似然法估计概率密度性别分类是一个经典的两类问题。假设男性身高x服从正态分布N(μ1, σ1^2)女性身高服从N(μ2, σ2^2)那么只需要从训练样本中估计出四个参数两个均值、两个方差就能写出最小错误率 Bayes 决策规则。这里的假设是“正态分布”而参数估计用的是最大似然法MLE因为在高斯分布下MLE 得到的均值就是样本均值方差就是样本方差——闭式解不需要迭代作为课程实验非常合适。FAMALE.TXT和MALE.TXT每行通常存两个特征身高cm和体重kg。实验要求单独用身高或体重做一维特征也要求组合成二维特征。下面代码先把数据读进来按特征维度做 MLE 估计import numpy as np def load_data(path): # 每行两个浮点数身高(cm) 体重(kg) data np.loadtxt(path, dtypefloat) return data[:, 0], data[:, 1] # 返回身高、体重 # 读入两类训练样本 male_h, male_w load_data(MALE.TXT) female_h, female_w load_data(FAMALE.TXT) # 最大似然估计高斯分布下均值样本均值方差样本方差除以N不是N-1 mu_m_h, var_m_h np.mean(male_h), np.var(male_h) mu_f_h, var_f_h np.mean(female_h), np.var(female_h) print(fMale height: mu{mu_m_h:.2f}, var{var_m_h:.2f}) print(fFemale height: mu{mu_f_h:.2f}, var{var_f_h:.2f})这段代码用np.loadtxt解析纯文本数据返回两个一维数组。注意这里方差用np.var而不是样本方差因为 MLE 估计的就是总体方差的似然值分母是 N 而不是 N-1。做实验报告时如果你用np.std(..., ddof1)得到的决策边界会略有偏移需要明确说明你用的是哪种估计量。2.2 决策规则与错误率计算得到参数后最小错误率决策规则是比较后验概率P(男|x)和P(女|x)谁大判给谁。在两类先验概率相等的情况下等价于比较似然值。对数化之后决策边界是一个二次函数若两类方差相等退化为线性。实验要求写出“决策规则”我在报告中会这样呈现def bayes_decision(x, mu1, var1, mu2, var2, p10.5, p20.5): # 计算两类高斯似然取对数便于数值稳定 log_lik1 -0.5 * np.log(2 * np.pi * var1) - (x - mu1) ** 2 / (2 * var1) log_lik2 -0.5 * np.log(2 * np.pi * var2) - (x - mu2) ** 2 / (2 * var2) # 加上先验对数得到后验对数此处先验相等可省略 g1 log_lik1 np.log(p1) g2 log_lik2 np.log(p2) return 1 if g1 g2 else 2 # 1男, 2女 # 测试一组新样本 test_heights [165.0, 175.0, 180.0, 158.0] for h in test_heights: pred bayes_decision(h, mu_m_h, var_m_h, mu_f_h, var_f_h) print(fheight{h:.1f} - {Male if pred 1 else Female})bayes_decision函数的核心是计算对数似然差。np.log(2 * np.pi * var)是高斯分布归一化常数的对数在比较时可以省略因为两类对同一个 x 而言是常数但保留可以让你直接看到数值。使用p1p20.5表示男女人数相当如果训练集本身不平衡需要按实际比例设置先验。当实验要求用身高体重二维特征时需要估计二维高斯分布的均值向量和协方差矩阵。下面用np.cov得到协方差并基于马氏距离做决策def load_2d(path): return np.loadtxt(path, dtypefloat) X_m load_2d(MALE.TXT) X_f load_2d(FAMALE.TXT) # 二维MLE均值向量与协方差矩阵 mu_m np.mean(X_m, axis0) mu_f np.mean(X_f, axis0) cov_m np.cov(X_m.T, biasTrue) # biasTrue 对应MLE cov_f np.cov(X_f.T, biasTrue) def gauss_2d(x, mu, cov): d len(mu) diff x - mu # 行列式可能接近0加一个小对角阵避免奇异 cov cov 1e-6 * np.eye(d) inv np.linalg.inv(cov) norm_const 1.0 / (np.sqrt((2 * np.pi) ** d * np.linalg.det(cov))) return norm_const * np.exp(-0.5 * diff inv diff) def predict_2d(x): p_m gauss_2d(x, mu_m, cov_m) p_f gauss_2d(x, mu_f, cov_f) return 1 if p_m p_f else 2二维情形下biasTrue让np.cov使用 N 作为分母与一维np.var保持一致。协方差矩阵奇异时加1e-6 * np.eye(d)是常见做法能防止np.linalg.det为零导致除零错误。测试时把每个样本的二维坐标代入predict_2d统计错误个数除以总数就得到测试错误率。2.3 调整特征对分类器性能的影响实验要求“调整特征、分类器等方面的一些因素”最常见的是对比一维身高、一维体重、二维身高体重三种情况下的错误率。我写了一个评估函数def evaluate(predictor, samples, labels): errors sum(1 for x, y in zip(samples, labels) if predictor(x) ! y) return errors / len(labels) # 以测试集为例这里用训练集代替演示 male_samples [ (h, w) for h, w in zip(male_h, male_w) ] female_samples [ (h, w) for h, w in zip(female_h, female_w) ] all_samples male_samples female_samples all_labels [1] * len(male_samples) [2] * len(female_samples) err_2d evaluate(predict_2d, all_samples, all_labels) print(f2D feature error rate: {err_2d:.2%})注意这里用训练集评估得到的是训练错误率会偏低。报告中应该用留出法划分训练/测试集或者直接使用实验提供的数据划分。通常结论是二维特征的错误率低于任意单一特征因为身高体重之间存在互补信息。3. Fisher 线性判别把高维投影到一条直线上求阈值3.1 Fisher 准则与投影方向Fisher 线性判别的思想是找一个投影方向w让两类样本投影后的均值差尽量大、类内散度尽量小。与 Bayes 分类器不同Fisher 不假设数据服从正态分布只关注一阶矩和二阶矩。定义类间散度矩阵S_B和类内散度矩阵S_W最优投影为w S_W^{-1} (μ1 - μ2)实操时我先从训练数据计算均值向量和协方差然后直接求解w。下面用身高体重二维数据演示def fisher_w(X1, X2): mu1 np.mean(X1, axis0) mu2 np.mean(X2, axis0) # 类内散度矩阵 两类协方差之和MLE形式 S_W np.cov(X1.T, biasTrue) np.cov(X2.T, biasTrue) diff mu1 - mu2 w np.linalg.inv(S_W) diff # 将w归一化使投影坐标可视化时长度有参考 w w / np.linalg.norm(w) return w, mu1, mu2 w, mu_m, mu_f fisher_w(X_m, X_f) print(Fisher projection direction:, w) # 投影到一维 proj_m X_m w proj_f X_f w这里S_W是 2×2 矩阵np.linalg.inv求逆。归一化w不是必须的但会让投影值范围更直观。投影后两类数据变成一维标量接下来需要确定决策阈值。3.2 阈值选择与分类结果Fisher 本身不给出阈值常见做法是用两个类均值投影点的中点作为阈值或者用 ROC 曲线选择最优阈值。最省事的是用中点def fisher_predict(x, w, threshold): proj x w return 1 if proj threshold else 2 threshold (mu_m w mu_f w) / 2 pred_m np.array([fisher_predict(x, w, threshold) for x in X_m]) pred_f np.array([fisher_predict(x, w, threshold) for x in X_f]) acc_m np.mean(pred_m 1) acc_f np.mean(pred_f 2) print(fMale acc: {acc_m:.2%}, Female acc: {acc_f:.2%})用中点作为阈值隐含了一个假设两类样本量相近且投影后方差相近。如果你的数据不满足可以尝试搜索让整体错误率最小的阈值all_proj np.concatenate([proj_m, proj_f]) labels np.array([1] * len(proj_m) [2] * len(proj_f)) best_th, best_err None, 1.0 for th in np.linspace(all_proj.min(), all_proj.max(), 200): preds np.where(all_proj th, 1, 2) err np.mean(preds ! labels) if err best_err: best_err, best_th err, th print(fBest threshold{best_th:.2f}, min error{best_err:.2%})这段网格搜索是在投影空间里找 200 个候选阈值适合样本量不大的情况。注意这样选出的阈值会在训练集上过拟合实验报告里要说清楚这是训练阈值测试时用固定值。3.3 Fisher 与 Bayes 在性别分类上的边界差异当两类协方差矩阵相同时Bayes 决策边界是线性的Fisher 投影方向与 Bayes 线性边界法向量一致。但 Fisher 只用一个标量阈值而二维 Bayes 用的是完整二次边界协方差不同时所以 Fisher 对非线性边界的表达能力有限。我在报告中做了一张对比表分类器数据假设决策边界形式需要估计的参数适合场景Bayes二维高斯各类正态分布且协方差可不同二次曲线均值×2、协方差×2数据确实近似高斯Fisher仅考虑一阶二阶矩线性超平面 阈值均值×2、类内散度样本量小、快速验证KNN无参数假设距离度量决定的局部边界无训练参数边界复杂但样本密度够这张表写进实验报告能让老师觉得你理解了两者的理论区别。实际运行中Fisher 的错误率往往略高于 Bayes但训练速度快一个数量级适合特征维度很高、需要快速降维的场景。4. KNN 与 PCA 人脸识别降维多少维k 取多少4.1 从 BMP 图像到特征向量人脸识别部分用s21_4.bmp这类灰度人脸图。每张图比如 112×92 像素展开成 10304 维向量。直接在原始像素上算 KNN距离度量会受光照、背景干扰而且计算开销大。所以先做 PCA 降维。数据文件名中的s21代表人编号_4代表该人的第几张样本。代码要先统一读取所有 BMPimport os from PIL import Image def load_faces(data_dir): files sorted(os.listdir(data_dir)) X, y [], [] for f in files: if f.lower().endswith(.bmp): img Image.open(os.path.join(data_dir, f)).convert(L) # 灰度 X.append(np.array(img).flatten()) person_id int(f.split(_)[0][1:]) # s21_4.bmp - 21 y.append(person_id) return np.array(X, dtypenp.float64), np.array(y) X, y load_faces(faces/) print(Data shape:, X.shape) # (N, 10304)用PIL读图并转成灰度flatten()后每张图是一个长向量。person_id解析文件名时要保证零填充格式一致比如s04_8.bmp会被解析为 4注意与s4_8.bmp区分——如果命名不规范需要按完整字符串映射类别。4.2 PCA 主成分的计算与维度选择PCA 的中心化是第一步每个特征像素减去训练集该位置的均值然后计算协方差矩阵的特征向量。但 10304 维协方差矩阵是 10304×10304直接求特征分解非常慢。常见做法是用 SVD 对数据矩阵做分解因为左奇异向量就是协方差矩阵的特征向量def pca(X, n_components): # X: (N, D) 每一行是一个样本 mean np.mean(X, axis0) X_centered X - mean # 用SVD求主方向比直接求协方差特征分解快得多 U, S, Vt np.linalg.svd(X_centered, full_matricesFalse) return Vt[:n_components], mean n_comp 20 components, mean_face pca(X, n_comp) print(Components shape:, components.shape) # 投影到20维 X_proj (X - mean_face) components.Tnp.linalg.svd返回的Vt行向量是主成分方向。full_matricesFalse避免生成冗余的零行节省内存。注意这里用X_centered的 SVD而不是np.cov是因为当 D N 时样本数只有几十个协方差矩阵的秩最多 N-1SVD 直接给出非零奇异值对应的主成分。n_components可设置为小于等于 N 的数超过 N-1 无意义。选择合适的降维维度常见做法是看累计方差贡献率eigen_vals S ** 2 / (len(X) - 1) # 特征值 奇异值平方/(n-1) cumsum np.cumsum(eigen_vals) / np.sum(eigen_vals) for dim in [5, 10, 20, 30, 50]: if dim len(eigen_vals): print(fTop {dim} dims explain {cumsum[dim-1]:.1%} variance)累计方差贡献率是一个经验指标人脸识别通常取 95% 或者固定维度如 20、30。但注意高方差的方向并不一定是最利于分类的方向所以实验要求“对不同降维维数下的准确率进行比较”而不是只看贡献率。4.3 N-交叉验证下的 KNN 准确率比较KNN 分类器没有训练过程只需要存储所有训练样本的特征和标签。预测时计算测试样本到所有训练样本的欧氏距离取前 k 个近邻投票。人脸识别中样本量少每个类只有 10 张左右N-交叉验证留一法是最常见的评估方式每次留一个人脸作为测试其余训练。def knn_predict(X_train, y_train, x_test, k): diff X_train - x_test dists np.linalg.norm(diff, axis1) top_k np.argsort(dists)[:k] labels y_train[top_k] # 返回出现次数最多的标签出现次数相同时取距离和最小的 unique, counts np.unique(labels, return_countsTrue) return unique[np.argmax(counts)] def leave_one_out(X, labels, k, n_comp): # 先做PCA注意L准则每次留出测试样本时PCA只能由训练集拟合 correct 0 n len(X) for i in range(n): train_idx [j for j in range(n) if j ! i] # 在训练集上做PCA mean np.mean(X[train_idx], axis0) X_c X[train_idx] - mean _, _, Vt np.linalg.svd(X_c, full_matricesFalse) W Vt[:n_comp] # 投影训练集和测试样本 train_proj (X[train_idx] - mean) W.T test_proj (X[i] - mean) W.T pred knn_predict(train_proj, labels[train_idx], test_proj, k) if pred labels[i]: correct 1 return correct / n关键细节PCA 的均值和主成分必须只用训练集计算不能把测试样本混进来否则会低估测试误差。上面代码在每次留出循环里重新做 PCA代价是 O(n × SVD)但样本量小完全可接受。如果你图快可以先在全部数据上做一次 PCA 再留一法但实验结果会偏乐观与课程期望的做法不符。我跑一遍 10、20、30 维k 取 1、3、5得到如下表格数据为示例实际以你的数据集为准PCA 维度k1k3k5568.0%65.3%62.0%1082.7%84.0%80.0%2089.3%90.7%88.0%3088.0%89.3%86.7%可以看到维度从 5 升到 20 时准确率明显提升但继续升到 30 反而下降这就是“维度诅咒”的一个具体体现——加入的维度包含噪声而 KNN 对噪声敏感。k 值增大通常能平滑边界但 k 太大会把决策边界拉平导致少数类别被淹没。5. 把实验报告写出区分度参数敏感性分析与留一法陷阱这一章分享两个写报告时特别容易踩的坑以及一个提升报告质量的技巧。第一个坑是 PCA 的标准化问题。原始像素值范围是 0255方差很大而 SVD 对量纲敏感。如果数据集中混入了不同亮度的人脸比如有人戴眼镜、有人光线暗直接 PCA 的前几个主成分会被整体亮度占据识别率不稳定。我这里通常对每个样本做零均值归一化即把每个样本的像素值减去该样本自身均值后再除以标准差X_norm (X - X.mean(axis1, keepdimsTrue)) / (X.std(axis1, keepdimsTrue) 1e-8)再把这个X_norm作为后续 PCA 的输入。实验报告里可以对比“不归一化”和“归一化”在留一法下的准确率差异这个对比本身就是一处加分点。第二个坑是交叉验证的分层问题。人脸数据中如果按每人的样本连续排列留出法时需要保证测试样本所在类的其余样本保留在训练集否则无法识别——因为测试类从未出现过。正确的 N-交叉验证应该保证训练集中包含该类别的其他样本。上面给出的leave_one_out正是基于这个逻辑。你可以在报告中写明本次实验使用样本留一而非类别留一因为目标是判断某人的某张照片能否被正确识别而不是判断一个陌生人属于哪一类。最后一个技巧是画投影后的二维散点图。把 Fisher 得到的w与 PCA 前两个主成分结合先用 PCA 降到 2 维再投影到 Fisher 方向上用不同颜色标出男女两类样本。图放在实验报告里一眼就能看出线性不可分的程度比纯文字说明更直观。代码非常简单不需要额外标题说明import matplotlib.pyplot as plt X_pca2 (X - mean_face) components[:2].T for label in [1, 2]: idx y label plt.scatter(X_pca2[idx, 0], X_pca2[idx, 1], labelfclass {label}) plt.legend() plt.savefig(pca_scatter.png, dpi150)保存的图片直接插入报告结果分析部分。整套代码跑完把上面几张表和图填充进实验报告模板你的实验课评分不会差——重点在于每个结果都对应了一个参数调整这正好命中课程的教学目标。本文还有配套的精品资源点击获取