根据模型的数学原理进行简单的代码自我复现以及使用测试,仅作自我学习用。模型原理此处不作过多赘述,仅罗列自己将要使用到的部分公式。
如文中或代码有错误或是不足之处,还望能不吝指正。
DBSCAN,是一种按照密度聚类的算法,其主要思想是将密度相连的各个点都认为是一类“簇”。
要解释“密度相连”的定义,就需要解释“密度直达”和“密度可达”。
密度直达:包含在的领域(所有与距离小于的点的集合)中,且 的领域中有超过给定阈值个点时,称 由密度直达
密度可达:比“密度直达”弱一些,如果点A和点B密度直达,点B和点C密度直达,那么称点A和点C密度可达
密度相连:比“密度可达”弱一些,如果点A和点B密度可达,点B和点C密度可达,那么称点A和点C密度可达
其中, 领域的半径以及为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来得要好。