四种常用聚类及代码(四):DBSCAN

本文介绍了密度聚类算法DBSCAN,包括其基本概念如核心对象、边界点、密度可达等。DBSCAN通过密度可达关系寻找最大密度相连的样本集合形成聚类簇,能发现任意形状的聚类并找出异常点。然而,参数(半径和MinPts)的选择较为困难,对不均匀密度数据集的聚类效果可能不佳。文章提供了DBSCAN的Python和C++实现参考链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


DBSCAN(Density-Based Spatial Clustering of Applications with Noise,具有噪声的基于密度的聚类方法)是一种很典型的密度聚类算法,和K-Means,BIRCH这些一般只适用于凸样本集的聚类相比,DBSCAN既可以适用于凸样本集,也可以适用于非凸样本集。
该算法将具有足够密度的区域划分为簇,并在具有噪声的空间数据库中发现任意形状的簇,它将簇定义为密度相连的点的最大集合。
算法的目标:
相比基于划分的聚类方法和层次聚类方法,需要更少的领域知识来确定输入参数;
发现任意形状的聚簇;
在大规模数据库上更好的效率。

1、什么是密度聚类

密度聚类算法一般假定类别可以通过样本分布的紧密程度决定。同一类别的样本,他们之间的紧密相连的,也就是说,在该类别任意样本周围不远处一定有同类别的样本存在。

通过将紧密相连的样本划为一类,这样就得到了一个聚类类别。通过将所有各组紧密相连的样本划为各个不同的类别,则我们就得到了最终的所有聚类类别结果。

2、一些基本概念

DBSCAN是基于一组邻域来描述样本集的紧密程度的,参数(ϵ, MinPts)用来描述邻域的样本分布紧密程度。其中,ϵ描述了某一样本的邻域距离阈值,MinPts描述了某一样本的距离为ϵ的邻域中样本个数的阈值。

假设我的样本集是 D = ( x 1 , x 2 , . . . , x m ) D=(x_1,x_2,...,x_m) D=(x1,x2,...,xm),则DBSCAN具体的密度描述定义如下:

ϵ-邻域:对于xj∈D,其ϵ-邻域包含样本集D中与xj的距离不大于ϵ的子样本集,即Nϵ(xj)={xi∈D|distance(xi,xj)≤ϵ}, 这个子样本集的个数记为|Nϵ(xj)|

核心对象:对于任一样本xj∈D,如果其ϵ-邻域对应的Nϵ(xj)至少包含MinPts个样本,即如果|Nϵ(xj)|≥MinPts,则xj是核心对象。 
边界点:设 x ϵ D x \epsilon D xϵD,且x落在某个核心点的 ϵ \epsilon ϵ 邻域内。一个边界点可能落在多个核心点的 ϵ \epsilon ϵ 邻域内。

密度直达:如果 x i 位 于 x j x_i位于x_j xixj的ϵ-邻域中,且 x j x_j xj是核心对象,则称 x i 由 x j x_i由x_j xixj密度直达。注意反之不一定成立,即此时不能说xj由xi密度直达, 除非且xi也是核心对象。

密度可达:对于xi和xj,如果存在样本样本序列 p 1 , p 2 , . . . , p T p_1,p_2,...,p_T p1,p2,...,pT,满足 p 1 = x i , p T = x j p_1=x_i,p_T=x_j p1=xi,pT=xj, 且 p t + 1 由 p t 密 度 直 达 , 则 称 x j 由 x i p_{t+1}由p_t密度直达,则称x_j由x_i pt+1ptxjxi密度可达。也就是说,密度可达满足传递性。此时序列中的传递样本 p 1 , p 2 , . . . , p T − 1 p_1,p_2,...,p_{T-1} p1,p2,...,pT1均为核心对象,因为只有核心对象才能使其他样本密度直达。注意密度可达也不满足对称性,这个可以由密度直达的不对称性得出。

密度相连:对于xi和xj,如果存在核心对象样本xk,使xi和xj均由xk密度可达,则称xi和xj密度相连。注意密度相连关系是满足对称性的。
类簇:设非空集合 C ⊂ X C\subset X CX,若满足: ∀ p , q \forall p,q p,q
(1) p ∈ C p\in C pC,且q从p密度可达,那么 q ∈ C q\in C qC
(2)p和q密度相连。
则称C构成一个类簇。

从下图可以很容易看出理解上述定义,图中MinPts=5,红色的点都是核心对象,因为其ϵ-邻域至少有5个样本。黑色的样本是非核心对象。所有核心对象密度直达的样本在以红色核心对象为中心的超球体内,如果不在超球体内,则不能密度直达。图中用绿色箭头连起来的核心对象组成了密度可达的样本序列。在这些密度可达的样本序列的ϵ-邻域内所有的样本相互都是密度相连的。
    在这里插入图片描述

3、DBSCAN聚类算法

DBSCAN的聚类定义很简单:由密度可达关系导出的最大密度相连的样本集合,即为我们最终聚类的一个类别,或者说一个簇。

这个DBSCAN的簇里面可以有一个或者多个核心对象。如果只有一个核心对象,则簇里其他的非核心对象样本都在这个核心对象的ϵ-邻域里;如果有多个核心对象,则簇里的任意一个核心对象的ϵ-邻域中一定有一个其他的核心对象,否则这两个核心对象无法密度可达。这些核心对象的ϵ-邻域里所有的样本的集合组成的一个DBSCAN聚类簇。

那么怎么才能找到这样的簇样本集合呢?DBSCAN使用的方法很简单,它任意选择一个没有类别的核心对象作为种子,然后找到所有这个核心对象能够密度可达的样本集合,即为一个聚类簇。接着继续选择另一个没有类别的核心对象去寻找密度可达的样本集合,这样就得到另一个聚类簇。一直运行到所有核心对象都有类别为止。

基本上这就是DBSCAN算法的主要内容了,是不是很简单?但是我们还是有三个问题没有考虑。

第一个是一些异常样本点或者说少量游离于簇外的样本点,这些点不在任何一个核心对象在周围,在DBSCAN中,我们一般将这些样本点标记为噪音点。

第二个是距离的度量问题,即如何计算某样本和核心对象样本的距离。在DBSCAN中,一般采用最近邻思想,采用某一种距离度量来衡量样本距离,比如欧式距离。这和KNN分类算法的最近邻思想完全相同。对应少量的样本,寻找最近邻可以直接去计算所有样本的距离,如果样本量较大,则一般采用KD树或者球树来快速的搜索最近邻。KD树,球树

第三种问题比较特殊,某些样本可能到两个核心对象的距离都小于ϵ,但是这两个核心对象由于不是密度直达,又不属于同一个聚类簇,那么如果界定这个样本的类别呢?一般来说,此时DBSCAN采用先来后到,先进行聚类的类别簇会标记这个样本为它的类别。也就是说DBSCAN的算法不是完全稳定的算法。
DBSCAN的核心思想是从某个核心点出发,不断向密度可达的区域扩张,从而得到一个包含核心点和边界点的最大化区域,区域中任意两点密度相连。
算法流程(伪代码)如下:

DBSCAN(D, eps, MinPts)
   C = 0                                          
   for each unvisited point P in dataset D        
      mark P as visited                           
      NeighborPts = regionQuery(P, eps)      //计算这个点的邻域    
      if sizeof(NeighborPts) < MinPts             
         mark P as NOISE                          
      else                                        
         C = next cluster                   //作为核心点,根据该点创建一个新类簇
         expandCluster(P, NeighborPts, C, eps, MinPts)   //根据该核心点扩展类别
          
expandCluster(P, NeighborPts, C, eps, MinPts)
   add P to cluster C                            //扩展类别,核心点先加入
   for each point P' in NeighborPts                    
      if P' is not visited
         mark P' as visited                              
         NeighborPts' = regionQuery(P', eps)    //如果该点为核心点,则扩充该类别
         if sizeof(NeighborPts') >= MinPts
            NeighborPts = NeighborPts joined with NeighborPts'
      if P' is not yet member of any cluster   //如果邻域内点不是核心点,并且无类别,比如噪音数据,则加入此类别
         add P' to cluster C
          
regionQuery(P, eps)                                       //计算点P的邻域
   return all points within P's eps-neighborhood

这里的regionQuery函数的作用计算点的邻域,是比较耗费时间的操作,不进行任何优化时,算法的时间复杂度是 O ( N 2 ) O(N^{2}) O(N2),通常可利用R-tree,k-d tree, ball tree索引来加速计算,将算法的时间复杂度降为 O ( N l o g ( N ) ) O(Nlog(N)) O(Nlog(N))

4、DBSCAN参数

DBSCAN算法的两个参数,这两个参数比较难指定,公认的指定方法简单说一下:

半径:半径是最难指定的 ,大了,圈住的就多了,簇的个数就少了;反之,簇的个数就多了,这对我们最后的结果是有影响的。我们这个时候K距离可以帮助我们来设定半径r,也就是要找到突变点,比如:

在这里插入图片描述
以上虽然是一个可取的方式,但是有时候比较麻烦 ,大部分还是都试一试进行观察,用k距离需要做大量实验来观察,很难一次性把这些值都选准。

MinPts:这个参数就是圈住的点的个数,也相当于是一个密度,一般这个值都是偏小一些,然后进行多次尝试

5、优缺点

和传统的K-Means算法相比,DBSCAN最大的不同就是不需要输入类别数k,当然它最大的优势是可以发现任意形状的聚类簇,而不是像K-Means,一般仅仅使用于凸的样本集聚类。同时它在聚类的同时还可以找出异常点,这点和BIRCH算法类似。

那么我们什么时候需要用DBSCAN来聚类呢?一般来说,如果数据集是稠密的,并且数据集不是凸的,那么用DBSCAN会比K-Means聚类效果好很多。如果数据集不是稠密的,则不推荐用DBSCAN来聚类。

下面对DBSCAN算法的优缺点做一个总结。

DBSCAN的主要优点有:

1) 可以对任意形状的稠密数据集进行聚类,相对的,K-Means之类的聚类算法一般只适用于凸数据集。

2) 可以在聚类的同时发现异常点,对数据集中的异常点不敏感。

3) 聚类结果没有偏倚,相对的,K-Means之类的聚类算法初始值对聚类结果有很大影响。

DBSCAN的主要缺点有:

1)如果样本集的密度不均匀、聚类间距差相差很大时,聚类质量较差,这时用DBSCAN聚类一般不适合。

2) 如果样本集较大时,聚类收敛时间较长,此时可以对搜索最近邻时建立的KD树或者球树进行规模限制来改进。

3) 调参相对于传统的K-Means之类的聚类算法稍复杂,主要需要对距离阈值ϵ,邻域样本数阈值MinPts联合调参,不同的参数组合对最后的聚类效果有较大影响。

python实现

# visitlist类用于记录访问列表
 2 # unvisitedlist记录未访问过的点
 3 # visitedlist记录已访问过的点
 4 # unvisitednum记录访问过的点数量
 5 class visitlist:
 6     def _init_(self, count=0):
 7         self.unvisitedlist=[i for i in range(count)]
 8         self.visitedlist=list()
 9         self.unvisitednum=count
10 
11     def visit(self, pointId):
12         self.visitedlist.append(pointId)
13         self.unvisitedlist.remove(pointId)
14         self.unvisitednum -= 1
import numpy as np
 2 import matplotlib.pyplot as plt
 3 import math
 4 import random
 5 
 6 def  dist(a, b):
 7     # 计算a,b两个元组的欧几里得距离
 8     return math.sqrt(np.power(a-b, 2).sum())
 9 
10 def dbscan(dataSet, eps, minPts):
11     # numpy.ndarray的 shape属性表示矩阵的行数与列数
12     nPoints = dataSet.shape[0]
13     # (1)标记所有对象为unvisited
14     # 在这里用一个类vPoints进行买现
15     vPoints = visitlist(count=nPoints)
16     # 初始化簇标记列表C,簇标记为 k
17     k = -1
18     C = [-1 for i in range(nPoints)]
19     while(vPoints.unvisitednum > 0):
20         # (3)随机上选择一个unvisited对象p
21         P = random.choice(vPoints.unvisitedlist)
22         # (4)标记p为visited
23         vPoints.visit(p)
24         # (5)if p的$\varepsilon$-邻域至少有MinPts个对象
25         # N是p的$\varepsilon$-邻域点列表
26         N = [i for i in range(nPoints) if dist(dataSet[i], dataSet[p])<= eps]
27         if  len(N) >= minPts:
28             # (6)创建个新簇C,并把p添加到C
29             # 这里的C是一个标记列表,直接对第p个结点进行赋植
30             k += 1
31             C[p]=k
32             # (7)令N为p的ε-邻域中的对象的集合
33             # N是p的$\varepsilon$-邻域点集合
34             # (8) for N中的每个点p'
35             for p1 in N:
36                 # (9) if p'是unvisited
37                 if p1 in vPoints.unvisitedlist:
38                     # (10)标记p’为visited
39                     vPoints.visit(p1)
40                     # (11) if p'的$\varepsilon$-邻域至少有MinPts个点,把这些点添加到N
41                     # 找出p'的$\varepsilon$-邻域点,并将这些点去重添加到N
42                     M=[i for i in range(nPoints) if dist(dataSet[i], \
43                         dataSet[p1]) <= eps]
44                     if len(M) >= minPts:
45                         for i in M:
46                             if i not in N:
47                                 N.append(i)
48                     # (12) if p'还不是任何簇的成员,把P'添加到C
49                     # C是标记列表,直接把p'分到对应的簇里即可
50                     if  C[p1] == -1:
51                         C[p1]= k
52         # (15)else标记p为噪声
53         else:
54             C[p]=-1
55 
56     # (16)until没有标t己为unvisitedl内对象
57     return C

c++实现
。。。
参考https://www.cnblogs.com/pinard/p/6208966.html
(https://www.cnblogs.com/tiaozistudy/p/dbscan_algorithm.html)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值