Python 實現DBSCAN 算法

 一、基於密度的聚類

K-Means 算法、K-Means++ 算法和Mean Shift 算法都是基於距離的聚類算法,當數據集中的聚類結果是球狀結構時,能夠得到比較好的結果,但當數據集中的聚類結果是非球狀的結構時,基於距離的聚類算法的聚類效果並不好。球狀結構的聚類如我們前面講的,非球類結構的聚類如下圖所示:

基於距離的三種聚類算法其解得的聚類結果都不對,在上圖中,數據的分佈呈現明顯的密度趨勢,所以基於密度的聚類算法 DBSCAN 被提出。

二、DBSCAN 算法原理

1.基本概念

DBSCAN 是一種典型的基於密度的聚類算法,它有兩個最基本的鄰域參數——\varepsilon鄰域、MinPts:

  • \varepsilon鄰域:在數據集 D 中與樣本點 xi 的距離不大於 \varepsilon的樣本,即N_{\varepsilon }(x_{i})=\left \{ x_{j} \in D|dist(x_{i},x_{j})\leq \varepsilon \right \},如下圖所示,x* 不在樣本點xi 的\varepsilon鄰域內,xi 的密度可由 xi 的\varepsilon 鄰域內的點數來估計。

  • MinPts:在樣本xi的\varepsilon鄰域內的最少樣本點的數目

基於鄰域參數\varepsilon鄰域和MinPts,在DBSCAN算法中將數據點分爲以下三類:

 

  • 核心點:半徑\varepsilon內含有超過MinPts數目的點;
  • 邊界點:在半徑\varepsilon內點的數量小於MinPts,但是落在覈心點的鄰域內;
  • 噪音點:既不是核心點也不是邊界點的點。

在上圖中,設置MinPts的值爲10,對應的x1的\varepsilon鄰域中有11個樣本點,大於MinPts,則x1爲核心點。x2的\varepsilon鄰域中有6個樣本點,小於MinPts且在x1的\varepsilon鄰域內,則x2爲邊界點,x*爲噪聲點。

還定義如下的一些概念:

直接密度可達:給定一個對象集合D,如果p在q的\varepsilon鄰域內,而q是一個核心對象,則稱對象p從對象q出發時是直接密度可達的;

密度可達:如果存在一個對象鏈  p1, …,pi,.., pn,滿足p1 = p 和pn = q,pi是從pi+1關於\varepsilon和MinPts直接密度可達的,則對象p是從對象q關於\varepsilon和MinPts密度可達的;

密度相連:如果存在對象O∈D,使對象p和q都是從O關於\varepsilon和MinPts密度可達的,那麼對象p到q是關於\varepsilon和MinPts密度相連的。

如下圖所示,設MinPts=3

“直接密度可達”和“密度可達”概念描述:根據前文基本概念的描述,由於有標記的各點­M、P、O和R的Eps近鄰均包含3個以上的點,因此它們都是覈對象;M­是從P“直接密度可達”;而Q則是從­M“直接密度可達”;基於上述結果,Q是從P“密度可達”;但P從Q無法“密度可達”(非對稱)。類似地,S和R從O是“密度可達”的;O、R和S均是“密度相連”(對稱)的。

2.DBSCAN算法原理

基於密度的聚類算法通過尋找被低密度區域分離的高密度區域,並將高密度區域作爲一個聚類“簇”,在DBSCAN算法中,聚類:簇“定義爲:由密度可達關係導出最大的密度連接樣本的集合。

若 x 爲核心對象,由 x 密度可達的所有樣本組成的集合記爲:

X={x'\inD|x'由x密度可達}

則X滿足連接性和最大性的簇。

3.DBSCAN算法流程

  • 根據給定的鄰域參數\varepsilon和MinPts確定所有的核心對象
  • 對每一個核心對象
  • 選擇一個未處理過的核心對象,找到其密度可達的樣本生成聚類“簇”
  • 重複以上過程

三、 DBSCAN算法實現

# -*- coding: utf-8 -*-
"""
Created on Wed Apr  3 10:41:02 2019

@author: 2018061801
"""
import matplotlib.pyplot as plt
import numpy as np
import math
MinPts = 6  # 定義半徑內的最少的數據點的個數
def load_data(file_path):
    '''導入數據
    input:  file_path(string):文件名
    output: data(mat):數據
    '''
    f = open(file_path)
    data = []
    for line in f.readlines():
        data_tmp = []
        lines = line.strip().split("\t")
        for x in lines:
            data_tmp.append(float(x.strip()))
        data.append(data_tmp)
    f.close()
    return np.mat(data)

def epsilon(data, MinPts):
    '''計算半徑
    input:  data(mat):訓練數據
            MinPts(int):半徑內的數據點的個數
    output: eps(float):半徑
    '''
    m, n = np.shape(data)
    xMax = np.max(data, 0)
    xMin = np.min(data, 0)
    eps = ((np.prod(xMax - xMin) * MinPts * math.gamma(0.5 * n + 1)) / (m * math.sqrt(math.pi ** n))) ** (1.0 / n)
    return eps
    
def distance(data):
    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):
    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):
    m = np.shape(data)[0]
    # 區分核心點1,邊界點0和噪音點-1
    types = np.mat(np.zeros((1, m)))
    sub_class = np.mat(np.zeros((1, m)))
    # 用於判斷該點是否處理過,0表示未處理過
    dealed = np.mat(np.zeros((m, 1)))
    # 計算每個數據點之間的距離
    dis = distance(data)
    # 用於標記類別
    number = 1
    
    # 對每一個點進行處理
    for i in range(m):
        # 找到未處理的點
        if dealed[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
                dealed[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:
                    dealed[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 dealed[ind_1[j], 0] == 0:
                                dealed[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

def save_result(file_name, source):
    f = open(file_name, "w")
    n = np.shape(source)[1]
    tmp = []
    for i in range(n):
        tmp.append(str(source[0, i]))
    f.write("\n".join(tmp))
    f.close()    

if __name__ == "__main__":
    # 1、導入數據
    print ("----------- 1、load data ----------")
    data = load_data("D:/anaconda4.3/spyder_work/data6.txt")
    # 2、計算半徑
    print ("----------- 2、calculate eps ----------")
    eps = epsilon(data, MinPts)
    # 3、利用DBSCAN算法進行訓練
    print ("----------- 3、DBSCAN -----------")
    types, sub_class = dbscan(data, eps, MinPts)
    # 4、保存最終的結果
    print ("----------- 4、save result -----------")
    save_result("types", types)
    save_result("sub_class", sub_class)
    
    
"""未使用聚類算法"""
f = open("D:/anaconda4.3/spyder_work/data6.txt")
x = []
y = []
for line in f.readlines():
    lines = line.strip().split("\t")
    if len(lines) == 2:
        x.append(float(lines[0]))
        y.append(float(lines[1]))
f.close()  
#顯示中文標題
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.figure(figsize=(10,8), dpi=80) 
plt.plot(x, y, 'b.', label="原始數據")
plt.title('未使用聚類算法')
plt.legend(loc="upper right")
plt.show()


"""打開兩個保存的文件"""
f = open("D:/anaconda4.3/spyder_work/sub_class.txt") 
center_x = []
center_y = []
for line in f.readlines():
    lines = line.strip().split("\t")
    if len(lines) == 2:
        center_x.append(lines[0])
        center_y.append(lines[1])
f.close()

f = open("D:/anaconda4.3/spyder_work/types.txt") 
types = []
for line in f.readlines():
    lines = line.strip().split("\t")
    if len(lines) == 1:
        types.append(float(lines[0]))
f.close() 


"""使用聚類算法"""
data1=load_data("D:/anaconda4.3/spyder_work/sub_class.txt")
data1=np.array(data1)
N = len(data1)
#核心點
core_x_0=[]
core_y_0=[]
core_x_1=[]
core_y_1=[]
core_x_2=[]
core_y_2=[]
core_x_3=[]
core_y_3=[]
#邊界點
boundary_x_0=[]
boundary_y_0=[]
boundary_x_1=[]
boundary_y_1=[]
boundary_x_2=[]
boundary_y_2=[]
boundary_x_3=[]
boundary_y_3=[]
#噪音點
noise_x=[]
noise_y=[]
for i in range(N):
    if data1[i]==-1:
        noise_x.append(data[i,0])
        noise_y.append(data[i,1])
    elif data1[i]==1:
        if types[i]==1:
            core_x_0.append(data[i,0])
            core_y_0.append(data[i,1])
        else:
            boundary_x_0.append(data[i,0])
            boundary_y_0.append(data[i,1])
    elif data1[i]==2:
        if types[i]==1:
            core_x_1.append(data[i,0])
            core_y_1.append(data[i,1])
        else:
             boundary_x_1.append(data[i,0])
             boundary_y_1.append(data[i,1])
    elif data1[i]==3:
        if types[i]==1:
            core_x_2.append(data[i,0])
            core_y_2.append(data[i,1])
        else:
             boundary_x_2.append(data[i,0])
             boundary_y_2.append(data[i,1])
    elif data1[i]==4:
        if types[i]==1:
            core_x_3.append(data[i,0])
            core_y_3.append(data[i,1])
        else:
             boundary_x_3.append(data[i,0])
             boundary_y_3.append(data[i,1])

plt.figure(figsize=(10,8), dpi=80)
plt.plot(core_x_0, core_y_0,'b+',label="core_0")
plt.plot(core_x_1, core_y_1,'k+',label="core_1")
plt.plot(core_x_2, core_y_2,'g+',label="core_2")
plt.plot(core_x_3, core_y_3,'c+',label="core_3")
plt.plot(boundary_x_0,boundary_y_0,'b.',label="boundary_0")
plt.plot(boundary_x_1,boundary_y_1,'k.',label="boundary_1")
plt.plot(boundary_x_2,boundary_y_2,'g.',label="boundary_2")
plt.plot(boundary_x_3,boundary_y_3,'c.',label="boundary_3")
plt.plot(noise_x,noise_y,'*r',label="noise")
plt.title('使用聚類算法')
plt.legend(loc="best")           
plt.show() 

結果: 

----------- 1、load data ----------
----------- 2、calculate eps ----------
----------- 3、DBSCAN -----------
----------- 4、save result -----------

上圖中,十字代表的核心點,圓點代表的是邊界點,紅色的星代表的是噪音點,不同的顏色代表着不同的類。

另外,在DBSCAN算法中,其聚類結果和\varepsilon的取值有關,若\varepsilon取值太小,則聚類結果中噪音點數量變多,持續減少\varepsilon的值,最終導致所有的樣本被劃分爲噪音點;如果\varepsilon取值過大,聚類結果中噪音點減少,持續增大\varepsilon的值,類的數量將減少,所以爲得到正確有效的聚類結果,需要設置合適的\varepsilon值。

 

數據鏈接

參考文獻

1.DBSCAN基本原理
2.趙志勇——Python 機器學習算法

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章