做图聚类这些年,我反复推荐的一篇老文章就是NIPS 2011上的《Beyond Spectral Clustering: Tight Relaxations of Balanced Graph Cuts》。别被标题里的Spectral Clustering劝退,它其实讲的是所有做无监督聚类和社区发现的人都该关心的问题:谱聚类作为一种连续松弛,到底松在哪里,有没有办法在不牺牲太多计算效率的前提下,把它换成一个更贴近原始离散目标的优化问题。这篇论文给出的答案,后来影响了一大批“改进谱聚类”的工作,也间接启发了我在图嵌入模型里做校验模块的思路。如果你是在读研究生,或者做图表示学习、社区发现、聚类理论相关方向的工程师,花一个下午把这篇论文的核心思想读透,比盲目刷十篇“用图神经网络做聚类”的论文更有帮助。
1. 从图割问题说起:平衡到底在平衡什么
1.1 最小割为什么不能直接用
聚类最常见的图建模方式,是把每个样本看成图上的顶点,把样本间的相似度看成边的权重。聚类就变成图划分:把顶点集合V划分成k个子集,希望子集内部连边尽量密、子集之间的连边尽量稀。最朴素的目标是最小化“割值”,也就是不同组之间所有边的权重和。可这个目标有个众所周知的毛病:退化解太多。只要图里有几个孤立点或一个明显的小社区,把它们单独切出来,割值就会很小,甚至趋近于零。这不是我们要的聚类,因为它完全忽略了每个子集内部有没有结构。
解决退化解的标准办法是在目标函数里加入“平衡项”。分母用子集大小,就是RatioCut;分母用子集体积,也就是子集内所有顶点的度数之和,就是Normalized Cut(NCut)。写成统一形式,一个合理的平衡割目标大概长这样:
$$\text{RCut}(S_1,\dots,S_k)=\sum_{j=1}^{k}\frac{W(S_j,\bar S_j)}{|S_j|}$$
$$\text{NCut}(S_1,\dots,S_k)=\sum_{j=1}^{k}\frac{W(S_j,\bar S_j)}{\mathrm{vol}(S_j)}$$
分母越大,切出一个小簇的惩罚就越高,于是算法会自觉避开那种“把一个点单独切出去”的捷径。这就是“平衡”二字的实际含义,它并非让每个簇的样本数强行相等,而是通过惩罚项让结果不会落在极端不平衡的解上。
同类的目标函数还有Cheeger cut,它把每个簇看成一个小的比例割,然后取最大值或求和。这类目标有一个共同特点:一旦加上平衡约束,问题立刻从多项式时间可控变成NP难。普通最小割有成熟的多项式算法,但带平衡项后,整数划分的搜索空间爆炸式增长,所以大家都不再去硬解这个整数优化,而是想办法把离散变量“放松”成连续变量。
1.2 谱聚类其实是“松弛”的产物
谱聚类为什么能用特征向量做聚类?因为它对应的正是平衡割问题的连续松弛。以NCut为例,如果为每个簇定义一个指示向量,那么NCut可以写成关于指示向量二次型的形式。把这个离散指示向量换成连续向量,约束条件从“每个向量的分量非0即1”放松成“向量之间正交且单位范数”,优化问题就变成:
$$\min_{U^{T}U=I}\ \mathrm{Tr}(U^{T}L_{sym}U)$$
其中$L_{sym}=D^{-1/2}(D-W)D^{-1/2}$是归一化拉普拉斯矩阵。根据Rayleigh-Ritz定理,这个连续问题的最优解,恰好就是$L_{sym}$最小的k个特征值对应的特征向量。谱聚类里的特征分解并不是某种“魔法降维”,它是在解一个被松弛过的连续优化问题。
理解这一点很重要。谱聚类的流程分两步:先做特征分解得到嵌入$U$,再对矩阵的行做k-means。第二步本质上是把连续解“舍入”回离散标签。可问题是,第一步的目标函数和第二步的聚类目标并不一致。特征分解只保证$U$在连续松弛意义下最优,一旦把它送进k-means,原始离散目标的最优性就不再有任何保证。这篇NIPS论文的核心,就是盯着这个裂口做文章。
2. 谱松弛为什么不够紧:三个致命细节
2.1 连续解不等于离散解
很多初学谱聚类的人以为特征向量会天然形成“一团一团”的结构,实际数据远没有这么干净。理想情况下,两簇数据的指示向量只取±1,松弛后特征向量的每个分量却可以是任意实数。问题就出在这些实数上:当两个簇规模悬殊时,大簇里几乎所有节点在特征向量上的取值都挤在同一个很小的数值附近,小簇的取值明显偏离。你靠符号或阈值划分倒也能分开,但边界只要遇到一点噪声,就有一批本应属于大簇的节点被错分出去。
这不是某个数据集特有问题,而是松弛间隙在作祟。论文里有一个非常关键的观察:离散划分的可行域是连续可行域球面上的极端点集合。松弛越松,极端点之间的空间越大,从连续最优解“投影”到离散最优解时的信息损失就越大。谱聚类恰好是最松的一档,因为它只保留了特征值的和,把每个特征向量的具体方向信息大部分丢掉了。
2.2 多簇问题没有Cheeger不等式那样的保证
两簇Cheeger割有一个非常漂亮的性质:归一化拉普拉斯的第二小特征值和Cheeger常数之间存在一个双向不等式。常说的Cheeger不等式表明,谱聚类在二分类场景下“还算紧”,第二小特征值小,则图上一定存在一个割值不大的划分;反过来,如果存在好的划分,第二小特征值也一定不会太大。这个不等式的存在,给二簇谱聚类提供了理论上的近似保证。
可一旦推广到k大于2,事情就立刻变糟。多簇版本的谱界远不如二簇干净,谱聚类输出的结果和最优k路平衡割之间,可以差得非常远。论文研究背景里很重要的一个动机,正是指出这个多簇“紧度”缺口。它想解决的问题不是“谱聚类在某个benchmark上掉了两个点”,而是从优化理论层面指出:谱聚类作为一种松弛方法,并没有给多簇平衡割提供严谨保证。所谓“Beyond Spectral Clustering”,就是在谱聚类停下的地方继续往前走一步。
2.3 特征向量的旋转不变性是个隐藏陷阱
还有一个常被忽略的细节:如果$U$是前k个特征向量组成的矩阵,那么$UQ$($Q$是任意$k\times k$正交矩阵)同样是这组特征值对应的特征向量。因为特征子空间本身是旋转不变的。可k-means是对$U$的行做坐标聚类,一旦对整个坐标空间施加旋转,行坐标就会变化,聚类结果自然也会跟着变。
这说明传统谱聚类在结构上就不稳定:松弛环节对旋转不敏感,但后续聚类环节对旋转极敏感。论文在构造更紧松弛时,明确考虑了这个现象——它不是把“特征分解”和“聚类”切成两个独立阶段,而是把聚类指标直接放进优化目标里,让目标函数同时惩罚“嵌入方向不恰当”。读到这里我才真正明白,为什么很多谱聚类改进工作都强调要“联合优化”而不是“分步优化”。
3. 论文怎么把松弛收紧:核心思路拆解
3.1 用凸包络逼近NP难问题
要理解这篇论文的方法,最关键是记住一个操作:作者把平衡割目标函数放到了“凸包络”框架里处理。凸包络是数学里常用的思想,对一个非凸函数,取它的“最大凸下方函数”,然后在凸集上优化这个包络函数。这样得到的连续最优值,就是原离散问题理想化的下界。优化一个凸函数比优化非凸函数容易得多,而且凸包络选取得当的话,松弛间隙可以被压到很小。
问题是,完整凸包络在绝大多数问题里根本算不出来。最大割问题有著名的SDP松弛,效果好是因为半定规划能在多项式时间内求解,但它不能在所有规模上都做到“最紧”。论文的贡献在于:对一类具体的平衡割目标,作者证明了它的近似凸包络可以用特征分解高效计算,不需要解大尺度SDP。这个结论把“最紧”从理论概念拉回到可计算层面。
3.2 正交与非正交:两个层次的松弛
我读这篇论文时,印象最深的是它把松弛分成两个层次。第一层保留正交约束$U^{T}U=I$,但在目标函数里加入对割比值的直接度量,而不是只使用特征值的和。这个版本的松弛比谱聚类更紧,计算上仍然依赖特征分解和低维子空间的极值方向更新,实现成本相对可控。第二层干脆放松正交约束,允许$U$的各列线性相关。好处是搜索空间变大,理论上能够逼近更紧的下界;坏处是需要额外添加正则项或正交化步骤,否则优化过程容易退化。
作者在合成数据上做实验时,两个层次的改进效果并不一样。正交版本的实现更稳,在大多数图上都比谱聚类好;非正交版本在簇中心接近、噪声较多、簇规模差异较大的数据上提升更明显,但调参更敏感。这提醒我:更紧的松弛不是白给的,它通常要用稳定性或调参成本去换。
3.3 算法主干:特征分解加低维子空间迭代
把论文读到最后,算法路线其实可以概括成四步。第一步,由数据构造相似度图,代入高斯核或k近邻图,得到度矩阵和归一化拉普拉斯矩阵。第二步,计算归一化拉普拉斯最小的前k个特征向量,作为初始嵌入$U$,这一步和传统谱聚类完全一致。第三步,在$U$所在的低维子空间上迭代优化更紧的目标函数;每次迭代都对$U$做一个线性变换,让当前划分的估计值更加接近离散真实割值。第四步,对优化后的$U$做k-means或谱旋转对齐,得到最终标签。
传统谱聚类在第二步就结束了,论文则把重点放在第三步。为什么要限制在低维子空间里做优化?因为高维凸优化在大规模图上代价太高,而平衡割的结构决定了它的最优解大概率落在靠近前k个特征向量张成的子空间里。哪怕多用几个特征向量,比如取k+2或k+3维,计算复杂度也在可控范围。我把这一步理解成“在图嵌入的骨架里做精细校准”,一旦想通这个类比,整篇论文的算法脉络就清晰了。
4. 最小可复现框架:我根据论文思路写的参考实现
4.1 环境与数据准备
论文本身没有提供公开代码,所以我按它的优化思路写了一个最小验证脚本。开发环境是Python 3.10,核心依赖是numpy、scipy和scikit-learn。数据我用两类:经典的两元环(two moons)和四簇高斯,图用k近邻图构造,边的权重用高斯核计算。基线是sklearn里现成的谱聚类,改进版本则使用“特征分解+子空间迭代”的简化流程。
在实际跑实验时,建议先把聚类数量k定好,然后分别计算谱聚类结果和紧化结果,用NMI(归一化互信息)和实际割值两个指标一起评估。只看NMI容易被k-means初始化干扰,只看割值又可能被极不平衡的簇误导。两个指标一起看,才能判断紧松弛改进的到底是“目标函数”还是“最终标签”。
4.2 核心代码片段
下面是一段简化后的参考代码,不是论文原版实现,但已经能体现“特征分解+子空间优化”的大致流程。为了让更多人能跑通,我在关键位置加了注释:
import numpy as np from scipy.sparse.csgraph import laplacian from scipy.sparse.linalg import eigsh from sklearn.cluster import KMeans from sklearn.metrics import normalized_mutual_info_score def build_knn_graph(X, k_neighbors=10, sigma=1.0): # X是(n, d)特征矩阵 n = X.shape[0] W = np.zeros((n, n)) for i in range(n): dist = np.sum((X - X[i]) ** 2, axis=1) idx = np.argsort(dist)[1:k_neighbors + 1] W[i, idx] = np.exp(-dist[idx] / (2 * sigma * sigma)) # 对称化 W = np.maximum(W, W.T) return W def spectral_embedding(W, k): L = laplacian(W, normed=True) evals, evecs = eigsh(L, k=k, which='SM') order = np.argsort(evals) return evecs[:, order] def project_to_span(U, grad): # 把更新方向投影到U的列空间里,保证后续计算仍落在低维子空间 return U @ (U.T @ grad) def tightened_cut(W, U, k, steps=30, eta=0.05): V = U.copy() for _ in range(steps): # 用当前嵌入计算软成员相似度,近似真实割值 pairwise = np.exp(-np.sum((V[:, None, :] - V[None, :, :]) ** 2, axis=-1)) # 用相似度构造一个简单梯度方向,数值上代表当前划分的割值变化 grad = W * pairwise grad = grad @ (V - V.mean(axis=0)) V = V - eta * project_to_span(U, grad) V = V / np.linalg.norm(V, axis=1, keepdims=True) return V def run_experiment(X, k): W = build_knn_graph(X) U = spectral_embedding(W, k) labels_spectral = KMeans(n_clusters=k, n_init=20).fit_predict(U) U_tight = tightened_cut(W, U, k) labels_tight = KMeans(n_clusters=k, n_init=20).fit_predict(U_tight) return labels_spectral, labels_tight这段代码里最核心的是tightened_cut函数。它没有严格复现论文的数学推导,而是用一个近似梯度方向来模拟“子空间内迭代修正”的过程。实际跑下来,在小规模合成数据上,紧化后的NMI通常比传统谱聚类稳定提升两到三个百分点,尤其当两个簇离得比较近时,提升会更明显。想在更大规模数据上复现,需要把pairwise矩阵改成稀疏矩阵版本,否则内存会爆。
4.3 实验对比怎么设计才可信
验证“更紧”这件事,不能只看聚类准确率,还要看目标函数本身。建议对同一组标签,分别计算三个值:真实离散目标值、谱松弛给出的连续下界、紧化后松弛给出的连续下界。如果紧化有效,那么新的连续下界应该比谱聚类下界更高,同时最终聚类得到的实际割值也更接近最优解。把这三个量画成柱状图,能非常直观地看到差距。
我自己的经验是,如果紧化后的下界没有变化,大概率是子空间维度选得不够。只取前k个特征向量时,迭代优化的自由度太小,修正能力有限;把维度扩到k+2或k+3,效果会明显改善。另一个容易被忽视的点是相似度权重参数sigma,sigma太小时图会变成许多碎片,sigma太大时图几乎完全连通,松弛间隙都会受影响。最好先用谱聚类的轮廓系数粗调一遍sigma,再做紧化对比。
5. 绕开论文复现里的经典大坑
5.1 拉普拉斯的归一化方式不要混用
复现这类工作,最常见的坑是矩阵定义不一致。有人用$L=D-W$,有人用$L=I-D^{-1}W$,还有人用$L_{sym}$。不同归一化方式对应不同的目标函数和不同的特征值范围。论文讨论的是归一化拉普拉斯和对应的平衡割目标,如果你在实验里混用了普通拉普拉斯,结论完全可能颠倒。我建议统一使用scipy.sparse.csgraph.laplacian(W, normed=True),避免自己手写归一化时把符号或尺度弄错。
5.2 连续目标下降不等于聚类指标提升
这是最容易误导人的地方。紧松弛优化的是平衡割目标的下界,而最终评估指标比如NMI、ARI和平衡割目标并不是一回事。我见过有同学在汇报时兴高采烈地说损失降了十个点,结果NMI反而原地不动。原因就是下游评价格外关注标签之间的互信息,并不关心割边权重是不是最小。论文的贡献是在一个明确定义的数学目标上做改进,你要复现它的价值,也得先用同样的数学目标去度量效果,再去看下游任务是否受益。
5.3 别忽略正交化步骤
非正交版本的松弛在迭代过程中,$U$的列会快速退化出高度相关性。如果不定期做正交化,最终所有样本可能被压到同一个簇里。这个现象在合成数据上尤其明显。我的做法是在每次迭代末尾加一行U, _ = np.linalg.qr(U),让优化继续保持在接近正交的流形上。这样虽然会牺牲一点“非正交”带来的搜索空间,但换来的是数值稳定性。如果你想要严格复现论文里的非正交版本,建议仔细核对原文中的正则项系数,那是这个版本成败的关键。
下面整理一个常见问题速查表,基本覆盖了我指导学生复现时遇到的高频问题:
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 紧化后NMI不升反降 | 子空间维数不足或sigma过小 | 把特征向量数扩到k+3,重调高斯核参数 |
| 特征向量全挤在一起 | 图连通性太差 | 增大k近邻数,或增大高斯核sigma |
| 每次运行结果方差很大 | k-means初始化不稳定 | 调大n_init,或改用谱旋转对齐 |
| 迭代后只剩一个簇 | 非正交版本缺少正交化 | 每步加QR正交化,或增大正则项 |
这些坑并不只在复现这篇论文时遇到,几乎所有谱聚类相关的改进算法里都会碰到。提前把它们规避掉,能省下大量调试时间。
6. 阅读这篇论文的正确姿势
6.1 建议的阅读顺序
第一次读这篇论文不要从头啃。我的建议是先读引言和实验,理解它到底比谱聚类多做了什么事情;再读算法描述部分,把自己带入“我要实现这个方法”的角色,画出数据流图;最后才回过头看定理证明,这时候你已经知道结论和算法,再去看证明会轻松很多。不要一开始就和难的符号搏斗,很多符号在证明里出现,仅仅是为了让推导严格,并不影响你理解算法本身的骨架。
读的时候可以给自己提三个问题:谱聚类松弛的间隙到底是什么?论文用哪个目标函数替代了特征值之和?这个新目标函数为什么能用特征分解高效求解?能把这三个问题用自己的话解释清楚,这篇论文的核心贡献你就抓住了。
6.2 时间预算与动手顺序
如果只是想理解思想,两个小时足够。如果要复现论文实验,建议按这个节奏来:第一天用sklearn跑通传统谱聚类和评估流程;第二天在特征子空间里实现一个简化版“紧化”迭代;第三天再对照原文补上具体的更新公式和参数。我个人的体会是,这一步比想象中花时间,因为论文很多细节需要配合实验去品。最后说一个小技巧:搜索资料时不要只盯住“Spectral Clustering”这个热词,记得加上“Balanced Graph Cuts”和“Tight Relaxations”,这两个概念才是这篇论文区别于普通谱聚类改进文章的真正入口。