基于广度优先搜索(BFS)实现DBSCAN聚类算法

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)

效果如下:

对比1
对比2
可以看出自己手动实现的DBSCAN聚类评估器和sklearn库中的评估器效果完全相同。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,以下是一个完整的 DBSCAN 聚类算法的 Python 代码实现: ```python import numpy as np from sklearn.neighbors import NearestNeighbors class DBSCAN: def __init__(self, eps=0.5, min_samples=5): self.eps = eps self.min_samples = min_samples def fit(self, X): n_samples = X.shape[0] labels = np.zeros(n_samples) visited = np.zeros(n_samples, dtype=bool) core_indices = self._get_core_indices(X) cluster_label = 0 for i in range(n_samples): if visited[i]: continue if i in core_indices: cluster_label += 1 self._expand_cluster(X, visited, labels, core_indices, i, cluster_label) else: labels[i] = -1 return labels def _expand_cluster(self, X, visited, labels, core_indices, index, cluster_label): visited[index] = True labels[index] = cluster_label neighbors = self._get_neighbors(X, index) if len(neighbors) < self.min_samples: return for neighbor_index in neighbors: if not visited[neighbor_index]: visited[neighbor_index] = True neighbor_label = labels[neighbor_index] if neighbor_index in core_indices and neighbor_label == 0: self._expand_cluster(X, visited, labels, core_indices, neighbor_index, cluster_label) else: labels[neighbor_index] = cluster_label def _get_neighbors(self, X, index): nbrs = NearestNeighbors(n_neighbors=self.min_samples, metric='euclidean').fit(X) distances, indices = nbrs.kneighbors([X[index]]) return indices[0].tolist() def _get_core_indices(self, X): nbrs = NearestNeighbors(n_neighbors=self.min_samples, metric='euclidean').fit(X) distances, indices = nbrs.kneighbors(X) core_indices = [] for i in range(len(X)): if len(distances[i]) >= self.min_samples: core_indices.append(i) return core_indices ``` 其中,`eps` 和 `min_samples` 分别表示 DBSCAN 算法的两个超参数,即邻域半径和最小样本数。在 `fit` 方法中,首先获取所有核心点的索引,然后遍历所有未访问过的样本点,如果该点是核心点,则将其标记为一个新的聚类,并通过 BFS广度优先搜索)遍历所有与该点密度可达的样本点,并将它们分配到该聚类中。最终返回所有样本点的聚类标签。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

#冷咖啡离开了杯垫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值