机器学习-*-DBSCAN聚类及代码实现

版权声明:原创文章未经博主允许不得转载O(-_-)O!!! https://blog.csdn.net/u013894072/article/details/84585419

DBSCAN

DBSCAN(Density-Based Spatial Clustering of Applications with Noise,具有噪声的基于密度的聚类方法)

原理

首先描述以下几个概念,假设我们有数据集 D = { x 1 , x 2 , x 3 , . . . , x n } D=\{x_1,x_2,x_3,...,x_n\} ,则
1. ϵ \epsilon 邻域:对于 x i D \forall x_i\in D ,有 N ϵ ( x i ) = { x j d i s t a n c e ( x i , x j ) < ϵ , i j } N_{\epsilon}(x_i)=\{x_j|distance(x_i,x_j)<\epsilon,此处认为i可以等于j\} ,该邻域的数据个数为 C ( N ϵ ( x i ) ) C(N_{\epsilon}(x_i))
2.核心点:设最小邻域内个数为 m i n p t s minpts ,若一点 x i x_i ϵ \epsilon 邻域的 C ( N ϵ ( x i ) ) > = m i n p t s C(N_{\epsilon}(x_i))>=minpts ,则称该点为核心点
3.边界点:若一点 x i x_i ϵ \epsilon 邻域内 C ( N ϵ ( x i ) ) < m i n p t s C(N_{\epsilon}(x_i))<minpts ,且处于核心点邻域内的点,则称该点为边界点
4.噪声点:不是核心点和边界点的其他点为噪声点。
5.密度直达:如果 x i x_i 在核心点 x j x_j 的邻域内,则它俩就是密度直达的
6.密度可达:若有核心对象序列 k 1 , k 2 , k 3 , . . . , k m k_1,k_2,k_3,...,k_m ,若 k t + 1 k_{t+1} 均可以由 k t k_t 密度直达,则称 k 1 k_1 k m k_m 密度可达
密度直达和密度可达是不对称的,不能绝对的反向导出,参见参考文献1
7.密度相连:有点 x i x_i x j x_j ,若他们与核心对象 x k x_k 密度可达,那么他们俩是密度相连的。这个概念可以对称的。
下边是网上截取的相关示例
在这里插入图片描述

聚类流程

首先阐述一下聚类的思想:以某一核心点为基点,根据密度可达关系、密度相连关系,导出最大样本集类簇,此为第一个类簇;再从剩下的核心点中选取新的为基点重复上述过程;直到所有的核心点被选取完毕,若剩下了样本点,每一个样本点均为一个类别。
具体流程:
1.初始化核心点集合 Ψ \Psi ,遍历数据找到所有的核心点,建立类簇集合 C C
2.若 Ψ = \Psi =\emptyset 停止迭代 转6,否则 Ψ \Psi 中随机选取一个核心点 o o (不放回),转3
3.以当前核心点为原点建立一个新的类簇 C o C_o ,建立当前子样本集 D o D_o ,子核心点集 Ψ o \Psi_{o} ,将核心点加入 Ψ o \Psi_{o} D o D_o
4.遍历 Ψ o \Psi_{o} ,拿取一个核心点(不放回),查询其相关邻域内的点 N ϵ o N_{\epsilon}o N ϵ o N_{\epsilon}o 中的点加入 D o D_o ,将 N ϵ o N_{\epsilon}o 与原始核心点集 Ψ \Psi 做交集,得到的集合再与 Ψ o \Psi_{o} 做并集。
5.不断重复3-4,直到 Ψ o \Psi_{o} 为空,将 D o D_o 中的点作为类簇 C o C_o 的成员, C o C_o 加入 C C ,转2
6.查找余下的未加入到 C C 中的数据,分别单独形成一个类簇加入。
Group的定义及import导入包,请见K均值聚类及代码实现

class Dbscan(object):
    """
    dbscan- Density-Based Spatial Clustering of Application with Noise
    基于密度的噪声应用空间聚类
    """
    def __init__(self,epos=0.1,minpts=5):
        self._epos = epos # 邻域半径范围
        self._minpts = minpts # 邻域内最小数据个数
        self._groups = [] #类簇集合
        self._kernel_points = [] #核心对象集合
        self._X = {} # 转化后的数据

    def _find_nearbours(self,xi,XX):
        """
        查找满足邻域值大小的数据索引列表
        :param xi: 
        :param XX: 
        :return: 
        """
        if XX.shape[0] == 0:
            return []
        distances= eculide(xi,XX)
        nearst_indexes = np.where(distances <= self._epos)[0].tolist()
        return nearst_indexes

    def _compat_X(self,X):
        """
        转化输入数据格式
        为了方便删除、存取操作
        :param X: 
        :return: 
        """
        rows,_ = X.shape
        CX = {}
        for row in range(rows):
            CX[row]=[False,X[row]]
        self._X = CX

    def _get_data(self,index):
        """
        获取索引的数据
        :param index: 
        :return: 
        """
        if isinstance(index,int):
            data = self._X.get(index)[-1]
        if isinstance(index,list):
            data = []
            for i in index:
                data.append(self._X.get(i)[-1])
        return data

    def _delete_data(self,indexes):
        """
        删除索引对应的数据
        :param indexes: 
        :return: 
        """
        if isinstance(indexes,int):
            self._X.get(indexes)[0] = True
        if isinstance(indexes, list):
            for index in indexes:
                self._X.get(index)[0] = True

    def _get_live_data(self):
        """
        获取未被加入到类簇集合的数据
        :return: 
        """
        live_data = {}
        for key,value in self._X.items():
            if value[0]==False:
                live_data[key] = value
        return live_data

    def fit(self,X):
        """
        聚类主体
        :param X: 
        :return: 
        """
        self._compat_X(X)#组合X的记录
        # 1.首先遍历数据集 找到所有的核心对象
        rows,_ = X.shape
        for i in range(rows):
            # 找到小于邻域参数的数据
            nearbours = self._find_nearbours(X[i],X)
            if len(nearbours)>=self._minpts:
                self._kernel_points.append(i)

        if len(self._kernel_points) == 0 :
            # 若没有核心对象,那么每个点都为单独的一类
            for i in range(rows):
                g = Group()
                g.name = i+1
                g.members = X[i]
                g.center = X[i]
                self._groups.append(g)
            return
        while(True):
            if len(self._kernel_points) == 0:
                break
            # 2.4直到当前核心对象集合为空
            # 2.从核心对象集合中取出一个核心对象,完成对一个类簇的生成
            init_index = int(np.random.randint(0,len(self._kernel_points),size=1).squeeze())
            kernel_points = self._kernel_points[init_index]
            self._kernel_points.remove(kernel_points)
            # 2.1拿取第一个核心对象生成类簇,加入当前核心对象集合
            current_points = set()  # 当前簇的样本集合
            current_kernel_points = set()
            g = Group()
            g.center = X[kernel_points]
            self._delete_data(kernel_points)
            current_kernel_points.add(kernel_points)
            delete_kernel_points = set()
            while len(current_kernel_points)!= 0:
                # 2.2然后当前核心对象集合中取一个核心点 找到该核心对象的所有邻域点,则邻域点即为该类簇的成员数据
                current_point_index = current_kernel_points.pop()
                current_points.add(current_point_index)
                delete_kernel_points.add(current_point_index)
                nearbours_points_indexes = self._find_nearbours(X[current_point_index],X)
                current_points = current_points.union(set(nearbours_points_indexes))
                # 2.3用这些成员与原始核心对象集合做交集,若有重合则将其加入当前核心对象集合中,重复上述查询邻域过程
                union_kernel_points = current_points.intersection(set(self._kernel_points))
                current_kernel_points = current_kernel_points.union(union_kernel_points)
                current_kernel_points = current_kernel_points.difference(delete_kernel_points)
                # 3 重复上述步骤
            current_datas = self._get_data(list(current_points))
            self._delete_data(list(current_points))
            g.members = current_datas
            g.name = len(self._groups) + 1
            self._groups.append(g)
            s = set(self._kernel_points)
            s = s.difference(current_points)
            self._kernel_points = list(s)
        # 查询现有余下的数据 也就是噪声点
        live_datas = self._get_live_data()
        for key ,value in live_datas.items():
            g = Group()
            g.name = len(self._groups) + 1
            g.center = value[-1]
            g.members = value[-1]
            self._groups.append(g)

    def plot_example(self):
        """
        画图
        """
        figure = plt.figure()
        ax = figure.add_subplot(111)
        ax.set_title("Dbscan Iris Example")
        plt.xlabel("first dim")
        plt.ylabel("two dim")
        legends = []
        cxs = []
        cys = []
        for i in range(len(self._groups)):
            group = self._groups[i]
            members = group.members
            x = [member[0] for member in members]
            y = [member[1] for member in members]
            cx = group.center[0]
            cy = group.center[1]
            cxs.append(cx)
            cys.append(cy)
            ax.scatter(x, y, marker='o')
            #ax.scatter(cx,cy,marker='+',c='r')
            legends.append(group.name)
        plt.scatter(cxs,cys,marker='+',c='k')
        plt.legend(legends, loc="best")
        plt.show()
def test_dbscan():
    X=np.array([[1,2],[2,1],[2,3],[4,3],[5,8],[6,7],[6,9],[7,9],[9,5],[1,12],[3,12],[5,12],[3,3]])
    data,t,tname = load_data()
    dbscan = Dbscan(2,3)
    dbscan.fit(X)
    dbscan.plot_example()

此处分别用了test_dbscan()中的X和data进行实验。
X
data是iris数据集
在这里插入图片描述
总结一下dbscan的特点:
DBSCAN的主要优点有(参考文献1):
1) 可以对任意形状的稠密数据集进行聚类,相对的,K-Means之类的聚类算法一般只适用于凸数据集。
2) 可以在聚类的同时发现异常点,对数据集中的异常点不敏感。
3) 聚类结果没有偏倚,相对的,K-Means之类的聚类算法初始值对聚类结果有很大影响。
DBSCAN的主要缺点有:
1)如果样本集的密度不均匀、聚类间距差相差很大时,聚类质量较差,这时用DBSCAN聚类一般不适合。
2) 如果样本集较大时,聚类收敛时间较长,此时可以对搜索最近邻时建立的KD树或者球树进行规模限制来改进。
3) 调参相对于传统的K-Means之类的聚类算法稍复杂,主要需要对距离阈值ϵ,邻域样本数阈值MinPts联合调参,不同的参数组合对最后的聚类效果有较大影响。

参考文献

1.DBSCAN密度聚类算法
2.无监督学习-DBSCAN聚类算法及应用

猜你喜欢

转载自blog.csdn.net/u013894072/article/details/84585419