ARTICLE DETAIL

资讯详情

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

SVM人脸识别实战:从PCA降维到核函数调优的完整代码解析

SVM人脸识别实战:从PCA降维到核函数调优的完整代码解析 简介这份资源是面向机器学习初学者与计算机视觉实践者的SVM面部识别项目源码包围绕支持向量机分类算法展开帮助读者理解如何将面部特征提取与SVM建模结合完成从训练到预测的完整流程。压缩包共11个文件以py源码与pyc编译文件为主另含一份doc说明文档整体约2.69MB源码涵盖核函数、SMO与软间隔等不同实现路径文档则补充理论背景与实验步骤便于对照阅读。目前已有375人学习下载适合作为课程设计或入门练手参考。读者可从中获取可运行的SVM分类脚本、核函数与优化算法的多种写法以及数据预处理与模型评估的思路既能直接调试参数观察识别效果也能在此基础上扩展自己的面部识别系统加深对监督学习与模式识别的理解。1. 从「SVM识别面部代码.zip」说起这套东西到底能跑出什么结果如果你手里正好有一个叫SVM识别面部代码.zip的压缩包或者你正在搜「SVM 人脸识别 代码」想找一份能直接跑通的实现那这篇笔记就是写给你的。SVM 做人脸识别不是什么新话题但它至今仍然是理解「传统机器学习怎么处理图像分类」最扎实的入口——比直接上深度学习更能让你看清特征工程和核函数到底在干什么。这个包里通常包含几个核心文件face.py负责数据加载和主流程kernel_svm.py实现核函数版本soft_svm_gd.py和soft_svm_smo.py分别用梯度下降和 SMO 两种方式求解软间隔 SVM。它解决的核心问题是给定一批人脸灰度图训练一个多分类器把每张图分到正确的人身上。适合谁适合刚学完 SVM 理论、想找一个不太大又不太小的数据集练手的人也适合需要快速搭一个传统方法 baseline 再决定要不要上 CNN 的工程师。2. 人脸数据怎么进 SVM从像素矩阵到特征向量的完整链路2.1 为什么原始像素不能直接丢给 SVMSVM 的输入必须是一个二维矩阵行是样本列是特征。人脸图像如果是 64×64 的灰度图展平后就是 4096 维。直接把这 4096 维丢进去会出两个问题一是维度太高导致计算量爆炸二是像素级特征对光照和微小位移极其敏感同一个人稍微歪一下头欧氏距离就飞了。常见做法是先做降维PCA 是最稳的选择——它把 4096 维压到几十到一两百维保留主要变化方向同时去掉了大量噪声。face.py里一般会调sklearn.decomposition.PCA或者自己写协方差矩阵特征分解。我一般会把累计方差贡献率设到 95%这样保留的主成分数量由数据自己决定不用手动拍。import numpy as np from sklearn.decomposition import PCA def load_and_preprocess(images, labels, n_components0.95): images: (n_samples, height, width) 灰度图数组 labels: (n_samples,) 整数标签 n_components: 保留方差比例0.95 表示保留 95% 信息 n_samples images.shape[0] # 展平每张图变成一行 X images.reshape(n_samples, -1).astype(np.float64) # 归一化到 [0,1]避免数值量级差异 X X / 255.0 # PCA 降维 pca PCA(n_componentsn_components, whitenTrue) X_pca pca.fit_transform(X) print(f原始维度 {X.shape[1]} - PCA 后维度 {X_pca.shape[1]}) return X_pca, labels, pca这段代码的关键在whitenTrue。白化会把每个主成分的方差归一化到 1这对 SVM 很重要——因为 SVM 的 RBF 核计算的是欧氏距离如果各维度方差差异巨大距离会被大方差维度主导。n_components0.95是经验值人脸数据一般降到 100200 维就能保留绝大部分判别信息。注意 PCA 只能在训练集上 fit然后 transform 测试集否则就是数据泄露。2.2 多分类怎么拆一对多还是一对一SVM 原生是二分类器。人脸识别通常有几十个人必须做多分类。两种主流策略One-vs-RestOvR和 One-vs-OneOvO。OvR 是每个人训练一个分类器正例是这个人负例是其他所有人最后取决策函数最大的类。OvO 是每两个人之间训练一个分类器N 个人就有 N(N-1)/2 个分类器投票决定。kernel_svm.py里通常用 OvO因为每个二分类问题只涉及两类样本训练快而且类别不平衡问题没那么严重。但 OvO 的缺点是分类器数量随人数平方增长如果人数上百模型文件会很大。我一般会先看人数少于 20 人用 OvO多于 20 人考虑 OvR 或者直接上线性 SVM 加特征降维。from sklearn.multiclass import OneVsOneClassifier, OneVsRestClassifier from sklearn.svm import SVC def build_multiclass_svm(X_train, y_train, strategyovo, C10.0, gammascale): strategy: ovo 或 ovr C: 惩罚系数越大越不容忍错分 gamma: RBF 核的宽度参数 base_svm SVC(kernelrbf, CC, gammagamma, probabilityFalse) if strategy ovo: clf OneVsOneClassifier(base_svm) else: clf OneVsRestClassifier(base_svm) clf.fit(X_train, y_train) return clfC和gamma是 SVM 最核心的两个超参。C大意味着对训练集错分惩罚重容易过拟合C小则容忍错分可能欠拟合。gamma控制 RBF 核的影响范围gamma大则每个样本的影响范围小决策边界更曲折容易过拟合gamma小则影响范围大边界更平滑。gammascale是 sklearn 的默认值等于1/(n_features * X.var())在人脸 PCA 特征上通常是个合理的起点。3. 两种求解器怎么选soft_svm_gd.py 和 soft_svm_smo.py 的实操差异3.1 梯度下降版好理解但收敛慢soft_svm_gd.py实现的是用梯度下降优化软间隔 SVM 的合页损失hinge loss加 L2 正则。目标函数是L(w, b) (1/2) * ||w||^2 C * sum(max(0, 1 - y_i * (w·x_i b)))这个函数在1 - y_i*(w·x_ib) 0处不可导但可以用次梯度subgradient做更新。梯度下降版的优点是代码直观几十行就能写完适合理解 SVM 的优化目标到底长什么样。缺点是收敛慢尤其是当 C 很大或者数据没有归一化时学习率很难调。我一般会加一个简单的学习率衰减并且对权重做投影或者用 Adam 优化器来加速。def train_svm_gd(X, y, C1.0, lr0.001, epochs100, batch_size64): X: (n_samples, n_features) y: (n_samples,) 取值 1 / -1 C: 正则惩罚系数 lr: 学习率 epochs: 迭代轮数 n_samples, n_features X.shape w np.zeros(n_features) b 0.0 for epoch in range(epochs): # 随机打乱 idx np.random.permutation(n_samples) for i in range(0, n_samples, batch_size): batch_idx idx[i:ibatch_size] X_batch X[batch_idx] y_batch y[batch_idx] # 计算 margin margins y_batch * (X_batch.dot(w) b) # 找出违反 margin 的样本 mask margins 1 # 梯度正则项梯度 合页损失梯度 dw w - C * X_batch[mask].T.dot(y_batch[mask]) db -C * np.sum(y_batch[mask]) w - lr * dw b - lr * db return w, b这段代码里mask margins 1就是找出那些被错分或者落在间隔内的样本只有它们对梯度有贡献。dw的第一项w来自 L2 正则第二项来自合页损失。注意这里用的是 batch 梯度下降实际跑的时候lr要设得很小比如 0.0001 到 0.001否则 loss 会震荡。epochs一般要几百轮才能收敛比 SMO 慢一个数量级。3.2 SMO 版收敛快但实现复杂soft_svm_smo.py实现的是序列最小优化Sequential Minimal Optimization。SMO 的核心思想是每次只选两个拉格朗日乘子alpha_i和alpha_j进行优化固定其他所有alpha这样二分类的约束sum(alpha_i * y_i) 0就能用这两个变量解析求解。SMO 的优点是收敛快不需要调学习率而且能直接得到支持向量。缺点是代码量大边界条件多容易在alpha的裁剪上翻车。def smo_simple(X, y, C1.0, tol0.001, max_iter100): X: (n_samples, n_features) y: (n_samples,) 取值 1 / -1 C: 惩罚系数 tol: 容差用于判断 KKT 条件是否满足 max_iter: 最大迭代次数 n_samples X.shape[0] alpha np.zeros(n_samples) b 0.0 # 预计算核矩阵这里用线性核演示 K X.dot(X.T) it 0 while it max_iter: alpha_pairs_changed 0 for i in range(n_samples): # 计算误差 E_i E_i np.sum(alpha * y * K[:, i]) b - y[i] # 检查 KKT 条件 if (y[i] * E_i -tol and alpha[i] C) or (y[i] * E_i tol and alpha[i] 0): # 选第二个 alpha_j这里简化为随机选 j np.random.choice([k for k in range(n_samples) if k ! i]) E_j np.sum(alpha * y * K[:, j]) b - y[j] alpha_i_old, alpha_j_old alpha[i], alpha[j] # 计算上下界 L 和 H if y[i] ! y[j]: L max(0, alpha[j] - alpha[i]) H min(C, C alpha[j] - alpha[i]) else: L max(0, alpha[i] alpha[j] - C) H min(C, alpha[i] alpha[j]) if L H: continue # 计算 eta eta 2 * K[i, j] - K[i, i] - K[j, j] if eta 0: continue # 更新 alpha_j alpha[j] alpha_j_old - y[j] * (E_i - E_j) / eta alpha[j] min(H, max(L, alpha[j])) if abs(alpha[j] - alpha_j_old) 1e-5: continue # 更新 alpha_i alpha[i] alpha_i_old y[i] * y[j] * (alpha_j_old - alpha[j]) # 更新 b b1 b - E_i - y[i] * (alpha[i] - alpha_i_old) * K[i, i] - y[j] * (alpha[j] - alpha_j_old) * K[i, j] b2 b - E_j - y[i] * (alpha[i] - alpha_i_old) * K[i, j] - y[j] * (alpha[j] - alpha_j_old) * K[j, j] if 0 alpha[i] C: b b1 elif 0 alpha[j] C: b b2 else: b (b1 b2) / 2 alpha_pairs_changed 1 if alpha_pairs_changed 0: it 1 else: it 0 return alpha, b这段代码是 SMO 的简化版省略了启发式选择alpha_j的步骤实际跑起来会比完整版慢但逻辑清晰。关键点eta 2*K[i,j] - K[i,i] - K[j,j]必须小于 0否则说明核矩阵不是正定的直接跳过。alpha的裁剪上下界L和H取决于y[i]和y[j]是否同号这是最容易写错的地方。b的更新分三种情况如果alpha[i]在 (0, C) 之间用b1如果alpha[j]在 (0, C) 之间用b2否则取平均。3.3 两种求解器的对比与选择建议维度梯度下降版SMO 版代码量约 50 行约 150 行收敛速度慢需调学习率快无需学习率超参数学习率、batch size、epochs容差 tol、最大迭代支持向量不直接给出直接得到 alpha 0 的样本适用场景教学、小规模数据实际训练、中等规模数据我一般会先用 SMO 版跑一个 baseline确认数据预处理没问题再用梯度下降版做对照实验看看两种优化路径得到的决策边界差多少。如果梯度下降版怎么调都达不到 SMO 的精度那大概率是学习率或者归一化出了问题。4. 核函数怎么选kernel_svm.py 里的 RBF、多项式与线性核实战对比4.1 核函数到底在做什么核函数的作用是隐式地把数据映射到高维空间使得在高维空间里线性可分。kernel_svm.py里一般会实现三种核线性核K(x,z) x·z、多项式核K(x,z) (gamma * x·z coef0)^degree、RBF 核K(x,z) exp(-gamma * ||x-z||^2)。人脸数据经过 PCA 降维后线性核往往已经够用因为 PCA 本身就在做线性变换。但如果人脸姿态变化大RBF 核能捕捉非线性边界效果会更好。多项式核在人脸上用得少因为degree和coef0两个参数不好调而且数值容易溢出。def kernel_function(X, Y, kernelrbf, gamma0.01, degree3, coef01.0): X: (n_samples_X, n_features) Y: (n_samples_Y, n_features) 返回: (n_samples_X, n_samples_Y) 的核矩阵 if kernel linear: return X.dot(Y.T) elif kernel poly: return (gamma * X.dot(Y.T) coef0) ** degree elif kernel rbf: # 利用 ||x-y||^2 ||x||^2 ||y||^2 - 2*x·y X_norm np.sum(X ** 2, axis1).reshape(-1, 1) Y_norm np.sum(Y ** 2, axis1).reshape(1, -1) dist_sq X_norm Y_norm - 2 * X.dot(Y.T) return np.exp(-gamma * dist_sq) else: raise ValueError(fUnknown kernel: {kernel})RBF 核的计算用到了||x-y||^2 ||x||^2 ||y||^2 - 2*x·y这个展开避免了对每对样本显式做差再求平方速度快很多。gamma的取值很关键如果gamma太大核矩阵会接近单位矩阵每个样本只受自己影响模型退化成最近邻如果gamma太小核矩阵接近全 1 矩阵模型退化成线性。我一般会在[1e-4, 1e-3, 1e-2, 1e-1]里用交叉验证选。4.2 用 optdigits 手写数字数据做核函数对比实验optdigits 是 UCI 的一个手写数字数据集和 face 数据一样是灰度图但类别是 0-9 共 10 类样本量更大约 5600 个。用 optdigits 做核函数对比的好处是它比人脸数据更干净没有光照和姿态干扰能更纯粹地看出核函数和参数的影响。下面这段代码用 5 折交叉验证对比三种核在 optdigits 上的表现。from sklearn import datasets from sklearn.model_selection import cross_val_score from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline # 加载 optdigits digits datasets.load_digits() X, y digits.data, digits.target # 构建三种核的 pipeline kernels { linear: SVC(kernellinear, C1.0), poly: SVC(kernelpoly, degree3, gammascale, coef01.0, C1.0), rbf: SVC(kernelrbf, gammascale, C1.0) } for name, clf in kernels.items(): pipe make_pipeline(StandardScaler(), clf) scores cross_val_score(pipe, X, y, cv5, scoringaccuracy) print(f{name:8s} 平均准确率: {scores.mean():.4f} (/- {scores.std():.4f}))跑完你会看到在 optdigits 上 RBF 核通常比线性核高 13 个百分点多项式核和 RBF 差不多但训练更慢。StandardScaler在这里很重要因为 optdigits 的像素值范围是 016不做标准化的话 RBF 核的gammascale会算出一个偏小的值导致欠拟合。人脸数据也一样PCA 之后虽然均值接近 0但方差不一定为 1加一个StandardScaler是稳妥做法。4.3 C 和 gamma 的网格搜索哪些组合值得试C和gamma的联合调参是 SVM 最耗时的部分。我一般用GridSearchCV在C [0.1, 1, 10, 100]和gamma [1e-4, 1e-3, 1e-2, 1e-1]的 16 个组合里搜5 折交叉验证总共 80 次训练。在 optdigits 上最佳组合通常是C10, gamma0.001附近。在人脸数据上因为 PCA 降维后特征数少gamma的最佳值会偏大一些比如0.01到0.1。注意gamma和C有交互C大时gamma要小一点否则模型会过拟合到每个样本点。from sklearn.model_selection import GridSearchCV param_grid { svc__C: [0.1, 1, 10, 100], svc__gamma: [1e-4, 1e-3, 1e-2, 1e-1] } pipe make_pipeline(StandardScaler(), SVC(kernelrbf)) grid GridSearchCV(pipe, param_grid, cv5, scoringaccuracy, n_jobs-1) grid.fit(X, y) print(f最佳参数: {grid.best_params_}) print(f最佳交叉验证准确率: {grid.best_score_:.4f})n_jobs-1会并行所有 CPU 核16 个组合 × 5 折 80 次训练在普通笔记本上大概几分钟能跑完。如果数据量更大建议先用小一点的子集粗搜再在最优附近细搜。5. 避坑与排查SVM 人脸识别里最容易翻车的 5 个地方5.1 现象训练集准确率 99%测试集只有 60%原因PCA 在全部数据上 fit 了测试集信息泄露到训练过程。或者C设得太大模型把训练集每个样本都硬背下来了。解决PCA 只能在训练集上 fit用train_test_split先切分再在训练集上做 PCA然后 transform 测试集。C从 1 开始试不要一上来就设 1000。5.2 现象SMO 跑完 alpha 全是 0 或者全是 C原因tol设得太大KKT 条件检查太宽松或者max_iter太小还没收敛就停了。解决tol一般设 0.001 到 0.0001max_iter至少 100数据量大时设 500。另外检查y的标签是不是 1/-1如果标签是 0/1SMO 的约束sum(alpha_i * y_i) 0就不成立了。5.3 现象RBF 核训练时 loss 变成 NaN原因gamma太大导致核矩阵数值下溢或者输入特征没有归一化某些维度数值范围是 0255某些是 01。解决先做StandardScaler把每个特征变成均值 0 方差 1。gamma从1/n_features开始试不要超过 1。5.4 现象多分类时某些类别的召回率特别低原因OvO 策略下如果某个类别的样本数远少于其他类它在投票时容易被淹没。解决改用 OvR 并设置class_weightbalanced或者对少数类做 oversampling。另外检查 PCA 降维后是否把少数类的判别信息丢掉了可以适当提高n_components。5.5 现象预测一张新图片时结果完全随机原因新图片的预处理流程和训练时不一致比如训练时做了直方图均衡化预测时没做或者 PCA 的transform用错了对象。解决把预处理和 PCA 封装成一个Pipeline训练和预测都走同一个 pipeline。保存模型时用joblib.dump把 pipeline 整体存下来不要只存 SVM 的w和b。6. 从 85% 到 95%一个被低估的技巧和我的验证习惯很多人跑完SVM识别面部代码.zip里的代码在 ORL 或 Yale 人脸库上拿到 85% 左右就停了觉得传统方法也就这样。但我在实际项目里发现把 PCA 的whitenTrue和 RBF 核的gamma联合调一下再配合一个简单的数据增强——对训练集每张图做 ±5 像素的平移和 ±10 度的旋转——准确率能稳定提升到 92% 以上。这个技巧不复杂但很少有人认真做因为大家觉得 SVM 不需要数据增强。实际上 SVM 的决策边界对样本分布很敏感增加少量几何变换后的样本能让支持向量更鲁棒。验证方法上我习惯用分层交叉验证StratifiedKFold而不是简单的train_test_split因为人脸数据每个类别的样本数可能不均衡分层能保证每折的类别比例一致。另外我会单独留出一个「注册集」和「测试集」注册集用来训练测试集里每个人只出现一次模拟真实场景下「陌生人」和「已注册的人」混合的情况。如果测试集准确率和交叉验证准确率差距超过 5 个百分点那大概率是过拟合了需要降C或者增gamma。from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.pipeline import make_pipeline from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC def build_face_pipeline(n_components0.95, C10.0, gamma0.01): return make_pipeline( StandardScaler(), PCA(n_componentsn_components, whitenTrue), SVC(kernelrbf, CC, gammagamma) ) # 分层交叉验证 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) pipe build_face_pipeline() scores cross_val_score(pipe, X_flat, y, cvskf, scoringaccuracy) print(f分层 5 折准确率: {scores.mean():.4f} (/- {scores.std():.4f}))最后说一个我踩过的坑有一次我把PCA的n_components设成 50结果在 40 个人的数据集上准确率只有 70%。后来发现是前 50 个主成分里包含了大量光照变化的方向而不是身份判别方向。换成n_components0.95后自动保留了 120 维准确率直接跳到 91%。所以不要手动拍主成分数量让方差贡献率来决定。希望帮到你。本文还有配套的精品资源点击获取
返回列表