ARTICLE DETAIL

资讯详情

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

K-Means聚类算法实战:质心计算、距离度量与效果评价全解析

K-Means聚类算法实战:质心计算、距离度量与效果评价全解析 K-Means这个名字很多做数据处理的人都不陌生甚至不少同学在学校里就已经把它写进过课程作业。但我在实际项目里踩过一圈之后发现书本上的K-Means和业务里的K-Means完全不是一回事。你以为是选几个中心点、算算距离、分分类的简单循环真正落到数据上质心计算、距离度量、聚类效果评价每一步都可能让结果和预期大相径庭。这篇文章我就把自己折腾K-Means聚类算法的完整理解整理出来讲清楚算法原理、质心迭代的细节、不同距离度量的影响以及最终怎么用指标和业务直觉去评价聚类效果。适合刚开始接触聚类算法的新手也适合那些已经会调包、但总觉得聚类结果“不稳定”“说不清为什么好”的从业者。1. 先搞清楚聚类要解决的问题再谈K-Means1.1 K-Means的目标函数让每个点到簇中心的平方距离之和最小聚类要解决的核心问题是在没有标签的情况下把样本划分成若干组让同组样本尽量相似、不同组样本尽量不同。K-Means把“相似”定义得非常具体每个样本到它所在簇中心点的欧氏距离平方和最小。这里有一个容易被忽略的点为什么要用“平方距离”而不是直接用“距离”从数学上看K-Means实际在最小化的目标函数是J Σᵢ₌₁ᵏ Σ_{x∈Cᵢ} ‖x − μᵢ‖²其中μᵢ是第i个簇的质心Cᵢ是第i个簇的样本集合。这个目标函数对μᵢ求导之后会发现最优解恰好是簇内所有样本的均值。也就是说质心计算里那个看似简单的“求平均”并不是拍脑袋定的而是平方距离定义了它的最优形态。如果你把目标函数换成绝对距离最优中心就变成了中位数而不是均值。所以我一直觉得K-Means真正的灵魂不是“聚类”这个直觉概念而是这个平方距离目标函数。它既是算法灵巧高效的原因也是后面很多局限性的根源——这点放到后面章节细说。1.2 K-Means的适用边界它适合什么形状的数据不适合什么形状理解目标函数之后你就能大概率预判K-Means在什么样数据上会“翻车”。K-Means隐含假设是每个簇可以用一个质心作为代表簇内样本围绕质心呈现一种近似球形的分布而且簇与簇之间在空间上能够明显分开。适合K-Means的典型场景客户分层把用户的消费频次、客单价、活跃天数等连续数值特征放进空间期望找到几个天然聚集的用户群体图像颜色量化把颜色像素聚类成几个代表色特征工程先用K-Means生成簇ID把簇ID作为新特征喂给下游模型大规模数据的粗略分桶K-Means速度快能在百万级样本上快速给出一个分组结果不适合K-Means的典型场景环形、月牙形、长条形等不规则簇形状簇与簇严重重叠、没有明显边界数据中包含大量噪声点和离群值类别数量未知且需要自适应判断判断数据适不适合K-Means有一个很笨但有效的方法先把数据降维到二维或者三维肉眼看一下散点图。如果看到的是几个比较紧凑的“团子”K-Means大概率能做好如果看到的是连续弧形或者互相缠绕的分布趁早换DBSCAN或者层次聚类。2. 手动走一遍迭代初始化、分配、更新、收敛2.1 标准迭代流程的四个步骤K-Means的完整流程看起来像是一个“猜测—修正—再猜测”的循环每一轮都在同时优化两个问题每个样本属于哪个簇以及每个簇的中心在哪里。这两个问题互相耦合——知道质心才能分配样本知道样本分配才能重新计算质心。标准流程如下选定聚类数K并初始化K个质心分配步骤计算每个样本到K个质心的距离把样本分配给距离最近的质心所在簇更新步骤针对每个簇重新计算簇内所有样本的均值作为新的质心重复第2步和第3步直到质心不再变化或者达到预设的最大迭代次数这个“分配—更新”循环本质上是坐标下降法在目标函数J上的应用。每一轮J的值都会单调下降不会变差。这让K-Means收敛过程非常稳定——你不用担心算法在迭代中“反弹”。2.2 一个具体小二数据集的完整手算只看公式容易飘我用一个极其简单的二维数据集把两轮迭代完整手算一遍。假设有6个样本A(1,1)、B(1,2)、C(2,2)D(7,6)、E(8,6)、F(8,7)肉眼已经能看出来大概分两簇。我们让K2初始质心取μ₁(1,1)μ₂(7,6)。第一轮分配按欧氏距离的平方来比较距离比较可以省略开方因为开方不改变大小顺序。样本到μ₁(1,1)的平方距离到μ₂(7,6)的平方距离分配结果A(1,1)061簇1B(1,2)152簇1C(2,2)241簇1D(7,6)610簇2E(8,6)741簇2F(8,7)852簇2第一轮更新质心μ₁ ((112)/3, (122)/3) (1.33, 1.67)μ₂ ((788)/3, (667)/3) (7.67, 6.33)第二轮分配用新的质心重新算样本到μ₁(1.33,1.67)的平方距离到μ₂(7.67,6.33)的平方距离分配结果A(1,1)0.5672.9簇1B(1,2)0.2263.2簇1C(2,2)0.5650.9簇1D(7,6)50.90.56簇2E(8,6)63.20.22簇2F(8,7)72.90.56簇2第二轮分配结果和第一轮完全一致说明簇成员已稳定质心也不会再变化算法收敛。两次迭代就结束了非常快。2.3 终止条件里的小心思前后两轮完全不变才算稳定实际工程实现里并不是必须等到质心“完全不动”才停。当数据量很大时质心可能在很小范围内来回抖动肉眼看起来几乎不动但理论上还没到平衡点。常用终止条件有几种质心变化量小于某个阈值比如变化量的二范数小于1e-4簇分配结果不再变化达到最大迭代次数我在实践里偏向于同时设置“质心变化阈值”和“最大迭代次数”两个条件其实质是先让算法快跑不要因为个别样本的微小抖动而无限循环。默认情况下scikit-learn里max_iter是300n_init是10我自己做项目时一般保留这个配置除非数据特别大才会调整。3. 距离度量不是“选个公式”那么简单3.1 距离度量与质心算法的绑定关系很多同学以为K-Means里只有欧氏距离这一种选择其实不对。K-Means的核心结构是“距离度量质心更新”这两者必须绑定。你用不同的距离度量对应的最优中心也不一样。欧氏距离L2对应均值也就是标准K-Means曼哈顿距离L1对应中位数这种变体叫K-Medoids或者K-Medians对异常值更鲁棒余弦距离常被用在文本或高维稀疏场景但它对应的质心本质上不再是最初欧氏空间里的均值而是需要额外做归一化处理为什么会这样因为目标函数变了最优解就变了。欧氏距离的平方求导出来是均值L1距离求导出来是符号函数平衡点也就是中位数。所以当你在网上看到有人说“我改了距离度量但质心还是用均值”那说明算法的每一步优化没有对齐最终结果其实是次优的。3.2 主流距离度量的适用场景我把常用距离度量放在一张表里方便对比距离度量计算公式最适合的场景注意点欧氏距离√(Σ(xᵢ−yᵢ)²)连续数值特征、点云坐标、标准化后的表格数据对量纲极其敏感必须先做标准化曼哈顿距离Σ|xᵢ−yᵢ|特征维度有实际增减约束的场景某些异常值较多的数据对应中位数鲁棒性更好余弦距离1 − (x·y)/(‖x‖·‖y‖)文本TF-IDF向量、用户行为向量等高维稀疏数据只关心方向不关心具体幅度实际项目里90%的表格型数据我直接用欧氏距离然后配合标准化。文本数据我才会切到余弦距离。3.3 被忽视的预处理标准化其实是在规范距离这里必须单独拿出来说因为我自己在这个坑上吃过亏。表面上K-Means选的是欧氏距离但如果你不标准化特征欧氏距离会被“数值大的特征”完全支配。举个例子做用户分群时“年消费金额”可能是几万元“月登录次数”可能是几十次。两者的数值尺度差了几千倍距离计算中几乎只有金额在起作用登录次数这个特征就被废掉了。你聚类出来看着也挺“合理”但其实是按单一维度分出来的假群。补救方式是在聚类前做Z-score标准化每个特征减去均值再除以标准差让所有特征处于同一量纲。或者用MinMax缩放把数据压到同一个区间。我自己的经验是Z-score对存在明显长尾或异常值的特征更稳妥。4. 质心初始化和K值的取舍决定你输出的上限4.1 K-Means为什么随机初始化不够标准K-Means对初始质心非常敏感。如果初始质心选得不好很容易陷入局部最优解聚类效果评价做出来很差但你并不知道问题出在哪里。一个经典反例是三个真实簇相互离得比较远但随机初始化的两个质心都落在同一个簇内部剩下两个真实簇被迫合并。这种情况下无论跑多少轮最终结果都会偏离全局最优。K-Means就是为了解决这个问题提出来的初始化策略核心思路是让初始质心之间尽量离得远。具体步骤从数据集中随机均匀选择一个样本作为第一个质心对每个样本计算它到当前所有已选质心的最近距离D(x)按D(x)²的比例概率随机选取下一个质心距离越远的样本越容易被选为质心重复第2和第3步直到选满K个质心这样选出来的初始质心能覆盖数据的不同区域有效降低陷入局部最优的概率。现在sklearn里的KMeans默认就使用K-Means这也是我推荐的默认选择。4.2 K值的选择肘部法、轮廓系数与业务对齐K值的选择没有一劳永逸的公式但在工程上有几条被验证有效的路径。肘部法是最直观的对一系列K分别计算SSE簇内平方距离总和然后画一条“K值—SSE”的曲线。随着K增大SSE一定下降因为簇越多、离质心越近是必然的。真正有用的是找到曲线从陡降转为平缓的“拐点”这个拐点就是肘部意味着再加更多簇对降低SSE的帮助已经变小了。轮廓系数则是从“簇内紧凑、簇间分离”两个维度同时评价。对每个样本先算它到同簇其他样本的平均距离a再算它到最近的其他簇中所有样本的平均距离b轮廓系数s(b−a)/max(a,b)。把所有样本的s取平均就是整体的轮廓系数范围在[-1,1]越接近1越好。选择轮廓系数最高的K是一个不错的参考。但我必须提醒一句肘部法和轮廓系数给出的K是统计最优不代表业务最优。我有一次做用户分层统计指标都指向K5但运营业务方只需要4个层来匹配不同运营动作——多一层就意味着多一套策略成本是实打实的。最终我们还是用了4。聚类结果不能脱离业务场景尤其是长尾VIP这类运营分层场景你要的是“可解释、可行动”的群组而不是纯粹数学意义上的最优簇。4.3 多次运行和随机种子即使有了K-Means结果仍然带有随机性。K-Means的初始化是随机的所以同一次数据运行十次可能得到十个不同的局部最优解。处理办法很朴素设置较高的n_init次数比如10次或20次算法会从不同的初始化出发分别跑完全过程最终保留SSE最小的那一次结果。这是时间换质量数据量不是特别大的时候很值得。另外做项目时一定要固定random_state。否则你昨天跑出来的聚类结果和今天跑出来的可能完全不同跟别人对不上出了问题也复现不了。固定随机种子是老生常谈但确实是工程交付的底线。4.4 长尾VIP场景里K-Means落地的一个具体思路前面提到“长尾VIP”这个热搜词它确实是一个典型的K-Means应用场景。长尾VIP的特征往往体现为高消费频次但低单次金额或者低活跃但高客单用单纯的RFM手工分箱很难发现规律。我做过的思路是先对用户最近购买时间、购买频率、购买金额等RFM特征做标准化然后用K-Means聚类分别尝试K3、4、5结合轮廓系数和对每个簇的画像描述去选择最终K。结果往往会出现一个“沉默高价值”簇、一个“活跃低客单”簇、一个“中坚力量”簇这正是运营侧能够直接设计差异化策略的群组。这个落地思路的关键在于聚类只是把人群分组真正让业务认可的是每个簇的画像描述。所以质心计算完成后一定要把每个簇的特征均值汇总成表格给每个簇起一个业务能理解的名字否则聚类结果只会停留在技术报表里。5. 聚类效果评价别只看“图挺好看”5.1 无标签时的内部指标无监督聚类的最大难点是没有标准答案所以内部评价指标本质上是在衡量“簇内是否紧凑、簇间是否分离”这两个性质。SSE/惯性也就是目标函数J本身。SSE越小、簇越紧凑但K越大SSE必定越小所以不能单看绝对数值要和肘部法配合使用。轮廓系数前面已经介绍了公式它是综合了簇内距离和最近邻簇距离的指标。单个样本的轮廓系数接近1说明它离自己簇很紧、离别的簇很远接近0说明样本在两个簇边界上负值说明分配的簇可能还不如隔壁簇合适。我习惯把每个簇的平均轮廓系数也打出来某个簇平均轮廓系数明显偏低说明这个簇本身可能就该拆开。Calinski-Harabasz指数它本质上算的是“簇间离散度与簇内离散度的比值”比值越大越好。这个指标的计算方法是(CH (B/(k−1)) / (W/(n−k)))其中B是簇间离差矩阵的迹W是簇内离差矩阵的迹。它的好处是计算快适合快速对比不同K值。Davies-Bouldin指数它考察每个簇和其他簇的相似度取的是“最差的那个簇对”作为代表数值越小越好。内部指标不能互相替代我在项目里一般至少看两个以上指标拥有不同的角度才不会恰好被构造性的偶然误导。5.2 有标签时的外部指标有一种情况比较特别你手里已经有了业务标签只是想确认聚类结果和标签是否一致。这时候就可以用外部评价指标。调整兰德指数ARI在随机划分基准上做了修正取值范围[-1,1]越接近1表示聚类结果和标签越一致归一化互信息NMI衡量聚类划分结果与真实标签之间共享的互信息量范围[0,1]越高越好纯度Purity每个簇里把样本按真实标签投票取多数派样本数占总样本数的比例。纯度计算最朴素但没有对“簇数量多”做惩罚容易虚高我在实际中更常用ARI和NMI因为这两个指标对随机划分做了修正不会给“把每个样本单独分一簇”这种无聊结果打高分。5.3 内外部指标与业务直觉的配合说实话指标只是辅助手段。我吃过一个亏当时在一批客户数据上跑K-Means轮廓系数和Calinski-Harabasz指数都表现不错看上去很完美。结果把聚类结果交给业务评审时对方一眼就发现其中一个簇把“刚注册还没登录的新用户”和“半年不活跃的流失用户”混在了一起——这两个群体行为表现不同但都表现为特征数值很低距离上非常接近。从那以后我形成了一套固定的效果评价流程不只依赖于指标本身跑一组候选K计算SSE、轮廓系数、CH指数先筛掉明显不好的K对每个簇计算特征均值并画出簇画像表随机抽取每个簇的部分样本人工查看原始数据确认簇内是否真的属于同一类业务场景如果发现某个簇内部仍然混合了多个业务含义考虑增加K或者调整特征6. K-Means失效的典型场景与我的应对6.1 局部最优即使初始化很好也不是万能药K-Means只是显著降低了局部最优的概率但没有根除。目标函数J是一个非凸函数梯度下降式的坐标循环只能保证落到某个局部最小值。数据分布越复杂、K越大局部最优问题越明显。我的应对方式是三层第一用K-Means初始化这是默认不做选择的选择。 第二用多次随机初始化取SSE最小的最优结果。 第三如果数据规模允许试试二分K-Means。二分K-Means的思路是一开始把所有样本看作一个簇然后反复选择一个SSE下降幅度最大的簇进行二分直到达到目标K。这个方法在降低多次运行的一些麻烦上往往效果不错而且它天生适合分层业务场景产出的是类似树状的聚类路径。6.2 非球形簇环形、带型数据有一个几乎所有教材都会提到的反例两个同心圆环数据K-Means无论怎么调K都不可能把内外环正确分开。因为在欧氏距离下内环和外环的某几个点可能离同一个质心一样近质心会落在圆环中间区域对环上样本的区分毫无意义。遇到这种情况第一步是审视是否必须用K-Means。如果数据结构本身是环状、月牙状DBSCAN或谱聚类会更合适。如果你希望保留K-Means的可解释性可以尝试先用核函数把数据映射到更高维空间再聚类这就是核K-Means的思路但工程实现成本较高。6.3 异常值把质心拖走均值的本质是“所有样本等权相加取平均”所以一个极端的异常值就能把质心往它的方向拖出很大一段距离。这在有财务金额、点击量等长尾特征的数据里尤其明显。处理方式有几种聚类之前做异常值检测把明显的离群点单独剔除或者单独分为一个“噪声簇”对特征做截断把超过某个分位数的值压缩到该分位数改用K-Medoids或者带权重的K-Means变体我自己的经验是不要把所有异常样本身不由分说地剔除。有时异常值本身就是一个非常重要的群组比如高价值大客户的特征表现就和普通客户完全不同。剔除之前先想清楚这个异常值是数据记录错误还是真实存在却数量稀少的群体如果是后者更好的做法是把它保留下来并以最后一个簇的形式呈现。6.4 类别不平衡问题K-Means的另一个局限是它默认每个簇的“大小”差异不会过于悬殊因为它把所有样本一视同仁地算进目标函数。如果真实数据中一个簇只有几十个样本另一个簇有几十万个样本小簇很可能被大簇吞并或者被拆得七零八落。在长尾VIP场景里这一点尤其常见。真正的VIP群体可能只占全部客户的1%甚至更少直接聚类很容易被淹没在普通客户的大背景里。我的做法是如果小群体是业务重点先做一轮粗聚类把头部客户单独筛出来再在筛出的子集上做第二轮聚类。这种“粗筛—细分”的两阶段思路比直接强行聚类整个数据集要稳得多。6.5 数据量太大时的工程方案Mini-Batch K-Means当数据量达到千万级别每一轮迭代都要计算所有样本到K个质心的距离时间和内存开销都不小。Mini-Batch K-Means的基本想法是每轮迭代不在全量数据上计算而是随机抽取一小批样本用这批样本的更新方向来近似全局方向然后更新质心。它的质心更新方式比标准版多了一个学习率的概念每一批样本只对质心做小幅修正。所以Mini-Batch K-Means通常迭代速度极快但收敛后的SSE会比标准K-Means略高。如果你能允许那一点误差它很适合在线场景比如次日更新一次用户分层结果。我的建议是先在全量数据的子样本上跑标准K-Means确定好K和特征预处理方式再在需要规模化生产时切到Mini-Batch版本。不要一上来就从Mini-Batch开始调参否则你很难区分结果是算法原因还是K选择原因。最后再分享一条实操经验我在实际项目里总结出一套稳定的K-Means工作流——先标准化特征再跑K-Means配合n_init10观察SSE曲线定K同时看轮廓系数确认后固定随机种子跑最终版最后对每个簇做画像分析并抽样核查。这套流程不花哨但保证了我交付出去的结果经得起业务追问。另外K-Means的产出不一定非要当作最终结论很多时候我会把“样本属于哪个簇”作为一列特征喂给下游的分类模型效果也很好。聚类算法群里K-Means不是万能的但它作为基线、作为特征工程工具、作为业务分群的第一步性价比实在太高。希望这篇内容能帮你少走一些我走过的弯路。
返回列表