k 均值聚类是最简单也最常用的聚类算法之一。它试图找到代表数据特定区域的簇中心
(cluster center)。算法交替执行以下两个步骤:将每个数据点分配给最近的簇中心,然后将每个簇中心设置为所分配的所有数据点的平均值。如果簇的分配不再发生变化,那么算法结束。下面的例子(图 3-23)在一个模拟数据集上对这一算法进行说明:
在这里插入代码片
簇中心用三角形表示,而数据点用圆形表示。颜色表示簇成员。我们指定要寻找三个簇,
所以通过声明三个随机数据点为簇中心来将算法初始化(见图中“Initialization”/“初
始化”)。然后开始迭代算法。首先,每个数据点被分配给距离最近的簇中心(见图中
“Assign Points (1)”/“分配数据点(1)”)。接下来,将簇中心修改为所分配点的平均值
(见图中“Recompute Centers (1)”/“重新计算中心(1)”)。然后将这一过程再重复两次。在第三次迭代之后,为簇中心分配的数据点保持不变,因此算法结束。
给定新的数据点,k 均值会将其分配给最近的簇中心。下一个例子(图 3-24)展示了图
3-23 学到的簇中心的边界:
在这里插入代码片
用 scikit-learn 应用 k 均值相当简单。下面我们将其应用于上图中的模拟数据。我们将
KMeans 类实例化,并设置我们要寻找的簇个数 3。然后对数据调用 fit 方法:
在这里插入代码片
算法运行期间,为 X 中的每个训练数据点分配一个簇标签。你可以在 kmeans.labels_ 属性中找到这些标签:
在这里插入代码片
因为我们要找的是 3 个簇,所以簇的编号是 0 到 2。
你也可以用 predict 方法为新数据点分配簇标签。预测时会将最近的簇中心分配给每个新
数据点,但现有模型不会改变。对训练集运行 predict 会返回与 labels_ 相同的结果:
在这里插入代码片
可以看到,聚类算法与分类算法有些相似,每个元素都有一个标签。但并不存在真实的标
签,因此标签本身并没有先验意义。我们回到之前讨论过的人脸图像聚类的例子。聚类的
结果可能是,算法找到的第 3 个簇仅包含你朋友 Bela 的面孔。但只有在查看图片之后才能知道这一点,而且数字 3 是任意的。算法给你的唯一信息就是所有标签为 3 的人脸都是相似的。
对于我们刚刚在二维玩具数据集上运行的聚类算法,这意味着我们不应该为其中一组的标
签是 0、另一组的标签是 1 这一事实赋予任何意义。再次运行该算法可能会得到不同的簇
编号,原因在于初始化的随机性质。
下面又给出了这个数据的图像(图 3-25)。簇中心被保存在 cluster_centers_ 属性中,我
们用三角形表示它们:
在这里插入代码片
我们也可以使用更多或更少的簇中心(图 3-26):
在这里插入代码片
即使你知道给定数据集中簇的“正确”个数,k 均值可能也不是总能找到它们。每个簇仅
由其中心定义,这意味着每个簇都是凸形(convex)。因此,k 均值只能找到相对简单的形状。k 均值还假设所有簇在某种程度上具有相同的“直径”,它总是将簇之间的边界刚好画在簇中心的中间位置。有时这会导致令人惊讶的结果,如图 3-27 所示:
在这里插入代码片
你可能会认为,左下方的密集区域是第一个簇,右上方的密集区域是第二个,中间密度较
小的区域是第三个。但事实上,簇 0 和簇 1 都包含一些远离簇中其他点的点。
k 均值还假设所有方向对每个簇都同等重要。图 3-28 显示了一个二维数据集,数据中包含
明确分开的三部分。但是这三部分被沿着对角线方向拉长。由于 k 均值仅考虑到最近簇中
心的距离,所以它无法处理这种类型的数据:
在这里插入代码片
如果簇的形状更加复杂,比如我们在第 2 章遇到的 two_moons 数据,那么 k 均值的表现也很差(见图 3-29):
在这里插入代码片
这里我们希望聚类算法能够发现两个半月形。但利用 k 均值算法是不可能做到这一点的。
虽然 k 均值是一种聚类算法,但在 k 均值和分解方法(比如之前讨论过的 PCA 和 NMF)
之间存在一些有趣的相似之处。你可能还记得,PCA 试图找到数据中方差最大的方向,而
NMF 试图找到累加的分量,这通常对应于数据的“极值”或“部分”(见图 3-13)。两种方法都试图将数据点表示为一些分量之和。与之相反,k 均值则尝试利用簇中心来表示每个数据点。你可以将其看作仅用一个分量来表示每个数据点,该分量由簇中心给出。这种观点将 k 均值看作是一种分解方法,其中每个点用单一分量来表示,这种观点被称为矢量量化(vector quantization)。
我们来并排比较 PCA、NMF 和 k 均值,分别显示提取的分量(图 3-30),以及利用 100 个分量对测试集中人脸的重建(图 3-31)。对于 k 均值,重建就是在训练集中找到的最近的簇中心:
在这里插入代码片
利用 k 均值做矢量量化的一个有趣之处在于,可以用比输入维度更多的簇来对数据进行编
码。让我们回到 two_moons 数据。利用 PCA 或 NMF,我们对这个数据无能为力,因为它只有两个维度。使用 PCA 或 NMF 将其降到一维,将会完全破坏数据的结构。但通过使用更多的簇中心,我们可以用 k 均值找到一种更具表现力的表示(见图 3-32):
在这里插入代码片
我们使用了 10 个簇中心,也就是说,现在每个点都被分配了 0 到 9 之间的一个数字。我们可以将其看作 10 个分量表示的数据(我们有 10 个新特征),只有表示该点对应的簇中心的那个特征不为 0,其他特征均为 0。利用这个 10 维表示,现在可以用线性模型来划分两个半月形,而利用原始的两个特征是不可能做到这一点的。将到每个簇中心的距离作为特征,还可以得到一种表现力更强的数据表示。可以利用 kmeans 的 transform 方法来完成这一点:
在这里插入代码片
k 均值是非常流行的聚类算法,因为它不仅相对容易理解和实现,而且运行速度也相对较
快。k 均值可以轻松扩展到大型数据集,scikit-learn 甚至在 MiniBatchKMeans 类中包含了一种更具可扩展性的变体,可以处理非常大的数据集。
k 均值的缺点之一在于,它依赖于随机初始化,也就是说,算法的输出依赖于随机种子。
默认情况下,scikit-learn 用 10 种不同的随机初始化将算法运行 10 次,并返回最佳结果。
k 均值还有一个缺点,就是对簇形状的假设的约束性较强,而且还要求指定所要寻找
的簇的个数(在现实世界的应用中可能并不知道这个数字)。
接下来,我们将学习另外两种聚类算法,它们都在某些方面对这些性质做了改进。
凝聚聚类(agglomerative clustering)指的是许多基于相同原则构建的聚类算法,这一原则是:算法首先声明每个点是自己的簇,然后合并两个最相似的簇,直到满足某种停止准则为止。scikit-learn 中实现的停止准则是簇的个数,因此相似的簇被合并,直到仅剩下指
定个数的簇。还有一些链接(linkage)准则,规定如何度量“最相似的簇”。这种度量总
是定义在两个现有的簇之间。
scikit-learn 中实现了以下三种选项。
ward 适用于大多数数据集,在我们的例子中将使用它。如果簇中的成员个数非常不同(比如其中一个比其他所有都大得多),那么 average 或 complete 可能效果更好。图 3-33 给出了在一个二维数据集上的凝聚聚类过程,要寻找三个簇。
在这里插入代码片
最开始,每个点自成一簇。然后在每一个步骤中,相距最近的两个簇被合并。在前四个步
骤中,选出两个单点簇并将其合并成两点簇。在步骤 5(Step 5)中,其中一个两点簇被
扩展到三个点,以此类推。在步骤 9(Step 9)中,只剩下 3 个簇。由于我们指定寻找 3 个簇,因此算法结束。
我们来看一下凝聚聚类对我们这里使用的简单三簇数据的效果如何。由于算法的工作原
理,凝聚算法不能对新数据点做出预测。因此 AgglomerativeClustering 没有 predict 方
法。为了构造模型并得到训练集上簇的成员关系,可以改用 fit_predict 方法。结果如图
3-34 所示。
在这里插入代码片
正如所料,算法完美地完成了聚类。虽然凝聚聚类的 scikit-learn 实现需要你指定希望
算法找到的簇的个数,但凝聚聚类方法为选择正确的个数提供了一些帮助,我们将在下
面讨论。
凝聚聚类生成了所谓的层次聚类(hierarchical clustering)。聚类过程迭代进行,每个点都从一个单点簇变为属于最终的某个簇。每个中间步骤都提供了数据的一种聚类(簇的个数也不相同)。有时候,同时查看所有可能的聚类是有帮助的。下一个例子(图 3-35)叠加显示了图 3-33 中所有可能的聚类,有助于深入了解每个簇如何分解为较小的簇:
在这里插入代码片
虽然这种可视化为层次聚类提供了非常详细的视图,但它依赖于数据的二维性质,因此不
能用于具有两个以上特征的数据集。但还有另一个将层次聚类可视化的工具,叫作树状图
(dendrogram),它可以处理多维数据集。
不幸的是,目前 scikit-learn 没有绘制树状图的功能。但你可以利用 SciPy 轻松生成树状
图。SciPy 的聚类算法接口与 scikit-learn 的聚类算法稍有不同。SciPy 提供了一个函数,
接受数据数组 X 并计算出一个链接数组(linkage array),它对层次聚类的相似度进行编码。然后我们可以将这个链接数组提供给 scipy 的 dendrogram 函数来绘制树状图(图 3-36)。
在这里插入代码片
树状图在底部显示数据点(编号从 0 到 11)。然后以这些点(表示单点簇)作为叶节点绘制一棵树,每合并两个簇就添加一个新的父节点。
从下往上看,数据点 1 和 4 首先被合并(正如你在图 3-33 中所见)。接下来,点 6 和 9 被合并为一个簇,以此类推。在顶层有两个分支,一个由点 11、0、5、10、7、6 和 9 组成,另一个由点 1、4、3、2 和 8 组成。这对应于图中左侧两个最大的簇。
树状图的 y 轴不仅说明凝聚算法中两个簇何时合并,每个分支的长度还表示被合并的簇之
间的距离。在这张树状图中,最长的分支是用标记为“three clusters”(三个簇)的虚线表
示的三条线。它们是最长的分支,这表示从三个簇到两个簇的过程中合并了一些距离非常
远的点。我们在图像上方再次看到这一点,将剩下的两个簇合并为一个簇也需要跨越相对
较大的距离。
不幸的是,凝聚聚类仍然无法分离像 two_moons 数据集这样复杂的形状。但我们要学习的下一个算法 DBSCAN 可以解决这个问题。
另一个非常有用的聚类算法是 DBSCAN(density-based spatial clustering of applications with noise,即“具有噪声的基于密度的空间聚类应用”)。DBSCAN 的主要优点是它不需要用户先验地设置簇的个数,可以划分具有复杂形状的簇,还可以找出不属于任何簇的点。DBSCAN 比凝聚聚类和 k 均值稍慢,但仍可以扩展到相对较大的数据集。
DBSCAN 的原理是识别特征空间的“拥挤”区域中的点,在这些区域中许多数据点靠近在
一起。这些区域被称为特征空间中的密集(dense)区域。DBSCAN 背后的思想是,簇形
成数据的密集区域,并由相对较空的区域分隔开。
在密集区域内的点被称为核心样本(core sample,或核心点),它们的定义如下。DBSCAN有两个参数:min_samples 和 eps。如果在距一个给定数据点 eps 的距离内至少有 min_samples 个数据点,那么这个数据点就是核心样本。DBSCAN 将彼此距离小于 eps 的核心样本放到同一个簇中。
算法首先任意选取一个点,然后找到到这个点的距离小于等于 eps 的所有的点。如果
距起始点的距离在 eps 之内的数据点个数小于 min_samples,那么这个点被标记为噪
声(noise),也就是说它不属于任何簇。如果距离在 eps 之内的数据点个数大于 min_
samples,则这个点被标记为核心样本,并被分配一个新的簇标签。然后访问该点的所有
邻居(在距离 eps 以内)。如果它们还没有被分配一个簇,那么就将刚刚创建的新的簇标
签分配给它们。如果它们是核心样本,那么就依次访问其邻居,以此类推。簇逐渐增大,
直到在簇的 eps 距离内没有更多的核心样本为止。然后选取另一个尚未被访问过的点,并重复相同的过程。
最后,一共有三种类型的点:核心点、与核心点的距离在 eps 之内的点(叫作边界点,
boundary point)和噪声。如果 DBSCAN 算法在特定数据集上多次运行,那么核心点的聚
类始终相同,同样的点也始终被标记为噪声。但边界点可能与不止一个簇的核心样本相
邻。因此,边界点所属的簇依赖于数据点的访问顺序。一般来说只有很少的边界点,这种
对访问顺序的轻度依赖并不重要。
我们将 DBSCAN 应用于演示凝聚聚类的模拟数据集。与凝聚聚类类似,DBSCAN 也不允
许对新的测试数据进行预测,所以我们将使用 fit_predict 方法来执行聚类并返回簇标签。
在这里插入代码片
如你所见,所有数据点都被分配了标签 -1,这代表噪声。这是 eps 和 min_samples 默认参数设置的结果,对于小型的玩具数据集并没有调节这些参数。min_samples 和 eps 取不同值时的簇分类如下所示,其可视化结果见图 3-37。
在这里插入代码片
在这张图中,属于簇的点是实心的,而噪声点则显示为空心的。核心样本显示为较大的标
记,而边界点则显示为较小的标记。增大 eps(在图中从左到右),更多的点会被包含在一个簇中。这让簇变大,但可能也会导致多个簇合并成一个。增大 min_samples(在图中从上到下),核心点会变得更少,更多的点被标记为噪声。
参数 eps 在某种程度上更加重要,因为它决定了点与点之间“接近”的含义。将 eps 设置得非常小,意味着没有点是核心样本,可能会导致所有点都被标记为噪声。将 eps 设置得非常大,可能会导致所有点形成单个簇。
设置 min_samples 主要是为了判断稀疏区域内的点被标记为异常值还是形成自己的簇。如果增大 min_samples,任何一个包含少于 min_samples 个样本的簇现在将被标记为噪声。因此,min_samples 决定簇的最小尺寸。在图 3-37 中 eps=1.5 时,从min_samples=3 到 min_samples=5,你可以清楚地看到这一点。min_samples=3 时有三个簇:一个包含 4 个点,一个包含 5 个点,一个包含 3 个点。min_samples=5 时,两个较小的簇(分别包含 3 个点和 4个点)现在被标记为噪声,只保留包含 5 个样本的簇。
虽然 DBSCAN 不需要显式地设置簇的个数,但设置 eps 可以隐式地控制找到的簇的个数。使用 StandardScaler 或 MinMaxScaler 对数据进行缩放之后,有时会更容易找到 eps 的较好取值,因为使用这些缩放技术将确保所有特征具有相似的范围。
图 3-38 展示了在 two_moons 数据集上运行 DBSCAN 的结果。利用默认设置,算法找到了两个半圆形并将其分开:
在这里插入代码片
由于算法找到了我们想要的簇的个数(2 个),因此参数设置的效果似乎很好。如果将 eps减小到 0.2(默认值为 0.5),我们将会得到 8 个簇,这显然太多了。将 eps 增大到 0.7 则会导致只有一个簇。
在使用 DBSCAN 时,你需要谨慎处理返回的簇分配。如果使用簇标签对另一个数据进行
索引,那么使用 -1 表示噪声可能会产生意料之外的结果。
eps
参数定义接近程度,从而间接影响簇的大小。三种方法都可以用于大型的现实世界数据集,都相对容易理解,也都可以聚类成多个簇。