專項練習 · 11 分鐘閱讀

Python k-means 練習:分配、更新與評估

把分配、更新和評估拆成練習,k-means 就沒那麼神秘。這些例子只使用二維小資料,每個質心都能手工核對。

練習:計算平方歐氏距離

平方距離保留最近鄰排序,還能省去開方。只有當實際單位對報告很重要時,才需要歐氏距離。

def squared_distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b))

print(squared_distance((0, 0), (3, 4)))

25 個平方單位

練習:給每個點分配簇

分配就是對所有點做列表推導。每個點選擇最近質心的索引。把索引也放進排序鍵,平局時結果才確定。

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 = [min(range(len(centroids)), key=lambda i: (distance(point, centroids[i]), i)) for point in points]
print(labels)

每個簇兩個點

練習:更新一個質心

質心是成員在每個坐標上的平均值。必須等所有分配完成後才能更新,否則會用只完成一半的聚類結果。

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))

簇 0 的平均值

練習:計算簇內平方和

簇內平方和是每個點到所屬質心的平方距離之和。數值越小簇越緊,但 k 增大時它一定下降,所以不能只看它選 k。

def squared_distance(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b))

points = [(0, 0), (0, 1), (5, 5), (5, 6)]
labels = [0, 0, 1, 1]
centroids = [(0, 0.5), (5, 5.5)]
inertia = sum(squared_distance(point, centroids[label]) for point, label in zip(points, labels))
print(round(inertia, 1))

聚類有多緊?

練習:迭代並評估

反覆分配和更新,直到標籤不再變化,然後同時報告標籤與簇內平方和。多試幾種初始化,因為迴圈只保證局部最優。

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]
        new_centroids.append((
            sum(x for x, _ in members) / len(members),
            sum(y for _, y in members) / len(members),
        ) if members else centroids[index])
    if new_labels == labels:
        break
    labels = new_labels
    centroids = new_centroids
inertia = sum(distance(point, centroids[label]) ** 2 for point, label in zip(points, labels))
print(labels)
print(round(inertia, 1))

標籤穩定後停止

你的練習清單

常見問題

k 怎麼選?

沒有通用規則。可以結合領域知識、觀察不同 k 的簇內平方和,並判斷聚類對實際任務是否有用。

為什麼每次執行聚類都不同?

k-means 找到的是依賴初始化的局部最優。固定隨機種子,或執行多次並保留簇內平方和最小的結果。

縮放會改變 k-means 嗎?

會。距離把每個坐標同等看待,數值範圍更大的特徵可能主導分配。

練習 k-means 迴圈

12 道題,覆蓋距離、分配、質心更新、簇內平方和、縮放與收斂。

打開練習

繼續閱讀: