前面的分類模型都有一個要對照的答案y。假如資料只有每位使用者的行為特徵,沒有「哪類人」的標籤,能不能先整理出幾種相似輪廓?這是分群可以協助探索的問題。
K-means會依你提供的特徵與距離,把樣本分到K個中心附近。它不會自行發現唯一正確的客群,也不會知道哪群叫「高價值」。你得到的是一個數學分割,接著才需要檢查它是否穩定、可解釋,以及是否對任務有用。
先決定什麼叫近,才談哪些點是一群
標準K-means使用平方歐氏距離。若兩個樣本在你選定的數值欄位上相近,放進同一群的代價較小;如果欄位的單位或權重不同,分群也會跟著變。
例如「年消費金額」可能以萬元變動,「購買次數」只有個位數差異。直接計算距離時,大尺度欄位可能主導結果。標準化能改變各欄位的相對影響,但不是沒有假設的自動清理:你等於選擇用另一個尺度衡量相似。
類別代碼也不能任意當距離。城市編成1、2、3,不表示城市1比城市3更接近城市2;前處理應依特徵意義設計。資料量大、欄位多或有離群值時,也要先檢查距離是否還在表達你的問題。
第一步:先放K個中心,還沒得到答案
圖1|灰色圓點是資料,三個彩色菱形是K=3的初始中心。中心可以從某種初始化策略得到,不是已知的真實群中心。來源:Google,What is k-means clustering?,CC BY 4.0;原始SVG未改。
K是這次希望得到的群數,通常需要由用途及探索結果決定。中心的位置則需要初始化。最簡單的想法是先選一些起點,再靠反覆更新改善;不同起點可能得到不同結果。
常見的k-means++讓初始中心較有機會分散,通常能改善起點選擇,但仍不保證找到全域最佳解。不要把「有比較聰明的初始化」當成不必檢查穩定性。
第二步:每個點分給最近的中心
圖2|資料點依最近中心取得顏色;虛線是目前中心對應的分區邊界,不是樣本移動或時間路徑。來源及授權同圖1,原始SVG未改。
每個點只取一個群號,這是硬分配。對固定的中心,把樣本分到最近中心,可降低或維持目前的平方距離目標;但此時中心還不一定是各群的平均位置。
群號0、1、2只是程式標記。兩次訓練把0與2的名稱互換,分割本身可能完全一樣。即使你事後有真實類別,也不能直接對群號做普通accuracy;需要使用不受標記置換影響的比較,或明確說明事後對齊方式。
第三步:用目前群內平均值更新中心
圖3|箭頭由灰色舊中心指向彩色新中心;圓點仍是同一批資料,移動的是中心。來源及授權同圖1,原始SVG未改。
對固定的群成員,平方歐氏距離和由群內平均值最小化,因此更新中心用的是mean。這也說明為何它叫K-means,而不是隨意把中心換成某個看起來居中的點。
離群值會拉動平均數。若某群突然多一個很遠的點,中心可能為了減少那筆很大的平方誤差而偏移。因此K-means通常不會把所有遠點自動標成噪音;一般情況下,每筆都仍要分配到某個中心。
第四步:中心變了,重新分配再繼續
圖4|新的黑色分區與舊的灰色虛線不同,部分點因最近中心改變而換群。這是一輪更新後的狀態,不是證明已收斂或已找到真實群。來源及授權同圖1,原始SVG未改。
交替做「固定中心,重新分配」及「固定成員,更新均值」,直到符合停止條件。這個常見流程稱Lloyd算法。實作可能因容忍值、最大輪數等條件停止,不一定等到每個小數位都完全不變。
理想的交替更新讓目標不增加,但不保證全域最佳。若某次分配出現空群,也需要工具有相應處理策略,不能對空集合直接算平均。一般程式使用成熟實作;下面手算例故意選沒有空群或平手的資料,專心看更新本身。
用四個點,把目標算出來
一維資料是0、2、8、10,K=2,初始中心0、8。分配後0、2在左群,8、10在右群。
目標 J = Σᵢ ||xᵢ − μ[zᵢ]||²
初始 J = (0−0)² + (2−0)² + (8−8)² + (10−8)² = 8
更新中心:左群(0+2)/2=1,右群(8+10)/2=9
更新 J = (0−1)² + (2−1)² + (8−9)² + (10−9)² = 4
sklearn稱這個訓練樣本到所屬中心的平方距離和為inertia_。它是距離目標,不是準確率。把所有值的尺度放大十倍,平方距離可放大一百倍;不能拿不同前處理或不同資料量的inertia直接判斷誰比較好。
增加K,理論上最佳可達的inertia不會變差,極端到每點一個中心時可以把誤差壓到0。這顯然不表示K越大越有用。實際多次跑局部搜尋還可能因起點不同出現不平順曲線,不能只看到一個轉折就當唯一正確群數。
群數、形狀、變異與大小,是不同問題
圖5|來源四格的顏色來自資料生成標籤,依序為一般Gaussian blobs、線性轉換後的細長分布、不等變異、群大小不均。一般資料1,500筆、seed=170;右下取500/100/10筆。來源:The scikit-learn developers,Demonstration of k-means assumptions,BSD-3-Clause;原圖未改,手機四格依列優先上下排。
人工生成資料知道它原本從哪個分布抽出,真實客群往往沒有這種答案。先看生成標籤,是為了和下面算法得到的分割對照,不是把顏色當K-means已推理出的結果。
圖6|對照圖5,左上故意設定K=2,其餘K=3。形狀細長、不同變異或群大小不均時,K-means結果未必跟生成類別一致。來源及授權同圖5,原圖未改,手機依相同四格順序排;顏色只是群號,不應跨圖逐色當固定身份。
K-means偏好以中心為基礎的緊密分割;當你心中的群像長條、彎月,或同一群有很大的內部變異時,距離目標可能和你想保留的結構不同。這不是數學上規定每份輸入必須真的來自高斯、各群必須剛好一樣大,而是它的目標與初始化在這些情境容易給出不合用途的結果。
改K與重啟初始化,可以修不同的問題
圖7|圖6左上的一般資料改成K=3,這次與生成的三群更一致。來源及授權同圖5,原圖未改。圖題Optimal Number of Clusters是這個已知生成情境,不表示工具從所有真實資料自動證明K=3最佳。
有時候問題確實是K選錯;有時候K合理,卻被不好的起點帶到局部解。它們要分開檢查。
圖8|來源針對500/100/10筆的不均衡例,明確設n_init=10重啟初始化,得到較符合此例生成結構的結果。來源及授權同圖5,原圖未改;不表示所有群大小不均都能只調n_init修好,也不是十輪中心更新的意思。
n_init=10表示嘗試十組初始化,再依最終目標選結果;不是每組只更新十次。sklearn 1.5.1有n_init="auto",使用k-means++時通常只做一次初始化,因此重現範例最好明寫次數與seed,不依賴不同年代的預設。
選K時,inertia、silhouette及穩定性可以提供不同證據。Silhouette結合群內與最近其他群的平均距離,較高代表這個距離下相對緊密且分離;它仍不是客群真實存在、可行銷或可改善業務的證明。若你是為了部署一套固定分群規則,選K和前處理也應留在開發資料,再評估新資料的表現與可用性。
有時候需要換的是群的模型
圖9|這兩格已改用GaussianMixture(n_components=3),不是K-means多跑幾次的結果。左為細長分布、右為不等變異;模型能表達不同協方差形狀,但仍有自己的分布假設與群數選擇。來源及授權同圖5;原圖未改,手機左右兩格上下排並各自保留座標、標題。
若你希望的群本來就不是以平均中心附近的平方距離來描述,只一直加K或重啟初始化,可能是在解錯問題。Gaussian mixture、密度式分群或其他方法各自能表達不同結構;下一篇會討論密度與階層式的觀點。
實跑:四點驗算,再把固定中心用到新資料
以下程式在WSL、Python 3.10.12、NumPy 1.26.3、scikit-learn 1.5.1實跑。第一段驗四點,第二段另外生成300筆獨立合成點,取200筆建立縮放與K=3規則,再把同一規則套用到100筆未參與fit的點。這不是圖5–9來源實驗的重現。
python -m pip install numpy==1.26.3 scikit-learn==1.5.1
python kmeans_steps.py
import numpy as np
import sklearn
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from threadpoolctl import threadpool_limits
points = np.array([0.0, 2.0, 8.0, 10.0]).reshape(-1, 1)
initial = np.array([0.0, 8.0]).reshape(-1, 1)
distance_sq = ((points[:, None, :] - initial[None, :, :]) ** 2).sum(axis=2)
assignment = distance_sq.argmin(axis=1)
centers = np.vstack([points[assignment == k].mean(axis=0) for k in range(2)])
initial_sse = distance_sq[np.arange(len(points)), assignment].sum()
updated_sse = ((points - centers[assignment]) ** 2).sum()
with threadpool_limits(limits=1):
toy = KMeans(n_clusters=2, init=initial, n_init=1,
algorithm="lloyd", random_state=42).fit(points)
print(f"numpy={np.__version__} sklearn={sklearn.__version__}")
print(f"toy initial_assignment={assignment.tolist()}")
print(f"toy initial_SSE={initial_sse:.6f} updated_SSE={updated_sse:.6f}")
print(f"toy updated_centers={centers.ravel().tolist()}")
print(f"toy fitted_centers={toy.cluster_centers_.ravel().tolist()} "
f"inertia={toy.inertia_:.6f}")
print(f"toy predict_3_and_8={toy.predict([[3.0], [8.0]]).tolist()}")
# Fictional independent points. Generator labels are intentionally unused.
X, _ = make_blobs(n_samples=300, centers=3, n_features=2,
cluster_std=0.8, random_state=42)
train, future = train_test_split(X, test_size=100, random_state=42)
pipe = Pipeline([
("scale", StandardScaler()),
("cluster", KMeans(n_clusters=3, n_init=10, init="k-means++",
algorithm="lloyd", random_state=42)),
])
pipe.fit(train)
train_scaled = pipe["scale"].transform(train)
train_labels = pipe["cluster"].labels_
future_labels = pipe.predict(future)
print(f"synthetic train={len(train)} future={len(future)} "
f"scaler_seen={pipe['scale'].n_samples_seen_}")
print(f"synthetic train_inertia_scaled={pipe['cluster'].inertia_:.6f}")
print(f"synthetic train_silhouette_scaled="
f"{silhouette_score(train_scaled, train_labels):.6f}")
print(f"synthetic train_cluster_sizes={np.bincount(train_labels).tolist()}")
print(f"synthetic future_cluster_sizes={np.bincount(future_labels, minlength=3).tolist()}")
四點的實際中心為1、9,inertia為4,和手算一致;新點3與8分別取得群號0、1。
300筆合成例則得到以下結果:
程式沒有把生成時的真標籤交給K-means,也沒有拿群號做分類accuracy。Silhouette是這次合成資料在所選距離下的內部分離證據,並非未來資料的保證或真實業務分群品質。
pipe.predict先用訓練時學好的縮放規則處理新點,再分給最近的既有中心。不要先在新批次重新fit Scaler,卻繼續沿用舊中心;那會讓資料與中心處在不同座標系。如果重新fit K-means,中心與群號可能都改變,也不再是同一套固定分群規則。
最後檢查群的輪廓、少量群、異常點與跨批穩定性,再決定如何命名和使用。K-means能快速給出可檢查的分割;它能否成為有用的群,仍取決於你選的特徵、距離與後續驗證。
原始來源、配圖與授權
依據Google K-means overview、sklearn KMeans 1.5系列API、官方K-means assumptions原例與完整程式。查核日2026-10-31。線上文件可能比本篇固定實跑版本新;Google頁面簡寫的計算量未直接當成完整成本,常見每輪距離計算仍與樣本數n、中心數K及維度d相關。
圖1–4是Google官方自有步驟原SVG,依CC BY 4.0與網站政策採用,原檔未改;封面取圖4完整等比渲染加白邊。圖5–9為The scikit-learn developers官方原例圖,BSD-3-Clause;手機圖5–6四格按左上、右上、左下、右下重排,圖9左右重排,不改資料、顏色或刻度。沒有採用受限教材書圖,也沒有暗示來源作者背書。
四點算例及300筆合成程式為本站新增教學設計;來源圖、來源實驗與本站實跑分開。以下保留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
登入後就能留言。
登入