一、基於密度的聚類
K-Means 算法、K-Means++ 算法和Mean Shift 算法都是基於距離的聚類算法,當數據集中的聚類結果是球狀結構時,能夠得到比較好的結果,但當數據集中的聚類結果是非球狀的結構時,基於距離的聚類算法的聚類效果並不好。球狀結構的聚類如我們前面講的,非球類結構的聚類如下圖所示:
基於距離的三種聚類算法其解得的聚類結果都不對,在上圖中,數據的分佈呈現明顯的密度趨勢,所以基於密度的聚類算法 DBSCAN 被提出。
二、DBSCAN 算法原理
1.基本概念
DBSCAN 是一種典型的基於密度的聚類算法,它有兩個最基本的鄰域參數——鄰域、MinPts:
- 鄰域:在數據集 D 中與樣本點 xi 的距離不大於 的樣本,即,如下圖所示,x* 不在樣本點xi 的鄰域內,xi 的密度可由 xi 的 鄰域內的點數來估計。
- MinPts:在樣本xi的鄰域內的最少樣本點的數目
基於鄰域參數鄰域和MinPts,在DBSCAN算法中將數據點分爲以下三類:
- 核心點:半徑內含有超過MinPts數目的點;
- 邊界點:在半徑內點的數量小於MinPts,但是落在覈心點的鄰域內;
- 噪音點:既不是核心點也不是邊界點的點。
在上圖中,設置MinPts的值爲10,對應的x1的鄰域中有11個樣本點,大於MinPts,則x1爲核心點。x2的鄰域中有6個樣本點,小於MinPts且在x1的鄰域內,則x2爲邊界點,x*爲噪聲點。
還定義如下的一些概念:
直接密度可達:給定一個對象集合D,如果p在q的鄰域內,而q是一個核心對象,則稱對象p從對象q出發時是直接密度可達的;
密度可達:如果存在一個對象鏈 p1, …,pi,.., pn,滿足p1 = p 和pn = q,pi是從pi+1關於和MinPts直接密度可達的,則對象p是從對象q關於和MinPts密度可達的;
密度相連:如果存在對象O∈D,使對象p和q都是從O關於和MinPts密度可達的,那麼對象p到q是關於和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'D|x'由x密度可達}
則X滿足連接性和最大性的簇。
3.DBSCAN算法流程
- 根據給定的鄰域參數和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算法中,其聚類結果和的取值有關,若取值太小,則聚類結果中噪音點數量變多,持續減少的值,最終導致所有的樣本被劃分爲噪音點;如果取值過大,聚類結果中噪音點減少,持續增大的值,類的數量將減少,所以爲得到正確有效的聚類結果,需要設置合適的值。
參考文獻
1.DBSCAN基本原理
2.趙志勇——Python 機器學習算法