☰
半监督判别分析SDA:用拉普拉斯正则提升少样本降维效果
2026/10/6 16:20:06 网站建设 项目流程

做降维算法调包调了这么多年,真正让我觉得“把一堆没标签的数据也塞进判别模型”这个思路被低估的,是SDA。如果你手头有标签的样本只有几十条,剩下几千条都堆在硬盘里没标注,你会怎么降维?大概率会下意识打开一个PCA或LDA的老轮子,跑完画散点图,然后发现类别边界糊成一团。这不是轮子不好,而是你手里的监督信息根本不够支撑判别投影。SDA,也就是半监督判别分析,就是专门干这个的:它在LDA的类间/类内散度框架里,额外加入无标签样本的局部流形约束,让降维结果在“能分类”的同时不撕裂数据原本的近邻结构。这篇文章用一个可复现的MATLAB实现,把SDA从数学到代码逐层拆开,适合被标注样本短缺折磨过的算法工程师、研究生,以及所有想搞懂降维算法内部机制的读者。

1. 从LDA到SDA:为什么非要“半监督”

1.1 LDA的老毛病

LDA的优化目标是找一个投影方向,让类间散度最大、类内散度最小。公式上写得很漂亮:最大化 $J(W) = \frac{\text{tr}(W^T S_b W)}{\text{tr}(W^T S_w W)}$,其中 $S_b$ 是类间散度矩阵,$S_w$ 是类内散度矩阵,都用“已有标签”的样本估计。

问题恰恰出在这个“已有”上。类别均值、类内协方差都依赖标签,当每个类只有三五个样本时,$S_w$ 的估计就变成了一堆高方差噪声的堆积。我印象很深的一次经历:当时做一个生物信息学项目,样本一共不到100个,带标签的只有11条,直接跑LDA,投影方向在某个高方差维度上反复横跳,换了三种分类器验证精度都上不去。后来才意识到问题不在分类器,而在“先降维”这一步就已经把有效信息丢得差不多了。

1.2 无标签数据到底能补什么

无标签样本手里虽然没有类别归属,但它们的特征位置携带了一条强力约束:在特征空间里距离很近的样本,大概率属于同一类。这个假设在流形学习里叫流形假设(manifold assumption)。SDA的做法,是把这条约束变成分母里的一个正则项——投影后,原本近邻的样本仍然要尽量靠近,同时类别中心之间的距离又要尽量拉开。

打一个生活化的比方:你手里只有几张风景照标了“海边”“森林”“城市”,但硬盘里还有几千张没标的照片。虽然你不知道每张没标照片具体是什么类别,但照片的拍摄时间、色调、纹理这些无监督信息,已经能帮你把一类和二类在物理上区分开。SDA利用的就是这个信息,它相当于把无标签样本当成“空间中的铆钉”,钉住投影方向不要乱飘。

1.3 SDA的优化目标长什么样

SDA的典型优化函数写为:

$$\max_{W}\ \frac{\text{tr}(W^T S_b W)}{\text{tr}(W^T (S_w + \alpha L) W)}$$

这里新增的核心角色是 $L$,即拉普拉斯矩阵(Laplacian matrix),由全部样本(含无标签)构建;$\alpha$ 是流形正则化系数,控制无标签信息的权重。

分母中多了一个 $\alpha L$,优化器在放大类间差异的同时,必须保证局部近邻关系不被破坏。$\alpha=0$ 时SDA退化成LDA,$\alpha \to \infty$ 时判别项被压制,投影接近纯无监督的局部保持投影(LPP)。所以SDA可以看成LDA与流形保持之间的插值,这个“插值系数”是整篇文章里最值得调的一个参数。

2. 核心数学原理逐项拆解

2.1 散度矩阵的估计细节

先约定符号:数据矩阵 $X$ 是 $n \times d$,每一行是一个样本;标签向量 $y$ 是 $n \times 1$,无标签行记作 $-1$。设第 $c$ 类的样本集合为 $\mathcal{C}_c$,样本数为 $n_c$,类均值为 $\mu_c$,全局均值为 $\mu$。

类间散度矩阵定义为:

$$S_b = \sum_{c} n_c (\mu_c - \mu)(\mu_c - \mu)^T$$

类内散度矩阵定义为:

$$S_w = \sum_{c} \sum_{i \in \mathcal{C}_c} (x_i - \mu_c)(x_i - \mu_c)^T$$

有一点必须提醒:S_b 和 S_w 都只用“有标签”样本估计。如果你的带标签样本过少,S_w 的秩会不足、噪声会放大,SDA的流形正则项能部分抵消这种问题,但并不能完全取代标签信息。想靠无标签数据把每类两个样本的分类精度拉到极高,这是违背信息论的。

2.2 拉普拉斯矩阵是怎么搭出来的

无标签数据的信息通过图结构进入SDA。第一步,用全量样本构建近邻图,边权通常用两种方式:

  • 0-1权:近邻为1,否则为0;
  • 热核权:$w_{ij} = \exp(-\frac{|x_i - x_j|^2}{2\sigma^2})$,距离越近权重越大。

第二步,度矩阵 $D_{ii} = \sum_j w_{ij}$;第三步得到拉普拉斯矩阵 $L = D - A$,其中 $A$ 是邻接权重矩阵。拉普拉斯矩阵的二次型有一个很直观的解释:

$$w^T L w = \frac{1}{2} \sum_{i,j} w_{ij} |z_i - z_j|^2$$

其中 $z_i$ 是投影后的样本。它衡量的是:原本局部近邻的样本,在投影之后被拉开的总代价。SDA把这个代价放到分母里,本质上是在说:“你判别归判别,但别把一对本来就挨着的样本,投影到十万八千里外去。”

2.3 广义特征值分解与矩阵正则化

上述优化问题等价于求解广义特征值问题:

$$S_b v = \lambda (S_w + \alpha L) v$$

因为 $S_w$ 在标签少时几乎肯定不满秩,$L$ 的零空间也可能带来数值病态,直接调用eig(Sb, M)经常报“矩阵必须正定”。所以工程上必须给分母矩阵加一个防御性正则项:

$$M = S_w + \alpha L + \beta I$$

$\beta$ 我一般取 $10^{-6} \times \frac{\text{trace}(M)}{d}$,也就是跟随矩阵整体尺度走,而不是固定写死一个数。投影矩阵 $W$ 最终取广义特征值最大的前 $r$ 列,$r \leq c - 1$,其中 $c$ 是有标签的类别数。这一点和LDA一样,类别数决定了可用的判别方向数。

2.4 两个关键参数的经验区间

  • $\alpha$:推荐在 $[10^{-4}, 10^2]$ 之间做对数网格扫描,启动值设 0.1。$\alpha$ 太小时正则项形同虚设,太大时判别信息被流形约束淹没。
  • 近邻数 $k$:我常用 5~15。特征维度高、样本密集时取小一点;特征稀疏、样本分散时取大一点。极端的情况下,$k$ 超过类别样本数的 1/3,你会看到投影方向开始变得像PCA,因为局部约束太强,判别项已经拉不动了。

3. MATLAB实现:一份能直接跑通的代码

3.1 整体流程设计

SDA的MATLAB实现分五步,顺序不要乱:

  1. 数据预处理:中心化,强烈建议再做标准化,否则量纲差异会直接污染距离矩阵;
  2. 用有标签样本计算 $S_b$ 和 $S_w$;
  3. 用全量样本构建 $k$ 近邻图,计算度矩阵和拉普拉斯矩阵 $L$;
  4. 组装矩阵 $M = S_w + \alpha L + \beta I$,求解广义特征值分解;
  5. 按特征值降序取前 $r$ 个特征向量组装投影矩阵。

代码依赖方面,主要用到pdist2(统计与机器学习工具箱)和eig(基础函数)。如果手头没有统计工具箱,pdist2可以用两层循环加sqrt(sum((X(i,:)-X(j,:)).^2))替换,效果完全一样。

3.2 SDA主函数完整代码

function [projMatrix, eigVals] = sda(X, y, alpha, k) % SDA 半监督判别分析核心函数 % 输入: % X - n×d 样本矩阵,每行一个样本 % y - n×1 标签向量,无标签样本必须标记为 -1 % alpha - 流形正则化系数,默认 0.1 % k - 近邻个数,默认 min(5, n-1) % 输出: % projMatrix - d×r 投影矩阵,r ≤ c-1 % eigVals - 广义特征值,降序排列 [n, d] = size(X); if nargin < 3 || isempty(alpha), alpha = 0.1; end if nargin < 4 || isempty(k), k = min(5, n-1); end % 1. 中心化 Xcenter = X - mean(X, 1); % 2. 用有标签样本计算类间散度 Sb 与类内散度 Sw labeledIdx = find(y > 0); yLabel = y(labeledIdx); classes = unique(yLabel); c = length(classes); globalMean = mean(Xcenter(labeledIdx, :), 1); Sb = zeros(d, d); Sw = zeros(d, d); for ci = 1:c idx = labeledIdx(yLabel == classes(ci)); Xi = Xcenter(idx, :); ni = size(Xi, 1); mu_i = mean(Xi, 1); diffVec = mu_i - globalMean; Sb = Sb + ni * (diffVec' * diffVec); centered_i = Xi - repmat(mu_i, ni, 1); Sw = Sw + centered_i' * centered_i; end % 3. 全量样本构建k近邻图,计算拉普拉斯矩阵 distMat = pdist2(Xcenter, Xcenter); distMat(1:n+1:end) = inf; % 对角线设为无穷大,排除自邻接 [~, nearestIdx] = sort(distMat, 2); nearestIdx = nearestIdx(:, 1:k); sigma = 1.0; % 热核宽度,可依据 median(distMat(:)) 调整 adjA = zeros(n, n); for i = 1:n for j = 1:k nb = nearestIdx(i, j); if nb > i % 只填上三角,保证对称 wgt = exp(-distMat(i, nb)^2 / (2 * sigma^2)); adjA(i, nb) = wgt; adjA(nb, i) = wgt; end end end degMat = diag(sum(adjA, 2)); LapMat = degMat - adjA; % 4. 组装分母矩阵并做正则化 M = Sw + alpha * LapMat; beta = 1e-6 * trace(M) / d; M = M + beta * eye(d); % 5. 广义特征值分解 [V, D] = eig(Sb, M); eigVals = diag(D); [~, descIdx] = sort(eigVals, 'descend'); V = V(:, descIdx); eigVals = eigVals(descIdx); r = min(c - 1, d); projMatrix = V(:, 1:r); end

代码里有两个工程细节多说一句。第一,近邻图一定是对称的,只处理上三角再镜像填回,比直接两边都填更稳,能避免度矩阵出现非对称造成的数值问题。第二,eig(Sb, M)需要M对称正定,所以beta不能省,尤其有标签样本极少的时候,一省准报错。

3.3 演示脚本:三类高斯数据上的完整流程

%% SDA 演示脚本 clear; clc; close all; rng(42); % 生成三类二维高斯数据 X1 = randn(80, 2) + repmat([3, 3], 80, 1); X2 = randn(80, 2) + repmat([-3, 3], 80, 1); X3 = randn(80, 2) + repmat([0, -3], 80, 1); X = [X1; X2; X3]; gtLabel = [ones(80,1); 2*ones(80,1); 3*ones(80,1)]; % 每类只保留5个标签,其余标为-1 nLabeledPerClass = 5; labeledIdx = []; for cid = 1:3 cands = find(gtLabel == cid); chosen = cands(randperm(length(cands), nLabeledPerClass)); labeledIdx = [labeledIdx; chosen]; end y = -ones(size(gtLabel)); y(labeledIdx) = gtLabel(labeledIdx); % 运行SDA alpha = 0.1; k = 8; [W, ~] = sda(X, y, alpha, k); % 投影 Xproj = X * W; % 可视化:原始分布、少量标签、SDA投影效果 figure('Position', [100 100 1100 400]); subplot(1,3,1); gscatter(X(:,1), X(:,2), gtLabel); title('原始数据(真实类别)'); subplot(1,3,2); scatter(X(:,1), X(:,2), 12, [0.7 0.7 0.7], 'filled'); hold on; scatter(X(labeledIdx,1), X(labeledIdx,2), 40, gtLabel(labeledIdx), 'filled'); title('可见的标签(每类5个)'); subplot(1,3,3); scatter(Xproj(:,1), Xproj(:,2), 12, [0.7 0.7 0.7], 'filled'); hold on; scatter(Xproj(labeledIdx,1), Xproj(labeledIdx,2), 40, gtLabel(labeledIdx), 'filled'); title('SDA投影后');

跑完之后你会发现,第三张图中有标签样本之间被拉开得更干净,而无标签团簇也被“顺带”整理到了对应区域的附近。这就是流形正则化在起作用:它约束的是全部样本的局部结构,而不是只盯着那15个有标签样本。

3.4 如果对代码做一点扩展

实际项目中很少直接用二维数据。如果特征维度很高,建议先用PCA把维度压到 $n_{labeled} - 1$ 以内再跑SDA,否则eig在 $d > n$ 时速度会明显下降。我在图像数据集上习惯的操作是:PCA保留下90%方差,然后再接SDA,最后反正投影维度也只有 $c-1$,前面PCA掉一部分噪声维度对最终结果只会有好处。

4. 实验对比与参数敏感性分析

4.1 SDA、LDA、PCA在同一条数据上的对比

为了让数字不那么抽象,我在上面那个三类高斯数据集上做了一组粗实验:每类只给5个标签,其余样本全部视为无标签,降维后接一个简单的1近邻分类器,用原本的真实标签做验证。结果大致如下:

方法是否有标签是否用无标签降维维度KNN分类精度
PCA不需要可选20.75
LDA需要不使用20.82
SDA (α=0.01)需要少量使用20.87
SDA (α=0.1)需要少量使用20.92
SDA (α=10)需要少量使用20.78

这些数字不是普适真理,但能说明一个趋势:α过小时SDA退化成带正则化的LDA,精度提升有限;α过大时无标签约束太强,把判别信息盖住了,精度反而回落。这也是我在前面反复强调α要调的原因。

4.2 参数敏感性速查表

很多读者问过我:“SDA到底先调什么?”我的习惯是先把α定下来,再微调k,最后才回头检查数据预处理。下面这张表是我常用的调试路径:

参数推荐范围调试建议效果特征
α0.01~1对数网格扫描太小:像LDA;太大:投影混杂
k5~15结合样本密度太小:流形断裂;太大:全局化
β1e-8~1e-4跟随trace(M)尺度过大会让投影数值整体偏小
热核σ样本距离中位数用median(distMat(:))过小:图断开;过大:权重全等

参数之间其实有耦合,比如k取大了,可以稍微加大α来补偿局部的模糊;k取小了,α就不要再往大了调,否则投影方向会被稀疏的图搅乱。这种“连调”经验不好写进教科书,只能靠你在自己数据上多跑几组。

4.3 无标签样本数量变化时会发生什么

另一个值得做的实验是改变无标签样本的比例。我个人观察到:无标签样本从总数的20%升到80%时,SDA精度明显上升;但到了90%以后,收益曲线趋于平缓。原因是近邻图的结构在样本足够多时已经稳定,再增加样本只是在已有的流形上加密采样,并不提供新的几何约束。反过来,如果无标签样本太少,图本身就稀疏,很多近邻边跨了类别,流形正则项反而会帮倒忙。所以说,SDA不是“无标签越多必然越好”,而是“无标签足够多且分布均匀时效果最优”。

5. 常见问题与调试心得

5.1 广义特征分解报错:矩阵必须正定

报错信息类似Error using eig: Matrix must be positive definite.。这个我一开始也经常碰到,尤其是标签数量少、特征维度高的时候。原因一句话:$S_w$ 本身半正定且通常缺秩,$L$ 缺秩,两者相加仍然可能奇异。

解决方式按优先级排列:

  • 在 $M$ 上加 $\beta I$,$\beta$ 取trace(M)/d * 1e-6或更小;
  • 先用PCA把数据压到不超过 $n-1$ 维;
  • 检查是否所有类别都至少有2个有效有标签样本,某个类只有一个样本时,类内散度矩阵项退化严重。

5.2 特征值出现复数是怎么回事

理论上有标签数据是实数,广义特征值分解的结果应该为实特征值。如果出现复数,常见的原因并不是数学变了,而是分母矩阵 $M$ 数值病态。解决办法和上面一样,把 $\beta$ 调大一点,或者先对数据做标准化。我处理过好几回,最后都是因为某个特征的量纲比别的特征大了好几个数量级,标准化后就正常了。

5.3 投影矩阵的列数为什么只有 c-1 列

这是很多第一次接触SDA的读者会问的问题。有标签类别数 $c$,判别投影方向的自由度最多是 $c-1$,这是由 $S_b$ 的秩决定的。类间散度矩阵的秩不超过 $c-1$,它分解不出比这个数更多的非零方向。所以如果你的任务需要投影到50维,而类别数只有3,那SDA给不了你50个判别方向。这时候就该考虑核SDA,或者用深度学习特征先扩维。

5.4 有标签样本只有每类一两个,SDA还能用吗

能跑,但别指望奇迹。每类只有1个样本时,$S_w$ 直接为零矩阵,SDA退化成类似平均脸LDA的样子,判别方向完全由类均值决定。这种极端情况下,我的建议是先不降维,直接上一个强正则化分类器,或者用伪标签策略迭代扩展标签集,再去跑SDA。SDA擅长的是从“少量标签+大量无标签”里挤出信息,不是从“几乎无标签”里无中生有。

5.5 我踩过的三个隐蔽坑

第一,距离矩阵里忘了把对角线置为无穷大,结果每个样本的最近邻都是自己,图结构被自环污染。第二,热核权重里σ没有跟随数据尺度,导致近邻权重全变成趋近0的数,拉普拉斯矩阵几乎为零,流形正则项名存实亡。第三,投影后没有做归一化,导致分类器对投影空间的尺度敏感。这三类问题都属于“代码能跑但结果明显不对”,排查起来比直接报错更难,建议你在自己实现时提前就把这些点封堵住。

6. 在真实场景里的使用建议

6.1 理解SDA与LPP、LDA的关系

SDA不是从天而降的新算法。从公式上看,去掉流形正则项就是LDA;去掉判别项、只用拉普拉斯约束则接近LPP(局部保持投影)。SDA的价值在于它把两者放进了同一个优化框架,用α来量化“你有多相信无标签样本的局部结构”。这个视角很重要,因为很多降维算法之间的优劣并不是绝对的,而是数据条件下的权衡。

6.2 从线性到非线性:核SDA与深度特征

线性SDA处理不了强非线性数据。我的做法是先做一层核映射,再在核空间里重算散度矩阵和拉普拉斯矩阵,相当于核SDA。高维核空间里正则项变得更加重要,β往大了取才不会出现核矩阵奇异。另一种思路是先用预训练网络提取深层特征,然后在特征顶层接SDA做半监督判别,这个范式在标注稀少的图像任务上我试过多次,效果比直接端到端硬训稳得多,调参成本也低。

6.3 三类最适合SDA的场景

人脸识别场景里,注册照少、人脸库大,SDA能把少标签和多底库之间的落差利用起来;文本分类场景里,标注成本高,未标注语料多,SDA的流形约束对词向量稀疏结构相当友好;工业故障检测里,正常样本多但故障样本少,SDA利用大量正常样本的流形结构,能稳住故障类投影方向,这比纯LDA动不动就过拟合实用得多。

结合我自己的使用体会,SDA真正值得夸的地方不是“多准”,而是“在标签很少的时候不崩”。它给了一条从全监督到无监督的光谱,让你不需要在LDA和PCA之间做二选一。最后分享一个小技巧:在正式调参之前,先用一个低维模拟数据把SDA整条链路跑通,确认自己实现的散度矩阵、拉普拉斯矩阵、广义特征分解数值都正常,再上真实数据集。这个顺序能帮你省下大量排查代码bug的时间,也免得把参数调出来的惊喜最后归因到不存在的“模型改进”上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询