聚類方法之 HDBSCAN —— 層次DBSCAN 的原理分析

HDBSCAN

HDBSCAN(Hierarchical Density-Based Spatial Clustering of Applications with Noise)是由Campello,Moulavi和Sander開發的聚類算法。 它通過將DBSCAN轉換爲分層聚類算法來擴展DBSCAN,然後基於聚類穩定性,使用了提取平面聚類地技術。

和傳統DBSCAN最大的不同之處在於,HDBSCAN可以處理密度不同的聚類問題。

本文和HDBSCAN的論文不同,將不從DBSCAN出發。相反的,本文將從該算法如何與Robust Single Linkage(魯棒單鏈接算法)緊密聯繫出發,並在其上面進行平面羣集提取。

在我們開始之前,我們將加載我們需要的庫,以及設置我們的可視化代碼。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import sklearn.datasets as data
%matplotlib inline
sns.set_context('poster')
sns.set_style('white')
sns.set_color_codes()
plot_kwds = {'alpha' : 0.5, 's' : 80, 'linewidths':0}

接下來我們需要的是一些數據。 爲了說明一個例子,我們需要數據量相當小,以便我們可以看到發生了什麼。  sklearn具有生成樣本聚類數據的工具,因此我將利用它並創建一百個數據點的數據集。

moons, _ = data.make_moons(n_samples=50, noise=0.05)
blobs, _ = data.make_blobs(n_samples=50, centers=[(-0.75,2.25), (1.0, 2.0)], cluster_std=0.25)
test_data = np.vstack([moons, blobs])
plt.scatter(test_data.T[0], test_data.T[1], color='b', **plot_kwds)

現在,解釋HDBSCAN的最佳方法就是實現它,剛開始不要因爲作爲一個調包俠。 所以讓我們加載hdbscan庫並開始工作。

import hdbscan

clusterer = hdbscan.HDBSCAN(min_cluster_size=5, gen_min_span_tree=True)
clusterer.fit(test_data)

HDBSCAN(algorithm='best', alpha=1.0, approx_min_span_tree=True,
    gen_min_span_tree=True, leaf_size=40, memory=Memory(cachedir=None),
    metric='euclidean', min_cluster_size=5, min_samples=None, p=None)

那麼現在我們已經聚集了數據 ,但算法背後究竟發生了什麼? 我們可以將其分解爲一系列步驟:

  1. 根據密度/稀疏度變換空間(Transform the space)
  2. 構建距離加權圖的最小生成樹(Build the minimum spanning tree)
  3. 構建集羣層次結構(Build the cluster hierarchy)
  4. 根據最小簇大小壓縮集羣層次結構(Condense the cluster tree)
  5. 從壓縮樹中提取穩定羣集(Extract the clusters)

根據密度/稀疏度變換空間(Transform the space)

爲了找到簇,就如我們希望在稀疏的噪聲海洋中找到高密度的島嶼 - 並且噪聲的假設很重要。因爲真實數據是混亂的,它可能具有異常值,數據損壞和噪聲。

聚類算法的核心是單鏈路聚類(single linkage clustering),它可能對噪聲非常敏感:一個噪聲數據點如果正好在兩個簇之間,那麼它可能就可以充當島嶼之間的橋樑,將它們粘合在一起(導致錯誤的聚類)。 我們希望我們的算法對噪聲具有魯棒性,因此我們需要找到一種方法來實現“降低海平面”,然後再運行單鏈路聚類算法。

但我們應該如何在不進行聚類的情況下描述“海洋”和“土地”的特徵?只要我們能夠估算出密度,我們就可以將低密度點視爲“海洋”。 這裏的目標不是完全準確地區分“海洋”和“陸地” ,因爲這是聚類的第一步,而不是輸出 。只是爲了使我們的聚類核心算法對噪聲更加魯棒。因此,爲了“海洋”的識別,我們希望降低海平面,更具體地來說,這意味着使“海洋”點與彼此和“土地”之間的距離更遠。

然而,這只是直覺分析。 那麼它在實踐中如何實現? 我們需要一個計算量很小的密度估計方法,最簡單的是KNN( kth nearest neighbor)。 如果我們有數據的距離矩陣(無論如何我們將迫切需要),我們可以簡單地讀取它。 讓我們將其形式化並(遵循DBSCAN,LOF和HDBSCAN文獻)定義核心距離:點 x (參數 k )的核心距離 \mathrm{core}_k(x)

現在我們需要一種方法來分散低密度點(具有高核心距離)。 這樣做的簡單方法是在點之間定義一個新的距離來度量。我們將調用(再次遵循文獻)相互可達度量距離(mutual reachability distance)。 我們定義相互可達度量距離如下:

                      

其中d(a, b) 是a和b之間的距離。 在這個度量下,密集點(具有低核心距離)保持彼此相同的距離,但是稀疏點被推開至少遠離任何其他點的核心距離。 這有效地“降低了海平面”,及散佈了稀疏的“海洋”點,同時保持“土地”不受影響。

這裏需要注意的是,這效果顯然取決於k的選擇; 較大的k值將更多的點歸在“海洋”中。 從圖片中可以更好的解釋上述內容,所以讓我們設k的值爲5。 然後,對於給定的點,我們可以根據核心距離來繪製一個圓圈,圓圈接觸到第六個(包括計算點本身)最鄰近的點,如下所示:

選擇另一個點,做同樣的計算,但這次是一組不同的鄰居(其中一個是我們挑選的第一個點)。

爲了更好的測量,我們運用另一組六個最近鄰和另一個半徑略有不同的圓。

現在,如果我們想知道藍點和綠點之間的相互可達度量距離,我們可以通過從繪製綠色和藍色之間的距離來開始:

穿過藍色圓圈,但不穿過綠色圓圈 - 綠色的核心距離大於藍色和綠色之間的距離。 因此,我們需要將藍色和綠色之間的相互可達度量距離標記爲更大的 - 等於綠色圓圈的半徑。

 

另一方面,從紅色到綠色的相互可達度量距離簡單地是從紅色點到綠色點的距離,因爲該距離大於任一核心距離(即距離箭頭穿過兩個圓圈)。

 

通常,有一個證明,表明以相互可達度量距離作爲一種變換方式,可以讓單鏈路聚類更加貼合地去擬合我們的樣本層次分佈,無論我們的樣本的真實密度是什麼樣的。

構建最小生成樹(Build the minimum spanning tree)

至此,我們在數據集上已有了一個新的相互可達度量距離,然後我們希望可以在密集的數據上找到“島嶼”。當然,密集的區域也是相對的,不同的島嶼可能有不同的密度。從概念上來說,我們將要做的是:

將數據視爲加權圖,數據點作爲頂點,而任意兩個頂點之間邊的權重,等於兩點之間的相互可達度量距離。

現在我們開始考慮一個閾值,從一個較高的值開始,然後逐步降低。我們丟棄加權圖中權重超過這個閾值的所有邊。在我們丟棄邊緣的同時,我們將斷開的圖連接到已有的分組。最終,我們將在不用的閾值水平上獲得一個層次的結構(從完全連接到完全斷開連接)。

然而在實踐過程中,這個操作是非常昂貴的,因爲有n^2條邊,我們並不希望算法的時間複雜度到這個級別。所以我們引入了最小生成樹,因爲只要斷開最小生成樹中的一條邊,就可以獲得一個完全分離的組。

我們可以通過Prim算法非常有效地生成一個最小生成樹。你可以看到下面HDBSCAN的最小生成樹,需要注意的是:該最小生成樹的權重是兩點間的相互可達度量距離。在這個案例中,我們令k=5。

當數據存儲於度量空間時,我們可以運用更快的方法,例如 Dual Tree Boruvka 來構建最小生成樹。

clusterer.minimum_spanning_tree_.plot(edge_cmap='viridis',
                                      edge_alpha=0.6,
                                      node_size=80,
                                      edge_linewidth=2)

構建集羣層次結構(Build the cluster hierarchy)

給定最小生成樹,下一步是將其轉換爲連接組件的層次結構。 這最容易以相反的順序完成:按距離(按遞增順序)對樹的邊進行排序,然後迭代,爲每個邊創建一個新的合併集羣。 這裏唯一有困難的部分是,對每一條邊,如何選擇是否將該邊連接在的兩個簇聚在一起。但這很容易通過union-find的數據結構來實現。 我們可以將結果繪製成樹形圖,如下所示:

clusterer.single_linkage_tree_.plot(cmap='viridis', colorbar=True)

這個方法可以讓我們得到最終點,也就是單鏈接的最終點。但我們想要的更多,雖然集羣的層次結構很好,但是我們需要的其實是一組平面聚羣。我們可以在上圖中會議一條水平線,並選擇它所切割的簇來實現這一點。但實際上,這也就是DBSCAN做的事情,但問題是,我們如何知道這條線在哪繪製? DBSCAN只是將其作爲一個參數(非常不直觀)。 更糟糕的是,我們真的想要處理變密度簇,任意選擇的切割線都是一個固定的密度水平。 理想情況下,我們希望能夠在不同的地方切割樹以選擇我們的聚類。 這就是HDBSCAN的下一步開始並與強大的單一鏈接產生差異的地方。

壓縮聚類樹(Condense the cluster tree)

類羣提取的第一步是將大而複雜的類羣層次結構縮小爲一個較小的樹。正如你在上面的層次結構中所看到的那樣,通常情況下,羣集拆分是從羣集中分離出來的一個或兩個點; 這是關鍵點 - 我們不希望將其視爲一個分裂爲兩個新集羣的集羣,而是將其視爲一個“丟失點”的持久集羣(這句話博主不是非常的理解,如果你有更好的見解請留言討論)。爲了使這個具體化,我們需要引入一個最小簇大小的概念,我們將其作爲HDBSCAN的參數。

一旦我們得到最小簇大小的值,我們就可以遍歷層次結構,並在每次拆分時判斷:由拆分創建的新簇之一是否具有比最小簇大小更少的點。如果我們的點數少於最小簇大小,我們將其聲明爲“點落在類羣之外”並讓較大的羣集保留父節點的羣集標識,從而標記出哪些點落在了羣集之外,以及這種情況發生的時候  的距離值。

另一方面,如果拆分爲兩個集羣(每個集羣至少與最小集羣大小一樣大),然後我們視其爲真正的集羣拆分,並讓該拆分在樹中持續存在。在遍歷整個層次結構並執行此操作後,我們最終得到一個小得多的樹,其中包含更少的節點,每個節點都有關於該節點上集羣大小如何隨着距離變化的數據。我們可以將其可視化出來,類似於上面的樹狀圖 - 我們可以再次使用線的寬度表示聚類中的點數。然而,這一次,這個寬度會隨着離羣點而變化。對於使用最小簇大小爲5的數據,結果如下所示:

clusterer.condensed_tree_.plot()

這更易於查看和處理,尤其是與我們當前的測試數據集一樣簡單的聚類問題。 但是,我們仍然需要選擇要用作平面聚類的羣集。通過前述的內容,應該會給你一些關於如何做到這一點的思路。

提取簇(Extract the clusters)

直觀地說,我們希望選擇羣集應該具有持續存在並具有更長生命週期的特徵; 短壽命集羣可能僅僅是單鏈接方法的工件。 看看之前的圖片,我們可以認爲我們想要選擇那些在圖中具有最大顏色面積的類羣。 如果要進行平面聚類,我們需要添加進一步的要求,如果你選擇一個類羣,則無法再選擇所有該集羣的後代。 事實上,這個直觀概念正是HDBSCAN所做的。 當然,我們需要將這個理念具象化成爲一個具體的算法。

首先,我們需要一個與距離不同的度量來考慮簇的持續性; 在這裏我們將用距離的倒數來度量,即\lambda = 1 / distance對於給定的集羣,我們可以將值\lambda _{birth}定義爲當集羣拆分成功,併成爲它自己的子集羣時的\lambda值;將\lambda _{death }定義爲當羣集被分成較小的羣集時的\lambda值。 反過來,對於給定的集羣,對於該集羣中的每個點p,我們可以將值 \lambda _{p} 定義爲該點 '被從該集羣中剔除' 的\lambda值。現在,對於每個集羣,我們令它的穩定性

聲明所有葉節點都是選定的簇。 現在通過樹進行遍歷(反向拓撲排序順序)。 如果子集羣的穩定性之和大於父集羣的穩定性,那麼我們將集羣穩定性設置爲子穩定性的總和。 另一方面,如果羣集的穩定性大於其子節點的總和,那麼我們將羣集聲明爲選定羣集並歸併其所有後代。 一旦我們到達根節點,我們將當前選定的集羣稱爲平面聚類並返回該節點。

上述的過程可能是冗長而複雜的,但它實際上只是執行我們的 “選擇具有最大總墨水面積的圖中的聚類” 並受到後代約束(我們之前所提及的)的過程。 我們通過該算法可以在壓縮樹的樹形圖中選擇簇,運行下面的代碼你可以得到你期望的結果:

clusterer.condensed_tree_.plot(select_clusters=True, selection_palette=sns.color_palette())

現在我們已經擁有了類羣,運用 sklearn 的 API將其轉換爲集羣標籤就可以了。 不在選定聚類中的任何一個點都被視爲是一個噪點(並指定標籤-1)。 我們可以做更多的事情:對於每個集羣,我們爲該集羣中的每個點p都有\lambda _{p}; 如果我們簡單地將這些值標準化(因此它們的範圍從0到1),那麼我們可以衡量集羣中每個點的是該集羣成員資格的強度。 hdbscan庫將其作爲clusterer對象的probabilities_屬性返回。 因此,有了標籤和會員優勢,我們可以製作標準圖,根據聚類標籤選擇點的顏色,並根據成員的強度去飽和顏色(並使非聚集點純灰色)。

palette = sns.color_palette()
cluster_colors = [sns.desaturate(palette[col], sat)
                  if col >= 0 else (0.5, 0.5, 0.5) for col, sat in
                  zip(clusterer.labels_, clusterer.probabilities_)]
plt.scatter(test_data.T[0], test_data.T[1], c=cluster_colors, **plot_kwds)

這就是HDBSCAN的工作方式。 這可能看起來有點複雜 - 算法中有相當多的可抑制部分 - 但最終每個部分實際上非常簡單並且可以很好地進行優化。 

博主附:

在去天津比賽的火車上把這篇寫完了,感覺對這個算法有了更深刻的認識,其實這種算法也是揉合了很多工程性的方法,然後達到了很好的效果。

文中還是有一些地方博主有一些疑惑,希望大家有見解的話多多留言交流。

Reference:

How HDBSCAN Works

 

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