機器學習聚類演算法Kmeans與DBSCAN
自己對兩種聚類方法的一點理解。
1. KMeans
1.1 演算法概述
Kmeans是一種劃分聚類的方法,基本K-Means演算法的思想很簡單,事先確定常數K,常數K意味著最終的聚類類別數,首先隨機選定初始點為質心,並通過計算每一個樣本與質心之間的相似度(這裡為歐式距離),將樣本點歸到最相似的類中,接著,重新計算每個類的質心(即為類中心),重複這樣的過程,直到質心不再改變,最終就確定了每個樣本所屬的類別以及每個類的質心。
由於每次都要計算所有的樣本與每一個質心之間的相似度,故在大規模的資料集上,K-Means演算法的收斂速度比較慢。
1.2 演算法流程
輸入:聚類個數k,資料集Xmxn。輸出:滿足方差最小標準的k個聚類。
(1) 選擇k個初始中心點,例如c[0]=X[0] , … , c[k-1]=X[k-1];
(2) 對於X[0]….X[n],分別與c[0]…c[k-1]比較,假定與c[i]差值最少,就標記為i;
(3) 對於所有標記為i點,重新計算c[i]={ 所有標記為i的樣本的每個特徵的均值};
(4) 重複(2)(3),直到所有c[i]值的變化小於給定閾值或者達到最大迭代次數。
Kmeans的時間複雜度:O(tkmn),空間複雜度:O((m+k)n)。其中,t為迭代次數,k為簇的數目,m為樣本數,n為特徵數。
1.3 Kmeans演算法優缺點
1.3.1 優點
(1). 演算法原理簡單。需要調節的超引數就是一個k。(2). 由具有出色的速度和良好的可擴充套件性。
1.3.2 缺點
(1). 在 Kmeans 演算法中 k 需要事先確定,這個 k 值的選定有時候是比較難確定。(2). 在 Kmeans 演算法中,首先需要初始k個聚類中心,然後以此來確定一個初始劃分,然後對初始劃分進行優化。這個初始聚類中心的選擇對聚類結果有較大的影響,一旦初始值選擇的不好,可能無法得到有效的聚類結果。多設定一些不同的初值,對比最後的運算結果,一直到結果趨於穩定結束。
(3). 該演算法需要不斷地進行樣本分類調整,不斷地計算調整後的新的聚類中心,因此當資料量非常大時,演算法的時間開銷是非常大的。
(4). 對離群點很敏感,通常發現的聚類形狀是凸形,類圓形。
(5). 從資料表示角度來說,在 Kmeans 中,我們用單個點來對 cluster 進行建模,這實際上是一種最簡化的資料建模形式。這種用點來對 cluster 進行建模實際上就已經假設了各 cluster的資料是呈圓形(或者高維球形)或者方形等分佈的。不能發現非凸形狀的簇。但在實際生活中,很少能有這種情況。所以在 GMM 中,使用了一種更加一般的資料表示,也就是高斯分佈。
(6). 從資料先驗的角度來說,在 Kmeans 中,我們假設各個 cluster 的先驗概率是一樣的,但是各個 cluster 的資料量可能是不均勻的。舉個例子,cluster A 中包含了10000個樣本,cluster B 中只包含了100個。那麼對於一個新的樣本,在不考慮其與A cluster、 B cluster 相似度的情況,其屬於 cluster A 的概率肯定是要大於 cluster B的。
(7). 在 Kmeans 中,通常採用歐氏距離來衡量樣本與各個 cluster 的相似度。這種距離實際上假設了資料的各個維度對於相似度的衡量作用是一樣的。但在 GMM 中,相似度的衡量使用的是後驗概率 αcG(x|μc,∑c) ,通過引入協方差矩陣,我們就可以對各維度資料的不同重要性進行建模。
(8). 在 Kmeans 中,各個樣本點只屬於與其相似度最高的那個 cluster ,這實際上是一種 hard clustering 。
針對Kmeans演算法的缺點,很多前輩提出了一些改進的演算法。例如 K-modes 演算法,實現對離散資料的快速聚類,保留了Kmeans演算法的效率同時將Kmeans的應用範圍擴大到離散資料。還有K-Prototype演算法,可以對離散與數值屬性兩種混合的資料進行聚類,在K-prototype中定義了一個對數值與離散屬性都計算的相異性度量標準。當然還有其它的一些演算法,這裡我 就不一一列舉了。
Kmeans 與 GMM 更像是一種 top-down 的思想,它們首先要解決的問題是,確定 cluster 數量,也就是 k 的取值。在確定了 k 後,再來進行資料的聚類。而 hierarchical clustering 則是一種 bottom-up 的形式,先有資料,然後通過不斷選取最相似的資料進行聚類。
1.4 利用python的sklearn機器學習庫進行示例:
from sklearn.cluster import KMeans
import numpy as np
import matplotlib.pyplot as plt
X = np.array([[15,17],[12,18],[14,15],[13,16],[12,15],
[16,12],[4,6],[5,8],[5,3],[7,4],[7,2],[6,5]])
y_pred = KMeans(n_clusters=2, random_state=0).fit_predict(X)
#方法fit_predict的作用是計算聚類中心,併為輸入的資料加上分類標籤
plt.figure(figsize=(20, 20))
color = ("red", "green")
colors=np.array(color)[y_pred]
plt.scatter(X[:, 0], X[:, 1], c=colors)
plt.show()
結果如下:
1.5 應用
影象壓縮領域,正如sklearn官方文件中對頤和園圖片的壓縮,針對含有9萬多種顏色的圖片,定義壓縮後顏色種類數為64,用kmeans跑過一遍後,得到了64種顏色的簇,而影象的整體質量沒有明顯的變化。
2. DBSCAN
2.1 基於密度的聚類演算法
開發原因: 彌補層次聚類演算法和劃分式聚類演算法往往只能發現凸型的聚類簇的缺陷。
核心思想: 只要一個區域中的點的密度大過某個閾值,就把它加到與之相近的聚類中去。
兩個關鍵詞:稠密樣本點、低密度區域(noise)
凸型的聚類簇:基於距離的聚類因為演算法的原因,最後實現的聚類通常是“類圓形”的,一旦面對具有複雜形狀的簇的時候,此類方法通常會無法得到滿意的結果。這種時候,基於密度的聚類方法就能有效地避免這種窘境。 與K-means比較起來,你不必輸入你要劃分的聚類個數,也就是說,你將不受聚類數目的限制;在需要時可以輸入過濾噪聲的引數;
這類演算法認為,在整個樣本空間點中,各類目標簇是由一群稠密樣本點組成的,,而這些稠密樣本點被低密度區域分割,我們通常把這類低密度區域稱為噪聲,而演算法的目的就是要過濾低密度區域,發現稠密樣本點。
密度的定義:傳統基於中心的密度定義為:
資料集中特定點的密度通過該點Eps半徑之內的點計數(包括本身)來估計。
顯然,密度依賴於半徑。
2.2 DBSCAN演算法的一些定義:
Density-Based Spatial Clustering of Applications with Noise核心點(core point) :在半徑Eps內含有超過MinPts數目的點,則該點為核心點,這些點都是在簇內的
邊界點(border point):在半徑Eps內點的數量小於MinPts,但是在核心點的鄰居,即在某個核心點的鄰域內。
噪音點(noise point):任何不是核心點或邊界點的點.
MinPts:給定點在E領域內成為核心物件的最小領域點數 ,簡單說就是核心點eps鄰域內點數量的最小值。
下面兩張圖可以更好理解相關的定義。
直接密度可達:給定一個物件集合D,如果p在q的Eps鄰域內,而q是一個核心物件,則稱物件p 從物件q出發時是直接密度可達的(directly density-reachable)。
密度可達:如果存在一個物件鏈 p1,p2...pn, p1=q, pn=p,pi+1是從 pi關於Eps和MinPts直接密度可達的,則物件p是從物件q關於Eps和MinPts密度可達的(density-reachable)
密度相連:如果存在物件O∈D,使物件p和q都是從O關於Eps和MinPts密度可達的,那麼物件p到q是關於Eps和MinPts密度相連的(density-connected)。
根據以上概念知道:由於有標記的各點M、P、O和R的Eps近鄰均包含3個以上的點,因此它們都是核對象(核心點);M是從P“直接密度可達”;而Q則是從M“直接密度可達”;基於上述結果,Q是從P“密度可達”;但P從Q無法“密度可達”(非對稱)。類似地,S和R從O是“密度可達”的;O、R和S均是“密度相連”的.
2.3 演算法原理
DBSCAN通過檢查資料集中每點的Eps鄰域來搜尋簇,如果點p的Eps鄰域包含的點多於MinPts個,則建立一個以p為核心物件的簇。並將p的Eps鄰域內所有點歸入p所在的簇。否則,p被標記為噪聲點。然後,DBSCAN迭代地聚集從這些核心物件直接密度可達的物件,這個過程可能涉及一些密度可達簇的合併。(就是p的Eps鄰域中的點)
當沒有新的點新增到任何簇時,該過程結束.
虛擬碼:
輸入:n個物件的資料集,半徑Eps,閾值MinPts
輸出:所有生成的簇
REPEAT
從資料庫選取未處理的點:
If :該點是核心點 then:找出它所有直接密度可達的點,形成一個簇
Else:該點是非核心點 ,尋找下一點。
Until:所有點都被處理
2.4 演算法優缺點:
優點:對噪音不敏感、可以處理不同形狀和大小的資料
不足:
1.對輸入引數敏感
確定引數eps和MinPts比較困難,一般由使用者在演算法執行前指定這兩個引數的大小,若選擇不當,往往會影響聚類的質量。
2.難以發現密度相差較大的簇
由於引數eps和MinPts是全域性唯一的,因此資料集中所有物件的鄰域大小是一致的。 當資料集密度不均勻時,若根據高密簇選取較小的eps值,那麼屬於低密簇的物件的鄰域中的資料點數將可能要小於MinPts,那麼這些物件將被認為是邊界點,從而不被用於所在類的進一步擴充套件,結果造成低密簇被劃分成多個性質相似的簇。若根據低密簇選取較大的eps值,那麼離得較近且高密的簇很可能被合併為一個簇。由此可知選取一個合適的eps值是比較困難的。因為引數eps和MinPts全域性唯一,導致演算法只能發現密度近似的簇
3.資料量增大時,要求較大的記憶體支援,I/O消耗大。
由於DBSCAN演算法進行聚類時需要把所有資料都載入記憶體,因此在資料量很龐大且沒有任何預處理的情況下對主存要求較高。
2.5 程式碼實現
# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
import math
import time
UNCLASSIFIED = False
NOISE = 0
def loadDataSet(fileName, splitChar='\t'):
"""
輸入:檔名
輸出:資料集
描述:從檔案讀入資料集
"""
dataSet = []
with open(fileName) as fr:
for line in fr.readlines():
curline = line.strip().split(splitChar)
fltline = list(map(float, curline))
dataSet.append(fltline)
return dataSet
def dist(a, b):
"""
輸入:向量A, 向量B
輸出:兩個向量的歐式距離
"""
return math.sqrt(np.power(a - b, 2).sum())
def eps_neighbor(a, b, eps):
"""
輸入:向量A, 向量B
輸出:是否在eps範圍內
"""
return dist(a, b) < eps
def region_query(data, pointId, eps):
"""
輸入:資料集, 查詢點id, 半徑大小
輸出:在eps範圍內的點的id
"""
nPoints = data.shape[1]
seeds = []
for i in range(nPoints):
if eps_neighbor(data[:, pointId], data[:, i], eps):
seeds.append(i)
return seeds
def expand_cluster(data, clusterResult, pointId, clusterId, eps, minPts):
"""
輸入:資料集, 分類結果, 待分類點id, 簇id, 半徑大小, 最小點個數
輸出:能否成功分類
"""
seeds = region_query(data, pointId, eps)
if len(seeds) < minPts: # 不滿足minPts條件的為噪聲點
clusterResult[pointId] = NOISE
return False
else:
clusterResult[pointId] = clusterId # 劃分到該簇
for seedId in seeds:
clusterResult[seedId] = clusterId
while len(seeds) > 0: # 持續擴張
currentPoint = seeds[0]
queryResults = region_query(data, currentPoint, eps)
if len(queryResults) >= minPts:
for i in range(len(queryResults)):
resultPoint = queryResults[i]
if clusterResult[resultPoint] == UNCLASSIFIED:
seeds.append(resultPoint)
clusterResult[resultPoint] = clusterId
elif clusterResult[resultPoint] == NOISE:
clusterResult[resultPoint] = clusterId
seeds = seeds[1:]
return True
def dbscan(data, eps, minPts):
"""
輸入:資料集, 半徑大小, 最小點個數
輸出:分類簇id
"""
clusterId = 1
nPoints = data.shape[1]
clusterResult = [UNCLASSIFIED] * nPoints
for pointId in range(nPoints):
point = data[:, pointId]
if clusterResult[pointId] == UNCLASSIFIED:
if expand_cluster(data, clusterResult, pointId, clusterId, eps, minPts):
clusterId = clusterId + 1
return clusterResult, clusterId - 1
def plotFeature(data, clusters, clusterNum):
nPoints = data.shape[1]
matClusters = np.mat(clusters).transpose()
fig = plt.figure()
scatterColors = ['black', 'blue', 'green', 'yellow', 'red', 'purple', 'orange', 'brown']
ax = fig.add_subplot(111)
for i in range(clusterNum + 1):
colorSytle = scatterColors[i % len(scatterColors)]
subCluster = data[:, np.nonzero(matClusters[:, 0].A == i)]
ax.scatter(subCluster[0, :].flatten().A[0], subCluster[1, :].flatten().A[0], c=colorSytle, s=50)
def main():
dataSet = loadDataSet('788points.txt', splitChar=',')
dataSet = np.mat(dataSet).transpose()
# print(dataSet)
clusters, clusterNum = dbscan(dataSet, 2, 15)
print("cluster Numbers = ", clusterNum)
# print(clusters)
plotFeature(dataSet, clusters, clusterNum)
if __name__ == '__main__':
start = time.clock()
main()
end = time.clock()
print('finish all in %s' % str(end - start))
plt.show()
結果如下: