跳转至

P4-17.1 聚类(clustering)的直觉

Section ID: P4-17.1 Version: v2026.07.20

在 P4-16 里,我们一路看到梯度提升(gradient boosting),跟着理解了在有正确答案标签(label)的问题里,模型怎样提高预测性能。到了这里,如果把视角稍微转一下,下一个问题就会出现。

如果根本没有正确答案标签,怎样才能在数据里找到结构?

这个问题,就是聚类(clustering)的出发点。

聚类是在没有正确答案标签的情况下,试图找出哪些彼此相似的数据会形成一个个聚类(cluster)的无监督学习(unsupervised learning)问题。

也就是说,聚类与其说是把答案猜对的问题,不如说更接近先去发现结构的问题

这一节说明 clusteringcluster,以及正确答案标签和聚类的差别。下一节会在这个把手的基础上继续当前语境里的判断,而把分组提议读成结构探索的基本感觉,会再次通过这一节和概念词汇表连起来。

本节范围

这一节回答下面这些问题。

  • 聚类和监督学习(supervised learning)有什么不同?
  • 为什么相似(similar)这个说法这么重要?
  • 为什么聚类(cluster)和正确类别(class)不一样?
  • k-means 和 DBSCAN 分别展示了什么不同的直觉?
  • 应该用什么态度来读聚类结果?

这一节会先收束 聚类在没有标签的结构探索里回答什么问题。结果解释时的注意点会在 P4-17.2 继续处理,层次聚类(hierarchical clustering)和谱聚类(spectral clustering)会在 P4-17.3 补充学习中继续,silhouette score 等聚类质量指标会在 P4-6.4 补充学习里继续,而和降维一起阅读结构时的注意点会在 P4-18.1、P4-18.2 继续。

用聚类(clustering)的直觉留下的判断标准

  • 能把聚类解释成在没有标签时寻找结构
  • 能说明聚类不是人事先贴好的正确答案,而是从数据里找到的分组。
  • 能说明 k-means 和 DBSCAN 拥有不同的聚类直觉。
  • 能初步感觉到:为什么把聚类结果立刻当成事实或原因会有风险。

为什么需要这一节

刚开始学机器学习时,人们通常先想到的是分类还是回归。但在真实工作里,没有标签的数据反而可能更常见。

例如:

  • 想把客户按几种使用模式重新分组
  • 想看看新闻文章会聚成哪些主题团块
  • 想看看传感器数据会不会分成几种状态
  • 想看看是否有单独突出的异常点(outlier)

这些问题问的不是“正确答案是什么?”,而是“里面藏着什么结构?”。聚类就是进入这类问题的第一件工具。

也就是说,17.1 是把视野从预测模型转向数据结构探索的一节。

什么时候应该先想到聚类

聚类不是出现在“把答案猜对”的问题里,而是在是不是想先在没有标签的情况下看看结构成为标准时,自然出现。

当前问题 为什么会先想到聚类 先确认什么
想在没有标签的情况下先看看使用模式如何分组 因为它适合提出相似数据的分组候选 会用什么特征来定义相似
比起预测,更优先理解探索性的结构 因为目标是发现结构,不是预测答案 有没有把聚类当成正确答案来读
想看看异常点或单独突出的分组 因为可以通过密度/距离型聚类来看结构 k-means 和 DBSCAN 哪个更合适
想把客户/文档/传感器状态先拆成几团 因为它会成为提出分群假设的起点 聚类数或密度标准的敏感性
想形成后续的标注或领域审查问题 因为聚类能提出解释候选 是否把聚类结果直接送进了策略

这张表的核心,是把聚类放在预测模型的替代品之外,而放在结构探索和假设生成工具的位置上。

聚类到底想做什么

scikit-learn 用户指南把 clustering 描述成对无标签数据执行的任务。也就是说,它不是在正确答案已给定后再去预测,而是让算法尝试寻找数据可以如何被分组的依据。

看着没有标签的点,把彼此接近或模式相似的点先试着归成一组。

这里重要的是相似性(similarity)。聚类最终都会连到一个问题:什么才算相似?

它和监督学习有什么不同

在分类(classification)里,人们通常会问这样的问题。

  • 这封邮件是不是垃圾邮件?
  • 这个客户会不会流失?

这里已经有了正确答案标签。

而聚类问的是下面这样的问题。

  • 这些客户看起来会按什么使用模式分开?
  • 这些文档看起来像哪些主题团块?

也就是说,聚类不是去预测标签的问题,而是在没有标签时提出分组的问题

问题 监督学习 聚类
有没有正确答案标签 没有
目标 预测正确答案 探索结构
输出 class, score, value cluster label, grouping
核心问题 要预测什么 什么会因相似而被分在一起

这里的 cluster label 并不是人预先赋予的意义,而只是算法为了方便加上的编号。

把这个差别再用图看一次,会像下面这样。

flowchart TD
  A["输入数据"]
  B{"给定标签了吗?"}
  C["学习映射关系"]
  D["预测已知目标"]
  E["寻找相似性结构"]
  F["提出聚类分组"]

  A --> B
  B -->|是| C --> D
  B -->|否| E --> F

这张图一眼就展示出监督学习和聚类在起点上是从哪里分开的。监督学习带着正确答案标签去学习映射,而聚类是在没有标签的情况下先找相似结构,之后还需要人重新解释那些分组。

为什么“相似”这个词这么重要

聚类最终都会建立在点之间的距离(distance)、密度(density)、连接(connectivity)、中心(center)等概念上。

这意味着,聚类不是凭空出现的,而是会随着你用什么标准来定义相似而改变。

例如,想一想客户数据。

  • 每月访问次数
  • 平均购买金额
  • 距离最近一次登录过去了多少天

如果用这三个特征(feature)来看客户,那么“相似”就可能意味着:在这三个坐标轴上的位置比较接近。

但如果是文本文件,相似就可能变成词分布上的接近,或嵌入(embedding)空间里的接近。

所以,在聚类里,“相似”不是一种感性表达,而是在特征空间(feature space)里如何定义关系

如果把它压缩成数据流,会像下面这样。

flowchart TD
  A["原始记录"]
  B["选择特征"]
  C["定义相似性<br/>距离 / 密度 / 连通性"]
  D["把相近或相关的点分组"]
  E["检查这种分组是否有意义"]

  A --> B --> C --> D --> E

这张图说明,聚类不是一个从原始数据里直接掏出答案的过程。只有在选定特征、决定好相似规则之后,聚类才会形成,所以结果总会受到表达方式和相似度定义的影响。

上面这张图的最后一步很重要。聚类不是答案生成器,而是结构提议器。结果出来之后,人还得继续解释这些分组到底意味着什么。

为什么聚类和类别(class)不一样

最常见的误解是这样的。

既然聚类已经出来了,那这不就是真正的类别吗?

但聚类和类别并不一样。

  • 类别(class):由人提前定义好意义的正确答案范围
  • 聚类(cluster):算法在数据里找到的分组

例如,就算把客户分成了 3 个聚类,也不代表它立刻就是像 VIP / 普通 / 流失风险 这样的正式类别。这种解释,是人在后面再加上去的。

所以,聚类并不会自动确定意义,它更接近于提出解释候选

把这一点画出来,就是下面这样。

flowchart TD
  A["算法输出<br/>簇 0 / 1 / 2"]
  B["检查代表性案例"]
  C["之后再附加可能的含义<br/>VIP? 普通? 风险?"]

  A --> B --> C

这张图把聚类编号和业务意义分开来读。像 cluster 0cluster 1 这样的输出,还只是临时编号;把它们解释成 VIP 客户轻度用户风险群,是在后面的人类审查阶段才发生的。

k-means 展示了什么直觉

scikit-learn 用户指南把 K-means 描述为:把样本切分成 n groups of equal variance,并试图减少 inertia,也就是簇内平方和。同时它也说明了:算法会根据中心点(centroid)把样本分配到最近的聚类里。

k-means 是通过放置几个中心点,再把每个点贴到最近的中心上,来形成聚类的。

也就是说,k-means 是一种以中心(center)为基础的直觉。

所以,它通常适合下面这些情况。

  • 可以提前决定聚类数量时
  • 聚类看起来比较圆,大小也相对均匀时
  • 想快速先看一个基本结构时

不过,正如 scikit-learn 文档也指出的那样,它对细长或复杂形状的聚类不一定适合。

DBSCAN 展示了什么直觉

scikit-learn 的 clustering 概览表把 DBSCAN 描述成适用于 non-flat geometryuneven cluster sizesoutlier removal 的方法。

DBSCAN 不是先决定中心点,而是看点到底聚得有多密。

也就是说,DBSCAN 是一种以密度(density)为基础的直觉。

所以,它会在下面这些情形里出现。

  • 聚类形状不是圆的
  • 想把部分点单独留作噪声(noise)或异常值(outlier)
  • 聚类大小可能很不均匀

反过来,如果不同区域的密度差异太大,或者参数不合适,聚类也可能很难被抓出来。

把 k-means 和 DBSCAN 并排来看

问题 k-means DBSCAN
聚类直觉 中心(center) 密度(density)
是否要提前决定聚类数 通常要 通常不要
异常值处理 较弱 相对更容易显出来
聚类形状 更适合圆形且均匀的形状 可以应对更复杂的形状

这个比较不是在说“谁更好”,而是在区分“你期待看到什么结构”。

如果只比较直觉,往往更容易把它们理解成:它们从完全不同的问题出发。

flowchart LR
  A["选择 k 个中心"]
  B["把点分配给最近的中心"]
  C["更新中心位置"]

  A --> B --> C
flowchart LR
  A["找到稠密邻域"]
  B["扩展相连的稠密区域"]
  C["把稀疏点保留为噪声"]

  A --> B --> C

前一张图展示的是:k-means 是先放中心再贴点;后一张图展示的是:DBSCAN 是扩展稠密相连的区域,并把稀疏点留在外面。把比较拆开,而不是塞进一张图里,更容易让读者意识到:中心型密度型其实是在回答不同的聚类问题。

案例与示例

案例 1. 当你想按使用模式,而不是按销售额排行,重新分组购物商城客户时

当电商团队查看客户时,很容易只按这个月消费额大不大这样的单一标准来分。但现实里,常常会混杂着这些模式:访问频繁但每次花得少的客户、访问少但单次消费很高的客户、最近不再访问的客户。只看消费额的话,它们很可能会被硬塞进同一类。聚类会把访问次数、购买金额、最近活跃度这些特征放在一起看,提出相似客户群。这样,团队就能发现那些在简单销售排行表里看不出来的以行为模式为中心的客户组,并在此基础上继续做解释和营销策略审查。

flowchart TD
  A["客户数据表"]
  B["只按购买额排序"]
  C["混合行为会被隐藏起来"]
  D["按访问、消费、最近性做聚类"]
  E["高频小额购买者"]
  F["低频高额购买者"]
  G["最近不活跃的客户"]
  H["业务含义稍后再复核"]

  A --> B --> C
  A --> D
  D --> E
  D --> F
  D --> G
  E --> H
  F --> H
  G --> H

把这个案例压缩成 review 备忘,可以写成下面这样。

候选分组 代表样本 现在不要立刻下的结论 下一步要确认的问题
访问频繁但小额购买很多的客户组 访问次数高、购买额中低的样本客户 不要马上叫它 VIP 或忠诚客户 它是否也和复访率、长期价值、流失率连得上?
访问不频繁但一次消费很高的客户组 访问次数低、购买额高的样本客户 不要立刻送进高价客户专门策略 这是季节性购买,还是重复模式?

案例 2. 当文章分类标签还不够,想先做编辑审查用的主题分组时

假设一个新闻服务团队要整理新进来的文章,但还没有足够多的 政治经济体育 这类标签。如果人工只是扫一眼标题后按顺序分类,那么像技术报道和产业报道这样边界模糊的文档,就会反复摇摆。这个时候,聚类能根据文章嵌入或词分布,提供一个先把相似文章聚成组的起点。

这里重要的是:聚类并不会直接代替正确类别。举例来说,如果某个分组把 半导体投资AI 芯片数据中心 的文章聚在一起,那么这个组既可能被读成 技术,也可能被读成 产业。也就是说,聚类是向编辑团队提出应该先放在一起审查的文档组,而最终的主题名和运营分类体系,仍然得由人重新贴上去。

flowchart TD
  A["进入系统的文章"]
  B["只看标题做人工粗看"]
  C["边界主题会变得不一致"]
  D["按文本相似度聚类"]
  E["检查聚好的文章集合"]
  F["之后再附加编辑标签"]

  A --> B --> C
  A --> D --> E --> F

把这个案例压缩成工作备忘,可以写成下面这样。

候选分组 代表样本 现在不要立刻下的结论 下一步要确认的问题
半导体投资和 AI 基础设施文章聚在一起的组 数据中心投资文章、AI 芯片需求文章 不要立刻把它确定为 技术 分组 产业文章和技术文章的边界到底从哪里分开?
球员比赛结果和俱乐部运营文章聚在一起的组 比赛总结文章、教练更换文章 不要马上只固定成一个 体育 比赛结果文章和运营文章是否还需要再细分?

练习与示例

这个例子只用两个坐标,做一个很小的练习:看看点是不是像两团一样。这里不会跑一次就停,而是连同“只改一个点时,聚类假设会怎样动摇”一起看。

  • 问题场景:看看没有标签的点是否自然像几个团块
  • 输入(input):二维坐标
  • 期望输出(output):对邻近点分组的直觉
  • 要确认的概念:
  • 聚类从位置关系开始
  • 即使没有标签,也会出现分组感

先看下面这些坐标,自己先写一下这些点看起来像几团。

点 ID 坐标
A (1.0, 1.2)
B (1.1, 0.9)
C (0.8, 1.0)
D (5.0, 5.1)
E (5.2, 4.9)
F (4.8, 5.0)

在这个场景里,读者通常会先作出下面这样的判断。

最先出现的分组假设 为什么容易这样读
A、B、C 看起来像一团 它们在左下方彼此靠得很近
D、E、F 看起来像另一团 它们在右上方彼此靠得很近
整体上像两团 两组点之间的间隔相对较大

这里首先要读到的是下面三点。

  1. 即使没有标签,只看点的位置,也会出现团块感。
  2. 聚类最终总会连到一个问题:在某个空间里,哪些点更靠近并聚在一起。
  3. 真正的算法要做的,是把这种分组标准进一步一般化,再自动把它找出来。

改一个值看看:如果中间插进来一个点,该怎么读?

这次我们把中间点 G 加进去,再读一遍。

点 ID 坐标
A (1.0, 1.2)
B (1.1, 0.9)
C (0.8, 1.0)
G (3.0, 3.1)
D (5.0, 5.1)
E (5.2, 4.9)
F (4.8, 5.0)

先自己回答下面几个问题。

  • 现在还能立刻说它是两团吗?
  • G 更像左边那团、右边那团,还是边界案例?
  • 现在更需要的是正确聚类编号,还是记下模糊点到底出现在什么地方

然后再和下面的解释比较。

场景里改变了什么 最先该重新看的是什么 为什么重要
中间多了一个边界点 G 两团之间的边界是否还清楚 只要一个边界案例,就可能动摇结构假设
原本简单的“左边 3 个、右边 3 个”不再成立 G 是否会被自动放到某一边 这会暴露出聚类不是答案,而是解释候选
点数从 3 比 3 变成了 3 比 4 现在是不是边界解释比团块大小更重要 在实务里,应该先记录模糊点,再记录聚类编号

只多了一个点,两团这个解释就没有那么清楚了。这里读者应该体会到的是:聚类不是给出答案的装置,而是看在某种相似标准下,结构假设会怎样变化的装置。同样的数据里,只要对边界附近的点读法不同,分组解释就会变,所以在实务里,应该先记下哪里是模糊的,而不是先记聚类编号。

把 k-means 和 DBSCAN 放在同一组点上比较

这次把 k-meansDBSCAN 并排用在同一份玩具数据上,直接看看中心型密度型视角会产生什么不同输出。

  • 问题场景:当数据里有两团稠密点和一个远离的点时,看看 k-meansDBSCAN 会怎样不同地读取这个点
  • 输入(input):两团稠密点 + 一个远离点
  • 期望输出(output):确认 k-means 仍会把远离点塞进某个聚类,而 DBSCAN 可以把它保留成噪声
  • 要确认的概念:
  • k-means 会把每个点都分配进预先决定的聚类数里
  • DBSCAN 可以把低密度点留在聚类外面
# 这个例子比较 k-means 和 DBSCAN 在同一组点上如何不同地处理像离群点的 G。
import numpy as np
from sklearn.cluster import DBSCAN, KMeans

names = ["A", "B", "C", "D", "E", "F", "G"]
X = np.array([
    [1.0, 1.1],
    [1.2, 0.9],
    [0.9, 1.0],
    [5.0, 5.1],
    [5.2, 4.8],
    [4.9, 5.0],
    [8.5, 1.0],
])

kmeans = KMeans(n_clusters=2, random_state=0, n_init=10)
km_labels = kmeans.fit_predict(X)

left_km = int(np.argmin(kmeans.cluster_centers_[:, 0]))
right_km = int(np.argmax(kmeans.cluster_centers_[:, 0]))
km_groups = {
    "left-centered group": [name for name, label in zip(names, km_labels) if label == left_km],
    "right-centered group": [name for name, label in zip(names, km_labels) if label == right_km],
}

dbscan = DBSCAN(eps=0.6, min_samples=2)
db_labels = dbscan.fit_predict(X)

dense_labels = sorted(label for label in set(db_labels) if label != -1)
db_groups = {}
for order, label in enumerate(dense_labels, start=1):
    db_groups[f"dense group {order}"] = [
        name for name, current in zip(names, db_labels) if current == label
    ]
db_groups["noise"] = [name for name, current in zip(names, db_labels) if current == -1]

print("k-means :", km_groups)
print("DBSCAN  :", db_groups)

执行结果示例如下。

k-means : {'left-centered group': ['A', 'B', 'C'], 'right-centered group': ['D', 'E', 'F', 'G']}
DBSCAN  : {'dense group 1': ['A', 'B', 'C'], 'dense group 2': ['D', 'E', 'F'], 'noise': ['G']}

从这个结果里,首先应该读到下面这些点。

比较点 k-means 里看到的 DBSCAN 里看到的 为什么重要
聚类数处理 它把所有点都放进了预先决定的两个聚类里 它形成了两团稠密分组,并把 G 留成噪声 这里直接体现了是否强行给定聚类数是否允许点留在聚类外的差别
远离点 G 它被分进了右侧那团 它没有被放进任何聚类 类似异常点的处理方式差别会直接显现出来
解释方式 它更像是在说每个点都必须属于某个聚类 它更像是在说只有稠密区域才算聚类,其余点可以留在外面 这样能在输出层面直接确认中心型与密度型直觉的差别

也就是说,就算输入相同,k-means 更像是在按预先决定好的聚类数,用中心去分配所有点来读问题,而 DBSCAN 更像是在按只把稠密区域当作聚类,稀疏点可以留在外面来读问题。所以,当你希望把异常点一样的点单独留下时,DBSCAN 更自然;当你只是想快速先看到几个分组的基准线时,k-means 更自然。

这个例子还要一起读什么

Part 4 要留下的判断标准不是罗列模型名字,而是说明“问题该如何定义、结果该如何阅读”。这个练习把四件事一起确认了:没有正确答案标签的结构探索问题这一问题定义、中间多一个点就会动摇聚类假设这一评估视角、k-means 和 DBSCAN 会把同一个点读成不同结果这一比较视角,以及应该把分组假设和代表样本一起记录这一应用原则。也就是说,如果你跑了一个聚类例子却依然感觉不到这套判断标准,通常不是因为跑得不够多,而是因为缺少了解释结果差异并把它递送到下一个问题的句子。

共通记录语言 这个例子里应该立刻留下的内容
看见的结构 原本看起来像两团的点集合,只因为一个边界点就可能被动摇
解释边界 聚类不是自动正确答案,而是建在相似标准上的解释候选
下一问题 如果改变距离规则、缩放方式、异常值处理,分组会怎么变化?
需要一起看的东西 这一节先读的问题 立刻接到哪里
分组假设 在什么特征标准下看见了什么分组? P4-17.2 聚类解释的注意点
代表样本 每个分组要怎样在样本层面上解释? 聚类摘要和领域解释
下一验证问题 要用什么重新确认这个结构是否真的有意义? 缩放、参数、后续结果比较

检查清单

  • 你是否已经明确:这里需要的不是预测正确答案,而是探索结构?
  • 你能把聚类解释成在无标签数据里寻找结构的无监督学习吗?
  • 你能说明聚类不是正确类别,而是解释候选吗?
  • 你是否理解相似性是建立在距离、密度、连接等标准之上的?
  • 当比较 k-means 和 DBSCAN 时,你能先说出“中心型”和“密度型”的直觉差别吗?
  • 你能把“用了什么相似标准”和“用了哪种聚类直觉(k-means/DBSCAN)”分开来说吗?
  • 你是否知道:聚类结果是解释的起点,而不是会自动固定下来的真理?

出处与参考资料