专项练习 · 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 道题,覆盖距离、分配、质心更新、簇内平方和、缩放与收敛。

打开练习

继续阅读: