☰
多视角谱聚类Ncut实战:Matlab融合策略与避坑指南
2026/10/20 7:50:12 网站建设 项目流程

我们做图像或者用户行为数据的时候,经常会遇到这种尴尬:同一样本明明有好几组特征,颜色直方图是一套、纹理是一套、边缘信息又是一套,但经典谱聚类一次只能用一组,硬生生把信息割裂了。多视角谱聚类就是专门解决这个问题的,它把多个视角的相似度信息在谱框架里融合起来,再通过Normalized Cut(Ncut)做最后的图划分。这篇文章围绕多视角谱聚类的Matlab实现展开,把构图、拉普拉斯矩阵、特征分解、视角融合、参数折腾这几块一次讲透,同时把我在实际跑实验时踩过的坑也一并列出来。新手可以直接把代码抄走做基线,有基础的可以重点看融合策略为什么这么设计。

1. 为什么要做多视角谱聚类:从单视角到多视角的问题拆解

1.1 单视角谱聚类到底在做什么

先回到最基本的谱聚类。把每个样本看成一个节点,样本之间的相似度看成节点之间边的权重,于是整个数据集就变成了一张带权无向图。聚类的任务就变成了在图里切几刀,把图分成若干个子图,让子图内部边权重尽可能大、子图之间的边权重尽可能小。这类办法的厉害之处在于它不假设簇是凸的,两个月牙形的类别、环绕形的结构都能处理得很好,这是K-means很难做到的。

切图的方式有很多种,最基本的是RatioCut,也就是最小化切割代价除以子图大小。但RatioCut有个毛病:它只惩罚切掉的边数,没有考虑子图本身连得紧不紧,容易切出不平衡的簇。Ncut在这个基础上做了改进,切割代价除以子图内部所有边的总权重,相当于把“切割要轻”和“子图要紧凑”两件事放在同一个目标里。回过头来看,我们常说的谱聚类流程基本固定:先算相似度矩阵,再算度矩阵,接着构造拉普拉斯矩阵,然后做特征分解取前几个特征向量当作新特征,最后在新特征空间里跑K-means。Ncut是这个流程里最有代表性的归一化做法之一。

1.2 多视角数据到底多了什么

单一视角的谱聚类只接收一个特征矩阵X。但实际场景很少这么理想。图像可以同时提取颜色特征和SIFT特征;文本可以用词频统计,也可以用预训练模型抽出的语义向量;一个用户既有点击行为序列,又有填写过的属性标签。这些不同来源的特征就是所谓的多个视角。每个视角单独拿来跑谱聚类,得到的结果很可能彼此不一致,因为不同视角包含的信息既有互补也有冗余,甚至某个视角本身就带噪声。

多视角谱聚类的目标不是简单地把多个聚类结果投票表决,而是在更底层做融合。它希望在谱空间里找到一个共享的嵌入表示,这个表示同时兼顾各个视角的信息。用我的理解来说,每个视角都给了我们一张关于样本关系的地图,单看一张地图可能不完整,多视角融合就是把这些地图叠在一起,找出最可靠的道路网络。

1.3 Ncut与其他谱聚类变体的关系

谱聚类大体上有两个经典分支。一个是最小化RatioCut,走的是未归一化拉普拉斯L = D - W的老路;另一个是Ncut这条线,走的是归一化拉普拉斯L_sym = I - D^{-1/2} W D^{-1/2}。在实际的Matlab代码里,很多人实现Ncut时直接对L_sym做特征分解,取前k个最小特征值对应的特征向量,再做行归一化和K-means,这套流程严格来说是NJW方法。真正Shi-Malik论文里的Ncut还要解一个广义特征方程(D-W)v = λDv,但数学上这两个描述可以互相转换,特征向量之间只差一个D^{-1/2}的变换,聚类结果基本一致。所以按标题里的Ncut来写代码,用归一化拉普拉斯这条路是完全站得住的,理解上也不吃亏。

2. Ncut的核心原理与Matlab实现基础

2.1 Ncut目标函数和归一化拉普拉斯

Ncut的切割目标可以写成:

Ncut(A, B) = cut(A, B) / assoc(A, V) + cut(A, B) / assoc(B, V)

其中cut(A,B)表示子图A和B之间被切掉的边权重总和,assoc(A,V)表示A中所有节点到全图所有节点的权重总和。可以看到,即使两个簇的切割代价一样,如果某个簇的总体连接度更小,这个目标会给更大的惩罚,从而避免把小簇单独切开。直接优化这个目标是一个NP难问题,常规做法是把它松弛成广义瑞利商的形式,最终对应到特征分解上。

拉普拉斯矩阵选择对结果影响很大。L_sym = I - D^{-1/2} W D^{-1/2}的作用是把节点度的影响先统一到同一个尺度。打个比方,某个节点连接了很多其他样本,它的度数很高,如果不做归一化,这个节点在特征分解里的权重就会被放大;L_sym相当于先对每个节点做了一次“度数标准化”,让嵌入空间里的距离更接近真实的相似度结构。

2.2 相似度矩阵怎么构建才稳妥

相似度矩阵是整条流水线的入口。最常用的高斯核函数:

W_ij = exp(-||x_i - x_j||² / 2σ²)

Matlab里最直接的写法:

D2 = pdist2(X, X, 'euclidean').^2; W = exp(-D2 / (2 * sigma^2)); W(1:n+1:end) = 0; % 对角线置零,自己和自己不构成边

这里有个很容易被忽视的问题:这种写法生成的是稠密矩阵。n=2000时W已经占32MB内存,n=10000时就要800MB。所以样本量稍大就必须做稀疏化。最常见的做法是KNN稀疏化,只保留每个样本最近的k个邻居:

D2 = pdist2(X, X, 'euclidean').^2; [~, idx] = sort(D2, 2, 'ascend'); W = zeros(n, n); for i = 1:n W(i, idx(i, 2:min(knn+1, n))) = exp(-D2(i, idx(i, 2:min(knn+1, n))) / (2 * sigma^2)); end W = max(W, W'); % 对称化

对称化这一步一定要做,因为谱聚类基于无向图。用max(W, W')还是(W + W') / 2差别不大,我习惯用max,因为它保留的是两边至少一方认为相似的那些边,更贴近KNN的非对称逻辑。

2.3 特征分解和谱嵌入的完整套路

拿到W以后,构造度矩阵和拉普拉斯:

D_mat = diag(sum(W, 2)); L_sym = eye(n) - D_mat^(-0.5) * W * D_mat^(-0.5);

然后对L_sym做特征分解,取最小的k个特征值对应的特征向量:

[V, E] = eig(L_sym); [~, idx] = sort(diag(E), 'ascend'); Z = V(:, idx(1:k)); Z = Z ./ sqrt(sum(Z.^2, 2)); % 行归一化 labels = kmeans(Z, k, 'Replicates', 20);

这里有两个细节值得解释。第一个是为什么要取“最小”特征值对应的特征向量:拉普拉斯矩阵最小的特征值对应图的连通分量,特征值为0的数量等于连通分量个数;如果图完全连通,最小特征值就是0,对应的特征向量是常向量,聚类信息藏在后面几个维度里。第二个是行归一化:特征向量每一行的模长和节点度有关,不归一化的话K-means容易被某些度数特别高的节点带偏,归一化之后每个样本在新空间里落在单位球面上,聚类边界干净得多。

3. 多视角融合策略:三种主流的Matlab落地方式

3.1 相似度矩阵级融合:最简单也最常用

多视角Ncut最直观的做法是每个视角分别计算相似度矩阵W_v,然后加权求和:

W_total = zeros(n, n); for v = 1:V D2 = pdist2(X_cell{v}, X_cell{v}, 'euclidean').^2; W_v = exp(-D2 / (2 * sigma(v)^2)); W_v(1:n+1:end) = 0; W_v = max(W_v, W_v'); W_total = W_total + alpha(v) * W_v / sum(W_v(:)); end

这种做法的好处非常明显:融合发生在构图阶段,后面的拉普拉斯、特征分解、K-means一步都不用改,实现成本最低。但它的命门在于尺度问题。如果第一个视角的特征值是0到255,第二个视角是0到1,那么颜色视角的距离天然比纹理视角大得多,高斯核算出来数值差异可能差几个数量级。不提前做归一化的话,等价于把大部分权重都压给了颜色视角,多视角融合就成了空话。我在代码里用sum(W_v(:))给每个视角的相似度矩阵做了整体归一化,至少保证每个视角对W_total的贡献处在同一个量级。

3.2 拉普拉斯矩阵级融合:目标函数更清晰

第二种做法是分别构造每个视角的拉普拉斯矩阵L_v,然后对拉普拉斯矩阵做融合:

L_total = zeros(n, n); for v = 1:V L_v = eye(n) - D_v^(-0.5) * W_v * D_v^(-0.5); L_total = L_total + alpha(v) * L_v; end [V, E] = eig(L_total);

从谱图理论的角度看,这个做法的解释性更强。谱聚类可以看成最小化tr(Z^T L Z),其中Z是谱嵌入。把多个L_v线性组合,相当于在一个统一的优化目标里同时要求嵌入在所有视角上图都足够平滑。这比在相似度层面融合更接近Ncut本身的数学框架。代价是如果某个视角的图结构本身很糟糕,它会直接污染融合后的拉普拉斯矩阵,而且不像W级融合那样可以直观地观察每个视角的相似度矩阵长什么样。

3.3 谱嵌入级融合:拼特征向量的想法

第三种做法很有意思,对每个视角单独做特征分解,得到各自的谱嵌入Z_v,然后把这些嵌入拼成一个大矩阵:

Z_all = []; for v = 1:V [V_v, E_v] = eig(L_sym_v); [~, idx_v] = sort(diag(E_v), 'ascend'); Z_v = V_v(:, idx_v(1:k)); Z_v = Z_v ./ sqrt(sum(Z_v.^2, 2)); Z_all = [Z_all, Z_v]; end labels = kmeans(Z_all, k, 'Replicates', 20);

拼接的思路是每个视角都贡献一组低维嵌入特征,最终让K-means在更高维的空间里寻找簇结构。这个方法实操里要注意两个坑。第一个是特征向量的符号问题,同一个视角的同一个特征向量,两次运行eig可能整体多乘一个-1,拼在一起的时候不同维度的符号不一致会让聚类结果不稳定,最好做个符号校正。第二个是不同视角的前k个特征向量不一定对应同样的语义维度,简单拼接可能引入冗余信息。适合作为对比实验和鲁棒性参考,不太适合作为唯一方案。

3.4 视角权重怎么设才能不翻车

多视角融合里最容易被问到的就是alpha怎么调。我的建议是:第一版先用等权重,但必须配上前面说的相似度矩阵归一化。等权重不出意外的话已经能赢过绝大多数单视角结果。然后可以按每个视角的判别能力给权重,一个简单代理指标是看特征值衰减速度,衰减快说明谱能量集中在少数几个维度,通常信息更集中。更复杂的方法就是用网格搜索,在多折交叉验证下把alpha当作超参数扫一遍,维度少的时候完全跑得起。

4. 一份可运行的多视角Ncut代码逐段拆解

4.1 主函数骨架

下面这份代码是我整理过的一个最小实现,输入是V个视角的特征矩阵组成的cell数组,输出是每个样本的聚类标签。

function labels = MultiViewNcut(X_cell, k, opts) % X_cell: 1 x V 的 cell,每个元素是 n x d_v 的特征矩阵 % k: 聚类类别数 % opts.sigma: 1 x V 的高斯核带宽,可以不传 % opts.knn: KNN 邻居个数,默认 10 % opts.alpha: 1 x V 的视角权重,默认等权重 % 返回值 labels: n x 1 的聚类标签

主函数内部按下面几步走:先检查参数,再做各视角特征标准化,接着算融合相似度矩阵,然后构造归一化拉普拉斯,最后做特征分解、行归一化和K-means。

4.2 核心代码分步解读

特征标准化这一步很多人会跳过,但它恰恰是多视角融合里性价比最高的防御动作:

for v = 1:V X_cell{v} = zscore(X_cell{v}); end

zscore把每个特征维度变成零均值单位方差,至少不让量纲差异毁掉后面的欧氏距离计算。如果特征本身带有很强的语义尺度,比如某种特征里数值大小本身有意义,可以换成min-max归一化,但要保证所有视角都统一处理。

构图和融合的代码:

n = size(X_cell{1}, 1); V = length(X_cell); if ~isfield(opts, 'alpha') alpha = ones(1, V) / V; else alpha = opts.alpha; end W_total = zeros(n, n); for v = 1:V D2 = pdist2(X_cell{v}, X_cell{v}, 'euclidean').^2; if isfield(opts, 'sigma') && ~isempty(opts.sigma) sigma_v = opts.sigma(v); else sigma_v = median(D2(:)); % 经验默认值 end W_v = exp(-D2 / (2 * sigma_v^2)); W_v(1:n+1:end) = 0; [~, idx] = sort(D2, 2, 'ascend'); knn = 10; if isfield(opts, 'knn') && ~isempty(opts.knn) knn = opts.knn; end mask = zeros(n, n); for i = 1:n mask(i, idx(i, 2:min(knn+1, n))) = 1; end mask = max(mask, mask'); W_v = W_v .* mask; W_total = W_total + alpha(v) * W_v / sum(W_v(:)); end

这一段值得注意的地方是KNN掩码用的是boolean乘法而不是把W_v直接置为KNN值,这样高斯核权重仍然保留真实相似度的大小,只把非邻居的相似度清零。

构造拉普拉斯并做特征分解:

D_total = diag(sum(W_total, 2)); L_sym = eye(n) - D_total^(-0.5) * W_total * D_total^(-0.5); L_sym = (L_sym + L_sym') / 2; % 防止数值不对称 [V, E] = eig(L_sym); [~, idx] = sort(diag(E), 'ascend'); Z = V(:, idx(1:k)); Z = Z ./ sqrt(sum(Z.^2, 2) + 1e-10); labels = kmeans(Z, k, 'Replicates', 20);

加1e-10是为了防止某一行全零导致除零报错,行归一化分母为0的情况在实际数据里偶尔出现,多半是某节点没有任何有效连接,说明构图需要调整。

4.3 sigma、KNN和k到底怎么选

这几个参数的组合直接决定实验结果。sigma的经验做法是把pairwise距离矩阵的中位数当作默认值,原因是中位数对异常距离不那么敏感,而且在这个尺度下高斯核的相似度分布会比较均匀,不会出现所有相似度都挤在0附近或者都接近1的极端情况。想要更精细就做个搜索,观察不同sigma下特征值gap的变化。

KNN邻居数一般10到30之间。KNN太小图容易碎,会出现多个连通分量;太大会让稀疏化失去意义,保留太多远距离的弱连接。我的习惯是先设10,看拉普拉斯特征值有几个接近0,如果明显大于1再逐步调大。聚类数k本身可以靠特征值gap判断,归一化拉普拉斯的前k个特征值如果出现明显跳变,那跳变位置通常对应合理的聚类数。当然半监督场景里直接用真实标签数也行。

5. 实操中掉进去的坑和排查技巧

5.1 特征尺度不一致:多视角融合的头号杀手

我最早跑多视角谱聚类时吃过一次大亏。我有一个视角是图像灰度直方图,数值在0到1之间,另一个视角是原始的RGB像素拼成的特征,数值范围0到255。直接构图后融合,所有相似度几乎被RGB视角主导,另一个视角再怎么调sigma都没存在感。后来诊断才发现是尺度问题。排查技巧也简单:把每个视角的W_v直接打印出来,看看mean和max差多少。如果不同视角的W_v均值差两个数量级,必须回头做特征归一化,而不是调权重。

5.2 图不连通带来的诡异结果

KNN设成5,样本分布又不均匀,很容易出现孤立点。表现是什么?拉普拉斯矩阵的特征值里会出现多个接近0的值,而理论上应该只有一个。谱嵌入里孤立点落在离群的位置,K-means会单独给它划一个簇出来。这看起来像完美聚类,其实是构图问题。我的处理方式是先检查图的连通分量数量,如果连通分量大于1,要么调大KNN,要么对每个连通分量单独跑谱聚类,最后把结果映射回去。

5.3 eig太慢:大规模样本必须换eigs

eig对稠密矩阵会计算全部特征向量,复杂度O(n³)。n到3000就已经很吃力,上5000基本没法等。替代方案是改用eigs求稀疏矩阵的少量特征向量:

W_total_sp = sparse(W_total); D_total = diag(sum(W_total_sp, 2)); L_sym = speye(n) - D_total^(-0.5) * W_total_sp * D_total^(-0.5); opts.issym = 1; opts.tol = 1e-8; [V, E] = eigs(L_sym, k, 'smallestreal', opts);

这里必须强调的是L_sym是实对称矩阵,设置opts.issym = 1能让求解器利用对称性。求最小特征值要明确指定'smallestreal',否则eigs默认求最大的几个,拿到的就是最大特征值对应的向量,聚类信息完全不对。我第一次用eigs时就犯过这个错,拿到一堆看不出结构的嵌入,检查半天才发现是求反了方向。

5.4 特征向量符号翻转和K-means随机性

谱嵌入之后K-means本身有随机初始化,所以同一次数据多次运行得到不同聚类结果太正常了。但eigs还有一个额外问题,就是特征向量的整体符号可能翻转。在做多视角谱嵌入拼接时,不同视角的符号不一致会让拼接后的空间完全错位。一个简单的防御手段是让每个特征向量的符号固定,比如令每个向量的第一个非零元素为正:

for j = 1:size(Z, 2) idx_nz = find(abs(Z(:, j)) > 1e-8, 1); if Z(idx_nz, j) < 0 Z(:, j) = -Z(:, j); end end

这不能保证绝对正确,但至少能让不同运行之间的嵌入在符号上保持一致,方便复现和比较。

5.5 多视角融合反而变差:先别急着上重方法

有一种很打击人的情况:融合了三个视角,结果反而比最好的单视角还差。我在实验里遇到过不止一次。原因通常是某个视角噪声太大,把它等权重融进W_total以后,等于给整个图注入了一堆虚假的弱连接。排查办法是逐个视角看单独聚类精度,如果某个视角明显低于其他视角,先尝试降低它的权重或者直接剔除。不要一上来就怀疑融合策略,很多时候是输入数据质量不齐,这是数据问题不是算法问题。

6. 多视角Ncut的适用边界和后续扩展

多视角谱聚类这套Ncut实现比较适合样本量在几千到几万、视角数在3到5个左右的场景。它最大的优点是不需要训练,拿到特征矩阵就能出结果,非常适合作为聚类分析的第一版基线。如果样本量超过十万,稠密的W矩阵和KNN构图都会成为瓶颈,那时候更合适的是MiniBatchKMeans结合图嵌入的近似方案,或者直接上基于深度网络的多视角聚类。

后续可以扩展的方向也很多。一个是在权重alpha上做文章,比如用多核学习的思想同时优化核权重和聚类目标;另一个是把随机游走的马尔可夫链视角用进来,每个视角对应一个转移概率矩阵,通过凸组合或乘积组合得到全图的随机游走过程;再一个就是和自编码器结合,先用网络学习每个视角的低维表示,再对学到的表示做Ncut融合。这些方向我都试过一部分,最直观的感受是先把手写版多视角Ncut跑通,再谈进阶方法,因为很多坑在简单版本里已经有了,换成复杂模型之后更难排查。

最后分享一个个人体会。多视角谱聚类里最容易被低估的是数据预处理和构图阶段,很多人把所有精力放在融合算法上,结果死在了特征量纲和KNN参数上。我后来每次跑新数据集,都会先把每个视角的W_v可视化一遍,或者至少打印出均值、最大值和稀疏度,确认图结构合理了再进入融合和聚类环节。这套折腾下来,多视角Ncut在大多数中等规模数据上都能给出稳定且合理的结果。

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

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

立即咨询