
人脸识别这个词听起来挺唬人但真动手做起来最经典的那套入门方案反而没那么玄乎。我最早接触这个方向的时候以为非得搞个深度网络、跑上几十万张图才能出效果后来发现用PCA主成分分析这套老方法在标准人脸数据集上照样能跑到九成以上的识别率而且整个流程用Python几十行就能跑通。这篇内容就是把我自己从零搭这套东西的完整过程拆开讲——包括PCA到底在人脸识别里干了什么、为什么它管用、特征脸是怎么回事、实际编码时哪些参数会坑你、以及怎么评估识别效果。适合刚入门计算机视觉的学生、想找个练手项目的开发者或者单纯好奇机器怎么认人的技术爱好者。不需要你有深度学习基础但得会一点Python和NumPy的基本操作。1. 先搞清楚PCA在人脸识别里到底扮演什么角色1.1 一张人脸图片在计算机眼里是什么一张灰度人脸图片假设尺寸是100×100像素在计算机里就是一个10000维的向量。每个像素的灰度值就是这一维的数值范围0到255。你想想10000维是什么概念——我们平时画个二维散点图就已经觉得挺直观了三维勉强能在脑子里转一转10000维的空间根本没法想象。但数学上它就是存在的而且人脸识别的问题本质上就是在这个超高维空间里做分类。问题来了在这个10000维空间里任意两张不同的人脸图片它们之间的欧氏距离可能都差不多大。因为维度太高像素级的差异被均匀分散到了各个维度上导致同一个人不同角度拍的两张照片和不同人拍的两张照片在距离上区分度很低。这就是所谓的维度灾难。PCA要解决的核心问题就是能不能用远低于10000维的一组新坐标来表示人脸同时保留住区分不同人所需的关键信息1.2 PCA降维的直觉理解PCA做的事情用一句话说就是找到一组新的坐标轴让数据在这些轴上的投影尽可能分散。为什么分散很重要因为如果所有人脸在某个方向上的投影都挤在一起那这个方向对区分不同人就没有贡献。反过来投影越分散的方向越能体现人脸之间的差异。打个比方。假设你要区分一群人的身高和体重但只能用一个指标。显然你会选一个综合了身高和体重的指标而不是选鞋码这种跟体型关系不大的。PCA就是在自动帮你找这种最有区分力的综合指标。它找到的第一个新轴叫第一主成分是数据方差最大的方向第二个轴叫第二主成分是与第一主成分正交且方差次大的方向以此类推。在人脸识别的语境下这些主成分有一个很形象的名字——特征脸Eigenfaces。因为每一个主成分向量如果还原成图片尺寸看起来就像一张模糊的、幽灵般的人脸。第一特征脸通常反映的是整体光照方向第二特征脸可能反映的是人脸左右对称性的差异后面的特征脸逐渐捕捉更细节的特征。1.3 为什么不用原始像素直接做分类有人可能会想我直接把10000维的像素向量丢给一个分类器不就行了为什么要多此一举做PCA原因有三个。第一计算量。10000维的协方差矩阵是10000×10000求特征向量在计算上非常吃力。而PCA降维之后可能只需要保留100到200个主成分计算量下降两个数量级。第二过拟合。维度越高分类器需要估计的参数越多在训练样本有限的情况下极易过拟合。你可能有几百张训练图但在10000维空间里这几百个点稀疏得可怜分类器随便画个超平面就能完美分开训练集但换到测试集就崩了。第三噪声。原始像素里包含大量与身份无关的信息——比如传感器噪声、轻微的位移、背景的杂物。PCA保留方差大的方向相当于把主要的结构信息留下把噪声压缩掉。注意PCA是无监督方法它降维时并不看标签只是找方差最大的方向。这意味着它保留下来的信息未必都对分类有用。这是PCA的固有局限也是后来LDA线性判别分析等方法出现的原因。2. 从零搭建PCA人脸识别流水线2.1 数据准备与预处理的几个关键决策我用的是一组标准的人脸数据集里面包含若干个人的多张正面灰度照片每人拍摄时的光照、表情、是否戴眼镜等条件都有变化。拿到数据后的第一步不是急着写PCA而是把预处理做扎实这一步偷懒后面全白搭。尺寸统一是必须的。原始图片尺寸可能不一致得全部缩放到相同大小。我选的是100×100这个尺寸在保留足够辨识信息和控制计算量之间比较平衡。如果你用更大的尺寸比如200×200维度直接变成40000PCA的计算时间会明显增加但识别率的提升未必显著。灰度化也是常规操作。彩色图片有三个通道直接展平会变成三倍维度而且颜色信息对人脸识别的贡献远不如纹理和形状。转成灰度后每个像素只有一个值处理起来干净利落。直方图均衡化这一步很多人会忽略但对识别率影响很大。不同照片的拍摄光照条件不同有的偏亮有的偏暗直方图均衡化可以把每张图的灰度分布拉均匀减少光照差异带来的干扰。实测下来加不加这一步识别率能差好几个百分点。向量化就是把每张100×100的图片展平成10000维的向量。把所有训练图片的向量按行堆叠得到一个矩阵行数是图片数量列数是10000。这个矩阵就是PCA的输入。2.2 计算流程的每一步在做什么PCA的计算流程说起来不复杂但每一步的数学含义值得掰开讲。第一步计算均值脸。把所有训练图片的向量加起来除以图片数量得到一张平均脸。这张脸看起来就是一团模糊的人脸轮廓因为它把所有个体差异都平均掉了。均值脸的作用是作为基准后续所有图片都要减去它这叫中心化。中心化的目的是让PCA关注的是差异而不是绝对位置。第二步计算协方差矩阵。中心化后的数据矩阵记为X形状是样本数n维度d。协方差矩阵就是X的转置乘以X再除以n-1得到一个d×d的矩阵。这个矩阵的每个元素表示两个像素维度之间的相关性。但d10000时这个矩阵是10000×10000直接求特征向量非常慢。第三步用奇异值分解SVD绕过巨型协方差矩阵。这是实操中最重要的一个技巧。我们不直接对10000×10000的协方差矩阵做特征分解而是对中心化后的数据矩阵Xn×d做SVD。SVD会给出三个矩阵Un×n、S对角矩阵、Vtd×d。其中Vt的前k行就是我们要的前k个主成分方向。这个技巧之所以有效是因为n通常远小于d比如n200d10000对X做SVD的计算量远小于对协方差矩阵做特征分解。第四步选择保留多少个主成分。这是一个需要权衡的决策。保留太少信息丢失严重识别率上不去保留太多噪声和冗余信息被带进来计算量也大。常用的方法是看累计方差贡献率——把每个主成分对应的奇异值平方除以所有奇异值平方之和得到每个主成分解释了多少比例的方差。通常保留累计贡献率达到90%到95%的主成分数量。第五步投影。把每张训练图片的向量减去均值脸然后乘以保留的主成分矩阵形状是d×k得到一个k维的向量。这个k维向量就是这张人脸在特征脸空间中的坐标也叫特征脸权重向量。后续的分类就在这个k维空间里做。2.3 代码实现中的关键片段下面这段代码展示了核心的PCA训练过程。我用的是NumPy手动实现而不是直接调sklearn的PCA原因是手动实现能让你看清楚每一步在干什么遇到问题时也知道去哪里调。import numpy as np def train_pca(X, n_components0.95): X: 训练数据矩阵形状为 (n_samples, n_features) n_components: 如果为0到1之间的小数表示保留的方差比例 如果为整数表示保留的主成分个数 返回: 均值向量、投影矩阵、投影后的训练数据 # 计算均值脸 mean_face np.mean(X, axis0) # 中心化 X_centered X - mean_face # 对中心化后的矩阵做SVD U, S, Vt np.linalg.svd(X_centered, full_matricesFalse) # 计算累计方差贡献率 explained_variance (S ** 2) / (X.shape[0] - 1) cumulative_variance np.cumsum(explained_variance) / np.sum(explained_variance) # 确定保留的主成分数量 if isinstance(n_components, float) and 0 n_components 1: k np.searchsorted(cumulative_variance, n_components) 1 else: k int(n_components) # 取前k个主成分方向 components Vt[:k, :] # 形状 (k, n_features) # 投影 X_projected X_centered components.T # 形状 (n_samples, k) return mean_face, components, X_projected这段代码里有一个细节值得注意np.linalg.svd的full_matricesFalse参数。如果不加这个参数SVD会返回完整的U和Vt矩阵对于大矩阵来说内存开销很大。设为False之后只返回经济型SVD的结果U的形状是(n, min(n,d))Vt的形状是(min(n,d), d)足够我们用了。2.4 投影之后怎么分类投影完成后每张训练图片变成了一个k维向量。分类的思路很直接对于一张新的测试图片先减去均值脸再投影到特征脸空间得到它的k维坐标然后看它离哪个训练样本的坐标最近。这就是最近邻分类器。最近邻的度量用欧氏距离就行。但有一个细节不同主成分方向上的方差不同第一主成分的方差远大于最后一个主成分。如果不做处理距离计算会被方差大的主成分主导。一种改进做法是白化——把每个主成分方向上的坐标除以其标准差让所有方向上的方差都变成1。白化之后再做最近邻效果通常更稳定。def predict(X_test, mean_face, components, X_train_projected, y_train): 对测试样本进行预测 # 中心化并投影 X_test_centered X_test - mean_face X_test_projected X_test_centered components.T # 计算与每个训练样本的欧氏距离 # 利用广播机制(n_test, 1, k) - (1, n_train, k) diff X_test_projected[:, np.newaxis, :] - X_train_projected[np.newaxis, :, :] distances np.sqrt(np.sum(diff ** 2, axis2)) # 取距离最小的训练样本的标签 nearest_indices np.argmin(distances, axis1) return y_train[nearest_indices]3. 实测中那些让你识别率暴跌的坑3.1 训练集和测试集的划分方式决定成败我一开始犯过一个很典型的错误把所有图片随机打乱后按8:2划分训练集和测试集。结果识别率虚高到了实际使用场景就崩了。原因在于同一个人可能有多张照片随机划分会导致同一个人的照片同时出现在训练集和测试集中。分类器在测试时看到的陌生人其实在训练时已经见过他另一张照片了这相当于作弊。正确的做法是按人划分某些人的所有照片都进训练集另一些人的所有照片都进测试集。这样测试集里的人对分类器来说是完全陌生的才能真实反映模型的泛化能力。这个细节在学术论文里叫subject-independent评估协议实际做项目时一定要按这个来。3.2 主成分数量不是越多越好我做过一组对比实验保留的主成分数量从10到300每隔一段取一个值看识别率怎么变。结果是一条先升后降的曲线。主成分太少时信息不够识别率低主成分增加到某个点后识别率达到峰值继续增加识别率反而下降因为后面的主成分主要捕捉的是噪声和训练集特有的模式对泛化没有帮助。具体峰值在哪里取决于数据集的大小和难度。我的经验是对于几百张图片、几十个人的数据集保留50到150个主成分通常比较合适。你可以用累计方差贡献率来辅助判断但最终还是要用验证集上的识别率来定。3.3 光照和表情变化是最大的敌人PCA对光照变化非常敏感。同一个人的两张照片如果一张是正面均匀光照另一张是侧面强光它们在像素空间里的差异可能比不同人在相同光照下的差异还大。PCA降维后这种光照差异会占据方差最大的几个主成分方向导致真正区分身份的信息被压缩到后面的主成分里。缓解的办法有几个。一是在预处理阶段做直方图均衡化这个前面提过了。二是对训练数据做数据增强把每张图片的亮度调高调低各生成一个副本让模型见过更多光照变化。三是用**局部二值模式LBP**等对光照不敏感的特征替代原始像素但这已经超出了纯PCA的范畴。表情变化的处理更棘手。微笑和中性表情之间的差异在像素层面可能比不同人之间的差异还大。PCA本身没有建模表情的能力只能靠训练数据里包含足够多的表情变化来教会模型忽略这些差异。3.4 计算SVD时的内存陷阱当图片尺寸较大时比如200×200展平后是40000维。如果训练集有500张图片中心化后的矩阵是500×40000占内存约500×40000×8字节160MB还能接受。但如果图片尺寸是500×500维度变成250000同样500张图片就是1GB内存SVD分解时还会产生中间变量很容易爆内存。解决办法有两个。一是缩小图片尺寸人脸识别并不需要很高的分辨率100×100甚至50×50往往就够了。二是用增量PCA把数据分批次送入每次只加载一部分数据逐步更新主成分。sklearn的IncrementalPCA就是干这个的适合数据量大到内存放不下的场景。4. 怎么评估这套方案到底行不行4.1 识别率和验证率的区别这两个指标经常被混淆。识别率identification rate衡量的是给一张测试图片系统能不能在已知的人脸库中找到正确的那个人。这是一个一对多的问题。验证率verification rate衡量的是给两张图片系统能不能判断它们是不是同一个人。这是一个一对一的问题。PCA最近邻的方案天然适合做识别任务因为最近邻分类器直接输出一个身份标签。如果要做验证需要设定一个距离阈值——距离小于阈值判为同一个人大于阈值判为不同人。阈值的选取需要在误接受率把不同人判为同一人和误拒绝率把同一人判为不同人之间权衡。4.2 用混淆矩阵看具体错在哪里光看一个总的识别率数字不够你还需要知道模型在哪些人身上容易出错。混淆矩阵的行是真实身份列是预测身份对角线上的数字是正确分类的数量非对角线上的数字是错误分类的数量。我跑过一次实验发现模型在某两个人之间频繁混淆。把这两个人的照片调出来一看发现他们拍摄时的光照条件非常接近而且脸型轮廓有相似之处。这种信息对于后续改进非常有价值——你可以针对性地增加这两个人的训练样本或者引入对光照更鲁棒的特征。4.3 和基线方法对比才有意义单独报一个识别率90%没有太大意义你得知道不用PCA、直接用原始像素做最近邻能到多少。我实测下来在同一个数据集上原始像素最近邻的识别率大约是70%出头PCA降维后能到90%左右提升非常明显。这个对比才能说明PCA确实起了作用。另一个值得对比的基线是随机投影——随机生成一个投影矩阵把高维数据降到同样的维度然后做最近邻。如果PCA的识别率显著高于随机投影说明PCA找到的主成分方向确实比随机方向更有区分力。如果两者差不多那可能说明你的数据集太简单或者降维后的维度还是太高。4.4 交叉验证的正确打开方式做交叉验证时同样要注意按人划分的问题。标准的k折交叉验证是随机把样本分成k份但这在人脸识别场景下会导致同一个人出现在不同的折里。正确的做法是按人分折把所有人分成k组每次拿一组作为测试集其余作为训练集。这样每一折的测试集里都是训练时没见过的人。from sklearn.model_selection import GroupKFold # 假设 subjects 是每个人的ID数组 gkf GroupKFold(n_splits5) for train_idx, test_idx in gkf.split(X, y, groupssubjects): X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] # 训练和评估...5. 从PCA出发还能往哪些方向走5.1 LDA让降维直接服务于分类PCA是无监督的它找的是方差最大的方向但这些方向未必对分类最有利。LDA线性判别分析则是有监督的它的目标是最大化类间距离与类内距离的比值。用人话说就是让同一个人不同照片的投影尽量聚在一起不同人的投影尽量分开。在人脸识别任务上LDA通常比PCA效果更好尤其是在训练样本充足的情况下。但LDA有一个限制它最多能提取C-1个判别方向其中C是类别数。如果人数很少降维后的维度会非常低可能不够用。实践中常见的做法是先用PCA降到一个中间维度再用LDA进一步降维这叫PCALDA两阶段方法。5.2 核PCA处理非线性结构PCA本质上是在做线性变换它假设数据的主要变化方向是线性的。但人脸图像的变化——比如姿态变化导致的脸部形变——往往是非线性的。核PCA通过一个核函数把数据映射到更高维的空间在那个空间里做线性PCA等价于在原空间里做非线性降维。常用的核函数有高斯核和多项式核。核PCA的缺点是计算量大因为核矩阵的大小是n×nn是样本数。如果样本数上万核矩阵就太大了。而且核PCA没有像标准PCA那样的特征脸可视化解释性差一些。5.3 增量PCA应对流式数据实际项目中人脸库是不断增长的今天来了新员工要录入明天有人离职要删除。如果每次变动都重新训练整个PCA计算成本太高。增量PCA允许你在已有模型的基础上用新数据更新主成分方向而不需要保留所有历史数据。sklearn的IncrementalPCA提供了partial_fit方法可以分批次送入数据。但要注意增量PCA得到的主成分方向可能和批量PCA略有不同因为它是近似算法。对于精度要求极高的场景还是得定期做一次完整的批量训练。5.4 从PCA到深度特征思路的延续现在主流的人脸识别方案都是用深度网络提取特征但PCA的核心思想——把高维数据投影到低维空间保留最有区分力的方向——在深度学习中依然存在。深度网络的最后一层全连接层本质上也是在做一个从高维特征到低维嵌入的映射。只不过这个映射是通过大量数据学习出来的比PCA的线性映射灵活得多。如果你理解了PCA的局限——线性、无监督、对光照和姿态敏感——你就能理解为什么深度方法能超越它。但PCA依然是一个极好的入门项目因为它让你亲手触摸到降维和特征提取这两个核心概念而且整个流程透明可控出了问题你知道去哪里找原因。6. 几个容易被忽略的实操细节6.1 均值脸到底要不要减中心化是PCA的标准步骤但在人脸识别里有一个细节训练时减了均值脸测试时也必须减同一个均值脸。我见过有人训练时减了测试时忘了减结果识别率直接掉到随机水平。原因很简单如果不减均值测试图片的投影坐标会和训练图片的投影坐标处于不同的坐标系里距离计算完全失去意义。另一个细节是均值脸应该只从训练集计算不能用全部数据计算。如果用全部数据算均值脸测试集的信息就泄露到了训练过程中评估结果会偏乐观。6.2 图片展平的顺序必须一致把二维图片展平成一维向量时展平的顺序——是按行优先还是按列优先——必须对所有图片保持一致。这个听起来是废话但我在实际项目中真的见过有人训练时用行优先测试时用列优先导致识别率惨不忍睹。更隐蔽的情况是用了不同的图像库读取图片有的库默认返回的数组形状是高度宽度有的是宽度高度如果不注意统一展平后的向量就完全对不上了。6.3 距离度量的选择欧氏距离是最常用的但余弦距离在某些场景下效果更好。欧氏距离关注的是绝对位置差异余弦距离关注的是方向差异。在人脸识别中如果图片的亮度整体偏移比如所有像素都加了一个常数欧氏距离会受影响但余弦距离不受影响因为亮度偏移不改变向量的方向。你可以两种都试一下看哪个在你的数据集上表现更好。切换距离度量只需要改一行代码成本很低。6.4 保存和加载模型的注意事项训练好的PCA模型需要保存下来供后续使用。需要保存的东西包括均值脸向量、主成分矩阵、训练集的投影坐标和对应的标签。用np.savez或者pickle都可以。加载时要注意数组的形状和数据类型必须和保存时一致否则投影计算会出错。还有一个容易忽略的点如果后续要用新的数据更新模型你得保留原始的训练数据或者至少保留中心化后的数据矩阵。只保存主成分矩阵是不够的因为增量更新需要用到原始数据的统计信息。6.5 处理类别不平衡如果人脸库里某些人有几十张照片另一些人只有两三张最近邻分类器会偏向那些样本多的类别。因为样本多测试图片找到近邻的概率就大。缓解的办法有对样本少的类别做数据增强或者用加权最近邻——距离相同时优先选择样本少的类别。这个问题在学术数据集上通常不明显因为标准数据集里每个人的照片数量差不多。但实际项目中人员照片的采集数量往往很不均衡这个坑迟早会踩到。7. 这套方案在实际场景中的边界PCA人脸识别有它擅长的场景也有它搞不定的情况。说清楚这个边界比盲目吹嘘方法本身更有价值。适合的场景光照条件相对可控的室内环境、用户配合拍摄的正面照片、人脸库规模在几百到几千人、对实时性要求不高的离线比对。比如门禁系统的底库比对、考勤系统的照片验证这些场景下PCA方案完全够用而且部署简单不需要GPU。不适合的场景监控视频中的远距离人脸、大角度侧脸、遮挡严重的人脸、光照剧烈变化的环境、人脸库规模上万甚至百万级。这些场景下PCA的线性假设和像素级特征提取方式都力不从心需要深度学习方法。我个人的经验是把PCA方案当作一个基线系统来用。任何新的人脸识别方案都应该先和PCA基线比一比。如果深度方案在某个场景下只比PCA好一点点那可能不值得为此增加的计算复杂度和部署成本。如果好很多那说明这个场景确实需要非线性特征提取。最后分享一个我在实际项目中总结的小技巧当你发现PCA方案的识别率不理想时先别急着换方法花半个小时检查一下预处理流程——图片尺寸是否统一、灰度化是否正确、直方图均衡化是否做了、训练集和测试集是否按人划分。我遇到过好几次问题根本不在PCA本身而是预处理环节出了纰漏。把预处理做扎实PCA的识别率往往能超出你的预期。