哈喽,大家好~
最近有一个问题,聚类是不是一定要先告诉模型分几类?
还真不是,咱们今儿来聊聊DBSCAN。
DBSCAN最大的特点是:不需要提前指定聚类数量,还能把离群点单独识别出来。
理解 DBSCAN
假设我们把地图上的商场、居民区和工业区看成一堆散落的坐标点。
有些区域点特别密集,说明这里可能是一个真实的区域;有些点零零散散,可能只是异常位置。DBSCAN做的事情,就是:
从密集区域出发,把相互靠近的点连接起来,形成一个簇;太孤立的点就标记为噪声。
它不关心簇是不是圆形,所以对于月牙形、环形、弯曲形数据,DBSCAN通常比K-Means更自然。
DBSCAN中有三个重要概念:
-
核心点:在半径 内,至少有 min_samples个点。 -
边界点:自己附近不够密集,但落在某个核心点附近。 -
噪声点:既不是核心点,也不属于任何核心点的邻域。
一句话概括:核心点负责“生长”簇,边界点负责补充簇,噪声点被单独留下。
DBSCAN核心原理
对于样本点 ,它的 邻域可以写成:
这里的 表示距离 不超过 的所有点。
如果邻域内点的数量满足:
那么 就是核心点。
DBSCAN会从一个核心点开始,把它邻域里的点加入当前簇。如果邻域里的点也是核心点,就继续向外扩展,最终把一整片密集区域连接起来。
参数主要有两个:
-
eps:邻域半径,越大越容易把不同簇连接起来。 -
min_samples:成为核心点所需的最少邻居数量,越大,模型越严格。
所以参数不能乱设。eps太小,会产生大量噪声;eps太大,不同簇可能被粘在一起。
完整案例
这次我们构造三类数据:
-
两个月牙形数据; -
一个环形数据; -
一些随机噪声点。
这种数据并不是规则圆团,非常适合观察DBSCAN的优势。代码如下:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_circles
from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler
np.random.seed(42)
# 月牙数据
X1, _ = make_moons(n_samples=4500, noise=0.08, random_state=1)
X1 = X1 * [2.2, 1.5] + [-2.0, 0.2]
# 环形数据
X2, _ = make_circles(
n_samples=5500, factor=0.45,
noise=0.06, random_state=2
)
X2 = X2 * 2.3 + [2.2, 0.0]
# 随机噪声
noise = np.random.uniform([-4.8, -3.0], [4.8, 3.0], size=(80, 2))
X = np.vstack([X1, X2, noise])
X = StandardScaler().fit_transform(X)
# DBSCAN聚类
model = DBSCAN(eps=0.16, min_samples=8)
labels = model.fit_predict(X)
# 计算每个点到第k近邻的距离,用于辅助选择eps
k = 8
neighbors = NearestNeighbors(n_neighbors=k).fit(X)
distances, _ = neighbors.kneighbors(X)
k_dist = np.sort(distances[:, -1])
# 绘图
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# 图1:原始数据
axes[0].scatter(
X[:, 0], X[:, 1],
c="#00BFFF", s=28, alpha=0.8,
edgecolors="white", linewidths=0.4
)
axes[0].set_title("Original Virtual Data")
axes[0].set_xlabel("Feature 1")
axes[0].set_ylabel("Feature 2")
# 图2:DBSCAN聚类结果
unique_labels = sorted(set(labels))
colors = plt.cm.turbo(np.linspace(0, 1, len(unique_labels)))
for color, label in zip(colors, unique_labels):
mask = labels == label
if label == -1:
color = "#111111"
name = "Noise"
size = 42
marker = "x"
else:
name = f"Cluster {label}"
size = 30
marker = "o"
axes[1].scatter(
X[mask, 0], X[mask, 1],
c=[color], s=size, marker=marker,
alpha=0.85, label=name
)
axes[1].set_title("DBSCAN Clustering Result")
axes[1].set_xlabel("Feature 1")
axes[1].set_ylabel("Feature 2")
axes[1].legend()
# 图3:k距离曲线
axes[2].plot(k_dist, color="#FF1493", linewidth=2.5)
axes[2].axhline(
y=0.16, color="#00AA55",
linestyle="--", label="eps = 0.16"
)
axes[2].set_title("k-distance Curve")
axes[2].set_xlabel("Sorted Samples")
axes[2].set_ylabel("Distance to 8th Neighbor")
axes[2].legend()
plt.tight_layout()
plt.show()
print("聚类数量:", len(set(labels)) - (1 if -1 in labels else 0))
print("噪声点数量:", np.sum(labels == -1))
第一张图是原始数据。可以看到,样本包含弯曲的月牙、环状结构以及散落在外围的噪声点。它们并不是几个标准的圆形簇。
第二张图是DBSCAN结果。不同鲜艳颜色代表不同簇,黑色叉号代表噪声点。你会发现,DBSCAN可以沿着月牙和环形结构进行聚类,而不是简单地用一个圆形中心去切分。
第三张图是k距离曲线。我们把每个点到第8近邻的距离排序,曲线突然变陡的位置,通常可以作为eps的参考。曲线平缓区域对应密集样本,后面快速上升的部分往往对应边界点和噪声点。
这里的eps=0.16不是固定答案,而是结合数据尺度和曲线形状调出来的。我们先做了标准化,所以这个值是在标准化后的空间中使用的。
DBSCAN的优点和注意事项
DBSCAN的优点很明显:
-
不需要提前指定聚类数量; -
可以识别任意形状的簇; -
能够自动发现噪声点; -
对异常检测、地理坐标聚类比较实用。
但它也有局限。如果不同簇的密度差异特别大,同一个eps很难同时适应所有区域。另外,数据维度很高时,距离的区分度会下降,DBSCAN的效果也可能变差。
实际使用时,可以这样做:
-
先对特征进行标准化; -
用k距离曲线辅助选择 eps; -
尝试几个不同的 min_samples; -
观察噪声比例和聚类结果是否符合业务直觉。
总结
DBSCAN可以理解成一种“基于密度连接”的聚类算法:密集点形成簇,稀疏点变成噪声。它不需要提前指定类别数量,尤其适合月牙、环形等复杂形状的数据。
大家可以继续尝试:把这份代码中的eps改成0.08或0.30,观察噪声点和簇数量如何变化;也可以加入真实的经纬度数据,体验DBSCAN在地理位置聚类中的效果~

