Maya 的客人分成早上趕路的、學生和在家工作的,可沒人給它們貼過標籤。k-means 能從行為裡提出分組。這個演算法真正在最佳化什麼?
k-means 是無監督演算法:沒有標籤可供預測。它反覆把每個點分配給最近的質心,再把質心移到成員平均值。迴圈很短,但距離縮放和初始化會決定它最終找到什麼。 你還會嘗試最遠優先初始化,並用慣性比較不同的 k。
計算距離
常用選擇是歐氏距離。平方距離保留相同排序,還能省去開方,但解釋結果時歐氏距離更直觀。坐標順序必須始終一致。
import math
def distance(a, b):
return math.dist(a, b)
print(distance((0, 0), (3, 4)))相距五個單位
把每個點分配給質心
對每個點,比較它到所有質心的距離並保留最近的索引。確定性的平局規則可以避免因為字典或集合順序變化而改變分配結果。
def distance(a, b):
return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5
centroids = [(0, 0), (5, 5)]
point = (1, 1)
label = min(range(len(centroids)), key=lambda i: (distance(point, centroids[i]), i))
print(label)最近的質心獲勝
把質心移到成員平均值
分配完成後,每個質心變成屬於它的所有點在每個坐標上的平均值。空叢集沒有成員,所以程式碼必須明確決定:保留舊質心、重新播種,還是減少 k。
points = [(0, 0), (0, 1), (5, 5), (5, 6)]
labels = [0, 0, 1, 1]
members = [point for point, label in zip(points, labels) if label == 0]
centroid = (
sum(x for x, _ in members) / len(members),
sum(y for _, y in members) / len(members),
)
print(tuple(round(value, 1) for value in centroid))成員坐標的平平均值
迭代到標籤不再變化
重複分配和更新,直到標籤或質心不再移動,或者達到步數上限。結果是局部最優,所以換一種初始化可能得到不同聚類。
def distance(a, b):
return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5
points = [(0, 0), (0, 1), (5, 5), (5, 6)]
centroids = [(0, 0), (5, 5)]
labels = None
for _ in range(10):
new_labels = [
min(range(len(centroids)), key=lambda i: (distance(point, centroids[i]), i))
for point in points
]
new_centroids = []
for index in range(len(centroids)):
members = [point for point, label in zip(points, new_labels) if label == index]
if members:
new_centroids.append((
sum(x for x, _ in members) / len(members),
sum(y for _, y in members) / len(members),
))
else:
new_centroids.append(centroids[index])
if new_labels == labels and new_centroids == centroids:
break
labels = new_labels
centroids = new_centroids
print(labels)
print([(round(x, 1), round(y, 1)) for x, y in centroids])兩個清楚的簇
最遠優先初始化讓質心分散
隨機初始化可能把兩個質心放進同一密集區域。最遠優先先選一個起點,然後反覆加入「到最近質心距離最大」的點。給定起點時它是確定性的,通常能得到更有用的初始分布。
def distance(a, b):
return sum((x - y) ** 2 for x, y in zip(a, b)) ** 0.5
points = [(0, 0), (1, 0), (5, 5), (6, 5)]
centroids = [points[0]]
while len(centroids) < 2:
candidate = max(points, key=lambda point: min(distance(point, c) for c in centroids))
centroids.append(candidate)
print(centroids)選擇離得遠的下一個質心
慣性有助於比較 k,但不能證明 k
慣性是每個點到所屬質心的平方距離之和。k 增大時慣性通常下降,所以最小值並不自動代表最佳選擇。應結合肘部形態、領域約束和不同初始化下的穩定性,並顯式處理空叢集。
def inertia(points, centroids):
total = 0
for point in points:
nearest = min(centroids, key=lambda c: sum((x - y) ** 2 for x, y in zip(point, c)))
total += sum((x - y) ** 2 for x, y in zip(point, nearest))
return total
points = [(0, 0), (1, 0), (5, 5), (6, 5)]
for k, centroids in [(1, [(3, 2.5)]), (2, [(0.5, 0), (5.5, 5)])]:
print(k, round(inertia(points, centroids), 1))慣性更低不自動等於更好
本課要點
- k-means 是無監督演算法,不使用目標標籤。
- 每輪先把樣本分配給最近的質心。
- 質心再移動到已分配樣本的平均值。
- 特徵縮放會改變距離,從而改變聚類結果。
- 結果依賴初始化,而且可能是局部最優。
- 最遠優先讓初始質心分散;慣性可以比較 k,但仍需判斷。
常見錯誤
- 忘記縮放量綱不同的特徵。
- 還沒分配完所有點就提前更新質心。
- 忽略空叢集,或悄悄算出 NaN 質心。
- 因為慣性最低就選最大的 k,然後把聚類結果說成有意義的發現。
用三個初始質心執行同樣的迴圈,收斂後列印最終標籤。
動手試試
改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。
練習本課
12 道題,作答後立即看到解釋。