☰
CFSFDP密度峰值聚类算法解析:从决策图到参数调优实战
2026/10/13 17:34:57 网站建设 项目流程

简介:这份资源提供CFSFDP算法的Python参考实现,面向需要处理模糊数据聚类与特征选择的研究人员、数据挖掘学习者。算法结合相关性分析与模糊数据分区(FDP),可在预处理阶段筛选高价值特征,适用于包含离散、连续或模糊属性的复杂数据集。压缩包内含7个文件,包括Python源码、示例数据CSV、说明文档(markdown)及许可证文件,约12KB,代码量精简适合作为学习起点。目前已有1125人学习下载。通过阅读CFSFDP.py可理解相关系数计算、贡献度评估与特征剔减的完整流程;fuzzy_clustering.py演示模糊隶属函数的应用;附带dataset.csv与ResultCenter.csv便于直接运行测试并对比聚类结果。整体结构清晰,适合初学者快速上手,也可为进阶研究者提供改进基线。

1. 聚类算法 CFSFDP:为什么它比 K-Means 更适合非球形簇

如果你处理过用户行为分群、图像分割或单细胞数据降维后的聚类,大概率遇到过这种尴尬:K-Means 对球形分布很听话,一碰到弯月、环状、密度不均的簇就全线崩溃。CFSFDP(Clustering by Fast Search and Find of Density Peaks,密度峰值聚类)正是为解决这类非球形、任意形状簇设计的一类聚类算法,它不需要预先指定簇数 K,而是通过局部密度 ρ 和与更高密度点的最小距离 δ 两张画像找出聚类中心,再让剩余点向最近的高密度点归队。2014 年论文发表后大量参考代码流传出来,这篇文章要拆的就是一套适合学习与复现的 CFSFDP 参考代码包,从公式、决策图到调参、踩坑一次性讲透,适合正在做聚类对比实验的开发者,以及读论文读得懂、动手写代码却总差一步的研究生。密度的定义、截断距离 dc 的标定、决策图的判读逻辑,这三个点决定你能否复现出论文里那种干净漂亮的聚类结果。

2. 密度峰值聚类的核心公式:ρ、δ 和决策图的判读逻辑

2.1 密度计算与截断距离 dc 的两种核函数

CFSFDP 的第一步,是对每个数据点计算局部密度 ρ。很多人误以为它跟 DBSCAN 的邻域计数一样,其实 CFSFDP 提供了两种核函数,默认参考代码里通常两种都写了,用哪个由参数控制。第一种是 Cutoff 核,也叫硬阈值核:

\u03C1_i = sum_j ( d_ij < dc ? 1 : 0 )

含义是统计所有距离小于截断距离 dc 的邻居个数。这种核的优势是计算快,对 dc 不敏感;缺点是离散性大,同一个 dc 附近容易出现两个距离很近但 ρ 相同的情况。第二种是高斯核:

\u03C1_i = sum_j exp( -( d_ij / dc )^2 )

高斯核把所有点的距离都纳入计算,只是距离越远权重衰减越快,ρ 变成连续值,决策图画出来更平滑,边界点区分度更高。复现论文图时我一般用高斯核,因为生成的三维散点图颜色过渡更自然;做大规模工程实验时用 Cutoff 核,因为只需要比较 d_ij 和 dc 的大小,不需要算指数函数。dc 的选择直接决定 ρ 的分布形态,常见做法是选所有点对距离升序排列后的 1% 到 2% 分位数,这个经验值的来源在 4.1 里细讲。还有一点容易坑人:ρ 计算前必须先做数据归一化,否则量纲大的维度会完全主导距离矩阵,聚类中心会偏向数值大的方向。

2.2 δ 距离和决策图:挑中心不是挑密度最高点

CFSFDP 的第二个核心量是 δ_i,表示点 i 到任何一个密度比它高的点的最小距离。计算公式分两种情况讨论:对密度最高的点,δ 定义为它与所有点的最大距离;对其余点,δ 定义为 min(d_ij),其中 j 要满足 ρ_j > ρ_i。这里的逻辑很巧妙:如果点 i 本身是聚类中心,它离比它密度更高的点会非常远,δ 就很大;如果点 i 只是某个高密度点的跟班,δ 的值就是它到最近高密度点的距离,很小。密度最高点被强制取 max,保证了它一定是潜在中心。把每个点的 ρ 和 δ 画成二维散点图,就是论文里著名的决策图。决策图里右上角的点是聚类中心嫌疑人,因为同时具备高密度和远距离两个特征。需要明确的是,挑中心不是单纯挑密度最高的点,因为在决策图里经常出现一个密度极高但 δ 很小的大中心,以及若干个密度中等但 δ 巨大的小中心,后者恰恰是分离度很好的独立簇。我第一遍跑参考代码时只按 ρ 降序取前几个点当中心,结果把一个大簇拆成了三块,后来才发现 δ 的权重必须一起看。

2.3 归一化与聚类中心判据:业界常用的 gamma 阈值法

不同量纲的特征会严重扭曲 δ 的计算结果,因此参考代码里一般会在计算距离矩阵前对 X 做 min-max 归一化或 z-score 归一化。min-max 归一化保住了分布形状,对离群点敏感;z-score 适合密度分布接近正态的数据。归一化之后,ρ 和 δ 各自的数值范围差很多,直接相乘或相加没有可比性。所以标准参考代码里会先把 ρ 和 δ 各自做 min-max 归一化到 [0, 1],再定义 gamma_i = ρ'_i * δ'_i。gamma 值越大,这个点是聚类中心的可能性越高。选择多少个聚类中心,业界常见做法是先对 gamma 降序排序,看排序曲线的明显拐点;拐点之前是中心,拐点之后是普通点。也有更聪明的做法,比如用 gamma 序列的差分最大值自动定位拐点,这个我在第 6 章会给出一个可复现的技巧。很多人会问:为什么不用 δ 单独挑中心?因为只凭 δ 会把所有离群点都选进来;只凭 ρ 会把大簇里的局部高密度区都选进来。两者的乘积天然过滤了这两类噪声。说句大实话,决策图 + gamma 序列双确认是我训练学生最有效的路径,只做一步很容易翻车。

3. 参考代码包的结构解析:仿真生成、主算法、画图三板斧

3.1 代码目录与函数职责

拿到参考代码包后,推荐先列一遍文件职责,不要急着运行。按这套 CFSFDP 参考代码的常见组织方式,会分成三块:仿真数据生成脚本、核心算法函数、可视化脚本。

文件/函数职责备注
generate_data.m / make_data.py生成仿真聚类数据集可生成 3 个高斯簇、月牙形、环形等
compute_rho.m / calc_density.py计算每个点的局部密度 ρ支持 Cutoff 核与高斯核
compute_delta.m / calc_delta.py计算 δ 距离并排序按 ρ 降序逐点扫描
choose_centers.m / select_centers.py决策图 + gamma 拐点选中心常需人工调整拐点位置
assign_points.m / assign_labels.py剩余点归属分配按密度降序依次归入高密度点的簇
plot_decision_graph.m绘制决策图和最终聚类图辅助调参

我一般在复现前会先把 assign_points 的逻辑读完,因为它是整条流水线里最容易写错的一环。分配规则一句话总结:所有点按 ρ 降序排列,从第二个点开始,依次把每个点划给「比它密度高且距离它最近的那个点」所属的簇。这段逻辑决定了同一个簇里的点是否在空间上连续,也决定了边界点会被谁吸纳。如果这里写成按密度升序分配,聚类结果会彻底错掉,而且画出来的图表面看还挺像样,实则是完全不同的结构。

3.2 主流程代码逐段跑通

下面这段是去掉了画图装饰的最小可运行 Python 版主流程,对应参考代码包里的核心逻辑,可直接对照修改:

import numpy as np from scipy.spatial.distance import pdist, squareform def cfsfdp(X, dc, rho_core='gaussian', center_th=0.5): # X: (n_samples, n_features),调用前先归一化 n = X.shape[0] # 1) 距离矩阵:用欧氏距离,超大数据集可换成 faiss 加速 dist = squareform(pdist(X, metric='euclidean')) rho = np.zeros(n) for i in range(n): if rho_core == 'cutoff': rho[i] = np.sum(dist[i] < dc) else: # 高斯核:注意 1e-12 防止数值下溢出现 log(0) rho[i] = np.sum(np.exp(-(dist[i] / dc) ** 2)) # 2) delta:按 rho 降序遍历,当前点找比自己密度高的最近点 order = np.argsort(rho)[::-1] delta = np.zeros(n) nearest_higher = np.zeros(n, dtype=int) for idx in range(n): i = order[idx] higher_mask = rho > rho[i] if not higher_mask.any(): # 密度最高点:delta 取最大距离 delta[i] = np.max(dist[i]) nearest_higher[i] = -1 else: candidates = np.where(higher_mask)[0] # 只比较比当前点密度更高的点,距离取 min j = candidates[np.argmin(dist[i, candidates])] delta[i] = dist[i, j] nearest_higher[i] = j # 3) 归一化 + gamma 选中心 rho_norm = (rho - rho.min()) / (rho.max() - rho.min() + 1e-12) delta_norm = (delta - delta.min()) / (delta.max() - delta.min() + 1e-12) gamma = rho_norm * delta_norm # center_th 是 gamma 序列拐点阈值,下面这行是最简单的截断 centers = np.where(gamma > center_th)[0] # 4) 按密度降序分配归属,保证簇内连续性 labels = -1 * np.ones(n, dtype=int) for c in centers: labels[c] = c for idx in range(n): i = order[idx] if labels[i] == -1: # 直接继承最近高密度点的簇标签 labels[i] = labels[nearest_higher[i]] return labels, rho, delta, gamma, centers

逻辑说明:第 1 段用 pdist 生成紧凑距离向量再转方阵,主要图后面按索引取距离方便;ρ 在两种核函数下都写成纯向量运算,大数据量时可以整体向量化,写成循环是为了让初学者更容易对公式。delta 部分按密度降序扫一遍,每个点记录最近的高密度近邻和距离,这个数组后面分配标签时还要再用一次,是这段代码的复用关键。gamma 选中心用的 center_th 是超参数,想自动确定可参考第 6 章的拐点检测。参数说明:dc 一般设为距离分位数 1%~2%,也可以用 4.1 的密度曲线变化率标定;rho_core 选 'cutoff' 会更快但对噪声更敏感,选 'gaussian'(默认)更平滑;center_th 需要结合决策图人工微调,代码里 0.5 只是示例值,不同数据集差异极大。

3.3 手工实现核心循环的 python 版对照

参考代码包里如果只有 MATLAB 实现,转 Python 时最容易懵的是矩阵索引方式。MATLAB 下标从 1 开始,Python 从 0 开始;MATLAB 的 find 族函数对应 Python 的 np.where;MATLAB 的 sort 默认升序,Python 的 np.argsort 默认也是升序,但用 [::-1] 反转后要注意索引是否还对应原数组位置。我踩过的一个经典坑是:用 np.argsort(rho)[::-1] 之后,在循环里想取「比当前点密度更高」的候选集,却直接用了 order[:idx],这里有个隐患——如果两个点的 ρ 完全相同,who 在前谁在后不影响分配,但如果用 order[:idx] 作为候选集,而 idx 已经过了某个同密度点的位置,就会漏掉它。正确做法永远是用 rho > rho[i] 这个布尔掩码,而不是用排序序号。参考代码里很多衍生版本用的就是这种掩码方式,它严谨地处理了密度相等的情况。

4. 参数怎么调:从 dc 到边界判定阈值的标定流程

4.1 dc 的标定:密度曲线变化率与经验范围

dc 是 CFSFDP 里最敏感的超参数。论文里的经典建议是「让每个点的平均邻居数占到总数据量的 1%~2%」,落到操作层面就是:先算所有点对距离,升序排列,取第 1% 或第 2% 分位数。实际操作中我一般这么标定:先取 0.5% 和 5% 各跑一遍,看决策图形态和最终簇数。如果 0.5% 时 ρ 大量为 0(Cutoff 核)或小到失去区分度,说明 dc 太小;如果 5% 时决策图里右上角只孤零零剩一个点,说明 dc 太大,把细碎小簇的密度都抹平了。还有一个更量化的做法:画出 dc 从 0.5% 到 5% 变化时,聚类中心数量随 dc 的变化曲线,找一个相对平台区。平台区的含义是,在区间内中心数量基本不变,这时 dc 的微小扰动不会导致聚类结构剧烈变化,是最稳的工作点。顺便提醒一句,对高斯核而言,ρ 对所有 dc 都不为 0,所以「ρ 是否为 0」只适合 Cutoff 核的调试场景。

4.2 离群点与边界簇:delta 截断、cluster tag 的边界判定

CFSFDP 本身没有显式的噪声标签,离群点的处理方式在不同参考代码实现里差异很大。常见做法分两种:第一种是计算每个簇内密度的均值与标准差,把低于某个倍数(比如 mean - 2*std)的点标记为噪声;第二种是用 δ 截断,因为离群点通常具备低 ρ、高 δ 的特征,在决策图里会落在左上角。棘手的是,边界点和真正的离群点在 ρ、δ 组合上有重叠,单纯阈值很难区分。我习惯的做法是两阶段:先用 gamma 选中心,再用簇内平均密度加阈值过滤离群点,最后手工检查决策图里被过滤掉的点是否成簇。如果一堆被过滤点连成一条线或一个弧,那多半不是噪声,而是一个低密度但真实的簇,需要用更低一级的 gamma 拐点重新选中心。参考代码里 cluster tag 的赋值顺序也影响边界判定:如果某个簇只有两三个点,常会被噪声规则误杀,这种情况我一般检查簇大小,小于 min_cluster_size 的簇先保留,人工判读后决定去留。

4.3 复现一个三环数据集:从决策图到聚类结果的完整走查

我用一个三环分布的数据集跑一遍完整流程,这是最直观感受参数量纲的方式。三个环半径分别是 1.0、2.0、3.0,每环 300 个点,加少量高斯噪声。第一步归一化,把坐标缩放到 [0,1];第二步算 dc,取距离分位数的 1%,得到 dc ≈ 0.08;第三步用高斯核算 ρ,Cutoff 核再跑一次对比;第四步算 δ 和 gamma;第五步看决策图。这个数据集的决策图会出现两个明显的「高 ρ 高 δ」拐角点,分别对应外环与中环,内环因为被嵌套,其中心点 ρ 高但 δ 明显偏小,在决策图里只表现为一个肩点。如果只盯右上角,很容易丢掉内环。这个现象说明核心要点:决策图选中心不能只选最右上角的点,要结合 gamma 序列的拐点,把「局部最高 + 远端分离」的点都选出来。最终聚类结果三环各有独立标签,颜色切分干净,边界点分布在环两侧。

5. 避坑:跑 CFSFDP 时最容易翻车的五类具体问题

5.1 现象:换了特征工程方案后聚类中心漂移了

原因定位在归一化顺序。我先对原始特征做了 PCA 降维,再对降维后的特征做 min-max 归一化,此时簇结构已经被旋转过,距离矩阵反映的是旋转后的骨架,与原始物理距离不一致;还有一次是先归一化再 PCA,两者结果天差地别。解决:CFSFDP 的参考代码默认你对原始特征直接归一化再算距离。如果你必须在 PCA 之后跑 CFSFDP,那就用 PCA 的白化矩阵把距离度量对齐,同时归一化要用 Z-score,否则主成分方向上的方差差异会被 min-max 放大。从那以后我总在代码里把归一化和降维写进同一段 pipeline,避免顺序因人而异。

5.2 现象:Cutoff 核下 ρ 全是小整数,决策图呈网格状

原因是 dc 取值太小,导致每个点的邻居数只有 0、1、2 这几个档,ρ 离散到失去区分力。解决:把 dc 提到距离分位数的 1% 以上,观察 ρ 是否出现连续的梯度值。判断技巧是打印 np.unique(rho),Cutoff 核下如果 unique 值少于 20 个,基本就是 dc 过小。如果是大数据集且你说服不了自己把 dc 提高,那就换高斯核,它的 ρ 天然是连续值,网格状会消失,但代价是 exp 运算耗时增加。

5.3 现象:gamma 取前 K 个中心全是离群点

这是经典误判,gamma = ρ' * δ',离群点 ρ 小,但 δ 极大;大簇中心 ρ 大,δ 小。乘积反而把普通点压下去,把离群点顶上来。解决:不要独信 gamma 排序,回去看决策图,把决策图右上角人工框选的点与 gamma 降序前 K 个点做交集,只有两边都命中才算中心。另一种做法是把 gamma 改成 δ' * (1 - ρ') 的变体,但这种变体会丢掉高密度小簇的中心,并不通用。

5.4 现象:决策图画出来很漂亮,最终聚类却乱成一团

常见错误在 δ 的实现用了升序扫描而不是降序扫描。论文里 δ 的定义是「到更高密度点的最近距离」,前提是只比较比自己密度高的点。有的参考代码直接对 rho 升序排后,令当前点的 delta 等于到前面所有点的最近距离,这在密度函数单调时等价,但一旦出现密度相等的点,升序会让「高于」被误读成「不高于」,等于点会被自己的高密度同党抢走,簇结构出现断裂。解决:把 rho 先用稳定排序降序,然后用掩码 rho > rho[i] 选候选集,或者在 ρ 相同点之间引入索引等次级排序键,保证严格全序。

5.5 现象:同一份数据的聚类结果随 dc 轻微变化而剧烈震荡

原因之一是距离矩阵没缓存,每次调用都重新算 pdist,随 dc 变化 ρ 更新了但数据没归一化对齐;另一个深层原因是数据集本身存在层级结构——一个簇里包含多个子簇,CFSFDP 在这个尺度上对参数是一阶敏感的。解决方法,第一层是固定随机种子、缓存距离矩阵、统一归一化参数;第二层是真要上台面时用 4.1 的「中心数量—dc 曲线」找平台区,把 dc 稳定在平台中点,再把最终结果与 DBSCAN 的 eps 扫描结果做交叉验证。交叉验证的意义在于,两种算法原理不同,如果结论一致,代码几乎没写错。

6. 用 gamma 序列差分拐点自动选中心:一个稳定复现的边界技巧

这一章给出一个我常用于替代人工看决策图的小工具:自动定位 gamma 排序序列的拐点。先对 gamma 降序排序,把它看作一条从左上往右下走的曲线,聚类中心集中在前面急剧下降段,普通点落在后面平缓段。拐点就是这条曲线从陡降转为平缓的位置,构造成曲线顺序的差分(一阶差分)或二阶差分,差分值从大到小的落点附近就是中心与普通点的分界。实际操作中,我不用全局最大差分,而是取差分序列排序后的指数衰减区间,因为第一个差分尖峰往往对应的是「最高密度中心」到「次高密度中心」的跳变,只有取到第二个或第三个尖峰才能把中小簇的中心全捞进来。正确的落地做法是取排序差分的前若干个大值对应的索引,然后用决策图人工确认。还有一种是参考了主流开源实现里的做法:先把 gamma 做对数变换,再用 kneedle 算法(曲线拐点自动检测)找拐点,这个方法在平衡密度分布的数据上表现不错,但数据集极不均衡时会失效。我现在的习惯是写成四步走:第一步,gamma 降序;第二步,计算一阶差分并取绝对值;第三步,把差分排序,取前 max(5, n_clusters_expected) 个峰值附近索引;第四步,人工核对决策图。这个四步走帮我把原先每天手动挑中心的重复劳动降为零,类别数都能一次定对。前阵子做某图像分割 Demo 的对比实验时,两个模型对同一批物体的分割指标差 3 个点,我用 CFSFDP 对分割后的超像素块做合并,gamma 拐点自动把过分割块归到正确的语义类里,省了半天的阈值调试时间。现在每次复现 CFSFDP 参考代码,我强制自己先跑一遍「dc 扫描平台区 + gamma 差分拐点 + 决策图人工确认」三件套,宁可多花五分钟画图,也不跳步直接看聚类结果。这套流程帮我避开了无数个「结果看起来对但换数据就崩」的尴尬时刻,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询