尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

K-Means聚类算法原理与实战:从数据标准化到用户分群完整指南

K-Means聚类算法原理与实战:从数据标准化到用户分群完整指南 聊一个我在实际项目里用得最多的聚类算法K-Means。很多时候我们拿到一批数据并不知道里面有几类、每类长什么样这时候第一件事不是急着训练分类模型而是先用无监督学习的K-Means把数据大致分堆。这个算法入门门槛低从原理到实现都不复杂但真正拿到业务场景里用的时候坑也不少。这篇文章就把我从原理、工程细节、实际案例到排查思路的完整经验写出来适合刚开始学机器学习的人也适合希望把K-Means用到实际项目里的朋友。1. K-Means到底在解决什么问题1.1 聚类任务和分类任务的区别很多人第一次接触机器学习时会先看到“分类”和“聚类”这两个词容易搞混。我想用一个很直观的例子解释假设你要教会机器“认识猫”。如果是分类任务你需要提前准备一大批标注好的图片告诉算法“这张是猫”“这张是狗”算法通过学习这些标签来学会判断新图片是不是猫。整个过程有老师、有标准答案所以叫监督学习。聚类完全反过来。同样是一堆猫咪和狗狗的图片没有人告诉你任何标签算法只能自己去观察哪些图片在颜色、轮廓、纹理等特征上更像然后自动把图片分成两堆。分完之后你再看哦这堆基本是猫那堆基本是狗。整个过程没有老师、没有标准答案所以叫无监督学习。K-Means就是无监督学习里最经典的聚类算法。它解决的核心问题可以概括成一句话在没有标签的情况下把样本划分成K个簇让同一个簇里的样本彼此尽量相似不同簇之间的样本尽量不同。这里的“相似”被量化成了“几何距离”距离越近越相似。1.2 K-Means的算法流程K-Means之所以流传广很大程度上是因为它的迭代思路特别好懂。我用一个班级同学分组的例子来讲。假设你班上有100个同学每人有两个维度的数据身高和体重。现在你想把大家分成若干个小组每组里的人身材比较接近。第一步你先随机挑K个人出来把他们当成K个小组的“临时组长”也就是质心。第二步其他所有人各自看看自己离哪个临时组长最近就站到哪个组里去。第三步每个组里所有人站定之后重新算这个组的平均身高和平均体重把这个平均值当作新的组长。第四步所有人再看一遍自己离新组长是不是最近如果不是就换组。重复这个过程直到没有人再换组或者换组的数量非常少算法就收敛了。这个流程总结下来就是四步初始化质心、分配样本到最近质心、更新质心、检查是否收敛。反复迭代直到质心不再明显变化。原理就这么朴实。1.3 目标函数和收敛条件如果只看流程很多人会误以为K-Means只是“反复移动中心”但为什么每次更新都取簇内平均值这背后是有数学逻辑的。K-Means的目标是最小化所有样本到所属质心距离的平方和也就是组内平方和WCSSWithin-Cluster Sum of Squares公式长这样[ J \sum_{k1}^{K}\sum_{x_i \in C_k} \lVert x_i - \mu_k \rVert^2 ]其中 (\mu_k) 是第k个簇的质心(C_k) 是这个簇里的样本集合。为什么距离用欧氏距离而不是曼哈顿距离因为可以证明在欧氏距离下使这个目标函数最小的中心点恰好就是簇内样本的均值所以“用平均值更新质心”这一步是有数学保证的。如果换了距离度量质心的更新方式也要跟着变比如用曼哈顿距离时质心应该取中位数而不是均值。还有一个很多初学者会忽略的点K-Means的目标函数不是凸函数迭代只能找到一个局部最优解不保证全局最优。换句话说最终结果很大程度上受初始质心的影响。这也是后面工程细节里为什么要重点处理初始化问题的根本原因。2. 动手实现前必须搞懂的工程细节2.1 标准化K-Means最容易踩的坑我先说一个我在项目里见过无数次的错误拿到数据直接跑K-Means结果聚类结果完全被某个特征主导。举例来说数据里有两个特征身高单位米数值在1.5到1.9之间和体重单位克数值在45000到90000之间。如果直接用原始数值计算欧氏距离体重这个特征对距离的贡献会远远大于身高因为它的数值范围大了几个数量级。最终聚类结果基本只由体重决定身高信息几乎被忽略。解决办法是先做标准化让每个特征都落在相近的尺度上。最常用的是Z-score标准化[ x \frac{x - \text{mean}(x)}{\text{std}(x)} ]在scikit-learn里一行代码就行from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X)这一步不是可选项而是K-Means的必要前置步骤。不止是标准化如果原始数据里有量纲差异大的特征比如年龄20-60和年收入5万-100万同样必须处理否则距离计算完全失真。2.2 初始化方式K-Means和random_state前面讲到K-Means对初始质心敏感。随机初始化如果选得不好可能出现几种情况有的质心落在了同一个簇里导致有些真实存在的簇被漏掉有的质心一开始就被离群点带偏导致最终收敛到很差的局部最优。标准做法是使用K-Means初始化。它的思想很简单第一个质心还是随机选但后续每个质心会尽量选择离已有质心远一些的样本。这样初始质心能更均匀地覆盖整个数据空间后面的迭代也更容易收敛到好结果。在scikit-learn里初始化参数默认就是K-Means但实际使用中还有两个参数要调n_init默认10表示算法会使用不同的随机初始化跑10轮最终选择目标函数最小的一次作为输出。random_state固定随机种子保证结果可以复现。做实验或者写报告时如果不设置这个参数你每次运行结果都不一样很容易让人怀疑代码有问题。我自己习惯在代码里固定random_state42因为复现性是工程化的基本要求。你永远不希望在演示的时候结果偶尔和上次对不上。2.3 手写实现与sklearn调包对比学K-Means有个很好的建议先用NumPy手写一遍再用scikit-learn调包。好处是你能彻底理解算法内部发生了什么而不是把库函数当成黑盒。一份极简的手写K-Means核心代码大概长这样import numpy as np def kmeans(X, k, max_iters100): # 随机初始化质心 idx np.random.choice(len(X), sizek, replaceFalse) centers X[idx] for _ in range(max_iters): # 分配步骤计算每个样本到所有质心的距离 distances np.linalg.norm(X[:, None, :] - centers[None, :, :], axis2) labels np.argmin(distances, axis1) # 更新步骤重新计算质心 new_centers np.array([X[labels i].mean(axis0) for i in range(k)]) # 检查是否收敛 if np.allclose(centers, new_centers): break centers new_centers return centers, labels这段代码虽然简单但整个过程是完整的初始化、分配、更新、收敛判断。实际项目中直接用这版效率不够高而且缺少K-Means初始化、空簇处理等细节所以生产环境还是推荐用scikit-learn封装好的实现from sklearn.cluster import KMeans kmeans KMeans(n_clusters4, initk-means, n_init10, random_state42) kmeans.fit(X_scaled) labels kmeans.labels_ centers kmeans.cluster_centers_学习时手写工程时调包这是一个很务实的路径。2.4 K值的三个选择思路K-Means有一个绕不开的问题K到底取几这其实没有唯一正确答案更多是在算法指标和业务需求之间找平衡。第一个思路是肘部法则。跑不同K值记录每个K对应的组内平方和WCSS然后画出来。随着K增大WCSS会不断下降因为簇越多、每个簇内部越紧。但下降速度会在某个K之后明显变缓这个转折点就像胳膊肘一样选这个K就够了。原因也很直观再增加聚类数对“减少组内距离”的收益已经不大了。第二个思路是轮廓系数。它综合衡量了样本和同类样本的紧密程度以及和其他类别的分离程度取值在-1到1之间。越接近1说明簇内紧密、簇间分离效果越好。计算方法是先算每个样本的簇内平均距离 (a) 和到最近其他簇的平均距离 (b)然后得到[ s \frac{b - a}{\max(a, b)} ]对所有样本取均值就是整体轮廓系数。第三个思路来自业务约束。比如运营团队说“我们就打算按用户价值做三种策略”那K3往往比K5更落地。K值不是数学题而是业务题。很多初学者把K值选择完全交给算法忽略了最终使用者能不能解释每个簇这是一个非常常见的误区。另外提一句如果样本量不大想更直观地确定K值可以跑一遍层次聚类画出树状图来看不同层次的分组情况。层次聚类在Python里用scipy.cluster.hierarchy就能实现但它的计算复杂度比K-Means高很多大数据集上不推荐。3. 实操案例用户分群从数据到策略3.1 案例背景与数据准备理论说再多不如走一个完整案例。我之前帮一个电商平台做过用户分群需求是把平台的活跃用户分成若干群体运营部门好针对不同群体做差异化活动。当时拿到的是近一年的用户行为流水。我先从流水里整理了三个核心特征这也是电商运营里最常用的RFM模型思路最近一次购买距今多少天RRecency过去一年购买次数FFrequency过去一年消费总金额MMonetaryK-Means在这里非常适合因为我们确实不知道用户天然分成几群连“大概有几种人”都是模糊的。先用聚类做探索性分析比直接拍脑袋定分类规则要靠谱得多。3.2 数据清洗与特征标准化拿到原始数据后先别急着聚类。我按这个顺序处理第一步处理缺失值。比如有些用户没有购买记录消费金额为空。这类用户要么剔除要么单独归为一个群体看业务上怎么定义。第二步处理极端值。消费金额的分布往往长尾严重少数大额用户会给距离计算造成很大干扰。我习惯先用箱线图或者分位数观察一下对明显异常的值做截断处理。第三步标准化。三个特征量纲完全不同天数可能是0到365次数可能是1到100金额可能是几百到几十万。如果不做标准化聚类结果基本等于只看消费金额。数据准备好之后我又做了一次主成分分析PCA降维到二维不是为了聚类输入而是为了最后可视化。严格来说高维数据直接聚类更好因为降维会损失一部分信息但二维散点图能很直观地展示队友和领导。3.3 确定K值肘部法则加轮廓系数我跑了一个循环把K从2到8都试了一遍。先看WCSS曲线的肘部import matplotlib.pyplot as plt from sklearn.cluster import KMeans wcss [] for k in range(2, 9): kmeans KMeans(n_clustersk, n_init20, random_state42) kmeans.fit(X_scaled) wcss.append(kmeans.inertia_) plt.plot(range(2, 9), wcss, markero) plt.xlabel(K) plt.ylabel(WCSS) plt.show()观察下来K从2到4时WCSS下降最快K4之后曲线明显平缓肘部位置在4。接着算轮廓系数K4的时候平均轮廓系数也在较高水平。业务那边也觉得分4个群体好讲故事于是最终定K4。3.4 聚类结果解读与落地方案聚类完之后关键工作是“给每个簇解释含义”。我打印了每个簇的质心把标准化后的数值还原成业务含义整理成一张表簇最近购买间隔购买次数消费金额业务解读簇0很短很高中高频活跃、客单价中等属于“日常复购型用户”簇1中等低很高单次消费高但频次低属于“大促型/价格不敏感用户”簇2很长低低基本流失或临期流失用户需要召回簇3很短低中新近购买但频次还不高属于“潜力用户”你看K-Means本身只是把样本分堆但每一堆落到业务上就有明确含义了。运营部门基于这张表分别制定了不同策略簇0做会员日促销簇1主推新品和高端线簇2发召回券簇3做新客成长任务。整个过程中我也反复校验过每个簇的样本量分布没有出现某个簇只有几十个人的极端情况说明K4的分群是比较稳健的。3.5 可视化输出散点图和热图聚类结果的呈现方式也很重要。我会把数据用PCA降到二维每个点按标签着色画出散点图。图形上一眼看得出哪几个群体在空间上分得比较开哪些边界模糊。这能帮助业务同事建立直观感觉。另一类有用的输出是聚类热图。做法很简单把原始特征矩阵按簇标签排序每一行是一个样本每一列是一个特征颜色深浅表示值的大小。这样“行”会自然形成几个明显的块特征在不同簇上的差异一眼就能看出来。生信领域常说的“聚类热图趋势图富集条目”思路和这个是一样的只是把特征换成了基因表达量或蛋白丰度。模式是可复用的聚类、排序、上色、找规律。4. 常见问题与排查技巧实录4.1 K值换一个就大变如果你发现“K3时结果很稳K4时簇的形状乱七八糟”先别急着怀疑算法。这往往说明数据本身没有非常清晰的簇结构真实分布更像连续的过渡带。K值的微小变化会导致算法在几个相近的解之间跳跃。这种情况我的处理办法是回到业务目标你到底希望划分出多少个可解释的群体如果业务上只需要粗粒度分群就选更小的K如果希望用聚类做精细化操作可以接受模糊边界。也可以用Gap Statistic这类统计方法来辅助判断但核心还是要看业务能不能把结果用起来。4.2 聚类结果不稳定有个朋友问过我为什么同样的数据、同样的K值每次跑出来的簇都不一样这基本就是初始化的问题。K-Means的目标函数非凸随机初始化会收敛到不同局部最优。解决方法是设置n_init为一个较大的值比如50或100让算法多尝试几次选目标函数最小的结果同时把random_state固定下来保证复现。如果你用了这些技巧后结果仍不稳定再检查一下数据里是不是有特别明显的离群点、或者特征分布严重偏态。数据的预处理质量对聚类稳定性的影响远大于算法参数。4.3 出现空簇空簇就是某个簇最终没有分配到任何样本这在K较大的时候偶尔出现。我会先检查是不是K值设置过大或者初始质心选得太集中。排除手段有两个一是换成K-Means降低初始化风险二是调小K值或者加大迭代轮数。如果业务上必须保留那么多簇可以考虑改用带监督信息的初始质心或先用层次聚类跑一个小样本作为初始质心再把质心传给K-Means。这种“先层次后K-Means”的组合拳在小样本场景效果不错。4.4 高维数据效果差与降维选择K-Means在高维空间的表现会变差原因在于“维数灾难”维度越高所有样本之间的距离会越来越接近最终变得差别不大。此时聚类结果往往没有实际意义。我的通常做法是先用PCA或者UMAP把维度压到合适范围保留主要信息再做K-Means或者先做特征选择剔除无关和冗余特征。比如在文本聚类场景原始向量维度动不动成千上万不降维根本没法用。有些做材料筛选、生物信息学这类特征极多的项目也会先通过K-Means做一趟探索把大规模计算放到分布式框架里跑但前提同样是先降维或特征筛选。4.5 离群点影响严重离群点对K-Means的影响有两个一是可能被单独分成一个簇二是会严重拉偏某个簇的质心。因为均值本身对极端值敏感一个离群点就能把一个簇的中心扯到奇怪的地方。数据量大的话我建议先基于业务规则剔除明显的异常记录或者用IQR、Z-score等方法做离群值截断。如果就是希望算法对离群点更鲁棒可以换K-Medoids中心点为实际样本不用均值或者换DBSCAN这类密度聚类它对离群点的处理天然更友好。K-Means的定位是快速、可解释的第一轮探索别指望它什么问题都能扛。4.6 大数据量处理Mini-Batch K-Means业务数据量一旦到百万级甚至亿级标准K-Means每轮迭代都要计算全部样本到所有质心的距离效率很低。scikit-learn提供了Mini-Batch K-Means思路是每次迭代只随机采一小批样本更新质心而不是用全量数据。实测下来几百万条数据用Mini-Batch可以把训练时间从小时级降到分钟级代价是结果会有轻微波动偏差通常可接受。如果量级更大还可以用Spark MLlib里的KMeans它在分布式环境下跑得更好。实验室搭建机器学习服务器时比如有朋友问“该买什么配置”我通常会说做K-Means这类聚类任务时CPU核数和内存比GPU更关键因为算法本质是大量距离计算而不是矩阵训练。硬件预算有限时优先堆CPU核数和内存容量。5. 工具选型与学习路径建议5.1 用什么软件这个问题被问过很多次K-Means到底用什么软件我在不同阶段用过不同选择。学校做实验、写作业阶段用Python和scikit-learn最方便生态全面代码量少可视化也顺手。Matlab自带kmeans函数很多工科教材里也有但工程落地性不如Python。做数据分析的人可能熟悉RR里的kmeans函数也很好用。如果要给一个通用建议新项目用Python。原因不是Matlab或R不好而是Python从数据处理、建模到部署的链路最完整。你在一个项目里通常不只是跑一个K-Means还会涉及清洗、可视化、模型解释、自动化调度这些工作在Python生态里无缝衔接。5.2 适合新手的资料热词里出现了很多资源名称像吴恩达机器学习、周志华《机器学习》、李宏毅机器学习、《机器学习实战》、PRML这些都值得看但不用一口气全部啃完。我的建议是按顺序来先看吴恩达机器学习的K-Means章节配合编程作业把算法实现跑通。这一份资料对零基础最友好数学要求也不高。如果想深入了解公式推导和聚类理论基础再看周志华《机器学习》第十四章聚类部分书里把不同聚类算法的优缺点讲得很透彻。李宏毅的课程更活泼适合补充概念和直觉。PRML那本偏数学理论不建议入门阶段硬啃等你想深挖泛化误差界、概率模型这类理论时再回来看。《机器学习实战》里的K-Means章节也值得实操一遍因为它会教你怎么用聚类做实际任务比如地图上的兴趣点分组。我个人的习惯是每学一个算法先不看答案自己手写一遍再用库函数实现一遍最后找一个真实数据跑一遍把这个过程写成笔记。这样做一遍比看十遍视频效果都好。5.3 课程作业和期末复习怎么抓重点热词里出现了很多高校的“机器学习期末”相关搜索说明不少人正在准备课程考核。K-Means几乎是各类机器学习课程期末的必考概念复习时抓住几个核心点就行算法步骤描述、目标函数、K值选择方法、K-Means与KNN的区别、K-Means与层次聚类的优缺点对比。这些点理解透彻应付考试足够。如果是实验报告或者课程设计重点想清楚三件事一是数据来源和预处理步骤二是K值确定的依据三是聚类结果如何可视化并解释。老师最看重的不只是代码能不能跑而是你是否理解了为什么会得到这个结果。汇报的时候把标准化的原因、K值选择的论证过程讲清楚比展示一堆花花绿绿的图更有说服力。最后说说我自己的一点体会。做机器学习项目这些年我发现K-Means这类“简单算法”最大的价值不是精度多高而是它能快速帮人建立起对数据的直觉。很多复杂模型本质上也是在解决“把相似的东西聚在一起”这个问题K-Means是最朴素的一种实现。所以我的建议是别因为它简单就轻视它把预处理、K值、初始化、结果解释这一整套工作流磨练扎实你会发现在很多任务里K-Means都能给出一个不错的基线方案甚至有时候基线已经够用了。
返回列表