ARTICLE DETAIL

资讯详情

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

MATLAB人脸识别实战:PCA+LDA+最近邻分类完整实现

MATLAB人脸识别实战:PCA+LDA+最近邻分类完整实现 简介这是一份面向人脸识别初学者的Matlab项目源码包基于ORL标准人脸数据库配合PCA、LDA等经典算法帮助用户系统地理解特征提取、数据降维与分类识别的完整流程适合新手快速搭建个人实验环境也有一定经验的开发者作为参考。压缩包共8个文件其中5个m源文件分别覆盖数据预处理、PCA降维、LDA特征提取、模型预测和多分类流程1个mat文件提供可直接加载的人脸数据集另有readme说明与docx算法文档辅助阅读整体仅174KB轻量且便于快速部署。目前已有1486人学习浏览代码均经过作者校正并保证直接运行若下载后遇到问题可联系作者获得指导或更换。读者拿到手即可在Matlab中运行对照源码理解每一步的实现细节同时借助文档快速定位关键函数与整体目录结构无论是用于课程设计、毕业设计还是作为人脸识别入门实验都是相当完整的参考资料。1. 从ORL数据库到Matlab人脸识别一份能直接跑通的基线工程ORL人脸数据库是入门人脸识别绕不开的公开数据集40个人、每人10张、总共400张92x112的灰度图姿态和表情变化有限但光照相对统一。拿到手时它往往是一堆.pgm散文件真正麻烦的不是算法本身而是怎么把这些文件读进Matlab、怎么划分训练测试集、怎么保证实验可重复。这里拆解的这个Matlab项目把PCA、LDA和基于最近邻的分类器串成了一条完整流水线自带preprocess.m、PCA.m、LDA.m、predict.m和multi.m适合刚接触人脸识别的人做课程设计或快速验证特征提取思路。它解决的痛点很实际调试算法时大部分时间花在数据装载和矩阵维度匹配上而这份代码把这一层处理好了。2. ORL人脸数据库目录结构与preprocess.m装载逻辑2.1 400张灰度图的组织方式ORL数据库下载后是一堆文件夹比如s1到s40每个文件夹里是1.pgm到10.pgm。在Matlab中直接用dir读取文件夹返回的列表顺序是乱的s1、s10、s2会混在一起。常见做法是先用正则表达式把文件夹名里的数字提取出来再排序或者干脆用for循环拼接固定路径。preprocess.m通常要干三件事读取所有图像、把二维矩阵拉成一维列向量、将对应的类别标签同步生成。每张图92x112拉直后是10304维400张图就组成一个10304x400的样本矩阵对应的标签是1到40的类别向量。这个组织方式决定了后续所有矩阵运算的shape先想清楚再动手。在工程实现上我一般会避免用空矩阵拼接的方式累积数据因为images [images, img(:)]在循环里会反复分配内存400次还不明显但换到更大的数据库时就会很慢。更稳的做法是预分配一个zeros(10304, 400)的矩阵然后用索引写入。判断一个Matlab人脸识别代码是否专业第一眼就看有没有预分配矩阵。2.2 标准化而不是归一化很多入门代码会把图像像素除以255但这在PCA人脸识别里不是必需因为PCA对图像的均值平移敏感对尺度相对不敏感。preprocess.m里更关键的一步是按行减均值也就是把每个特征像素位置的均值从所有样本里减去让数据零均值。这样后续计算协方差矩阵时特征值才对应真正的方差方向。这里给出一段常见的读取实现function [images, labels] preprocess(root_path) % 返回的images每一列是一张拉直的人脸图 n_persons 40; n_per 10; images zeros(92*112, n_persons*n_per); labels zeros(n_persons*n_per, 1); idx 1; for i 1:n_persons for j 1:n_per img_path fullfile(root_path, [s num2str(i)], [num2str(j) .pgm]); img double(imread(img_path)); images(:, idx) img(:); labels(idx) i; idx idx 1; end end images images - mean(images, 2); % 减去平均脸 end这里的images是10304行、400列和第2.1节说的一致。mean(images, 2)会得到每一行像素点在400张图上的平均灰度减去之后后续PCA特征分解求的是偏离平均脸的方差结构。如果这里漏掉减均值第一特征脸会近似于平均脸而且识别率通常下降3到5个百分点。注意我把像素值转成了double因为pgm默认是uint8直接相减会变成0到255之外无法表达。如果你在调试中发现很多人脸图像显示出来是一团黑多半是数据类型问题。2.3 训练集与测试集划分的两种约定ORL官方没有规定训练集划分但最常见的两种方案是留出法和随机划分。留出法用每个人前5张做训练、后5张做测试好处是复现结果容易论文里写“5 train / 5 test”所有人都能理解。随机划分则是在每个人10张里随机抽5张训练其他测试需要设置随机种子才能保证实验可复现。preprocess.m只负责装载划分逻辑通常在PCA.m或multi.m里通过索引完成。如果发现识别率忽高忽低先检查是不是每次运行都在重新随机划分评估模型时要用多次随机划分的平均值。2.4 用Yale_32x32.mat做边界测试这个项目里还附带了一个Yale_32x32.mat可以用来验证代码在新的数据分布上是否依然成立。Yale库的图像尺寸被整理成32x32Matlab的load命令读入后一般会得到data矩阵和gnd标签向量data的行是样本、列是特征和ORL预处理后的方向正好相反。统一的方式是如果data是nSample x nFeature就转置成nFeature x nSample再进入后续流程。下表列出两者的差异方便切换数据库时少踩坑数据项ORLYale(整理后)图像尺寸92x11232x32类别数4015每类张数1011特征维度103041024数据形态目录pgm单个.mat切换时要改的参数主要是n_persons、n_per以及图像尺寸的前两行。如果不改加载.mat时会直接维度报错。另外Yale的人脸包含更多光照变化PCA在该库上的识别率通常低于ORL这是数据本身难度造成的不要怀疑代码坏了。3. PCA.m实现特征脸从10304维降到80维的取舍3.1 为什么用“小矩阵技巧”而不是直接特征分解直接对10304x10304的协方差矩阵做特征分解在普通电脑上会占用大量内存而且特征求解极慢。ORL数据库样本数N400维度D10304经典做法是先算一个400x400的Gram矩阵求出它的特征向量再左乘原始样本矩阵映射回10304维空间。PCA.m里通常就是这样实现的function [eigvecs, eigvals] PCA(X, k) % X: d x n每一列是已经减过均值的样本 [~, n] size(X); L X * X; % 构造 n x n 小矩阵 [V, D] eig(L); % V 的列是特征向量 [eigvals, idx] sort(diag(D), descend); V V(:, idx); eigvecs X * V(:, 1:k); % 映射回 d 维 for i 1:k eigvecs(:, i) eigvecs(:, i) / norm(eigvecs(:, i)); end eigvals eigvals(1:k); end第4行L矩阵的大小只有400x400eig计算几乎瞬间完成。第7行把n维向量映射回d维得到的就是特征脸方向。第8到10行归一化是为了让投影后的特征与特征值保持对应同时也便于把特征脸直接当图像显示。k是目标降维维数在ORL上我一般从80附近开始尝试。这里有个容易忽略的点Matlab的eig函数不保证特征值排序所以必须先对diag(D)排序否则取前k个可能是最小的特征值方向识别率会崩掉。3.2 主成分数量怎么定能量比与识别率要同时看人脸识别的PCA通常不按能量比选k而是交叉验证看识别率。能量比是特征值累积占比ORL上保留95%能量大约需要100个主成分但识别率峰值往往出现在60到80之间因为再往后的维度开始编码光照和噪声。可以画出能量曲线辅助判断cum_energy cumsum(eigvals) / sum(eigvals); figure; plot(cum_energy); xlabel(主成分数量); ylabel(能量累积占比);cumsum计算特征值的累积和cum_energy(k)表示前k个特征值占全部特征值的比例。这条曲线通常在100维附近已经接近0.95但不要只依赖这个数字。更直接的方法是写一个循环对k 40:20:120分别训练分类器记录识别率。ORL上常见结果是k40时欠拟合k80时到达平台期k120后识别率有小幅波动甚至下降因为更多维度引入了类内噪声。3.3 特征脸可视化与数据类型的坑把eigvecs的第i列reshape成92x112再用imagesc显示就能看到特征脸。误区在于特征脸数值有正有负直接显示会全黑或全灰需要先做灰度拉伸。常见做法是eigface reshape(eigvecs(:, 1), [112, 92]); % 注意行列顺序 imshow(mat2gray(eigface));这里reshape的尺寸顺序是[112, 92]对应原图的高和宽。如果写成[92, 112]图像会被转置虽然不影响识别率但可视化时看着别扭。mat2gray会把最小值映射到0最大值映射到1方便显示。另一个常见的坑如果在PCA前忘了减均值第一特征脸会近似于整张平均脸而不是像五官差异方向从图上就能看出来不用等到算识别率。3.4 PCA输出到底给谁用PCA.m的输出一般有两个去向一个是直接投影训练和测试样本交给predict.m做最近邻分类这是最常用的基线另一个是作为LDA.m的前置降维步骤先把维度降到N-c避免类内散度矩阵奇异。前者的投影代码通常在PCA.m内部就完成后者的LDA.m则需要同时接收训练样本和测试样本。在multi.m里两种投影结果分别保留最后做分数级融合。这个结构让代码复用起来很自然想单测PCA效果就把PCA.m输出接到predict.m想跑Fisherface就再接一层LDA.m。4. LDA.m与predict.mFisherface线性判别与最近邻分类4.1 类内散度与类间散度的矩阵构造PCA不关心类别标签所以它对光照变化比较敏感。LDA把人脸看作带标签的样本目标是找到一个投影方向使得投影后同一类样本尽量聚集不同类样本尽量分开。对ORL的40类问题LDA的投影方向数最多是类别数减1也就是39维。LDA.m的第一步是构造类内散度矩阵Sw和类间散度矩阵Sb然后求广义特征值分解。function W LDA(X, labels, dim) % X: d x n已通过PCA降维避免 Sw 奇异 c max(labels); [d, n] size(X); Sw zeros(d, d); Sb zeros(d, d); mu mean(X, 2); for i 1:c Xi X(:, labels i); mu_i mean(Xi, 2); Sw Sw (Xi - mu_i) * (Xi - mu_i); Sb Sb size(Xi,2) * (mu_i - mu) * (mu_i - mu); end [V, D] eig(Sb, Sw); [~, idx] sort(diag(D), descend); W V(:, idx(1:dim)); end先看循环内部。Xi是第i个人的所有样本mu_i是该类平均脸。Sw累积每个样本偏离自己类均值的平方和代表类内离散程度。Sb累积的是每个类均值偏离全局均值的平方和还要乘以该类的样本数。eig(Sb, Sw)求解广义特征值特征值大的方向意味着类间散度相对类内散度更高。注意dim最大只能取c-1因为Sb的秩受限于类别数取多了会出现零特征值方向。实际调用时先跑一遍PCA将维度降到N-c360再进LDA降到39这样Sw是满秩的数值稳定得多。4.2 predict.m余弦距离和最近邻predict.m接收训练投影、训练标签和测试投影返回预测类别。人脸识别里余弦距离比欧氏距离更稳定因为投影系数的模长可能受到光照和对比度影响而方向夹角更能反映结构相似性。常见实现function pred predict(train_proj, train_labels, test_proj) n_test size(test_proj, 2); pred zeros(n_test, 1); train_norm sqrt(sum(train_proj.^2, 1)); for i 1:n_test tt test_proj(:, i); t_norm norm(tt); % 余弦相似度越接近1越相似取最大 sim (train_proj * tt) ./ (train_norm * t_norm); [~, idx] max(sim); pred(i) train_labels(idx); end end这里sim是与所有训练样本的余弦相似度向量train_proj * tt是向量内积train_norm * t_norm是模长乘积。内积运算在Matlab里比循环快很多所以一次性算完。用欧氏距离时只需把sim替换成-sum((train_proj - tt).^2, 1)同样的取最大逻辑。最近邻分类不需要训练过程所以predict.m很简单核心在于train_proj和test_proj必须是同一个W投影出来的否则相似度没有任何意义。4.3 不同特征方案的识别率对照在ORL数据库上常见配置下的识别率有一个经验区间。我用这个项目多次随机划分后得到的大致结果如下供调参时对照特征方案每类5张训练每类7张训练说明原始像素最近邻86%-91%91%-94%维度高计算慢容易过拟合PCA(80维)最近邻93%-96%95%-97%稳定适合作为基线PCA(80维)LDA(39维)最近邻95%-98%97%-99%LDA提升明显但更依赖训练样本数如果你的结果明显低于表中下限先检查两个常见问题第一训练集和测试集是否发生了重叠随机划分时索引写错会让同一张图同时出现在两边第二LDA的输入是否来自PCA降维后的特征直接对10304维原始数据做LDA会产生奇异矩阵Matlab会给出警告或返回NaN。表中的数据不是官方基准只是这个项目复现时的参考区间但偏差超过5个百分点时基本可以断定是预处理或划分逻辑的问题。4.4 LDA对训练集规模的敏感度LDA在每类样本只有3到5张时类内散度估计非常不稳定。ORL每类10张还算够用如果用到Yale的11张也要注意类别数只有15投影维度上限降低。当训练样本过少时LDA的识别率可能反而不如PCA因为广义特征分解在噪声较大的方向上产生了过拟合的投影轴。multi.m里保留PCA分支的用意就在这里即使LDA失效PCA分支还能把识别率稳定在90%以上。5. multi.m多方法融合分数级投票与交叉验证调参5.1 为什么用分数级融合而不是硬投票multi.m的作用是把PCA和LDA两个分支的预测结果融合起来。最简单的是硬投票每个分支输出一个类别票多者胜。但ORL有40类投票平票的概率很高而且两个分支可能同时犯错。更稳的是分数级融合把每个测试样本对所有训练样本的相似度矩阵归一化到[0,1]再加权求和得到一个新的得分矩阵然后取最大值得出类别。这样保留了“第二名和第一名差多少”的信息。一个参考结构function pred multi(sim_pca, sim_lda, train_labels, w) % sim_pca / sim_lda: n_train x n_test 的相似度矩阵 sp (sim_pca - min(sim_pca, [], 1)) ./ ... (max(sim_pca, [], 1) - min(sim_pca, [], 1)); sl (sim_lda - min(sim_lda, [], 1)) ./ ... (max(sim_lda, [], 1) - min(sim_lda, [], 1)); score w(1) * sp w(2) * sl; [~, idx] max(score, [], 1); pred train_labels(idx); end归一化这里用的是min-max对每一个测试样本的得分向量做缩放。sp和sl都变成0到1之间然后按w加权。w默认可以取[0.5, 0.5]但如果在验证集上发现LDA分支波动大就加大PCA分支的权重比如[0.6, 0.4]。score矩阵的第i列代表第i个测试样本对每个训练样本的融合相似度max取每一列的最大值索引再映射到类别。要注意相似度矩阵的行方向是训练样本列方向是测试样本方向反了会得到完全不同的结果。5.2 用5折交叉验证搜索k和权重这个项目没有内置自动调参但可以自己写一个轻量交叉验证循环。将每个人的人脸分成5折每份包含2张轮流作为测试集其余8张做训练。对每个候选的PCA维数k和权重w计算平均识别率最后选最优组合。搜索范围建议是k从40到120步长20w从0到1步长0.1。由于ORL数据量小一次完整搜索在普通电脑上只需几分钟。实际跑下来k80、w0.6左右通常是一个稳定的工作点但这会随着预处理细节变化不要照搬。5.3 快速自检用一个样本验证投影方向一致性调参时最隐蔽的问题是两个分支使用了不同版本的训练集导致投影方向不对齐。一个快速自检方法固定随机种子后取第一个人的第一张训练图把它同时投影到PCA和LDA空间再分别计算它与同一张图的余弦相似度理论值应该是1。如果出现NaN或负数说明归一化或特征向量构造有误。另一个自检是打印投影后矩阵的尺寸PCA投影应为k x nLDA投影应为(c-1) x n。只要尺寸和数值范围符合预期multi.m的融合结果才有意义。本文还有配套的精品资源点击获取
返回列表