从零开始学AI24——无监督学习之K均值

聚类 (Clustering),就是**"物以类聚"**的算法版。

如果说监督学习(如分类)是**"有老师教"**:老师拿着苹果和梨,告诉你"这是苹果,这是梨",然后让你去认。

那么无监督学习(聚类)就是**"没老师教,全靠悟"**:给你一堆水果(苹果、梨、香蕉混在一起),没有任何标签,让你凭感觉把它们分成几堆。

你大概率会这样做:

  • 圆圆的红色的放一堆(苹果)。
  • 圆圆的黄色的放一堆(梨)。
  • 长长的黄色的放一堆(香蕉)。

这就叫聚类。


聚类的核心逻辑:什么叫"一类"?

机器没有人眼,它怎么知道哪些东西应该归为一类?

核心标准通常有两个:

  1. 类内距离小: 同一个小圈子里的人,必须长得越像越好(紧密团结)。
  2. 类间距离大: 不同的圈子之间,必须长得越不像越好(井水不犯河水)。

常见的聚类"流派"

1. K-Means (K均值) ------ "插旗占地盘"

这是最简单、最常用的流派。

  • 玩法:
    1. 你想分 3 类(K=3K=3),就先随机在地上插 3 面旗子(质心)。
    2. 所有人(数据点)看哪面旗子离自己近,就站到哪面旗子下面去。
    3. 旗手看到大家站好了,就走到自己这一队人的中心位置重新插旗。
    4. 重复第 2、3 步,直到旗子不再移动为止。
  • 特点: 简单快,但必须预先知道分几类(K值),而且喜欢圆圆的形状,怕离群点捣乱。
2. Hierarchical Clustering (层次聚类) ------ "家族族谱"
  • 玩法:
    1. 最开始,每个人都是一个独立的部落。
    2. 找到离得最近的两个人,合并成一个小部落。
    3. 再找离得最近的两个小部落,合并成一个大部落......
    4. 一直合,直到所有人都合成了一个大帝国,或者到了你满意的层级。
  • 特点: 不需要预设 K 值,能画出一棵漂亮的树状图(Dendrogram),清楚地看到谁和谁是亲戚。
3. DBSCAN (密度聚类) ------ "抱团取暖"
  • 玩法:
    1. 随便找个人,看看他身边 1米范围内有多少人。
    2. 如果人很多(比如超过 5 个),那这里就是个热闹的核心区,大家算一伙的。
    3. 如果人很少(比如就他自己),那他就是个孤独的流浪汉(噪声点),不带他玩。
    4. 通过人拉人,把所有热闹的地方连成一片。
  • 特点: 最智能 。能发现各种奇形怪状的圈子(比如月牙形、环形),而且能自动把噪声点(异常值)踢出去。

一句话总结

  • K-Means: 还要我告诉你分几类?(适合预知 K 值,数据规整)。
  • 层次聚类: 给我画个家谱看看。(适合小数据,分析层级关系)。
  • DBSCAN: 帮我把那帮混混和流浪汉找出来。(适合不规则形状,抗噪)。

简单理解

K-Means (K均值) 是聚类算法里的"扛把子",因为它极其简单、直观,且效果通常不错。

它的核心逻辑就是一场**"抢人游戏"**。

我们可以把它想象成:在操场上给一堆散乱的学生分班。


第一步:内定班长(初始化)

假设你要把这群学生分成 3 个班 (即 K=3K=3)。

最开始,你也不知道谁适合当班长,于是你随机指定了 3 个学生(或者操场上的 3 个位置)作为临时的"中心点"(质心 Centroid)。

  • 红旗手(红班中心)
  • 蓝旗手(蓝班中心)
  • 绿旗手(绿班中心)

第二步:学生站队(分配)

这就是 K-Means 里最核心的计算:算距离。

所有其他的学生,看一看这三面旗子:

  • "我离红旗手最近,那我就站到红队去。"
  • "我离绿旗手最近,我就站到绿队去。"

这样,所有人就有了临时的归属。

第三步:班长换届(更新)

现在的队伍虽然分好了,但刚才的班长是随机瞎指派的,不能服众(比如红班的班长可能站在队伍的最边缘,根本不是中心)。

于是,每个队伍内部开会:

  • 红队所有人算出自己的平均坐标 (比如平均身高、平均体重),找到这个队伍真正的物理中心。
  • 那个位置,就是新一代的红班长(新质心)。

第四步:循环往复

  • 有了新班长(位置变了),原来的学生发现:"哎?我现在好像离绿班长更近了。"
  • 于是大家重新站队(重复第二步)。
  • 再重新选班长(重复第三步)。

这个过程一直重复,直到有一天:

  • 新选出来的班长,位置和老班长一模一样(质心不再移动)。
  • 或者大家都懒得动了(达到最大迭代次数)。

游戏结束,聚类完成。


K-Means 的"死穴"

虽然它好用,但它有两个著名的弱点:

  1. K 值难定:

    • 你必须提前告诉它分几类。如果你设 K=3K=3,它就死活给你分出 3 堆;哪怕这数据明明是 2 堆或者 4 堆。
    • 人话: 它是个听话的执行者,不是决策者。
  2. 怕"刺头"(对异常值敏感):

    • 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 万张图,但都没打标签。请人一张张标太贵了。
  • 做法:
    1. 先用 K-Means 把这 10 万张图聚成 100 类。
    2. 你只需要从每一类里抽出一张图看看,发现第 1 类全是猫,第 2 类全是狗...
    3. 你只要标 100 次,就能把这 10 万张图的标签大概齐都打上了。

5. 异常检测 (Anomaly Detection)

虽然 DBSCAN 更擅长这个,但 K-Means 也能做简单的。

  • 场景: 监控服务器流量。
  • 做法: 聚类出几个"正常流量模式"。
  • 检测: 如果来了一个新数据,发现它离任何一个聚类中心的距离都特别远,那它很可能就是异常攻击。

K-Means 什么时候不好用?

  1. 数据形状很奇怪时:
    • K-Means 喜欢球形 的簇。如果你的数据是长条形的、环形的(比如甜甜圈形状),K-Means 会切得乱七八糟。这时候要用 DBSCAN。
  2. 噪声很多时:
    • K-Means 必须要给每个点都找个归宿,哪怕它是离群很远的垃圾数据,也会被强行拉进来,导致中心偏移。
  3. 不同类别的密度差异很大时:
    • 比如一堆数据很密集,另一堆很稀疏,K-Means 这种基于距离的算法处理不好。

总结

K-Means 是数据分析师手中的"瑞士军刀"。

当你面对一堆陌生数据,完全没头绪时,先用 K-Means 跑一下,看看能分出几个什么堆,往往能给你带来最初的洞察(Insight)。

数学推导

K-Means 的数学推导不像 SVM 那样充满高等数学技巧,它更像是一个直观的迭代优化过程。

它的核心数学目标非常简单:让所有点到它所属的中心点的距离平方和最小。

我们分三步来推导:

  1. 定义目标函数(损失函数): 我们想优化什么?
  2. E步(Expectation): 固定中心,分配数据。
  3. 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=1K​rnk​∣∣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=arg⁡min⁡j∣∣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=1N​rnk​(xn​−μk​)2

(这里为了简便用标量形式示意,向量求导逻辑一样)

求导:

∂J∂μk=∑n=1Nrnk⋅2(xn−μk)⋅(−1)=0∂μk​∂J​=∑n=1N​rnk​⋅2(xn​−μk​)⋅(−1)=0

展开方程:

∑n=1Nrnkxn−∑n=1Nrnkμk=0∑n=1N​rnk​xn​−∑n=1N​rnk​μk​=0

移项:

∑n=1Nrnkμk=∑n=1Nrnkxn∑n=1N​rnk​μk​=∑n=1N​rnk​xn​

因为 μkμk​ 是常数,可以提出来:

μk∑n=1Nrnk=∑n=1Nrnkxnμk​∑n=1N​rnk​=∑n=1N​rnk​xn​

最终得到 μkμk​ 的更新公式:

μk=∑n=1Nrnkxn∑n=1Nrnkμk​=∑n=1N​rnk​∑n=1N​rnk​xn​​

这个公式的物理含义:

  • 分子: 归属到第 kk 类的所有点的坐标之和。
  • 分母: 归属到第 kk 类的点的总个数。
  • 结果: 算术平均值。

(人话:新的中心,就是这一堆点的平均位置。)


总结推导逻辑

K-Means 算法之所以能收敛(J 会越来越小),是因为:

  1. 在 E 步,我们给每个点找最近的中心,距离肯定变小(或不变),所以 JJ 下降。
  2. 在 M 步,我们将中心移到平均位置,这是平方误差最小的位置,所以 JJ 继续下降。

像下楼梯一样,一步步走,直到 JJ 降不动了(收敛),算法结束。

代码实例

这段代码将包含两个部分:

  1. 手写版 (From Scratch): 我们根据上面的推导(E步分配,M步更新),用 Numpy 手写一个 K-Means。
  2. 可视化动画: 我将用 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 "随机开局 →→ 迭代修正 →→ 稳定收敛" 的全过程。

相关推荐
叼烟扛炮22 分钟前
[LangChain] 学习笔记(05):提示词模板(Prompt)与少样本注入(Few-Shot)实战
linux·人工智能·笔记·学习·langchain·prompt
云和数据.ChenGuang11 小时前
LangGraph 异步 Node 的设计与写法实践
开发语言·人工智能·python·机器学习·php
天诰12 小时前
SA-K-11 检索会消失吗与四个未解问题
java·服务器·人工智能·python·机器学习·ai编程
龙腾AI白云12 小时前
仿真到现实(Sim-to-Real)迁移:跨越仿真与现实鸿沟
数据库·机器学习·plotly·知识图谱·tornado
La Pulga13 小时前
【FreeRTOS源码学习笔记】任务延时列表
笔记·mcu·学习·嵌入式·freertos·实时操作系统
楚山之石14 小时前
【JCO】mpMRI 先行替代 TURBT 初始分期:BladderPath 随机试验让 MIBC 患者提前 45 天得到正确治疗
人工智能·学习·mri·医学影像
武天旭15 小时前
AI安全--AI大模型的对抗样本攻击与防御解析
人工智能·深度学习·安全·机器学习
Peter-OK18 小时前
PostgreSQL学习笔记(入门-进阶-精通说明)
笔记·学习·postgresql