跳至主要內容

    DBSCAN 與階層分群:群集不是只有圓形

    用鄰域、密度與合併準則,重新定義哪些點應該在一起

    sunny 桑尼· · 19 MIN READ · 284 次瀏覽

    DBSCAN 與階層分群:群集不是只有圓形
    本文目錄

    K-means把點分給平均中心,但有些群更像沿著彎曲路徑聚在一起,有些點則離任何群都很遠。這時候,「靠近同一個中心」可能不是你需要的相似定義。

    DBSCAN用局部鄰域的密度把群擴張,也允許部分點成為噪音;階層分群則保留從小群合併成大群的過程。兩者都需要你決定距離及參數,並不會免除特徵選擇,也不保證自動找出唯一正確的群。

    DBSCAN先問:附近是否有足夠多的點?

    圖1|Chire的DBSCAN-Illustration:紅色點是核心點,黃色B/C是邊界點,藍色N是噪音。圓表示相同半徑的鄰域,黑色連線帶細箭頭,示意由核心向鄰居的方向;核心間可雙向,指向B/C不表示邊界點能反向擴張。作者標minPts=3;本篇依sklearn把點本身算入鄰域。來源:Chire,原作品頁,CC BY-SA 3.0 Unported;原始SVG未改。

    先決定距離,再指定兩個重要參數:eps是鄰域半徑,min_samples是成為核心點所需的鄰域樣本數。本文無樣本權重,因此直接數點,而且包含自己。

    點的類型這裡的判斷方式

    核心core
    eps鄰域內的點數,含自己,至少達到min_samples
    邊界border
    自己不夠核心條件,但位於某核心點的eps鄰域
    噪音noise
    無法由核心群擴張納入,最後留下的點

    圖中的N雖然有一個大圓,圓也和其他圓重疊,但這不代表它和那些點的距離已落在eps內。判斷的是點到點的距離和核心條件,不是兩個鄰域圓有沒有相交。

    群可以沿核心點延伸,邊界點不負責繼續擴張

    由一個核心點出發,把它的鄰居納入;遇到另一個核心點,就繼續展開那個核心點的鄰域。這讓群可以沿著密度足夠的路徑延伸,而不必圍繞單一中心。

    非核心的邊界點可以被納入,但不能只靠它把兩個本來分離的核心群接成一群。圖1的B與C經由中間的核心群相連,不表示B能單憑自己的鄰域一路擴張到C;「由核心點可達」與任意兩點能彼此直接到達也不同。

    若某個非核心點同時鄰近兩個不同核心群,其歸屬可能受走訪順序影響。群號本身也只是標記。因此檢查穩定性時,需要分清核心結構、共享邊界和單純群號置換,不是看到labels改一個數字就當算法完全失效。

    五個點,直接數一次鄰域

    一維資料0、0.1、0.2、0.4、2.0,設eps=0.21、min_samples=3。半徑刻意選0.21,避免十進位邊界的浮點比較分散這個小例子的重點。

    位置eps內鄰居,含自己點數結果
    00、0.1、0.23核心
    010、0.1、0.23核心
    020、0.1、0.2、0.44核心
    040.2、0.42邊界,接在0.2核心旁
    202.01噪音

    四個近點在同群,最後一點標−1。這也說明eps不是「整個群的最大直徑」:0與0.4距離0.4,大於eps,仍可經核心鄰域連在同群。

    有噪音標記,不代表已判定真實異常

    圖2|來源750筆合成blobs、三個指定生成中心、cluster_std=.4、seed=0,先做StandardScaler。這是分群前散點,沒有用顏色畫生成標籤。來源:The scikit-learn developers,Demo of DBSCAN clustering algorithm,BSD-3-Clause;原圖未改。

    圖3|同一資料設eps=.3、min_samples=10,得到三群與18個噪音。大點是core,小點是非core,黑色是noise;不是兩張不同參數的實驗。來源及授權同圖2,原圖未改。本篇程式另重算相同資料/模型設定,得到679個core;沒有重畫來源圖或重跑其全部評估指標。

    −1只表示這個距離與密度設定下未被分入群,可能是有意義的稀有樣本、雜訊,也可能是eps太小。它不是已完成調查的詐欺、故障或其他真實異常標籤。

    eps太小,可能留下很多噪音、切碎群;太大,可能把不同群接起來。資料尺度一變,同一個0.3代表的鄰近程度也不同。不同群密度差很多時,一個共同eps不一定能同時保住稀疏群又分開密集群;高維距離也可能變得不易區分。不要把「可找非凸形狀」擴成任何資料都有效。

    sklearn的DBSCAN沒有和K-means相同的標準predict介面。重新對一批資料fit,鄰域與群可能改變;若要把新點指派給既有核心群,需要另定規則、處理多群可達和無群可達的情況,並驗證那個新增流程。不能把最近中心的程式換個類別名就當部署完成。

    階層分群保留的是合併過程

    凝聚式階層分群從每筆一群開始,每次選兩群合併,再重算群之間的連結關係。重複之後得到一棵樹;你可以依群數或高度選一個切法,而不只拿到單一固定分割。

    圖4|來源Iris原四維特徵、未先標準化,AgglomerativeClustering採預設Ward;先建完整樹,圖只顯示上面三層。括號數字是子樹樣本數,無括號的數字是單點索引。x排列不是一個連續資料欄位;y是合併高度,不是時間、機率或所有方法通用的兩點原距離。來源:The scikit-learn developers,Plot Hierarchical Clustering Dendrogram,BSD-3-Clause;原圖未改。

    看樹狀圖時,兩個枝在較低的位置接起來,表示依這個linkage準則比較早、在較小高度合併。你可以想像畫一條水平線,線以下尚未互相合併的子樹各成一群;實際閾值剛好等於合併高度時,邊界比較規則也要依工具確認。

    關鍵在於:兩群「距離」如何定義?改linkage,即使資料相同,也可能合出不同的樹。

    四種linkage,用同一對群算出四個高度

    準則如何比較兩群

    single
    跨兩群所有點對中,最短的距離
    complete
    跨兩群所有點對中,最長的距離
    average
    跨兩群所有點對的距離平均
    Ward
    選擇讓群內平方誤差和增加最少的合併;要求相容的歐氏幾何

    single容易沿鄰近點串成長鏈;complete要求連最遠的兩點也不能太遠;average看所有跨群點對;Ward則關心平方誤差增量。Ward不是「兩個中心的普通距離」的另一個名字,它還會考慮兩群的樣本數。

    一維資料0、2、7、9,先把0/2合成A、7/9合成B,兩群中心為1、8,各有2筆。接下來:

    跨群距離:|0−7|=7、|0−9|=9、|2−7|=5、|2−9|=7
    single = 5
    complete = 9
    average = (7+9+5+7)/4 = 7
    
    Ward的SSE增量 = nA*nB/(nA+nB) * ||meanA−meanB||²
                  = 2*2/4 * (1−8)² = 49
    

    sklearn這裡回傳的Ward合併高度是√(2×SSE增量),所以最後高度√98≈9.899495,不能把49直接填成圖的y軸高度。四種準則在這個小例子的前兩次高度都為2,但這不代表它們對一般資料一定有相同合併順序。

    同一份資料換準則,形狀也會跟著變

    圖5|官方六列比較中的同心圓一列:每列1,500筆、先標準化;本列明確要求兩群。左到右single、average、complete、Ward,這是四個不同準則,不是四個迭代步驟。來源:The scikit-learn developers,Comparing different hierarchical linkage methods on toy datasets,BSD-3-Clause;從原圖保留共同欄標與第一列,手機四欄上下排。

    在這個來源例,single比較能沿同一個圓的鄰接結構合併;其他準則得到不同切法。這說明準則改變「什麼形狀算近」,不是single在任何資料都比較正確。

    圖6|來源第二列noisy moons、1,500筆、noise=.05、seed=170,要求兩群,欄順序同圖5。截取原第二列並移入原共同欄標,手機逐欄排;點、顏色及原計時未改。不同列是不同合成資料,不是DBSCAN的eps搜尋過程。來源及BSD-3-Clause授權同圖5。

    形狀不是唯一因素。如果少量點在兩個區域之間形成橋,single也可能因最近點很近而一路串接;是否希望保留這種連結,要回到任務與噪音條件。

    圖7|來源第三列不等變異blobs、1,500筆、三個生成分布std=1/2.5/.5,要求三群。欄順序同圖5;截取該列並移入原欄標,手機逐欄排。右下角秒數為其硬體與實驗的計時,不是本站實測或通用速度排名。來源及BSD-3-Clause授權同圖5。

    以上只選了原例三列來看準則差異;沒有把全部聚類工具排成一個通用排行榜。參數與生成群數在來源已經設定,這不是各工具在未知資料上自行選群數的公平競賽。Ward在某些來源例看起來較穩,也不能推成所有密度、維度或噪音下的最佳方案。

    用程式核對密度與合併高度

    以下在WSL、Python 3.10.12、NumPy 1.26.3、scikit-learn 1.5.1實跑。它做三件限定的事:驗五點鄰域、重算來源750點DBSCAN的資料與模型設定、驗四點的四種linkage。沒有重跑來源整套計時,也沒有重畫原圖。

    將程式存成density_and_linkage.py後執行:

    python -m pip install numpy==1.26.3 scikit-learn==1.5.1
    python density_and_linkage.py
    
    import numpy as np
    import sklearn
    from sklearn.cluster import DBSCAN, AgglomerativeClustering
    from sklearn.datasets import make_blobs
    from sklearn.preprocessing import StandardScaler
    
    print(f"numpy={np.__version__} sklearn={sklearn.__version__}")
    X = np.array([0.0, 0.1, 0.2, 0.4, 2.0]).reshape(-1, 1)
    eps, min_samples = 0.21, 3
    distances = np.abs(X - X.T)
    counts = (distances <= eps).sum(axis=1)
    db = DBSCAN(eps=eps, min_samples=min_samples).fit(X)
    core = set(db.core_sample_indices_)
    kinds = ["core" if i in core else "noise" if label == -1 else "border"
             for i, label in enumerate(db.labels_)]
    print(f"toy neighbor_counts_including_self={counts.tolist()}")
    print(f"toy core_indices={db.core_sample_indices_.tolist()}")
    print(f"toy point_types={kinds}")
    print(f"toy labels={db.labels_.tolist()}")
    
    # Recompute only the official DBSCAN example's data/model setup, not its plots
    # or full metric benchmark. Ground-truth labels are not passed to the model.
    raw, _ = make_blobs(n_samples=750,
                        centers=[[1, 1], [-1, -1], [1, -1]],
                        cluster_std=0.4, random_state=0)
    scaled = StandardScaler().fit_transform(raw)
    source_setup = DBSCAN(eps=0.3, min_samples=10).fit(scaled)
    labels = source_setup.labels_
    n_noise = np.sum(labels == -1)
    n_clusters = len(set(labels)) - int(-1 in labels)
    print(f"source_setup n={len(raw)} clusters={n_clusters} noise={n_noise} "
          f"core={len(source_setup.core_sample_indices_)}")
    
    points = np.array([0.0, 2.0, 7.0, 9.0]).reshape(-1, 1)
    for method in ["single", "complete", "average", "ward"]:
        tree = AgglomerativeClustering(
            n_clusters=None, distance_threshold=0.0,
            metric="euclidean", linkage=method,
        ).fit(points)
        print(f"{method} children={tree.children_.tolist()} "
              "heights=" + ",".join(f"{x:.6f}" for x in tree.distances_))
    delta_sse = (2 * 2 / (2 + 2)) * (1.0 - 8.0) ** 2
    print(f"ward_final_delta_SSE={delta_sse:.6f} "
          f"height=sqrt(2*delta)={np.sqrt(2 * delta_sse):.6f}")
    

    實跑五點的鄰居數為3、3、4、2、1;核心索引0/1/2,點型別為core、core、core、border、noise,labels是0、0、0、0、−1。750點設定得到三群、18 noise、679 core,與圖2–3的模型條件相符。

    四點的實際合併如下,children_中的編號0–3代表原始點,之後4與5代表前兩次新形成的群:

    linkage本例的合併對合併高度
    single[0,1]、[2,3]、[4,5]2、2、5
    complete[0,1]、[2,3]、[4,5]2、2、9
    average[0,1]、[2,3]、[4,5]2、2、7
    Ward[0,1]、[2,3]、[4,5]2、2、9.899495

    程式用distance_threshold=0與n_clusters=None建完整樹,此設定也讓distances_提供這裡要讀的合併高度;不能把children_中的節點編號當距離。當前示範使用metric="euclidean",沒有照搬較舊介面的affinity名稱。

    最後要檢查的是你的群定義

    你需要什麼可以先探索什麼仍要驗證什麼
    沿局部密度連成群,允許部分點不歸群DBSCAN距離尺度、eps、min_samples、不同密度與邊界穩定性
    保留由細到粗的群關係凝聚式階層分群linkage含義、合併高度、切群規則與計算資源
    固定中心,對每批新資料直接指派前篇K-means之類的固定規則中心/縮放版本、群號對齊與新分布

    對DBSCAN計算silhouette時,也要交代是否保留−1:若把所有噪音當同一個label,便不是忽略噪音的同一個分數。若先排除噪音,需檢查留下的群數與樣本數是否符合指標條件。數字可以協助比較,但不會替你定義哪種群才有意義。

    群的形狀、密度與階層是不同觀點。選擇方法時,先說清楚你希望哪些點因為什麼理由放在一起,再用原圖、數值與新資料檢查結果,會比只挑一張最漂亮的彩色散點圖可靠。

    原始來源、配圖與授權

    技術依據sklearn clustering章、DBSCAN官方原例、Dendrogram原例、Linkage比較原例及完整程式與Chire原作品說明。查核日2026-10-31;線上1.5系列頁目前標1.5.2,本篇實跑1.5.1,stable原圖來源也可能較新。

    圖1採Chire原始SVG,CC BY-SA 3.0 Unported;封面將完整圖1等比渲染加白邊,該封面改作同樣依CC BY-SA 3.0提供,不限制於非商業用途。此ShareAlike適用該圖及其改作,不將本站獨立教學文字冒稱為該圖改作。沒有重刊權利未確認的1996論文截圖,也沒有暗示原作者背書。

    圖2–7採The scikit-learn developers圖像,BSD-3-Clause。圖2–4未改;圖5–7取原linkage比較的第一、二、三列,各保留原共同欄標;手機把每列四個完整面板按single、average、complete、Ward順序上下排,原有點、色彩、計時均保留,未重畫資料。讀者可由來源連結查看完整六列。

    五點DBSCAN與四點linkage是本站自行設計的教學驗算;750點只重算來源的資料/模型設定,未宣稱重現其全部指標或計時。scikit-learn圖像完整聲明如下。

    BSD 3-Clause License
    
    Copyright (c) 2007-2026 The scikit-learn developers.
    All rights reserved.
    
    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:
    
    * Redistributions of source code must retain the above copyright notice, this
      list of conditions and the following disclaimer.
    
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    
    * Neither the name of the copyright holder nor the names of its
      contributors may be used to endorse or promote products derived from
      this software without specific prior written permission.
    
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    

    留言0

    登入後就能留言。

    登入
    sunny 桑尼
    sunny 桑尼

    AI 資料科學 · 產品經理 · 蒙奇 D 桑尼

    專注於 AI 工具應用與資料科學教育,致力於讓每個人都能用 AI 提升工作效率。曾任職於科技業,現為全職教育創作者。

    讀更多文章 →想自己動手試?領免費 Starter Kit →