MLE · Phone Coding

Apple MLE 手写 KMeans:初始化、空簇与收敛

给出可运行 Lloyd 训练版,逐步解释分配/均值更新、相等距离、空簇和随机初始化边界。

题目重点:给出可运行 Lloyd 训练版,逐步解释分配/均值更新、相等距离、空簇和随机初始化边界。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练版输入是有限浮点坐标的同维点列表;`1≤k≤不同点数量`,初始中心取输入中前 k 个不同点;距离用欧氏距离平方。
  • 相同距离选较小中心编号,空簇保留旧中心,最多迭代 max_iter 轮;题目未明确这些规则,真实实现可能要求 k-means++、随机种子或空簇重启。

解题路线

  1. 验证非空/维度/有限数,建立 k 个确定性初始中心;初始化方式会显著影响局部最优与重现性。
  2. 每轮将点分给最近中心,距离相同时稳定选较小编号;若标签与上一轮完全相同即可停止。
  3. 根据分配结果对每簇逐维求均值;空簇按训练契约保留原中心,不除以零。达到迭代上限后,最终标签再对最终中心计算,避免返回旧中心的标签。
  4. Lloyd 算法每次分配与均值更新不会提高目标平方误差(在中心精确均值且分配稳定的理想条件下),但只保证局部最优。用独立朴素参考实现核对 33,435 组短数据/迭代上限组合。

手动走一遍

点 (0,0)、(0,2)、(4,4),k=2,初始中心前两点。按最近中心分配后,(4,4) 靠近 (0,2),第二簇均值变为 (2,3);下一轮重新分配,直到标签不再变化或达到上限。不同初始中心可能给出不同终态。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from __future__ import annotations

from math import isfinite


def kmeans(points: list[tuple[float, ...]], k: int,
           max_iter: int = 100) -> tuple[list[tuple[float, ...]], list[int]]:
    """Deterministic Lloyd training contract; first k distinct input points seed centers.

    Ties choose lower center index. Empty clusters keep their previous center.
    """
    if not points or k < 1 or max_iter < 1:
        raise ValueError("nonempty points, positive k and max_iter required")
    dimension = len(points[0])
    if dimension == 0 or any(len(p) != dimension or not all(isfinite(x) for x in p) for p in points):
        raise ValueError("points must have same positive dimension and finite coordinates")
    seeds = list(dict.fromkeys(points))[:k]
    if len(seeds) != k:
        raise ValueError("need at least k distinct points")
    centers = [tuple(map(float, p)) for p in seeds]
    labels = [-1] * len(points)
    for _ in range(max_iter):
        assigned = [min(range(k), key=lambda c: (
            sum((x - y) ** 2 for x, y in zip(p, centers[c])), c)) for p in points]
        if assigned == labels:
            break
        labels = assigned
        sums = [[0.0] * dimension for _ in range(k)]
        counts = [0] * k
        for point, label in zip(points, labels):
            counts[label] += 1
            for axis, value in enumerate(point):
                sums[label][axis] += value
        centers = [tuple(value / counts[c] for value in sums[c]) if counts[c]
                   else centers[c] for c in range(k)]
    # Return labels consistent with the final centers after the last allowed update.
    labels = [min(range(k), key=lambda c: (
        sum((x - y) ** 2 for x, y in zip(p, centers[c])), c)) for p in points]
    return centers, labels

复杂度

n 点、k 簇、d 维、I 轮,时间 O(I·n·k·d),存储中心 O(kd)、标签 O(n) 与累加 O(kd)。大数据可分批或并行计算,但批量近似的收敛语义不同。

易错点与追问

容易失分的边界

  • 把 kmeans 当成保证全局最优的算法。
  • 空簇时均值除零。
  • 相等距离不定规则,导致测试/复现不稳定。
  • 用最新中心却返回上一轮标签。
  • 特征尺度差异很大时未经标准化,距离被单一维度支配。

继续深挖

  • k-means++ 如何降低坏初始化概率?
  • 空簇改为重置最远点时目标函数可能怎样变化?
  • 怎样用 silhouette/inertia 与业务指标共同选择 k?
  • 类别特征和离群点很多时 KMeans 有何局限?
✓已复制微信号!