原作者:The scikit-learn developers。阅读原文:Demo of HDBSCAN clustering algorithm。
版本与范围:scikit-learn 1.9.1 官方 stable 完整示例;sklearn.cluster.HDBSCAN 自 1.3 起提供;原文核验日期:2026-10-05。本译稿覆盖原文章节及代码示例;示例输出来自原文,未在本地运行。
译者说明:本译稿保留完整 12 个源码块与官方原文 10 幅结果图。图片、簇数和原文记录的运行时间均来自原网站,未在本地运行生成。末节换用新标签后仍用原 probabilities_ 控制点大小,是原示例的重要局限,已在图注和核验说明中标出。
说明
原文末尾提供完整示例代码下载,也可以通过 JupyterLite 或 Binder 在浏览器中运行。本译稿保留下载入口,未代为运行示例。
本示例从推广 cluster.DBSCAN 算法的角度介绍 cluster.HDBSCAN。我们会在指定数据集上比较两种算法,最后考察 HDBSCAN 对几个超参数的敏感程度。
原文参考:cluster.HDBSCAN;cluster.DBSCAN。
为便于后续绘图,先定义几个辅助函数。
# Authors: The scikit-learn developers
# SPDX-License-Identifier: BSD-3-Clause
import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import DBSCAN, HDBSCAN
from sklearn.datasets import make_blobs
def plot(X, labels, probabilities=None, parameters=None, ground_truth=False, ax=None):
if ax is None:
_, ax = plt.subplots(figsize=(10, 4))
labels = labels if labels is not None else np.ones(X.shape[0])
probabilities = probabilities if probabilities is not None else np.ones(X.shape[0])
# Black removed and is used for noise instead.
unique_labels = set(labels)
colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]
# The probability of a point belonging to its labeled cluster determines
# the size of its marker
proba_map = {idx: probabilities[idx] for idx in range(len(labels))}
for k, col in zip(unique_labels, colors):
if k == -1:
# Black used for noise.
col = [0, 0, 0, 1]
class_index = (labels == k).nonzero()[0]
for ci in class_index:
ax.plot(
X[ci, 0],
X[ci, 1],
"x" if k == -1 else "o",
markerfacecolor=tuple(col),
markeredgecolor="k",
markersize=4 if k == -1 else 1 + 5 * proba_map[ci],
)
n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0)
preamble = "True" if ground_truth else "Estimated"
title = f"{preamble} number of clusters: {n_clusters_}"
if parameters is not None:
parameters_str = ", ".join(f"{k}={v}" for k, v in parameters.items())
title += f" | {parameters_str}"
ax.set_title(title)
plt.tight_layout()
生成示例数据
与 DBSCAN 相比,HDBSCAN 的主要优势之一,是采用默认设置时就具有较好的稳健性,在由异质数据混合而成的数据集上尤其明显。与 DBSCAN 一样,它可以处理各种形状和分布的簇;区别在于,它不需要指定那个既敏感、又往往缺乏直观依据的 eps 超参数。
例如,下面从三个二维各向同性高斯分布的混合中生成一个数据集。
centers = [[1, 1], [-1, -1], [1.5, -1.5]]
X, labels_true = make_blobs(
n_samples=750, centers=centers, cluster_std=[0.4, 0.1, 0.75], random_state=0
)
plot(X, labels=labels_true, ground_truth=True)

尺度不变性
需要记住,虽然 DBSCAN 为 eps 提供了默认值,但这个值很难普遍适用,必须针对正在使用的数据集进行调整。
一个简单的演示是:先为某个数据集调好 eps,再保持 eps 不变,观察将同一数据集缩放之后得到的聚类。
fig, axes = plt.subplots(3, 1, figsize=(10, 12))
dbs = DBSCAN(eps=0.3)
for idx, scale in enumerate([1, 0.5, 3]):
dbs.fit(X * scale)
plot(X * scale, dbs.labels_, parameters={"scale": scale, "eps": 0.3}, ax=axes[idx])

要保持相同结果,eps 也必须乘以相同的缩放因子。
fig, axis = plt.subplots(1, 1, figsize=(12, 5))
dbs = DBSCAN(eps=0.9).fit(3 * X)
plot(3 * X, dbs.labels_, parameters={"scale": 3, "eps": 0.9}, ax=axis)

虽然对数据进行标准化,例如使用 sklearn.preprocessing.StandardScaler,可以缓解这一问题,但仍需仔细选择合适的 eps。
原文参考:sklearn.preprocessing.StandardScaler。
在这个意义上,HDBSCAN 更稳健:可以把它理解为考察所有可能 eps 值对应的聚类,再从全部候选簇中提取合适的簇,详见用户指南。一个直接好处是,HDBSCAN 对尺度具有不变性。译者补充:这里的尺度变化指示例中的所有特征共同乘以相同正数,不是任意按列缩放或非线性变换。
原文参考:User Guide。
fig, axes = plt.subplots(3, 1, figsize=(10, 12))
hdb = HDBSCAN(copy=True)
for idx, scale in enumerate([1, 0.5, 3]):
hdb.fit(X * scale)
plot(
X * scale,
hdb.labels_,
hdb.probabilities_,
ax=axes[idx],
parameters={"scale": scale},
)

多尺度聚类
HDBSCAN 的能力不止尺度不变性,它还能进行多尺度聚类,处理密度各不相同的簇。传统 DBSCAN 使用单一密度尺度,而 HDBSCAN 不受这一限制。下面用另一个数据集演示。
centers = [[-0.85, -0.85], [-0.85, 0.85], [3, 3], [3, -3]]
X, labels_true = make_blobs(
n_samples=750, centers=centers, cluster_std=[0.2, 0.35, 1.35, 1.35], random_state=0
)
plot(X, labels=labels_true, ground_truth=True)

由于不同簇的密度和空间分隔不同,这个数据集对 DBSCAN 更困难:
如果 eps 过大,两个较密集的簇可能通过密度可达关系连接起来,被错误地合并为一个簇。
如果 eps 过小,较稀疏的簇又可能被切碎成许多不真实的小簇。
而且,还需要不断手动调整 eps,直到找到可以接受的折中。
fig, axes = plt.subplots(2, 1, figsize=(10, 8))
params = {"eps": 0.7}
dbs = DBSCAN(**params).fit(X)
plot(X, dbs.labels_, parameters=params, ax=axes[0])
params = {"eps": 0.3}
dbs = DBSCAN(**params).fit(X)
plot(X, dbs.labels_, parameters=params, ax=axes[1])

要正确区分那两个密集簇,需要更小的 epsilon。然而在 eps=0.3 时,稀疏簇已经开始碎裂;继续减小 epsilon 只会使问题更严重。在这个数据集上,DBSCAN 似乎无法同时分开两个密集簇、又避免稀疏簇碎裂。下面与 HDBSCAN 比较。
hdb = HDBSCAN(copy=True).fit(X)
plot(X, hdb.labels_, hdb.probabilities_)

无需调整参数,HDBSCAN 就能适应这个数据集的多尺度结构。具有一定复杂度的真实数据集仍然需要调参;不过,这个例子说明,HDBSCAN 有时能够在用户不加干预的情况下,得到 DBSCAN 无法通过单一 eps 实现的、更合适的一类聚类结果。
超参数稳健性
在真实应用中,调参终究是重要的一步。下面考察 HDBSCAN 的几个主要超参数。它虽然省去了 DBSCAN 的 eps,仍然需要通过 min_cluster_size 和 min_samples 等参数调整与密度有关的聚类行为。接下来可以看到,这些参数的含义较清晰,因此比较便于调节,也有助于 HDBSCAN 在不同场景中取得较稳健的表现。
min_cluster_size:最小簇大小
min_cluster_size 指一组样本要被视为一个簇,至少必须包含多少个样本。
小于这一大小的组会保留为噪声。默认值为 5,通常根据需要向更大的值调整。值较小时,被标记为噪声的点可能更少;但若设得太小,算法可能识别并优先选择并无真实意义的小子簇。值较大时,对含噪数据通常更稳健,例如方差较大且相互重叠明显的簇。
PARAM = ({"min_cluster_size": 5}, {"min_cluster_size": 3}, {"min_cluster_size": 25})
fig, axes = plt.subplots(3, 1, figsize=(10, 12))
for i, param in enumerate(PARAM):
hdb = HDBSCAN(copy=True, **param).fit(X)
labels = hdb.labels_
plot(X, labels, hdb.probabilities_, param, ax=axes[i])

min_samples:核心点所需的邻域样本数
min_samples 是一个点成为核心点所需的邻域样本数量,包含该点自身。默认情况下,它取 min_cluster_size 的值。与 min_cluster_size 类似,增大 min_samples 可以增强模型对噪声的稳健性,但也可能忽略或丢弃规模虽小、却有意义的簇。通常应先找到合适的 min_cluster_size,再调整 min_samples。
PARAM = (
{"min_cluster_size": 20, "min_samples": 5},
{"min_cluster_size": 20, "min_samples": 3},
{"min_cluster_size": 20, "min_samples": 25},
)
fig, axes = plt.subplots(3, 1, figsize=(10, 12))
for i, param in enumerate(PARAM):
hdb = HDBSCAN(copy=True, **param).fit(X)
labels = hdb.labels_
plot(X, labels, hdb.probabilities_, param, ax=axes[i])

dbscan_clustering:复用层次树得到固定距离下的聚类
在 fit 期间,HDBSCAN 会建立单连接树,用来编码不同 DBSCAN eps 值下所有点的聚类关系。因此,无须重新计算核心距离、互达距离和最小生成树等中间结果,就能高效绘制与评估不同距离截断对应的聚类。只需指定想要使用的 cut_distance,其角色相当于 eps。译者补充:该接口得到的是 DBSCAN* 风格的结果,边界点处理不保证与普通 DBSCAN 完全相同。
原文参考:DBSCAN。
PARAM = (
{"cut_distance": 0.1},
{"cut_distance": 0.5},
{"cut_distance": 1.0},
)
hdb = HDBSCAN(copy=True)
hdb.fit(X)
fig, axes = plt.subplots(len(PARAM), 1, figsize=(10, 12))
for i, param in enumerate(PARAM):
labels = hdb.dbscan_clustering(**param)
plot(X, labels, hdb.probabilities_, param, ax=axes[i])

原文记录的脚本总运行时间为 12.603 秒。这是原网站生成示例时的记录,不是本译稿的实测数据,也不构成性能保证。
下载 Jupyter Notebook:plot_hdbscan.ipynb。
原文参考:Download Jupyter notebook: plot_hdbscan.ipynb。
下载 Python 源码:plot_hdbscan.py。
原文参考:Download Python source code: plot_hdbscan.py。
下载压缩包:plot_hdbscan.zip。
原文参考:Download zipped: plot_hdbscan.zip。
相关示例
在玩具数据集上比较不同聚类算法。
原文参考:Comparing different clustering algorithms on toy datasets。
scikit-learn 1.3 发布亮点。
原文参考:Release Highlights for scikit-learn 1.3。
DBSCAN 聚类算法示例。
原文参考:Demo of DBSCAN clustering algorithm。
随机投影嵌入的 Johnson–Lindenstrauss 界。
原文参考:The Johnson-Lindenstrauss bound for embedding with random projections。
原文示例库由 Sphinx-Gallery 生成。
原文参考:Gallery generated by Sphinx-Gallery。
译者核验与使用边界
原文依赖 NumPy、Matplotlib 和 scikit-learn,使用 sklearn.cluster.HDBSCAN,不是第三方 hdbscan 包。全部数据通过 make_blobs 合成,random_state=0;官方图来自原文示例构建,不代表本地复现。
尺度不变性仅指该示例默认欧氏距离下,对全部维度作同一正数的统一缩放;不能推广为任意逐列标准化、不同量纲、非线性变换或含固定距离阈值配置时结果都不变。
min_samples 在 scikit-learn 实现中包含点自身;默认 None 时跟随 min_cluster_size。原文 min_cluster_size 对比同时隐式改变了 min_samples,因此不是严格控制变量的单参数实验。
probabilities_ 表示点对其已分配簇的成员强度,不是经过校准的业务概率,也不是聚类标签的正确率。合成数据中的 labels_true 来自数据生成器,不能把真实业务数据也假定存在这样的可验证真值。
实际发现:最后 dbscan_clustering 循环得到新 labels,却继续使用初次 HDBSCAN 拟合得到的 hdb.probabilities_ 调节点大小。不能把这组大小解读为各个新截断簇的置信度。为忠实保留原图与原代码,未暗中修改;图注明确标明。如果另行改绘,可用统一点大小,但那属于与原文不同的绘图版本。
官方 HDBSCAN API 明确 dbscan_clustering 是不含普通 DBSCAN 边界点归属的 DBSCAN* 风格结果,因此不能把 cut_distance 相同当作与 sklearn.cluster.DBSCAN 完全等价。核验来源:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.HDBSCAN.html#sklearn.cluster.HDBSCAN.dbscan_clustering 。
原 plot 辅助函数仅把 -1 当噪声并从簇数中排除;若将示例扩展到包含无穷值或缺失值的数据,HDBSCAN 的 -2/-3 特殊标签也应单独处理。现有 make_blobs 数据有限且无缺失,不触发此限制。
较高维度会影响距离解释;样本量、距离计算方式与邻域树选择影响成本。原文的低维合成例子和 12.603 秒运行记录不能推广为所有真实数据均无需调参或具有同样性能。
来源、署名与授权
本文译自 The scikit-learn developers 的 Demo of HDBSCAN clustering algorithm。原文源码明确标注 Authors: The scikit-learn developers 与 SPDX-License-Identifier: BSD-3-Clause,均原样保留。适用版权与 BSD-3-Clause 许可全文见下文; 译者补充与原文内容已作区分。
补充核验来源
HDBSCAN 官方 API:成员强度及 DBSCAN* 截断:probabilities_ 是成员强度;dbscan_clustering 不含普通 DBSCAN 边界点处理。
scikit-learn 官方 BSD-3-Clause 许可:保留版权、条件和免责声明;完整文本保留在本文下方。
适用代码许可
以下为原示例适用的完整 BSD-3-Clause 许可与版权声明;中文说明和编校增补已标明。
BSD 3-Clause License Copyright (c) 2007-2026 The scikit-learn developers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.













暂无评论内容