很多做聚类的人其实都遇到过这么一个问题:用 Spectral Clustering 跑了半天,结果在某个结构稍微不均衡的数据集上,效果还不如一个简单的 KMeans。这时候你大概率会怀疑是自己的图构造没弄好,或者 k 值选错了。但说句实话,问题可能根本不在参数上,而是在谱聚类这个方法的"松弛"上——它为了能求解,把原始离散优化问题放得太松了。NIPS 2011 上那篇《Beyond Spectral Clustering: Tight Relaxations of Balanced Graph Cuts》,讲的就是这件事:它分析清楚了谱聚类到底在哪个环节"松"了,然后提出了更紧的凸松弛。这篇文章我想以复现者的角度,聊聊这套方法背后的逻辑、我在实现中踩过的坑,以及它到底适合用在哪类场景。
1. 谱聚类本质上是个"放松了两次"的离散优化
1.1 平衡图割的数学目标
先回到问题本身。所有基于图割的聚类,核心目标只有一句话:把图切开的代价最小,同时让切出来的两块尽量平衡。这里的"代价"通常用割边权重来衡量,而"平衡"则用块的规模或块的度总和来度量。
最常见的目标函数有三个,虽然长得像,但行为差异很大:
| 目标函数 | 数学形式 | 平衡方式 |
|---|---|---|
| RatioCut | ( \frac{\mathrm{cut}(S,\bar{S})}{ | S |
| NormalizedCut | ( \frac{\mathrm{cut}(S,\bar{S})}{\mathrm{vol}(S)} + \frac{\mathrm{cut}(S,\bar{S})}{\mathrm{vol}(\bar{S})} ) | 用度总和保持平衡 |
| CheegerCut | ( \frac{\mathrm{cut}(S,\bar{S})}{\min(\mathrm{vol}(S), \mathrm{vol}(\bar{S}))} ) | 只惩罚更小的一侧 |
我自己的体会是,Cheeger Cut 在数学性质上最"凶狠",它只关注小块的相对割比,所以它对"从一个大簇里切出一小块离群点"这种事特别敏感。而 NormalizedCut 是两端各打五十大板,反而容易出现某一边莫名被削小的解。
但关键是:所有这些都是离散组合优化,S 是顶点集的一个子集,问题是 NP 难的。你不可能直接枚举。
1.2 特征分解把约束放成了什么样
谱聚类的做法大家应该很熟:先构造图的拉普拉斯矩阵,取前 k 个特征向量,做归一化之后再 k-means。它的每一步推导似乎都很合理,但实际上做了一次非常猛的放松。
原始问题里,每个节点 i 对一个簇的隶属度 (x_i) 是二值的,要么 0,要么 1。到了谱聚类这边的连续松弛里,(x_i) 变成了任意实数;原来那个"每个点必须且只能归一个簇"的约束,被放宽成了 (X^T D X = I) 这种正交归一约束。也就是说,解出来的"簇指示向量"可以是任意实值向量,节点之间甚至能出现中间状态的"半归属"。
这还不是最狠的。更关键的是,谱聚类用的最小化方式是二次型 (x^T L x),这在数学上就等效于在做某种"全局平滑性"的惩罚。它是 L2 的,是平方的,这意味着只要你把某个节点的值从 0 改到 0.3,惩罚会按平方量级变化。对二值 0/1 解来说,这种惩罚方式根本不能准确反映"砍掉一条边的实际代价"。
1.3 松弛太松会怎样:失衡簇和高密度背景的问题
我最初测试谱聚类失败的那个场景非常典型:一个图形数据里,大的簇有 2000 个点,小的簇只有 80 个点,两个簇之间有少量连接。用 Standard Normalized Cut 的谱聚类去跑,出来的决策边界永远是把大簇分成两块,而不是把那个小簇单独切出来。
为什么?因为连续松弛下的解空间比离散解大太多了,它总能找到一个"分数解",让目标函数值比真实整数解更小。比如把大簇里的一条"裂缝"上的节点值全部设为 0.5,就能在几乎不损失什么的情况下让 Rayleigh 商降低。最后你再去 k-means,也只能在这个平滑的分数解上做二值化,失真必然存在。
所以谱聚类的问题不在于特征分解这个工具本身,而在于它的目标函数与原始离散割目标之间的 gap 太大了。这也是 NIPS 2011 那篇工作想要解决的核心问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 这篇工作是怎么把松弛"收紧"的
2.1 用总变差替代平方惩罚
要理解这篇工作的思路,得先接受一个观点:谱聚类用在 Rayleigh 商里的 (x^T L x = \sum w_{ij}(x_i - x_j)^2),它惩罚的是边的两端值差的平方;但真正衡量图割代价的,是边的两端值"不同"这件事本身,也就是 ((x_i - x_j)) 的绝对值,而不是平方。
于是作者把目光转向了总变差(Total Variation):( \mathrm{TV}(x) = \frac{1}{2}\sum w_{ij}|x_i - x_j| )。
这个量有个非常好的性质:当 (x) 是 0/1 指示向量时,TV 严格等于割边权重之和。也就是说,它是离散割目标在连续空间上的一个"忠实延拓",而不是像 L2 那样扭曲过的版本。
我换句话来解释这种差异:L2 惩罚会把"半分明"的解罚得很舒服,因为数值小的差值平方后更小;L1 惩罚则是线性的,你对那些介于 0 和 1 之间的模糊节点,惩罚不会含糊地缩小。这就让最优解更倾向于走向两端,也就是 0 和 1,从而更接近真实的离散分割。
2.2 松弛后的优化问题形式
引入总变差之后,Cheeger Cut 的松弛可以写成下面这种形式:
[
\min_{x\in[0,1]^n}\ \frac{\mathrm{TV}(x)}{\min(d^T x,\ d^T(1-x))}
]
它的分母是在求软指标下两个簇的体积总和,取两者的较小值,这就保住了"平衡"的约束。不过这个带 min 的分母会让问题变成非光滑凸优化,直接求梯度是走不通的,所以作者们把它做了一个等价变换,引入一个辅助变量 (t),变成:
[
\min_{x\in[0,1]^n, t}\ t
]
[
\text{s.t.}\quad \mathrm{TV}(x) \le t\cdot d^T x
]
[
\mathrm{TV}(x) \le t\cdot d^T(1-x)
]
这一下就把问题变形成了一个凸可行性问题:在两条"砍断代价必须小于 t 乘以某边体积"的约束下,不断压低 t。这里特别需要注意的是,这两个约束同时成立,要求的不仅是割本身小,还要求不管是把哪一侧当作被保护的小块,割对它来说都不能太大。这比 NormalizedCut 那种"两边一起平均"的方式严格得多。
2.3 "紧"的理论含义
很多人第一次听到 "Tight Relaxations" 这个词,会以为它的意思就是松弛后的解永远全是整数解,可以无误差回到离散问题。实际上不是这么回事。
"紧"是相对于谱聚类而言的。在谱聚类那里,松弛解空间非常大,和离散问题最优值之间的 gap 没有保证,随便一个图形数据都可能翻车。而这篇文章提出的凸松弛,在一定的结构条件下(比如图本身有清晰的簇结构时),它的最优值可以逼近甚至等于原始离散 Cheeger Cut 的最优值,至少在理论上给出了一个更紧的界。
"松紧"这个词,我倾向于用这样一句话来记:"松弛是把一个离散山脊变成连续山地,松的松弛把山峰也抹平了,紧的松弛只把山谷填上,但山峰还在。" 这句话虽然不严谨,但能帮你直观理解为什么紧松弛得到的解更容易通过阈值化或 k-means 恢复成好的离散割。
3. 求解这组凸问题:我复现时用的原始-对偶方法
3.1 对偶化与 min-max 重写
直接拿这个带有线性约束和非光滑目标的问题去调库,你会发现大部分通用凸优化求解器在图上跑得特别慢。原因在于 (n) 个顶点、(m) 条边的规模,会让二阶方法里的 Hessian 或线性方程组变得格外庞大。实际操作中我用的是一阶原始-对偶方法,也就是常说的 PDHG。
先把问题写成一个对玩家更友好的格式。总变差有个经典的对偶表示:
[
\mathrm{TV}(x)=\max_{|p|_\infty \le 1}\ \langle \nabla^T p, x\rangle
]
这里的 (p) 是边上的对偶变量,取值范围被限制在 ([-1,1]),物理含义是每条边上的"流"大小受到限制;(\nabla) 是图上的散度算子。这样一来,原来的 min 问题就变成了一个 min-max 问题:外层最小化 (x),内层最大化 (p)。这也是观察谱聚类问题一个很好的视角:谱聚类是在特征空间里做正交化,而这个方法是在"边流空间"里做有界的对偶博弈。
3.2 迭代更新的可运行伪代码
在我实际复现时,整个求解循环非常短。下面是一段 Python 伪代码风格的迭代核心,基本可以直接翻译成 NumPy 操作:
python复制def solve_cheeger_relaxation(L, d, alpha=0.5, n_iter=1000, tau=0.02, sigma=0.02):
n = L.shape[0]
x = np.full(n, alpha) # 初始化为均匀软分配
x_bar = x.copy() # 外推变量
p = np.zeros(L.shape[0]) # 边上的对偶变量
# L 是图的边-点关联矩阵(m x n),不是拉普拉斯
for _ in range(n_iter):
p_new = p + sigma * (L @ (2 * x_bar - x))
p_new = np.clip(p_new, -1.0, 1.0) # 投影到无穷范数球
x_new = x - tau * (L.T @ p_new)
x_new = np.clip(x_new, 0.0, 1.0) # 投影到 [0,1] 盒
# 这里可以再加一个体积约束投影,让 d^T x 保持目标比例
# x_new = project_volem(x_new, d, alpha * d.sum())
x_bar = 2 * x_new - x # 外推加速
p, x = p_new, x_new
return x
这段代码里最关键的一行是那个体积约束投影。我之前漏掉它的时候,结果会退化成分割比例完全失衡的极端解,比如 (x) 全部变成 0 或 1,目标函数却数值很小,完全失去平衡割的意义。加上约束以后,问题的行为就正常多了。
3.3 收敛检查与超参数经验
用一阶方法求解非光滑凸问题,最怕的就是不知道怎么判断收敛。我的经验是可以同时跟踪两个量:第一个是原始残差和对偶残差,第二是目标值 (\mathrm{TV}(x) / \min(d^T x, d^T(1-x)))。
当你看到目标值在几百次迭代后开始震荡而不是单调下降,不要慌。非光滑问题在最优值附近的震荡是正常的,关键是看震荡中心有没有稳定下来。这时候我会把步长 (tau) 和 (sigma) 同时缩小一半,再跑一遍,确认震荡幅度跟着缩小,那就说明没有实现错误,只是数值上的正常波动。
还有一点:图连接方式对收敛速度的影响比想象中大得多。用 kNN 图加高斯核权重,收敛通常很快;如果用全连接图加高斯核,边的数量是 (O(n^2)),内存直接爆炸不说,收敛也极其缓慢。我后来都把 weight 矩阵稀疏化到每行保留 10 到 20 个邻居,效果和全连接基本一致,速度快了不止一个数量级。
4. 复现实验:谱聚类先翻车、紧松弛后翻车的几个场景
4.1 人工构造的二分类失衡数据
我最先做的是一个有"预设答案"的合成实验。生成两个高斯簇,一个半径 1.0,样本 2000 个;另一个半径 0.5,样本 80 个。两个簇中心距离 3.5,中间用几条低权重的边连接。
这种数据谱聚类输得很干脆:Normalized Cut 谱聚类最后选出来的割线几乎把大簇切成了两半,小簇反而并到了大簇的一部分里。原因就是我在前面讲的,连续松弛找了一个更低的分数解,那个解里的"大簇裂缝"在 Rayleigh 商意义上很便宜。
而用上面那段 PDHG 求解的 Cheeger 紧松弛,跑出来的 ({x_i \ge 0.5}) 区域就是那个小簇所在的位置,几乎不用再做后续 k-means 就能直接得到正确答案。这是让我对这套方法产生信心的第一个实验。
4.2 与 Normalized Cut 谱聚类的对比结果
我整理了一次对比实验的典型结果,好让大家直观感受两者差异:
| 实验场景 | Normalized Cut 谱聚类 | Cheeger Cut 紧松弛 |
|---|---|---|
| 两个规模相近的簇 | 很好,与紧松弛相当 | 很好 |
| 失衡比 25:1 的两个簇 | 大簇被切裂,小簇被吞并 | 小簇被单独识别 |
| 三簇中有一簇细长条 | 两个小簇常被合并 | 能分出细长簇 |
| 有噪声背景点的双簇 | 背景点形成虚假簇 | 噪声被归入体积更大的一侧 |
注意第四行那个现象很有意思。当大量背景噪声点存在时,Cheeger 紧松弛倾向于把噪声归入大簇,而不是单独形成一个簇。因为单独形成一个噪声簇会让噪声簇的体积很小,而它的割比会变得巨大,平衡约束不允许这种事发生。这个性质在实际使用中其实非常有用——它天然避免了谱聚类那种"噪声点少但结构松散,被特征分解误当成一个簇"的问题。
4.3 真实数据上要付出的计算代价
在 MNIST 的一个子集上(10 类,每类 200 张图),我用 784 维像素特征构造 kNN 图做了一次测试:谱聚类从建图到出结果,总共大概 3 秒;而紧松弛方法需要迭代约 3000 次,才能让目标值稳定,总共花了将近 40 秒。这差距主要来自每次迭代都要在图的边上做一次矩阵乘法,而且没法像特征分解那样利用现有 BLAS 优化打得特别深。
如果你还要做多分类而不是两分,情况会进一步变复杂。虽然理论上可以把指示向量扩展成矩阵 (X \in [0,1]^{n \times k}),每一列做一个总变差项,再对列方向加平衡约束,但这样每次迭代的计算量会乘以 k。在我测试 k=10 的情况下,运行时间直接冲到了好几分钟,和谱聚类完全不在一个量级。所以做多分类时更合理的做法是递归二分,而不是一次性求解多类问题。
5. 这套方法的使用边界和选型建议
5.1 最该替换谱聚类的场景
我在实践里总结出三个最该认真考虑换成紧松弛的场景:
第一,簇的大小严重失衡。如果你怀疑数据里有一个占比只有 5% 的细小簇,而又特别在意它能不能被识别出来,那么谱聚类很可能让你失望,紧松弛的平衡约束会对这种"小体积高隔离"的结构特别敏感。
第二,存在大量背景噪声点。前面实验里已经看到,背景噪声在谱聚类的特征空间里会产生假簇,但在 Cheeger 紧松弛里容易被大簇"吸收",形成更干净的分割结果。
第三,你愿意为质量多等几秒甚至几分钟。离线场景(比如数据分析、科研实验)完全可以把时间预算从毫秒级放宽到秒级;但如果是线上实时预测,就要慎重了。
5.2 不建议硬上的场景
反过来,有三类场景我不建议直接硬上。
一是顶点规模在百万级以上的大图。稀疏特征分解在前端处理海量图数据时依然有不可替代的优势,一阶迭代每轮扫一遍边集,在很多图上会慢到让人失去耐心。
二是你其实只需要一个粗糙的初始聚类。如果把聚类结果当作下游任务的输入特征或数据筛选用,谱聚类快、稳定、足够好,不需要用重炮打蚊子。
三是当你的最终目标不是图割质量,而是要让特征向量有全局几何意义的时候。谱聚类得到的低频特征向量本身就可以做谱嵌入,紧松弛方法得到的是最优化割的结果,没有免费给你一套好用的低维表示。
5.3 从谱聚类迁移过去的最短路径
如果你以前只写过谱聚类的代码,想尝试这套紧松弛方法,我最推荐的迁移路径是:先不要动多分类,也不要一次性写十几个函数。先在两分类问题里,把谱聚类的代码替换成上面那段 PDHG 迭代,然后对比同一个 kNN 图上的两种结果。
验证方式很简单:跑谱聚类,把 Fiedler 向量画到一维数轴上,看它是否有清晰的阈值间隙。如果 Fiedler 向量在 0 附近有大量半吊子的值,说明松弛太宽松,你大概率能从紧松弛里获得收益;如果 Fiedler 向量分布本身就是两堆清清楚楚的点,那谱聚类已经够好了,换方法可能只是浪费时间。这套诊断方法我用了很多次,它比看 ACC 或 NMI 这类指标更能揭示问题本质。
从我个人的经验来看,这篇 NIPS 2011 工作最大的价值不是提供了一个完全替代谱聚类的工程方案,而是提供了一种审视方法的视角:当你抱怨某种算法在某类数据上表现差的时候,先别急着调参,回头看看它的松弛到底在哪个地方"放水"了。很多时候换一种更贴身的凸松弛,比调三天超参数都管用。
