Applied Scientist / AS Intern · Phone Screen / VO Coding

Microsoft Applied Scientist 手写 K-Means:分配、更新与空簇

不用机器学习框架也能解释 Lloyd 迭代:初始化中心、最近中心分配、按簇均值更新、收敛与空簇处理。

题目重点:不用机器学习框架也能解释 Lloyd 迭代:初始化中心、最近中心分配、按簇均值更新、收敛与空簇处理。

题意与假设

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

  • 训练输入为 n 个同维数值点,1≤k≤n;用欧氏距离平方分配,最终返回 k 个中心及每点簇标签。
  • 为可复现性取前 k 个点作初始中心,等距时选较小簇 ID;初始化质量可能不好,实际应用建议多次随机重启或 k-means++。
  • 空簇保留旧中心;最多迭代 max_iter 次或标签不变时停止。不同空簇策略都会改变最终结果,不能当作原题指定。

解题路线

  1. 每轮对每个点计算到全部 k 个中心的距离平方,选最近簇;平方与开方的最小点相同,省去 sqrt。
  2. 按标签对每簇每维求和/计数,非空簇新中心是该簇均值;这是固定标签下最小化平方误差的最优中心。
  3. 标签不变则再次更新仍得到同样中心,可以停;若轮数上限先达到,返回最后一次分配及其中心。
  4. 目标函数是所有点到所属中心距离平方之和。固定中心时最近中心分配不增目标,固定标签时求均值也不增目标,所以迭代单调不增,但可能卡局部最优。
  5. 讨论特征标准化、离群点、不同 k 选择和空簇策略;K-Means 不适合需要任意形状或类别变量直接相减的场景。

手动走一遍

点 [0]、[1]、[9]、[10] 取初始中心 [0]、[1] 时,初始分配可能先不均衡;更新后中心朝两团移动,后续迭代调整。两个相同初始点可导致空簇,训练实现保留旧中心,避免除零;真实系统可把空簇重置到最远点。

Python 参考实现

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

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

"""Deterministic Lloyd k-means training contract, using plain Python."""


def kmeans(points: list[list[float]], k: int, max_iter: int = 100) -> tuple[list[list[float]], list[int]]:
    if not points or not 1 <= k <= len(points) or max_iter < 1:
        raise ValueError("nonempty data, 1 <= k <= n, and positive iterations required")
    dimension = len(points[0])
    if dimension == 0 or any(len(point) != dimension for point in points):
        raise ValueError("fixed positive feature dimension required")

    centroids = [list(map(float, points[i])) for i in range(k)]
    labels = [-1] * len(points)
    for _ in range(max_iter):
        new_labels = []
        for point in points:
            closest = min(
                range(k),
                key=lambda cluster: (
                    sum((coordinate - center) ** 2 for coordinate, center in zip(point, centroids[cluster])),
                    cluster,
                ),
            )
            new_labels.append(closest)
        if new_labels == labels:
            break
        labels = new_labels
        totals = [[0.0] * dimension for _ in range(k)]
        counts = [0] * k
        for point, cluster in zip(points, labels):
            counts[cluster] += 1
            for axis, coordinate in enumerate(point):
                totals[cluster][axis] += coordinate
        for cluster in range(k):
            if counts[cluster]:
                centroids[cluster] = [value / counts[cluster] for value in totals[cluster]]
            # Empty cluster keeps its old center; alternatives must be specified.
    return centroids, labels

复杂度

T 次迭代、n 点、k 簇、d 维:距离分配 O(Tnkd),求和更新 O(Tnd),中心与标签空间 O(kd+n)。默认实现每轮 materialize 标签和求和;多次随机重启再乘重启数。

易错点与追问

容易失分的边界

  • 对空簇直接除以 0。
  • 把 K-Means 说成保证全局最优;初始化不同可落不同局部最优。
  • 忘记等距时的确定性 tie-break,测试不稳定。
  • 不同量纲特征未经处理直接用欧氏距离。
  • 只会调用框架函数,讲不清每轮分配/更新及复杂度。

继续深挖

  • k-means++ 如何降低糟糕初始化概率?
  • 怎样判断是否收敛,中心移动阈值与标签不变有何区别?
  • 百万点和高维稀疏向量下如何批量/分布式更新?
  • K-Means 与 Gaussian Mixture Model 的目标和软硬分配有何不同?
✓已复制微信号!