按需计算距离,构建单链聚类层次
scikit-learn 的单链聚类使用 Prim 派生的 MST-LINKAGE-CORE,维护每个未加入样本到当前集合的最小距离,随后用并查集生成聚类层次。
工程问题#
单链聚类以两个簇之间最近的样本距离决定合并顺序。若先保存全部 n 个样本之间的距离,需要 O(n²) 空间;对于可以直接计算距离的向量输入,完整距离矩阵并非必须。
实际实现#
未提供 connectivity、选择 single linkage 且使用内置可用距离度量时,linkage_tree 调用 mst_linkage_core。算法保存未加入样本到已处理集合的当前最小距离,每次纳入距离最小的样本,再用这个新样本更新其余距离。这沿用 Prim 的扩展规则,但仅记录足以恢复单链聚类的代表节点三元组;稳定排序后,single_linkage_label 用并查集给每次合并生成簇编号。
处理流程#
- 确认进入 single linkage 的向量距离优化路径,准备连续的 double 输入
- 按需计算新样本到其余样本的距离,更新到当前集合的最小距离
- 选取最小值对应的样本继续扩展,记录聚类所需的代表节点与距离
- 稳定排序记录,用并查集追踪已合并簇并生成 children_ 和合并距离
源码片段#
固定版本 · 原文摘录569 if (570 linkage == "single"571 and affinity != "precomputed"572 and not callable(affinity)573 and affinity in METRIC_MAPPING64574 ):575 # We need the fast cythonized metric from neighbors576 dist_metric = DistanceMetric.get_metric(affinity)577578 # The Cython routines used require contiguous arrays579 X = np.ascontiguousarray(X, dtype=np.double)580581 mst = _hierarchical.mst_linkage_core(X, dist_metric)582 # Sort edges of the min_spanning_tree by weight583 mst = mst[np.argsort(mst.T[2], kind="mergesort"), :]584585 # Convert edge list into standard hierarchical clustering format586 out = _hierarchical.single_linkage_label(mst)587 else:588 out = hierarchy.linkage(X, method=linkage, metric=affinity)只有内置可用度量的 single 路径调用 mst_linkage_core;随后稳定排序并用 single_linkage_label 生成聚类树,其他输入走 scipy.cluster.hierarchy.linkage。
478 for i in range(n_samples - 1):479480 in_tree[current_node] = 1481482 new_distance = INFINITY483 new_node = 0484485 for j in range(n_samples):486 if in_tree[j]:487 continue488489 right_value = current_distances[j]490 left_value = dist_metric.dist(&raw_data[current_node, 0],491 &raw_data[j, 0],492 num_features)493494 if left_value < right_value:495 current_distances[j] = left_value496497 if current_distances[j] < new_distance:498 new_distance = current_distances[j]499 new_node = j将 current_node 加入集合;对未加入样本计算新距离,与已知最小距离比较,然后选择最小的候选。这是 Prim 派生的逐点扩展规则。
501 result[i, 0] = current_node502 result[i, 1] = new_node503 result[i, 2] = new_distance504 current_node = new_node505506 return np.array(result)保存 current_node、new_node 和当前最小距离,但不保存这个最小距离对应的真实来源端点;结果用于聚类,不能直接当作原图的 MST 边表。
338 @cython.wraparound(True)339 cdef intp_t fast_find(self, intp_t n) noexcept:340 cdef intp_t p341 p = n342 # find the highest node in the linkage graph so far343 while self.parent[n] != -1:344 n = self.parent[n]345 # provide a shortcut up to the highest node346 while self.parent[p] != n:347 p, self.parent[p] = self.parent[p], n348 return n查找当前簇代表,并沿途压缩父节点路径,供后续合并查询复用。
376 result_arr = np.zeros((L.shape[0], 4), dtype=np.float64)377 U = UnionFind(L.shape[0] + 1)378379 for index in range(L.shape[0]):380381 left = <intp_t> L[index, 0]382 right = <intp_t> L[index, 1]383 delta = L[index, 2]384385 left_cluster = U.fast_find(left)386 right_cluster = U.fast_find(right)387388 result_arr[index][0] = left_cluster389 result_arr[index][1] = right_cluster390 result_arr[index][2] = delta391 result_arr[index][3] = U.size[left_cluster] + U.size[right_cluster]392393 U.union(left_cluster, right_cluster)394395 return np.asarray(result_arr)按已排序记录查找两端的当前簇,记录簇编号、合并距离和大小,再创建新的合并关系。这是生成模型 children_ 前的实际层次表示。
五段连续原文串起公开估计器使用的内部调用、Prim 派生内核和并查集合并。原论文明确说明该简化内核不维护完整 MST,因此案例保留这一边界。
收益与取舍#
工程分析带来的收益
- 距离按需生成,无须保留 O(n²) 的全点对距离矩阵
- 以线性规模的距离数组和合并记录组织计算,并查集跟踪不断变化的簇编号
付出的代价
- n 个样本需要 O(n²) 次距离比较;如果每次距离计算为 O(d),距离阶段为 O(n²d),并未因此消除二次时间成本
- 此内核除输入外使用 O(n) 级工作数组和输出;Python 层转换连续 double 输入可能额外复制 O(nd) 数据
- 单链方法容易沿局部近邻产生链状合并;这是聚类准则本身的特点,和距离矩阵是否保存无关
可比较的选择
预先保存完整距离矩阵适合已有距离数据或需要反复使用全部距离的场景,但保存 n×n 距离需要 O(n²) 空间。
提供稀疏 connectivity 约束限制可合并的邻接关系,并进入另一条实现路径;计算规模和可获得的聚类结果也可能改变。
版本与适用范围#
对应 scikit-learn 1.6.1:connectivity=None、linkage="single"、affinity 非 precomputed 且非 callable,并在 METRIC_MAPPING64 中。高层公开参数为 metric,内部 linkage_tree 沿用 affinity 命名。MST-LINKAGE-CORE 是 Prim 派生的聚类算法;它省略了恢复真正最小生成树所需的端点记录,不能把返回三元组逐行当作原图的 MST 边。这里的并查集为新簇创建父节点,含路径压缩但不是 LLVM 案例中的按秩合并。
参考来源#
源码、官方文档与原始论文sklearn/cluster/_agglomerative.py · linkage_tree · 优化路径调用
sklearn/cluster/_hierarchical_fast.pyx · mst_linkage_core · 按需距离与集合扩展
sklearn/cluster/_hierarchical_fast.pyx · UnionFind / single_linkage_label
相关实现与比较#
1 个相关案例LLVM 对控制流图按边权从高到低选生成树,用并查集排除环,把边计数器放在非树边上;这是 Kruskal 的最大生成树变体。