CFSFDP密度峰值聚类:从决策图到参考代码拆解与实战避坑

发布时间:2026/10/13 14:51:53
CFSFDP密度峰值聚类:从决策图到参考代码拆解与实战避坑 简介面向聚类与特征选择研究者的CFSFDP参考代码包完整实现了基于相关性分析与模糊数据分区FDP的特征选择流程通过计算特征间相关性与对聚类性能的贡献度来筛选有效特征帮助处理含模糊、离散与连续属性的数据集简化模型并提升聚类精度。资源为zip压缩包共7个文件含3个csv示例数据集、1份Python主程序及说明文档整体仅12KB结构紧凑便于快速定位核心模块。已有1125人学习下载适合机器学习初学者与需要复现该算法的研究人员。除主算法实现外代码还附有模糊聚类模块、测试脚本和多组csv数据可直接运行并观察特征贡献度排序与聚类性能变化为理解特征选择与模糊聚类技术的结合提供直观范例也便于后续二次开发与参数调优。1. 为什么 CFSFDP 的参考代码值得一行行读CFSFDPClustering by Fast Search and Find of Density Peaks这份参考代码是我见过的聚类算法实现里性价比极高的一类算法思路来自一篇顶级期刊论文实现却精简到一晚上能读完还自带决策图让你用手而不是黑匣子来选簇中心。很多做数据分析的开发者第一次读它时会愣一下——原来聚类可以不用 KMeans 那种固定形状假设也不用被 DBSCAN 的 eps 反复折磨。它用“局部密度”和“更高密度点距离”两个量就把簇中心、离群点和边界点分得明明白白。这份代码适合三类人一是被 KMeans 的 k 值和 DBSCAN 的 eps 调参搞烦的开发者想换一种更直观的聚类范式二是要做密度聚类二次开发的研究者代码结构清晰改起来比从论文自己推快得多三是刚接触聚类的新手想找一个“能跑通、能画图、能解释”的入门项目。本文会从算法骨架讲起再给出一条最小复现路径最后把参数边界和常见坑一次说清。读完你会发现这类参考代码不是拿来背的是拿来拆的。2. 读代码前先立住算法骨架密度、距离和决策图2.1 CFSFDP 的聚类中心长什么样局部密度与更高密度点距离CFSFDP 的核心假设只有一句话聚类中心是被密度更低区域包围的局部密度高点而且它和任何密度比它更高的点都保持一个相对远的距离。为了实现这句话参考代码里会给每个点算两个量。第一个是局部密度 ρ常见做法是以截断距离 dc 为半径统计落在这个半径内的邻居数量也可以用高斯核做加权求和距离越近贡献越大。第二个量是 δ表示“到更高密度点的最近距离”。如果一个点的密度已经是全局最高那它的 δ 就直接取到所有其他点的最大距离。有了这两个量每个点就变成一个二维点ρδ。簇中心长在决策图的右上方ρ 大说明附近密集δ 大说明它和更高密度的区域隔得远符合“局部密度峰值”的直觉。值得注意的是离群点也会有大的 δ但它的 ρ 往往很小所以看决策图时不能只盯 δ必须 ρ 和 δ 一起看。这也决定了 CFSFDP 选中心是一个“看图决策”的过程而不是像 KMeans 那样直接算个距离平方和。读参考代码时我一般会先确认它实现的是哪种密度定义。两种定义的差异很实在硬截断密度是计数对 dc 的变化特别敏感dc 稍微调小一点邻居数可能从 20 掉到 3高斯核密度是加权求和对 dc 的敏感度低一些密度分布也更平滑。参考代码里通常会留一个 method 参数来切换这两种模式这个开关是读懂全篇的钥匙。2.2 分配标签的关键一跳更高密度最近邻数组选完簇中心之后剩下所有点要怎么归簇CFSFDP 的做法不是像 KMeans 那样迭代更新中心而是排序后一次性扫过去每个非中心点沿着“比它密度更高的最近邻”一路爬直到爬到某个簇中心为止。这个“更高密度最近邻”在参考代码里通常存成一个数组叫 nneigh 或者 higher_neighbor是整个分配过程的数据基础。分配过程用文字描述就是先把所有点按密度从高到低排序密度最高的点没有更高密度邻居跳过其余每个点找比自己密度高的点里距离最近的那个记下来。然后再次按密度从高到低扫一遍把自己的簇标签赋给更高的邻居所对应的簇。这一步只要 O(N) 的扫描就能完成不需要迭代也不需要预先指定簇数。这也是 CFSFDP 被称为快速聚类的原因之一。实现里有个细节容易忽略密度相等时怎么处理。如果 A 和 B 密度完全一样它们互不算“更高密度邻居”排序顺序就会影响分配结果。参考代码里有的实现用严格大于号避免成环有的会加一个微小扰动打破平局。我自己的习惯是排序时用稳定排序并且让密度相同的点互相之间距离小的优先分配这样结果可复现不会出现每次运行标签不同的问题。2.3 决策图的可解释性为什么它是这个算法的记忆点决策图是 CFSFDP 最有辨识度的输出横轴是局部密度 ρ纵轴是 δ。把数据点画上去之后右上角的点几乎不需要解释。这种可视化让聚类过程从“调参黑匣子”变成了“看图说话”也让非技术合作方更容易理解你为什么选这几个簇。我在某物流项目的实际落地里就是用决策图向业务方解释为什么把某个仓库分区聚成五类而不是拍脑袋定的五块。不过决策图也有它的“玄学”成分右上角多少个点算“明显”并没有硬性标准。常见做法是画两条辅助线一条横线对应 δ 阈值一条竖线对应 ρ 阈值。先看竖线左边的点排除掉低密度候选再看横线上方的点剩下的交集就是簇中心。参考代码里通常会提供一个画决策图的函数参数就是这两个阈值后面第 4 章会细讲怎么取值。3. 用参考代码跑通自己的数据最小复现路径3.1 前置准备一张距离矩阵和一份“密度不均”的测试数据在动代码之前先搞清楚输入格式。绝大多数 CFSFDP 参考代码接受两种输入一是 N 行 m 列的特征矩阵内部自己算两两距离二是直接给 N×N 的距离矩阵。前者适合大多数场景后者用于你已经有了非欧距离度量的情况。无论哪种数据最好先做标准化不然量纲差异会把密度计算带偏。我这里构造一份密度差异明显的二维数据来展示算法特性外侧是一个半环形内侧是一个密集小簇形状和密度都不一样KMeans 在这份数据上基本会翻车而密度峰聚类正好能体现出优势。import numpy as np def make_two_shapes(n600, seed42): 构造环形簇 中心密集簇密度差异明显。 rng np.random.default_rng(seed) half n // 2 # 半环形半径 6 的大圆弧加少量噪声 t np.linspace(0, np.pi, half) ring np.stack([6 * np.cos(t), 3 * np.sin(t)], axis1) ring rng.normal(scale0.15, sizering.shape) # 中心小而密的簇 core rng.normal(loc[0, 0], scale[0.5, 0.5], size(half, 2)) X np.vstack([ring, core]) labels_true np.array([0] * half [1] * half) # 标准化避免量纲影响距离 X (X - X.mean(axis0)) / X.std(axis0) return X, labels_true X, y_true make_two_shapes() # 全距离矩阵后续所有计算都基于它 D np.linalg.norm(X[:, None, :] - X[None, :, :], axis2)这段代码里n 控制总样本数half 决定两个簇各占一半scale0.5 让中心簇的半径明显小于环形制造密度差。标准化那一步很关键如果省略横纵坐标单位不一致距离矩阵会被数值大的维度主导。距离矩阵 D 用广播的方式一次算出N600 时约 360 万 float64内存 28 MB完全可接受但到第 5 章你会看到这种方法在更大样本上会很快爆掉。3.2 参考代码最核心的三段估计 dc、算 ρ/δ、分配标签下面这段是参考代码的骨架实现我把它压缩成三个函数各自职责单一。compute_dc 用“平均邻居占比”自动估计截断距离compute_rho_delta 算每个点的密度和更高密度距离assign_labels 按密度排序传播簇标签。这三段看懂整份参考代码就懂了大半。def compute_dc(D, percent2.0): 自动估计截断距离 dc让每点平均邻居数约为总样本数的 percent%。 N D.shape[0] # 取上三角去掉重复距离和自身距离 dists D[np.triu_indices(N, k1)] dists np.sort(dists) # 找排序后位于 percent% 位置的距离作为 dc idx int(len(dists) * percent / 100.0) - 1 idx max(idx, 0) return dists[idx] def compute_rho_delta(D, dc, methodcutoff): 计算局部密度 rho 和更高密度最近距离 delta。 N D.shape[0] rho np.zeros(N) for i in range(N): if method cutoff: # 硬截断距离小于 dc 的邻居个数去掉自身 rho[i] np.sum(D[i] dc) - 1 else: # 高斯核加权距离越近权重越高同样去掉自身距离 0 rho[i] np.sum(np.exp(-(D[i] / dc) ** 2)) - 1 order np.argsort(-rho) # 按密度从大到小排序 delta np.zeros(N) nneigh np.zeros(N, dtypeint) # 更高密度最近邻编号 delta[order[0]] np.max(D[order[0]]) # 密度最高点的 delta 取全局最大距离 for i in range(1, N): idx order[i] higher order[:i] # 密度更高的点集合 d D[idx, higher] j np.argmin(d) delta[idx] d[j] nneigh[idx] higher[j] return rho, delta, nneigh def assign_labels(D, centers, nneigh, rho): 从簇中心出发按密度从高到低传播标签。 N D.shape[0] labels np.full(N, -1, dtypeint) for k, c in enumerate(centers): labels[c] k order np.argsort(-rho) for i in order: if labels[i] -1: labels[i] labels[nneigh[i]] return labels三个函数要连起来看。compute_dc 里的 percent 默认取 2.0意思是一个点期望有 N×2% 个邻居这个经验范围对应 1%~2%是论文和参考代码里最常见的设置。compute_rho_delta 里有两个容易被忽略的细节cutoff 模式用了严格小于号所以自身距离 0 不会被算进去但保险起见还是减了 1高斯核模式里 exp(-(0/dc)²)1同样要减 1 去掉自身。delta 的计算只依赖密度排序所以复杂度是排序 O(NlogN) 加扫描 O(N²)和距离矩阵计算 O(N²) 相比压力主要集中在内存而不是 CPU。assign_labels 的输入 centers 是人工从决策图选出的点编号。注意传播方向密度高的点先被分配低密度点顺着 nneigh 一路找到归宿。只要簇中心选对这个传播过程不需要任何迭代也不存在收敛问题。代价是如果 centers 里混进了一个低密度离群点它会把附近一串点都带偏所以选中心宁可少选、别多选。3.3 画决策图、圈出簇中心并完成可视化有了 ρ 和 δ决策图就是一张散点图。不过光画出来还不够要配合两条辅助线确定阈值再用选出的中心跑一次 assign_labels最后把结果画回原始坐标空间。这一步是整个参考代码里最有感觉的时刻簇的形状、密度差异、边界点全部显形。import matplotlib.pyplot as plt def plot_decision(rho, delta, rho_thNone, delta_thNone): 决策图横轴密度 rho纵轴更高密度距离 delta。 plt.figure(figsize(5, 4)) plt.scatter(rho, delta, s8, csteelblue) if rho_th is not None: plt.axvline(rho_th, colorgray, ls--, lw1) if delta_th is not None: plt.axhline(delta_th, colorgray, ls--, lw1) plt.xlabel(local density rho) plt.ylabel(delta) def plot_clusters(X, labels): 把聚类结果画回二维坐标空间。 plt.figure(figsize(6, 4)) plt.scatter(X[:, 0], X[:, 1], clabels, s8, cmapplasma) plt.axis(equal)决策图的横竖辅助线是最直接的调参平面竖线左侧的点排除横线下方的点排除剩余候选就是簇中心。可视化时如果两类颜色混在一起优先检查是不是 dc 选太大导致两个簇被密度桥连起来了如果出现大量 -1 的灰色点多半是簇中心选少了有些孤岛没被传播到。这些排查逻辑比任何参数公式都实用。4. 参数怎么设dc、密度核和自动选中心的边界4.1 dc 不是半径而是“邻居配比”1%~2% 的直觉与实现很多初学者把 dc 当成邻域半径来调这是最常见的误读。dc 的真正作用是通过截断距离控制密度的分辨率dc 太小每个点都只有少量邻居密度曲线像梳子一样锯齿状决策图上几乎所有点都是孤立峰dc 太大密度平滑成一条单调曲线簇中心之间的差异被抹平。参考代码里用“平均邻居数占总数 1%~2%”来定 dc本质上是一种自适应策略——样本量大dc 自动变小保证每点的邻居数稳定在几十个的量级。我在实际项目里的经验是1%~2% 是很好的起点但不是终点。如果样本本身噪声比较大比如传感器数据建议把 percent 降到 0.5%~1%让密度估计更保守避免噪声点互相抱团形成假中心如果数据密度分布极不均匀比如一个簇特别密、另一个特别疏那全局 dc 注定只能顾一头这时候要考虑第 6 章的局部 dc 技巧。判断 dc 是否合适的粗暴方法把距离矩阵排序后画一条曲线取 dc 处附近的斜率变化。斜率比较陡说明你选在了“邻居数快速上升”的位置这个位置通常比较合理。4.2 三个必调参数的推荐区间和相互影响参考代码的参数表面上看只有 dc、method、簇中心阈值三个实际调起来互相牵制。我整理了一张常用参数表方便对照排查。参数出现位置含义常用设定设错的表现percentcompute_dc邻居数占总样本百分比1.0~2.0过小决策图全是孤立点过大所有点粘成一团methodcompute_rho_delta密度核类型gaussian / cutoffcutoff 对 dc 太敏感边界毛刺多rho_thplot_decision簇中心的最小密度看决策图分布取分界太低离群点被当中心太高漏掉小簇delta_thplot_decision簇中心的最小 δ看决策图分布取分界太低簇内部点被误选为中心centers_numassign_labels实际选择的簇中心数决策图右上点数多选一个簇被劈碎少选两个簇被合并其中 method 的选择容易被低估。cutoff 密度计算只有 dc 的判断速度稍快但边界点的密度对 dc 的轻微变化会成片波动gaussian 核让每个点对密度的贡献随距离平滑衰减边界点不再是“非黑即白”聚类结果明显稳定。除非参考代码的性能瓶颈严重否则我一律建议优先使用 gaussian。4.3 自动选簇中心的 γ 排序能用但不万能手动从决策图选中心适合分析场景但如果你要写成一个自动化流程比如每晚定时聚类一批新数据就需要一个自动选中心的启发式。最常见的做法是计算 γ_i ρ_i × δ_iγ 越大说明该点越是“高密度且远离其他高密度点”把 γ 降序排列后取前 k 个。这个启发式在簇大小相对均匀时表现不错但它有一个明显缺陷δ 的数量级往往远大于 ρ导致 γ 被 δ 主导结果是决策图左上角那批“低密度但很孤”的离群点反而拿到高分。我的修正习惯是给 ρ 设一个下界或者对 ρ 和 δ 分别做百分位排名后再相乘这样两个维度贡献均衡。另外k 的取值可以看 γ 曲线的拐点拐点之后 γ 快速下降说明再往下选的多半是噪声点。这个“看拐点”的操作没有严格数学定义但比硬编码 k 值要靠谱得多也能让自动化流程不至于太离谱。5. CFSFDP 避坑手册从参考代码到真实数据的高频翻车点5.1 决策图右上角挤满一片点选不出几个清晰中心现象把完全陌生的数据直接丢进参考代码dc 用默认 2% 算出后画出的决策图右上角笼罩着一大片点怎么圈都圈不出明显的孤立点强行选中心后聚类结果混乱。原因最常见的是数据里有大量重复或近似重复的样本距离矩阵里大量距离接近 0导致密度分布出现“平台”一堆点的 ρ 几乎一样另一个高频原因是数据量纲差异大某个维度的数值差直接主导了所有距离。解决先做两步预处理。第一步是标准化让每个特征方差为 1第二步是去重或合并近似点我用 np.unique 对标准化后的数据取整去重减少平台效应。之后重新算距离矩阵决策图通常会清爽很多。如果还是挤成一团把 percent 调小到 1.0 再试。5.2 一个簇被切成两半或者两个簇死活粘在一起现象外部形状上明明是一个连续的带状簇聚类结果却从中间断开变成两坨反过来两个距离很近的簇却分不开边界互相交错。原因前者通常是 dc 设小了或者 cut-off 密度对边界点的跳动太敏感后者多数是 dc 设大了两个簇之间的“密度桥”被当成同一片区域密度峰连成一片。解决先把 method 切到 gaussian它会让密度分布平滑很多边界粘滞问题会改善不少。如果还断就提高 percent 到 2.5~3.0如果还粘就降到 1.0。这个正反调节通常能在两三次内找到平衡点远比盲目在 dc 上二分快。5.3 离群点被当成了簇中心一个簇瞬间变成一组噪声点现象决策图里挑中心时看到一个 δ 极大、ρ 也还行的点于是把它选成中心结果这个簇分出来全是零零散散的点几乎没有内部结构。原因δ 大是“孤立”的信号ρ 不小可能是“小簇”的信号但在真实数据里这两者经常同时出现在离群点身上——它离主簇远周围虽然有几个近邻但整体密度远低于真正的簇。解决选中心前盯住 rho_th不要只看 δ。做法是先画一条 ρ 的竖线把左半边的候选全部排除每次选中心后看该簇的样本数量和平均密度如果样本数小于总样本的 1%基本可以判定为误选的噪声簇。这在参考代码里没有自动提示是我的血泪经验。5.4 5 万样本内存爆掉全距离矩阵是罪魁祸首现象数据量从几千涨到几万代码在算 D 矩阵那一行直接卡死或者内存占用冲到十几 GB 最终被杀。原因全距离矩阵的规模是 O(N²)N5 万时 float64 矩阵需要 20 GB 内存大多数个人机器扛不住。参考代码为了教学清晰通常不优化这一步但真实数据不能这么跑。解决改用 K 近邻稀疏图代替全距离矩阵。用 KDTree 求出每个点的最近 k 个邻居和对应距离然后只在邻居对之间评估密度和 δ复杂度降到 O(Nk)。密度全局最高的点在稀疏图里可能没有更高密度邻居需要单独处理把它标记为虚拟根节点。这个改造会牺牲一点精度但能让样本量提升两个数量级。5.5 相同密度的点分配结果反复横跳边界标签不稳定现象同一份数据只是改变排序方式或者稍微改一下 dc边界一带的标签就换来换去簇中心没变但边界归属不稳定。原因CFSFDP 分配阶段依赖严格的“更高密度”关系当两个点密度相等时谁比谁高完全看排序顺序。离散化产生的等密度点越多这种抖动越明显。解决在 compute_rho_delta 里把密度排序改成二级排序第一关键字是 rho第二关键字是最小邻居距离这样相等密度能按几何位置破平或者给 rho 加一个极小的随机扰动扰动固定随机种子保证每次运行结果一致。稳定排序加固定种子是我习惯的底线至少能让结果在多次运行时可复现。6. 把全局 dc 改成局部 dc一个提升密度不均场景效果的小技巧前面提过全局 dc 只能提供一个统一的尺度遇到“一个簇密、一个簇疏”的数据会顾此失彼。我实际用过的一个有效变体是局部 dc对每个点取它到第 k 近邻的距离作为该点的局部尺度 dci然后用高斯核计算密度时让每个点用自己的 dci 而不是全局统一的 dc。这样密区自动用小尺度疏区自动用大尺度密度图谱便平滑了很多。from scipy.spatial import cKDTree def compute_local_rho(X, k20): 局部 dc 的高斯核密度每个点用自己的第 k 近邻距离做尺度。 tree cKDTree(X) # query 返回 [距离, 索引]取第 k1 个即第 k 近邻 dist_k, _ tree.query(X, kk 1) dc_i dist_k[:, -1] 1e-8 # 逐个点算高斯密度用到自身时 exp(0)1要减掉 rho np.zeros(X.shape[0]) for i in range(X.shape[0]): d np.linalg.norm(X - X[i], axis1) rho[i] np.sum(np.exp(-(d / dc_i[i]) ** 2)) - 1 return rho参数 k 取 20 是我平常用的起点k 越小局部尺度越敏感容易把单个点自身的局部密度抬得很高k 越大越接近全局 dc 的效果。验证这个改动值不值得最直接的方法是真标签数据上跑 ARI或者在业务数据上看簇内距离是否下降。比如对前面那个环形加中心簇的测试集全局 dc 版本的分离往往依赖运气局部 dc 版本则稳定地把环和中心簇分开边界也更干净。这个技巧并不适合所有场景如果你的数据密度本来就均匀局部 dc 反而会引入额外噪声。但遇到不确定情况我会直接跑一遍对比数据说话。克劳德·香农有句话我很认同“信息来源于差异。”CFSFDP 的参考代码真正教会我的不是调参技巧而是让差异在决策图上自己说话。每次拿到新数据我如今都会先画决策图再看几眼右上角的形状再决定这个项目适不适合密度峰这条路。这条路不一定每次都对但它值得一试希望帮到你。本文还有配套的精品资源点击获取