机器学习学习笔记之——无监督学习之聚类-爱代码爱编程
聚类
我们前面说过,聚类(clustering
)是将数据集划分成组的任务,这些组叫作簇(cluster
)。其目标是划分数据,使得一个簇内的数据点非常相似且不同簇内的数据点非常不同。与分类算法类似,聚类算法为每个数据点分配(或预测)一个数字,表示这个点属于哪个簇。
1、K 均值聚类
k 均值聚类是最简单也最常用的聚类算法之一。它试图找到代表数据特定区域的簇中心(cluster center
)。算法交替执行以下两个步骤:将每个数据点分配给最近的簇中心,然后将每个簇中心设置为所分配的所有数据点的平均值。如果簇的分配不再发生变化,那么算法结束。下面的例子在一个模拟数据集上对这一算法进行说明:
mglearn.plots.plot_kmeans_algorithm()
簇中心用三角形表示,而数据点用圆形表示。颜色表示簇成员。我们指定要寻找三个簇,所以通过声明三个随机数据点为簇中心来将算法初始化(见图中 “Initialization
” / “初始化”)。然后开始迭代算法。首先,每个数据点被分配给距离最近的簇中心(见图中 “Assign Points (1)
” / “分配数据点(1)”)。接下来,将簇中心修改为所分配点的平均值(见图中 “Recompute Centers (1)
” / “重新计算中心(1)”)。然后将这一过程再重复两次。在第三次迭代之后,为簇中心分配的数据点保持不变,因此算法结束。
给定新的数据点,k 均值会将其分配给最近的簇中心。下一个例子展示了上图学到的簇中心的边界:
mglearn.plots.plot_kmeans_boundaries()
用 scikit-learn
应用 k 均值相当简单。下面我们将其应用于上图中的模拟数据。我们将 KMeans
类实例化,并设置我们要寻找的簇个数。然后对数据调用 fit
方法:
如果不指定
n_clusters
,它的默认值是 8。使用这个值并没有什么特别的原因。
# 生成模拟的二维数据
X, y = make_blobs(random_state=1)
# 构建聚类模型
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
算法运行期间,为 X
中的每个训练数据点分配一个簇标签。你可以在 kmeans.labels_
属性中找到这些标签:
print("Cluster memberships:\n{}".format(kmeans.labels_))
'''
Cluster memberships:
[1 0 0 0 2 2 2 0 1 1 0 0 2 1 2 2 2 1 0 0 2 0 2 1 0 2 2 1 1 2 1 1 2 1 0 2 0
0 0 2 2 0 1 0 0 2 1 1 1 1 0 2 2 2 1 2 0 0 1 1 0 2 2 0 0 2 1 2 1 0 0 0 2 1
1 0 2 2 1 0 1 0 0 2 1 1 1 1 0 1 2 1 1 0 0 2 2 1 2 1]
'''
因为我们要找的是 3 个簇,所以簇的编号是 0 到 2。
你也可以用 predict
方法为新数据点分配簇标签。预测时会将最近的簇中心分配给每个新数据点,但现有模型不会改变。对训练集运行 predict
会返回与 labels_
相同的结果:
print(kmeans.predict(X))
'''
[1 0 0 0 2 2 2 0 1 1 0 0 2 1 2 2 2 1 0 0 2 0 2 1 0 2 2 1 1 2 1 1 2 1 0 2 0
0 0 2 2 0 1 0 0 2 1 1 1 1 0 2 2 2 1 2 0 0 1 1 0 2 2 0 0 2 1 2 1 0 0 0 2 1
1 0 2 2 1 0 1 0 0 2 1 1 1 1 0 1 2 1 1 0 0 2 2 1 2 1]
'''
可以看到,聚类算法与分类算法有些相似,每个元素都有一个标签。但并不存在真实的标签,因此标签本身并没有先验意义。我们回到之前讨论过的人脸图像聚类的例子。聚类的结果可能是,算法找到的第 3 个簇仅包含你朋友 Bela
的面孔。但只有在查看图片之后才能知道这一点,而且数字 3 是任意的。算法给你的唯一信息就是所有标签为 3 的人脸都是相似的。
对于我们刚刚在二维玩具数据集上运行的聚类算法,这意味着我们不应该为其中一组的标签是 0、另一组的标签是 1 这一事实赋予任何意义。再次运行该算法可能会得到不同的簇编号,原因在于初始化的随机性质。
下面又给出了这个数据的图像。簇中心被保存在 cluster_centers_
属性中,我们用三角形表示它们:
mglearn.discrete_scatter(X[:, 0], X[:, 1], kmeans.labels_, markers='o')
mglearn.discrete_scatter(
kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], [0, 1, 2],
markers='^', markeredgewidth=2)
我们也可以使用更多或更少的簇中心:
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
# 使用2个簇中心:
kmeans = KMeans(n_clusters=2)
kmeans.fit(X)
assignments = kmeans.labels_
mglearn.discrete_scatter(X[:, 0], X[:, 1], assignments, ax=axes[0])
# 使用5个簇中心:
kmeans = KMeans(n_clusters=5)
kmeans.fit(X)
assignments = kmeans.labels_
mglearn.discrete_scatter(X[:, 0], X[:, 1], assignments, ax=axes[1])
1.1、k 均值的失败案例
即使你知道给定数据集中簇的 “正确” 个数,k 均值可能也不是总能找到它们。每个簇仅由其中心定义,这意味着每个簇都是凸形(convex
)。因此,k 均值只能找到相对简单的形状。k 均值还假设所有簇在某种程度上具有相同的 “直径”,它总是将簇之间的边界刚好画在簇中心的中间位置。有时这会导致令人惊讶的结果,如下图所示:
X_varied, y_varied = make_blobs(n_samples=200,
cluster_std=[1.0, 2.5, 0.5],
random_state=170)
y_pred = KMeans(n_clusters=3, random_state=0).fit_predict(X_varied)
mglearn.discrete_scatter(X_varied[:, 0], X_varied[:, 1], y_pred)
plt.legend(["cluster 0", "cluster 1", "cluster 2"], loc='best')
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
你可能会认为,左下方的密集区域是第一个簇,右上方的密集区域是第二个,中间密度较小的区域是第三个。但事实上,簇 0 和簇 1 都包含一些远离簇中其他点的点。
k 均值还假设所有方向对每个簇都同等重要。下图显示了一个二维数据集,数据中包含明确分开的三部分。但是这三部分被沿着对角线方向拉长。由于 k 均值仅考虑到最近簇中心的距离,所以它无法处理这种类型的数据:
# 生成一些随机分组数据
X, y = make_blobs(random_state=170, n_samples=600)
rng = np.random.RandomState(74)
# 变换数据使其拉长
transformation = rng.normal(size=(2, 2))
X = np.dot(X, transformation)
# 将数据聚类成3个簇
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
y_pred = kmeans.predict(X)
# 画出簇分配和簇中心
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap=mglearn.cm3)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
marker='^', c=[0, 1, 2], s=100, linewidth=2, cmap=mglearn.cm3)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
如果簇的形状更加复杂,比如我们在前面遇到的 two_moons
数据,那么 k 均值的表现也很差:
# 生成模拟的two_moons数据(这次的噪声较小)
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
# 将数据聚类成2个簇
kmeans = KMeans(n_clusters=2)
kmeans.fit(X)
y_pred = kmeans.predict(X)
# 画出簇分配和簇中心
plt.scatter(X[:, 0], X[:, 1], c=y_pred, cmap=mglearn.cm2, s=60)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
marker='^', c=[mglearn.cm2(0), mglearn.cm2(1)], s=100, linewidth=2)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
这里我们希望聚类算法能够发现两个半月形。但利用 k 均值算法是不可能做到这一点的。
1.2、矢量量化,或者将 k 均值看作分解
虽然 k 均值是一种聚类算法,但在 k 均值和分解方法(比如之前讨论过的 PCA
和 NMF
)之间存在一些有趣的相似之处。你可能还记得,PCA
试图找到数据中方差最大的方向,而 NMF
试图找到累加的分量,这通常对应于数据的 “极值” 或 “部分”。两种方法都试图将数据点表示为一些分量之和。与之相反,k 均值则尝试利用簇中心来表示每个数据点。你可以将其看作仅用一个分量来表示每个数据点,该分量由簇中心给出。这种观点将 k 均值看作是一种分解方法,其中每个点用单一分量来表示,这种观点被称为矢量量化(vector quantization
)。
我们来并排比较 PCA
、NMF
和 k 均值,分别显示提取的分量,以及利用 100 个分量对测试集中人脸的重建。对于 k 均值,重建就是在训练集中找到的最近的簇中心:
from sklearn.datasets import fetch_lfw_people
people = fetch_lfw_people(min_faces_per_person=20, resize=0.7)
image_shape = people.images[0].shape
mask = np.zeros(people.target.shape, dtype=np.bool)
for target in np.unique(people.target):
mask[np.where(people.target == target)[0][:50]] = 1
X_people = people.data[mask]
y_people = people.target[mask]
X_train, X_test, y_train, y_test = train_test_split(
X_people, y_people, stratify=y_people, random_state=0)
nmf = NMF(n_components=100, random_state=0)
nmf.fit(X_train)
pca = PCA(n_components=100, random_state=0)
pca.fit(X_train)
kmeans = KMeans(n_clusters=100, random_state=0)
kmeans.fit(X_train)
X_reconstructed_pca = pca.inverse_transform(pca.transform(X_test))
X_reconstructed_kmeans = kmeans.cluster_centers_[kmeans.predict(X_test)]
X_reconstructed_nmf = np.dot(nmf.transform(X_test), nmf.components_)
fig, axes = plt.subplots(3, 5, figsize=(8, 8),
subplot_kw={'xticks': (), 'yticks': ()})
fig.suptitle("Extracted Components")
for ax, comp_kmeans, comp_pca, comp_nmf in zip(
axes.T, kmeans.cluster_centers_, pca.components_, nmf.components_):
ax[0].imshow(comp_kmeans.reshape(image_shape))
ax[1].imshow(comp_pca.reshape(image_shape), cmap='viridis')
ax[2].imshow(comp_nmf.reshape(image_shape))
axes[0, 0].set_ylabel("kmeans")
axes[1, 0].set_ylabel("pca")
axes[2, 0].set_ylabel("nmf")
fig, axes = plt.subplots(4, 5, subplot_kw={'xticks': (), 'yticks': ()},
figsize=(8, 8))
fig.suptitle("Reconstructions")
for ax, orig, rec_kmeans, rec_pca, rec_nmf in zip(
axes.T, X_test, X_reconstructed_kmeans, X_reconstructed_pca,
X_reconstructed_nmf):
ax[0].imshow(orig.reshape(image_shape))
ax[1].imshow(rec_kmeans.reshape(image_shape))
ax[2].imshow(rec_pca.reshape(image_shape))
ax[3].imshow(rec_nmf.reshape(image_shape))
axes[0, 0].set_ylabel("original")
axes[1, 0].set_ylabel("kmeans")
axes[2, 0].set_ylabel("pca")
axes[3, 0].set_ylabel("nmf")
利用 k 均值做矢量量化的一个有趣之处在于,可以用比输入维度更多的簇来对数据进行编码。让我们回到 two_moons
数据。利用 PCA 或 NMF,我们对这个数据无能为力,因为它只有两个维度。使用 PCA 或 NMF 将其降到一维,将会完全破坏数据的结构。但通过使用更多的簇中心,我们可以用 k 均值找到一种更具表现力的表示:
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
kmeans = KMeans(n_clusters=10, random_state=0)
kmeans.fit(X)
y_pred = kmeans.predict(X)
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=60, cmap='Paired')
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], s=60,
marker='^', c=range(kmeans.n_clusters), linewidth=2, cmap='Paired')
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
print("Cluster memberships:\n{}".format(y_pred))
'''
Cluster memberships:
[4 7 6 9 7 7 4 1 4 1 8 3 7 1 0 4 2 3 2 0 5 1 2 1 8 6 7 5 6 2 4 8 1 6 4 5 3
4 0 6 3 8 2 6 7 8 4 0 6 1 0 3 5 9 1 4 2 1 2 8 3 9 7 4 1 9 8 7 8 9 3 9 3 6
1 9 6 4 2 3 5 8 3 5 6 8 4 8 3 5 2 4 5 0 5 7 7 3 9 6 1 5 8 4 9 6 9 8 7 2 0
8 8 9 4 1 2 5 3 4 4 0 6 8 6 0 4 6 1 5 4 0 9 3 1 7 1 9 5 4 6 6 2 8 8 4 6 1
2 6 3 7 4 2 3 8 1 3 2 2 6 1 2 7 3 7 2 3 7 1 2 9 0 0 6 1 5 0 0 2 7 0 5 7 5
2 8 3 9 0 9 2 4 4 6 0 5 6 2 7]
'''
我们使用了 10 个簇中心,也就是说,现在每个点都被分配了 0 到 9 之间的一个数字。我们可以将其看作 10 个分量表示的数据(我们有 10 个新特征),只有表示该点对应的簇中心的那个特征不为 0,其他特征均为 0。利用这个 10 维表示,现在可以用线性模型来划分两个半月形,而利用原始的两个特征是不可能做到这一点的。将到每个簇中心的距离作为特征,还可以得到一种表现力更强的数据表示。可以利用 kmeans
的 transform
方法来完成这一点:
distance_features = kmeans.transform(X)
print("Distance feature shape: {}".format(distance_features.shape))
# Distance feature shape: (200, 10)
print("Distance features:\n{}".format(distance_features))
'''
Distance features:
[[1.54731274 1.03376805 0.52485524 ... 1.14060718 1.12484411 1.80791793]
[2.56907679 0.50806038 1.72923085 ... 0.149581 2.27569325 2.66814112]
[0.80949799 1.35912551 0.7503402 ... 1.76451208 0.71910707 0.95077955]
...
[1.12985081 1.04864197 0.91717872 ... 1.50934512 1.04915948 1.17816482]
[0.90881164 1.77871545 0.33200664 ... 1.98349977 0.34346911 1.32756232]
[2.51141196 0.55940949 1.62142259 ... 0.04819401 2.189235 2.63792601]]
'''
1.3、总结
-
优点
k 均值是非常流行的聚类算法,因为它不仅相对容易理解和实现,而且运行速度也相对较快。k 均值可以轻松扩展到大型数据集,
scikit-learn
甚至在MiniBatchKMeans
类中包含了一种更具可扩展性的变体,可以处理非常大的数据集。 -
缺点
k 均值的缺点之一在于,它依赖于随机初始化,也就是说,算法的输出依赖于随机种子。默认情况下,
scikit-learn
用 10 种不同的随机初始化将算法运行 10 次,并返回最佳结果。k 均值还有一个缺点,就是对簇形状的假设的约束性较强,而且还要求指定所要寻找的簇的个数(在现实世界的应用中可能并不知道这个数字)。
2、凝聚聚类
凝聚聚类(agglomerative clustering
)指的是许多基于相同原则构建的聚类算法,这一原则是:算法首先声明每个点是自己的簇,然后合并两个最相似的簇,直到满足某种停止准则为止。scikit-learn
中实现的停止准则是簇的个数,因此相似的簇被合并,直到仅剩下指定个数的簇。还有一些链接(linkage
)准则,规定如何度量 “最相似的簇”。这种度量总是定义在两个现有的簇之间。
scikit-learn
中实现了以下三种选项:
-
ward
默认选项。
ward
挑选两个簇来合并,使得所有簇中的方差增加最小。这通常会得到大小差不多相等的簇。 -
average
average
链接将簇中所有点之间平均距离最小的两个簇合并。 -
complete
complete
链接(也称为最大链接)将簇中点之间最大距离最小的两个簇合并。
ward
适用于大多数数据集,在我们的例子中将使用它。如果簇中的成员个数非常不同(比如其中一个比其他所有都大得多),那么 average
或 complete
可能效果更好。
下图给出了在一个二维数据集上的凝聚聚类过程,要寻找三个簇:
mglearn.plots.plot_agglomerative_algorithm()
最开始,每个点自成一簇。然后在每一个步骤中,相距最近的两个簇被合并。在前四个步骤中,选出两个单点簇并将其合并成两点簇。在步骤 5(Step 5
)中,其中一个两点簇被扩展到三个点,以此类推。在步骤 9(Step 9
)中,只剩下 3 个簇。由于我们指定寻找 3 个簇,因此算法结束。
我们来看一下凝聚聚类对我们这里使用的简单三簇数据的效果如何。由于算法的工作原理,凝聚算法不能对新数据点做出预测。因此 AgglomerativeClustering
没有 predict
方法。为了构造模型并得到训练集上簇的成员关系,可以改用 fit_predict
方法。结果如下图所示。
我们也可以使用
labels_
属性,正如 k 均值所做的那样。
from sklearn.cluster import AgglomerativeClustering
X, y = make_blobs(random_state=1)
agg = AgglomerativeClustering(n_clusters=3)
assignment = agg.fit_predict(X)
mglearn.discrete_scatter(X[:, 0], X[:, 1], assignment)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
正如所料,算法完美地完成了聚类。虽然凝聚聚类的 scikit-learn
实现需要你指定希望算法找到的簇的个数,但凝聚聚类方法为选择正确的个数提供了一些帮助,我们将在下面讨论。
2.1、层次聚类与树状图
凝聚聚类生成了所谓的层次聚类(hierarchical clustering
)。聚类过程迭代进行,每个点都从一个单点簇变为属于最终的某个簇。每个中间步骤都提供了数据的一种聚类(簇的个数也不相同)。有时候,同时查看所有可能的聚类是有帮助的。下一个例子叠加显示了上图中所有可能的聚类,有助于深入了解每个簇如何分解为较小的簇:
mglearn.plots.plot_agglomerative()
虽然这种可视化为层次聚类提供了非常详细的视图,但它依赖于数据的二维性质,因此不能用于具有两个以上特征的数据集。但还有另一个将层次聚类可视化的工具,叫作树状图(dendrogram
),它可以处理多维数据集。
不幸的是,目前 scikit-learn
没有绘制树状图的功能。但你可以利用 SciPy
轻松生成树状图。SciPy
的聚类算法接口与 scikit-learn
的聚类算法稍有不同。SciPy
提供了一个函数,接受数据数组 X
并计算出一个链接数组(linkage array
),它对层次聚类的相似度进行编码。然后我们可以将这个链接数组提供给 scipy
的 dendrogram
函数来绘制树状图。
# 从SciPy中导入dendrogram函数和ward聚类函数
from scipy.cluster.hierarchy import dendrogram, ward
X, y = make_blobs(random_state=0, n_samples=12)
# 将ward聚类应用于数据数组X
# SciPy的ward函数返回一个数组,指定执行凝聚聚类时跨越的距离
linkage_array = ward(X)
# 现在为包含簇之间距离的linkage_array绘制树状图
dendrogram(linkage_array)
# 在树中标记划分成两个簇或三个簇的位置
ax = plt.gca()
bounds = ax.get_xbound()
ax.plot(bounds, [7.25, 7.25], '--', c='k')
ax.plot(bounds, [4, 4], '--', c='k')
ax.text(bounds[1], 7.25, ' two clusters', va='center', fontdict={'size': 15})
ax.text(bounds[1], 4, ' three clusters', va='center', fontdict={'size': 15})
plt.xlabel("Sample index")
plt.ylabel("Cluster distance")
树状图在底部显示数据点(编号从 0 到 11)。然后以这些点(表示单点簇)作为叶节点绘制一棵树,每合并两个簇就添加一个新的父节点。
从下往上看,数据点 1 和 4 首先被合并。接下来,点 6 和 9 被合并为一个簇,以此类推。在顶层有两个分支,一个由点 11、0、5、10、7、6 和 9 组成,另一个由点 1、4、3、2 和 8 组成。这对应于图中左侧两个最大的簇。
树状图的 y 轴不仅说明凝聚算法中两个簇何时合并,每个分支的长度还表示被合并的簇之间的距离。在这张树状图中,最长的分支是用标记为“three clusters”(三个簇)的虚线表示的三条线。它们是最长的分支,这表示从三个簇到两个簇的过程中合并了一些距离非常远的点。我们在图像上方再次看到这一点,将剩下的两个簇合并为一个簇也需要跨越相对较大的距离。
不幸的是,凝聚聚类仍然无法分离像 two_moons
数据集这样复杂的形状。但我们要学习的下一个算法 DBSCAN 可以解决这个问题。
3、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
方法来执行聚类并返回簇标签。
from sklearn.cluster import DBSCAN
X, y = make_blobs(random_state=0, n_samples=12)
dbscan = DBSCAN()
clusters = dbscan.fit_predict(X)
print("Cluster memberships:\n{}".format(clusters))
'''
Cluster memberships:
[-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
'''
如你所见,所有数据点都被分配了标签 -1
,这代表噪声。这是 eps
和 min_samples
默认参数设置的结果,对于小型的玩具数据集并没有调节这些参数。min_samples
和 eps
取不同值时的簇分类如下所示,其可视化结果见下图:
mglearn.plots.plot_dbscan()
'''
min_samples: 2 eps: 1.000000 cluster: [-1 0 0 -1 0 -1 1 1 0 1 -1 -1]
min_samples: 2 eps: 1.500000 cluster: [0 1 1 1 1 0 2 2 1 2 2 0]
min_samples: 2 eps: 2.000000 cluster: [0 1 1 1 1 0 0 0 1 0 0 0]
min_samples: 2 eps: 3.000000 cluster: [0 0 0 0 0 0 0 0 0 0 0 0]
min_samples: 3 eps: 1.000000 cluster: [-1 0 0 -1 0 -1 1 1 0 1 -1 -1]
min_samples: 3 eps: 1.500000 cluster: [0 1 1 1 1 0 2 2 1 2 2 0]
min_samples: 3 eps: 2.000000 cluster: [0 1 1 1 1 0 0 0 1 0 0 0]
min_samples: 3 eps: 3.000000 cluster: [0 0 0 0 0 0 0 0 0 0 0 0]
min_samples: 5 eps: 1.000000 cluster: [-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
min_samples: 5 eps: 1.500000 cluster: [-1 0 0 0 0 -1 -1 -1 0 -1 -1 -1]
min_samples: 5 eps: 2.000000 cluster: [-1 0 0 0 0 -1 -1 -1 0 -1 -1 -1]
min_samples: 5 eps: 3.000000 cluster: [0 0 0 0 0 0 0 0 0 0 0 0]
'''
在这张图中,属于簇的点是实心的,而噪声点则显示为空心的。核心样本显示为较大的标记,而边界点则显示为较小的标记。增大 eps
(在图中从左到右),更多的点会被包含在一个簇中。这让簇变大,但可能也会导致多个簇合并成一个。增大 min_samples
(在图中从上到下),核心点会变得更少,更多的点被标记为噪声。
参数 eps
在某种程度上更加重要,因为它决定了点与点之间 “接近” 的含义。将 eps
设置得非常小,意味着没有点是核心样本,可能会导致所有点都被标记为噪声。将 eps
设置得非常大,可能会导致所有点形成单个簇。
设置 min_samples
主要是为了判断稀疏区域内的点被标记为异常值还是形成自己的簇。如果增大 min_samples
,任何一个包含少于 min_samples
个样本的簇现在将被标记为噪声。因此,min_samples
决定簇的最小尺寸。在上图中 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
的较好取值,因为使用这些缩放技术将确保所有特征具有相似的范围。
下图展示了在 two_moons
数据集上运行 DBSCAN
的结果。利用默认设置,算法找到了两个半圆形并将其分开:
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
# 将数据缩放成平均值为0、方差为1
scaler = StandardScaler()
scaler.fit(X)
X_scaled = scaler.transform(X)
dbscan = DBSCAN()
clusters = dbscan.fit_predict(X_scaled)
# 绘制簇分配
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=clusters, cmap=mglearn.cm2, s=60)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
由于算法找到了我们想要的簇的个数(2 个),因此参数设置的效果似乎很好。如果将 eps
减小到 0.2
(默认值为 0.5
),我们将会得到 8 个簇,这显然太多了。将 eps
增大到 0.7
则会导致只有一个簇。
在使用 DBSCAN
时,你需要谨慎处理返回的簇分配。如果使用簇标签对另一个数据进行索引,那么使用 -1
表示噪声可能会产生意料之外的结果。
4、聚类算法的对比与评估
在应用聚类算法时,其挑战之一就是很难评估一个算法的效果好坏,也很难比较不同算法的结果。在讨论完 k 均值、凝聚聚类和 DBSCAN
背后的算法之后,下面我们将在一些现实世界的数据集上比较它们。
4.1、用真实值评估聚类
有一些指标可用于评估聚类算法相对于真实聚类的结果,其中最重要的是调整 rand 指数(adjusted rand index
,ARI
)和归一化互信息(normalized mutual information
,NMI
),二者都给出了定量的度量,其最佳值为 1,0 表示不相关的聚类(虽然 ARI
可以取负值)。
下面我们使用 ARI
来比较 k 均值、凝聚聚类和 DBSCAN
算法。为了对比,我们还添加了将点随机分配到两个簇中的图像:
from sklearn.metrics.cluster import adjusted_rand_score
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
# 将数据缩放成平均值为0、方差为1
scaler = StandardScaler()
scaler.fit(X)
X_scaled = scaler.transform(X)
fig, axes = plt.subplots(1, 4, figsize=(15, 3),
subplot_kw={'xticks': (), 'yticks': ()})
# 列出要使用的算法
algorithms = [KMeans(n_clusters=2), AgglomerativeClustering(n_clusters=2),
DBSCAN()]
# 创建一个随机的簇分配,作为参考
random_state = np.random.RandomState(seed=0)
random_clusters = random_state.randint(low=0, high=2, size=len(X))
# 绘制随机分配
axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c=random_clusters,
cmap=mglearn.cm3, s=60)
axes[0].set_title("Random assignment - ARI: {:.2f}".format(
adjusted_rand_score(y, random_clusters)))
for ax, algorithm in zip(axes[1:], algorithms):
# 绘制簇分配和簇中心
clusters = algorithm.fit_predict(X_scaled)
ax.scatter(X_scaled[:, 0], X_scaled[:, 1], c=clusters,
cmap=mglearn.cm3, s=60)
ax.set_title("{} - ARI: {:.2f}".format(algorithm.__class__.__name__,
adjusted_rand_score(y, clusters)))
调整 rand
指数给出了符合直觉的结果,随机簇分配的分数为 0,而 DBSCAN
(完美地找到了期望中的聚类)的分数为 1。
用这种方式评估聚类时,一个常见的错误是使用 accuracy_score
而不是 adjusted_rand_score
、normalized_mutual_info_score
或其他聚类指标。使用精度的问题在于,它要求分配的簇标签与真实值完全匹配。但簇标签本身毫无意义——唯一重要的是哪些点位于同一个簇中。
from sklearn.metrics import accuracy_score
# 这两种点标签对应于相同的聚类
clusters1 = [0, 0, 1, 1, 0]
clusters2 = [1, 1, 0, 0, 1]
# 精度为0,因为二者标签完全不同
print("Accuracy: {:.2f}".format(accuracy_score(clusters1, clusters2)))
# Accuracy: 0.00
# 调整rand分数为1,因为二者聚类完全相同
print("ARI: {:.2f}".format(adjusted_rand_score(clusters1, clusters2)))
# ARI: 1.00
4.2、在没有真实值的情况下评估聚类
我们刚刚展示了一种评估聚类算法的方法,但在实践中,使用诸如 ARI
之类的指标有一个很大的问题。在应用聚类算法时,通常没有真实值来比较结果。如果我们知道了数据的正确聚类,那么可以使用这一信息构建一个监督模型(比如分类器)。因此,使用类似 ARI
和 NMI
的指标通常仅有助于开发算法,但对评估应用是否成功没有帮助。
有一些聚类的评分指标不需要真实值,比如轮廓系数(silhouette coeffcient
)。但它们在实践中的效果并不好。轮廓分数计算一个簇的紧致度,其值越大越好,最高分数为 1。虽然紧致的簇很好,但紧致度不允许复杂的形状。
下面是一个例子,利用轮廓分数在 two_moons
数据集上比较 k 均值、凝聚聚类和 DBSCAN
:
from sklearn.metrics.cluster import silhouette_score
X, y = make_moons(n_samples=200, noise=0.05, random_state=0)
# 将数据缩放成平均值为0、方差为1
scaler = StandardScaler()
scaler.fit(X)
X_scaled = scaler.transform(X)
fig, axes = plt.subplots(1, 4, figsize=(15, 3),
subplot_kw={'xticks': (), 'yticks': ()})
# 创建一个随机的簇分配,作为参考
random_state = np.random.RandomState(seed=0)
random_clusters = random_state.randint(low=0, high=2, size=len(X))
# 绘制随机分配
axes[0].scatter(X_scaled[:, 0], X_scaled[:, 1], c=random_clusters,
cmap=mglearn.cm3, s=60)
axes[0].set_title("Random assignment: {:.2f}".format(
silhouette_score(X_scaled, random_clusters)))
algorithms = [KMeans(n_clusters=2), AgglomerativeClustering(n_clusters=2),
DBSCAN()]
for ax, algorithm in zip(axes[1:], algorithms):
clusters = algorithm.fit_predict(X_scaled)
# 绘制簇分配和簇中心
ax.scatter(X_scaled[:, 0], X_scaled[:, 1], c=clusters, cmap=mglearn.cm3,
s=60)
ax.set_title("{} : {:.2f}".format(algorithm.__class__.__name__,
silhouette_score(X_scaled, clusters)))
如你所见,k 均值的轮廓分数最高,尽管我们可能更喜欢 DBSCAN 的结果。对于评估聚类,稍好的策略是使用基于鲁棒性的(robustness-based
)聚类指标。这种指标先向数据中添加一些噪声,或者使用不同的参数设定,然后运行算法,并对结果进行比较。其思想是,如果许多算法参数和许多数据扰动返回相同的结果,那么它很可能是可信的。不幸的是,在写作本书时,scikit-learn
还没有实现这一策略。
即使我们得到一个鲁棒性很好的聚类或者非常高的轮廓分数,但仍然不知道聚类中是否有任何语义含义,或者聚类是否反映了数据中我们感兴趣的某个方面。我们回到人脸图像的例子。我们希望找到类似人脸的分组,比如男人和女人、老人和年轻人,或者有胡子的人和没胡子的人。假设我们将数据分为两个簇,关于哪些点应该被聚类在一起,所有算法的结果一致。我们仍不知道找到的簇是否以某种方式对应于我们感兴趣的概念。算法找到的可能是侧视图和正面视图、夜间拍摄的照片和白天拍摄的照片,或者 iPhone
拍摄的照片和安卓手机拍摄的照片。要想知道聚类是否对应于我们感兴趣的内容,唯一的办法就是对簇进行人工分析。
4.3、在人脸数据集上比较算法
我们将 k 均值、DBSCAN
和凝聚聚类算法应用于 Wild 数据集中的 Labeled Faces
,并查看它们是否找到了有趣的结构。我们将使用数据的特征脸表示,它由包含 100 个成分的 PCA(whiten=True)
生成:
# 从lfw数据中提取特征脸,并对数据进行变换
from sklearn.decomposition import PCA
pca = PCA(n_components=100, whiten=True, random_state=0)
pca.fit_transform(X_people)
X_pca = pca.transform(X_people)
我们之前见到,与原始像素相比,这是对人脸图像的一种语义更强的表示。它的计算速度也更快。这里有一个很好的练习,就是在原始数据上运行下列实验,不要用 PCA
,并观察你是否能找到类似的簇。
用 DBSCAN 分析人脸数据集。我们首先应用刚刚讨论过的 DBSCAN
:
# 应用默认参数的DBSCAN
dbscan = DBSCAN()
labels = dbscan.fit_predict(X_pca)
print("Unique labels: {}".format(np.unique(labels)))
# Unique labels: [-1]
我们看到,所有返回的标签都是 -1,因此所有数据都被 DBSCAN
标记为“噪声”。我们可以改变两个参数来改进这一点:第一,我们可以增大 eps
,从而扩展每个点的邻域;第二,我们可以减小 min_samples
,从而将更小的点组视为簇。我们首先尝试改变 min_samples
:
dbscan = DBSCAN(min_samples=3)
labels = dbscan.fit_predict(X_pca)
print("Unique labels: {}".format(np.unique(labels)))
# Unique labels: [-1]
即使仅考虑由三个点构成的组,所有点也都被标记为噪声。因此我们需要增大 eps
:
dbscan = DBSCAN(min_samples=3, eps=15)
labels = dbscan.fit_predict(X_pca)
print("Unique labels: {}".format(np.unique(labels)))
# Unique labels: [-1 0]
使用更大的 eps
(其值为 15
),我们只得到了单一簇和噪声点。我们可以利用这一结果找出“噪声”相对于其他数据的形状。为了进一步理解发生的事情,我们查看有多少点是噪声,有多少点在簇内:
# 计算所有簇中的点数和噪声中的点数。
# bincount不允许负值,所以我们需要加1。
# 结果中的第一个数字对应于噪声点。
print("Number of points per cluster: {}".format(np.bincount(labels + 1)))
# Number of points per cluster: [ 14 463]
噪声点非常少——只有 27 个,因此我们可以查看所有的噪声点:
noise = X_people[labels==-1]
fig, axes = plt.subplots(3, 9, subplot_kw={'xticks': (), 'yticks': ()},
figsize=(12, 4))
for image, ax in zip(noise, axes.ravel()):
ax.imshow(image.reshape(image_shape), vmin=0, vmax=1)
5、聚类方法小结
本节的内容表明,聚类的应用与评估是一个非常定性的过程,通常在数据分析的探索阶段很有帮助。我们学习了三种聚类算法:k 均值、DBSCAN
和凝聚聚类。这三种算法都可以控制聚类的粒度(granularity
)。k 均值和凝聚聚类允许你指定想要的簇的数量,而 DBSCAN
允许你用 eps
参数定义接近程度,从而间接影响簇的大小。三种方法都可以用于大型的现实世界数据集,都相对容易理解,也都可以聚类成多个簇。
每种算法的优点稍有不同。
-
k 均值
k 均值可以用簇的平均值来表示簇。它还可以被看作一种分解方法,每个数据点都由其簇中心表示。
-
DBSCAN
DBSCAN
可以检测到没有分配任何簇的 “噪声点”,还可以帮助自动判断簇的数量。与其他两种方法不同,它允许簇具有复杂的形状,正如我们在two_moons
的例子中所看到的那样。DBSCAN
有时会生成大小差别很大的簇,这可能是它的优点,也可能是缺点。 -
凝聚聚类
凝聚聚类可以提供数据的可能划分的整个层次结构,可以通过树状图轻松查看。
本章介绍了一系列无监督学习算法,可用于探索性数据分析和预处理。找到数据的正确表示对于监督学习和无监督学习的成功通常都至关重要,预处理和分解方法在数据准备中具有重要作用。
分解、流形学习和聚类都是加深数据理解的重要工具,在没有监督信息的情况下,也是理解数据的仅有的方法。即使是在监督学习中,探索性工具对于更好地理解数据性质也很重要。通常来说,很难量化无监督算法的有用性,但这不应该妨碍你使用它们来深入理解数据。学完这些方法,你就已经掌握了机器学习从业者每天使用的所有必要的学习算法。
我们建议你在 scikit-learn
中包含的二维玩具数据和现实世界数据集(比如 digits
、iris
和 cancer
数据集)上尝试聚类和分解方法。
估计器接口小结
我们简要回顾一下监督学习和非监督学习中介绍的 API。
scikit-learn
中的所有算法——无论是预处理、监督学习还是无监督学习算法——都被实现为类。这些类在scikit-learn
中叫作估计器(estimator
)。为了应用算法,你首先需要将特定类的对象实例化:from sklearn.linear_model import LogisticRegression logreg = LogisticRegression()
估计器类包含算法,也保存了利用算法从数据中学到的模型。
在构建模型对象时,你应该设置模型的所有参数。这些参数包括正则化、复杂度控制、要找到的簇的数量,等等。所有估计器都有
fit
方法,用于构建模型。fit
方法要求第一个参数总是数据X
,用一个NumPy
数组或SciPy
稀疏矩阵表示,其中每一行代表一个数据点。数据X
总被假定为具有连续值(浮点数)的NumPy
数组或SciPy
稀疏矩阵。监督算法还需要有一个y
参数,它是一维NumPy
数组,包含回归或分类的目标值(即已知的输出标签或响应)。在
scikit-learn
中,应用学到的模型主要有两种方法。要想创建一个新输出形式(比如y
)的预测,可以用predict
方法。要想创建输入数据X
的一种新表示,可以用transform
方法。表 3-1 汇总了predict
方法和transform
方法的使用场景。表3-1:
scikit-learn
API小结
estimator.fit(X_train, [y_train])
estimator.predict(X_test)
estimator.transform(X_test)
分类 预处理 回归 降维 聚类 特征提取 特征选择 此外,所有监督模型都有
score(X_test, y_test)
方法,可以评估模型。在表 3-1 中,X_train
和y_train
指的是训练数据和训练标签,而X_test
和y_test
指的是测试数据和测试标签(如果适用的话)。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/qq_36879493/article/details/111091768