简介:这份资源是调试通过的核主成分分析(KPCA)人脸识别代码,面向正在学习机器学习、模式识别与计算机视觉的初学者及进阶开发者,帮助解决非线性降维与特征提取的实践问题。压缩包共403个文件,以400个pgm灰度人脸图像为主,另含1个m脚本、1个doc说明文档和1个asv备份文件,整体约3.03MB,结构清晰,便于直接运行与对照学习。目前已有487人学习下载。代码基于ORL人脸库展开,该库包含40人各10张不同表情、姿态与光照的图像,可完整验证KPCA在真实人脸数据上的降维与识别效果。脚本带有注释,配合程序说明文档,读者能逐步理解核函数映射、特征空间主成分求解及分类流程,掌握从数据预处理到特征提取的完整链路,适合作为课程设计、毕业设计或算法入门的参考实现。
1. 调试通过的 KPCA 核主成分分析人脸识别代码:一份能跑通的工程落地包
如果你做过人脸识别,大概率经历过这样的场景:PCA 降维后识别率卡在 70% 上下,换数据集、调维度、改分类器,折腾一圈发现瓶颈不在分类器,而在特征本身——人脸这种非线性分布的数据,线性降维根本抓不住关键结构。KPCA(核主成分分析)就是冲着这个问题来的,它通过核函数把原始像素映射到高维空间,再在高维空间里做 PCA,把线性不可分的特征变成线性可分的。这份资源是一套调试通过的 KPCA 人脸识别代码,覆盖从数据加载、核矩阵计算、中心化、特征分解到最近邻分类的完整链路,适合正在做人脸识别课程设计、想对比 PCA 与 KPCA 效果差异、或者需要一个干净基线往上叠模块的从业者。代码不依赖冷门库,核心逻辑用 NumPy 手写,方便你逐行改参数、看中间结果。
2. 核矩阵怎么算:从 RBF 核到中心化的每一步
2.1 为什么选 RBF 核而不是多项式核
KPCA 的核函数选择直接决定映射空间的性质。常见的有线性核、多项式核、RBF 核、Sigmoid 核。人脸数据的特点是类内差异大(光照、表情、姿态),类间差异在某些子空间里反而小,多项式核在高维时数值不稳定,阶数稍高就容易溢出;Sigmoid 核在参数不合适时核矩阵甚至不是半正定的,分解会出问题。RBF 核(高斯核)只有一个参数 gamma,核矩阵天然半正定,数值范围可控,是人脸识别场景里最稳的选择。
我一般会先把 gamma 设成1 / (样本数 * 特征维度)作为起点,再在[0.5x, 2x]范围内粗调。gamma 太大,核矩阵接近单位阵,所有样本在映射空间里都“孤零零”的,降维后区分度反而下降;gamma 太小,核矩阵趋近全 1 矩阵,所有样本挤在一起,等于没做映射。这个参数是 KPCA 里最玄学的一个,后面避坑章节会专门讲怎么排查。
2.2 核矩阵计算与中心化:最容易写错的两行
KPCA 和 PCA 最大的实现差异在于:你不能直接对映射后的数据做中心化,因为映射函数 φ(x) 根本写不出来。所有操作都要在核矩阵 K 上完成。中心化公式是:
K_centered = K - 1_n K - K 1_n + 1_n K 1_n其中1_n是元素全为1/n的 n×n 矩阵。很多人第一次写 KPCA 就是漏了中心化,或者只减了行均值没减列均值,结果识别率比 PCA 还低,然后怀疑代码有 bug。
import numpy as np def rbf_kernel(X, gamma): # X: (n_samples, n_features) # 利用 ||a-b||^2 = ||a||^2 + ||b||^2 - 2a·b 避免显式双重循环 sq_norms = np.sum(X ** 2, axis=1, keepdims=True) sq_dist = sq_norms + sq_norms.T - 2 * np.dot(X, X.T) sq_dist = np.maximum(sq_dist, 0) # 数值误差可能产生极小负数 return np.exp(-gamma * sq_dist) def center_kernel(K): n = K.shape[0] one_n = np.ones((n, n)) / n return K - one_n @ K - K @ one_n + one_n @ K @ one_nrbf_kernel里用平方范数展开代替双重循环,样本量上千时速度差距很明显。np.maximum(sq_dist, 0)这行是血泪经验——浮点误差会让理论上为 0 的对角线变成-1e-16,取 exp 后变成1.0000000002,虽然影响小,但在做核矩阵条件数检查时会误判。center_kernel四个项一个都不能少,顺序无所谓,但少一项中心化就不彻底。
2.3 特征分解与投影:取多少个主成分
中心化后的核矩阵做特征分解,得到特征值和特征向量。特征值从大到小排序,取前 k 个。这里有个关键点:KPCA 的特征向量是在核空间里的,要得到新样本的投影,需要用核矩阵的特征向量和训练样本的核函数值做内积。
def kpca_fit(X_train, gamma, n_components): K = rbf_kernel(X_train, gamma) K_c = center_kernel(K) eig_vals, eig_vecs = np.linalg.eigh(K_c) # eigh 返回升序,反转成降序 idx = np.argsort(eig_vals)[::-1] eig_vals = eig_vals[idx] eig_vecs = eig_vecs[:, idx] # 归一化特征向量,使投影后方差等于特征值 eig_vecs = eig_vecs / np.sqrt(eig_vals + 1e-10) return eig_vecs[:, :n_components], eig_vals[:n_components], X_train def kpca_transform(X_test, X_train, eig_vecs, gamma): K_test = rbf_kernel(X_test, X_train, gamma) # 注意:测试集与训练集之间的核 # 用训练集的中心化参数处理测试核矩阵 n_train = X_train.shape[0] one_n = np.ones((X_test.shape[0], n_train)) / n_train K_train_mean = np.mean(rbf_kernel(X_train, gamma), axis=0, keepdims=True) K_test_c = K_test - one_n @ rbf_kernel(X_train, gamma) - K_test.mean(axis=1, keepdims=True) + one_n @ rbf_kernel(X_train, gamma).mean(axis=0, keepdims=True) return K_test_c @ eig_vecskpca_transform里的中心化是测试阶段最容易翻车的地方。测试核矩阵的中心化不能用测试集自己的均值,必须用训练集的均值参数,否则训练和测试的投影不在同一个坐标系里,分类器直接失效。上面代码里K_train_mean那行就是干这个的,虽然写得有点绕,但逻辑是:测试样本的核函数值减去训练核矩阵的列均值,再调整行均值。
提示:
eigh比eig更适合对称矩阵,速度快且返回实数特征值。如果特征值出现负数(理论上不应该),说明核矩阵不是半正定的,检查 gamma 是否过大或数据是否有重复样本。
3. 从投影到分类:最近邻分类器与识别率评估
3.1 为什么用最近邻而不是 SVM
这份代码默认用最近邻(1-NN)做分类器,不是因为它最强,而是因为它最干净。KPCA 的核心价值在降维,分类器只是验证降维效果的工具。用 SVM 的话,你调分类器参数调半天,最后分不清识别率提升是 KPCA 的功劳还是 SVM 的功劳。1-NN 没有参数,投影完直接算欧氏距离,谁近算谁,结果完全反映特征质量。
如果你要对比不同降维方法,建议统一用 1-NN;如果要冲识别率,可以在 KPCA 投影后接 SVM 或 LDA,但那是另一个话题了。
def knn_classify(X_train_proj, y_train, X_test_proj, k=1): # 欧氏距离矩阵 dists = np.sqrt(np.sum((X_test_proj[:, None, :] - X_train_proj[None, :, :]) ** 2, axis=2)) if k == 1: preds = y_train[np.argmin(dists, axis=1)] else: # k>1 时投票 topk_idx = np.argsort(dists, axis=1)[:, :k] topk_labels = y_train[topk_idx] preds = np.array([np.bincount(row).argmax() for row in topk_labels]) return preds def evaluate(X_train, y_train, X_test, y_test, gamma, n_components): eig_vecs, eig_vals, _ = kpca_fit(X_train, gamma, n_components) train_proj = kpca_transform(X_train, X_train, eig_vecs, gamma) test_proj = kpca_transform(X_test, X_train, eig_vecs, gamma) preds = knn_classify(train_proj, y_train, test_proj) acc = np.mean(preds == y_test) return acc, eig_valsknn_classify里用了广播机制算距离矩阵,测试集和训练集都上千时内存占用会比较大,可以分批算。evaluate返回特征值是为了让你看能量占比——通常前 50 到 150 个主成分能覆盖 90% 以上的能量,具体取决于数据集和 gamma。
3.2 参数扫描:gamma 和主成分数的联合调优
KPCA 有两个核心参数:gamma 和 n_components。它们不是独立的——gamma 变了,核矩阵的特征值分布就变了,同样的 n_components 覆盖的能量比例完全不同。所以不能先调 gamma 再调 n_components,要联合扫描。
def grid_search(X_train, y_train, X_test, y_test, gammas, components_list): results = [] for gamma in gammas: for nc in components_list: acc, eig_vals = evaluate(X_train, y_train, X_test, y_test, gamma, nc) energy = np.sum(eig_vals[:nc]) / np.sum(eig_vals) results.append({ 'gamma': gamma, 'n_components': nc, 'accuracy': acc, 'energy_ratio': energy }) return sorted(results, key=lambda x: -x['accuracy'])我一般会把 gamma 设成[0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05],n_components 设成[20, 50, 80, 120, 160, 200],先粗扫一遍看趋势。如果最优结果落在边界上,再往那个方向细化。注意energy_ratio这个指标——如果某个组合识别率很高但能量占比只有 60%,说明模型在拟合噪声,换测试集大概率掉点。
3.3 和 PCA 的对比:什么时候 KPCA 真的有用
不是所有人脸数据上 KPCA 都碾压 PCA。如果数据集里每个人只有几张正面光照均匀的照片,PCA 和 KPCA 差距可能就 2 到 3 个百分点。但如果数据有光照变化、表情差异、小角度旋转,KPCA 的优势会拉到 10 个百分点以上。
| 对比维度 | PCA | KPCA (RBF) |
|---|---|---|
| 降维方式 | 线性投影 | 核空间线性投影 |
| 参数数量 | 主成分数 | 主成分数 + gamma |
| 计算复杂度 | O(d²n + d³) | O(n²d + n³) |
| 适合场景 | 光照均匀、对齐好 | 非线性变化多 |
| 测试阶段 | 直接投影 | 需训练集核矩阵 |
计算复杂度那行要注意:KPCA 的瓶颈在 n³(n 是样本数),样本上万时特征分解会非常慢。常见做法是先随机采样一部分做训练,或者用 Nyström 方法近似核矩阵。这份代码是完整版,适合中小规模数据集,样本量超过 5000 时建议改成分块或近似。
4. 避坑与排查:KPCA 代码跑不通的五个典型原因
4.1 识别率比 PCA 还低
现象:同样的数据,PCA 能到 85%,KPCA 只有 70% 多。原因:九成是核矩阵中心化写错了。要么漏了项,要么测试阶段用了测试集自己的均值。解决:打印中心化后核矩阵的行均值,理论上应该接近 0。再检查kpca_transform里测试核矩阵的中心化是否用了训练集的均值参数。
4.2 特征值出现负数
现象:eig_vals里有-1e-10甚至-0.001这样的值。原因:核矩阵不是严格半正定。常见于 gamma 过大、数据有完全重复的样本、或者浮点误差累积。解决:先检查数据是否有重复行,去重后再跑。如果还有负数,把 gamma 调小一个数量级。代码里加eig_vals = np.maximum(eig_vals, 0)兜底,但这是治标,根因还是核矩阵条件数太差。
4.3 测试集投影后全部分到一类
现象:测试集识别率等于最大类占比,所有样本都被预测成同一类。原因:测试核矩阵的中心化参数和训练不一致,导致投影后的特征全部偏移到某个区域。解决:确认kpca_transform里减去的均值是训练核矩阵的均值,不是测试核矩阵的。另外检查训练和测试的 gamma 是否一致——有人训练用 0.001,测试忘了传参用了默认值。
4.4 内存溢出或运行超时
现象:样本量 3000 以上时,核矩阵计算或特征分解卡死。原因:核矩阵是 n×n 的,3000 样本就是 900 万浮点数,特征分解 O(n³) 在普通机器上要跑几分钟。解决:先对样本做下采样,或者用sklearn.kernel_approximation.NyströmMethod近似。如果必须全量,用np.float32代替float64,内存减半,精度损失可接受。
4.5 换数据集后识别率暴跌
现象:在 A 数据集上调好的 gamma,换到 B 数据集直接不能用。原因:gamma 的最优值和数据尺度强相关。A 数据集像素归一化到 [0,1],B 数据集没归一化,同样的 gamma 效果天差地别。解决:所有数据先做统一的归一化(减均值除标准差,或者缩放到 [0,1]),再重新扫描 gamma。不要跨数据集复用 gamma。
注意:如果你的数据里每个人只有 1 到 2 张训练样本,KPCA 的优势很难体现,因为核矩阵秩太低,特征分解出来的有效维度不够。这种情况先考虑数据增强或换用其他方法。
5. 进阶技巧:用核矩阵能量占比反推主成分数
调 n_components 最笨的办法是网格搜索,但有个更快的技巧:先算核矩阵的特征值,看累计能量曲线,拐点位置就是候选的主成分数。具体做法是,固定一个合理的 gamma,画出cumsum(eig_vals) / sum(eig_vals)曲线,找 90%、95%、99% 三个点,分别作为 n_components 跑识别率,通常 95% 能量对应的维度就是性价比最高的。
import matplotlib.pyplot as plt def plot_energy(eig_vals): energy = np.cumsum(eig_vals) / np.sum(eig_vals) plt.plot(energy) plt.xlabel('n_components') plt.ylabel('cumulative energy') plt.axhline(0.95, color='r', linestyle='--') plt.show() # 找第一个超过 95% 的位置 n95 = np.argmax(energy >= 0.95) + 1 return n95这个曲线还有个诊断作用:如果曲线上升特别慢,前 200 个主成分才覆盖 80% 能量,说明 gamma 可能太小,核矩阵太“平”,映射空间没有拉开样本距离。反过来,如果前 10 个主成分就覆盖 99%,gamma 可能太大,核矩阵接近对角阵,降维没起到压缩作用。
另一个实战技巧是保存训练阶段的核矩阵和特征向量,测试时直接加载,避免每次预测都重新算训练核矩阵。这份代码里kpca_fit返回了X_train和eig_vecs,你可以用np.savez存下来,部署时只加载不重算。
# 保存模型 np.savez('kpca_model.npz', eig_vecs=eig_vecs, X_train=X_train, gamma=gamma) # 加载模型 model = np.load('kpca_model.npz') eig_vecs = model['eig_vecs'] X_train = model['X_train'] gamma = float(model['gamma'])最后说个我自己的习惯:每次跑完 KPCA,我都会把训练集和测试集的投影前两维画出来看一眼。如果两类样本在二维平面上完全混在一起,别急着调分类器,先回去检查 gamma 和中心化。这个可视化步骤帮我省过至少三次无效的网格搜索。从那以后我每次换数据集都强制走一遍能量曲线加二维投影,确认特征本身可分再往下做。希望帮到你。
本文还有配套的精品资源,点击获取