机器学习基础篇这个系列走到了第十篇,今天聊聚类。很多刚开始学无监督学习的朋友都有过一个相似的疑问:分类和回归都有标签,训练完还能对答案,聚类什么都没有,真做完了怎么知道自己做得对不对?我的答案很简单——聚类本来就不是为了求一个“标准答案”,而是帮你做结构发现。你给它一堆没有标签的数据,它按照某种相似性定义,把这些数据切成几块相对整齐的组。至于切得合不合理,最终的裁判往往不是数学指标,而是你能不能解释这些组,能不能拿它们去做点什么。
这篇文章我会把聚类背后最关键的几个算法、K值怎么选、聚类效果怎么评估,以及我实际做项目时踩过的坑都过一遍。内容尽量少堆公式,多讲直觉和可复现的操作。适合刚学完分类和回归、准备进入无监督学习的读者,也适合面试前想快速把聚类知识梳理清楚的人。
1. 聚类和分类只差一个字,思路却差了一个维度
1.1 没有标准答案怎么学?——聚类的目标与判定逻辑
聚类问题的标准表述其实非常简单:给定N个没有任何标签的样本,把这N个样本划分成K个组,使得组内样本尽量相似,组与组之间尽量不同。
这句话里有两个关键的地方都留了“活口”。第一个是“相似”怎么定义?第二个是K到底是多少?分类任务里,标签替你把答案定死了,你只需要学一条边界;聚类任务里,这两个问题都需要你来定,所以聚类玩到后面,更像是在回答“你觉得这些数据在结构上应该长什么样”。
我把“相似”这个词说透一点。假如你有一堆书要分到几个架子上,你可以按作者分、按出版社分、按厚度分、也可以按读过的次数分。每种分法都成立,区别只在于你选择的相似性标准是什么。数据也是一样,同一个客户数据集,你用消费金额做距离和用注册天数做距离,得到的群体几乎肯定不一样。所以理解聚类的第一步不是背算法,而是先把业务上的“相似性”想清楚,再决定用什么特征、什么距离。
按照这个思路,一个正经的聚类项目基本就是一套固定流程:确定要用哪些特征、做标准化、选择相似性度量、选择聚类算法、确定簇数、评估并解释结果。后面的章节基本都绕着这条线展开。
1.2 聚类在生活中已经替你做了很多决定
以前我在讲聚类的时候常用一句话:“分类和回归是回答问题的,聚类是帮你寻找提问角度的。”最典型的是用户分群。电商平台把用户的行为数据整理成向量,比如注册天数、月均消费次数、平均客单价、最近一次访问距今多少天,然后把用户聚成几类,再给每类起一个运营名字。这样后续发什么券、做什么活动就有了非常具体的抓手。
异常检测也大量依赖聚类思维。正常数据往往形成少数几片密集区域,异常点会落在远离这些区域的位置。信用卡交易风控里就常把正常交易聚成几块,看看哪些样本离所有簇都太远,离群值再去人工复核。图像处理里也有个经典玩法:颜色量化。一张图片可能有几十万种颜色,用聚类把颜色压成16种或者32种,然后重新映射整张图,文件体积会明显变小,同时视觉上还能保持基本观感。这个思路听起来很玄,实际上K-Means就能干。
搜索引擎里把相似文档聚合、推荐系统冷启动阶段给内容分组、零售里面给SKU做商品结构分层,背后都是聚类。这些应用场景看着五花八门,但本质上都在做同一件事——在没有先验答案的情况下,让数据自己把结构暴露出来。
2. 四个最常用的聚类算法,各自的脾气与门道
聚类算法实在太多,但入门阶段把节奏感找到最要紧。我先挑四个最有代表性的展开:K-Means、层次聚类、DBSCAN、高斯混合模型。前两个分属重心型和层级型,后两个覆盖密度型和概率型。四个走一遍,你对整个聚类的光谱也就见到底了。
2.1 K-Means:简单但前提苛刻
K-Means是绝大多数人学到的第一个聚类算法。它的逻辑特别朴素:你先指定K个重心,每个样本分给离它最近的重心,然后每个簇内部算均值作为新重心,反复迭代,直到重心基本不动。
它真正在优化的是一个很清晰的指标——簇内平方距离和,也就是SSE。形式化一点:
SSE = Σ Σ ||xi - μk||²
其中μk是第k个簇的质心。K-Means的每一次迭代,都在往“让每个点到质心的距离更近”这个方向挪,因此最终结果通常会保住一个不错的局部最优解。它速度快、实现简单、对大数据集友好,几十万行数据跑起来也没有压力。sklearn里的KMeans由于default初始化策略,通常会在多次随机初始化之后选一个SSE最小的解。
但K-Means的前提条件相当苛刻。第一,它默认你用欧氏距离判断远近,这暗中假设了簇的形状是球形;第二,它对离群点非常敏感,一个离群很远的值都能把质心拖走;第三,如果不同簇的规模、密度差异很大,它很容易把大簇切碎,把小簇往外撵;第四,所有特征你得提前调整到差不多的尺度,否则量纲大的特征会主宰整个距离。
所以我的建议是:K-Means当入门和快速原型很好用,但它不是万能模板。等数据形状稍复杂一点,你就要考虑下面几个算法了。
2.2 层次聚类:画出一棵树的耐心
层次聚类完全没有K值这个概念,它通过不断合并最相似的簇,给你生成一棵完整的分层树。最常用的凝聚式思路是:初始时每个样本自己是一个簇,然后每轮找出两个距离最近的簇合并,一直并到只剩一个簇为止。最后画出来那张树状图里,你可以任意剖一刀,得到不同粒度的分组。
这里最核心的是“簇与簇之间的距离”如何定义。sklearn里常见几种linkage:ward表示合并后让簇内方差增量最小,倾向于生成大小相近的球形簇,是最常用的;complete取两个簇之间最远样本的距离,结果偏紧簇;single取两个簇之间最近样本的距离,容易产生链条状结构,也容易被噪声带偏;average则是对两个簇所有样本的距离取平均,稳定性居中。我实际项目里用ward和average最多。
层次聚类的优势之一是不需要提前定K,直接看树的冠状结构就能感知大概有几群。很多年前我在某个数据挖掘项目里就习惯先用层次聚类看树状图,再用它给出的K去跑KMeans,效果很稳。劣势也很明显:复杂度差不多是O(n²)往上,样本超过一两万条就非常吃力;而且合并是一次性的,一旦合并错了,后面没法回退。
2.3 DBSCAN:处理形状不规则数据的利器
当数据呈现月牙形、环形、S形这类奇奇怪怪的形状时,K-Means基本上就废了,因为它骨子里只认球形。DBSCAN是密度聚类这一派的代表,思想也很有意思——哪里密度高,哪里就是簇;哪里的点太稀疏,那就算噪声。
DBSCAN有两个重要超参数:eps(邻域半径)和min_samples(一个点周围至少要有多少个样本,才能被当成“核心点”)。如果一个点周围的样本数不小于min_samples,它就是核心点;如果一个点不是核心点,但落在某个核心点的邻域内,它就是边界点;如果一个点既不是核心点也不挨着任何核心点,它就是噪声点,聚类结果里直接标成-1。算法从任意核心点出发,一路吸收邻域内的点和这些点能到达的核心点,最终生成簇。
相比K-Means,DBSCAN不需要指定K,能发现各种自由形状的簇,也能自动分离离群点,这在很多数据质量堪忧的场景里非常省心。它的缺点同样明显:两个超参数要调,而且当数据密度层次不一的时候很尴尬;维度一高,半径和邻域的概念就变得微妙起来,距离都差不多,密度判定容易失真。
选eps有个土办法:对每个点计算它到第min_samples近的邻居距离,把这些距离排个序画折线,曲线拐点附近的距离值就可以作为eps参考。这个技巧我后面章节还会提。
2.4 高斯混合模型:软聚类的另一个世界
K-Means给每个样本分配一个确定的簇标签,这叫硬聚类。高斯混合模型则是软聚类的代表:它假设整体数据是由若干个高斯分布混合生成的,每个样本不是“必须属于某簇”,而是“以多大的概率属于某簇”。
算法通过EM来迭代:E步是给定当前参数,计算每个样本归属到各个高斯分量的后验概率;M步是拿着这些概率作为权重,重新估计每个高斯分布的均值、协方差矩阵和混合系数。走几轮之后,参数逐渐稳定,你就得到了一个个椭圆形分布的簇中心,以及每个样本的归属概率。
GMM最大的价值在于带概率输出。医疗场景里,如果你想知道某个患者样本更像正常群体还是风险群体,拒绝直接给一个“硬标签”,一个概率数值就能给你非常充分的信息。GMM也可以通过协方差结构限制来控制复杂度,比如仅用球形高斯或者对角高斯,来防止参数过多。实际使用中,它通常用n_components来指定簇数,可以用BIC或AIC辅助选择。
下面这张表可以帮你快速回忆四种算法的适用边界:
| 算法 | 是否需指定K | 噪声处理能力 | 适合的形状 | 主要超参数 | 典型复杂度 | 选它的大致理由 |
|---|---|---|---|---|---|---|
| K-Means | 需要 | 较差,离群点会干扰质心 | 球状、规模相近 | K、init方式 | O(n×K×iter) | 数据量大、特征均匀、快速出原型 |
| 层次聚类 | 不需要 | 较差 | 由linkage决定,ward偏球形 | linkage、终止条件 | O(n²)以上 | 小数据、想同时看不同粒度结构 |
| DBSCAN | 不需要 | 自带噪声标签 | 任意形状、密度均匀 | eps、min_samples | 平均O(n log n) | 形状不规则、异常点多 |
| GMM | 需要 | 一般,但会有低概率兜底 | 椭圆形、可重叠 | n_components、协方差形式 | O(n×K×iter) | 需要概率结果、簇间边界模糊 |
3. K值选择与聚类效果评估:别只盯着眼睛看
K怎么选,是聚类入门里最让人头疼的问题。很多教程直接画两个轴的数据,说“你看这个图就能看出来是3簇”,这话看着轻巧,但真实项目的特征维度一多,根本无法直接可视化。所以必须有一套可量化的方法。
3.1 肘部法则的观察方法
肘部法则本质上是看SSE的边际收益。你分别用K=2、3、4……去跑K-Means,记录每一轮的SSE。K越大,簇数越多,每个点到质心的距离会不停下降,所以SSE必然单调递减。但福利在于,当K到达真实的簇数附近时,继续增加簇带来的“切碎”效果并不大,曲线会明显放缓,形成一个像手肘一样的拐点。
画出来是一条先陡降、再快速平缓的折线,拐弯处那个K就是候选答案。理论上很顺,实操里要泼一盆冷水——真实数据往往没有特别明显的拐点,尤其是连续型数据,曲线从头到尾都在平缓下降。这时候不能硬凹一个“肘部”,要结合业务判断,看看哪个K下的簇解释起来最顺。
量化方法也不只有肘部。Gap Statistic以及聚类包里的各种指标都可以用,但对入门者来说,先用肘部把区间缩小,再用后面的轮廓系数做最终判断,已经算非常高效的工作流。
3.2 剪影系数与内部评估指标
轮廓系数(Silhouette Coefficient)是评估聚类质量时我几乎必看的指标。它的核心思想是把“组内紧凑”和“组间分离”放到同一个数字里。
对某个样本i,记a为i到同簇所有其他样本的平均距离,这是衡量它待在簇里“有多挤”的指标;记b为i到最近的其他簇的平均距离,这是衡量它离别的簇“有多远”的指标。轮廓系数等于:
s = (b - a) / max(a, b)
s接近1,表示样本离自己簇很近、离最近的其他簇很远,分类是干净利落的;s接近0,表示这个样本正好卡在两个簇的交界处;s接近负值,说明它可能被分错了簇。把所有样本的轮廓系数取平均,就得到全局轮廓系数。
当然,除了轮廓系数还有别的内部指标,我单独列个表,方便对比:
| 指标 | 核心思想 | 取值范围 | 喜欢看到什么 | 注意点 |
|---|---|---|---|---|
| Silhouette Coefficient | 簇内紧凑度与簇间分离度的比较 | [-1, 1] | 越接近1越好 | 计算O(n²),大样本需要抽样 |
| Calinski-Harabasz | 簇间离差与簇内离差的比值 | 无上限 | 越大越好 | 对分布形状有假设 |
| Davies-Bouldin | 簇内散布和簇间距离的比率 | [0, +∞) | 越小越好 | 不同K之间对比用 |
| SSE / inertia | 簇内平方距离和 | 非负 | 同一方案内越小越好 | 单调递减,跨K无法直接比较 |
实际操作时,我会循环遍历候选K,分别计算平均轮廓系数,选系数最大的K作为参考。但是记住一个隐含问题:轮廓系数基于样本之间的距离定义,它是在“几何空间”里评分,而不是在“业务空间”里评分。
3.3 “看起来对”不等于“算法对”
这是我想重点强调的一句话。聚类的评估有几个层面,几何指标评估的是“数据在特征空间里是否被切成了几堆”,只要距离定义合理,指标高说明几何上分得开。但业务上是否真的有效,几何指标说了不算。
举个例子,你把一批用户按消费行为聚成三簇,轮廓系数很高,三簇在图上分得干干净净。结果导出一个簇,里面既有真实新用户,也有流失后又回归的老用户,还有一批只用了一两次的高潜力用户,运营团队根本没法给这一簇写统一的沟通话术。那这个分群几何上再漂亮,业务价值也是打折的。
所以我的习惯是把聚类项目的验收拆成两步:第一步用轮廓系数这类指标筛选候选K值和候选特征组合,快速淘汰明显不靠谱的配置;第二步是给选出来的每个簇逐一做“可解释性检查”——这个簇里的人有什么共同点?他们应该看什么内容、拿什么优惠?如果他们之间找不出共性,说明这个簇没有运营价值,那么几何指标多高我都不放心。
4. 从数据到聚类:一个完整的可复现案例
前面讲了这么多原理,现在把整套流程串起来。为了不让你被乱七八糟的真实数据洗得云里雾里,我先生成一组结构清晰的模拟数据,跑一遍聚类全流程,再讨论怎么把聚类结果落地成业务语言。我在线下培训时也一直用这个套路,先让学员把技术链路跑顺,再去接真实场景。
4.1 数据准备与标准化的重要性
这里用sklearn的make_blobs生成三团数据:每一团内部是高斯的,彼此之间有比较明显的间隔,一共360个样本。在sklearn里make_blobs会同时返回样本矩阵和真实标签,真实标签我们故意丢掉,因为聚类就是要在没有标准答案的情况下找结构。
为了把“标准化”这一步体现出来,我先不引入真实数据,但做法和真实数据完全一样:直接用StandardScaler对特征做均值0、标准差1的变换。为什么要这么做?因为聚类算法判断相似性靠的是距离,而距离计算里,数值范围更大的特征会天然获得更大的话语权。比如“年龄0到100”和“年消费0到100000”,欧氏距离几乎完全被消费金额主导,年龄维度在距离里根本不值一提。标准化之后,每个特征的尺度才在同一水平线上,距离也才能相对公平地综合所有维度。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler # 生成三团结构清晰的模拟数据 X, _ = make_blobs(n_samples=360, centers=3, cluster_std=0.8, random_state=42) # 标准化:每个特征变成均值0、标准差1 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 画一下原始形态,确认是三团 plt.scatter(X_scaled[:, 0], X_scaled[:, 1], s=12) plt.show()标准化这里有个细节:如果你在业务上明确知道某个特征更重要,可以不缩放它,或者单独给它一个更大的缩放系数。标准化只是默认策略,不是铁律。
4.2 用肘部法和轮廓系数确定K值
数据已经准备好,接下来先用肘部法则看K的大致范围。我会跑一个从K=1到K=10的循环,记录每次的SSE。注意K=1时只有一个簇,SSE就是所有样本到数据中心点的距离和,作为曲线起点很自然。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse = [] for k in range(1, 11): km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) km.fit(X_scaled) sse.append(km.inertia_) plt.plot(range(1, 11), sse, marker='o') plt.xlabel('K') plt.ylabel('SSE') plt.title('Elbow Method') plt.show() # 再算一下K=2到K=10的轮廓系数 sil_scores = [] for k in range(2, 11): km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X_scaled) sil_scores.append(silhouette_score(X_scaled, labels)) for k, s in zip(range(2, 11), sil_scores): print(f"K={k}, silhouette={s:.3f}")用上面这份数据跑出来的结果一般是:SSE曲线在K=3之后下降速度明显变缓,也就是手肘的位置在3附近;轮廓系数也是K=3时最高。两个指标指向同一个答案,这个K就可以拿去做建模了。
4.3 建模、可视化和结果解释
确定K=3后,正式跑一次K-Means。我用init='k-means++',n_init=10,random_state固定为42。k-means++的作用是让初始质心彼此离得远一些,降低陷入糟糕局部最优解的风险;n_init=10表示算法会做10次随机初始化,最后返回SSE最小的一次;固定random_state是为了结果可复现,这是做分析报告时一个很容易忽略的细节——上次跑和这次跑结果完全一样,你才能安心调参。
km = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X_scaled) plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=labels, cmap='viridis', s=12) plt.scatter(km.cluster_centers_[:, 0], km.cluster_centers_[:, 1], color='red', marker='x', s=80, label='centers') plt.legend() plt.show() # 输出每个簇的中心点坐标(标准化空间里) centers_scaled = pd.DataFrame(km.cluster_centers_, columns=["dim1", "dim2"]) print(centers_scaled)画完图,多半能看到三团非常干净的颜色分布和三个红色的叉号质心。但我要强调,聚类做到这里只走完一半,剩下的一半是把结果解释给业务听。我在做会员分群项目时,习惯把聚类之后的结果再往原始特征投影回去,查看每个簇在每个维度上的均值和中位数。比如模拟一个场景,这里的dim1是“月均消费次数”,dim2是“平均客单价”,那生成的三个簇可能对应:
- 簇0:次数高、客单价低,像是高频日常型用户,适合推送优惠券促进连带销售;
- 簇1:次数低、客单价高,像是大促驱动的价值用户,适合做重点客户维护;
- 簇2:次数和客单价都居中,属于普通用户,可以按常规节奏养。
给每个簇“起名字”这个动作非常关键。能从算法结果反推业务语义,说明你用的特征和K是合理的;如果反推不出来,就要回前面调整特征设计,而不是继续调参。
5. 实战踩过的坑与一些思考
最后这部分是纯经验分享。聚类坑非常多,很多坑看着是算法问题,实际是数据或者业务理解的问题。我按踩坑频率从高到低挑几个最典型的讲。
5.1 标准化与特征选择比调参更影响结果
我第一次在一个会员数据项目里做K-Means时,把“注册天数”“累计消费”“最近一次登录距今时间”一股脑丢进去。第一次跑出来的结果惨不忍睹,一个簇几乎全是注册天数特别长的老用户,另一个全是新用户,消费维度完全没有发挥作用。原因就是“注册天数”数值上万,方差巨大,彻底把其他特征按死在距离计算里。
标准化之后好了一些,但接着又发现一个问题——“最近一次登录距今时间”这个特征用原始天数不太好,因为几百年一遇的老僵尸用户和真正活跃用户之间差值巨大,离群值严重影响质心。后来我把它改造成“最近30天是否活跃”之类的0/1特征,或者做一些截断处理,聚类结构才变清晰。
特征选择的坑同样隐蔽。聚类模型会把每个特征都当成同类信息参与距离计算,如果你不小心塞进去一两个无关变量,它们产生的距离干扰可能会把原本清晰的簇活生生打散。所以我的建议是:聚类前先做一轮特征初筛,基于业务理解把明显无关的变量拿掉;当你拿不准某个特征要不要留时,可以跑两轮聚类对比,看哪一轮结果的轮廓系数更高、业务可解释性更清楚。
5.2 KMeans随机性的坑要从配置上堵死
K-Means是一个依赖随机性的算法。如果你只跑一次,很可能运气不好落在糟糕的局部最优解里。很多初学者会问:“上次我设置的random_state和这次不一样,怎么结果也跟着变了?”那是正常的,因为你没有控制随机种子。
我日常使用的固定套路是这样的:n_init设置成10或者更多,让算法多试几次随机初始化;线上或者交付给业务时固定random_state,保证可复现;如果条件允许,同一份数据跑5次不同种子,观察哪些样本的簇归属经常变动。归属经常变的样本往往位于簇与簇之间的模糊地带,这些样本在后续运营里需要单独对待,不应该和其他边界清晰的样本享受完全一样的策略。
还有一点,KMeans对离群值特别敏感。虽然直觉上“离群点只要距离远就会自己形成一个簇”,但如果这个离群点恰好落在两个簇之间,它能把质心拉偏。遇到离群点较多的脏数据集,可以先用异常检测筛选掉极端值,再用KMeans;或者干脆换K-Medoids这种对离群值更稳健的变体。
5.3 高维数据的陷阱,以及降维的“度”
特征维度一高,聚类会遇到一个非常隐秘的问题——距离集中化。在几十维乃至上百维的空间里,任意两个点的欧氏距离都差不多大,这不是数据本身没有结构,而是空间太大导致“最近邻”概念退化。这时候强行跑K-Means,结果往往像在均匀噪声里切格子,轮廓系数可能还不低,但业务解释根本套不住。
常见的应对办法是先降维。PCA是我用得最多的,把高维数据先投影到2到3维,再在低维空间里聚类。这样做的最大好处是能可视化,方便你和业务方沟通。但不要直接在降维后的二维图上接结论,这个二维投影只是原空间的线性或者非线性压缩,你还需要回到原始特征空间去检查每个簇的真实差异。我见过有人对着UMAP降维后的图说“这个K肯定对”,然后丢进了项目——UMAP对局部结构特别敏感,画出来“分开”不等于原始空间里可分。
所以我的建议是:先把特征压缩到有效范围,再用聚类脚本分别测试原始空间和降维空间的结果,选业务解释更自然的那一个。
5.4 聚类没有裁判,但你仍然需要验收标准
做分类项目时,你可以拿着Accuracy、F1分数跟人拍胸脯说结果好不好;聚类项目没有一个这样的单点指标,因为“好”的定义在每个项目里都不同。这是聚类的难,也是聚类的有意思之处。
我个人经过几个项目之后形成了一套验收习惯:先列业务目标,明确这个聚类到底是给谁用的,是还想做用户分群、商品分层还是行为分段;然后再列允许的特征,排除掉不能用或者不该用的变量;接着跑多组候选配置,用肘部法加轮廓系数初步过滤;最后把每个簇导出明细,逐条人工抽查几十条样本。看起来最“笨”的这最后一步,往往能发现很多指标发现不了的问题,比如某簇样本其实来自同一个设备类型、某个簇在时间上有明显聚集等。
最后再分享一个我自己保留的小习惯:一个聚类项目收尾时,我会专门写一段“每个簇的一句话定义”。这段定义必须能用大白话讲清楚,比如“这是一个高频低客单价的年轻用户群”“这是一批在夜间活跃的内容消费用户”。如果这段话我写不出来,我会回去重新调整特征、距离或者K,而不是强行上线。聚类的结果说到底不是让机器满意,而是让决策更为清晰。
微信号谈机器学习的同行经常问我要一份聚类的入门清单,我一般就会把这一篇发过去。从业务定义到算法选型,从K值判断到落地验收,整条链路走一遍,后续见到再复杂的无监督问题,至少你知道该从哪个方向思考了。