手写k-means不是为了性能,而是为了深入理解收敛逻辑、距离定义、质心更新等底层细节;它比sklearn慢且易错,但可定制初始化、距离度量、约束条件及嵌入自定义流程,核心必须实现初始化、分配、更新、收敛判断四步闭环。

为什么手写 K-Means 比调 sklearn 快不了,还容易出错
手写 K-Means 不是为了性能,而是为了理解收敛逻辑、距离定义、质心更新边界这些底层细节。sklearn 的 KMeans 经过高度优化(比如用三角不等式加速、多线程、K-Means++ 初始化),自己写的裸循环在 10k 样本以上基本扛不住。但如果你要改目标函数(比如换成曼哈顿距离)、加约束(如每个簇最小样本数)、或嵌入到自定义训练流程里,那就绕不开手写。
核心四步必须手动实现:初始化、分配、更新、收敛判断
标准 K-Means 就是这四个环节反复迭代,缺一不可。C++ 里最容易漏掉的是「收敛判断」的数值稳定性——不能只比对质心坐标是否完全相等,得用 std::abs(new_centroid - old_centroid) ,否则浮点误差会让算法死循环。
- 初始化:别用随机选点(
std::uniform_real_distribution从数据范围里采样),推荐 K-Means++(先随机选一个点,后续按距离平方概率选) - 分配:对每个样本计算到所有
k个质心的欧氏距离,选最小索引 → 注意避免重复开方,直接比distance_squared - 更新:对每个簇,把所有属于它的样本坐标求平均;如果某簇没分到任何点(空簇),必须重置该质心(比如重采一个未被选过的样本点)
- 收敛判断:建议同时监控质心移动最大偏移量(
max_delta)和迭代次数上限(比如 300),防止不收敛
vector> 存数据?小心内存和 cache 友好性
用 std::vector<:vector>></:vector> 存 n × d 数据,每次访问第 i 行第 j 列要两次指针跳转,CPU cache 不友好。实际中更推荐扁平化存储:std::vector<double> data(n * d)</double>,第 i 个样本第 j 维是 data[i * d + j]。质心同理,用 std::vector<double> centroids(k * d)</double>。
距离计算示例(欧氏距离平方):
double dist_sq(const std::vector<double>& data, int i, const std::vector<double centroids int k_idx d double sum="0.0;" for j="0;" diff="data[i" return><h3>常见崩溃点:空簇、nan 质心、迭代发散</h3> <p>空簇最常发生在初始化不合理或 <code>k</code> 设得太大时,不处理会导致除零(算平均时除以 0)或 nan 质心;而 nan 一旦进质心数组,后续所有距离都会变成 nan,整个结果废掉。</p> <ul> <li>检测空簇:在分配阶段用 <code>std::vector<int> cluster_size(k, 0)</int></code> 计数,更新前检查是否为 0</li> <li>空簇恢复:把该质心设为离当前所有质心最远的那个样本点(或随机挑一个未被分配过的样本)</li> <li>防 nan:每次更新质心后,用 <code>std::isnan(centroids[i])</code> 检查,有就立即重置</li> <li>防发散:记录上一轮质心,若某次更新后所有质心移动距离都变大(而非缩小),说明可能初始点太差,可考虑重启</li> </ul> <p>真正难的不是写完能跑,而是让每次运行都稳定收敛到合理簇——这取决于距离度量合理性、初始化鲁棒性、以及空簇策略是否贴合你的数据分布。这些细节不打日志、不画中间结果,很容易以为“跑通了”,其实聚得一塌糊涂。</p></double></double>
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











