ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

K-Means++:从原理到实践的聚类初始化优化指南

K-Means++:从原理到实践的聚类初始化优化指南 1. 为什么会有k-means随机初始化埋的雷1.1 k-means只解决了一半问题k-means可能是机器学习里最“老少皆宜”的聚类算法原理简单、容易理解、实现起来也就几十行代码所以很多人在入门时都会拿它练手。但真正用过一段时间就会意识到一个问题——k-means非常吃初始中心点。同样的数据同样的k值你换一组随机种子聚出来的结果可能完全不一样有的甚至明显不合理。为什么因为k-means的本质是在做坐标下降法先固定中心点分配样本再固定样本重新计算中心点反复迭代。这个流程只能保证收敛到某个局部最优解而不能保证找到全局最优。初始中心点一旦选得不好比如好几个中心挤在一起或者某个中心恰好落在离所有样本都很远的地方聚类的最终效果就大打折扣。这里的“效果”通常是看SSE簇内样本到其中心点的距离平方和SSE越低说明样本离自己所属的中心越近聚类越紧凑。在k-means出现之前最常用的办法是多跑几次随机初始化取SSE最低的那次。这招叫“多重随机启动”确实有效但有代价冗余计算量大而且依然有碰运气的成分。如果你跑20次随机初始化大概率能找到不错的结果可如果数据量很大、维度很高跑20次完整k-means的代价非常可观。k-means就是在这样的背景下被提出的。它不是改变k-means的迭代过程而是给k-means换一套更聪明的初始中心点选择方式。论文由David Arthur和Sergei Vassilvitskii在2007年提出后来被集成进scikit-learn等主流工具库成为默认初始化方式。它的目标很直接用一次预处理换取更高的聚类质量稳定性和更少的迭代次数。1.2 初始化失败的真实案例我试过一个非常典型的例子用二维模拟数据三个团簇彼此分隔但团簇大小不太一样。如果随机初始化时三个初始中心恰好落在最大的那个团簇里后面迭代就会把这个大团簇硬切成三块同时旁边两个小团簇被合并到一起整个聚类结果和真实结构完全对不上。还有另一种更隐蔽的失败模式当数据里存在异常值或离群点时随机初始化有一个中心点可能被选在离主体数据很远的位置。迭代过程中这个中心只能“抓住”自己附近那一两个点其他中心在主体区域里争抢样本最后聚类边界变得扭曲。SSE算下来还不算特别差但画出来一眼就知道这个聚类结果没法用。这种情况在实际业务里非常常见用户分群、图像分割、异常检测前的聚类都可能在初始化阶段埋雷。k-means的核心思路就是尽量让初始中心点互相远离并且兼顾数据本身的分布密度。接下来我详细拆解它的实现原理再给出手写代码和实验对比。2. k-means初始化算法一步一步拆开看2.1 核心思想就一句话让种子中心互相“离得远”k-means的初始化过程并不复杂核心就一句话第一个中心随机选后面的中心尽量选在离已有中心远的地方。为什么这个逻辑成立因为k-means聚类本质上是希望每个中心点都能代表一片局部密集的区域。如果初始中心点靠得太近它们最终可能收敛到同一个簇里导致结果里出现空簇或极不平衡的簇。让初始中心尽可能分散相当于在一开始就给每个簇一个合理的“领地预期”后续迭代只需要微调边界即可。当然如果完全不考虑数据分布只选彼此距离最远的k个点也很容易踩坑。因为离群点往往距离其他所有点都很远只选最远点会把离群点选成中心导致某个簇只有一个样本。所以k-means不是简单地选“最远的点”而是按照距离平方加权的概率来随机挑选让距离越远的点被选中的概率越高但又不是一定选中它。这里的设计很微妙如果按距离等比例加权远点被选中的概率高但依然给近点机会。距离用平方而不是一次方可以进一步放大远点的优势让中心点更容易落在数据密集但远离已有中心的位置而不是总被极端的离群点带走。这就是k-means与纯贪心的“farthest-first”做法的本质区别。2.2 加权概率采样的细节具体来说在已经选中了若干个中心点之后对每一个样本点x计算它与所有已选中心之间的最近距离D(x)D(x)越小说明x离已有中心越近越不需要被选为新中心D(x)越大说明x在“覆盖范围”之外有成为新中心的潜力。计算每个点的D(x)之后把它们全部求一个平方和记为SS sum(D(x)^2)然后每个样本x被选为下一个中心点的概率是P(x) D(x)^2 / S相当于把所有样本的D(x)^2值放在一个转盘上面积占比越大的样本越可能被抽中。用代码实现时不需要真的画转盘而是先随机生成一个0到1之间的数值再通过累积概率分布找到对应的样本。这个操作在numpy里直接用np.random.choice配合权重参数就能搞定。需要注意两个细节D(x)是“到最近已有中心的距离”不是到某个具体中心的距离。如果已经选了3个中心对每个点都要分别计算到3个中心的距离然后取最小值。平方加权的效果是D(x)为2的点被选中的概率是D(x)为1的点的4倍而不是2倍。距离越远优势越明显但不会完全垄断。我在第一次实现的时候犯过一个低级错误计算概率时用了D(x)而不是D(x)^2结果初始化效果和随机初始化差别不大。后来重新检查论文才发现距离平方这个细节是算法的关键所在少了这一步整个算法就失去了意义。2.3 完整流程梳理把整个k-means初始化流程完整列出来大致是四步从数据集中均匀随机选择一个样本点作为第一个中心点c1。对每个样本点x计算它到当前所有中心点的最近距离D(x)。根据P(x) D(x)^2 / sum(D(x)^2)的概率分布随机选取下一个中心点。重复第2、3步直到选够k个中心点。选完k个中心点之后后面就是标准k-means迭代分配样本、更新中心、重复直到收敛。也就是说k-means并不是一个独立的聚类算法它只是k-means的“初始化插件”但正因为初始点选得好后续迭代次数和最终SSE都能得到有效改善。为什么这个算法有效论文里给出了理论保证k-means的初始化结果在期望意义上可以达到最优解的O(log k)近似比。通俗讲就是用k-means初始化的聚类结果其SSE不会比最优SSE差太多这个理论界是随机初始化给不了的。虽然O(log k)这个界偏理论化实际中往往比这个界好得多但至少说明这个初始化方式有数学依据不是单纯的经验技巧。这里还有一个很多人忽视的点第一个中心点的选择也影响结果。虽然第一步是均匀随机选择不同种子得到的第一个中心可能不同但由于后面的步骤会基于距离进行加权采样第一个中心最终导致的聚类结果差异通常比随机初始化要小得多。这也是k-means在多次运行中结果比较稳定的原因之一。3. 代码实现从零手写初始化模块到一行调用3.1 自己写初始化模块练手版虽然scikit-learn里已经内置了k-means但如果你是初学者或者要做一些定制化改造建议还是手动实现一遍初始化逻辑。这不仅帮助你理解算法精髓也方便你后续修改权重计算、加入自己的距离度量。下面这份代码是我在实际练手时使用的版本不依赖sklearn的KMeans只用numpy实现import numpy as np def kmeans_plusplus_init(X, k, random_stateNone): k-means 初始化中心点 参数: X: shape (n_samples, n_features), 输入数据 k: 聚类数量 random_state: 随机种子 返回: centers: shape (k, n_features), 初始中心点 rng np.random.default_rng(random_state) n_samples X.shape[0] # 第一步随机选择第一个中心点 first_idx rng.integers(0, n_samples) centers [X[first_idx]] # 记录每个样本到最近中心的距离平方 min_dist_sq np.full(n_samples, np.inf) for _ in range(1, k): # 更新每个样本到最近中心的距离平方 for i in range(n_samples): dist_sq np.sum((X[i] - centers[-1]) ** 2) if dist_sq min_dist_sq[i]: min_dist_sq[i] dist_sq # 按概率平方加权选择下一个中心 probs min_dist_sq / min_dist_sq.sum() next_idx rng.choice(n_samples, pprobs) centers.append(X[next_idx]) return np.array(centers)一个可以优化的点是上面代码里每选择一个中心就要遍历一遍所有样本时间复杂度是O(k·n·d)其中n是样本数d是维度。如果不做任何优化这比随机初始化要慢不少。所以scikit-learn里的官方实现做了一个小优化它会维护一个每个样本到最近中心的距离数组每增加一个中心时只需用新中心去更新这个数组而不是全部重新计算。注意到上面代码中min_dist_sq初始化成np.inf这个细节很重要。第一次计算距离时任何有限的数值都会被保留下来。如果你初始化成0所有距离都会被当成0后面的加权概率就直接失效了。3.2 sklearn 一行调用如果你想在实际项目里快速使用直接用scikit-learn自带的功能就行from sklearn.cluster import KMeans model KMeans(n_clusters3, initk-means, n_init10, random_state42) labels model.fit_predict(X)这里initk-means是默认值也就是说你平时用KMeans(n_clusters3)底层已经在使用k-means了。n_init控制的是重复运行初始化的次数默认值是10。sklearn的做法是执行n_init次独立的k-means初始化聚类最后返回SSE最低的那一次结果。这个设计非常实用相当于在k-means的基础上又叠了一层保险。3.3 参数n_init到底设多少合适很多人会忽略n_init的影响。n_init1意味着只做一次初始化速度快但结果波动可能比较大。默认n_init10能够在大多数情况下兼顾速度和稳定性。如果数据量不大、k值也小可以设n_init20或更高来进一步减小随机性。我实际跑过一些对比实验在包含3个团簇的模拟数据上n_init1的时候偶尔会出现某个簇合并、另一个簇被切开的情况调到n_init10之后10次里几乎没有明显的失败初始化。但如果是超大数据集每一轮完整k-means迭代都很贵n_init10会让总耗时变成单轮的10倍。这时候有几种策略减少n_init到3或5配一个固定的random_state保证结果可复现。改用MiniBatchKMeans它对初始化的敏感度要低一些处理大数据也更高效。先用KMeans在小规模采样上确定k和大致中心再用这些中心作为init传入正式模型。另外scikit-learn新版里n_init的默认值会随着版本变化老版本默认10新版本可能还会调整。所以写代码时建议显式指定random_state和n_init避免版本升级带来行为变化。4. 效果对比实验同样数据换个初始化差多少4.1 测试设置为了更直观地展示k-means的作用我做了一个简单的对比实验用make_blobs生成三类团簇数据每类500个样本标准差设为1.5让三类数据之间有部分重合但不严重。这样既接近真实场景又不会难到所有算法都发挥不出来。对比的三组设置如下初始化方式参数设置说明随机初始化initrandom, n_init1只跑一次随机初始化k-meansinitk-means, n_init1只跑一次k-means初始化k-means多次initk-means, n_init10跑10次取最优每次实验固定random_state记录最终SSE和迭代次数。为了让对比公平同一套数据、同一个random_state下三种设置的随机性来源分别是random初始化的初始点完全随机k-means第一次随机选中心时使用同样的随机状态n_init10则是多次运行取最优。4.2 实验结果记录与解读在我实际跑出的结果中典型情况是这样的有一次random_state0下随机初始化的SSE约在2540左右迭代了9次k-means单次运行SSE约在2315左右迭代次数6次k-means跑10次的最优结果SSE约2308迭代次数也是6次。从数字上看k-means单次运行就能把SSE降低大约9%迭代次数减少三分之一。而n_init10相比n_init1的提升幅度就没那么大了说明k-means单次运行的质量已经比较稳定。更值得关注的是多次重复的方差我换了多个random_state跑随机初始化的SSE波动范围很大出现过高到2800的糟糕结果而k-means的SSE基本稳定在2300~2400之间。对一个需要自动化运行的项目来说初始化方式的稳定性甚至比绝对最优值更重要因为你不会希望某天跑批任务时突然冒出一个明显偏低的聚类结果。4.3 什么时候提升不明显需要注意的是k-means并不是在所有情况下都比随机初始化有巨大优势。当数据本身簇结构非常明显、团簇之间界限清晰、密度均匀时随机初始化多跑几次也能轻松找到好结果k-means的改善幅度就没那么惊人。另一个情况是数据维度很高、样本量很大的时候D(x)的值在空间中趋于接近平方加权的区分度被稀释k-means相比随机初始化的优势会缩小。但即便如此它依然有理论上的近似比保证不会比随机初始化差到哪去。所以实际项目中我基本都无脑用k-means只有在对比实验里才会特地把init改成random做对照组。5. 实际使用中的坑与注意事项5.1 k值选择对初始化效果的影响k-means假定你已经确定了k值它只负责在给定k下选好初始中心。如果k选得和真实簇数相差太远再好的初始化也救不回来。比如数据本来只有3个簇你把k设成10k-means会尽可能把10个中心分散开但最终聚类结果必然会把大簇切碎SSE自然也不会特别好看。所以在实际项目中我一般先用肘部法则或轮廓系数粗定k的范围然后再用k-means做正式聚类。也有一种做法是把k也当成超参数结合聚类稳定性评估来选。但需要强调的是k-means并不能替代k选择它只是让给定k之下的聚类结果更可靠。5.2 大数据量下的近似优化k-means有一个明显的痛点每选一个中心都要遍历全部样本计算距离当样本量达到百万级别、k达到几百时初始化阶段的时间开销就很可观了。有些大规模场景下k-means的初始化时间甚至能占据整个训练时间的大半。针对这个问题业界有一个改进版本叫k-means||读作k-means parallel or k-means double pipe它每轮采样多个候选点而不是只采一个再通过多轮采样得到一个规模更大的候选集合最后在候选集合上再做一次加权聚类得到k个中心。这种近似方法能把初始化过程的遍历次数从k次降到O(log k)轮非常实用。在scikit-learn里KMeans的参数init其实还可以传一个callable或数组。如果你用的是Spark MLlib它的KMeans实现已经默认采用类似k-means||的初始化策略。对于中小规模数据不需要这款进阶优化但如果你在迭代跑超大规模聚类值得研究一下相关实现。5.3 怎么判断聚类结果是局部最优即使用了k-means也不能100%保证每次聚类都收敛到全局最优。判断是否踩进局部最优我常用的方法有这么几个看SSE是否明显高于多次运行的中位数。如果某次运行结果比其他运行高出一大截大概率是初始化没选好。看簇的样本量是否过于悬殊。正常情况下每个簇的样本量应该和数据的空间分布一致如果出现某个簇只有一两个样本另一些簇却包含九成样本需要警惕。看迭代是否很快收敛比如1~2次。如果中心点初始化已经比较合理迭代次数通常会在个位数如果只迭代一两次就停了且结果明显不合理多半是初始化出了岔子。这时候最简单的处理就是调整random_state重跑或者加大n_init。k-means已经显著降低了“需要重跑”的概率但自动化任务里保留重跑机制仍然是个好习惯。5.4 k-means的变体思路除了前面提到的k-means||k-means还有几个常见的变体思路理解它们能帮助你在特殊场景里做选择改进初始化顺序第一个中心不要完全随机选而是先采样一个小批量计算均值把最接近均值中心的点作为第一个中心。这个思路能减少极端离群点被当作首个中心的概率。用其他距离度量默认k-means用欧氏距离但如果你在做文本或特殊特征空间里的聚类可以改成余弦距离或马氏距离只是加权概率的计算也需要同步调整不能直接换距离公式就完事。与层次聚类结合先在小规模采样上跑层次聚类确定中心点再用这些中心启动k-means这在某些低维数据上效果不错但计算复杂度更高。变体虽然不少但绝大多数实际需求用标准的k-means就已经够了。追求复杂的替代方案前建议先评估一下当前问题是不是真的卡在初始化上。6. 适合在什么场景下用我的选型经验6.1 传统聚类分析做用户画像、市场分群、地理位置聚类这类传统聚类任务时k-means是我默认的首选初始化方式。原因很简单这些任务中k通常不大3~20数据维度也不高一般不超过几十维k-means的额外计算开销可以忽略不计但能换来稳定、可复现的聚类结果。我在做用户分群时专门踩过坑同一个数据集上随机初始化某个种子跑出来的分群结果中有一个群体几乎是另一个群体的子集两个群体高度重叠业务方完全无法解读。换成k-means之后分群之间的区分度明显提升业务方也更愿意信任这个结果。6.2 图像压缩与视觉词袋k-means在图像领域有个经典应用是颜色量化——把图片的像素颜色聚类成k种代表色然后重建图像达到压缩或风格化的目的。这种场景下k可以大到几十甚至几百对初始化方式的效率要求就高了。好在k-means初始化在这种任务上不仅效果稳定而且能显著减少后续迭代次数因为像素颜色数据量很大能少迭代一轮就能省不少时间。视觉词袋模型里也常用k-means对局部特征做聚类这时特征维度可能很高如128维、512维k值也比较大几百到几千。这种情况建议直接用sklearn的MiniBatchKMeans并把init_size设置成样本量的一定比例让初始化过程更接近k-means||的思路。6.3 和Mini-Batch K-Means结合MiniBatchKMeans是处理大规模数据的常用选择它在每次迭代中随机抽取一小批样本更新中心点速度比标准k-means快很多。但注意MiniBatchKMeans的默认initk-means它会先在全部数据或者一个采样子集上做标准k-means初始化之后再用批量更新。如果你设定了batch_size初始化阶段依然需要计算全量样本的距离所以数据量特别大时初始化时间可能成为瓶颈。这时候可以把init换成自己传入的数组或某个小规模预聚类的结果。比如先用MiniBatchKMeans在一个子集上跑一遍拿到中心再把这个中心作为参数传给正式模型。这种方式在百万级样本上实测下来能省掉不少初始化时间聚类质量损失也比较小。6.4 评估聚类效果时的一个建议不管用什么初始化方式评估聚类结果时不要只盯着SSE。SSE天然偏向簇数多、簇内紧凑的结果就算初始化完美也不代表聚类结果有意义。我更建议在业务场景下把聚类结果可视化出来看簇是否在业务上可解释。k-means能帮你把算法的随机性降到最低但算法本身解出来的结构是否符合业务预期那需要人的判断。我个人的习惯是先用k-means快速跑一轮拿一个基准结果再手动检查簇中心是否合理、样本分布是否均衡、跨簇的边界是否清晰。如果发现问题再调整k或特征而不是盲目重跑。在我使用k-means这些年里最后想分享一个小经验如果你只在代码里用了KMeans(n_clusters3)而不知道底层默认就是k-means那你其实已经在享受它的红利了。真正需要警惕的不是要不要用k-means而是不要觉得换掉随机初始化就万事大吉。初始化只是聚类的第一步数据质量、特征选择、k值确定、结果验证每一步都决定最终效果。k-means更像是一个可靠的起点放大器——它让你的起点更稳但路还是要自己走。
返回列表