Learning Objectives
- 說明分群和分類的差別:分群的資料沒有標籤
- 用 numpy 寫出 k-means 的指派與更新,並算出慣性
- 說明起點為什麼會影響結果,以及 k-means++ 與 n_init 在做什麼
- 用手肘法與輪廓係數比較不同的 k,並說明兩者不一定給同一個答案
資料沒有標籤時,還能學什麼
前面幾課的資料都有答案:及格或不及格、期末幾分。模型的工作是學會從特徵猜出答案,這叫監督式學習。 這一課開始,資料沒有標籤:手上只有 240 位同學每週的讀書、睡眠、看螢幕與運動時數,沒有人告訴你他們該分成哪幾類。 這時候能問的問題是:這些人裡面,有沒有幾群彼此相似的人?這叫分群(clustering),屬於非監督式學習。
k-means 是最常用的分群方法。先決定要分成 k 群,再重複兩個步驟:
- 指派:每一點歸給最近的群心(centroid);
- 更新:每個群心移到自己這一群所有點的平均位置。
分組不再改變時就停下來,這叫收斂。衡量分得多緊的數字是慣性(inertia):
慣性 = 每一點到自己群心的距離平方,全部加起來
這個流程常稱為 Lloyd 演算法:Lloyd 在 1957 年的 Bell Labs 內部報告提出,1982 年才正式發表;「k-means」這個名稱來自 MacQueen 在 1967 年的論文。 先用 numpy 親手寫一次。
逐步看:用 numpy 寫 k-means
八筆資料、兩個特徵,看得出分成左下與右上兩團。兩個起點故意都放在左下角。注意 labels 怎麼變,以及最後一欄的慣性。
k-means 保證什麼,不保證什麼
每一步都不會讓慣性變大:指派時每一點改選更近的群心,距離只會變短;更新時群心移到平均,而平均正是讓距離平方總和最小的位置。 可能的分組方式是有限的,所以 k-means 一定會停。
但它只保證走到「再怎麼換都不會更好」的位置,不保證是所有分法裡最好的。不同的起點可能停在不同的解。
另外兩件要記得的事:
- 群的編號沒有意義:第 0 群和第 1 群換個名字,分組完全一樣。
- 靠距離的方法要注意尺度:和第 8 課的 k 近鄰一樣,數字範圍大的特徵會主導距離,通常要先標準化。
接下來自己動手:選起點,一步一步走,看慣性怎麼下降,也試試不好的起點會停在哪裡。
起點怎麼選:k-means++ 與 n_init
剛才看到起點會影響結果,實務上有兩個對策,scikit-learn 的 KMeans 都做了:
- k-means++(
init的預設值):挑起點時,離已選群心越遠的點越容易被選中,起點比較分散。 - 多試幾次(
n_init):從不同起點跑很多次,留慣性最小的那一次。
| 參數 | 預設值(scikit-learn 1.6) | 說明 |
|---|---|---|
n_clusters |
8 | 分成幾群,通常要自己設 |
init |
'k-means++' |
也可以給 'random' 或直接給起點陣列 |
n_init |
'auto' |
用 k-means++ 或給起點陣列時只跑 1 次;用 'random' 時跑 10 次 |
max_iter |
300 | 單次最多幾輪 |
預設只跑 1 次,所以範例都明確寫 n_init=10,並固定 random_state,結果才能重現。
逐步看:不好的起點差多少
用 make_blobs 產生四團分得很開的資料。一次用手動指定的壞起點,一次用 k-means++ 試 10 次。
逐步看:把 240 位同學分成三群
habits.csv 是程式產生的模擬資料,不是任何學校的真實資料:四個欄位都是每週或每天的時數,沒有標籤。
看三件事:標準差差多少、每群有幾個人、群心換回原本的單位之後長什麼樣子。
選 k:手肘法與輪廓係數
k 要自己決定。沒有標籤可以算準確率,常用的是這兩個指標:
手肘法:畫出 k 和慣性的折線。慣性一定隨 k 下降,看哪裡開始降得沒那麼快,那個轉折叫手肘。它是看圖判斷,沒有明確的公式。
輪廓係數(silhouette,Rousseeuw 在 1987 年提出):每一點算一個分數,再取平均:
a = 這一點到同一群其他點的平均距離
b = 這一點到最近的另一群所有點的平均距離
輪廓係數 = (b − a) / max(a, b)
值在 −1 到 1 之間。接近 1 表示離自己這一群近、離別群遠;接近 0 表示夾在兩群中間;負的表示可能分錯群。
逐步看:兩個指標各選了誰
同一份 habits.csv,k 從 2 試到 7。看輸出的三欄:k、慣性、輪廓係數。
分群沒有標準答案
上面的結果沒有算錯:手肘法指向 k=3,輪廓係數指向 k=2。兩者問的問題不同:
- 輪廓係數偏好彼此分得很開的群。這份資料裡,看螢幕多的那一群離另外兩群比較遠,k=2 時正好把它和其他人分開,所以分數最高。
- 手肘法看的是多分一群能讓每一群緊多少。從 2 群變 3 群慣性大幅下降,表示人數多的那一群還能再分成兩群。
因為沒有標籤,也就沒有「測試準確率」可以裁判,指標只能提供參考。k 該選多少,要回到你要拿分群結果做什麼: 想把同學大致分成兩類時,k=2 就夠了;想看出幾種不同的生活型態時,k=3 的三個群心比較有東西可以解讀。 把每一群的群心印出來、看看人數、換幾個 k 比較,判斷哪一種分法對你的問題有用,也要說清楚你是怎麼選的。
另外要記得,k-means 不管資料有沒有分群結構,都會交出 k 群。分出來不代表這些群真的存在。這份資料是模擬的,也不能用它推論真實學生的生活型態。
實作:寫一個 kmeans 函數
完成 kmeans(X, centers, rounds):重複 rounds 輪指派與更新,回傳最後的 labels、centers 與慣性。寫法和第一個逐步範例相同。
小測驗:k-means
下面程式會印出什麼?
import numpy as np
X = np.array([[0.0], [2.0], [9.0], [11.0]])
labels = np.array([0, 0, 1, 1])
centers = np.array([X[labels == c].mean(axis=0) for c in range(2)])
print(((X - centers[labels]) ** 2).sum())
關於 k-means 的慣性,下面哪一句正確?
資料裡有「年收入(元)」和「每週運動時數」,直接做 k-means,最可能發生什麼?
scikit-learn 1.6 寫 KMeans(n_clusters=3),沒有指定 init 和 n_init,它會怎麼做?
手肘法指向 k=3,輪廓係數指向 k=2。下面哪一個做法最恰當?
k-means 把一份資料分成 3 群,輪廓係數 0.3。下面哪一句最恰當?
重點整理
- 分群處理沒有標籤的資料,找出彼此相似的幾群;群的編號沒有意義,名字是人看了群心後取的。
- k-means 重複指派與更新,每一步慣性都不會變大,一定會收斂,但不保證是最好的解。
- 起點會影響結果:
k-means++讓起點分散,n_init多試幾次留最好的。scikit-learn 的預設在 k-means++ 時只跑 1 次。 - 靠距離的方法要先標準化,群心再用
inverse_transform換回原本的單位解讀。 - 手肘法與輪廓係數不一定給同一個答案,分群沒有標準答案,要回到你要回答的問題,並說明你怎麼選的。
- 下一課:k-means 假設每一群大致是圓的,形狀像月牙時就不行了,我們會看階層式分群與 DBSCAN。