DBSCAN聚类是试图通过寻找特征空间中点的分布密度较低的区域作为边界,并进一步以此划分数据集。正是因为以低密度区域作为边界,DBSCAN最终对数据的划分边界很有可能是不规则的,从而突破了K-Means依据中心点划分数据集从而使得边界是凸型的限制。
在DBSCAN中,我们通过两个概念和密度密切相关:
- 半径(eps)
- 半径范围内点的个数(num_samples)。
对于数据集中任意一个点,只要给定一个eps,就能算出对应的num_samples,例如对于下述A点,在一个eps范围内,num_samples为7(包括自己)。
eps越小、num_samples越大,则说明该点所在区域密度较高。当然,我们可以据此设置一组参数,即半径(eps)和半径范围内至少包含多少点(min_samples)作为评估指标,来对数据集中不同的点进行密度层面的分类:例如我们令eps=Eps(某个数),min_samples=6,并且如果某点在一个Eps范围内包含的点的个数大于min_samples,则称该点为核心点(core point),如下图中的A点;而如果某个点不是核心点,但是在某个核心点的一个eps领域内,则称该点为边界点,例如下图B点;而如果某点既不是核心点也不是边界点,则成该点为噪声点,如下图的C点。
在迭代的过程中,核心点、边界点和噪声点是相对的,即每个点都有可能成为核心点。当将一个点作为核心点时,在其eps范围内的点都成为其边界点,而不在eps范围内的点都成为其噪声点。核心点和边界点是**共用一个标签(label)**的。
我们对数据集中所有的数据点进行广度优先遍历(BFS),算法流程如下:
BFS实现DBSCAN代码部分:
from queue import Queue
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_circles
from sklearn.cluster import DBSCAN as DBSCAN2
class DBSCAN:
def __init__(self, min_samples=10, eps=0.15):
self.min_samples = min_samples # 半径范围内至少包含多少点
self.eps = eps # 半径
self.dataset = None # 数据集
self.num_examples = None # 数据集中样本的个数(行数)
self.num_features = None # 数据集中特征的个数(列数)
self.n_class = 0
# 聚类标签
self.labels_ = None # 聚类的标签
def fit(self, dataset):
'''
利用广度优先搜索(BFS)进行聚类,并将聚类标签存入labels_
Parameters
----------
dataset : np.array(num_examples * num_features)
聚类数据集
Returns
-------
None.
'''
self.dataset = dataset
self.n_class = 0 # 簇的个数初始化为0(认为噪声点的类别也都为0,即循环开始时所有点都是噪声点)
self.labels_ = np.zeros(dataset.shape[0]) # labels_初始化为一个全0向量 np.array(num_examples * 1)
queue = Queue() # 创建一个空队列
# 遍历dataset中的每一行数据
for i, data in enumerate(dataset):
if self.labels_[i] == 0:
num_samples = np.argwhere((np.sqrt(np.sum((dataset - data) ** 2, axis=1)) <= self.eps)).flatten().shape[0]
# num_samples: 半径eps范围内点的个数
if num_samples >= self.min_samples: # 如果第i行数据的“边界点”数量大于min_samples,则他们可以聚为一类,否则i及其“边界点”不能划为一类
self.n_class += 1 # 若i及其“边界点”可以划为一类,n_class加1,并以i为起点进行BFS操作
queue.put(i) # 寻找到某一类的第一个点初始标签一定为0,将其入队
while not queue.empty():
core = queue.get() # core为取出的某“核心点”
mark = np.argwhere(np.sqrt(np.sum((dataset - dataset[core]) ** 2, axis=1)) <= self.eps).flatten()
# mark为核心点core周围所有“边界点”的索引
if mark.shape[0] >= self.min_samples:
neighbours_of_core = np.argwhere((np.sqrt(np.sum((dataset - dataset[core]) ** 2, axis=1)) <= self.eps) & (self.labels_ == 0)).flatten()
# neighbours_of_core为core的所有(未添加标签的)“边界点”
self.labels_[mark] = self.n_class
for neighbour in neighbours_of_core:
queue.put(neighbour) # 将所有(未添加标签的)“边界点”入队,下一次以它们为“核心点”进行搜索
分别使用sklearn算法库中的创建环形和月牙形的数据集构造函数进行检验
if __name__ == '__main__':
# 创建数据集(环形、月牙形)
# dataset, labels = make_circles(n_samples=1000, factor=0.07, noise=0.1)
dataset, labels = make_moons(n_samples=500, noise=0.10)
plt.figure(figsize=(8,4), dpi=300)
# 实例化手写实现DBSCAN
db_my = DBSCAN(eps=0.15, min_samples=4)
db_my.fit(dataset)
plt.subplot(121)
plt.scatter(dataset[:,0], dataset[:,1], c=db_my.labels_)
plt.title("DBSCAN of Mine", fontsize=13)
# 实例化sklearn中的DBSCAN
db_sklearn = DBSCAN2(eps=0.15, min_samples=4)
db_sklearn.fit(dataset)
plt.subplot(122)
plt.scatter(dataset[:,0], dataset[:,1], c=db_sklearn.labels_)
plt.title("DBSCAN of Sklearn", fontsize=13)
效果如下:
可以看出自己手动实现的DBSCAN聚类评估器和sklearn库中的评估器效果完全相同。