一、K-means 的背景
在机器学习领域,许多任务涉及 训练模型来做预测或分类 。比如,医生可能希望通过以往的病例数据来预测某个病人未来是否会患上某种疾病,或者新闻网站可能需要根据文章的主题将新闻自动分类。这些任务通常依赖于有标签的数据 ,也就是每个数据点都带有一个已知的类别或输出(比如健康或生病,新闻类别)。这种学习方式被称为监督学习,因为模型是在学习数据与标签之间的关系。
带标签数据的局限性
然而,在现实中,带标签的数据往往是稀缺的,因为为数据打标签需要大量的时间和人力。例如:
- 在图像分类中,每一张图像都需要人工标注类别,比如这是一只猫还是一只狗。
- 在用户行为分析中,没有预定义的标签告诉我们某个用户属于哪一类人群。
标注数据不仅费时费力,还可能带来高昂的成本 。因此,在许多实际应用中,收集的大量数据往往是无标签的。也就是说,数据本身虽然丰富多样,但没有一个明确的类别告诉我们每个数据点属于什么。
无监督学习的引入
针对这种无标签数据的情况,我们就需要一种不依赖标签的学习方式,这就是无监督学习 。在无监督学习中,我们并没有预先定义的输出或类别,而是希望算法能从数据本身中发现其内在的结构或模式 。也就是说,我们把一堆没有标签的数据交给算法,让它去 自动 寻找数据中的规律。
无监督学习不依赖已知的目标或输出,而是通过分析数据点的特征及其分布,找出数据之间的联系,并自动将这些数据划分为不同的组或模式。
聚类任务
无监督学习中的一个典型任务就是聚类 。所谓聚类,就是把一组数据点根据它们的相似性分成若干个簇,使得同一个簇中的数据点相互之间差异最小 ,而不同簇之间的数据点差异最大。换句话说,聚类的目标是让每个簇中的数据彼此非常相似,而让簇与簇之间的数据显得明显不同。
K-means 算法:最经典的聚类方法
K-means 是聚类任务中最常用、最经典的算法之一。它的核心思想是将数据划分为 k k k 个簇,其中 k k k 是事先定义好的簇的数量。
K-means 算法有两个重要的任务:
- 找到每个簇的中心点 ,称为 质心(centroid)。质心是每个簇的代表点,它是簇内所有数据点的平均值或几何中心。
- 将每个数据点分配给与它距离最近的质心所属的簇。也就是说,数据点会被分配到那个最能"代表"它的簇。
K-means 的目标是通过反复调整质心的位置,使得每个数据点与它所属的质心的距离尽可能短,从而让每个簇内的数据点尽可能接近。
二、K-means 目标函数的推导
在上一部分中,我们讨论了 K-means 的背景和基本思想:将数据划分为 k k k 个簇,并将每个数据点分配给与它最近的质心。接下来,我们将从数学上一步步推导 K-means 的目标函数,并解释它的优化过程。
K-means 算法的核心目标是最小化簇内数据点与质心之间的距离。为了实现这一目标,我们首先需要对"距离"的概念进行数学上的定义,并且定义一个目标函数来量化这个目标。
1. 定义簇内数据点到质心的距离
给定一个包含 n n n 个数据点的集合 X = { x 1 , x 2 , ... , x n } X = \{x_1, x_2, \dots, x_n\} X={x1,x2,...,xn},每个数据点 x i ∈ R d x_i \in \mathbb{R}^d xi∈Rd 是一个 d d d 维向量,表示数据的特征。我们的目标是将这些数据点分成 k k k 个簇,记为 C 1 , C 2 , ... , C k C_1, C_2, \dots, C_k C1,C2,...,Ck,其中每个簇 C j C_j Cj 中的数据点彼此之间相似性较高。
在 K-means 中,我们用 欧氏距离 来度量数据点与质心之间的距离。假设簇 C j C_j Cj 的质心为 μ j \mu_j μj,则数据点 x i x_i xi 到质心 μ j \mu_j μj 的欧氏距离可以定义为:
∥ x i − μ j ∥ 2 = ∑ l = 1 d ( x i ( l ) − μ j ( l ) ) 2 \|x_i - \mu_j\|^2 = \sum_{l=1}^{d} (x_i^{(l)} - \mu_j^{(l)})^2 ∥xi−μj∥2=l=1∑d(xi(l)−μj(l))2
其中 x i ( l ) x_i^{(l)} xi(l) 和 μ j ( l ) \mu_j^{(l)} μj(l) 分别是数据点 x i x_i xi 和质心 μ j \mu_j μj 在第 l l l 个特征上的值。
2. 定义 K-means 的目标函数
K-means 算法的目标是找到一组簇划分 { C 1 , C 2 , ... , C k } \{C_1, C_2, \dots, C_k\} {C1,C2,...,Ck} 和质心 { μ 1 , μ 2 , ... , μ k } \{\mu_1, \mu_2, \dots, \mu_k\} {μ1,μ2,...,μk},使得 簇内所有数据点与质心之间的距离之和 最小化 。我们可以将这个优化目标用数学公式表示为一个 目标函数 :
J = ∑ j = 1 k ∑ x i ∈ C j ∥ x i − μ j ∥ 2 J = \sum_{j=1}^{k} \sum_{x_i \in C_j} \|x_i - \mu_j\|^2 J=j=1∑kxi∈Cj∑∥xi−μj∥2
这里:
- J J J 是我们要最小化的目标函数,它表示所有数据点到各自质心的平方距离之和;
- C j C_j Cj 是第 j j j 个簇,包含了所有分配给第 j j j 个簇的数据点;
- μ j \mu_j μj 是第 j j j 个簇的质心;
- ∥ x i − μ j ∥ 2 \|x_i - \mu_j\|^2 ∥xi−μj∥2 是数据点 x i x_i xi 到质心 μ j \mu_j μj 的欧氏距离的平方。
这个公式表示的是所有簇中,簇内数据点到质心的 距离平方和 ,我们希望通过选择最佳的簇划分和质心位置,使这个平方和最小化。
3. 两步优化:簇划分和质心更新
K-means 的核心优化过程可以分为两个交替的步骤,每次优化一个方面,直到目标函数 J J J 不再显著下降。这两个步骤分别是 簇分配 和 质心更新 。
第一步:固定质心,优化簇分配
假设质心 { μ 1 , μ 2 , ... , μ k } \{\mu_1, \mu_2, \dots, \mu_k\} {μ1,μ2,...,μk} 已经固定不变,我们需要为每个数据点 x i x_i xi 分配一个簇,使得数据点 x i x_i xi 到质心的距离最小。具体来说,数据点 x i x_i xi 被分配到最近的簇 C j C_j Cj,即:
C j = { x i : ∥ x i − μ j ∥ 2 ≤ ∥ x i − μ l ∥ 2 , ∀ l ≠ j } C_j = \{x_i : \|x_i - \mu_j\|^2 \leq \|x_i - \mu_l\|^2, \forall l \neq j\} Cj={xi:∥xi−μj∥2≤∥xi−μl∥2,∀l=j}
这个步骤通过比较每个数据点到所有质心的距离,选择距离最小的质心对应的簇。其本质是将数据点"拉"向最近的质心。
第二步:固定簇分配,优化质心
一旦所有数据点都被分配到了对应的簇,我们接下来要更新每个簇的质心。质心的最佳选择是 簇内所有数据点的几何中心 ,即簇内数据点的平均值:
μ j = 1 ∣ C j ∣ ∑ x i ∈ C j x i \mu_j = \frac{1}{|C_j|} \sum_{x_i \in C_j} x_i μj=∣Cj∣1xi∈Cj∑xi
其中 ∣ C j ∣ |C_j| ∣Cj∣ 是簇 C j C_j Cj 中数据点的数量, ∑ x i ∈ C j x i \sum_{x_i \in C_j} x_i ∑xi∈Cjxi 是簇内所有数据点的总和。
这个步骤的几何意义是,质心会"移动"到当前簇内所有数据点的中心位置。更新质心的目的是为了进一步减少簇内数据点与质心的距离。
4. 迭代优化过程
K-means 通过上述两个步骤的交替迭代,逐步优化目标函数 J J J。每一次迭代,簇分配和质心位置的变化都会导致目标函数 J J J 的值不断减小。虽然 K-means 不能保证找到全局最优解,但由于每一步都会减少目标函数的值,所以算法一定会在某个点收敛到局部最优解。通常,K-means 会在以下条件满足时停止迭代:
- 簇分配不再发生变化;
- 质心位置不再移动;
- 或者达到了预设的迭代次数上限。
5. K-means 的局部最优解问题
值得注意的是,K-means 并不能保证找到全局最优解,因为它依赖于 初始质心的选择 。如果初始质心选择不当,算法可能会陷入局部最优解。为了缓解这个问题,常见的做法是多次运行 K-means,每次使用不同的初始质心,然后选择最优的结果。
三、示例数据与初始质心
我们假设有如下 6 个二维数据点:
X = { ( 1 , 2 ) , ( 1 , 4 ) , ( 3 , 2 ) , ( 5 , 8 ) , ( 7 , 8 ) , ( 9 , 11 ) } X = \{(1, 2), (1, 4), (3, 2), (5, 8), (7, 8), (9, 11)\} X={(1,2),(1,4),(3,2),(5,8),(7,8),(9,11)}
我们希望将这些点划分为 k = 2 k = 2 k=2 个簇,并且假设初始质心为:
μ 1 = ( 1 , 2 ) , μ 2 = ( 5 , 8 ) \mu_1 = (1, 2), \quad \mu_2 = (5, 8) μ1=(1,2),μ2=(5,8)
第 1 次迭代
第一步:固定质心,分配簇
对每个数据点计算其与两个质心的欧氏距离,然后将其分配给最近的质心。
-
点 ( 1 , 2 ) (1, 2) (1,2):
- 与 μ 1 = ( 1 , 2 ) \mu_1 = (1, 2) μ1=(1,2) 的距离:
∥ ( 1 , 2 ) − ( 1 , 2 ) ∥ 2 = 0 \| (1, 2) - (1, 2) \|^2 = 0 ∥(1,2)−(1,2)∥2=0 - 与 μ 2 = ( 5 , 8 ) \mu_2 = (5, 8) μ2=(5,8) 的距离:
∥ ( 1 , 2 ) − ( 5 , 8 ) ∥ 2 = ( 1 − 5 ) 2 + ( 2 − 8 ) 2 = 4 2 + 6 2 = 52 \| (1, 2) - (5, 8) \|^2 = (1 - 5)^2 + (2 - 8)^2 = 4^2 + 6^2 = 52 ∥(1,2)−(5,8)∥2=(1−5)2+(2−8)2=42+62=52
分配给 C 1 C_1 C1
- 与 μ 1 = ( 1 , 2 ) \mu_1 = (1, 2) μ1=(1,2) 的距离:
-
点 ( 1 , 4 ) (1, 4) (1,4):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 1 , 4 ) − ( 1 , 2 ) ∥ 2 = ( 4 − 2 ) 2 = 4 \| (1, 4) - (1, 2) \|^2 = (4 - 2)^2 = 4 ∥(1,4)−(1,2)∥2=(4−2)2=4 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 1 , 4 ) − ( 5 , 8 ) ∥ 2 = ( 1 − 5 ) 2 + ( 4 − 8 ) 2 = 32 \| (1, 4) - (5, 8) \|^2 = (1 - 5)^2 + (4 - 8)^2 = 32 ∥(1,4)−(5,8)∥2=(1−5)2+(4−8)2=32
分配给 C 1 C_1 C1
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 3 , 2 ) (3, 2) (3,2):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 3 , 2 ) − ( 1 , 2 ) ∥ 2 = 4 \| (3, 2) - (1, 2) \|^2 = 4 ∥(3,2)−(1,2)∥2=4 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 3 , 2 ) − ( 5 , 8 ) ∥ 2 = ( 3 − 5 ) 2 + ( 2 − 8 ) 2 = 40 \| (3, 2) - (5, 8) \|^2 = (3 - 5)^2 + (2 - 8)^2 = 40 ∥(3,2)−(5,8)∥2=(3−5)2+(2−8)2=40
分配给 C 1 C_1 C1
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 5 , 8 ) (5, 8) (5,8):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 5 , 8 ) − ( 1 , 2 ) ∥ 2 = 52 \| (5, 8) - (1, 2) \|^2 = 52 ∥(5,8)−(1,2)∥2=52 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 5 , 8 ) − ( 5 , 8 ) ∥ 2 = 0 \| (5, 8) - (5, 8) \|^2 = 0 ∥(5,8)−(5,8)∥2=0
分配给 C 2 C_2 C2
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 7 , 8 ) (7, 8) (7,8):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 7 , 8 ) − ( 1 , 2 ) ∥ 2 = 72 \| (7, 8) - (1, 2) \|^2 = 72 ∥(7,8)−(1,2)∥2=72 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 7 , 8 ) − ( 5 , 8 ) ∥ 2 = 4 \| (7, 8) - (5, 8) \|^2 = 4 ∥(7,8)−(5,8)∥2=4
分配给 C 2 C_2 C2
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 9 , 11 ) (9, 11) (9,11):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 9 , 11 ) − ( 1 , 2 ) ∥ 2 = 145 \| (9, 11) - (1, 2) \|^2 = 145 ∥(9,11)−(1,2)∥2=145 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 9 , 11 ) − ( 5 , 8 ) ∥ 2 = 25 \| (9, 11) - (5, 8) \|^2 = 25 ∥(9,11)−(5,8)∥2=25
分配给 C 2 C_2 C2
- 与 μ 1 \mu_1 μ1 的距离:
结果:
簇 C 1 = { ( 1 , 2 ) , ( 1 , 4 ) , ( 3 , 2 ) } C_1 = \{(1, 2), (1, 4), (3, 2)\} C1={(1,2),(1,4),(3,2)}
簇 C 2 = { ( 5 , 8 ) , ( 7 , 8 ) , ( 9 , 11 ) } C_2 = \{(5, 8), (7, 8), (9, 11)\} C2={(5,8),(7,8),(9,11)}
如图所示:
质心 ( 1 , 2 ) (1, 2) (1,2) 和 ( 5 , 8 ) (5, 8) (5,8) 的位置使得 左侧点(如 ( 1 , 2 ) (1, 2) (1,2)) 明显离 ( 1 , 2 ) (1, 2) (1,2) 质心更近,因此形成了偏向左侧的蓝色区域。相对的, ( 5 , 8 ) (5, 8) (5,8) 的吸引范围较小,但主要覆盖了右上部分的数据点。
决策边界(Voronoi 图的等高线):
- 决策边界 是两个质心之间的等距离线 ,即:
∥ x − μ 1 ∥ = ∥ x − μ 2 ∥ \|x - \mu_1\| = \|x - \mu_2\| ∥x−μ1∥=∥x−μ2∥- 决策边界上的点与两个质心距离相等,成为簇的分界线。在二维平面上,如果质心之间的距离用欧氏距离度量,这条线通常是一条直线。
第二步:固定簇,更新质心
计算每个簇的质心(即簇内所有点的平均值)。
-
簇 C 1 C_1 C1:
数据点: ( 1 , 2 ) , ( 1 , 4 ) , ( 3 , 2 ) (1, 2), (1, 4), (3, 2) (1,2),(1,4),(3,2)
μ 1 = 1 3 ( ( 1 , 2 ) + ( 1 , 4 ) + ( 3 , 2 ) ) = ( 1 + 1 + 3 3 , 2 + 4 + 2 3 ) = ( 1.67 , 2.67 ) \mu_1 = \frac{1}{3} \left((1, 2) + (1, 4) + (3, 2)\right) = \left(\frac{1 + 1 + 3}{3}, \frac{2 + 4 + 2}{3}\right) = (1.67, 2.67) μ1=31((1,2)+(1,4)+(3,2))=(31+1+3,32+4+2)=(1.67,2.67) -
簇 C 2 C_2 C2:
数据点: ( 5 , 8 ) , ( 7 , 8 ) , ( 9 , 11 ) (5, 8), (7, 8), (9, 11) (5,8),(7,8),(9,11)
μ 2 = 1 3 ( ( 5 , 8 ) + ( 7 , 8 ) + ( 9 , 11 ) ) = ( 5 + 7 + 9 3 , 8 + 8 + 11 3 ) = ( 7 , 9 ) \mu_2 = \frac{1}{3} \left((5, 8) + (7, 8) + (9, 11)\right) = \left(\frac{5 + 7 + 9}{3}, \frac{8 + 8 + 11}{3}\right) = (7, 9) μ2=31((5,8)+(7,8)+(9,11))=(35+7+9,38+8+11)=(7,9)
第 2 次迭代
第一步:固定质心 μ 1 = ( 1.67 , 2.67 ) \mu_1 = (1.67, 2.67) μ1=(1.67,2.67) 和 μ 2 = ( 7 , 9 ) \mu_2 = (7, 9) μ2=(7,9),重新分配簇
对每个数据点计算与这两个质心的欧氏距离,并将其分配给距离较近的簇。
-
点 ( 1 , 2 ) (1, 2) (1,2):
- 与 μ 1 = ( 1.67 , 2.67 ) \mu_1 = (1.67, 2.67) μ1=(1.67,2.67) 的距离:
∥ ( 1 , 2 ) − ( 1.67 , 2.67 ) ∥ 2 = ( 1 − 1.67 ) 2 + ( 2 − 2.67 ) 2 = ( − 0.67 ) 2 + ( − 0.67 ) 2 = 0.4489 + 0.4489 = 0.8978 \| (1, 2) - (1.67, 2.67) \|^2 = (1 - 1.67)^2 + (2 - 2.67)^2 = (-0.67)^2 + (-0.67)^2 = 0.4489 + 0.4489 = 0.8978 ∥(1,2)−(1.67,2.67)∥2=(1−1.67)2+(2−2.67)2=(−0.67)2+(−0.67)2=0.4489+0.4489=0.8978 - 与 μ 2 = ( 7 , 9 ) \mu_2 = (7, 9) μ2=(7,9) 的距离:
∥ ( 1 , 2 ) − ( 7 , 9 ) ∥ 2 = ( 1 − 7 ) 2 + ( 2 − 9 ) 2 = ( − 6 ) 2 + ( − 7 ) 2 = 36 + 49 = 85 \| (1, 2) - (7, 9) \|^2 = (1 - 7)^2 + (2 - 9)^2 = (-6)^2 + (-7)^2 = 36 + 49 = 85 ∥(1,2)−(7,9)∥2=(1−7)2+(2−9)2=(−6)2+(−7)2=36+49=85
分配给 C 1 C_1 C1
- 与 μ 1 = ( 1.67 , 2.67 ) \mu_1 = (1.67, 2.67) μ1=(1.67,2.67) 的距离:
-
点 ( 1 , 4 ) (1, 4) (1,4):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 1 , 4 ) − ( 1.67 , 2.67 ) ∥ 2 = ( 1 − 1.67 ) 2 + ( 4 − 2.67 ) 2 = 0.4489 + 1.7689 = 2.2178 \| (1, 4) - (1.67, 2.67) \|^2 = (1 - 1.67)^2 + (4 - 2.67)^2 = 0.4489 + 1.7689 = 2.2178 ∥(1,4)−(1.67,2.67)∥2=(1−1.67)2+(4−2.67)2=0.4489+1.7689=2.2178 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 1 , 4 ) − ( 7 , 9 ) ∥ 2 = ( 1 − 7 ) 2 + ( 4 − 9 ) 2 = 36 + 25 = 61 \| (1, 4) - (7, 9) \|^2 = (1 - 7)^2 + (4 - 9)^2 = 36 + 25 = 61 ∥(1,4)−(7,9)∥2=(1−7)2+(4−9)2=36+25=61
分配给 C 1 C_1 C1
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 3 , 2 ) (3, 2) (3,2):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 3 , 2 ) − ( 1.67 , 2.67 ) ∥ 2 = ( 3 − 1.67 ) 2 + ( 2 − 2.67 ) 2 = 1.7689 + 0.4489 = 2.2178 \| (3, 2) - (1.67, 2.67) \|^2 = (3 - 1.67)^2 + (2 - 2.67)^2 = 1.7689 + 0.4489 = 2.2178 ∥(3,2)−(1.67,2.67)∥2=(3−1.67)2+(2−2.67)2=1.7689+0.4489=2.2178 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 3 , 2 ) − ( 7 , 9 ) ∥ 2 = ( 3 − 7 ) 2 + ( 2 − 9 ) 2 = 16 + 49 = 65 \| (3, 2) - (7, 9) \|^2 = (3 - 7)^2 + (2 - 9)^2 = 16 + 49 = 65 ∥(3,2)−(7,9)∥2=(3−7)2+(2−9)2=16+49=65
分配给 C 1 C_1 C1
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 5 , 8 ) (5, 8) (5,8):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 5 , 8 ) − ( 1.67 , 2.67 ) ∥ 2 = ( 5 − 1.67 ) 2 + ( 8 − 2.67 ) 2 = 11.0889 + 28.2889 = 39.3778 \| (5, 8) - (1.67, 2.67) \|^2 = (5 - 1.67)^2 + (8 - 2.67)^2 = 11.0889 + 28.2889 = 39.3778 ∥(5,8)−(1.67,2.67)∥2=(5−1.67)2+(8−2.67)2=11.0889+28.2889=39.3778 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 5 , 8 ) − ( 7 , 9 ) ∥ 2 = ( 5 − 7 ) 2 + ( 8 − 9 ) 2 = 4 + 1 = 5 \| (5, 8) - (7, 9) \|^2 = (5 - 7)^2 + (8 - 9)^2 = 4 + 1 = 5 ∥(5,8)−(7,9)∥2=(5−7)2+(8−9)2=4+1=5
分配给 C 2 C_2 C2
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 7 , 8 ) (7, 8) (7,8):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 7 , 8 ) − ( 1.67 , 2.67 ) ∥ 2 = ( 7 − 1.67 ) 2 + ( 8 − 2.67 ) 2 = 28.2889 + 28.2889 = 56.5778 \| (7, 8) - (1.67, 2.67) \|^2 = (7 - 1.67)^2 + (8 - 2.67)^2 = 28.2889 + 28.2889 = 56.5778 ∥(7,8)−(1.67,2.67)∥2=(7−1.67)2+(8−2.67)2=28.2889+28.2889=56.5778 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 7 , 8 ) − ( 7 , 9 ) ∥ 2 = ( 7 − 7 ) 2 + ( 8 − 9 ) 2 = 0 + 1 = 1 \| (7, 8) - (7, 9) \|^2 = (7 - 7)^2 + (8 - 9)^2 = 0 + 1 = 1 ∥(7,8)−(7,9)∥2=(7−7)2+(8−9)2=0+1=1
分配给 C 2 C_2 C2
- 与 μ 1 \mu_1 μ1 的距离:
-
点 ( 9 , 11 ) (9, 11) (9,11):
- 与 μ 1 \mu_1 μ1 的距离:
∥ ( 9 , 11 ) − ( 1.67 , 2.67 ) ∥ 2 = ( 9 − 1.67 ) 2 + ( 11 − 2.67 ) 2 = 53.5556 + 69.5556 = 123.1112 \| (9, 11) - (1.67, 2.67) \|^2 = (9 - 1.67)^2 + (11 - 2.67)^2 = 53.5556 + 69.5556 = 123.1112 ∥(9,11)−(1.67,2.67)∥2=(9−1.67)2+(11−2.67)2=53.5556+69.5556=123.1112 - 与 μ 2 \mu_2 μ2 的距离:
∥ ( 9 , 11 ) − ( 7 , 9 ) ∥ 2 = ( 9 − 7 ) 2 + ( 11 − 9 ) 2 = 4 + 4 = 8 \| (9, 11) - (7, 9) \|^2 = (9 - 7)^2 + (11 - 9)^2 = 4 + 4 = 8 ∥(9,11)−(7,9)∥2=(9−7)2+(11−9)2=4+4=8
分配给 C 2 C_2 C2
- 与 μ 1 \mu_1 μ1 的距离:
- 簇 C 1 = { ( 1 , 2 ) , ( 1 , 4 ) , ( 3 , 2 ) } C_1 = \{(1, 2), (1, 4), (3, 2)\} C1={(1,2),(1,4),(3,2)}
- 簇 C 2 = { ( 5 , 8 ) , ( 7 , 8 ) , ( 9 , 11 ) } C_2 = \{(5, 8), (7, 8), (9, 11)\} C2={(5,8),(7,8),(9,11)}
和第 1 次迭代的簇分配一致,因此算法在此处已经 收敛,可以停止迭代。
质心更新为 ( 1.67 , 2.67 ) (1.67, 2.67) (1.67,2.67) 和 ( 7 , 9 ) (7, 9) (7,9) 后,可以看到:
- 蓝色质心的范围稍微向右上方扩大,因为质心移动后,它变得对部分中间位置的数据点更有吸引力。
- 红色区域则变得更加集中在右上角的部分,反映出质心更新后的范围变化。
通过上述计算,我们可以看到,第 2 次迭代的簇分配与第 1 次相同,说明质心位置已经稳定,簇划分不再变化。此时算法达到了收敛条件,可以停止。
最终结果为:
- 簇 1: { ( 1 , 2 ) , ( 1 , 4 ) , ( 3 , 2 ) } \{(1, 2), (1, 4), (3, 2)\} {(1,2),(1,4),(3,2)}
质心: μ 1 = ( 1.67 , 2.67 ) \mu_1 = (1.67, 2.67) μ1=(1.67,2.67) - 簇 2: { ( 5 , 8 ) , ( 7 , 8 ) , ( 9 , 11 ) } \{(5, 8), (7, 8), (9, 11)\} {(5,8),(7,8),(9,11)}
质心: μ 2 = ( 7 , 9 ) \mu_2 = (7, 9) μ2=(7,9)
此例展示了 K-means 的完整迭代过程。即使数据简单,K-means 也展示了其通过不断优化目标函数、最小化簇内距离平方和的能力。
总结:K-means 的代码逻辑
通过上述步骤,我们可以总结出 K-means 算法的核心逻辑:
-
初始化:
随机选择 k k k 个质心。
-
迭代直到收敛:
- 簇分配步骤:
对每个数据点,计算其与所有质心的距离,并将其分配给最近的簇。 - 质心更新步骤:
计算每个簇的新的质心(簇内所有点的平均值)。
- 簇分配步骤:
-
返回结果:
输出最终的簇划分和质心位置。
四、实现代码
在本节中,我们将使用 K-means 算法对一组复杂的数据进行聚类。首先,我们将展示初始数据点的分布情况,然后逐步讲解 K-means 算法的每个步骤,包括初始化质心、簇分配、质心更新以及可视化迭代过程。
初始数据点分布
我们使用 make_blobs
函数生成一个包含 300 个样本、3 个簇的随机数据集。初始数据点分布情况如下所示:
1. 初始化质心
在 K-means 算法中,第一步是随机选择 k k k 个质心。以下是初始化质心的代码:
python
# 1. 随机初始化 k 个质心
def initialize_centroids(data_points, k):
np.random.seed(42) # 设置随机种子以确保可复现性
random_indices = np.random.choice(data_points.shape[0], size=k, replace=False)
return data_points[random_indices]
随机选择初始化质心后的可视化效果如下:
2. 簇分配步骤
在这个步骤中,我们将每个数据点分配给距离最近的质心。以下是相关代码:
python
# 2. 分配数据点到最近的质心
def assign_clusters(data_points, centroids):
distances = np.linalg.norm(data_points[:, np.newaxis] - centroids, axis=2) # 计算距离
return np.argmin(distances, axis=1) # 返回距离最小的索引
3. 质心更新步骤
在每次迭代中,我们需要更新每个簇的质心。质心是簇内所有数据点的平均值。以下是更新质心的代码:
python
# 3. 更新质心
def update_centroids(data_points, cluster_assignments, k):
return np.array([data_points[cluster_assignments == j].mean(axis=0) for j in range(k)]) # 计算每个簇的均值
4. 迭代过程与可视化
我们将实现 K-means 算法的迭代过程,并可视化每一步的聚类结果。以下是用于绘制每次迭代状态的函数:
python
# 4. 迭代绘制和聚类过程
def plot_kmeans_iteration(centroids, data_points, cluster_assignments=None, iteration=0):
plt.figure(figsize=(8, 6))
if cluster_assignments is not None:
plt.scatter(data_points[:, 0], data_points[:, 1], c=cluster_assignments, cmap='viridis', marker='o') # 根据簇分配上色
else:
plt.scatter(data_points[:, 0], data_points[:, 1], c='blue', marker='o', label='Data Points')
# 绘制质心
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='Centroids')
plt.title(f'K-means Iteration {iteration}')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.grid(True)
plt.legend()
plt.show()
K-means 算法实现
最后,我们将结合上述步骤实现完整的 K-means 算法。该算法将进行迭代直至收敛:
python
def kmeans(data_points, k, max_iters=10):
# 初始化质心
centroids = initialize_centroids(data_points, k)
plot_kmeans_iteration(centroids, data_points, iteration=0) # 初始状态
for iteration in range(max_iters):
# 分配簇
cluster_assignments = assign_clusters(data_points, centroids)
plot_kmeans_iteration(centroids, data_points, cluster_assignments, iteration + 1) # 绘制当前簇分配
# 更新质心
new_centroids = update_centroids(data_points, cluster_assignments, k)
# 如果质心没有变化,则收敛
if np.all(centroids == new_centroids):
break
centroids = new_centroids # 更新质心
# 最终结果
plot_kmeans_iteration(centroids, data_points, cluster_assignments, 'Final') # 绘制最终结果
return centroids, cluster_assignments
运行 K-means
最后,执行 K-means 算法:
python
# 运行 K-means
final_centroids, final_cluster_assignments = kmeans(data_points, k=3, max_iters=10)
在每次迭代中,我们可以观察到数据点的簇分配逐步优化,直到达到最终的聚类结果。以下是 K-means 聚类过程中的几个重要步骤的可视化结果:
-
初始状态:
-
第一次迭代:
-
最终结果:
完整代码
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
# 生成较为复杂的聚类数据
data_points, _ = make_blobs(n_samples=300, centers=3, cluster_std=1.0, random_state=42)
# K-means 的代码逻辑
# 1. 随机初始化 k 个质心
def initialize_centroids(data_points, k):
np.random.seed(42)
random_indices = np.random.choice(data_points.shape[0], size=k, replace=False)
return data_points[random_indices]
# 2. 分配数据点到最近的质心
def assign_clusters(data_points, centroids):
distances = np.linalg.norm(data_points[:, np.newaxis] - centroids, axis=2)
return np.argmin(distances, axis=1)
# 3. 更新质心
def update_centroids(data_points, cluster_assignments, k):
return np.array([data_points[cluster_assignments == j].mean(axis=0) for j in range(k)])
# 4. 迭代绘制和聚类过程
def plot_kmeans_iteration(centroids, data_points, cluster_assignments=None, iteration=0):
plt.figure(figsize=(8, 6))
if cluster_assignments is not None:
plt.scatter(data_points[:, 0], data_points[:, 1], c=cluster_assignments, cmap='viridis', marker='o')
else:
plt.scatter(data_points[:, 0], data_points[:, 1], c='blue', marker='o', label='Data Points')
# 绘制质心
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='Centroids')
plt.title(f'K-means Iteration {iteration}')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.grid(True)
plt.legend()
plt.show()
# K-means 算法实现,迭代过程
def kmeans(data_points, k, max_iters=10):
# 初始化质心
centroids = initialize_centroids(data_points, k)
plot_kmeans_iteration(centroids, data_points, iteration=0) # 初始状态
for iteration in range(max_iters):
# 分配簇
cluster_assignments = assign_clusters(data_points, centroids)
plot_kmeans_iteration(centroids, data_points, cluster_assignments, iteration + 1)
# 更新质心
new_centroids = update_centroids(data_points, cluster_assignments, k)
# 如果质心没有变化,则收敛
if np.all(centroids == new_centroids):
break
centroids = new_centroids
# 最终结果
plot_kmeans_iteration(centroids, data_points, cluster_assignments, 'Final')
return centroids, cluster_assignments
# 运行 K-means
final_centroids, final_cluster_assignments = kmeans(data_points, k=3, max_iters=10)