DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是基于密度的聚类算法,核心优势是能发现任意形状的聚类、自动识别离群点,无需提前指定聚类数量,这是 K-Means 等基于距离的聚类算法做不到的。

一、核心概念

        DBSCAN 用 “密度” 定义聚类:聚类是空间中密度相连的点的集合,噪声是密度过低的孤立点。

1. 两个超参数

DBSCAN 仅需两个人工设置的超参数,是参数极少的聚类算法:

  • ε(Epsilon,邻域半径):以某个点为中心,半径为 ε 的圆形区域,称为该点的ε- 邻域(二维)和ε- 球(高维)。
  • MinPts(最小点数):一个点的 ε- 邻域内至少包含的点数,其中包括自身,是判断点类型的核心阈值。

2. 三种点的类型

DBSCAN 将数据集中的点分为 3 类,点的类型决定了聚类的生成逻辑:

  1. 核心点(Core Point):若点p的 ε- 邻域内的点数 ,则p为核心点。核心点是聚类的 “种子”,是密度聚类的核心支撑。
  2. 边界点(Border Point):若点q的 ε- 邻域内的点数,但q落在某个核心点的 ε- 邻域内,则q为边界点。边界点是聚类的 “边缘”,依附于核心点存在。
  3. 离群点(噪声点,Noise Point):既不是核心点、也不是边界点的点,即:ε- 邻域内点数 ,且不落在任何核心点的 ε- 邻域内。噪声点是算法自动识别的无效点。

3. 两个密度关系

点与点之间的密度关系,是连接成聚类的 “纽带”,决定了哪些点属于同一个聚类:

  1. 直接密度可达:若点q在核心点p的 ε- 邻域内,则称q从p直接密度可达,仅核心点能触发该关系。
  2. 密度相连:若存在点o,使得点p和点q都从o密度可达,则称p和q密度相连。

4. 聚类的定义

DBSCAN 中,一个聚类是满足以下两个条件的最大点集:

  1. 密度相连性:聚类内任意两点都密度相连;
  2. 最大性:聚类外的点与聚类内的点都不密度相连。

        总结:聚类是由核心点及其密度可达的所有点组成的集合,边界点依附核心点存在,噪声点无任何核心点支撑。

二、数学公式

        设数据集为D=\left \{ p_{1} ,p_{2} ,...,p_{n} \right \},其中p_{i}为 d 维空间中的数据点,n为样本数;

        设距离函数为dist\left ( p,q \right )(常用欧氏距离,DBSCAN 可兼容任意距离函数,如曼哈顿、余弦距离);

        超参数:邻域半径ε>0,最小点数MinPts≥2,可以记作一般 MinPts≥维度 + 1,如二维数据 MinPts≥3)。

1. 点p的 ε- 邻域

N_{\varepsilon }\left ( p \right )=\left \{ q\epsilon D | dist\left ( p,q \right )\leq \varepsilon \right \}

解释:N_{\varepsilon }\left ( p \right )表示数据集中所有与p的距离≤ε 的点的集合,集合的元素个数记为∣Nε​(p)∣。

2. 核心点

p 是核心点  ⟺  \left | N_{\varepsilon } \left ( p \right )\right |\geq MinPts

解释:ε- 邻域内的点数以及自身点≥MinPts,p即为核心点。

3. 直接密度可达

q 从 p 直接密度可达⟺p 是核心点 且 q∈Nε​(p);

解释:仅核心点能让其他点直接密度可达,边界点(噪声点)无法触发该关系。

4. 密度可达

        若存在点序列p_{0}= p,p_{1},p_{2},p_{3},...,p_{m}=q,使得p_{i+1}p_{i}直接密度可达(i=0,1,...,m−1),则称q从p密度可达。

记为p⟶ε,MinPts​q

解释:直接密度可达的传递闭包,核心点 A 的 ε- 邻域内的核心点 B,其 ε- 邻域内的点 C,从 A 密度可达。

5. 密度相连

        若存在点o∈D,使得p从o密度可达且q从o密度可达,则称p和q密度相连。

        解释:密度相连的点共享同一个 “核心点祖先”,属于同一个聚类。

6. 噪声点

        p 是噪声点⟺∄核心点 o∈D, 使得 p∈Nε​(o)。

        解释:不存在任何核心点,使得p落在其 ε- 邻域内,p即为噪声点。

三、数学计算

模块一:导入必须库

import numpy as np  # 数值计算,处理数组(核心)
import matplotlib.pyplot as plt  # 数据可视化--二维聚类展示

模块二:DBSCAN 类的初始化方法

  1. 超参数仅epsmin_pts,是 DBSCAN 的核心;
  2. labels_遵循 sklearn 接口规范,方便用户习惯;
  3. cluster_id用于标记不同聚类,每生成一个新聚类,ID 自增 1。
def __init__(self, eps, min_pts):
    self.eps = eps  # 保存邻域半径ε
    self.min_pts = min_pts  # 保存最小点数MinPts
    self.labels_ = None  # 存储聚类标签,sklearn风格命名(下划线结尾表示训练后生成)
    self.cluster_id = 0  # 聚类ID计数器,从0开始

模块三:欧氏距离计算

def _euclidean_distance(self, p, q):
    return np.sqrt(np.sum((p - q) ** 2))

模块四:获取 ε- 邻域

  • 核心辅助方法:根据索引找到目标点的所有 ε- 邻域点索引,存储索引比存储点本身更高效,尤其高维数据;
  • 输入Xn×d的 numpy 数组,n 样本数,d 维度,p_idx是目标点的索引;
  • 输出是邻域内所有点的索引列表,列表长度即为∣Nε​(p)∣。
def _get_epsilon_neighborhood(self, X, p_idx):
    neighbors = []  # 存储邻域内点的索引(而非点本身,节省内存)
    p = X[p_idx]    # 取出目标点p
    for q_idx in range(X.shape[0]):  # 遍历所有点
        # 计算p和q的距离,≤eps则加入邻域
        if self._euclidean_distance(p, X[q_idx]) <= self.eps:
            neighbors.append(q_idx)
    return neighbors

模块五:拟合方法

def fit(self, X):
    n = X.shape[0]  # 样本数n
    # 初始化标签数组:全-1(噪声),int类型
    self.labels_ = np.full(shape=n, fill_value=-1, dtype=int)
    # 初始化访问标记数组:全False(未访问),bool类型
    visited = np.full(shape=n, fill_value=False, dtype=bool)

    # 遍历每个点,按顺序处理
    for p_idx in range(n):
        if visited[p_idx]:  # 已访问的点直接跳过,避免重复处理
            continue
        visited[p_idx] = True  # 标记为已访问
        neighbors = self._get_epsilon_neighborhood(X, p_idx)  # 找ε-邻域

        # 情况1:邻域点数<min_pts → 噪声点,标签保持-1
        if len(neighbors) < self.min_pts:
            self.labels_[p_idx] = -1
        # 情况2:邻域点数≥min_pts → 核心点,扩展生成新聚类
        else:
            self._expand_cluster(X, p_idx, neighbors, visited)
            self.cluster_id += 1  # 聚类生成后,ID+1

模块六:聚类扩展方法

        这是 DBSCAN 的核心实现,负责循环吸收所有密度可达的点,用列表模拟队列实现循环扩展,比递归更稳定,避免栈溢出:

def _expand_cluster(self, X, p_idx, neighbors, visited):
    # 第一步:将核心点分配到当前聚类(cluster_id)
    self.labels_[p_idx] = self.cluster_id
    # 用列表实现队列,i为队列指针,从0开始遍历
    i = 0
    while i < len(neighbors):
        q_idx = neighbors[i]  # 取出队列中的第i个点q
        # 若q未访问,先标记为已访问,并检查是否为核心点
        if not visited[q_idx]:
            visited[q_idx] = True
            q_neighbors = self._get_epsilon_neighborhood(X, q_idx)  # 找q的ε-邻域
            # 若q是核心点,将其邻域加入队列,继续扩展(密度可达)
            if len(q_neighbors) >= self.min_pts:
                # 集合去重→列表,避免队列中出现重复索引,节省计算
                neighbors = list(set(neighbors + q_neighbors))
        # 若q未分配聚类(标签-1),分配到当前聚类
        if self.labels_[q_idx] == -1:
            self.labels_[q_idx] = self.cluster_id
        # 指针后移,处理下一个点
        i += 1

模块七:测试代码

if __name__ == "__main__":
    from sklearn.datasets import make_moons, make_blobs
    np.random.seed(42)  # 固定随机种子,结果可复现
    X1, _ = make_moons(n_samples=200, noise=0.05)  # 两个月牙形(非凸)
    X2, _ = make_blobs(n_samples=100, centers=[[1, 1]], cluster_std=0.1)  # 高斯凸聚类
    X = np.vstack([X1, X2])  # 合并数据
    noise = np.random.randn(30, 2) * 0.5 + [0, 2]  # 30个随机噪声点
    X = np.vstack([X, noise])

    # 初始化DBSCAN,训练
    dbscan = DBSCAN(eps=0.15, min_pts=5)
    dbscan.fit(X)
    labels = dbscan.labels_

    # 可视化
    plt.figure(figsize=(10, 6))
    # 绘制聚类点
    for cluster in set(labels) - {-1}:
        mask = labels == cluster
        plt.scatter(X[mask, 0], X[mask, 1], label=f'Cluster {cluster}', s=50)
    # 绘制噪声点(黑色×)
    plt.scatter(X[labels==-1, 0], X[labels==-1, 1], c='black', marker='x', label='Noise', s=50)
    plt.title('DBSCAN Clustering Result', fontsize=14)
    plt.xlabel('Feature 1')
    plt.ylabel('Feature 2')
    plt.legend()
    plt.grid(alpha=0.3)
    plt.show()

    # 输出聚类统计信息
    n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
    n_noise = list(labels).count(-1)
    print(f"检测到的聚类数:{n_clusters}")
    print(f"检测到的噪声点数:{n_noise}")

运行结果

检测到的聚类数:36
检测到的噪声点数:32

四、结语

  • DBSCAN 是基于密度的聚类,用ε- 邻域和MinPts定义密度,将点分为核心点、边界点、噪声点;
  • 聚类由核心点及其所有密度可达的点组成,边界点依附核心点,噪声点无核心点支撑;
  • 核心逻辑是核心点的 ε- 邻域扩展,将所有密度相连的点纳入同一个聚类。

感谢大家的观看!如果有不足,请大家的批评指正!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐