简介:面向机器学习与数据挖掘初学者的CFSFDP参考代码,对应模糊数据分区中的特征选择问题。算法结合相关性度量与模糊聚类思想,通过计算特征间关联并评估每个特征对聚类效果的贡献,迭代剔除低价值特征,帮助简化模型并提升聚类精度,尤其适合包含离散、连续及模糊属性的复杂数据集。压缩包共7个文件,包含Python实现源码、CSV样例数据及Markdown格式说明文档,整体仅12KB,轻量易读,方便对照论文逐步理解。目前已有1124人学习,证明其在聚类算法研究中有一定参考价值。代码中除主算法外,还提供了模糊聚类模块、示例数据集和测试脚本,可直接运行并观察特征筛选前后的聚类质量变化。借助这份代码,读者可以掌握特征贡献度评估、阈值设定与后验指标解读等关键环节,为后续改进聚类模型打下扎实基础。 CFSFDP这个缩写,刷过聚类方向论文的人应该都不陌生。2014年发表在Science上的那篇Clustering by fast search and find of density peaks,提出了一个特别直观的想法:聚类中心不是光看密度,也不是光看距离,而是两者结合——它既要被一帮比自己密度低的点围着,又要离那些密度更高的点足够远。论文本身就属于“读完想拍大腿”的类型,更难得的是作者在论文里留了参考代码和测试数据,这些年一直是很多人学习聚类算法的入门素材。我也啃过好几遍这组参考代码,每次都有收获,这篇文章就把里头的关键点、实现细节和容易踩的坑一次说清楚。
这套参考代码适合谁看?我的判断是:刚接触密度聚类、想搞懂CFSFDP不只是“画个决策图”那么简单的同学,或者已经在调参但总感觉结果不稳定、想回去看看原始实现到底怎么处理的从业者。代码本身不复杂,但里面藏了不少论文里一句话带过、实际写起来却要斟酌的细节,把这些细节抠明白,比单纯调包有意思得多,也管用得多。
1. 核心算法思路拆解:密度峰值的两个“硬性指标”
CFSFDP全称是Clustering by Fast Search and Find of Density Peaks,直译就是“基于密度峰值的快速搜索与发现聚类”。它的出发点非常朴素:一个理想的聚类中心,应该满足两个条件,缺一不可。
1.1 第一个条件:局部密度ρ——它得是“周围人眼里的大哥”
局部密度ρ_i描述的是点i周围密集程度。参考代码里用了原始论文推荐的截断核:
ρ_i = Σ_j χ(d_ij - d_c)
其中χ(x)在x < 0时为1,否则为0。说白了,就是数一数在截断距离d_c范围内有多少个邻居。这个定义简单到有点“粗暴”,但效果出奇稳定。后来很多实现会用高斯核代替截断核,目的只是让密度估计更平滑,但参考代码里的截断核更接近论文原始设定,也更容易理解。
这里有个容易忽略的细节:计算ρ_i时要不要包含自身?参考代码在计算邻居数时通常会排除自己,但有些复现版本没排除,导致每个点的密度都多1。在样本量小的时候,这个误差会直接影响后面的聚类中心判定。
1.2 第二个条件:高密度距离δ——它得和“上面的人”保持距离
全局密度最高那个点,δ定义为到所有点的最大距离。其他点i的δ_i定义为:在所有密度比ρ_i高的点中,找到离i最近的那个,记录下这段距离。如果密度已经全场最高,那δ_i就是到最远点的距离。
“密度又高、距离又远”这两个词放一起,就是聚类中心的本质画像。普通簇内点密度高但δ小,离群点δ大但密度低,只有聚类中心两者都占。参考代码里用一张决策图把ρ和δ画出来,右上角的点就是要找的聚类中心。这个可视化设计特别聪明,比直接输出一堆数字直观得多。
1.3 分配策略:密度降序贪心,一步到位
找到聚类中心后,剩下的点怎么归类?参考代码用了一个极其高效的策略:把所有点按密度从高到低排序,然后遍历——每个非中心点直接归属到“密度高于它且距离最近”的那个点所在的簇。因为密度高的点一定先被处理,所以这个分配过程不需要迭代,一次排序加一次遍历就能完成全部点的归属。
这种贪心分配为什么有效?它等价于沿着“密度爬升”的路径,把每个点连接到最终的山顶。CFSFDP论文里有个很形象的说法:每个点就像沿着水流的方向,最终汇入某一个密度峰值所在的湖泊。参考代码里这个逻辑用几个数组就实现了,效率高得惊人,哪怕几万个点也是毫秒级完成分配。
2. 参考代码结构解析:文件怎么看、函数怎么配合
拿到参考代码第一件事不是跑,而是先把文件之间的关系理清楚。原始代码通常包含一个主脚本、距离计算函数、密度计算函数、聚类中心选择函数和分配函数。我习惯把整个流程分成四个阶段,对应四段代码逻辑。
2.1 距离矩阵:一切计算的起点
CFSFDP所有计算都建立在距离矩阵之上。参考代码里最常用的就是scipy的pdist和squareform组合:
from scipy.spatial.distance import pdist, squareform dists = squareform(pdist(data, metric='euclidean'))pdist计算的是压缩后的距离向量,squareform把它还原成对称矩阵。为什么不用双重for循环?因为pdist底层是C实现的,速度比Python循环快几十倍不止。在数据量超过一万时,这个差距会从“能等”变成“等不起”。
距离矩阵的存储也要留心:n个点的距离矩阵是n×n的float64数组,一万个点就是800MB内存,两万个点直接3.2GB。很多人的电脑在跑参考代码时突然卡死,多半就是这里爆了。
2.2 截断距离d_c:全流程最敏感的参数
参考代码里d_c的默认策略是:把所有点对的距离排序,取某个百分位数。论文建议取1%到2%的分位数,使得每个点的平均邻居数约为总点数的1%到2%。但实际使用时,这个值非常依赖数据本身的尺度。
def estimate_dc(dists, percent=2.0): n = dists.shape[0] triu_idx = np.triu_indices(n, k=1) all_dists = dists[triu_idx] sorted_dists = np.sort(all_dists) index = int(len(sorted_dists) * percent / 100.0) return sorted_dists[index]有个经验值:如果数据是归一化到[0,1]区间的,d_c通常在0.01到0.1之间;如果数据没有归一化,建议先做标准化再算距离,否则d_c会完全被量纲带偏。参考代码里一般不主动做数据预处理,这个步骤得自己在外面完成。
2.3 计算ρ和δ:两个关键函数的实现细节
ρ的计算在截断核设定下非常快:
def compute_rho(dists, dc): rho = (dists < dc).sum(axis=1) - 1 return rho减1是为了排除自身。这里的(dists < dc)会生成一个布尔矩阵,True记为1,求和就是邻居数。
δ的计算稍微绕一点。我按参考代码的逻辑重新梳理了一个清晰版本:
def compute_delta(rho, dists): n = len(rho) delta = np.zeros(n) nearest_higher = np.zeros(n, dtype=int) # 按密度降序排列 order = np.argsort(rho)[::-1] # 密度最高点的delta设为全局最大距离 delta[order[0]] = dists[order[0]].max() nearest_higher[order[0]] = -1 for i in range(1, n): idx = order[i] higher_points = order[:i] distances_to_higher = dists[idx, higher_points] nearest = np.argmin(distances_to_higher) delta[idx] = distances_to_higher[nearest] nearest_higher[idx] = higher_points[nearest] return delta, nearest_highernearest_higher这个数组后面分配簇时还要用,所以别省,一定要存下来。
2.4 决策图与聚类中心选择
拿到ρ和δ后,参考代码会画决策图。但决策图怎么“读”,很多人第一次是懵的。我自己的判断标准是:先把ρ×δ算出来,看它的降序排列,找拐点或明显的跳变处。
gamma = rho * delta sorted_gamma = np.sort(gamma)[::-1] plt.plot(range(1, len(sorted_gamma) + 1), sorted_gamma, 'o-')如果曲线在某个位置之后断崖式下降,前面的点就是候选聚类中心。这个方法比纯肉眼看散点图更可操作,尤其在点比较多的时候。参考代码里有的版本提供自动选择中心的方法,但现实情况千变万化,手动看γ曲线通常更稳。
3. 实操记录:用参考代码跑通一个完整案例
光看逻辑容易飘,我拿一个实际数据集走一遍完整流程,你用同样的步骤就能复现。
3.1 准备环境与示例数据
我用的是经典的Aggregation数据集,一共788个点,7个簇,形状不规则,很适合检验密度聚类。环境就三件套:numpy、scipy、matplotlib。
pip install numpy scipy matplotlib数据加载直接用pandas读取,然后转成numpy数组。这里有个实操建议:数据最好先做z-score标准化,因为Aggregation数据在不同维度上尺度不一样,不标准化的话距离矩阵会被某些维度主导。
3.2 完整实现参考代码
我整合了一份最精简但保留所有核心步骤的版本:
import numpy as np from scipy.spatial.distance import pdist, squareform import matplotlib.pyplot as plt def cfsfdp(data, dc_percent=2.0, n_clusters=None): # 1. 距离矩阵 dists = squareform(pdist(data, metric='euclidean')) n = dists.shape[0] # 2. 截断距离 triu_idx = np.triu_indices(n, k=1) sorted_dists = np.sort(dists[triu_idx]) dc = sorted_dists[int(len(sorted_dists) * dc_percent / 100.0)] # 3. 局部密度 rho = (dists < dc).sum(axis=1) - 1 # 4. 高密度距离 delta = np.zeros(n) nearest_higher = np.zeros(n, dtype=int) order = np.argsort(rho)[::-1] delta[order[0]] = dists[order[0]].max() nearest_higher[order[0]] = -1 for i in range(1, n): idx = order[i] higher = order[:i] distances = dists[idx, higher] nearest = np.argmin(distances) delta[idx] = distances[nearest] nearest_higher[idx] = higher[nearest] # 5. 选择聚类中心(用gamma找拐点) gamma = rho * delta sorted_idx = np.argsort(gamma)[::-1] centers = [] if n_clusters is None: # 观察gamma曲线,手动选择中心 plt.figure(figsize=(6, 4)) plt.plot(range(1, n + 1), gamma[sorted_idx], 'o-') plt.xlabel('rank') plt.ylabel('gamma') plt.show() centers = list(map(int, input("请输入聚类中心索引(空格分隔): ").split())) else: centers = sorted_idx[:n_clusters].tolist() # 6. 分配:按密度降序,归属到最近的高密度点 labels = np.full(n, -1, dtype=int) for c in centers: labels[c] = c for i in range(n): idx = order[i] if labels[idx] == -1: labels[idx] = labels[nearest_higher[idx]] return labels, centers, rho, delta, dc这段代码基本就是参考代码的浓缩版,但保留了所有关键步骤。你在学习时可以逐行打印中间变量,看看每个点的ρ和δ分别是多少,对理解算法非常有帮助。
3.3 运行结果与边界分析
我在Aggregation数据上跑了这个实现,设置dc_percent=1.5,选择7个聚类中心后,分配结果非常接近论文里的标准输出。有几个点被分到了“错误”的簇——但仔细看,这些点本来就处于两个簇的交界处,密度低、边界模糊,任何无监督算法都会在这里摇摆。CFSFDP在这里的表现已经算相当好。
有一点值得注意:参考代码默认把所有点都分配进了簇,没有做噪声点剔除。原始论文里其实有“边界区域密度阈值”的处理——先找出跨簇的边界点,再取其中密度的最大值作为阈值,低于这个阈值的点标记为噪声。但很多参考代码把这个环节省略了,所以在有噪声的数据集上,直接跑参考代码会看到“噪声也挂到某个簇里”的情况。
如果你需要噪声识别,加上这段逻辑就行:
def find_border_rho(dists, labels, dc, n_clusters): n = dists.shape[0] min_rho_border = np.zeros(n_clusters) for i in range(n): for j in range(i + 1, n): if dists[i, j] < dc and labels[i] != labels[j]: rho_ij = min(rho[i], rho[j]) if rho_ij > min_rho_border[labels[i]]: min_rho_border[labels[i]] = rho_ij if rho_ij > min_rho_border[labels[j]]: min_rho_border[labels[j]] = rho_ij return min_rho_border密度低于对应簇阈值的点就是噪声。参考代码不做这个,但学会自己加,才是真的掌握了。
4. 学习参考代码的常见问题与避坑实录
这段是我自己反复折腾总结出来的经验,参考代码网上也有很多版本,但几乎每个版本都有一些“隐藏关卡”,过不去就容易劝退人。
4.1 d_c对结果的影响有多大?
非常大。d_c调小,密度高的点可能变成噪声;d_c调大,所有点密度都差不多,聚类中心彻底淹没。我在一个2000点的模拟数据上测试过,d_c从1%调到5%,聚类数量肉眼可见地从4个变成2个。
排查思路也很简单:先画决策图,如果ρ的值全部挤在一起没有层次,说明d_c太大;如果大部分点ρ=0,说明d_c太小。参考代码给的1%-2%只是一个起点,实际必须结合决策图微调。
4.2 为什么参考代码跑出来的结果和别人论文里的不一致?
最常见原因是数据集本身没做标准化。CFSFDP对欧氏距离的尺度极其敏感,两个特征的量纲差10倍,距离矩阵基本就只看那个大尺度特征了。
其次是聚类中心的选择方式不同。参考代码有时候用的是“人工从决策图选中心”,有时候用“自动从γ排序选前K个”,两种方式得到的结果本身就可能有差异。我的建议:初学阶段全部用人工选中心,把决策图看明白,比追求自动化有意义得多。
4.3 距离矩阵太大导致内存不够怎么办?
一万个点以上的数据,距离矩阵就会吃掉几百MB到几GB内存。参考代码图省事直接全量计算,但实际使用时,可以用分块计算和近似最近邻来规避。一个简单的替代方案是用sklearn的NearestNeighbors先建索引,只保留每个点的k近邻距离,损失一点精度,换来内存的大幅下降。
不过学习阶段我不建议一上来就优化内存。先把n=1000以下的数据跑通,理解算法逻辑,再考虑扩展性问题。
4.4 聚类中心分配出来后“逻辑上总觉得不对”怎么办?
可能是簇中心点选多了或选少了。CFSFDP对聚类中心的数量比较敏感,多选一个中心就会把一个完整簇切成两半,少选一个又会把两个簇粘在一起。我在实际项目中见过很多次:不是算法错了,而是决策图上的中心数量没拿准。
辅助判断方法:选完中心后,看看每个簇的点数和形态是否合理。如果某个簇的点明显少于预期,多半是中心选多了;如果两个簇的中心点在决策图上离得很近,试试合并。
4.5 学这套参考代码的正确姿势
如果只是跑一遍、看个图、然后关掉,收获会非常有限。我的建议是先按顺序做四件事:第一,把ρ、δ的中间结果打印出来,逐行对照公式,确认自己理解每个数值怎么来的;第二,只用numpy手写一遍全流程,不调pdist,不用squareform,哪怕是for循环都行——这步能帮你把矩阵乘法和索引逻辑彻底刻进脑子;第三,改一个变量,比如把截断核换成高斯核,看看结果变化,理解两种核的区别;第四,跑一个有噪声的数据集,自己实现论文里的边界噪声检测,比对加与不加的差异。
这四步走完,你对CFSFDP的理解深度不会亚于任何“调包熟练工”。
5. 参考代码的扩展玩法:把CFSFDP用在真实场景时要注意什么
学参考代码不只是为了复现论文,最终还是要用到实际问题里。这几年CFSFDP常被用在轨迹聚类、图像分割、异常检测等方向,我挑两个典型场景聊聊扩展时容易踩的坑。
5.1 轨迹聚类场景:距离度量必须重设计
如果你直接拿CFSFDP处理轨迹数据,会发现“欧氏距离”根本没法用——轨迹长度不同,起止点不同,逐点对齐算距离既不科学也不稳定。参考代码里的距离矩阵计算是通用的,但那个“距离”本身要换成DTW、LCSS这类轨迹相似度度量。
我做过一个实验:把同一批用户轨迹用欧氏距离和DTW分别跑CFSFDP,前者的聚类结果基本是“按轨迹长度分组”,后者才是真正的“按移动模式分组”。所以参考代码只是给了你一个聚类的骨架,距离定义必须依据业务场景自己换。
5.2 异常检测场景:噪声识别要保留
CFSFDP天然适合做异常检测,因为噪声点就是那些密度低、但又没有归属到任何高密度区域的点。但参考代码默认不做噪声检测,你得把边界密度阈值识别那段逻辑补上,并把簇内的异常点单独标记出来。
我实践下来比较有效的做法是:CFSFDP聚类后,对每个簇计算内部的密度分布,把所有低于该簇第5百分位密度的点标记为“簇内异常”。这样既保留了簇结构,又能输出业务上可解释的异常清单。
5.3 小样本场景:谨慎使用d_c百分位
当数据量只有几十到一两百个点,d_c取所有距离的1%-2%分位数,大概率只会保留极少几个邻居,密度估计会非常稀疏。这种情况下我建议把d_c放宽到5%-10%,或者直接用高斯核。参考代码里d_c的自动估计在小样本上并不稳健,这一点论文里没有明说,但实测下来很值得注意。
我的实操心得
我把这套参考代码反复啃过好几轮,最大的体会是:CFSFDP的核心竞争力不在“准”,而在“快”和“直观”。它不像DBSCAN那样需要调一堆参数才能得到稳定结果,也不像K-means那样对初始值和簇数那么敏感。它的设计哲学更像是“先让人看懂,再让机器求解”——决策图这个东西,是真的能让非算法人员也理解“为什么这些点是一类”的。
最后分享一个我一直在用的小技巧:学习CFSFDP的参考代码时,别一上来就在大型数据集上折腾。拿一个1000点以内的二维数据集,把每个中间步骤的结果可视化出来——ρ画成热力图,δ画成箭头图,分配过程画成gif动图。把这些图看明白了,算法所有行为特征就再也忘不掉了,后续做任何变种和改进都有了底子。
本文还有配套的精品资源,点击获取