聚类算法学习之HDBSCAN

目录

DBSCAN

DBSCAN算法流程

DBSCAN优缺点总结

HDBSCAN

建立最小生成树

构建簇层次结构

提取簇

HDBSCAN使用实例

 参数选择

参考资料:


 

DBSCAN

先前的文章中介绍了基于密度的聚类方法DBSCAN。

在DBSCAN算法中,还定义了如下一些概念:

  • 密度直达(directly density-reachable):我们称样本点 p 是由样本点 q 对于参数 {Eps,MinPts} 密度直达的,如果它们满足 p∈NEps(q) 且 |NEps(q)|≥MinPts (即样本点 q 是核心点)
  • 密度可达(density-reachable):我们称样本点 p 是由样本点 q 对于参数{Eps,MinPts}密度可达的,如果存在一系列的样本点 p1,…,pn(其中 p1=q,pn=p)使得对于i=1,…,n−1,样本点 pi+1 可由样本点 pi 密度可达
  • 密度相连(density-connected):我们称样本点 p 与样本点 q 对于参数 {Eps,MinPts} 是密度相连的,如果存在一个样本点 o,使得 p 和 q 均由样本点 o 密度可达。

聚类算法学习之HDBSCAN_第1张图片

基于密度的聚类算法通过寻找被低密度区域分离的高密度区域,并将高密度区域作为一个聚类的“簇”。在DBSCAN算法中,聚类“簇”定义为:由密度可达关系导出的最大的密度连接样本的集合。 

DBSCAN算法流程

在DBSCAN算法中,由核心对象出发,找到与该核心对象密度可达的所有样本形成“簇”。DBSCAN算法的流程为:

  • 根据给定的邻域参数Eps和MinPts确定所有的核心对象
  • 对每一个核心对象
    • 选择一个未处理过的核心对象,找到由其密度可达的的样本生成聚类“簇”
  • 重复以上过程

 伪代码:

 首先将数据集D中的所有对象标记为未处理状态  
 for(数据集D中每个对象p) do  
    if (p已经归入某个簇或标记为噪声) then  
         continue;  
    else  
         检查对象p的Eps邻域 NEps(p) ;  
         if (NEps(p)包含的对象数小于MinPts) then  
                  标记对象p为边界点或噪声点;  
         else  
                 标记对象p为核心点,并建立新簇C, 并将p邻域内所有点加入C  
                 for (NEps(p)中所有尚未被处理的对象q)  do  
                       检查其Eps邻域NEps(q),若NEps(q)包含至少MinPts个对象,则将NEps(q)中未归入任何一个簇的对象加入C;  
                 end for  
        end if  
    end if  
 end for

python代码:

# -*- coding: utf-8 -*-
import numpy as np
 
 
def distance(data):
    '''计算样本点之间的距离
    :param data(mat):样本
    :return:dis(mat):样本点之间的距离
    '''
    m, n = np.shape(data)
    dis = np.mat(np.zeros((m, m)))
    for i in range(m):
        for j in range(i, m):
            # 计算i和j之间的欧式距离
            tmp = 0
            for k in range(n):
                tmp += (data[i, k] - data[j, k]) * (data[i, k] - data[j, k])
            dis[i, j] = np.sqrt(tmp)
            dis[j, i] = dis[i, j]
    return dis
 
 
def find_eps(distance_D, eps):
    '''找到距离≤eps的样本的索引
    :param distance_D(mat):样本i与其他样本之间的距离
    :param eps(float):半径的大小
    :return: ind(list):与样本i之间的距离≤eps的样本的索引
    '''
    ind = []
    n = np.shape(distance_D)[1]
    for j in range(n):
        if distance_D[0, j] <= eps:
            ind.append(j)
    return ind
 
 
def dbscan(data, eps, MinPts):
    '''DBSCAN算法
    :param data(mat):需要聚类的数据集
    :param eps(float):半径
    :param MinPts(int):半径内最少的数据点数
    :return:
        types(mat):每个样本的类型:核心点、边界点、噪音点
        sub_class(mat):每个样本所属的类别
    '''
    m = np.shape(data)[0]
    # 在types中,1为核心点,0为边界点,-1为噪音点
    types = np.mat(np.zeros((1, m)))
    sub_class = np.mat(np.zeros((1, m)))
    # 用于判断该点是否处理过,0表示未处理过
    dealt = np.mat(np.zeros((m, 1)))
    # 计算每个数据点之间的距离
    dis = distance(data)
    # 用于标记类别
    number = 1
 
    # 对每一个点进行处理
    for i in range(m):
        # 找到未处理的点
        if dealt[i, 0] == 0:
            # 找到第i个点到其他所有点的距离
            D = dis[i,]
            # 找到半径eps内的所有点
            ind = find_eps(D, eps)
            # 区分点的类型
            # 边界点
            if len(ind) > 1 and len(ind) < MinPts + 1:
                types[0, i] = 0
                sub_class[0, i] = 0
            # 噪音点
            if len(ind) == 1:
                types[0, i] = -1
                sub_class[0, i] = -1
                dealt[i, 0] = 1
            # 核心点
            if len(ind) >= MinPts + 1:
                types[0, i] = 1
                for x in ind:
                    sub_class[0, x] = number
                # 判断核心点是否密度可达
                while len(ind) > 0:
                    dealt[ind[0], 0] = 1
                    D = dis[ind[0],]
                    tmp = ind[0]
                    del ind[0]
                    ind_1 = find_eps(D, eps)
 
                    if len(ind_1) > 1:  # 处理非噪音点
                        for x1 in ind_1:
                            sub_class[0, x1] = number
                        if len(ind_1) >= MinPts + 1:
                            types[0, tmp] = 1
                        else:
                            types[0, tmp] = 0
 
                        for j in range(len(ind_1)):
                            if dealt[ind_1[j], 0] == 0:
                                dealt[ind_1[j], 0] = 1
                                ind.append(ind_1[j])
                                sub_class[0, ind_1[j]] = number
                number += 1
 
    # 最后处理所有未分类的点为噪音点
    ind_2 = ((sub_class == 0).nonzero())[1]
    for x in ind_2:
        sub_class[0, x] = -1
        types[0, x] = -1
 
    return types, sub_class

 

DBSCAN优缺点总结

优点:

  • 相比K-Means,DBSCAN 不需要预先声明聚类数量。
  • 可以对任意形状的稠密数据集进行聚类,相对的,K-Means之类的聚类算法一般只适用于凸数据集。
  • 可以在聚类的同时发现异常点,对数据集中的异常点不敏感。
  • 聚类结果没有偏倚,相对的,K-Means之类的聚类算法初始值对聚类结果有很大影响。

缺点:

  • 当空间聚类的密度不均匀、聚类间距差相差很大时,聚类质量较差,因为这种情况下参数MinPts和Eps选取困难。
  • 如果样本集较大时,聚类收敛时间较长,此时可以对搜索最近邻时建立的KD树或者球树进行规模限制来改进。
  • 在两个聚类交界边缘的点会视乎它在数据库的次序决定加入哪个聚类,幸运地,这种情况并不常见,而且对整体的聚类结果影响不大(DBSCAN*变种算法,把交界点视为噪音,达到完全决定性的结果。)
  • 调参相对于传统的K-Means之类的聚类算法稍复杂,主要需要对距离阈值eps,邻域样本数阈值MinPts联合调参,不同的参数组合对最后的聚类效果有较大影响。

HDBSCAN

今天要学习的是HDBSCAN。单从名字上看,两者必然存在一定的关系。我们先来看看官方的介绍:

HDBSCAN – Hierarchical Density-Based Spatial Clustering of Applications with Noise. Performs DBSCAN over varying epsilon values and integrates the result to find a clustering that gives the best stability over epsilon. This allows HDBSCAN to find clusters of varying densities (unlike DBSCAN), and be more robust to parameter selection.

从介绍中我们可以知道是DBSCAN算法与基于层次聚类算法结合而来的。DBSCAN算法的原理是:对于聚类中的每个对象,在给定的半径邻域内的数据对象必须超过某个阀值。其算法简洁,对噪声点不敏感,而且可以发现任意形状的簇,但还是存在不足之处:

  • 由于需要在整个数据空间构建树,算法需要很大的IO开销
  • 算法输入参数没有一个很完美的科学标准来作为参考,这就使得人为干扰的因素变得很大,参数选取略有偏差对于聚类的效果有时会呈现出完全不同的效果

HDBSCAN算法是对OPTICS算法的一种改进,但并不是没有缺点。比如其对于边界点的处理方面效果却不是很理想。

HDBSCAN的使用方式

import hdbscan
 
clusterer = hdbscan.HDBSCAN(min_cluster_size=5, gen_min_span_tree=True)
clusterer.fit(test_data)

 上述代码非常的简单,但中间可以把它拆成如下几个步骤:

  • 根据密度/稀疏度对空间进行变换
  • 建立距离加权图的最小生成树
  • 构造连接组件的簇层次结构
  • 根据最小的簇大小压缩簇层次结构
  • 从压缩树中提取稳定的簇

 

为了找到簇,我们希望在一片稀疏的噪音海洋中找到密度更高的孤岛。 聚类算法的核心是单链接聚类,它对噪声非常敏感: 一个位于错误位置的单个噪声数据点可以充当岛屿之间的桥梁,将它们粘合在一起。 显然,我们希望我们的算法对噪声是鲁棒的,所以我们需要找到一种方法,以帮助”降低海平面”之前运行一个单一的连接算法。

我们如何在不进行聚类的情况下描述“海洋”和“陆地”?我们只要能够得到一个密度的估计,我们就可以把密度较低的点看作是“海洋”。 这里的目标不是完全区分”海洋”和”陆地”,只是为了使我们的簇核心对噪音更加健壮。 因此,鉴于”海洋”的定义,我们希望降低海平面。就实际目的而言,这意味着使”海洋”中的点彼此之间和”陆地”之间的距离更远。

然而,这只是设想。它在实践中是如何工作的?我们需要一个非常低成本的密度估计,最简单的是到 kth 最近邻距离。将其称为为针对点 x 的参数 k 定义的核心距离(定义为当前点到其第k近的点的距离),并表示为:

  

现在我们需要一种方法,以低密度(相应的高核心距离)分散点。要做到这一点,简单的方法是定义一个新的点之间的距离度量,我们将调用相互可达距离。 我们将相互可达距离定义如下:

式中,d(a,b)是a与b的原始距离。在该式中密集点(核心距离较低)彼此保持相同的距离,但较稀疏的点被推开,以使其核心距离至少远离任何其他点。 这实际上”降低了海平面”,稀疏的”海洋”指向外界,而”陆地”则没有受到影响。这里需要注意的是,这显然取决于k的选择,较大的k值将更多的点解释为处于“海洋”中。所有这些用一张图片来说都比较容易理解,我们使用 k 值为5,然后对于给定的一个点,我们可以画一个核心距离的圆,作为与第六个最近邻接触的圆(包括点本身) ,如下所示:

聚类算法学习之HDBSCAN_第2张图片

再选择另外一个点,我们可以做同样的事情,这一次用一组不同的邻居(其中一个甚至包含我们选择的第一个点):

聚类算法学习之HDBSCAN_第3张图片

我们可以再用另一组六个最近邻,和另一个半径略有不同的圆:

聚类算法学习之HDBSCAN_第4张图片

现在,如果我们想知道蓝点和绿点之间的相互可达距离,我们可以先画一个箭头,给出绿点和蓝点之间的距离:

聚类算法学习之HDBSCAN_第5张图片

它穿过蓝色的圆圈,但不是绿色的圆圈——绿色的核心距离大于蓝色和绿色之间的距离。因此,我们需要将蓝色和绿色之间的相互可达距离标记为大于等于绿色圆的半径。另外,从红色到绿色的相互反应距离就是从红色到绿色的距离,因为这个距离大于两个核心距离:

聚类算法学习之HDBSCAN_第6张图片

一般来说,有潜在的理论来证明,相互可达距离作为一种变换,可以很好地允许单链接聚类更接近水平集的层次结构,无论我们采样的点的实际密度分布是什么。

建立最小生成树

现在我们在数据上有了一个新的相互可达性度量,我们希望开始在稠密数据上寻找孤岛。 当然,密集区域是相对的,不同的岛屿可能有不同的密度。 从概念上讲,我们将要做的是: 将数据看作一个加权图,其中数据点为顶点,任意两点之间的边的权重等于这些点之间的相互可达距离。

现在考虑一个阈值,从高开始,逐步降低。 删除任何重量超过该阈值的边。 当我们删除边时,我们将开始断开图形的连接组件。 最终,我们将在不同的阈值水平上得到一个连接组件的层次结构(从完全连接到完全不连接)。在实践中,这是非常低效的:我们有n^2个边,并且不期望连接的组件算法运算那么多次。正确的做法是找到一个最小的边集合,这样从集合中删除任何边都会导致组件断开。幸运的是,图论为我们提供了这样一个东西: 图的最小生成树。

我们可以通过 Prim 算法非常有效地构建最小生成树树-我们一次构建一条边,总是添加最小的权重边,将当前的树连接到树中还没有的顶点。您可以看到下面构造的HDBSCAN树。注意这是相互可达距离的最小生成树,它不同于图中的纯距离。 在这个例子中,k 值为5。

构建簇层次结构

给定最小生成树,下一步是将其转换为连接组件的层次结构。这很容易以相反的顺序完成:根据距离对树的边进行排序(按增加的顺序),然后遍历,为每条边创建一个新的合并的簇。这里唯一困难的部分是确定每个将2个簇接在一起的边,但可以通过联合查找数据结构很容易实现。

压缩簇层次结构

簇抽取的第一步是将庞大而复杂的簇层次结构压缩到一个更小的树中。正如上面的层次结构中看到的,通常情况下簇拆分是从一个簇中分离出一个或两个点,而不是将其视为一个簇拆分为两个新的簇。为了使这个具体化,我们需要一个最小簇大小的概念,我们将它作为HDBSCAN的一个参数。一旦我们有了最小簇大小的值,我们现在就可以遍历层次结构,并在每次分割时询问是否有一个由分割创建的新簇的点数少于最小簇大小。如果我们有少于最小的簇大小的点,我们声明它是’从簇中剔除的点’,并有较大的簇保留父簇的身份。另一方面,如果拆分为两个簇,每个簇至少与最小簇大小一样大,那么我们认为簇拆分就是让这个拆分保留在树中。在遍历了整个层次结构之后,我们最终得到了一个拥有少量节点的小得多的树,每个节点都有关于该节点的簇大小如何随着不同距离减小的数据.我们可以将其可视化为一个树状图,类似于上面的树状图,用线的宽度来表示簇中的点数。但是,当点被剔除时,该宽度随线的长度而变化。

提取簇

直观地说,我们希望选择的簇能够持续存在并且有更长的生命周期; 短命的簇可能仅仅是单链接方法的产物。在前面的图中,我们可以说,我们要选择那些簇有最大面积的情节油墨。 为了创建一个平面集群,我们需要添加一个进一步的要求,如果您选择了一个簇,那么您就不能选择它的后代的任何簇。事实上,关于应该做什么的直观概念正是HDBSCAN所做的。

参考链接:https://hdbscan.readthedocs.io/en/latest/how_hdbscan_works.html

HDBSCAN使用实例

import numpy as np
import pandas as pd
import hdbscan
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from math import pi, cos, sin, atan2, sqrt
 
 
def get_centroid(cluster):
    x = y = z = 0
    coord_num = len(cluster)
    for coord in cluster:
        lat = coord[0] * pi / 180
        lon = coord[1] * pi / 180
 
        a = cos(lat) * cos(lon)
        b = cos(lat) * sin(lon)
        c = sin(lat)
 
        x += a
        y += b
        z += c
    x /= coord_num
    y /= coord_num
    z /= coord_num
    lon = atan2(y, x)
    hyp = sqrt(x * x + y * y)
    lat = atan2(z, hyp)
    return [lat * 180 / pi, lon * 180 / pi]
 
 
df = pd.read_excel("test.xlsx")
 
hotel_df = df[['latitude', 'longitude']]
hotel_df = hotel_df.dropna(axis=0, how='any')
hotel_coord = hotel_df.values
 
hotel_dbsc = hdbscan.HDBSCAN(metric="haversine", min_cluster_size=int(len(hotel_df) / 50)).fit(np.radians(hotel_coord))
hotel_df['labels'] = hotel_dbsc.labels_
hotel_df['probab'] = hotel_dbsc.probabilities_
hotel_df.loc[hotel_df['probab'] < 0.5, 'labels'] = -1  # HDBSCAN边界可能存在问题,将置信度<0.5的设为为噪音点
 
cluster_list = hotel_df['labels'].value_counts(dropna=False)
center_coords = []
for index, item_count in cluster_list.iteritems():
    if index != -1:
        df_cluster = hotel_df[hotel_df['labels'] == index]
        center_coord = get_centroid(df_cluster[["latitude", "longitude"]].values)
        center_lat = center_coord[0]
        center_lon = center_coord[1]
        center_coords.append(center_coord)
center_coords = pd.DataFrame(center_coords, columns=['latitude', 'longitude'])
print(center_coords)
 
# 可视化
fig, ax = plt.subplots(figsize=[20, 12])
facility_scatter = ax.scatter(hotel_df['longitude'], hotel_df['latitude'], c=hotel_df['labels'], cmap=cm.Dark2,
                              edgecolor='None',
                              alpha=0.7, s=120)
centroid_scatter = ax.scatter(center_coords['longitude'], center_coords['latitude'], marker='x', linewidths=2,
                              c='k', s=50)
ax.set_title('Facility Clusters & Facility Centroid', fontsize=30)
ax.set_xlabel('Longitude', fontsize=24)
ax.set_ylabel('Latitude', fontsize=24)
ax.set_xlim(120, 122)
ax.set_ylim(30, 33)
ax.legend([facility_scatter, centroid_scatter], ['Facilities', 'Facility Cluster Centroid'], loc='upper right',
          fontsize=20)
plt.show()

 参数选择

min_cluster_size:一个类中至少要有min_cluster_size个样本,这个参数越大,最终的聚类种类数会越少。使用时必须设置大于1,否者会报错。

min_samples:一个点邻域范围内至少有min_samples个样本,才会被视为核心点;提供的min_samples的值越大,聚类越保守,将更多的点声明为噪声,并且聚类将被限制在逐渐密集的区域。

cluster_selection_epsilon:在某些情况下,我们希望选择一个较小的min_cluster_size,因为即使是很少点的组也可能对我们感兴趣。 但是,如果我们的数据集还包含对象集中度很高的分区,则此参数设置可能会导致大量的微簇。 为cluster_selection_epsilon选择一个值有助于我们合并这些区域中的集合。 换句话说,它确保了低于给定阈值的集合不会进一步分裂。

alpha:默认情况下,alpha设置为1.0。 增加alpha将使聚类更加保守,但范围会更紧密。
注意:调整alpha将导致重新计算单个链接树的难度。

 

 

参考资料:

https://hdbscan.readthedocs.io/en/latest/parameter_selection.html

项目地址:

  • https://github.com/lmcinnes/hdbscan
  • https://github.com/scikit-learn-contrib/hdbscan

官方文档:

  • https://hdbscan.readthedocs.io/en/latest/index.html

你可能感兴趣的:(algorithm)