说句实在话,做数据这行当久了,聚类算法都快成条件反射了。拿到一批没有标签的数据,先跑个聚类看看形态,几乎成了常规动作。但问题也出在这——很多人一上来就KMeans(n_clusters=3),跑完画个散点图就算交差,结果换批数据就翻车。聚类算法不是这么用的,它背后的选型逻辑、参数含义、评估方式,每一项都值得掰开揉碎讲清楚。
这篇内容聚焦聚类算法本身,重点覆盖 K-Means、层次聚类、DBSCAN 和 GMM 这几类主流方案,会讲到它们各自的适用场景、为什么这么选、以及实际跑数据时最容易踩的坑。适合刚接触无监督学习的人,也适合那些已经在用但总感觉哪里不对劲的同行。不管你是做用户分群、异常检测还是数据预处理,聚类这套东西都绕不开,不如一次理顺。
1. 聚类算法到底在解决什么问题
1.1 一句话理解聚类
聚类的目标说起来很简单:把相似的数据点归到一起,让组内尽量紧凑、组间尽量疏远。但这句话落到实际数据上,就有无数种"相似"的定义方式。欧氏距离算相似?还是余弦相似度?或者干脆不靠距离,靠密度?不同的定义方式催生了不同的算法,也决定了每种算法的适用边界。
我习惯把聚类比喻成收拾房间。K-Means 像是按"东西大小"分类,先大致划几个区域,然后反复微调;层次聚类像是从每一个单件物品开始,逐步把相近的合并进同一个抽屉;DBSCAN 则更像按"使用频率"判断——经常一起用的放一起,偶尔出现的孤品自己待着。没有哪种方式绝对正确,只有合不合适。
1.2 什么时候该用聚类
聚类不是万能的,但它解决的问题足够典型。最常见的有三类:
- 探索性分析:数据刚到手,没有任何标签,想先看看整体结构。比如电商用户的行为数据,跑一遍聚类,可能自然分出"高活跃付费型""周末冲动型""沉睡用户"几类,后续精细化运营就有抓手了。
- 异常检测的前置手段:密度类聚类算法能天然识别离群点,DBSCAN 里那些不属于任何簇的点,往往就是异常样本。我在处理交易风控数据时,就经常用这个方法先粗筛一遍。
- 特征工程与数据压缩:将几千个原始特征聚成几十个簇,再用簇中心或簇编号作为新特征喂给下游模型,既能降维又能保留一定的非线性结构信息。
有一点必须提前泼冷水:聚类是"无监督"的,它只能告诉你数据里有几坨、每坨长什么样,不会告诉你这些坨叫什么名字。给每个簇打业务标签,永远是人的工作,算法替代不了。
2. 主流聚类算法横向对比
2.1 K-Means:最快上手的聚类方案
K-Means 是绝大多数人接触的第一个聚类算法,也是我工作中跑得最多的。它的核心逻辑很朴素:随机初始化 K 个中心点,计算每个样本到各中心的距离,归属到最近的中心,然后重新计算每个簇的中心位置,反复迭代直到中心不再明显移动。
这个算法的优点极其突出:快、简单、可解释性强。样本量十万级别的数据,几秒钟就能跑完。但它的短板也藏得很深:
- 必须预先指定 K。这是 K-Means 最让人头疼的问题,后面会专门讲怎么定 K。
- 对初始中心敏感。不同的随机种子可能得到不同的结果,所以实际使用中通常要跑多次取最优。
- 倾向于凸形簇。K-Means 用欧氏距离划分边界,本质是 Voronoi 划分,对"月牙形""环形"这类非凸形状的簇几乎无能为力。
- 对离群点敏感。均值本身不具有稳健性,一个极端点就能把整个簇中心拉偏。
我见过太多人在 K-Means 上翻车,不是算法不行,而是没搞明白它的假设前提:它假设每个簇是各向同性的高斯分布,即球形簇。数据长得不像球,就别怪 K-Means 不给力。
2.2 层次聚类:结果自带树状结构
层次聚类分两种:自底向上的凝聚法和自顶向下的分裂法。实际中用得最多的是凝聚法:先把每个样本当成一个独立的簇,然后每次合并距离最近的两个簇,直到所有样本都合并成一个簇为止。合并的过程可以用树状图(dendrogram)完整记录下来。
这个算法的价值在于:聚类的过程完全透明。你可以从树状图上看到数据从细到粗的整个层级关系,然后根据需要的粒度去"切一刀",得到想要的簇数。不像 K-Means 那样必须提前拍脑袋定 K,层次聚类给了你一个后验的选择空间。
但层次聚类有个致命弱点:时间复杂度高。经典实现是 O(n²) 甚至 O(n³),数据量超过一万条就会明显变慢。而且合并操作不可逆,一旦两个簇被合并,后面就没有回头路。所以它在小样本、对可解释性要求高的场景里更有优势,比如物种分类、文档主题层级归纳。
另外,层次聚类里"簇间距离"的定义方式也有讲究。单连接(最小距离)容易产生细长的链状簇,全连接(最大距离)偏向紧凑的球形簇,平均连接居中,Ward 法则通过最小化合并后的方差增量来合并,通常效果最稳。我个人默认用 Ward。
2.3 DBSCAN:处理任意形状簇的利器
热搜里专门提到 dbscan聚类算法,这确实是个值得认真对待的家伙。DBSCAN 的全称是 Density-Based Spatial Clustering of Applications with Noise,基于密度的空间聚类。它跟 K-Means 的底层逻辑完全不同——它不关心"中心",只关心"密度连通"。
核心思想就两句话:在半径 eps 范围内,如果邻居数超过 min_samples,这个点就是核心点;核心点的邻居不断往外扩展,密度相连的点组成一个簇;那些既不是核心点、也够不着任何核心点的样本,被标记为噪声。
这个机制带来几个 K-Means 给不了的能力:
- 可以识别任意形状的簇。环形的、月牙形的、S 形的,只要密度连续,DBSCAN 都能给你揪出来。
- 天然处理离群点。噪声点被单独标出来,这在对脏数据比较敏感的场景里太宝贵了。
- 不需要预先指定簇数。簇的数量由数据密度分布自动决定。
但 DBSCAN 的痛点同样明显。eps 和 min_samples 这对参数对结果影响极大,而数据密度不均匀时,一个全局的 eps 根本不够用——密度高的区域连成一片,密度低的区域全被当成噪声。遇到这种情况,后续要单独说怎么应对。
2.4 高斯混合模型(GMM)
GMM 可以理解为 K-Means 的概率化升级版。K-Means 是"硬聚类",每个样本非此即彼;GMM 假设每个簇服从一个高斯分布,然后通过 EM 算法迭代估计每个分布的均值、方差和权重,最终给出每个样本属于每个簇的概率。
这个"概率输出"在某些场景下太关键了。比如做用户分群时,一个用户可能 60% 像高价值用户、40% 像潜在流失用户,这种软归属信息比硬分类丰富得多。而且 GMM 允许不同簇有不同的大小和形状,比 K-Means 的"球形假设"灵活不少。
代价是计算量更大、参数更多,而且容易陷入局部最优。EM 算法的初始值同样重要,需要多跑几次对比。
把这几个算法放到一起看,选型逻辑就清晰了。
| 算法 | 簇形状 | 簇数指定 | 噪声处理 | 数据规模 | 主要短板 |
|---|---|---|---|---|---|
| K-Means | 凸形/球形 | 必须指定 | 不友好 | 大 | 对形状和离群点敏感 |
| 层次聚类 | 较灵活 | 可通过树状图后定 | 一般 | 小 | 计算复杂度高 |
| DBSCAN | 任意形状 | 自动 | 天然支持 | 中 | 密度不均时易失效 |
| GMM | 椭圆形 | 必须指定 | 一般 | 中 | 计算量大,易局部最优 |
3. 实操:从数据到聚类结果的完整流程
3.1 数据准备与预处理
很多聚类翻车案例,问题不是出在算法,而是出在数据进入算法之前。预处理这一步我踩过的坑最深,先说三个关键点:
量纲归一化不是可选项,是必选项。K-Means 和 DBSCAN 都依赖距离计算,如果特征 A 的取值范围是 0.1 到 0.9,特征 B 是 1000 到 90000,那 B 几乎会完全主导距离,A 相当于白给。我用 StandardScaler 把每个特征标准化到均值为 0、方差为 1,这是最常规也最稳的做法。
高维数据先降维再看效果。聚类在低维空间的表现通常比高维直观得多。我习惯先用 PCA 降到二维或三维,跑一遍聚类后把结果可视化,确认簇的分离度,再回到原始维度去做正式建模。这不只是为了画图,更重要的是能提前发现数据形态问题。
离散特征处理要谨慎。类别型特征如果用 one-hot 编码,会让特征维度暴涨,而且距离计算的意义会变得模糊。经验是:如果类别特征很多,先考虑是否有必要纳入聚类;真要纳入,可以用适当的编码方式(如 target encoding 或 MDS 降维后的向量)替代裸 one-hot。
3.2 代码实现:三种算法跑一遍
直接用 scikit-learn 就能完成绝大多数聚类需求。我把三种算法的核心调用方法写出来,顺便标注最容易犯错的地方。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import make_blobs, make_moons from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN from sklearn.mixture import GaussianMixture # 构造一个人造数据集:两个弧形簇 + 少量噪声 X, y_true = make_moons(n_samples=500, noise=0.08, random_state=42) rng = np.random.RandomState(42) noise = rng.uniform(-1.5, 2.5, size=(50, 2)) X = np.vstack([X, noise]) X = StandardScaler().fit_transform(X)# K-Means kmeans = KMeans(n_clusters=2, random_state=42, n_init=10) labels_kmeans = kmeans.fit_predict(X) # 层次聚类(Ward 连接) hier = AgglomerativeClustering(n_clusters=2, linkage='ward') labels_hier = hier.fit_predict(X) # DBSCAN dbscan = DBSCAN(eps=0.3, min_samples=5) labels_dbscan = dbscan.fit_predict(X) # DBSCAN 中标签为 -1 的样本是噪声# GMM gmm = GaussianMixture(n_components=2, covariance_type='full', random_state=42) labels_gmm = gmm.fit_predict(X) proba_gmm = gmm.predict_proba(X) # 软归属概率光看调用代码没啥感觉,真正有价值的是放在同一张图上对比。以我构造的这个"弧形簇 + 噪声"数据为例,K-Means 会强行把两个弧形从上到下切成两半,层次聚类(Ward)也好不到哪去,但 DBSCAN 能干净地把两个弧形分开,并把散布的噪声单独标记出来。这就是直观感受算法差异最好的方式。
n_init=10是 K-Means 需要特别留意的参数。旧版本默认只跑一次,很容易撞上局部最优;新版本默认是 10 次取最优。如果你还在用老代码,建议显式指定n_init=10,不要吃默认值的亏。
3.3 如何评估聚类效果
聚类没有"标准答案",但这不代表没法评估。分两种情况:
有真实标签:用外部指标,最常用的是调整兰德指数(ARI)和归一化互信息(NMI)。这两个指标都校正了随机分组的影响,1 表示完全一致,0 表示接近随机。直接用sklearn.metrics.adjusted_rand_score(y_true, labels)就能算。
没有真实标签:用内部指标。轮廓系数(Silhouette Score)是最常用的,对每个样本计算它到同簇其他样本的平均距离 a,以及它到最近的其他簇样本平均距离 b,轮廓系数 = (b-a)/max(a,b)。取值在 -1 到 1 之间,越接近 1 说明簇内紧凑且簇间分离。
from sklearn.metrics import silhouette_score, adjusted_rand_score # 有标签的评估 print("ARI - KMeans:", adjusted_rand_score(y_true, labels_kmeans)) print("ARI - DBSCAN:", adjusted_rand_score(y_true, labels_dbscan)) # 无标签的评估(剔除 DBSCAN 的噪声点) mask = labels_dbscan != -1 print("轮廓系数 - DBSCAN:", silhouette_score(X[mask], labels_dbscan[mask]))注意一个细节:轮廓系数只在 DBSCAN 剔除噪声后再计算。噪声点的存在会严重拉低轮廓值,不剔除的话评估结果没有参考意义。
4. 常见问题与调参避坑
4.1 K 值到底怎么定
这是 K-Means 和 GMM 绕不过去的问题。我的做法是三个方法交叉验证:
肘部法则:把 K 从 1 试到 10,记录每个 K 对应的簇内误差平方和(SSE),画折线图。SSE 随 K 增大必然下降,但下降速度会在某个 K 处明显放缓,这个"肘部"就是推荐值。SSE 可以直接用kmeans.inertia_取到。
轮廓系数法:对每个 K 计算整体轮廓系数,取最大值对应的 K。不过轮廓系数偏向小的 K,所以我会结合肘部图一起看。
业务合理性:这是最重要的一条。算出来的 K 再好,如果分出来的簇在业务上没法解释、没法落地,就得调。比如用户聚类 K=3 时每类都有清晰的运营动作,K=5 时有两类怎么解释都牵强,那就用 3。
4.2 DBSCAN 的 eps 和 min_samples 怎么调
DBSCAN 调参让人头大的点在于:eps 是关键参数,而它的合适值高度依赖数据尺度和密度分布。这里有两个实用方法:
K-距离图法:对每个样本计算它到第 k 个最近邻居的距离(k 通常取 min_samples),把这些距离从小到大排序,画折线图。曲线会发生突变的位置,对应的距离就是合适的 eps。这个方法的原理是:簇内的第 k 近邻距离小,边缘点和噪声的第 k 近邻距离大,突变点就是密度与非密度的分界线。
from sklearn.neighbors import NearestNeighbors k = 5 nn = NearestNeighbors(n_neighbors=k).fit(X) distances, _ = nn.kneighbors(X) k_dist = np.sort(distances[:, -1]) # 每个样本到第 k 近邻的距离 plt.plot(k_dist)曲线出现明显拐弯的地方,读取对应的 y 轴值,作为 eps 的初始值。然后再往两边微调,结合轮廓系数和簇数合理性做最终确定。
min_samples 的经验值:min_samples 通常取数据维度数的两倍或更大。比如二维数据就取 4~5,高维数据可以取 10~20。这个参数影响簇的平滑程度,取太小簇会很碎,取太大又容易把细密的簇合并或全部标成噪声。
4.3 数据密度不均匀怎么办
这是 DBSCAN 最大的软肋。一个全局 eps 对高密度区域来说是"局部范围太大",对低密度区域来说又"什么都够不着"。我实际遇到过的场景是:用户行为数据中,头部用户行为密集,长尾用户行为稀疏,DBSCAN 跑出来头部一片噪声、尾部也一片噪声,中间勉强有一两个簇,毫无业务价值。
应对方案有几种:
- 分层处理:先用粗粒度的方式进行划分,比如按业务维度拆成不同子集,每个子集单独调参。这个做法最直接,也最容易解释给业务方。
- 密度自适应算法:换用 OPTICS 算法,它本质上是 DBSCAN 的推广,不需要全局 eps,而是生成一个可达距离图,可以看作对聚类结构的"多尺度扫描"。sklearn 里有
sklearn.cluster.OPTICS,代价是更慢。 - 先降噪再聚类:先用孤立森林等异常检测方法把明显的异常点去掉,再用 DBSCAN 跑剩余数据,这样密度分布会更均匀一些。
4.4 聚类结果不稳定怎么办
K-Means 和 GMM 都有随机初始化的问题,同一份数据跑两次结果可能不同,这会让业务方质疑你的稳定性。我的经验是:
- 固定随机种子(
random_state=42),保证结果可复现,这在项目交付时是底线。 - 对多个随机种子跑多次,计算簇标签的一致性。如果两个随机种子下的结果差异很大,说明数据本身的簇结构不清晰,这时候要回头审视特征选择和数据质量,而不是加个种子硬扛。
- 用层次聚类或 DBSCAN 这类确定性算法做交叉验证。如果 K-Means 的结果和数据密度关系有明显出入,通常说明 K 或者预处理方式有问题。
5. 什么场景选什么算法
想清楚场景,再决定算法,顺序不能反。我把这些年的选型经验整理成一张速查逻辑:
- 数据量大、簇是凸形、业务方就要一个可解释的分群结果:K-Means,没得说。配合 PCA 可视化调 K 值,一套组合拳下来又快又稳。
- 样本量小、需要完整层次关系:层次聚类 + Ward 连接。比如给一组用户标签做层级归类,写成树状图给业务方看,比任何其他算法的输出都直观。
- 数据形状不规则、有噪声、不知道分几类:DBSCAN 优先。我处理地理位置数据、轨迹数据这类"簇形状天知道长什么样"的数据时,默认先跑 DBSCAN。
- 需要概率归属、簇的大小差异大:GMM。比如支付行为数据,不同簇的分布范围差几个数量级,GMM 的协方差矩阵能更好地刻画这种异质性。
- 数据密度极不均匀:先用 OPTICS 或分层处理,别指望一个全局 eps 解决所有问题。
选型时还有一个容易被忽略的因素:解释成本。你用的算法越复杂,给业务方讲清楚的难度就越大。在大部分商业场景下,"K-Means 把用户分成三类,均值分别是……"这种朴素结论的接受度,远高于"GMM 拟合出三个高斯分布的贝叶斯后验概率"。算法是为决策服务的,别为了炫技牺牲可解释性。
最后再分享一个小习惯:跑聚类之前,永远先画图看数据。二维三维能直接看,高维就先用 PCA 或 t-SNE 降维再看。数据长什么样,该用什么算法,心里就有数了。我见过太多人拿着几万字写好的调参策略,结果数据本身一团乱麻,怎么调都白搭。记住这条原则,聚类的路会顺很多。