手写K-Means是为了掌握底层原理并支持定制化需求,如更换距离度量、添加权重或限制质心范围;需用NumPy广播高效计算距离矩阵,妥善处理空簇,并以质心位移而非仅max_iter判断收敛。

为什么不用 sklearn.cluster.KMeans 而要手写?
因为 sklearn 的 KMeans 默认用 k-means++ 初始化,且内部做了收敛加速、异常处理和多线程优化;手写能看清每一步怎么算距离、怎么更新质心、怎么判断收敛——尤其当你需要定制逻辑(比如用曼哈顿距离、加权重、限制质心范围)时,绕不开底层实现。
euclidean_distance 和向量化距离计算的区别
手写时最容易卡在距离计算:逐点循环调 euclidean_distance(a, b) 效率极低;正确做法是用 NumPy 广播机制一次性算出所有点到所有质心的距离矩阵。
- 错误写法:
for i in range(n_samples): for j in range(k): d[i,j] = np.sqrt(np.sum((X[i] - centroids[j])**2)) - 正确写法:
distances = np.sqrt(((X[:, None, :] - centroids[None, :, :])**2).sum(axis=2))—— 这样得到 shape 为(n_samples, k)的距离矩阵,后续用np.argmin(distances, axis=1)直接得到全部标签 - 注意:如果数据维度高、样本量大,
[:, None, :]会临时占用大量内存;此时可改用scipy.spatial.distance.cdist(X, centroids, 'euclidean')替代
质心更新时的空簇问题怎么处理
当某次分配后某个簇没分到任何样本,np.mean 会返回全 nan,下一轮距离计算就崩了。这不是理论漏洞,而是真实数据中高频出现的问题(尤其初始化差或 k 设得过大)。
- 检测空簇:
for j in range(k): if len(clusters[j]) == 0: - 常见修复策略(选一种):
– 随机重采一个未被选作质心的样本点替换该空簇质心
– 把离当前质心最远的那个点挪过来
– 直接报错并提示减小k值(适合调试阶段) - 别用
np.nanmean:它会把nan当 0 处理,结果完全失真
max_iter 和收敛阈值哪个更可靠
只靠 max_iter 容易提前终止(比如第 5 次质心已稳定,但你设了 10);只靠质心位移 np.linalg.norm(centroids_new - centroids_old) 又可能因浮点误差永远不触发。两者必须配合。
立即学习“Python免费学习笔记(深入)”;
- 推荐组合:
max_iter=300+tol=1e-4(对 float64 数据足够) - 注意:
tol是质心整体位移范数,不是单个坐标变化;用np.allclose(centroids_new, centroids_old, atol=tol/np.sqrt(X.shape[1]))更精细,但通常没必要 - 真正容易被忽略的是:收敛判断必须在更新质心后立即做,而不是在分配簇之后——否则你会多跑一轮无意义迭代
实际部署时,k 值不确定、数据含噪、特征尺度差异大,这些都会让纯 NumPy 实现的结果波动很大。手写是理解路径,不是生产首选;但一旦你要嵌入边缘设备、或对接非标准距离度量,这段逻辑就是绕不开的底盘。


















