机器学习模型自我代码复现:DBSCAN

根据模型的数学原理进行简单的代码自我复现以及使用测试,仅作自我学习用。模型原理此处不作过多赘述,仅罗列自己将要使用到的部分公式。

如文中或代码有错误或是不足之处,还望能不吝指正。

DBSCAN,是一种按照密度聚类的算法,其主要思想是将密度相连的各个点都认为是一类“簇”。

要解释“密度相连”的定义,就需要解释“密度直达”和“密度可达”。

密度直达:x_j包含在x_i\epsilon领域(所有与x_i距离小于\epsilon的点的集合)中,且x_i\epsilon领域中有超过给定阈值MinPts个点时,称 x_jx_i密度直达

密度可达:比“密度直达”弱一些,如果点A和点B密度直达,点B和点C密度直达,那么称点A和点C密度可达

密度相连:比“密度可达”弱一些,如果点A和点B密度可达,点B和点C密度可达,那么称点A和点C密度可达

其中, \epsilon领域的半径\epsilon以及MinPts为DBSCAN的超参数。

相比于“给定簇个数,不断更新簇中心点,直至簇心变化小于阈值”的KMeans算法,DBSCAN尽管不能指定聚类的簇个数,但是有着能够识别“离群值”,以及能够在非凸集中效果更佳的特点。

此处由于自我实现的KD-Tree似乎并不完善,故而使用skit-learn中自带的KDTree。

import numpy as np
import pandas as pd
import random
from sklearn.preprocessing import  PolynomialFeatures
from sklearn.neighbors import KDTree
from collections import deque

class DBSCAN:
    def pre_processing(self,data,polynomial_degree=0,sinusoid_degree=0,normalize_data=False):
        """
        数据预处理
        polynomial_degree:多项式处理,1为仅添加偏置
        sinusoid_degree:正弦处理次数
        normalize_data:是否标准化
        """
        if polynomial_degree>=1:#多项式
            poly = PolynomialFeatures(polynomial_degree)
            data = poly.fit_transform(data)
            
        if sinusoid_degree>0:#sin函数非线性变换
            sinusoid = np.empty((data.shape[0],0))
            for i in range(1,sinusoid_degree+1):
                sinusoid_feature = np.sin(i*data)
                sinusoid = np.concatenate((sinusoid,sinusoid_feature),axis=1)
            data = sinusoid
        if normalize_data:
            data = (data-self.feature_mean)/self.feature_std
        return data
    
    def __init__(self,data,polynomial_degree=0,sinusoid_degree=0,normalize_data=False):
        """
        初始化
        data:特征矩阵
        polynomial_degree:多项式处理,1为仅添加偏置
        sinusoid_degree:正弦处理次数
        normalize_data:是否标准化
        """
        self.normalize_data=normalize_data
        self.feature_mean = np.mean(data)
        self.feature_std = np.std(data)
        self.data = self.pre_processing(data,polynomial_degree,sinusoid_degree,normalize_data)
        self.polynomial_degree=polynomial_degree
        self.sinusoid_degree=sinusoid_degree
        self.feature_num = self.data.shape[1]
        #初始化
        self.tree = KDTree(data)
        self.used_idx = [False for _ in range(data.shape[0])] #标识该点是否已经用过
        self.Cluster = {}#每个簇包含点坐标的字典
        self.outliers = []#离群点坐标
        self.Cluster_idx = {}#每个簇包含点索引的字典
        self.outliers_idx = []#离群点索引
        self.minPts=0 #初始化领域中最少点,之后使用这个属性作预测
        self.eps=0 #初始化领域值,之后使用这个属性作预测
        self.idx_2_clust={} #索引到簇的一一对应,方便之后作预测
    
    def train(self,minPts=5,eps=0.5):
        """
        训练
        minPts:领域内至少又多少个点
        eps:领域半径
        """
        idxs = list(range(self.data.shape[0]))
        self.eps = eps
        self.minPts = minPts
        C_num = 1
        #针对所有尚未"标识"的店
        while sum(self.used_idx) < self.data.shape[0]:
            idx = random.sample([i for i in idxs if self.used_idx[i]==False],1)[0]
            self.used_idx[idx] = True
            res = self.kdSearch(idx,minPts,eps)
            #只有1个点,额外当作是离群点
            if len(res)<minPts:
                for i in res:
                    self.outliers.append(self.data[i,:])
                    self.outliers_idx.append(res)
            #有多个点密度可达,当作是一个簇
            else:
                clust = "C_"+str(C_num)
                C_num += 1
                self.Cluster[clust]=np.array(self.data[res])
                self.Cluster_idx[clust]=np.array(res)
        #索引到簇的一一对应
        for i in self.Cluster_idx.keys():
            for j in self.Cluster_idx[i]:
                self.idx_2_clust[j]=i
                
    def kdSearch(self,idx,minPts,eps):
        """
        从一个点开始搜索密度可达的所有点
        minPts:领域内至少又多少个点
        eps:领域半径
        """
        que = deque([idx])
        res = [idx]
        #que用以维护之后需要进行“在领域内搜索符合要求的点”的操作。如有,也将其加入que中,并加入res
        while len(que)>0:
            point = que.popleft()
            dist,ind = self.tree.query([self.data[point]],k=minPts+1) #每个点到point的距离以及索引,此处应当社区本身的点(距离为0)
            dist = dist[0]
            ind = ind[0]
            for i in range(len(ind)):
                if dist[i] == 0:
                    del_idx = i
                    break
            dist = [dist[i] for i in range(len(dist)) if i != del_idx]
            ind = [ind[i] for i in range(len(ind)) if i != del_idx]
            if min(dist)>eps:#最小的距离依然大于领域的半径,此时ind中所有的点都略过
                continue
            else:
                for i in range(minPts):
                    if dist[i]<=eps and self.used_idx[ind[i]]==False:
                        self.used_idx[ind[i]]=True
                        res.append(ind[i])
                        if len(dist)>=minPts:
                            que.append(ind[i])

        return res

    def get_clust(self,data,add_in=False):
        """
        给定新点,判断新点究竟在哪个中
        data:新点集合
        add_in:是否加入加入属性中
        """
        res = []
        for d in range(data.shape[0]):
            dist,ind = self.tree.query([data[d]],k=self.minPts)
            dist = dist[0]
            ind = ind[0]
            i = np.argmin(dist)
            if ind[i] not in self.idx_2_clust.keys():
                res.append("outliers")
                if add_in:
                    self.outliers.append(d)
            else:
                nearest_clust = self.idx_2_clust[ind[i]]
                res.append(nearest_clust)
                if add_in:
                    self.Cluster[nearest_clust].append(d)
        return res

自己生成的数据集对比KMeans算法与DBSCAN。

x1 = np.linspace(-3,3,200)
round1 = []
for i in range(len(x1)):
    round1.append(np.array([x1[i],float(np.sqrt(9-x1[i]**2)+np.random.randn(1)*0.1)]))
    round1.append(np.array([x1[i],float(-np.sqrt(9-x1[i]**2)+np.random.randn(1)*0.1)]))
round1 = np.array(round1)

x2 = np.linspace(-4,4,200)
round2 = []
for i in range(len(x2)):
    round2.append(np.array([x2[i],float(np.sqrt(16-x2[i]**2)+np.random.randn(1)*0.1)]))
    round2.append(np.array([x2[i],float(-np.sqrt(16-x2[i]**2)+np.random.randn(1)*0.1)]))
round2 = np.array(round2)

outliers = np.array([
    np.array([5,5])
    ,np.array([-5,5])
    ,np.array([-5,-5])
    ,np.array([5,-5])
])

np_final = np.r_[outliers,round1]
np_final = np.r_[np_final,round2]
from matplotlib import pyplot as plt
import matplotlib as mpl

mpl.rcParams["font.family"]="SimHei"
mpl.rcParams["axes.unicode_minus"]=False

plt.scatter(np_final[:,0],np_final[:,1])
plt.title("原始数据集")
plt.show()

KMeans聚类:

from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters=2)
res_kmeans=kmeans.fit_predict(np_final)

c_dict = list(res_kmeans)
for i in range(len(c_dict)):
    if res_kmeans[i]==0:
        c_dict[i]="blue"
    else:
        c_dict[i]="red"
        
plt.scatter(np_final[:,0],np_final[:,1],c=c_dict)
plt.title("K_Means结果")
plt.show()

 DBSCAN聚类:

dbScan = DBSCAN(np_final)
dbScan.train(eps=0.75,minPts=10)

for i in dbScan.Cluster.keys():
    plt.scatter(dbScan.Cluster[i][:,0],dbScan.Cluster[i][:,1],label = i)
plt.scatter(np.array(dbScan.outliers)[:,0],np.array(dbScan.outliers)[:,1],label="离群点")
plt.legend()
plt.show()

可以看出, 在构造的数据集上,DBSCAN的效果比KMeans来得要好。

猜你喜欢

转载自blog.csdn.net/thorn_r/article/details/124001797