DBSCAN 跟 K-means 最大的性格差異:它不強迫每個點都有家。夠密的地方長出群(編號 0、1、2⋯⋯),密度不夠、沒被任何群收編的落單點,被標成 -1——雜訊點(noise point)。本頁實跑「大海撈針」,一字不差重現題目印出的那 8 個數字,拆解核心點/邊界點/噪點三種身分,再把 -1 在下游的專屬紀律(群數要減 1、silhouette 會被拖累、還能拿來抓異常)一次講完。
對標準化後資料執行 DBSCAN,取得部分分群標籤如下。其中 labels_ 或 fit_predict 結果中的 -1 代表什麼?
from sklearn.cluster import DBSCAN # 拿出密度分群工具 db = DBSCAN(eps=0.45, min_samples=5) # 半徑 0.45、鄰域至少 5 點(含自己) labels = db.fit_predict(X_scaled) # 分群,回傳每筆的標籤 print(labels[:8]) # 印出前 8 筆 # [ 0 0 -1 1 1 -1 0 1] # ← 執行結果:有 0、有 1,還有兩個 -1
想像一個廣場,人群自然聚成一圈一圈聊天。DBSCAN 的規則很直白:站得夠密才算一圈——圈子依序編號 0、1、2⋯⋯。但廣場上總有幾個人站在空曠處,方圓幾步內湊不齊人、也沒有任何圈子把他拉進去。DBSCAN 不會硬把落單的人塞進最近的圈子(那是 K-means 的作風),而是給他們一個特別的記號:-1。
所以讀 [ 0 0 -1 1 1 -1 0 1] 的正確方式是:第 1、2、7 筆在 0 號群,第 4、5、8 筆在 1 號群,第 3、6 筆不屬於任何群——它們是雜訊點。
點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
四行程式加一行輸出,一行一行走完。右上角的「看位置」可以把這一行放回完整程式裡看。
from sklearn.cluster import DBSCAN # 從「分群」抽屜拿出 DBSCAN
sklearn.cluster 是分群演算法的抽屜,上一頁的 KMeans 也住這裡。DBSCAN 的全名值得看一眼:Density-Based Spatial Clustering of Applications with Noise——名字的最後一個字就是 Noise(噪音)。它從演算法設計的第一天起,就打算把「不夠密的點」留在群外,而不是硬塞。
對照記憶:K-means 的世界觀是「每個點都必須有家」;DBSCAN 的世界觀是「夠密才成家,落單就標 -1」。這題考的就是這個世界觀。
db = DBSCAN(eps=0.45, min_samples=5) # 半徑 0.45;圈內至少 5 點(含自己)才算核心
組一台 DBSCAN,貼上 db 的名牌。兩個參數就是它的全部人生觀:eps=0.45 是鄰域半徑——以每個點為圓心、0.45 為半徑畫圈;min_samples=5 是密度門檻——圈內至少要有 5 個點(sklearn 的數法含自己),這個點才夠格當「核心點」。
題目說資料是「標準化後」的,所以 0.45 的單位是標準差的尺度,不是原始單位——這正是上一頁(分群前為什麼要 StandardScaler)鋪好的路:eps 是帶單位的半徑,先縮放它才有意義。
labels = db.fit_predict(X_scaled) # 對標準化資料分群,回傳每筆的標籤陣列
fit_predict 一口氣做完「找出所有核心點 → 把密度相連的核心點串成群 → 收編邊界點 → 剩下的標 -1」,回傳每一筆資料一個標籤的陣列。括號裡只有 X_scaled、沒有 y——分群是非監督學習,從頭到尾沒有人給過答案。
db.fit(X_scaled) 再讀 db.labels_,跟 fit_predict 的回傳值逐筆完全相同(np.array_equal → True)。一個是「做完順手給你」,一個是「事後從模型身上拿」,內容一字不差。print(labels[:8]) # 切片:從頭取 8 筆,印出來
[:8] 是切片——「從頭取到第 8 筆」。labels 是 numpy 陣列,印出來的長相跟 Python 串列不同:沒有逗號、用空格對齊,所以才會長成 [ 0 0 -1 ...] 這種樣子——-1 佔兩個字元,正數前面就多墊一格,讓每個數字右對齊。看懂這個排版,考場上就不會被「奇怪的空格」分心。
# [ 0 0 -1 1 1 -1 0 1] # 註解行=把執行結果貼回程式裡
逐筆讀:第 1、2、7 筆 → 群 0;第 4、5、8 筆 → 群 1;第 3、6 筆 → -1,不屬於任何群。光憑這 8 筆可以說「至少有 2 個群、至少有 2 個噪點」——注意是「至少」,後面 92 筆還沒看。
題目沒給 X_scaled 是什麼。為了讓每個數字都有出處,本站掃了上千組資料設定,找出一份「標準化後跑 DBSCAN(eps=0.45, min_samples=5)、印出來一字不差」的資料——最後在月牙資料 make_moons(n_samples=100, noise=0.22, random_state=1972) 撈到它:print(labels[:8]) 印出的正是 [ 0 0 -1 1 1 -1 0 1],連 numpy 的對齊空格都一樣。
把印出來的 8 筆逐一「驗明正身」——每一筆去數它 eps=0.45 圈內有幾個點(含自己):
| 印出順位 | 印出的值 | eps 圈內點數(含自己) | 身分判定 |
|---|---|---|---|
| 第 1 筆 | 0 | 7 | 7 ≥ 5 → 核心點,屬於群 0 |
| 第 2 筆 | 0 | 7 | 7 ≥ 5 → 核心點,屬於群 0 |
| 第 3 筆 | -1雜訊點 | 3 | 3 < 5、圈內也沒有核心點收編 → -1 |
| 第 4 筆 | 1 | 7 | 7 ≥ 5 → 核心點,屬於群 1 |
| 第 5 筆 | 1 | 11 | 11 ≥ 5 → 核心點,屬於群 1 |
| 第 6 筆 | -1雜訊點 | 3 | 3 < 5、圈內也沒有核心點收編 → -1 |
| 第 7 筆 | 0 | 6 | 6 ≥ 5 → 核心點,屬於群 0 |
| 第 8 筆 | 1 | 6 | 6 ≥ 5 → 核心點,屬於群 1 |
DBSCAN 給每個點的身分判定只有兩條規則,依序問:
第一問:以你為圓心、eps 為半徑畫圈,圈內點數(含自己)≥ min_samples 嗎?是 → 你是核心點,有資格長群、也有資格收編別人。
第二問(第一問沒過才問):你的圈內有沒有任何核心點?有 → 你被收編為邊界點,拿那個核心點所屬群的正常群號;沒有 → 你是雜訊點,標 -1。
db.core_sample_indices_),只有噪點拿 -1。換一份 300 筆的乾淨月牙(DBSCAN 的主場:兩道彎彎的弧,K-means 最怕的形狀),標準化後用 eps=0.25, min_samples=5 實跑。同一批點、三種視角切換著看:
「這筆資料是不是雜訊點」不是寫在資料裡的事實——它是 eps 與 min_samples 畫出來的。同一份 300 筆月牙,把兩個旋鈕都掃一遍(本站實跑 20 組):
eps=0.15, min_samples=20 門檻嚴到沒人及格——300 筆全部標 -1、0 個群;eps=0.50, min_samples=5 半徑大到人人都是鄰居——1 群、0 個噪點(連兩個月牙都被黏在一起)。同一份資料,噪點數從 0 到 300 都做得出來。所以「-1 代表什麼」的精確答案是:在這組 eps 與 min_samples 定義的密度下,該點未被納入任何密度群集——參數換了,名單就換。-1 混在 labels 裡,下游每一步都要記得它「不是群」。三個最常踩的坑,配上標準寫法:
import numpy as np # 拿出陣列工具 print(np.unique(labels, return_counts=True)) # 每種標籤各幾筆:-1 有 12 筆 n_clusters = len(set(labels)) - (1 if -1 in labels else 0) # 數群數:-1 要扣掉 mask = labels != -1 # 真假名單:非噪點為 True X_kept = X_scaled[mask] # 只留被分到群的點做後續分析
mask = labels != -1 排除後是 0.384——-1 那「群」的成員散落四處,硬算會把整體分數拖低。群數也一樣:len(set(labels)) 數出 3({0, 1, -1}),但真正的群只有 2 個。outliers = X_scaled[labels == -1] 就撈出異常名單(實跑 7 筆)。這是 DBSCAN 常被拿來做異常偵測的原因。但要記得 05 節的教訓:名單是參數畫出來的,調一下 eps 名單就變,撈出來仍要人工檢視。群號從 0 開始編——第一個群集的正式編號是 0,題目印出的 8 個數字裡自己就有。-1 被刻意選在群號序列(0, 1, 2, ⋯)之外,正是為了讓「不屬於任何群」在數字上一眼可辨。實跑重現的資料共 4 群,編號 0~3,-1 從頭到尾不在編號隊伍裡。
缺值連 DBSCAN 的門都進不了——本站實跑把一筆資料改成 NaN 再餵進去,直接拋出 ValueError: Input X contains NaN. DBSCAN does not accept missing values(sklearn 原文),根本輪不到輸出 labels。-1 說的是「位置太空曠」,不是「資料缺了格」——缺值要在前處理階段先補或先刪。
實跑逐字驗證:那兩個 -1 的點,eps=0.45 圈內只有 3 個點(含自己)湊不滿 min_samples=5——當不成核心點;圈內也沒有任何核心點的鄰域覆蓋它們——當不成邊界點;於是被標 -1:未被納入任何密度群集的雜訊點。這是 DBSCAN 的設計哲學:夠密才成群,落單不硬塞。
分群是非監督學習——fit_predict(X_scaled) 括號裡只有 X 沒有 y,演算法從頭到尾不知道任何「真實標籤」,自然談不上「已知」;「負類(negative class)」更是監督式分類的詞彙,用在分群身上是張冠李戴。DBSCAN 的 -1 是幾何與密度的判定,跟任何真值無關。
再看一次同一段程式。這次你知道那兩個 -1 經歷了什麼了。
對標準化後資料執行 DBSCAN,取得部分分群標籤如下。其中 labels_ 或 fit_predict 結果中的 -1 代表什麼?
db = DBSCAN(eps=0.45, min_samples=5) # 「夠密」的定義:半徑 0.45 內至少 5 點(含自己) labels = db.fit_predict(X_scaled) # 分群(labels_ 與回傳值完全相同) print(labels[:8]) # 印前 8 筆 # [ 0 0 -1 1 1 -1 0 1] # 群 0、群 1⋯⋯與兩個不屬於任何群的 -1
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
fit_predict 分群拿標籤、第 4 行印前 8 筆、第 5 行是貼回來的輸出。fit_predict(X_scaled) 裡只有 X 沒有 y——分群是非監督學習。len(set(labels)) - (1 if -1 in labels else 0);直接 len(set) 會把 {0, 1, -1} 數成 3 群。mask = labels != -1 排除後 0.384;-1 那「群」成員四散,硬算會拖低分數。labels == -1 一行撈出離群候選名單——DBSCAN 兼職異常偵測的原因;但名單隨參數變,撈出後仍要檢視。ValueError: Input X contains NaN,輪不到輸出 -1。D 的死因:非監督沒有「真實標籤」,「負類」是監督式分類的詞彙。「DBSCAN 的 -1/核心點邊界點噪點」是中級科目三非監督分群的高頻考點,而且問法很固定。最常見的六種:① 給一段 labels 輸出問 -1 是什麼(本題這種,答案往「未被納入任何密度群集的雜訊點」靠);② 問核心點的條件(eps 鄰域內點數 ≥ min_samples,含自己);③ DBSCAN 與 K-means 的對照(要不要先給 k、能不能抓噪點、擅長什麼形狀——DBSCAN:不用給 k、有 -1、擅長不規則形狀);④ eps/min_samples 調整方向對噪點數的影響(門檻越嚴噪點越多);⑤ 有 -1 時群數怎麼數(len(set) 要減 1);⑥ DBSCAN 為什麼能兼職異常偵測(-1 就是離群候選名單)。一句口訣:夠密當核心、被收編當邊界、沒人要的標 -1——群號從 0 開始,-1 不是群。
不是。sklearn 的分群標籤從 0 開始編:第一個群集是 0、第二個是 1,依序下去(本站實跑的重現資料有 4 群,編號 0~3)。-1 被刻意放在這個序列之外,專門表示「不屬於任何群」——選它而不選別的數字,正是因為 0 以上都可能是合法群號,負數一眼就能跟群號區分。所以看到 -1 不要數成一個群:實跑 labels 的 set 是 {0, 1, 2, 3, -1},len 是 5,但群數是 4。順帶一提,群號本身也只是任意編號、不是類別標籤——哪群叫 0 純看演算法先從哪裡長起。
兩條規則依序問。第一問:以該點為圓心、eps 為半徑的鄰域內,點數是否 ≥ min_samples——是就是核心點。sklearn 的數法包含該點本身(文件原文:The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself),所以 min_samples=5 等於「自己+至少 4 個鄰居」。第二問(非核心才問):鄰域內有沒有任何核心點——有就是邊界點,被收編進那個核心點的群、拿正常群號;沒有就是噪點,標 -1。實跑範例:某點鄰域 16 點 → 核心;某點鄰域只有 2 點但那位鄰居是核心 → 邊界;某點鄰域只有自己 → -1。注意 labels 陣列裡邊界點與核心點無法區分(都拿群號),要分辨得看 db.core_sample_indices_。
因為「夠不夠密」不是資料的內在屬性,是這兩個參數定義出來的:eps 決定「多近算鄰居」、min_samples 決定「幾個鄰居才算密」。本站實跑同一份 300 筆月牙掃 20 組參數:eps=0.15+min_samples=20 時 300 筆全部是噪點(0 個群);eps=0.50 時 0 個噪點(但兩個月牙被黏成 1 群);中間的 eps=0.25+min_samples=5 才漂亮(2 群+7 噪點)。所以嚴謹的說法是:-1 代表「在這組參數定義的密度下」未被納入任何群集。考題問「把 eps 調小、min_samples 調大,噪點會怎樣」——門檻更嚴,噪點通常變多。
不會。K-means 的規則是「每一筆分給最近的質心」——每個點都必然有家,labels 取值只有 0 到 k-1,字典裡沒有 -1(實跑對照:DBSCAN 標的 7 個噪點,在 K-means 裡照樣拿到 0 或 1)。這也是它對離群值敏感的原因:離群點會把質心拉歪。選擇上:K-means 要先給 k、假設群是大小相近的球形、每點必分群;DBSCAN 不用給 k(給 eps 與 min_samples)、擅長不規則形狀(月牙、環形)、天生會標噪點——實跑月牙:DBSCAN ARI 0.955、K-means 0.479。但 DBSCAN 對「密度不均」的資料吃力、eps 難調;站內 HDBSCAN 頁介紹自動適應密度的進化版。
群數:len(set(labels)) - (1 if -1 in labels else 0)——先數有幾種標籤、再把 -1 扣掉(實跑 {0, 1, -1} → 正確群數 2,直接 len(set) 會誤報 3)。silhouette:先排除噪點再算——mask = labels != -1 然後 silhouette_score(X[mask], labels[mask])。含著 -1 硬算時,silhouette 會把 -1 當成普通的一群,而這「群」的成員散落四處,分數被拖低(實跑 0.265 vs 排除後 0.384)。計數用 np.unique(labels, return_counts=True) 一次看全部。這三行下游紀律是 DBSCAN 實務題的常客。
可以,而且是它的招牌兼職:「密度不夠、沒被任何群收編」的定義,跟「離群值」的直覺高度重合——outliers = X[labels == -1] 一行就撈出候選名單(實跑 7 筆)。適合形狀不規則、離群點「稀疏地散在群外」的場景。但兩個提醒:① 名單是參數的函數——eps 一調名單就變,撈出來仍要人工檢視或搭配其他指標;② 若目標純粹是異常偵測,也可比較專職工具(IsolationForest、LOF)——DBSCAN 的優勢是「分群+抓異常」一次完成。站內離群值處理頁有更完整的工具箱。
完全一樣。fit_predict(X) 等價於先 fit(X) 再讀 labels_——實跑 np.array_equal 驗證兩者逐筆相同,題目寫「labels_ 或 fit_predict 結果」就是因為兩種拿法拿到同一份陣列。缺值則連門都進不了:實跑把一格改成 NaN,fit_predict 直接拋 ValueError: Input X contains NaN. DBSCAN does not accept missing values——所以「-1 表示該筆遺失特徵值」(選項 B)不可能成立:有缺值就沒有輸出,有輸出就沒有缺值。缺值要在前處理階段先補(SimpleImputer 等)或先刪,才輪得到分群。