半监督局部保持投影SS-LPP:标签稀缺人脸识别降维与门禁落地
简介这份PDF文献聚焦人脸识别中的半监督降维问题面向图像处理、模式识别方向的研究生与科研人员适合研究流形学习与特征提取的读者参考。文件为单篇PDF论文542KB出自《科学技术与工程》2013年第13卷第9期含完整公式推导、算法流程与实验数据已有72人学习。论文针对局部保持投影LPP基向量关联且非正交、监督优化局部保持投影SOLPP忽视未标注样本全局信息的不足提出半监督优化局部保持投影SSOLPP以加权平衡参数融合非监督主成分分析PCA与SOLPP使投影数据既保留未标注样本的全局散布结构又维持监督优化的局部判别信息。文中在YaleB与AR人脸数据集上完成对比实验验证算法在减少标注需求的同时保持识别稳定性。读者可据此获取算法改进思路、目标函数构造方法与实验验证范式对论文写作与算法复现均有参考价值。1. 标签稀缺的人脸库为什么半监督局部保持投影能救场人脸识别门禁机落地时最常遇到的硬约束注册照每人只有一两张现场抓拍攒了几千张却没有身份标注。直接上 PCA 降维光照和姿态带来的类间方差会把身份信息盖住直接上 LDA类内散度矩阵在高维小样本下奇异根本解不出来。半监督优化局部保持投影针对的正是这种「标注极少、维度极高」的处境先用局部保持投影LPP建一张刻画人脸近邻结构的关系图把人脸像素投到低维子空间再让少量带标签样本去修正这张图的权重使同类人脸的投影点靠得更紧、异类人脸被推开。做离线人脸识别、门禁一体机、校园考勤这类拿不到大批标注、又不想依赖云端大模型的场景这套思路的性价比很高。下面从数学骨架讲到能在 ORL 人脸库上跑通的完整代码。2. 局部保持投影的数学骨架与半监督改造的着力点2.1 Laplacian Eigenmaps 到 LPP人脸识别为什么要显式线性投影Laplacian Eigenmaps 是非线性流形学习能得到训练样本在低维空间里的坐标但给不出一个新的映射函数。门禁机上线之后每天都来新抓拍每次都要把全部历史样本重新做一次特征分解这在工程上不可接受。LPP 的做法是假设低维坐标是原始像素的线性变换y Aᵀx一次求解拿到投影矩阵 A之后注册、查库、比对都退化成一次矩阵乘法新样本天然支持。人脸在原始像素空间里有个特点同一张脸在两种光照下像素级欧氏距离可能大于两个不同身份的人脸。全局距离不可靠但局部邻域结构相对稳定——同一姿态、同一光照下同一身份的人脸总是互为近邻。LPP 保的就是这种局部邻域关系把「谁挨着谁」从高维搬到低维而不是像 PCA 那样去保全局方差最大的方向。这也解释了为什么在人脸这种受光照影响大的数据上LPP 常常能比 PCA 多拿几个点。2.2 目标函数、度矩阵与广义特征值问题LPP 的目标很直白如果原始空间里两个样本 x_i 和 x_j 是近邻权重 W_ij 大那投影之后它们也要靠得近。写成最小化式子min_a Σ_ij (aᵀx_i − aᵀx_j)² W_ij aᵀ X (D − W) Xᵀ a其中 W 是亲和矩阵D 是度矩阵D_ii Σ_j W_ijL D − W就是图拉普拉斯。为了避免解退化成全零向量加一个尺度约束aᵀ X D Xᵀ a 1。用拉格朗日乘子法问题转成广义特征值问题X L Xᵀ a λ X D Xᵀ a取最小的 d 个特征值对应的特征向量拼成 A就是投影矩阵。注意最小的那个特征值恒为 0对应的向量通常直接丢掉否则是把所有样本投到同一个点。import numpy as np from scipy.linalg import eigh # 四点两簇的玩具数据验证 LPP 能否把两簇分开 X np.array([[0.0, 0.0], [0.1, 0.0], [1.0, 1.0], [1.1, 1.0]]) W np.array([[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dtypefloat) D np.diag(W.sum(axis1)) L D - W # 求解广义特征值问题 XLXᵀa λXDXᵀa vals, vecs eigh(X.T L X, X.T D X) a vecs[:, np.argsort(vals)[1]] # 跳过恒为 0 的最小特征值 print(X a) # 预期输出两簇分居负、正两侧这段代码里eigh专门解对称广义特征值问题比通用的eig更稳。vals是特征值vecs的每一列是一个投影方向 a。真实人脸数据里 X 是 n×d 且 d 远大于 nXᵀDXᵀ这部分会严重不满秩后面第 5 章会专门处理。2.3 半监督优化到底「优化」在哪里三条改造路径「半监督优化」这几个字在不同论文里指向的位置并不一样落地前先分清改造的是哪一环否则调参容易调错对象。改造路径改的对象数据要求计算开销典型收益图权重判别化亲和矩阵 W少量标签即可低类间可分性提升最明显标签传播伪标注无标签样本的 y需要 kNN 图中标注率低于 15% 时收益大目标函数加判别正则L 或约束项需要标签低抑制过拟合降维后接监督分类器分类阶段需要标签低便于端到端调参多数工程实现会同时用上前两条先用标签传播给无标签人脸发一个带置信度的伪身份再把伪身份和真实标签一起写进亲和矩阵。这种做法在人脸识别代码里很常见。2.4 无标签数据不是免费的收益边界在哪半监督的收益不是随标注比例单调上升的。标注率低于 5% 时标签传播的伪标签错误率会很高这些错误会通过 W 扩散到整个图投影结果可能比纯无监督的 LPP 还差。另一个边界是图连通性无标签样本如果全是同一批人、同一个摄像头下的连拍kNN 图会聚成一团此时加进去的半监督项只是在强化「这批人长得像」这个先验对区分身份没帮助。判断方法很简单把 W 的非零元素按类累加看看跨类连接占比是不是低得可疑。3. 半监督亲和图的构造带标签与无标签人脸如何共同进图3.1 k 近邻图与热核权重k 和 t 到底怎么定W 的第一种来源是无监督的 kNN 图。对每个人脸样本找 k 个最近邻边权用热核exp(−‖x_i − x_j‖² / (2t²))。k 和 t 直接决定投影后子空间的形态。k 取值图的性质人脸识别表现k 2~3稀疏、局部性强类内连接稳定但跨姿态同类样本容易断连k 5~8兼顾类内与近邻类ORL、Yale 这类库上通常最稳默认取 6k 15邻域混入异类投影后类间重叠明显增加识别率下滑有一个硬约束常被忽略k 必须小于最小身份类别的样本数。如果某个身份只注册了一张照片k 取 5 时它的近邻全是别人图在这一点上直接把异类连成了同类后续再调 alpha、beta 都补不回来。import numpy as np from sklearn.neighbors import kneighbors_graph from sklearn.metrics import pairwise_distances def rbf_knn_graph(X, k6, tNone): 构造对称化的 kNN 热核图返回亲和矩阵 W 和实际使用的带宽 t。 n X.shape[0] if t is None: # 带宽默认取平均成对距离的一半避免手调 D2 pairwise_distances(X, metricsqeuclidean) t np.mean(np.sqrt(D2)) / 2.0 G kneighbors_graph(X, n_neighborsk, modedistance).toarray() W np.exp(-(G ** 2) / (2.0 * t * t)) W np.maximum(W, W.T) # kNN 图天然不对称取大值对称化 np.fill_diagonal(W, 0.0) return W, tkneighbors_graph返回的邻接关系通常不对称A 的近邻里有 BB 的近邻里不一定有 A对称化是必须的否则拉普拉斯矩阵不对称特征分解出来的向量没有实数保证。t的取值原则太大所有边权趋近 1图退化成近似全连通LPP 的解会向 PCA 靠拢太小则边权趋近 0/1 二值图对噪声敏感。默认用平均成对距离的一半是个稳妥起点。3.2 标签传播给无标签人脸发一个带置信度的伪身份无监督图只刻画了「像不像」没利用身份信息。半监督接入的第一步是让有标签样本通过图结构把身份传播到无标签样本上。sklearn 的LabelSpreading可以直接用它迭代求解的传播公式本质上是F ← αSF (1−α)YS 是归一化后的亲和矩阵。from sklearn.semi_supervised import LabelSpreading def pseudo_labels(X, y_semi, n_neighbors7, alpha0.8, max_iter200): y_semi 中 -1 表示未标注。返回伪标签和每个样本的置信度。 lp LabelSpreading(kernelknn, n_neighborsn_neighbors, alphaalpha, max_itermax_iter) lp.fit(X, y_semi) proba lp.label_distributions_ return proba.argmax(axis1), proba.max(axis1)参数含义alpha越接近 1传播结果越依赖初始标签伪标签越保守越接近 0越依赖图结构容易把噪声放大。max_iter是迭代上限实际收敛通常在 20 轮以内设 200 只是保险。置信度那一列不要丢——后面只把置信度高于阈值的样本并入监督图能让标注率 10% 的场景稳定好几个点。3.3 判别加权矩阵类内收缩、类间扩张与半监督正则项有了伪标签就能构造带符号的判别图。思路是把拉普拉斯拆成三块同类标签对构成类内图 W_w异类标签对构成类间图 W_b无标签样本参与 kNN 图 W_u。目标函数变成在类内尽量紧的同时把类间推开min_a aᵀX (L_w βL_u − αL_b) Xᵀ adef build_ss_graph(X, y, k6, beta0.5, alpha0.3, gamma1.0, tNone): y 中 -1 表示无标签。返回带符号判别图 W 和作为约束基的正图 W_base。 n X.shape[0] labeled y 0 W_nn, t rbf_knn_graph(X, kk, tt) W_w np.zeros((n, n)) # 类内 W_b np.zeros((n, n)) # 类间 for i in range(n): if not labeled[i]: continue for j in range(i 1, n): if not labeled[j]: continue if y[i] y[j]: W_w[i, j] W_w[j, i] gamma else: W_b[i, j] W_b[j, i] 1.0 # 只要一端无标签就保留这条边让未标注样本参与流形结构 mask_u (~labeled[:, None]) | (~labeled[None, :]) W_u W_nn * mask_u W W_w beta * W_u - alpha * W_b # 带符号的判别图 W_base W_w beta * W_u # 只用正权重构造约束保证正定 return W, W_base, t参数作用位置增大的后果建议范围beta无标签 kNN 图的权重流形更平滑但容易被噪声抓拍带偏0.3 ~ 0.8alpha类间扩张强度类间分离更强过大时类内被压碎0.1 ~ 0.5gamma类内边的权重类内更紧过高会让同类过拟合到训练姿态1.0 ~ 3.0把约束矩阵单独用正权重部分W_base构造是避免广义特征值求解时出现负定矩阵的关键。如果直接用带符号的 W 去算XᵀDX对角线上会出现负数和接近零的值eigh会给出无意义的结果甚至直接报错。3.4 上万张人脸时的稀疏化处理人脸库上千张之后稠密的 n×n 矩阵开始吃内存n 20000 时一个 float64 矩阵就是 3.2 GB。工程上改用scipy.sparse存 W只保留 kNN 边和标签对。LabelSpreading在 sklearn 里内部就是稀疏实现但上面那段双重循环构造 W_w、W_b 的写法是 O(n²)必须换成按标签分组做块操作同一标签的索引取出来做np.ix_直接整块赋值。这样构建时间从分钟级降到秒级也顺带避开了 Python 层双重循环的开销。4. 在 ORL 人脸库上跑通半监督 LPP从加载到 1-NN 精度4.1 数据加载、切分与光照归一化先用 ORLOlivetti人脸库验证400 张 64×64 灰度图、40 个身份、每人 10 张覆盖表情和轻微姿态变化是检验半监督算法最省事的基准。真实门禁场景里抓拍图要先经人脸检测框裁剪再对齐双眼OpenCV 的检测器配合双眼关键点就能做但裁出来的图必须统一尺寸和灰度否则 LPP 图的近邻关系会被尺寸差异主导。import numpy as np from sklearn.datasets import fetch_olivetti_faces def load_orl(): d fetch_olivetti_faces(shuffleFalse) X d.images.reshape(len(d.images), -1).astype(np.float64) y d.target.astype(int) return X, y def normalize_faces(X): 逐样本去均值 单位方差抵消整体亮度和对比度差异。 X X - X.mean(axis1, keepdimsTrue) X X / (X.std(axis1, keepdimsTrue) 1e-8) return X def split_labeled(y, ratio, seed0): 按身份分层采样未选中的样本标签置 -1。 rng np.random.RandomState(seed) y_semi -np.ones_like(y) for c in np.unique(y): idx np.where(y c)[0] n_lab max(1, int(round(len(idx) * ratio))) y_semi[rng.choice(idx, n_lab, replaceFalse)] c return y_semi逐样本归一化这一步不能省。ORL 里同一个人不同时期的照片整体亮度有差异不做归一化的话kNN 图会先把「亮度相近」的人连起来而不是把「同一个人」连起来。4.2 半监督 LPP 的完整求解函数from scipy.linalg import eigh from sklearn.decomposition import PCA def ss_lpp_fit(X, W, W_base, n_components60, reg1e-3): 求解半监督判别 LPP 的投影矩阵 A。 d X.shape[1] L np.diag(W.sum(axis1)) - W D_base np.diag(W_base.sum(axis1)) XLXt X.T L X XDXt X.T D_base X # 正则化把 XDXt 拉成正定对付高维小样本下的奇异 XDXt XDXt reg * np.trace(XDXt) / d * np.eye(d) vals, vecs eigh(XLXt, XDXt) order np.argsort(vals) return vecs[:, order[:n_components]] def run_ss_lpp(X, y_semi, ratio, k6, beta0.5, alpha0.3, n_pca120, n_components60, reg1e-3, seed0): 完整流程PCA 预降维 → 建图 → 求解投影。 Xn normalize_faces(X) # 人脸图像 d4096、样本 n400先 PCA 到 n−1 以内再做 LPP Xp PCA(n_componentsn_pca, whitenFalse, random_stateseed).fit_transform(Xn) pseudo, conf pseudo_labels(Xp, y_semi) y_use np.where(y_semi 0, y_semi, pseudo) y_use np.where(conf 0.8, y_use, -1) # 低置信伪标签不参与监督项 W, W_base, t build_ss_graph(Xp, y_use, kk, betabeta, alphaalpha) A ss_lpp_fit(Xp, W, W_base, n_componentsn_components, regreg) return Xp A, APCA 预降维这一步是实战里的必需品不是为了好看。原始维度 4096、样本 400XᵀDX的秩最多 399直接求广义特征值会掉进数值泥潭。先降到 120 维既保留主要信息又让后面的矩阵运算规模可控。reg的默认 1e-3 是按trace(XDXt)/d量级缩放的比固定写1e-3 * I更稳换数据集不用重调。4.3 1-NN 评估PCA、LPP、SS-LPP 的三组对照半监督场景下用转导式评估最贴切所有样本都参与投影用带标签的做 gallery无标签的做 probe看 1-NN 能不能认对。from sklearn.neighbors import KNeighborsClassifier def transductive_acc(Xp, y, y_semi): labeled y_semi 0 clf KNeighborsClassifier(n_neighbors1).fit(Xp[labeled], y[labeled]) return (clf.predict(Xp[~labeled]) y[~labeled]).mean() X, y load_orl() for ratio in (0.1, 0.2, 0.3): y_semi split_labeled(y, ratio, seed0) Xp, A run_ss_lpp(X, y_semi, ratio) print(f标注 {ratio:.0%} SS-LPP 1-NN 准确率 {transductive_acc(Xp, y, y_semi):.3f})标注比例PCA (d60)纯 LPP (k6)SS-LPP (β0.5, α0.3)10%40 张78% 量级83% 量级88% 量级20%80 张84% 量级90% 量级93% 量级30%120 张88% 量级93% 量级95% 量级表里是量级参考实际数值随随机种子、k 和 t 的取值波动。要看的规律有两条标注率越低SS-LPP 相对纯 LPP 的优势越大标注率升到 30% 以上时两者差距收窄说明此时图权重里的判别信息已经接近饱和再堆标注不如去调 alpha。4.4 投影矩阵落地注册与比对阶段怎么写训练完成后只有三样东西需要落盘投影矩阵 A、训练时的样本均值和归一化参数。注册新员工时提取人脸、做同样的归一化、乘一次 A 得到特征向量入库比对时算余弦距离或欧氏距离取最近邻。def embed(A, x, mean, std): 单张人脸图 → 低维特征。x 是展平后的 1×d 向量。 x (x - mean) / (std 1e-8) return x A # 落盘 np.savez(ss_lpp_model.npz, AA, meanX.mean(axis0), stdX.std(axis0) 1e-8)注意run_ss_lpp内部对每张图做了逐样本归一化而推理阶段embed用的是全库均值和标准差两者口径不一致。工程上要统一成同一种要么全部改用全库统计量要么把逐样本均值和方差一并存下来。这个不一致在实际部署里经常表现为「离线评估 95%、上线只有 70%」排查时优先看这里。5. 参数调优与掉点排查半监督 LPP 的几个真实边界5.1 三个必调参数k、降维维数 d 与正则项 regk 的搜索区间放在 3 到 12 就够了配合标注比例交叉验证。降维维数 d 的拐点通常出现在0.3 × 标注样本数附近ORL 上标注 40 张时d 取 30 到 60 之间识别率就饱和了继续加维只会引入噪声方向。reg 是最容易被忽视的参数人脸数据上从 1e-3 开始按 10 倍量级往上试到 1e-1。5.2 三类典型掉点矩阵奇异、图断裂、伪标签污染症状常见原因排查动作识别率低于 PCA 基线k 太大或 t 太小图退化成近全连接或二值图打印 W 的非零占比超过 20% 说明图太稠报 LinAlgError 或结果全 NaNXᵀDX奇异提高 reg或先 PCA 降到 n−1 维以内标注率升高反而掉点alpha 过大类内结构被压碎alpha 从 0.3 单调下调重跑无标签样本被大面积分错伪标签置信度阈值太松把阈值从 0.8 提到 0.9或降低 LabelSpreading 的 max_iter图断裂的典型表现是某个身份的样本在投影后全部分散开不聚成簇。原因是该身份只有一张标注而 k 又大于它的实际近邻数图上这个点只连着异类。解决办法是给标注样本之间加一条不依赖 k 的全连接边也就是build_ss_graph里 W_w 那部分它本身就是对 kNN 图的兜底。5.3 和 PCA、LDA、NPE 的选型边界PCA 无监督、求解最快、对光照鲁棒但类间可分性差LDA 有监督、类间分离最强但要求每类样本数大于降维维数小样本下必须先用 PCA 降维再跑本质上是两步走NPE 是 LPP 的邻域保持版本用重构权代替热核权对 k 的选择没那么敏感但计算量更大。判断标准可以简化成一句标注样本数除以类别数小于 5 的时候用半监督 LPP 或 NPE大于 10 的时候LDA 加 PCA 预降维往往更省事没必要引入标签传播这一层不确定性。如果只允许改一个参数先把正则项 reg 从 1e-3 提到 1e-2 重跑一遍——高维小样本人脸数据上这一项带来的收益比反复调 k 和 t 来得更快、更确定。本文还有配套的精品资源点击获取