亲和力传播算法(AP聚类)详解:原理、调参与实战
2026/9/16 6:11:55 网站建设 项目流程

1. 为什么聚类时你应该认识 AP

第一次被 K-Means 逼疯,是在做客户分群项目的时候。业务方甩过来十几个维度的行为数据,我问他们想分几类,他们说“你觉得呢”;我去画轮廓系数,曲线拐点模糊得没法看。后来翻到 Affinity Propagation(AP,亲和力传播算法),才知道有个聚类方法压根不需要预设簇数,它让数据点之间自己谈判、互相推举代表,最后自然长出几个簇来。注意啊,这里的 AP 不是路由器上那个无线接入点,而是亲和力传播算法的缩写,2007 年发表在 Science 上。虽然标题写的是“一分钟理解”,但你要真想把它用好,建议还是花二十分钟搞懂它背后的消息传播机制。这篇文章我按自己的理解,从原理到代码再到调参,把 AP 完整拆一遍。

1.1 K-Means 的先天短板

说 AP 之前,先看看 K-Means 的三个老毛病。第一是 K 值难以确定。你在做聚类时,业务方通常只能告诉你“看着分吧”,数据分析师只好画肘部图、算轮廓系数,可真实数据往往是一堆乱七八糟的团,SSE 曲线从头到尾都在下降,根本找不到明显拐点,K 选 3 还是选 7 全看直觉。第二是对初始点敏感。同样的 K,random_state 更换一下,聚类结果就完全不同;有些初始中心一旦选偏,就会掉进局部最优,几个簇缠在一起分不开。第三是中心点本身不可解释。K-Means 输出的簇中心是各维度的均值向量,它可能不落在任何真实样本上,你很难对业务方解释“这个虚拟用户到底是谁”。这三个问题都根源于 K-Means 的设定:你得先告诉它分几类,它才动手。

1.2 AP 凭什么不需要指定 K

AP 的思路是把“分几类”的问题转成“哪个点能代表哪些点”的问题。它先把每个样本都当成一个潜在聚类中心,论文里叫 exemplar,然后让点与点之间互相打分、传递消息。当一个点积累到足够的支持率,它就成为代表点;其他点自动归顺到最适合自己的代表点那里。整个过程不要求你输入 K,簇数是竞争之后自然形成的。你可以通过一个叫 preference 的偏好值来调节竞争氛围,比如设置相似度矩阵的中位数,但偏好值不等于簇数,它只是“每个点有多想当代表”的一种期望。跟 K-Means 相比,AP 还有一个隐性优势:它接收的相似度矩阵可以是任意成对打分,不限于欧氏距离。比如你算文本之间的余弦相似度、用户之间的共同点击量、甚至编辑距离,都能直接丢进去跑,这一点在项目里真的非常实用。

2. 亲和力传播的核心:点与点之间的“消息”

2.1 输入:相似度矩阵

AP 的全部家当就是一张 N×N 的相似度矩阵 S。S[i][k] 表示点 i 认为点 k 适合做自己的代表的程度,越相似值越大。注意,S 不要求对称,因为 i 对 k 的评价和 k 对 i 的评价在业务语境下本来就可以不同;也不要求满足三角不等式,它甚至不需要是一个严格的距离度量,只要你的相似度计算是有意义的,AP 都接得住。常规做法是取欧氏距离的负平方,比如 distance = ||x_i - x_k||,similarity = -distance²,这样距离越近相似度越高。也可以直接用余弦相似度或高斯核,效果差别不大,关键是量纲统一。矩阵对角线 S[i][i] 就是该点当中心的自评分,也叫 preference p(i)。p(i) 大,意味着这个点乐于站出来当代表;p(i) 小,它更愿意附着在别人旁边。整张矩阵构造完成之后,AP 的输入准备就结束了,剩下的都是消息传播的事。

2.2 两种消息:responsibility 和 availability

算法里有两种消息,英文缩写 R 和 A,翻译成责任度和可用度。先看责任度 r(i,k),方向从 i 到 k,一句话解释:在 i 眼里,k 比所有其他候选中心好多少。计算公式是:

r(i,k) = S[i][k] - max_{k' != k} ( A[i][k'] + S[i][k'] )

这里的 A[i][k'] 是上一轮得到的可用度。为什么要加上它再求最大值?因为 i 在判断时不仅要看相似度,还要看候选中心 k' 当前能不能获得其他人的支持。一个相似度高但没人支持的点,并不一定适合做代表。r(i,k) 越接近正数,说明 k 确实值得被 i 选。再看可用度 a(i,k),方向从 k 到 i,一句话解释:k 在收听到场外反馈之后,有多少底气站出来代表 i。公式分两种情况:

if i != k: a(i,k) = min{ 0, r(k,k) + sum_{i' not in {i,k}} max(0, r(i',k)) } if i == k: a(i,k) = sum_{i' != k} max(0, r(i',k))

第一个公式里的 min{0, ...} 是在限制可用度不要变成正数,它想让点 i 只在一个方向上被代表,防止两个点互相追捧;第二个公式是代表点对自身的可用度,它只收集别人对它的正向责任度。第一次看这两条公式确实容易晕,但抓住本质就清楚了:r 负责竞争,a 负责背书,两者交替更新,最后谁能获得足够的“责任+可用”,谁就是代表点。

2.3 用类比理解 r/a 更新

为了不让你被公式劝退,用职场例子类比一遍。假设部门要选几个项目组长,每个成员都可以投出自己的候选名单。responsibility 就是成员小 i 对候选 k 的表态:“我看了一圈,k 的综合条件比其他人好,我投 k;要是 k 不如别人,那我就不投。” availability 是候选 k 收到大家的反馈后回的消息:“既然有这么多同事支持我,我有信心站出来带领你们;要是支持我的人寥寥无几,那我也不强出头,更不会硬拽着某个人跟我干。”这两个表态你来我往,经过十几轮迭代,有人发现自己确实众望所归,就当了组长;有人发现自己的责任度全是负的,就自动退下,安心找个组长归队。数据里的“组长”就是 exemplar,簇就是组长管辖的小组。这也是 AP 和 K-Means 最大的气质差别:它更像一场自组织的选举,而不是一次中心拉网。

3. 迭代、阻尼与收敛:让传播真正稳定下来

3.1 消息更新循环和 exemplar 决策

AP 的完整流程其实非常像博弈,每轮更新一次 R、一次 A,再加一次阻尼。具体步骤如下:

初始化 A = 0 循环: 1. 用当前 A 和 S 更新 R 2. 用最新 R 更新 A 3. 对每个点 i,计算 c(i) = argmax_k ( R[i][k] + A[i][k] ) 4. 如果 c(i) == i,i 成为 exemplar 5. 若连续若干次 exemplar 集合不变,停止;否则回到 1

第 3 步的 R[i][k]+A[i][k] 可以理解成“k 对 i 的最终吸引力”。当一个点 i 计算完所有 k 的分数,最高分对应的 k 就是它的归属中心;如果这个 k 恰好是 i 自己,说明 i 当选为代表,它单独成立一个簇。这里有个容易忽略的顺序问题:为什么 R 要先于 A 更新?因为 A 的计算依赖最新的 R,如果顺序反了,就变成上一轮的信息在带下一轮的节奏,算法会散架。你不用手写这个循环,但理解了顺序,你至少能看懂 sklearn 的 n_iter_ 到底在迭代什么。

3.2 阻尼系数为什么重要

消息传播和很多迭代算法一样,容易在高维空间里振荡。具体表现是:这轮大家认 k 当代表,下一轮一部分人叛变到 k',再下一轮又有人叛变回来,代表集合像钟摆一样来回换,迟迟不收敛。数学上是因为更新公式里的 max 会放大或截断数值,消息矩阵更新是非线性的,没有一个天然的能量函数保证单调下降。论文给的做法是加阻尼系数 damping,记作 λ,取值通常在 0.5 到 1 之间。每次算出的新消息不能全信,要和上一轮消息做加权混合:

R_new = λ * R_old + (1 - λ) * R_calculated A_new = λ * A_old + (1 - λ) * A_calculated

λ 越大,新旧消息越接近,系统越平滑,不容易振荡,但收敛速度也越慢;λ 越小,消息更新越快,但容易跳来跳去。默认 0.5 是一个比较激进的起点,我在实际项目中很少直接用默认值,一般先切成 0.8 到 0.9。如果你的数据噪声大、点之间相似度没有明显断层,则非常容易出现“振荡——提高阻尼——再振荡”的循环。

3.3 收敛判断与振荡处理

如何判断 AP 真正收敛?最严格的做法是比较整个 R 和 A 矩阵的变化量,但 O(N²) 的矩阵比较在大数据上太贵,工程实现一般采取“代表集合是否稳定”来替代。比如 scikit-learn 的 AffinityPropagation 会检查连续 15 轮(convergence_iter 参数)代表点集合是否完全一样,如果一样就认为聚类结果已稳定,提前收工;如果始终没达到,就会撞上 max_iter 强行结束,并抛出一条 did not converge 警告。遇到振荡时,我的处理顺序是:先把 damping 提到 0.9,再把 max_iter 从默认 200 调到 500 甚至 1000,观察输出的 n_iter_。如果 n_iter_ 已经接近 max_iter 但还是没收敛,就不要再硬加了,大概率是相似度构造或者 preference 设置的问题;换一种相似度定义往往比调参更有效。

4. 实战:用 Python 跑通一个 AP 聚类

4.1 最小可运行示例(scikit-learn)

最舒服的跑通方式是用 scikit-learn。以下代码生成 300 个二维样本,分散在 4 个高斯团附近,然后直接用 AffinityPropagation 聚类:

import numpy as np from sklearn.datasets import make_blobs from sklearn.cluster import AffinityPropagation X, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.7, random_state=42) ap = AffinityPropagation(damping=0.9, max_iter=500) y_pred = ap.fit_predict(X) print('聚类簇数:', len(set(y_pred))) print('真实簇数:', len(set(y_true))) print('每个簇的样本数:', np.bincount(y_pred)) print('代表点索引:', ap.cluster_centers_indices_)

跑出来大概率是 4 簇附近,偶尔会有 5 簇,取决于相似度构造的默认偏好。我们刻意把真实簇数埋进数据里,但算法并没有偷看。这里的 damping 我设成了 0.9 而不是默认的 0.5,是想让第一次运行就稳定一点;max_iter 也改成了 500,避免在同样的随机种子下出现振荡警告。scikit-learn 的 AffinityPropagation 默认参数 preference=None 时,会用相似度矩阵的中位数作为所有点的自评分,这是一个非常安全的起点。用这个最小案例跑通之后,你就能理解后面调 preference 时结果为什么变化那么明显了。

4.2 手写伪代码看内循环

如果你还是想亲眼看一遍内循环,下面这份伪代码比数学公式更贴近实现:

初始化 A = zeros((n, n)) R = zeros((n, n)) for iteration in range(max_iter): # 更新 R for i in range(n): for k in range(n): others = max(A[i][k'] + S[i][k'] for k' in range(n) if k' != k) R[i][k] = S[i][k] - others # 更新 A for k in range(n): for i in range(n): if i != k: positive_sum = sum(max(0, R[j][k]) for j in range(n) if j != i and j != k) A[i][k] = min(0, R[k][k] + positive_sum) else: A[k][k] = sum(max(0, R[j][k]) for j in range(n) if j != k) # 阻尼 R = lam * R_old + (1 - lam) * R A = lam * A_old + (1 - lam) * A # 决策 for i in range(n): c[i] = argmax_k (R[i][k] + A[i][k])

这段伪码没有做向量化,也不带收敛判断,但骨架是对的。你如果自己实现,要特别注意 max 的目标集合不能包含 k 本身,否则每个点都会选中自己做代表,算法立刻报废;还要注意加阻尼的镜像:R_old 和 A_old 要用更新前的矩阵,不能边算边覆盖。我在第一次手写时就栽在就地更新上,结果消息传播完全乱套,最后干脆老老实实复制了一份矩阵再计算,问题立刻解决。

4.3 输出怎么解读

跑完 AP,最直观的输出是 cluster_centers_indices_:它是一个整数数组,存的是代表点在原始样本中的行号。比如代表点索引是 57,说明第 57 行样本就是这个簇的典型代表,它的特征就是该簇最核心、最能代表群体的画像。K-Means 输出的是虚拟均值中心,你还需要额外解释一番;AP 直接给你一个真实个体,这在业务汇报里有天然优势。第二个输出是 labels_,每个点属于哪个簇,和 y_pred 是一样的。还有一个 n_iter_,表示实际迭代次数,我一般把它当健康度指标:如果 n_iter_ 接近 max_iter,说明参数给得太紧,收敛吃力;如果 n_iter_ 只有几十,说明消息传播很顺利。实际使用时,可以把代表点的原始记录整理成表格发给业务方,他们第一时间能看懂:“原来这一组用户的核心代表是这个人”,沟通成本一下子降下来了。

5. preference 和 damping 的调参心得

5.1 preference 如何决定簇数

preference 是 AP 里最值得玩的一个旋钮,它对簇数的控制比 damping 直接得多。回到相似度矩阵,对角线 S[i][i] 就是 preference p(i)。如果 p(i) 大,这个点会对“自己当选代表”有很高的期待,于是更多人想独立成簇,簇数自然变多;如果 p(i) 特别小,几乎没有点愿意当代表,大家只好被迫归顺到少数几个中心,簇数就变少。实践中,常见策略是取相似度矩阵的中位数作为统一 p,这时你能得到一组相对平衡的簇;取最小值会让簇变粗,适合你想看宏观结构;取 0 或正数会有大量单点簇,适合做异常检测,而不是常规聚类。我曾经在新闻数据上对比过,p 取中位数时得到 12 个主题簇,p 放大到 10 倍中位数时簇数变成 50 多个,p 取最小值时只剩 3 个。想找一个合适的簇数,不必一个个试,可以在中位数和最小值之间做二分搜索。

5.2 damping 调高与迭代次数

damping 在实践中比理论听上去更重要。默认 0.5 虽然收敛快,但消息矩阵更新太猛,在相似度结构不清晰的数据上,代表点经常在两三个候选之间反复横跳。我通常会先设成 0.9,跑一次看 n_iter_;如果已经收敛且结果稳定,就沿用;如果还在振荡,慢慢往上加 0.95。但有个搭配问题:damping 提高到 0.9 之后,消息更新的有效步长变短,达到同样稳定状态需要更多迭代,如果你不把 max_iter 加大会出现“还没稳定就被喊停”的尴尬,然后抛出一条收敛警告。我的经验是 damping=0.9 时配套 max_iter 至少 500,数据点超过 3000 就提到 1000。当然,如果提高阻尼后仍然疯狂提示不收敛,别死磕参数,回去检查相似度矩阵是不是太满、噪声太大,或者数据本身并不适合 AP。

5.3 实际项目中容易犯错的地方

再说三个在项目里最容易踩的坑。第一个是标准化问题。相似度矩阵对特征的量纲极其敏感,如果 X 第一列取值范围是 0 到 10000,第二列是 0 到 1,欧氏距离会被第一列完全主导,AP 的簇结构几乎只反映第一列。必须先对特征做标准化,比如 StandardScaler 或 MinMaxScaler,再算相似度。第二个是相似度方向搞反。AP 要求相似度越大代表越相似;如果你习惯用 sklearn 的 pairwise_distances 得到距离矩阵,直接丢进去就会得到反效果,相近的点被判定为不相似。正确的做法是 similarity = -distance,或者用 rbf 核函数。第三个是内存爆炸。N 个样本会构造出一个 N×N 矩阵,5000 个样本的 float64 矩阵大约 200MB,10000 个就是 800MB,还没开始迭代内存就先挂了。这时要么用 Mini-Batch 思路先降采样,要么用稀疏相似度矩阵,只在 K 近邻边上有值。很多人忽略稀疏输入,其实 scikit-learn 的 AffinityPropagation 是支持 CSR 矩阵的,能救回不少内存。

6. AP 的适用边界:什么时候值得用它

6.1 适合 AP 的场景

结合我个人的项目经历,AP 在四类场景里特别值得用。第一是你对簇数完全没概念,K-Means 的肘部图又画不出明确拐点时,AP 给出的簇数可以作为后续分析的参考。第二是你需要每个簇有一个真实样本当代表,比如要做新品企划,从几百个 SKU 里挑典型款,exemplar 就是那款最典型的商品;或者做用户分群,业务方问“这一群人的代表到底是谁”,你直接把代表点的标签甩出来。第三是数据量不大但相似度定义很复杂,可能来自业务规则、编辑距离、图上的最短路径,这些非欧相似度 K-Means 根本没法直接算,AP 却很自在。第四是你在做探索性数据分析,想快速看一看一份新数据大概能分成几个结构,AP 能在没有先验的情况下给你一个可解释的初始结果。

6.2 不适合 AP 的场景

AP 最现实的短板是计算复杂度。每一轮迭代都要遍历 N×N 矩阵,原作者报告的时间复杂度在 O(N² log N) 级别,一万个点跑一次在普通笔记本上能明显感觉到延迟,几万个点基本劝退。这和 K-Means 的一次迭代 O(NK) 完全不是一个量级。第二个不适合的场景是噪声很多的数据,AP 对每个点都一视同仁,离群点如果没有足够强的归属对象,就会自立门户变成单点簇,簇数里掺进一堆噪声。第三个是稀疏高维特征,比如几十万维的 TF-IDF,相似度被长尾维度稀释,消息传播很难找到强的中心,最好先做降维;降到 100 维以内再用 AP,效果会好得多。还有在线场景,AP 一旦样本变化就得重跑全量,基本没办法做增量学习,实时性是硬伤。所以做实时推荐、实时风控时,我会优先用 Mini-Batch K-Means 或者流式聚类,让 AP 在离线分析阶段提供初始簇数和代表点。

6.3 和 K-Means / DBSCAN / 谱聚类怎么搭配选择

如果实在不知道怎么选,可以套用我常用的判断逻辑:数据量上万、簇近似凸,选 K-Means 或 Mini-Batch K-Means;簇形状任意、有大量噪声,选 DBSCAN;数据有明确图结构、簇结构在拓扑上明显,选谱聚类;不知道怎么定簇数、又希望代表点是真实样本,选 AP。下面这张表可以快速对照:

算法是否需要指定簇数中心的含义数据规模友好度簇形状容忍度
K-Means需要均值向量凸簇为主
DBSCAN不需要密度可达区域任意形状
谱聚类需要特征向量空间任意形状
AP不需要真实样本小到中对距离敏感

表只是辅助,实际项目里我经常组合用:先用 AP 在小样本或采样数据上跑出簇数和 exemplar,再用这些 exemplar 作为 K-Means 的初始中心,对更大的数据集继续聚类。这样做既拿到了可解释的代表点,又弥补了 AP 在大数据上的性能短板。我在推荐系统冷启动里就用过这个组合,从几十万商品中先抽一万,跑 AP 得到几百个典型商品,再把它作为初始中心扩展到全量,业务方看到典型商品后顿悟般地一拍大腿:“原来用户要的是这些”,那次的沟通效率比直接甩聚类报告高太多了。

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

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

立即咨询