
聚类 (Clustering),就是**"物以类聚"**的算法版。
如果说监督学习(如分类)是**"有老师教"**:老师拿着苹果和梨,告诉你"这是苹果,这是梨",然后让你去认。
那么无监督学习(聚类)就是**"没老师教,全靠悟"**:给你一堆水果(苹果、梨、香蕉混在一起),没有任何标签,让你凭感觉把它们分成几堆。
你大概率会这样做:
- 圆圆的红色的放一堆(苹果)。
- 圆圆的黄色的放一堆(梨)。
- 长长的黄色的放一堆(香蕉)。
这就叫聚类。
聚类的核心逻辑:什么叫"一类"?
机器没有人眼,它怎么知道哪些东西应该归为一类?
核心标准通常有两个:
- 类内距离小: 同一个小圈子里的人,必须长得越像越好(紧密团结)。
- 类间距离大: 不同的圈子之间,必须长得越不像越好(井水不犯河水)。
常见的聚类"流派"
1. K-Means (K均值) ------ "插旗占地盘"
这是最简单、最常用的流派。
- 玩法:
- 你想分 3 类(K=3K=3),就先随机在地上插 3 面旗子(质心)。
- 所有人(数据点)看哪面旗子离自己近,就站到哪面旗子下面去。
- 旗手看到大家站好了,就走到自己这一队人的中心位置重新插旗。
- 重复第 2、3 步,直到旗子不再移动为止。
- 特点: 简单快,但必须预先知道分几类(K值),而且喜欢圆圆的形状,怕离群点捣乱。
2. Hierarchical Clustering (层次聚类) ------ "家族族谱"
- 玩法:
- 最开始,每个人都是一个独立的部落。
- 找到离得最近的两个人,合并成一个小部落。
- 再找离得最近的两个小部落,合并成一个大部落......
- 一直合,直到所有人都合成了一个大帝国,或者到了你满意的层级。
- 特点: 不需要预设 K 值,能画出一棵漂亮的树状图(Dendrogram),清楚地看到谁和谁是亲戚。
3. DBSCAN (密度聚类) ------ "抱团取暖"
- 玩法:
- 随便找个人,看看他身边 1米范围内有多少人。
- 如果人很多(比如超过 5 个),那这里就是个热闹的核心区,大家算一伙的。
- 如果人很少(比如就他自己),那他就是个孤独的流浪汉(噪声点),不带他玩。
- 通过人拉人,把所有热闹的地方连成一片。
- 特点: 最智能 。能发现各种奇形怪状的圈子(比如月牙形、环形),而且能自动把噪声点(异常值)踢出去。
一句话总结
- K-Means: 还要我告诉你分几类?(适合预知 K 值,数据规整)。
- 层次聚类: 给我画个家谱看看。(适合小数据,分析层级关系)。
- DBSCAN: 帮我把那帮混混和流浪汉找出来。(适合不规则形状,抗噪)。
简单理解
K-Means (K均值) 是聚类算法里的"扛把子",因为它极其简单、直观,且效果通常不错。
它的核心逻辑就是一场**"抢人游戏"**。
我们可以把它想象成:在操场上给一堆散乱的学生分班。
第一步:内定班长(初始化)
假设你要把这群学生分成 3 个班 (即 K=3K=3)。
最开始,你也不知道谁适合当班长,于是你随机指定了 3 个学生(或者操场上的 3 个位置)作为临时的"中心点"(质心 Centroid)。
- 红旗手(红班中心)
- 蓝旗手(蓝班中心)
- 绿旗手(绿班中心)
第二步:学生站队(分配)
这就是 K-Means 里最核心的计算:算距离。
所有其他的学生,看一看这三面旗子:
- "我离红旗手最近,那我就站到红队去。"
- "我离绿旗手最近,我就站到绿队去。"
这样,所有人就有了临时的归属。
第三步:班长换届(更新)
现在的队伍虽然分好了,但刚才的班长是随机瞎指派的,不能服众(比如红班的班长可能站在队伍的最边缘,根本不是中心)。
于是,每个队伍内部开会:
- 红队所有人算出自己的平均坐标 (比如平均身高、平均体重),找到这个队伍真正的物理中心。
- 那个位置,就是新一代的红班长(新质心)。
第四步:循环往复
- 有了新班长(位置变了),原来的学生发现:"哎?我现在好像离绿班长更近了。"
- 于是大家重新站队(重复第二步)。
- 再重新选班长(重复第三步)。
这个过程一直重复,直到有一天:
- 新选出来的班长,位置和老班长一模一样(质心不再移动)。
- 或者大家都懒得动了(达到最大迭代次数)。
游戏结束,聚类完成。
K-Means 的"死穴"
虽然它好用,但它有两个著名的弱点:
-
K 值难定:
- 你必须提前告诉它分几类。如果你设 K=3K=3,它就死活给你分出 3 堆;哪怕这数据明明是 2 堆或者 4 堆。
- 人话: 它是个听话的执行者,不是决策者。
-
怕"刺头"(对异常值敏感):
- K-Means 也是用平均值来选中心的。
- 如果红队里混进了一个身高 3 米的巨人(异常值),他一个人就能把红队的平均身高拉高一大截,导致红队的中心偏离了大多数正常人。
总结
K-Means 就是**"选中心 →→ 归队 →→ 算新中心 →→ 归队..."** 的无限循环,直到找到最稳定的那个平衡点。
应用场景
K-Means 因为简单、快速,是工业界做数据探索和初步分组的首选算法。只要你的目的是**"把东西分堆,方便管理"**,基本都可以用它。
以下是 K-Means 最核心的几大应用场景:
1. 客户分群 (Customer Segmentation) ------ 绝对主场
这是商业分析中最经典的应用。
- 场景: 公司有 100 万用户,想做精细化运营。不可能给每个人都制定一套策略,也不能所有人都用同一套。
- 做法: 收集用户的特征(消费金额、消费频次、最近一次购买时间、年龄)。
- K-Means 聚类: 设定 K=4K=4,算法自动把用户分成 4 类:
- 群体 A(高富帅): 买得多、买得贵 →→ 策略: 只有 VIP 客服服务,推销高端新品。
- 群体 B(薅羊毛党): 买得少,只买打折的 →→ 策略: 发优惠券刺激。
- 群体 C(沉睡户): 以前买过,最近没动静 →→ 策略: 短信唤醒。
- 群体 D(潜力股): 刚注册,正在观望 →→ 策略: 新人礼包。
2. 图像压缩 / 颜色量化
- 场景: 一张真彩色图片有 1600 万种颜色,文件太大。我想把它压缩成只有 256 种颜色的 GIF 图,但又不想让它看起来失真太严重。
- 做法: 把图片里的每一个像素点(R, G, B)看作一个数据点。
- K-Means 聚类: 设定 K=256K=256。算法会在 1600 万种颜色里,找出最具代表性的 256 个"颜色中心"。
- 替换: 把原图中所有的像素,都替换成离它最近的那种"中心色"。这样只需存 256 个色号,体积瞬间缩小。
3. 文档归类 / 话题发现
- 场景: 新闻网站每天抓取几万篇文章,想自动把它们分成"体育"、"娱乐"、"政治"等版块,但又没有人工打标签。
- 做法: 把每篇文章变成一个向量(TF-IDF 或 Word2Vec)。
- K-Means 聚类: 设定 K=10K=10。算法会自动把内容相似的文章聚在一起。
- 人工命名: 你看一眼第一堆,全是"球"、"射门",你就把这堆命名为"体育";看一眼第二堆,全是"股指"、"K线",命名为"财经"。
4. 辅助打标 (Semi-supervised Learning)
- 场景: 你要训练一个猫狗识别模型,手里有 10 万张图,但都没打标签。请人一张张标太贵了。
- 做法:
- 先用 K-Means 把这 10 万张图聚成 100 类。
- 你只需要从每一类里抽出一张图看看,发现第 1 类全是猫,第 2 类全是狗...
- 你只要标 100 次,就能把这 10 万张图的标签大概齐都打上了。
5. 异常检测 (Anomaly Detection)
虽然 DBSCAN 更擅长这个,但 K-Means 也能做简单的。
- 场景: 监控服务器流量。
- 做法: 聚类出几个"正常流量模式"。
- 检测: 如果来了一个新数据,发现它离任何一个聚类中心的距离都特别远,那它很可能就是异常攻击。
K-Means 什么时候不好用?
- 数据形状很奇怪时:
- K-Means 喜欢球形 的簇。如果你的数据是长条形的、环形的(比如甜甜圈形状),K-Means 会切得乱七八糟。这时候要用 DBSCAN。
- 噪声很多时:
- K-Means 必须要给每个点都找个归宿,哪怕它是离群很远的垃圾数据,也会被强行拉进来,导致中心偏移。
- 不同类别的密度差异很大时:
- 比如一堆数据很密集,另一堆很稀疏,K-Means 这种基于距离的算法处理不好。
总结
K-Means 是数据分析师手中的"瑞士军刀"。
当你面对一堆陌生数据,完全没头绪时,先用 K-Means 跑一下,看看能分出几个什么堆,往往能给你带来最初的洞察(Insight)。
数学推导
K-Means 的数学推导不像 SVM 那样充满高等数学技巧,它更像是一个直观的迭代优化过程。
它的核心数学目标非常简单:让所有点到它所属的中心点的距离平方和最小。
我们分三步来推导:
- 定义目标函数(损失函数): 我们想优化什么?
- E步(Expectation): 固定中心,分配数据。
- M步(Maximization): 固定数据,更新中心。
(注:K-Means 其实是 EM 算法的一种特殊、简化形式)
第一步:定义目标函数 (Loss Function)
假设我们有 NN 个数据点 {x1,x2,...,xN}{x1,x2,...,xN},想要把它们分成 KK 个簇。
每个簇都有一个中心点(质心):{μ1,μ2,...,μK}{μ1,μ2,...,μK}。
我们要定义一个指标 JJ,用来衡量**"分得好不好"**。
显然,如果大家都紧紧地围在自己班长身边,那就分得好。
所以,目标函数(也叫畸变函数 Distortion Function)定义为:
J=∑n=1N∑k=1Krnk∣∣xn−μk∣∣2J=∑n=1N∑k=1Krnk∣∣xn−μk∣∣2
- ∣∣xn−μk∣∣2∣∣xn−μk∣∣2:第 nn 个点到第 kk 个中心的欧氏距离平方。
- rnkrnk:一个指示变量(0 或 1)。
- 如果点 xnxn 属于第 kk 类,则 rnk=1rnk=1。
- 否则 rnk=0rnk=0。
我们的任务: 找到最优的 rnkrnk(分配方案)和 μkμk(中心位置),让 JJ 最小。
第二步:E步 ------ 固定中心,优化分配 (rnkrnk)
假设现在的中心点 μkμk 已经固定不动了(比如刚开始随机选好的)。
我们要怎么选 rnkrnk 才能让 JJ 最小?
看公式 J=∑rnk∣∣xn−μk∣∣2J=∑rnk∣∣xn−μk∣∣2。
对于每一个数据点 xnxn,为了让它的贡献最小,我们只需要让它归属到离它最近的那个中心去。
数学表达:
rnk={1,if k=argminj∣∣xn−μj∣∣20,otherwisernk={1,0,if k=argminj∣∣xn−μj∣∣2otherwise
(人话:谁离我近,我就跟谁混。)
第三步:M步 ------ 固定分配,优化中心 (μkμk)
现在大家都站好队了(rnkrnk 固定了),我们要重新选班长(μkμk),让 JJ 最小。
目标函数 JJ 对 μkμk 求偏导,并令导数为 0:
J(μk)=∑n=1Nrnk(xn−μk)2J(μk)=∑n=1Nrnk(xn−μk)2
(这里为了简便用标量形式示意,向量求导逻辑一样)
求导:
∂J∂μk=∑n=1Nrnk⋅2(xn−μk)⋅(−1)=0∂μk∂J=∑n=1Nrnk⋅2(xn−μk)⋅(−1)=0
展开方程:
∑n=1Nrnkxn−∑n=1Nrnkμk=0∑n=1Nrnkxn−∑n=1Nrnkμk=0
移项:
∑n=1Nrnkμk=∑n=1Nrnkxn∑n=1Nrnkμk=∑n=1Nrnkxn
因为 μkμk 是常数,可以提出来:
μk∑n=1Nrnk=∑n=1Nrnkxnμk∑n=1Nrnk=∑n=1Nrnkxn
最终得到 μkμk 的更新公式:
μk=∑n=1Nrnkxn∑n=1Nrnkμk=∑n=1Nrnk∑n=1Nrnkxn
这个公式的物理含义:
- 分子: 归属到第 kk 类的所有点的坐标之和。
- 分母: 归属到第 kk 类的点的总个数。
- 结果: 算术平均值。
(人话:新的中心,就是这一堆点的平均位置。)
总结推导逻辑
K-Means 算法之所以能收敛(J 会越来越小),是因为:
- 在 E 步,我们给每个点找最近的中心,距离肯定变小(或不变),所以 JJ 下降。
- 在 M 步,我们将中心移到平均位置,这是平方误差最小的位置,所以 JJ 继续下降。
像下楼梯一样,一步步走,直到 JJ 降不动了(收敛),算法结束。
代码实例
这段代码将包含两个部分:
- 手写版 (From Scratch): 我们根据上面的推导(E步分配,M步更新),用 Numpy 手写一个 K-Means。
- 可视化动画: 我将用 Matplotlib 动态展示每一次迭代时,质心是如何移动的 ,以及点的颜色是如何变化的。这将非常直观地帮你理解算法流程。
Python 代码实现
请确保安装了 numpy, matplotlib, sklearn。
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from copy import deepcopy
# ==========================================
# 0. 准备数据
# ==========================================
# 生成 3 堆数据 (Blobs)
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)
# ==========================================
# 1. 手写 K-Means 核心类
# ==========================================
class MyKMeans:
def __init__(self, k=3, max_iters=100, tol=0.0001):
self.k = k
self.max_iters = max_iters
self.tol = tol # 收敛阈值
self.centroids = None
self.history = [] # 用于存储每次迭代的质心位置,方便画图
def _euclidean_distance(self, a, b):
# 计算两点间距离: sqrt(sum((a-b)^2))
return np.linalg.norm(a - b)
def fit(self, X):
m, n = X.shape
# --- 初始化: 随机选 K 个点作为初始质心 ---
# 更好的方法是 K-Means++,这里为了简单直接随机选
idx = np.random.choice(m, self.k, replace=False)
self.centroids = X[idx]
self.history.append(self.centroids) # 记录初始位置
for i in range(self.max_iters):
# --- E步: 分配 (Expectation) ---
# 这里的 labels 对应数学推导中的 r_nk
labels = []
for point in X:
# 算该点到所有质心的距离
distances = [self._euclidean_distance(point, centroid) for centroid in self.centroids]
# 选最近的那个
labels.append(np.argmin(distances))
labels = np.array(labels)
# --- M步: 更新 (Maximization) ---
new_centroids = np.zeros((self.k, n))
for j in range(self.k):
# 找到属于第 j 类所有的点
points_in_cluster = X[labels == j]
# 计算平均值 (Mean)
if len(points_in_cluster) > 0:
new_centroids[j] = np.mean(points_in_cluster, axis=0)
else:
# 防止某一类为空的情况 (虽少见但可能发生)
new_centroids[j] = self.centroids[j]
# --- 检查是否收敛 ---
# 如果质心移动距离极小,就停止
shift = np.linalg.norm(self.centroids - new_centroids)
self.centroids = new_centroids
self.history.append(self.centroids) # 记录轨迹
if shift < self.tol:
print(f"K-Means 在第 {i+1} 轮收敛。")
break
self.labels_ = labels
return self
# ==========================================
# 2. 运行与可视化
# ==========================================
k = 3
kmeans = MyKMeans(k=k)
kmeans.fit(X)
# --- 绘图展示 ---
history = np.array(kmeans.history)
labels = kmeans.labels_
plt.figure(figsize=(10, 6))
# 1. 画数据点 (根据最终分类染色)
colors = ['r', 'g', 'b']
for i in range(k):
points = X[labels == i]
plt.scatter(points[:, 0], points[:, 1], s=30, c=colors[i], alpha=0.6, label=f'Cluster {i+1}')
# 2. 画质心移动轨迹
# history 形状: (iters, k, features)
for i in range(k):
# 取出第 i 个质心的所有历史位置
traj = history[:, i, :]
plt.plot(traj[:, 0], traj[:, 1], 'k--', linewidth=1, alpha=0.8) # 轨迹线
plt.scatter(traj[:-1, 0], traj[:-1, 1], marker='x', s=50, c='black') # 中间过程点
plt.scatter(traj[-1, 0], traj[-1, 1], marker='*', s=300, c='yellow', edgecolors='black', label=f'Final Centroid {i+1}') # 终点
plt.title(f"Visualizing K-Means Convergence (K={k})")
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

代码解析与看点
1. 核心逻辑 (MyKMeans 类)
- 初始化:
np.random.choice随机选了 3 个点。 - E步 (分配): 双重循环(实际中可用矩阵运算加速),算出每个点离哪个质心近,
labels就是那个 rnkrnk。 - M步 (更新):
np.mean(points_in_cluster, axis=0)这行代码直接对应了数学公式 μk=∑xnNμk=N∑xn。算术平均值就是新的中心。
2. 可视化图表
运行代码后,你会看到一张很直观的图:
- 彩色圆点: 最终被分好类的数据点(红、绿、蓝)。你会发现它们被聚成了清晰的 3 堆。
- 黑色虚线与黄星: 这是最精彩 的部分。
- 黑色叉叉 (x) 是质心刚开始随机落脚的地方,可能偏得很离谱。
- 虚线 记录了质心一步步"爬"向数据中心的路径。
- 黄色大星星 是质心最终停下的位置,它一定位于那一堆数据的正几何中心。
这完美展示了 K-Means "随机开局 →→ 迭代修正 →→ 稳定收敛" 的全过程。