机器学习————DBSCAN 聚类
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是基于密度的聚类算法,核心优势是能发现任意形状的聚类、自动识别离群点,无需提前指定聚类数量,这是 K-Means 等基于距离的聚类算法做不到的。
一、核心概念
DBSCAN 用 “密度” 定义聚类:聚类是空间中密度相连的点的集合,噪声是密度过低的孤立点。
1. 两个超参数
DBSCAN 仅需两个人工设置的超参数,是参数极少的聚类算法:
- ε(Epsilon,邻域半径):以某个点为中心,半径为 ε 的圆形区域,称为该点的ε- 邻域(二维)和ε- 球(高维)。
- MinPts(最小点数):一个点的 ε- 邻域内至少包含的点数,其中包括自身,是判断点类型的核心阈值。
2. 三种点的类型
DBSCAN 将数据集中的点分为 3 类,点的类型决定了聚类的生成逻辑:
- 核心点(Core Point):若点p的 ε- 邻域内的点数 ,则p为核心点。核心点是聚类的 “种子”,是密度聚类的核心支撑。
- 边界点(Border Point):若点q的 ε- 邻域内的点数,但q落在某个核心点的 ε- 邻域内,则q为边界点。边界点是聚类的 “边缘”,依附于核心点存在。
- 离群点(噪声点,Noise Point):既不是核心点、也不是边界点的点,即:ε- 邻域内点数 ,且不落在任何核心点的 ε- 邻域内。噪声点是算法自动识别的无效点。
3. 两个密度关系
点与点之间的密度关系,是连接成聚类的 “纽带”,决定了哪些点属于同一个聚类:
- 直接密度可达:若点q在核心点p的 ε- 邻域内,则称q从p直接密度可达,仅核心点能触发该关系。
- 密度相连:若存在点o,使得点p和点q都从o密度可达,则称p和q密度相连。
4. 聚类的定义
DBSCAN 中,一个聚类是满足以下两个条件的最大点集:
- 密度相连性:聚类内任意两点都密度相连;
- 最大性:聚类外的点与聚类内的点都不密度相连。
总结:聚类是由核心点及其密度可达的所有点组成的集合,边界点依附核心点存在,噪声点无任何核心点支撑。
二、数学公式
设数据集为,其中
为 d 维空间中的数据点,n为样本数;
设距离函数为(常用欧氏距离,DBSCAN 可兼容任意距离函数,如曼哈顿、余弦距离);
超参数:邻域半径ε>0,最小点数MinPts≥2,可以记作一般 MinPts≥维度 + 1,如二维数据 MinPts≥3)。
1. 点p的 ε- 邻域
解释:表示数据集中所有与p的距离≤ε 的点的集合,集合的元素个数记为∣Nε(p)∣。
2. 核心点
p 是核心点 ⟺
解释:ε- 邻域内的点数以及自身点≥MinPts,p即为核心点。
3. 直接密度可达
q 从 p 直接密度可达⟺p 是核心点 且 q∈Nε(p);
解释:仅核心点能让其他点直接密度可达,边界点(噪声点)无法触发该关系。
4. 密度可达
若存在点序列,使得
从
直接密度可达(i=0,1,...,m−1),则称q从p密度可达。
记为p⟶ε,MinPtsq
解释:直接密度可达的传递闭包,核心点 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 类的初始化方法
- 超参数仅
eps和min_pts,是 DBSCAN 的核心; labels_遵循 sklearn 接口规范,方便用户习惯;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))
模块四:获取 ε- 邻域
- 核心辅助方法:根据索引找到目标点的所有 ε- 邻域点索引,存储索引比存储点本身更高效,尤其高维数据;
- 输入
X是n×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定义密度,将点分为核心点、边界点、噪声点;
- 聚类由核心点及其所有密度可达的点组成,边界点依附核心点,噪声点无核心点支撑;
- 核心逻辑是核心点的 ε- 邻域扩展,将所有密度相连的点纳入同一个聚类。
感谢大家的观看!如果有不足,请大家的批评指正!
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)