1. 程式人生 > 實用技巧 >基於劃分的聚類演算法(K-Means)與基於密度的聚類演算法(DBSCAN)的程式碼實現與分析

基於劃分的聚類演算法(K-Means)與基於密度的聚類演算法(DBSCAN)的程式碼實現與分析

基於劃分的聚類演算法(K-Means)與基於密度的聚類演算法(DBSCAN)對比分析

在開始閱讀前可以看一下有關這兩個演算法的描述和視覺化效果展示
Visualizing K-Means Clustering
Visualizing DBSCAN Clustering


使用Python實現了兩個演算法,演算法設計場景是對平面二維樣本點的計算

K-Means演算法步驟

  1. 隨機生成k個點作為樣本資料的中心點
  2. 計算所有點分別到k箇中心點的距離
  3. 對於二維的每個樣本點,比較到哪一個中心點的距離近(歐式距離最小),就被劃分到哪一類,並更新樣本點的類別表
  4. 對於歸類後的資料重新計算中心點的座標
  5. 判斷中心點是否有明顯變化,如果有,跳轉到2,如果沒有,跳轉到6
  6. 程式結束,劃分完成

DBSCAN演算法步驟

  1. 設定與樣本集等長的訪問標記列表(最初所有的點被標記為noise或unvisted)
  2. 設定鄰域epsilon,點鄰域形成團的最少點數量 min_points
  3. 遍歷樣本點集D, 檢查點p的表示是否visited,如果unvisited,執行4,否則繼續進行遍歷直到結束
  4. 檢查距p在epsilon範圍內的點數量是否達到min_points,如果是,則跳轉到5
  5. 將鄰域內所有點加入到該群集clusters中
  6. 檢查這些點在其鄰域內是否有不少於min_points的點,如果是,遞迴擴充該群集
  7. 完全有可能選擇的點在其epsilon球中少於minPoints,並且也不屬於任何其他群集。如果是這種情況,則將其視為不屬於任何群集的“噪聲點”。

演算法實現

K-Means演算法實現

# 計算兩點之間的距離,傳入narray作為[[],[],[]]做多個點距離計算,axis=1
def dist(a, b, axis=1):
    return np.linalg.norm(a - b, axis=axis)


def kmeans_alg(k, D):
    """
    params:
    k 設定劃分的區域數目
    D 樣本點資料集
    """
    # 隨機初始化中心點 方法一
    center_x = np.random.randint(D.min(0)[0], D.max(0)[0], size=k)
    center_y = np.random.randint(D.min(0)[1], D.max(0)[1], size=k)
    centers = np.array(list(zip(center_x, center_y)), dtype=D[:,0].dtype)
    # 隨機初始化中心點 方法二
    centers = D[np.random.randint(0, len(D), size=k)]
    
    # c_new用來儲存迭代產生的新中心點座標
    centers_new = np.zeros(centers.shape)
    # 用於儲存資料所屬劃分
    labels = np.zeros(len(D))
    # 設定容忍度,該變數為迭代前後中心點的變化明顯性,低於這個明顯性的時候,不再進行迭代
    tol_move = 10.0
    tol = True
    
    while tol:
        for i in range(len(D)):
            #記錄每個樣本點的所屬劃分,計算距哪個中心點的距離最小
            cluster = np.argmin(dist(D[i], centers))
            labels[i] = cluster
        for i in range(k):
            # c_p_sets = [D[j] for j in range(len(D)) if labels[j] == i]
            c_p_sets = D[labels == i]        
            if len(c_p_sets)!=0:
                centers_new[i] = np.mean(c_p_sets, axis=0) 
        # 當k箇中心點的位置都不再發生變化的時候,結束迴圈,也可使用tol_move自定義容忍度   
        tol = dist(centers_new, centers).any()
        centers = deepcopy(centers_new)
    return labels, centers

DBSCAN演算法實現



# DBSCAN演算法中,計算兩點之間的距離 [] 
def dist1(a, b,axis=0):
    return np.linalg.norm(a - b,axis=axis)

def find_ngb_eps(p, D,epsilon):
    # 在樣本點中尋找p點鄰域epsilon內的點,不包含p
    neighbors = [i for i in range(len(D)) if 0<dist1(D[p], D[i]) < epsilon]
    return neighbors

def expand_cluster_rec(labels, p, cid, D, min_points, epsilon):
	# 求群集方法一
    # 遞迴求群集
    if labels[p] == -1:    
        labels[p]=cid
    if labels[p] == 0:
        labels[p]=cid
        neighbors = find_ngb_eps(p, D, epsilon)
        if(len(neighbors) >= min_points):
            for ngb in neighbors:
                expand_cluster_rec(ngb, cid, D, min_points, epsilon)

def expand_cluster_bfs(labels,p, cid, D, min_points, epsilon):
	# 求群集方法二
    # 廣度優先搜尋求群集
    expand_queue = Queue()
    expand_queue.put(p)
    while not expand_queue.empty():
        seed = expand_queue.get()
        neighbors = find_ngb_eps(seed, D,epsilon)
        if (len(neighbors) >= min_points):
            for ngb in neighbors:
                if(labels[ngb] == -1):
                    labels[ngb] = cid
                if(labels[ngb] == 0):
                    labels[ngb] = cid
                    expand_queue.put(ngb)

def dbscan_alg(D,epsilon,min_points):
    """
    params:
    兩個密度引數epsilon ,min_points
    epsilon  聚類半徑
    min_points 聚類點數
    """
    # 樣本點所屬cluster的id,從1開始
    cid = 0 
    # 樣本點的標記 unvisited(0),visited(用該點的cid表示), noise(用-1表示)
    # 所有樣本初始標記為unvisited
    labels = np.zeros(len(D))
    
    
    for i in range(len(D)):
        if labels[i] == 0:
            if(len(find_ngb_eps(i, D,epsilon)) >= min_points):
                cid+=1
                expand_cluster_bfs(labels, i, cid, D, min_points, epsilon)
            else:
                labels[i] = -1
                # 後續的遍歷可能會把該點連線為其他cluster,最終的標記為-1,才會被認定為noise
    return labels, cid

聚類效果分析與思考

效果展示採用的方法:

為了展示實現的效果,使用sklearn.cluster的對應KMeans和DBSCAN模型對相同資料集進行聚類,對比分類效果

K-Means聚類效果展示

輸入:要事先給出k值,即要生成簇的數目,資料集D
輸出:分類結果,各類中心點

效果展示採用的資料集:

使用sklearn.datasets中的聚類資料生成器make_blobs來產生資料集:

X, y = make_blobs(n_samples=150, random_state=170)

資料分佈:

效果展示及對比如下:

中心點輸出結果:
[[-4.73502019 0.10448638],[-9.13517967 -5.39468265],[1.97517735 0.64725381]]

[[ 1.97517735 0.64725381] ,[-9.13517967 -5.39468265] , [-4.73502019 0.10448638]]

結論:經對比發現自己程式設計實現的kmeans演算法和KMeans模型在資料分類和簇中心點的位置是完全一致的。

DBSCAN聚類效果展示

輸入:設定兩個關於密度計算的引數, 聚類半徑( epsilon )和 聚類點數( min_points )
輸出:分類結果,DBSCAN不需要事先知道要形成的簇類的數量,通過引數自動得到簇數;

效果展示採用的資料集:

使用sklearn.datasets中的聚類資料生成器make_blobs來產生資料集:

X,y = datasets.make_moons(500,noise = 0.1,random_state=1)

資料分佈:

效果展示及對比如下:

本例使用的引數
eps = 0.2
min_points=20

結論:經對比發現自己程式設計實現的dbscan_alg演算法和DBSCAN模型在資料分類結果是完全一致的。

對於DBSCAN演算法來說,引數的選擇對於聚類質量的影響很大。

  1. 令min_points的值不變,增長eps的值,修改為0.25
    對比效果如下:(右邊是引數修改後分類效果)

    可以發現在min_points的值不變,稍微增加eps的值後,視覺化圖中可以明顯發現一部分噪音點因為鄰域半徑的增加被歸為相鄰的群集中。

  2. 令eps的值不變,增長min_points的值,修改為25
    對比效果如下:(左邊的是修改後的分類效果)

    經過實驗,可以發現在eps的值不變,緩慢增加min_points的值,可以發現類別會增多

這兩個修改引數的實驗效果是和基於密度聚類的演算法思想是吻合的。

聚類效果對比分析

案例1:鳶尾花資料集

使用sklearn.datasets中的iris資料集,對資料集進行降維處理,得到本次實驗所需的二維資料集,用已知結果對兩種演算法進行聚類效果的對比分析。

資料分佈:


原始標籤下的資料分佈:

使用原始標籤下的資料分佈,來對編寫的kmeans演算法和dbscan演算法的聚類效果進行對比分析


聚類效果對比:
左圖1是kmeans演算法的聚類效果,k設定為3
右圖上是DBSCAN演算法的一個出現二分類聚類效果的引數設定
右圖下是DBSCAN演算法的一個出現三分類聚類效果的引數設定
(紅星標誌為噪音點)

通過上述視覺化展示的直觀對比分析,可以發現DBSCAN能夠降低噪聲的干擾,但是聚類的結果與引數的設定有很大的關係,受人為因素的影響很大。受引數的影響,在實驗過程中,引數調整後會發現,密度較大且聚集相近的簇會被併到一個簇中,如上圖右上的兩類就被合併成一類。

案例2:雙月資料集

資料分佈:

聚類效果對比:
左圖是kmeans演算法的聚類效果,k設定為2
右圖是dbscan演算法的聚類效果,引數除錯(eps=0.1,min_points=3)使得分類類別數為2

這個實驗,意圖是想要得到dbscan演算法的分類結果,但是由於kmeans演算法的是各個點距離中心點的距離,因為演算法本身的原因造成了實驗結果的不理想,對於這類凹形狀的資料分佈,Kmeans演算法是不適合的,而dbscan的表現是很好的。




附加實驗:Kmeans聚類結果探究

問題點一:初始中心點選取對實驗結果的影響

初始中心點選取辦法一:

1,找到樣本點兩個維度上的最大值和最小值
2,選取中心點的座標時,是隨機選取樣本點各維度的 [ 最小值, 最大值 ] 範圍內的整數值;

# D為資料集
center_x = np.random.randint(D.min(0)[0], D.max(0)[0], size=k)
center_y = np.random.randint(D.min(0)[1], D.max(0)[1], size=k)
centers = np.array(list(zip(center_x, center_y)), dtype=D[:,0].dtype)

這樣做是考慮讓初始中心點的位置在樣本點整體範圍內分散開;但是缺點是會受到離群點的較大幹擾

初始中心點選取辦法二:

centers = D[np.random.randint(0, len(D), size=k)]

這樣就是在資料集中隨機選取k個點,隨機性較高,不考慮初始中心點分佈位置

問題點二:測試資料集的選用對實驗結果的影響

實驗展示:

資料集一:
實驗中選用的測試集的分佈如下:總樣本點為1502

實驗對比: 左圖是我的K-Means演算法聚類的效果,右圖是sklearn庫中的KMeans模型的聚類效果。

輸入: k=2

左圖的各類樣本點數量及中心點座標:

右圖的各類樣本點數量及中心點座標:

這是二分類多次實驗結果對比發現,結果是完全一致的,並且經過多次的執行,絕大部分兩個模型跑出來的都是一致的結果

輸入: k=4

k=4實驗結果一: 中心點位置和各類樣本點的分佈與模型跑出來的結果出現了明顯差異
左圖的各類樣本點數量及中心點座標:

[218, 395, 397, 492]

[[ 1.39172245 0.03217653]
[-0.76217937 0.54538487]
[ 0.48779272 -0.26739042]
[ 0.30262734 0.81337307]]

右圖的各類樣本點數量及中心點座標:
[218, 385, 428, 471]

[[ 0.74221635 -0.01429847]
[ 0.23034468 0.82802393]
[ 1.72011121 -0.08372242]
[-0.77602224 0.53373488]]

k=4實驗結果二:這次選擇的結果是和模型跑出結果相似的情況,可以看到樣本的分類結果以及中心點的位置都是非常接近的。
左圖的各類樣本點數量及中心點座標:

[219, 385, 427, 471]

[[ 0.74115753 -0.01307078]
[-0.77602224 0.53373488]
[ 0.23034468 0.82802393]
[ 1.7177104 -0.08579915]]

右圖的各類樣本點數量及中心點座標:
[216, 385, 429, 472]

[[ 0.74399 -0.01948443]
[ 0.23284281 0.8270763 ]
[-0.77602224 0.53373488]
[ 1.72492047 -0.07981352]]

資料集二

實驗中選用的測試集的分佈如下:總樣本點為178

實驗對比: 左圖是我的K-Means演算法聚類的效果,右圖是sklearn庫中的KMeans模型的聚類效果。

k=3

k=3實驗結果一

k=3實驗結果二

k=4

k=4實驗結果:樣本點分佈的區域出現了差異
左圖的各類樣本點數量及中心點座標:
[23, 32, 57, 66]
[[-294.43555355 -2.01756568]
[ 270.62843354 1.89743236]
[ 591.6978191 -4.30663452]
[ -49.76163471 3.00866831]]
右圖的各類樣本點數量及中心點座標:
[23, 39, 57, 59]
[[ -87.66016499 1.67523347]
[ 591.6978191 -4.30663452]
[ 238.80453682 3.61824549]
[-311.41187791 -2.47189043]]

綜上實驗得到結論:

  1. K值相同時,Kmeans演算法模型在每次執行得到的分類結果一般是不同的,這是受到演算法中初始中心點選擇的影響
  2. 如果選取的資料集的分佈是很明顯的幾個簇,並且選取的k與簇的個數一致時,初始中心點的選取對最後的實驗結果的影響就很小了。

實驗相關參考:

  1. 聚類和分類資料的資料集

  2. Python中Matplotlib 繪圖 marker的型別標示方法-備忘

  3. 多元統計分析——聚類分析——鳶尾花資料集在K-均值、層次、DBSCAN上的比較