前言
前期回顾: Python机器学习基础篇二《为什么用Python进行机器学习》
上面这篇里面写了文本和序列相关。
我们要讨论的第二种机器学习算法是无监督学习算法。无监督学习包括没有已知输出、没 有老师指导学习算法的各种机器学习。在无监督学习中,学习算法只有输入数据,并需要 从这些数据中提取知识。
3.1 无监督学习的类型
本章将研究两种类型的无监督学习:数据集变换与聚类。
数据集的无监督变换(unsupervised transformation
)是创建数据新的表示的算法,与数据的 原始表示相比,新的表示可能更容易被人或其他机器学习算法所理解。无监督变换的一个 常见应用是降维(dimensionality reduction
),它接受包含许多特征的数据的高维表示,并找到表示该数据的一种新方法,用较少的特征就可以概括其重要特性。降维的一个常见应用是为了可视化将数据降为二维。
无监督变换的另一个应用是找到“构成”数据的各个组成部分。这方面的一个例子就是对文本文档集合进行主题提取。这里的任务是找到每个文档中讨论的未知主题,并学习每个 文档中出现了哪些主题。这可以用于追踪社交媒体上的话题讨论,比如选举、枪支管制或流行歌手等话题。
与之相反,聚类算法(clustering algorithm
)将数据划分成不同的组,每组包含相似的物项。思考向社交媒体网站上传照片的例子。为了方便你整理照片,网站可能想要将同一个 人的照片分在一组。但网站并不知道每张照片是谁,也不知道你的照片集中出现了多少个 人。明智的做法是提取所有的人脸,并将看起来相似的人脸分在一组。但愿这些人脸对应 同一个人,这样图片的分组也就完成了。
3.2 无监督学习的挑战
无监督学习的一个主要挑战就是评估算法是否学到了有用的东西。无监督学习算法一般用于不包含任何标签信息的数据,所以我们不知道正确的输出应该是什么。因此很难判断一个模型是否“表现很好”。例如,假设我们的聚类算法已经将所有的侧脸照片和所有的正面照片进行分组。这肯定是人脸照片集合的一种可能的划分方法,但并不是我们想要的那种方法。然而,我们没有办法“告诉”算法我们要的是什么,通常来说,评估无监督算法结果的唯一方法就是人工检查。
因此,如果数据科学家想要更好地理解数据,那么无监督算法通常可用于探索性的目的, 而不是作为大型自动化系统的一部分。无监督算法的另一个常见应用是作为监督算法的预 处理步骤。学习数据的一种新表示,有时可以提高监督算法的精度,或者可以减少内存占 用和时间开销。
在开始学习“真正的”无监督算法之前,我们先简要讨论几种简单又常用的预处理方法。 虽然预处理和缩放通常与监督学习算法一起使用,但缩放方法并没有用到与“监督”有关的信息,所以它是无监督的。
3.3 预处理与缩放
上一章我们学到,一些算法(如神经网络和 SVM)对数据缩放非常敏感。因此,通常的做法是对特征进行调节,使数据表示更适合于这些算法。通常来说,这是对数据的一种简单的按特征的缩放和移动。下面的代码(图 3-1)给出了一个简单的例子:
In[2]:
mglearn.plots.plot_scaling()
图 3-1:对数据集缩放和预处理的各种方法
3.3.1 不同类型的预处理
在图 3-1 中,第一张图显示的是一个模拟的有两个特征的二分类数据集。第一个特征(x 轴)位于 10 到 15 之间。第二个特征(y 轴)大约位于 1 到 9 之间。
接下来的 4 张图展示了 4 种数据变换方法,都生成了更加标准的范围。scikit-learn
中 的 StandardScaler
确保每个特征的平均值为 0、方差为 1,使所有特征都位于同一量级。但这种缩放不能保证特征任何特定的最大值和最小值。RobustScaler
的工作原理与 StandardScaler
类似,确保每个特征的统计属性都位于同一范围。但 RobustScaler
使用的 是中位数和四分位数 1 ,而不是平均值和方差。这样 RobustScaler
会忽略与其他点有很大不 同的数据点(比如测量误差)。这些与众不同的数据点也叫异常值(outlier
),可能会给其他缩放方法造成麻烦。
与之相反,MinMaxScaler
移动数据,使所有特征都刚好位于 0 到 1 之间。对于二维数据集来说,所有的数据都包含在 x 轴 0 到 1 与 y 轴 0 到 1 组成的矩形中。
最后,Normalizer
用到一种完全不同的缩放方法。它对每个数据点进行缩放,使得特征向量的欧式长度等于 1。换句话说,它将一个数据点投射到半径为 1 的圆上(对于更高维度的情况,是球面)。这意味着每个数据点的缩放比例都不相同(乘以其长度的倒数)。如果只有数据的方向(或角度)是重要的,而特征向量的长度无关紧要,那么通常会使用这种 归一化。
3.3.2 应用数据变换
前面我们已经看到不同类型的变换的作用,下面利用 scikit-learn 来应用这些变换。我们 将使用第 2 章见过的 cancer 数据集。通常在应用监督学习算法之前使用预处理方法(比 如缩放)。举个例子,比如我们想要将核 SVM(SVC)应用在 cancer 数据集上,并使用 MinMaxScaler 来预处理数据。首先加载数据集并将其分为训练集和测试集(我们需要分开 的训练集和数据集来对预处理后构建的监督模型进行评估):
In[3]:
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target,
random_state=1)
print(X_train.shape)
print(X_test.shape)
Out[3]:
(426, 30)
(143, 30)
提醒一下,这个数据集包含 569 个数据点,每个数据点由 30 个测量值表示。我们将数据 集分成包含 426 个样本的训练集与包含 143 个样本的测试集。
与之前构建的监督模型一样,我们首先导入实现预处理的类,然后将其实例化:
In[4]:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
然后,使用 fit 方法拟合缩放器(scaler),并将其应用于训练数据。对于 MinMaxScaler
来 说,fit 方法计算训练集中每个特征的最大值和最小值。与第 2 章中的分类器和回归器 (regressor)不同,在对缩放器调用 fit 时只提供了 X_train,而不用 y_train:
In[5]:
scaler.fit(X_train)
Out[5]:
MinMaxScaler(copy=True, feature_range=(0, 1))
为了应用刚刚学习的变换(即对训练数据进行实际缩放),我们使用缩放器的 transform
方法。在 scikit-learn
中,每当模型返回数据的一种新表示时,都可以使用 transform 方法:
In[6]:
# 变换数据
X_train_scaled = scaler.transform(X_train)
# 在缩放之前和之后分别打印数据集属性
print("transformed shape: {}".format(X_train_scaled.shape))
print("per-feature minimum before scaling:\n {}".format(X_train.min(axis=0)))
print("per-feature maximum before scaling:\n {}".format(X_train.max(axis=0)))
print("per-feature minimum after scaling:\n {}".format(
X_train_scaled.min(axis=0)))
print("per-feature maximum after scaling:\n {}".format(
X_train_scaled.max(axis=0)))
Out[6]:
transformed shape: (426, 30)
per-feature minimum before scaling:
[ 6.98 9.71 43.79 143.50 0.05 0.02 0. 0. 0.11
0.05 0.12 0.36 0.76 6.80 0. 0. 0. 0.
0.01 0. 7.93 12.02 50.41 185.20 0.07 0.03 0.
0. 0.16 0.06]
per-feature maximum before scaling:
[ 28.11 39.28 188.5 2501.0 0.16 0.29 0.43 0.2
0.300 0.100 2.87 4.88 21.98 542.20 0.03 0.14
0.400 0.050 0.06 0.03 36.04 49.54 251.20 4254.00
0.220 0.940 1.17 0.29 0.58 0.15]
per-feature minimum after scaling:
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
per-feature maximum after scaling:
[ 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.
1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
变换后的数据形状与原始数据相同,特征只是发生了移动和缩放。你可以看到,现在所有特征都位于 0 到 1 之间,这也符合我们的预期。
为了将 SVM 应用到缩放后的数据上,还需要对测试集进行变换。这可以通过对 X_test
调用 transform
方法来完成:
In[7]:
# 对测试数据进行变换
X_test_scaled = scaler.transform(X_test)
# 在缩放之后打印测试数据的属性
print("per-feature minimum after scaling:\n{}".format(X_test_scaled.min(axis=0)))
print("per-feature maximum after scaling:\n{}".format(X_test_scaled.max(axis=0)))
Out[7]:
per-feature minimum after scaling:
[ 0.034 0.023 0.031 0.011 0.141 0.044 0. 0. 0.154 -0.006
-0.001 0.006 0.004 0.001 0.039 0.011 0. 0. -0.032 0.007
0.027 0.058 0.02 0.009 0.109 0.026 0. 0. -0. -0.002]
per-feature maximum after scaling:
[ 0.958 0.815 0.956 0.894 0.811 1.22 0.88 0.933 0.932 1.037
0.427 0.498 0.441 0.284 0.487 0.739 0.767 0.629 1.337 0.391
0.896 0.793 0.849 0.745 0.915 1.132 1.07 0.924 1.205 1.631]
你可以发现,对测试集缩放后的最大值和最小值不是 1 和 0,这或许有些出乎意料。有些特征甚至在 0~1 的范围之外!对此的解释是,MinMaxScaler
(以及其他所有缩放器)总是对训练集和测试集应用完全相同的变换。也就是说,transform
方法总是减去训练集的最小值,然后除以训练集的范围,而这两个值可能与测试集的最小值和范围并不相同。
3.3.3 对训练数据和测试数据进行相同的缩放
为了让监督模型能够在测试集上运行,对训练集和测试集应用完全相同的变换是很重要 的。如果我们使用测试集的最小值和范围,下面这个例子(图 3-2)展示了会发生什么:
In[8]:
from sklearn.datasets import make_blobs
# 构造数据
X, _ = make_blobs(n_samples=50, centers=5, random_state=4, cluster_std=2)
# 将其分为训练集和测试集
X_train, X_test = train_test_split(X, random_state=5, test_size=.1)
# 绘制训练集和测试集
fig, axes = plt.subplots(1, 3, figsize=(13, 4))
axes[0].scatter(X_train[:, 0], X_train[:, 1],
c=mglearn.cm2(0), label="Training set", s=60)
axes[0].scatter(X_test[:, 0], X_test[:, 1], marker='^',
c=mglearn.cm2(1), label="Test set", s=60)
axes[0].legend(loc='upper left')
axes[0].set_title("Original Data")
# 利用MinMaxScaler缩放数据
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 将正确缩放的数据可视化
axes[1].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1],
c=mglearn.cm2(0), label="Training set", s=60)
axes[1].scatter(X_test_scaled[:, 0], X_test_scaled[:, 1], marker='^',
c=mglearn.cm2(1), label="Test set", s=60)
axes[1].set_title("Scaled Data")
# 单独对测试集进行缩放
# 使得测试集的最小值为0,最大值为1
# 千万不要这么做!这里只是为了举例
test_scaler = MinMaxScaler()
test_scaler.fit(X_test)
X_test_scaled_badly = test_scaler.transform(X_test)
# 将错误缩放的数据可视化
axes[2].scatter(X_train_scaled[:, 0], X_train_scaled[:, 1],
c=mglearn.cm2(0), label="training set", s=60)
axes[2].scatter(X_test_scaled_badly[:, 0], X_test_scaled_badly[:, 1],
marker='^', c=mglearn.cm2(1), label="test set", s=60)
axes[2].set_title("Improperly Scaled Data")
for ax in axes:
ax.set_xlabel("Feature 0")
ax.set_ylabel("Feature 1")
图 3-2:对左图中的训练数据和测试数据同时缩放的效果(中)和分别缩放的效果(右)
第一张图是未缩放的二维数据集,其中训练集用圆形表示,测试集用三角形表示。第二张 图中是同样的数据,但使用 MinMaxScaler 缩放。这里我们调用 fit 作用在训练集上,然后 调用 transform 作用在训练集和测试集上。你可以发现,第二张图中的数据集看起来与第 一张图中的完全相同,只是坐标轴刻度发生了变化。现在所有特征都位于 0 到 1 之间。你 还可以发现,测试数据(三角形)的特征最大值和最小值并不是 1 和 0。
第三张图展示了如果我们对训练集和测试集分别进行缩放会发生什么。在这种情况下,对 训练集和测试集而言,特征的最大值和最小值都是 1 和 0。但现在数据集看起来不一样。 测试集相对训练集的移动不一致,因为它们分别做了不同的缩放。我们随意改变了数据的排列。这显然不是我们想要做的事情。
再换一种思考方式,想象你的测试集只有一个点。对于一个点而言,无法将其正确地缩放以满足 MinMaxScaler 的最大值和最小值的要求。但是,测试集的大小不应该对你的处理方式有影响。
3.3.4 预处理对监督学习的作用
现在我们回到 cancer 数据集,观察使用 MinMaxScaler 对学习 SVC 的作用(这是一种不同的 方法,实现了与第 2 章中相同的缩放)。首先,为了对比,我们再次在原始数据上拟合 SVC:
In[10]:
from sklearn.svm import SVC
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target,
random_state=0)
svm = SVC(C=100)
svm.fit(X_train, y_train)
print("Test set accuracy: {:.2f}".format(svm.score(X_test, y_test)))
Out[10]:
Test set accuracy: 0.63
下面先用 MinMaxScaler 对数据进行缩放,然后再拟合 SVC:
In[11]:
# 使用0-1缩放进行预处理
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 在缩放后的训练数据上学习SVM
svm.fit(X_train_scaled, y_train)
# 在缩放后的测试集上计算分数
print("Scaled test set accuracy: {:.2f}".format(
svm.score(X_test_scaled, y_test)))
Out[11]:
Scaled test set accuracy: 0.97
正如我们上面所见,数据缩放的作用非常显著。虽然数据缩放不涉及任何复杂的数学,但良好的做法仍然是使用 scikit-learn 提供的缩放机制,而不是自己重新实现它们,因为即使在这些简单的计算中也容易犯错。
你也可以通过改变使用的类将一种预处理算法轻松替换成另一种,因为所有的预处理类都 具有相同的接口,都包含 fit 和 transform 方法:
In[12]:
# 利用零均值和单位方差的缩放方法进行预处理
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 在缩放后的训练数据上学习SVM
svm.fit(X_train_scaled, y_train)
# 在缩放后的测试集上计算分数
print("SVM test accuracy: {:.2f}".format(svm.score(X_test_scaled, y_test)))
Out[12]:
SVM test accuracy: 0.96
前面我们已经看到了用于预处理的简单数据变换的工作原理,下面继续学习利用无监督学习进行更有趣的变换。
3.4 降维、特征提取与流形学习
前面讨论过,利用无监督学习进行数据变换可能有很多种目的。最常见的目的就是可视 化、压缩数据,以及寻找信息量更大的数据表示以用于进一步的处理。
为了实现这些目的,最简单也最常用的一种算法就是主成分分析。我们也将学习另外两种 算法:非负矩阵分解(NMF)和 t-SNE,前者通常用于特征提取,后者通常用于二维散点 图的可视化。
3.4.1 主成分分析
主成分分析(principal component analysis
,PCA)是一种旋转数据集的方法,旋转后的特征在统计上不相关。在做完这种旋转之后,通常是根据新特征对解释数据的重要性来选择它的一个子集。下面的例子(图 3-3)展示了 PCA 对一个模拟二维数据集的作用:
In[13]:
mglearn.plots.plot_pca_illustration()
图 3-3:用 PCA 做数据变换
第一张图(左上)显示的是原始数据点,用不同颜色加以区分。算法首先找到方差最大的 方向,将其标记为“成分 1”(Component 1)。这是数据中包含最多信息的方向(或向量), 换句话说,沿着这个方向的特征之间最为相关。然后,算法找到与第一个方向正交(成直 角)且包含最多信息的方向。在二维空间中,只有一个成直角的方向,但在更高维的空间 中会有(无穷)多的正交方向。虽然这两个成分都画成箭头,但其头尾的位置并不重要。 我们也可以将第一个成分画成从中心指向左上,而不是指向右下。利用这一过程找到的方 向被称为主成分(principal component),因为它们是数据方差的主要方向。一般来说,主 成分的个数与原始特征相同。
第二张图(右上)显示的是同样的数据,但现在将其旋转,使得第一主成分与 x 轴平行且 第二主成分与 y 轴平行。在旋转之前,从数据中减去平均值,使得变换后的数据以零为中 心。在 PCA 找到的旋转表示中,两个坐标轴是不相关的,也就是说,对于这种数据表示, 除了对角线,相关矩阵全部为零。
我们可以通过仅保留一部分主成分来使用 PCA 进行降维。在这个例子中,我们可以仅保 留第一个主成分,正如图 3-3 中第三张图所示(左下)。这将数据从二维数据集降为一维数 据集。但要注意,我们没有保留原始特征之一,而是找到了最有趣的方向(第一张图中从 左上到右下)并保留这一方向,即第一主成分。
最后,我们可以反向旋转并将平均值重新加到数据中。这样会得到图 3-3 最后一张图中的 数据。这些数据点位于原始特征空间中,但我们仅保留了第一主成分中包含的信息。这种 变换有时用于去除数据中的噪声影响,或者将主成分中保留的那部分信息可视化。
- 将 PCA 应用于 cancer 数据集并可视化
PCA 最常见的应用之一就是将高维数据集可视化。正如第 1 章中所说,对于有两个以上特 征的数据,很难绘制散点图。对于 Iris(鸢尾花)数据集,我们可以创建散点图矩阵(见 第 1 章图 1-3),通过展示特征所有可能的两两组合来展示数据的局部图像。但如果我们想 要查看乳腺癌数据集,即便用散点图矩阵也很困难。这个数据集包含 30 个特征,这就导 致需要绘制 30 * 14 = 420 张散点图!我们永远不可能仔细观察所有这些图像,更不用说试 图理解它们了。
不过我们可以使用一种更简单的可视化方法——对每个特征分别计算两个类别(良性肿瘤 和恶性肿瘤)的直方图(见图 3-4)。
In[14]:
fig, axes = plt.subplots(15, 2, figsize=(10, 20))
malignant = cancer.data[cancer.target == 0]
benign = cancer.data[cancer.target == 1]
ax = axes.ravel()
for i in range(30):
_, bins = np.histogram(cancer.data[:, i], bins=50)
ax[i].hist(malignant[:, i], bins=bins, color=mglearn.cm3(0), alpha=.5)
ax[i].hist(benign[:, i], bins=bins, color=mglearn.cm3(2), alpha=.5)
ax[i].set_title(cancer.feature_names[i])
ax[i].set_yticks(())
ax[0].set_xlabel("Feature magnitude")
ax[0].set_ylabel("Frequency")
ax[0].legend(["malignant", "benign"], loc="best")
fig.tight_layout()
图 3-4:乳腺癌数据集中每个类别的特征直方图
这里我们为每个特征创建一个直方图,计算具有某一特征的数据点在特定范围内(叫作 bin)的出现频率。每张图都包含两个直方图,一个是良性类别的所有点(蓝色),一个是 恶性类别的所有点(红色)。这样我们可以了解每个特征在两个类别中的分布情况,也可 以猜测哪些特征能够更好地区分良性样本和恶性样本。例如,“smoothness error”特征似乎 没有什么信息量,因为两个直方图大部分都重叠在一起,而“worst concave points”特征 看起来信息量相当大,因为两个直方图的交集很小。
但是,这种图无法向我们展示变量之间的相互作用以及这种相互作用与类别之间的关系。 利用 PCA,我们可以获取到主要的相互作用,并得到稍为完整的图像。我们可以找到前两 个主成分,并在这个新的二维空间中用散点图将数据可视化。
在应用 PCA 之前,我们利用 StandardScaler 缩放数据,使每个特征的方差均为 1:
In[15]:
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
scaler = StandardScaler()
scaler.fit(cancer.data)
X_scaled = scaler.transform(cancer.data)
学习并应用 PCA 变换与应用预处理变换一样简单。我们将 PCA 对象实例化,调用 fit 方 法找到主成分,然后调用 transform 来旋转并降维。默认情况下,PCA 仅旋转(并移动) 数据,但保留所有的主成分。为了降低数据的维度,我们需要在创建 PCA 对象时指定想要 保留的主成分个数:
In[16]:
from sklearn.decomposition import PCA
# 保留数据的前两个主成分
pca = PCA(n_components=2)
# 对乳腺癌数据拟合PCA模型
pca.fit(X_scaled)
# 将数据变换到前两个主成分的方向上
X_pca = pca.transform(X_scaled)
print("Original shape: {}".format(str(X_scaled.shape)))
print("Reduced shape: {}".format(str(X_pca.shape)))
Out[16]:
Original shape: (569, 30)
Reduced shape: (569, 2)
现在我们可以对前两个主成分作图(图 3-5):
In[17]:
# 对第一个和第二个主成分作图,按类别着色
plt.figure(figsize=(8, 8))
mglearn.discrete_scatter(X_pca[:, 0], X_pca[:, 1], cancer.target)
plt.legend(cancer.target_names, loc="best")
plt.gca().set_aspect("equal")
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
图 3-5:利用前两个主成分绘制乳腺癌数据集的二维散点图
重要的是要注意,PCA 是一种无监督方法,在寻找旋转方向时没有用到任何类别信息。它 只是观察数据中的相关性。对于这里所示的散点图,我们绘制了第一主成分与第二主成分 的关系,然后利用类别信息对数据点进行着色。你可以看到,在这个二维空间中两个类别 被很好地分离。这让我们相信,即使是线性分类器(在这个空间中学习一条直线)也可以 在区分这个两个类别时表现得相当不错。我们还可以看到,恶性点比良性点更加分散,这 一点也可以在图 3-4 的直方图中看出来。
PCA 的一个缺点在于,通常不容易对图中的两个轴做出解释。主成分对应于原始数据中的 方向,所以它们是原始特征的组合。但这些组合往往非常复杂,这一点我们很快就会看 到。在拟合过程中,主成分被保存在 PCA 对象的 components_ 属性中:
In[18]:
print("PCA component shape: {}".format(pca.components_.shape))
Out[18]:
PCA component shape: (2, 30)
components 中的每一行对应于一个主成分,它们按重要性排序(第一主成分排在首位, 以此类推)。列对应于 PCA 的原始特征属性,在本例中即为“mean radius”“mean texture” 等。我们来看一下 components 的内容:
In[19]:
print("PCA components:\n{}".format(pca.components_))
Out[19]:
PCA components:
[[ 0.219 0.104 0.228 0.221 0.143 0.239 0.258 0.261 0.138 0.064
0.206 0.017 0.211 0.203 0.015 0.17 0.154 0.183 0.042 0.103
0.228 0.104 0.237 0.225 0.128 0.21 0.229 0.251 0.123 0.132]
[-0.234 -0.06 -0.215 -0.231 0.186 0.152 0.06 -0.035 0.19 0.367
-0.106 0.09 -0.089 -0.152 0.204 0.233 0.197 0.13 0.184 0.28
-0.22 -0.045 -0.2 -0.219 0.172 0.144 0.098 -0.008 0.142 0.275]]
我们还可以用热图将系数可视化(图 3-6),这可能更容易理解:
In[20]:
plt.matshow(pca.components_, cmap='viridis')
plt.yticks([0, 1], ["First component", "Second component"])
plt.colorbar()
plt.xticks(range(len(cancer.feature_names)),
cancer.feature_names, rotation=60, ha='left')
plt.xlabel("Feature")
plt.ylabel("Principal components")
图 3-6:乳腺癌数据集前两个主成分的热图
你可以看到,在第一个主成分中,所有特征的符号相同(均为正,但前面我们提到过,箭 头指向哪个方向无关紧要)。这意味着在所有特征之间存在普遍的相关性。如果一个测量 值较大的话,其他的测量值可能也较大。第二个主成分的符号有正有负,而且两个主成分 都包含所有 30 个特征。这种所有特征的混合使得解释图 3-6 中的坐标轴变得十分困难。
- 特征提取的特征脸
前面提到过,PCA 的另一个应用是特征提取。特征提取背后的思想是,可以找到一种数据 表示,比给定的原始表示更适合于分析。特征提取很有用,它的一个很好的应用实例就是 图像。图像由像素组成,通常存储为红绿蓝(RGB)强度。图像中的对象通常由上千个像 素组成,它们只有放在一起才有意义。
我们将给出用 PCA 对图像做特征提取的一个简单应用,即处理 Wild 数据集 Labeled Faces (标记人脸)中的人脸图像。这一数据集包含从互联网下载的名人脸部图像,它包含从 21 世纪初开始的政治家、歌手、演员和运动员的人脸图像。我们使用这些图像的灰度版本, 并将它们按比例缩小以加快处理速度。你可以在图 3-7 中看到其中一些图像:
In[21]:
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
fix, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for target, image, ax in zip(people.target, people.images, axes.ravel()):
ax.imshow(image)
ax.set_title(people.target_names[target])
图 3-7:来自 Wild 数据集中 Labeled Faces 的一些图像
一共有 3023 张图像,每张大小为 87 像素 ×65 像素,分别属于 62 个不同的人:
In[22]:
print("people.images.shape: {}".format(people.images.shape))
print("Number of classes: {}".format(len(people.target_names)))
Out[22]:
people.images.shape: (3023, 87, 65)
Number of classes: 62
但这个数据集有些偏斜,其中包含 George W. Bush(小布什)和 Colin Powell(科林 • 鲍威 尔)的大量图像,正如你在下面所见:
In[23]:
# 计算每个目标出现的次数
counts = np.bincount(people.target)
# 将次数与目标名称一起打印出来
for i, (count, name) in enumerate(zip(counts, people.target_names)):
print("{0:25} {1:3}".format(name, count), end=' ')
if (i + 1) % 3 == 0:
print()
Out[23]:
Alejandro Toledo 39 Alvaro Uribe 35
Amelie Mauresmo 21 Andre Agassi 36
Angelina Jolie 20 Ariel Sharon 77
Arnold Schwarzenegger 42 Atal Bihari Vajpayee 24
Bill Clinton 29 Carlos Menem 21
Colin Powell 236 David Beckham 31
Donald Rumsfeld 121 George Robertson 22
George W Bush 530 Gerhard Schroeder 109
Gloria Macapagal Arroyo 44 Gray Davis 26
Guillermo Coria 30 Hamid Karzai 22
Hans Blix 39 Hugo Chavez 71
Igor Ivanov 20 Jack Straw 28
Jacques Chirac 52 Jean Chretien 55
Jennifer Aniston 21 Jennifer Capriati 42
Jennifer Lopez 21 Jeremy Greenstock 24
Jiang Zemin 20 John Ashcroft 53
John Negroponte 31 Jose Maria Aznar 23
Juan Carlos Ferrero 28 Junichiro Koizumi 60
Kofi Annan 32 Laura Bush 41
Lindsay Davenport 22 Lleyton Hewitt 41
Luiz Inacio Lula da Silva 48 Mahmoud Abbas 29
Megawati Sukarnoputri 33 Michael Bloomberg 20
Naomi Watts 22 Nestor Kirchner 37
Paul Bremer 20 Pete Sampras 22
Recep Tayyip Erdogan 30 Ricardo Lagos 27
Roh Moo-hyun 32 Rudolph Giuliani 26
Saddam Hussein 23 Serena Williams 52
Silvio Berlusconi 33 Tiger Woods 23
Tom Daschle 25 Tom Ridge 33
Tony Blair 144 Vicente Fox 32
Vladimir Putin 49 Winona Ryder 24
为了降低数据偏斜,我们对每个人最多只取 50 张图像(否则,特征提取将会被 George W. Bush 的可能性大大影响):
In[24]:
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]
# 将灰度值缩放到0到1之间,而不是在0到255之间
# 以得到更好的数据稳定性
X_people = X_people / 255.
人脸识别的一个常见任务就是看某个前所未见的人脸是否属于数据库中的某个已知人物。 这在照片收集、社交媒体和安全应用中都有应用。解决这个问题的方法之一就是构建一个 分类器,每个人都是一个单独的类别。但人脸数据库中通常有许多不同的人,而同一个人 的图像很少(也就是说,每个类别的训练样例很少)。这使得大多数分类器的训练都很困 难。另外,通常你还想要能够轻松添加新的人物,不需要重新训练一个大型模型。
一种简单的解决方法是使用单一最近邻分类器,寻找与你要分类的人脸最为相似的 人脸。这个分类器原则上可以处理每个类别只有一个训练样例的情况。下面看一下 KNeighborsClassifier 的表现如何:
In[25]:
from sklearn.neighbors import KNeighborsClassifier
# 将数据分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X_people, y_people, stratify=y_people, random_state=0)
# 使用一个邻居构建KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train, y_train)
print("Test set score of 1-nn: {:.2f}".format(knn.score(X_test, y_test)))
Out[25]:
Test set score of 1-nn: 0.27
我们得到的精度为 26.6%。对于包含 62 个类别的分类问题来说,这实际上不算太差(随机 猜测的精度约为 1/62=1.5%),但也不算好。我们每识别四次仅正确识别了一个人。
这里就可以用到 PCA。想要度量人脸的相似度,计算原始像素空间中的距离是一种相当糟 糕的方法。用像素表示来比较两张图像时,我们比较的是每个像素的灰度值与另一张图像 对应位置的像素灰度值。这种表示与人们对人脸图像的解释方式有很大不同,使用这种原 始表示很难获取到面部特征。例如,如果使用像素距离,那么将人脸向右移动一个像素将 会发生巨大的变化,得到一个完全不同的表示。我们希望,使用沿着主成分方向的距离可 以提高精度。这里我们启用 PCA 的白化(whitening)选项,它将主成分缩放到相同的尺 度。变换后的结果与使用 StandardScaler 相同。再次使用图 3-3 中的数据,白化不仅对应 于旋转数据,还对应于缩放数据使其形状是圆形而不是椭圆(参见图 3-8):
In[26]:
mglearn.plots.plot_pca_whitening()
图 3-8:利用启用白化的 PCA 进行数据变换
我们对训练数据拟合 PCA 对象,并提取前 100 个主成分。然后对训练数据和测试数据进行 变换:
In[27]:
pca = PCA(n_components=100, whiten=True, random_state=0).fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
print("X_train_pca.shape: {}".format(X_train_pca.shape))
Out[27]:
X_train_pca.shape: (1547, 100)
新数据有 100 个特征,即前 100 个主成分。现在,可以对新表示使用单一最近邻分类器来 将我们的图像分类:
In[28]:
knn = KNeighborsClassifier(n_neighbors=1)
knn.fit(X_train_pca, y_train)
print("Test set accuracy: {:.2f}".format(knn.score(X_test_pca, y_test)))
Out[28]:
Test set accuracy: 0.36
我们的精度有了相当显著的提高,从 26.6% 提升到 35.7%,这证实了我们的直觉,即主成 分可能提供了一种更好的数据表示。
对于图像数据,我们还可以很容易地将找到的主成分可视化。请记住,成分对应于输入空 间里的方向。这里的输入空间是 87 像素 ×65 像素的灰度图像,所以在这个空间中的方向 也是 87 像素 ×65 像素的灰度图像。
我们来看一下前几个主成分(图 3-9):
In[29]:
print("pca.components_.shape: {}".format(pca.components_.shape))
Out[29]:
pca.components_.shape: (100, 5655)
In[30]:
fix, axes = plt.subplots(3, 5, figsize=(15, 12),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (component, ax) in enumerate(zip(pca.components_, axes.ravel())):
ax.imshow(component.reshape(image_shape),
cmap='viridis')
ax.set_title("{}. component".format((i + 1)))
图 3-9:人脸数据集前 15 个主成分的成分向量
虽然我们肯定无法理解这些成分的所有内容,但可以猜测一些主成分捕捉到了人脸图像的哪些方面。第一个主成分似乎主要编码的是人脸与背景的对比,第二个主成分编码的是人 脸左半部分和右半部分的明暗程度差异,如此等等。虽然这种表示比原始像素值的语义稍强,但它仍与人们感知人脸的方式相去甚远。由于 PCA 模型是基于像素的,因此人脸的 相对位置(眼睛、下巴和鼻子的位置)和明暗程度都对两张图像在像素表示中的相似程度 有很大影响。但人脸的相对位置和明暗程度可能并不是人们首先感知的内容。在要求人们 评价人脸的相似度时,他们更可能会使用年龄、性别、面部表情和发型等属性,而这些属 性很难从像素强度中推断出来。重要的是要记住,算法对数据(特别是视觉数据,比如人 们非常熟悉的图像)的解释通常与人类的解释方式大不相同。
不过让我们回到 PCA 的具体案例。我们对 PCA 变换的介绍是:先旋转数据,然后删除方 差较小的成分。另一种有用的解释是尝试找到一些数字(PCA 旋转后的新特征值),使我 们可以将测试点表示为主成分的加权求和(见图 3-10)。
图 3-10:图解 PCA:将图像分解为成分的加权求和
这里 x0、x1 等是这个数据点的主成分的系数,换句话说,它们是图像在旋转后的空间中的 表示。
我们还可以用另一种方法来理解 PCA 模型,就是仅使用一些成分对原始数据进行重建。 在图 3-3 中,在去掉第二个成分并来到第三张图之后,我们反向旋转并重新加上平均值, 这样就在原始空间中获得去掉第二个成分的新数据点,正如最后一张图所示。我们可以对 人脸做类似的变换,将数据降维到只包含一些主成分,然后反向旋转回到原始空间。回到 原始特征空间可以通过 inverse_transform 方法来实现。这里我们分别利用 10 个、50 个、 100 个和 500 个成分对一些人脸进行重建并将其可视化(图 3-11):
In[32]:
mglearn.plots.plot_pca_faces(X_train, X_test, image_shape)
图 3-11:利用越来越多的主成分对三张人脸图像进行重建
可以看到,在仅使用前 10 个主成分时,仅捕捉到了图片的基本特点,比如人脸方向和明 暗程度。随着使用的主成分越来越多,图像中也保留了越来越多的细节。这对应于图 3-10 的求和中包含越来越多的项。如果使用的成分个数与像素个数相等,意味着我们在旋转后 不会丢弃任何信息,可以完美重建图像。
我们还可以尝试使用 PCA 的前两个主成分,将数据集中的所有人脸在散点图中可视化 (图 3-12),其类别在图中给出。这与我们对 cancer 数据集所做的类似:
In[33]:
mglearn.discrete_scatter(X_train_pca[:, 0], X_train_pca[:, 1], y_train)
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
图 3-12:利用前两个主成分绘制人脸数据集的散点图(cancer 数据集的对应图像见图 3-5)
如你所见,如果我们只使用前两个主成分,整个数据只是一大团,看不到类别之间的分 界。这并不意外,因为即使有 10 个成分(正如图 3-11 所示),PCA 也仅捕捉到人脸非常 粗略的特征。
3.4.2 非负矩阵分解
非负矩阵分解(non-negative matrix factorization,NMF)是另一种无监督学习算法,其目 的在于提取有用的特征。它的工作原理类似于 PCA,也可以用于降维。与 PCA 相同,我 们试图将每个数据点写成一些分量的加权求和,正如图 3-10 所示。但在 PCA 中,我们想 要的是正交分量,并且能够解释尽可能多的数据方差;而在 NMF 中,我们希望分量和系 数均为非负,也就是说,我们希望分量和系数都大于或等于 0。因此,这种方法只能应用 于每个特征都是非负的数据,因为非负分量的非负求和不可能变为负值。
将数据分解成非负加权求和的这个过程,对由多个独立源相加(或叠加)创建而成的数据 特别有用,比如多人说话的音轨或包含多种乐器的音乐。在这种情况下,NMF 可以识别 出组成合成数据的原始分量。总的来说,与 PCA 相比,NMF 得到的分量更容易解释,因 为负的分量和系数可能会导致难以解释的抵消效应(cancellation effect)。举个例子,图 3-9 中的特征脸同时包含正数和负数,我们在 PCA 的说明中也提到过,正负号实际上是任 意的。在将 NMF 应用于人脸数据集之前,我们先来简要回顾一下模拟数据。
- 将 NMF 应用于模拟数据
与使用 PCA 不同,我们需要保证数据是正的,NMF 能够对数据进行操作。这说明数据相 对于原点 (0, 0) 的位置实际上对 NMF 很重要。因此,你可以将提取出来的非负分量看作是 从 (0, 0) 到数据的方向。
下面的例子(图 3-13)给出了 NMF 在二维玩具数据上的结果:
In[34]:
mglearn.plots.plot_nmf_illustration()
图 3-13:两个分量的非负矩阵分解(左)和一个分量的非负矩阵分解(右)找到的分量
对于两个分量的 NMF(如左图所示),显然所有数据点都可以写成这两个分量的正数组 合。如果有足够多的分量能够完美地重建数据(分量个数与特征个数相同),那么算法会 选择指向数据极值的方向。
如果我们仅使用一个分量,那么 NMF 会创建一个指向平均值的分量,因为指向这里可以 对数据做出最好的解释。你可以看到,与 PCA 不同,减少分量个数不仅会删除一些方向, 而且会创建一组完全不同的分量! NMF 的分量也没有按任何特定方法排序,所以不存在 “第一非负分量”:所有分量的地位平等。
NMF 使用了随机初始化,根据随机种子的不同可能会产生不同的结果。在相对简单的情 况下(比如两个分量的模拟数据),所有数据都可以被完美地解释,那么随机性的影响很 小(虽然可能会影响分量的顺序或尺度)。在更加复杂的情况下,影响可能会很大。
- 将 NMF 应用于人脸图像
现在我们将 NMF 应用于之前用过的 Wild 数据集中的 Labeled Faces。NMF 的主要参数是 我们想要提取的分量个数。通常来说,这个数字要小于输入特征的个数(否则的话,将每 个像素作为单独的分量就可以对数据进行解释)。
首先,我们来观察分量个数如何影响 NMF 重建数据的好坏(图 3-14):
In[35]:
mglearn.plots.plot_nmf_faces(X_train, X_test, image_shape)
图 3-14:利用越来越多分量的 NMF 重建三张人脸图像
反向变换的数据质量与使用 PCA 时类似,但要稍差一些。这是符合预期的,因为 PCA 找 到的是重建的最佳方向。NMF 通常并不用于对数据进行重建或编码,而是用于在数据中 寻找有趣的模式。
我们尝试仅提取一部分分量(比如 15 个),初步观察一下数据。其结果见图 3-15。
In[36]:
from sklearn.decomposition import NMF
nmf = NMF(n_components=15, random_state=0)
nmf.fit(X_train)
X_train_nmf = nmf.transform(X_train)
X_test_nmf = nmf.transform(X_test)
fix, axes = plt.subplots(3, 5, figsize=(15, 12),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (component, ax) in enumerate(zip(nmf.components_, axes.ravel())):
ax.imshow(component.reshape(image_shape))
ax.set_title("{}. component".format(i))
图 3-15:使用 15 个分量的 NMF 在人脸数据集上找到的分量
这些分量都是正的,因此比图 3-9 所示的 PCA 分量更像人脸原型。例如,你可以清楚地看 到,分量 3(component 3)显示了稍微向右转动的人脸,而分量 7(component 7)则显示 了稍微向左转动的人脸。我们来看一下这两个分量特别大的那些图像,分别如图 3-16 和图 3-17 所示。
In[37]:
compn = 3
# 按第3个分量排序,绘制前10张图像
inds = np.argsort(X_train_nmf[:, compn])[::-1]
fig, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (ind, ax) in enumerate(zip(inds, axes.ravel())):
ax.imshow(X_train[ind].reshape(image_shape))
compn = 7
# 按第7个分量排序,绘制前10张图像
inds = np.argsort(X_train_nmf[:, compn])[::-1]
fig, axes = plt.subplots(2, 5, figsize=(15, 8),
subplot_kw={'xticks': (), 'yticks': ()})
for i, (ind, ax) in enumerate(zip(inds, axes.ravel())):
ax.imshow(X_train[ind].reshape(image_shape))
图 3-16:分量 3 系数较大的人脸
图 3-17:分量 7 系数较大的人脸
正如所料,分量 3 系数较大的人脸都是向右看的人脸(图 3-16),而分量 7 系数较大的人 脸都向左看(图 3-17)。如前所述,提取这样的模式最适合于具有叠加结构的数据,包括 音频、基因表达和文本数据。我们通过一个模拟数据的例子来看一下这种用法。
假设我们对一个信号感兴趣,它是三个不同信号源合成的(图 3-18):
In[38]:
S = mglearn.datasets.make_signals()
plt.figure(figsize=(6, 1))
plt.plot(S, '-')
plt.xlabel("Time")
plt.ylabel("Signal")
图 3-18:原始信号源
不幸的是,我们无法观测到原始信号,只能观测到三个信号的叠加混合。我们想要将混合 信号分解为原始分量。假设我们有许多种不同的方法来观测混合信号(比如有 100 台测量 装置),每种方法都为我们提供了一系列测量结果。
In[39]:
# 将数据混合成100维的状态
A = np.random.RandomState(0).uniform(size=(100, 3))
X = np.dot(S, A.T)
print("Shape of measurements: {}".format(X.shape))
Out[39]:
Shape of measurements: (2000, 100)
我们可以用 NMF 来还原这三个信号:
In[40]:
nmf = NMF(n_components=3, random_state=42)
S_ = nmf.fit_transform(X)
print("Recovered signal shape: {}".format(S_.shape))
Out[40]:
Recovered signal shape: (2000, 3)
为了对比,我们也应用了 PCA:
In[41]:
pca = PCA(n_components=3)
H = pca.fit_transform(X)
图 3-19 给出了 NMF 和 PCA 发现的信号活动:
In[42]:
models = [X, S, S_, H]
names = ['Observations (first three measurements)',
'True sources',
'NMF recovered signals',
'PCA recovered signals']
fig, axes = plt.subplots(4, figsize=(8, 4), gridspec_kw={'hspace': .5},
subplot_kw={'xticks': (), 'yticks': ()})
for model, name, ax in zip(models, names, axes):
ax.set_title(name)
ax.plot(model[:, :3], '-')
图元 3-19:利用 NMF 和 PCA 还原混合信号源
图中包含来自 X 的 100 次测量中的 3 次,用于参考。可以看到,NMF 在发现原始信号源 时得到了不错的结果,而 PCA 则失败了,仅使用第一个成分来解释数据中的大部分变化。 要记住,NMF 生成的分量是没有顺序的。在这个例子中,NMF 分量的顺序与原始信号完 全相同(参见三条曲线的颜色),但这纯属偶然。
还有许多其他算法可用于将每个数据点分解为一系列固定分量的加权求和,正如 PCA 和 NMF 所做的那样。讨论所有这些算法已超出了本书的范围,而且描述对分量和系数的约 束通常要涉及概率论。如果你对这种类型的模式提取感兴趣,我们推荐你学习 scikitlearn 用户指南中关于独立成分分析(ICA)、因子分析(FA)和稀疏编码(字典学习)等 的内容,所有这些内容都可以在关于分解方法的页面中找到(http://scikit-learn.org/stable/ modules/decomposition.html)。
3.4.3 用t-SNE进行流形学习
虽然 PCA 通常是用于变换数据的首选方法,使你能够用散点图将其可视化,但这一方法 的性质(先旋转然后减少方向)限制了其有效性,正如我们在 Wild 数据集 Labeled Faces 的散点图中所看到的那样。有一类用于可视化的算法叫作流形学习算法(manifold learning algorithm),它允许进行更复杂的映射,通常也可以给出更好的可视化。其中特别有用的一 个就是 t-SNE 算法。
流形学习算法主要用于可视化,因此很少用来生成两个以上的新特征。其中一些算法(包 括 t-SNE)计算训练数据的一种新表示,但不允许变换新数据。这意味着这些算法不能用 于测试集:更确切地说,它们只能变换用于训练的数据。流形学习对探索性数据分析是很 有用的,但如果最终目标是监督学习的话,则很少使用。t-SNE 背后的思想是找到数据的 一个二维表示,尽可能地保持数据点之间的距离。t-SNE 首先给出每个数据点的随机二维 表示,然后尝试让在原始特征空间中距离较近的点更加靠近,原始特征空间中相距较远的 点更加远离。t-SNE 重点关注距离较近的点,而不是保持距离较远的点之间的距离。换句 话说,它试图保存那些表示哪些点比较靠近的信息。
我们将对 scikit-learn 包含的一个手写数字数据集 2 应用 t-SNE 流形学习算法。在这个数 据集中,每个数据点都是 0 到 9 之间手写数字的一张 8×8 灰度图像。图 3-20 给出了每个 类别的一个例子。
In[43]:
from sklearn.datasets import load_digits
digits = load_digits()
fig, axes = plt.subplots(2, 5, figsize=(10, 5),
subplot_kw={'xticks':(), 'yticks': ()})
for ax, img in zip(axes.ravel(), digits.images):
ax.imshow(img)
图 3-20:digits 数据集的示例图像
我们用 PCA 将降到二维的数据可视化。我们对前两个主成分作图,并按类别对数据点着 色(图 3-21):
In[44]:
# 构建一个PCA模型
pca = PCA(n_components=2)
pca.fit(digits.data)
# 将digits数据变换到前两个主成分的方向上
digits_pca = pca.transform(digits.data)
colors = ["#476A2A", "#7851B8", "#BD3430", "#4A2D4E", "#875525",
"#A83683", "#4E655E", "#853541", "#3A3120", "#535D8E"]
plt.figure(figsize=(10, 10))
plt.xlim(digits_pca[:, 0].min(), digits_pca[:, 0].max())
plt.ylim(digits_pca[:, 1].min(), digits_pca[:, 1].max())
for i in range(len(digits.data)):
# 将数据实际绘制成文本,而不是散点
plt.text(digits_pca[i, 0], digits_pca[i, 1], str(digits.target[i]),
color = colors[digits.target[i]],
fontdict={'weight': 'bold', 'size': 9})
plt.xlabel("First principal component")
plt.ylabel("Second principal component")
图 3-21:利用前两个主成分绘制 digits 数据集的散点图
实际上,这里我们用每个类别对应的数字作为符号来显示每个类别的位置。利用前两个主 成分可以将数字 0、6 和 4 相对较好地分开,尽管仍有重叠。大部分其他数字都大量重叠 在一起。
我们将 t-SNE 应用于同一个数据集,并对结果进行比较。由于 t-SNE 不支持变换新数据, 所以 TSNE 类没有 transform 方法。我们可以调用 fit_transform 方法来代替,它会构建模型并立刻返回变换后的数据(见图 3-22):
In[45]:
from sklearn.manifold import TSNE
tsne = TSNE(random_state=42)
# 使用fit_transform而不是fit,因为TSNE没有transform方法
digits_tsne = tsne.fit_transform(digits.data)
In[46]:
plt.figure(figsize=(10, 10))
plt.xlim(digits_tsne[:, 0].min(), digits_tsne[:, 0].max() + 1)
plt.ylim(digits_tsne[:, 1].min(), digits_tsne[:, 1].max() + 1)
for i in range(len(digits.data)):
# 将数据实际绘制成文本,而不是散点
plt.text(digits_tsne[i, 0], digits_tsne[i, 1], str(digits.target[i]),
color = colors[digits.target[i]],
fontdict={'weight': 'bold', 'size': 9})
plt.xlabel("t-SNE feature 0")
plt.xlabel("t-SNE feature 1")
图 3-22:利用 t-SNE 找到的两个分量绘制 digits 数据集的散点图
t-SNE 的结果非常棒。所有类别都被明确分开。数字 1 和 9 被分成几块,但大多数类别都 形成一个密集的组。要记住,这种方法并不知道类别标签:它完全是无监督的。但它能够 找到数据的一种二维表示,仅根据原始空间中数据点之间的靠近程度就能够将各个类别明 确分开。
t-SNE 算法有一些调节参数,虽然默认参数的效果通常就很好。你可以尝试修改 perplexity 和 early_exaggeration,但作用一般很小。
3.5 聚类
我们前面说过,聚类(clustering)是将数据集划分成组的任务,这些组叫作簇(cluster)。 其目标是划分数据,使得一个簇内的数据点非常相似且不同簇内的数据点非常不同。与分 类算法类似,聚类算法为每个数据点分配(或预测)一个数字,表示这个点属于哪个簇。
3.5.1 k均值聚类
k 均值聚类是最简单也最常用的聚类算法之一。它试图找到代表数据特定区域的簇中心 (cluster center)。算法交替执行以下两个步骤:将每个数据点分配给最近的簇中心,然后将 每个簇中心设置为所分配的所有数据点的平均值。如果簇的分配不再发生变化,那么算法 结束。下面的例子(图 3-23)在一个模拟数据集上对这一算法进行说明:
In[47]:
mglearn.plots.plot_kmeans_algorithm()
图 3-23:输入数据与 k 均值算法的三个步骤
簇中心用三角形表示,而数据点用圆形表示。颜色表示簇成员。我们指定要寻找三个簇, 所以通过声明三个随机数据点为簇中心来将算法初始化(见图中“Initialization”/“初 始化”)。然后开始迭代算法。首先,每个数据点被分配给距离最近的簇中心(见图中 “Assign Points (1)”/“分配数据点(1)”)。接下来,将簇中心修改为所分配点的平均值 (见图中“Recompute Centers (1)”/“重新计算中心(1)”)。然后将这一过程再重复两次。 在第三次迭代之后,为簇中心分配的数据点保持不变,因此算法结束。 给定新的数据点,k 均值会将其分配给最近的簇中心。下一个例子(图 3-24)展示了图 3-23 学到的簇中心的边界:
In[48]:
mglearn.plots.plot_kmeans_boundaries()
图 3-24:k 均值算法找到的簇中心和簇边界
用 scikit-learn 应用 k 均值相当简单。下面我们将其应用于上图中的模拟数据。我们将 KMeans 类实例化,并设置我们要寻找的簇个数 3 。然后对数据调用 fit 方法:
In[49]:
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
# 生成模拟的二维数据
X, y = make_blobs(random_state=1)
# 构建聚类模型
kmeans = KMeans(n_clusters=3)
kmeans.fit(X)
算法运行期间,为 X 中的每个训练数据点分配一个簇标签。你可以在 kmeans.labels_ 属性 中找到这些标签:
In[50]:
print("Cluster memberships:\n{}".format(kmeans.labels_))
Out[50]:
Cluster memberships:
[1 2 2 2 0 0 0 2 1 1 2 2 0 1 0 0 0 1 2 2 0 2 0 1 2 0 0 1 1 0 1 1 0 1 2 0 2
2 2 0 0 2 1 2 2 0 1 1 1 1 2 0 0 0 1 0 2 2 1 1 2 0 0 2 2 0 1 0 1 2 2 2 0 1
1 2 0 0 1 2 1 2 2 0 1 1 1 1 2 1 0 1 1 2 2 0 0 1 0 1]
因为我们要找的是 3 个簇,所以簇的编号是 0 到 2。
你也可以用 predict 方法为新数据点分配簇标签。预测时会将最近的簇中心分配给每个新 数据点,但现有模型不会改变。对训练集运行 predict 会返回与 labels_ 相同的结果:
In[51]:
print(kmeans.predict(X))
Out[51]:
[1 2 2 2 0 0 0 2 1 1 2 2 0 1 0 0 0 1 2 2 0 2 0 1 2 0 0 1 1 0 1 1 0 1 2 0 2
2 2 0 0 2 1 2 2 0 1 1 1 1 2 0 0 0 1 0 2 2 1 1 2 0 0 2 2 0 1 0 1 2 2 2 0 1
1 2 0 0 1 2 1 2 2 0 1 1 1 1 2 1 0 1 1 2 2 0 0 1 0 1]
可以看到,聚类算法与分类算法有些相似,每个元素都有一个标签。但并不存在真实的标 签,因此标签本身并没有先验意义。我们回到之前讨论过的人脸图像聚类的例子。聚类的 结果可能是,算法找到的第 3 个簇仅包含你朋友 Bela 的面孔。但只有在查看图片之后才能 知道这一点,而且数字 3 是任意的。算法给你的唯一信息就是所有标签为 3 的人脸都是相 似的。
对于我们刚刚在二维玩具数据集上运行的聚类算法,这意味着我们不应该为其中一组的标 签是 0、另一组的标签是 1 这一事实赋予任何意义。再次运行该算法可能会得到不同的簇 编号,原因在于初始化的随机性质。
下面又给出了这个数据的图像(图 3-25)。簇中心被保存在 clustercenters 属性中,我 们用三角形表示它们:
In[52]:
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)
图 3-25:3 个簇的 k 均值算法找到的簇分配和簇中心
我们也可以使用更多或更少的簇中心(图 3-26):
In[53]:
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])
图 3-26:使用 2 个簇(左)和 5 个簇(右)的 k 均值算法找到的簇分配
- k均值的失败案例
即使你知道给定数据集中簇的“正确”个数,k 均值可能也不是总能找到它们。每个簇仅 由其中心定义,这意味着每个簇都是凸形(convex)。因此,k 均值只能找到相对简单的形 状。k 均值还假设所有簇在某种程度上具有相同的“直径”,它总是将簇之间的边界刚好画 在簇中心的中间位置。有时这会导致令人惊讶的结果,如图 3-27 所示:
In[54]:
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")
图 3-27:簇的密度不同时,k 均值找到的簇分配
你可能会认为,左下方的密集区域是第一个簇,右上方的密集区域是第二个,中间密度较 小的区域是第三个。但事实上,簇 0 和簇 1 都包含一些远离簇中其他点的点。
k 均值还假设所有方向对每个簇都同等重要。图 3-28 显示了一个二维数据集,数据中包含 明确分开的三部分。但是这三部分被沿着对角线方向拉长。由于 k 均值仅考虑到最近簇中 心的距离,所以它无法处理这种类型的数据:
In[55]:
# 生成一些随机分组数据
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")
图 3-28:k 均值无法识别非球形簇
如果簇的形状更加复杂,比如我们在第 2 章遇到的 two_moons 数据,那么 k 均值的表现也 很差(见图 3-29):
In[56]:
# 生成模拟的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")
图 3-29:k 均值无法识别具有复杂形状的簇
这里我们希望聚类算法能够发现两个半月形。但利用 k 均值算法是不可能做到这一点的。
- 矢量量化,或者将k均值看作分解
虽然 k 均值是一种聚类算法,但在 k 均值和分解方法(比如之前讨论过的 PCA 和 NMF) 之间存在一些有趣的相似之处。你可能还记得,PCA 试图找到数据中方差最大的方向,而 NMF 试图找到累加的分量,这通常对应于数据的“极值”或“部分”(见图 3-13)。两种 方法都试图将数据点表示为一些分量之和。与之相反,k 均值则尝试利用簇中心来表示每 个数据点。你可以将其看作仅用一个分量来表示每个数据点,该分量由簇中心给出。这种 观点将 k 均值看作是一种分解方法,其中每个点用单一分量来表示,这种观点被称为矢量 量化(vector quantization)。
我们来并排比较 PCA、NMF 和 k 均值,分别显示提取的分量(图 3-30),以及利用 100 个 分量对测试集中人脸的重建(图 3-31)。对于 k 均值,重建就是在训练集中找到的最近的 簇中心:
In[57]:
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_)
In[58]:
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")
图 3-30:对比 k 均值的簇中心与 PCA 和 NMF 找到的分量
图 3-31:利用 100 个分量(或簇中心)的 k 均值、PCA 和 NMF 的图像重建的对比——k 均值的每 张图像中仅使用了一个簇中心
利用 k 均值做矢量量化的一个有趣之处在于,可以用比输入维度更多的簇来对数据进行编 码。让我们回到 two_moons 数据。利用 PCA 或 NMF,我们对这个数据无能为力,因为它 只有两个维度。使用 PCA 或 NMF 将其降到一维,将会完全破坏数据的结构。但通过使用 更多的簇中心,我们可以用 k 均值找到一种更具表现力的表示(见图 3-32):
In[59]:
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))
Out[59]:
Cluster memberships:
[9 2 5 4 2 7 9 6 9 6 1 0 2 6 1 9 3 0 3 1 7 6 8 6 8 5 2 7 5 8 9 8 6 5 3 7 0
9 4 5 0 1 3 5 2 8 9 1 5 6 1 0 7 4 6 3 3 6 3 8 0 4 2 9 6 4 8 2 8 4 0 4 0 5
6 4 5 9 3 0 7 8 0 7 5 8 9 8 0 7 3 9 7 1 7 2 2 0 4 5 6 7 8 9 4 5 4 1 2 3 1
8 8 4 9 2 3 7 0 9 9 1 5 8 5 1 9 5 6 7 9 1 4 0 6 2 6 4 7 9 5 5 3 8 1 9 5 6
3 5 0 2 9 3 0 8 6 0 3 3 5 6 3 2 0 2 3 0 2 6 3 4 4 1 5 6 7 1 1 3 2 4 7 2 7
3 8 6 4 1 4 3 9 9 5 1 7 5 8 2]
图 3-32:利用 k 均值的许多簇来表示复杂数据集中的变化
我们使用了 10 个簇中心,也就是说,现在每个点都被分配了 0 到 9 之间的一个数字。我们 可以将其看作 10 个分量表示的数据(我们有 10 个新特征),只有表示该点对应的簇中心的 那个特征不为 0,其他特征均为 0。利用这个 10 维表示,现在可以用线性模型来划分两个 半月形,而利用原始的两个特征是不可能做到这一点的。将到每个簇中心的距离作为特征, 还可以得到一种表现力更强的数据表示。可以利用 kmeans 的 transform 方法来完成这一点:
In[60]:
distance_features = kmeans.transform(X)
print("Distance feature shape: {}".format(distance_features.shape))
print("Distance features:\n{}".format(distance_features))
Out[60]:
Distance feature shape: (200, 10)
Distance features:
[[ 0.922 1.466 1.14 ..., 1.166 1.039 0.233]
[ 1.142 2.517 0.12 ..., 0.707 2.204 0.983]
[ 0.788 0.774 1.749 ..., 1.971 0.716 0.944]
...,
[ 0.446 1.106 1.49 ..., 1.791 1.032 0.812]
[ 1.39 0.798 1.981 ..., 1.978 0.239 1.058]
[ 1.149 2.454 0.045 ..., 0.572 2.113 0.882]]
k 均值是非常流行的聚类算法,因为它不仅相对容易理解和实现,而且运行速度也相对较 快。k 均值可以轻松扩展到大型数据集,scikit-learn 甚至在 MiniBatchKMeans 类中包含了 一种更具可扩展性的变体,可以处理非常大的数据集。
k 均值的缺点之一在于,它依赖于随机初始化,也就是说,算法的输出依赖于随机种子。 默认情况下,scikit-learn 用 10 种不同的随机初始化将算法运行 10 次,并返回最佳结果。 k 均值还有一个缺点,就是对簇形状的假设的约束性较强,而且还要求指定所要寻找 的簇的个数(在现实世界的应用中可能并不知道这个数字)。 接下来,我们将学习另外两种聚类算法,它们都在某些方面对这些性质做了改进。
3.5.2 凝聚聚类
凝聚聚类(agglomerative clustering)指的是许多基于相同原则构建的聚类算法,这一原则 是:算法首先声明每个点是自己的簇,然后合并两个最相似的簇,直到满足某种停止准则 为止。scikit-learn 中实现的停止准则是簇的个数,因此相似的簇被合并,直到仅剩下指 定个数的簇。还有一些链接(linkage)准则,规定如何度量“最相似的簇”。这种度量总 是定义在两个现有的簇之间。
scikit-learn 中实现了以下三种选项。
ward
默认选项。ward 挑选两个簇来合并,使得所有簇中的方差增加最小。这通常会得到大 小差不多相等的簇。
average
链接将簇中所有点之间平均距离最小的两个簇合并。
complete
链接(也称为最大链接)将簇中点之间最大距离最小的两个簇合并。
ward 适用于大多数数据集,在我们的例子中将使用它。如果簇中的成员个数非常不同(比 如其中一个比其他所有都大得多),那么 average 或 complete 可能效果更好。
图 3-33 给出了在一个二维数据集上的凝聚聚类过程,要寻找三个簇。
In[61]:
mglearn.plots.plot_agglomerative_algorithm()
图 3-33:凝聚聚类用迭代的方式合并两个最近的簇
最开始,每个点自成一簇。然后在每一个步骤中,相距最近的两个簇被合并。在前四个步 骤中,选出两个单点簇并将其合并成两点簇。在步骤 5(Step 5)中,其中一个两点簇被 扩展到三个点,以此类推。在步骤 9(Step 9)中,只剩下 3 个簇。由于我们指定寻找 3 个 簇,因此算法结束。
我们来看一下凝聚聚类对我们这里使用的简单三簇数据的效果如何。由于算法的工作原 理,凝聚算法不能对新数据点做出预测。因此 AgglomerativeClustering 没有 predict 方 法。为了构造模型并得到训练集上簇的成员关系,可以改用 fit_predict 方法。5 结果如图 3-34 所示。
In[62]:
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")
图 3-34:使用 3 个簇的凝聚聚类的簇分配
正如所料,算法完美地完成了聚类。虽然凝聚聚类的 scikit-learn 实现需要你指定希望 算法找到的簇的个数,但凝聚聚类方法为选择正确的个数提供了一些帮助,我们将在下 面讨论。
- 层次聚类与树状图
凝聚聚类生成了所谓的层次聚类(hierarchical clustering)。聚类过程迭代进行,每个点都从一个单点簇变为属于最终的某个簇。每个中间步骤都提供了数据的一种聚类(簇的个数 也不相同)。有时候,同时查看所有可能的聚类是有帮助的。下一个例子(图 3-35)叠加 显示了图 3-33 中所有可能的聚类,有助于深入了解每个簇如何分解为较小的簇:
In[63]:
mglearn.plots.plot_agglomerative()
图 3-35:凝聚聚类生成的层次化的簇分配(用线表示)以及带有编号的数据点(参见图 3-36)
虽然这种可视化为层次聚类提供了非常详细的视图,但它依赖于数据的二维性质,因此不 能用于具有两个以上特征的数据集。但还有另一个将层次聚类可视化的工具,叫作树状图 (dendrogram),它可以处理多维数据集。
不幸的是,目前 scikit-learn 没有绘制树状图的功能。但你可以利用 SciPy 轻松生成树状 图。SciPy 的聚类算法接口与 scikit-learn 的聚类算法稍有不同。SciPy 提供了一个函数, 接受数据数组 X 并计算出一个链接数组(linkage array),它对层次聚类的相似度进行编码。 然后我们可以将这个链接数组提供给 scipy 的 dendrogram 函数来绘制树状图(图 3-36)。
In[64]:
# 从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")
图 3-36:图 3-35 中聚类的树状图(用线表示划分成两个簇和三个簇)
树状图在底部显示数据点(编号从 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 可以解决这个问题。
3.5.3 DBSCAN
另一个非常有用的聚类算法是 DBSCAN(density-based spatial clustering of applications with noise,即“具有噪声的基于密度的空间聚类应用”)。DBSCAN 的主要优点是它不需要用户先验地设置簇的个数,可以划分具有复杂形状的簇,还可以找出不属于任何簇的点。 DBSCAN 比凝聚聚类和 k 均值稍慢,但仍可以扩展到相对较大的数据集。
DBSCAN 的原理是识别特征空间的“拥挤”区域中的点,在这些区域中许多数据点靠近在 一起。这些区域被称为特征空间中的密集(dense)区域。DBSCAN 背后的思想是,簇形 成数据的密集区域,并由相对较空的区域分隔开。
在密集区域内的点被称为核心样本(core sample,或核心点),它们的定义如下。DBSCAN 有两个参数:minsamples 和 eps。如果在距一个给定数据点 eps 的距离内至少有 min samples 个数据点,那么这个数据点就是核心样本。DBSCAN 将彼此距离小于 eps 的核心 样本放到同一个簇中。
算法首先任意选取一个点,然后找到到这个点的距离小于等于 eps 的所有的点。如果 距起始点的距离在 eps 之内的数据点个数小于 minsamples,那么这个点被标记为噪 声(noise),也就是说它不属于任何簇。如果距离在 eps 之内的数据点个数大于 min samples,则这个点被标记为核心样本,并被分配一个新的簇标签。然后访问该点的所有 邻居(在距离 eps 以内)。如果它们还没有被分配一个簇,那么就将刚刚创建的新的簇标 签分配给它们。如果它们是核心样本,那么就依次访问其邻居,以此类推。簇逐渐增大, 直到在簇的 eps 距离内没有更多的核心样本为止。然后选取另一个尚未被访问过的点, 并重复相同的过程。
最后,一共有三种类型的点:核心点、与核心点的距离在 eps 之内的点(叫作边界点, boundary point)和噪声。如果 DBSCAN 算法在特定数据集上多次运行,那么核心点的聚 类始终相同,同样的点也始终被标记为噪声。但边界点可能与不止一个簇的核心样本相 邻。因此,边界点所属的簇依赖于数据点的访问顺序。一般来说只有很少的边界点,这种 对访问顺序的轻度依赖并不重要。
我们将 DBSCAN 应用于演示凝聚聚类的模拟数据集。与凝聚聚类类似,DBSCAN 也不允 许对新的测试数据进行预测,所以我们将使用 fit_predict 方法来执行聚类并返回簇标签。
In[65]:
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))
Out[65]:
Cluster memberships:
[-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
如你所见,所有数据点都被分配了标签 -1,这代表噪声。这是 eps 和 min_samples 默认参 数设置的结果,对于小型的玩具数据集并没有调节这些参数。min_samples 和 eps 取不同值 时的簇分类如下所示,其可视化结果见图 3-37。
In[66]:
mglearn.plots.plot_dbscan()
Out[66]:
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]
图 3-37:在 min_samples 和 eps 参数不同取值的情况下,DBSCAN 找到的簇分配
在这张图中,属于簇的点是实心的,而噪声点则显示为空心的。核心样本显示为较大的标 记,而边界点则显示为较小的标记。增大 eps(在图中从左到右),更多的点会被包含在一 个簇中。这让簇变大,但可能也会导致多个簇合并成一个。增大 min_samples(在图中从 上到下),核心点会变得更少,更多的点被标记为噪声。
参数 eps 在某种程度上更加重要,因为它决定了点与点之间“接近”的含义。将 eps 设置 得非常小,意味着没有点是核心样本,可能会导致所有点都被标记为噪声。将 eps 设置得 非常大,可能会导致所有点形成单个簇。
设置 min_samples 主要是为了判断稀疏区域内的点被标记为异常值还是形成自己的簇。如 果增大 min_samples,任何一个包含少于 min_samples 个样本的簇现在将被标记为噪声。因 此,min_samples 决定簇的最小尺寸。在图 3-37 中 eps=1.5 时,从 minsamples=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 的结果。利用默认设置,算法找到了 两个半圆形并将其分开:
In[67]:
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")
图 3-38:利用默认值 eps=0.5 的 DBSCAN 找到的簇分配
由于算法找到了我们想要的簇的个数(2 个),因此参数设置的效果似乎很好。如果将 eps 减小到 0.2(默认值为 0.5),我们将会得到 8 个簇,这显然太多了。将 eps 增大到 0.7 则 会导致只有一个簇。
在使用 DBSCAN 时,你需要谨慎处理返回的簇分配。如果使用簇标签对另一个数据进行 索引,那么使用 -1 表示噪声可能会产生意料之外的结果。
3.5.4 聚类算法的对比与评估
在应用聚类算法时,其挑战之一就是很难评估一个算法的效果好坏,也很难比较不同算法 的结果。在讨论完 k 均值、凝聚聚类和 DBSCAN 背后的算法之后,下面我们将在一些现 实世界的数据集上比较它们。
- 用真实值评估聚类
有一些指标可用于评估聚类算法相对于真实聚类的结果,其中最重要的是调整 rand 指数 (adjusted rand index,ARI)和归一化互信息(normalized mutual information,NMI),二者 都给出了定量的度量,其最佳值为 1,0 表示不相关的聚类(虽然 ARI 可以取负值)。
下面我们使用 ARI 来比较 k 均值、凝聚聚类和 DBSCAN 算法。为了对比,我们还添加了 将点随机分配到两个簇中的图像(见图 3-39)。
In[68]:
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)))
图 3-39:利用监督 ARI 分数在 two_moons
数据集上比较随机分配、k 均值、凝聚聚类和 DBSCAN
调整 rand 指数给出了符合直觉的结果,随机簇分配的分数为 0,而 DBSCAN(完美地找到 了期望中的聚类)的分数为 1。
用这种方式评估聚类时,一个常见的错误是使用 accuracy_score 而不是 adjustedrand score、normalized_mutual_info_score 或其他聚类指标。使用精度的问题在于,它要求分 配的簇标签与真实值完全匹配。但簇标签本身毫无意义——唯一重要的是哪些点位于同一 个簇中。
In[69]:
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)))
# 调整rand分数为1,因为二者聚类完全相同
print("ARI: {:.2f}".format(adjusted_rand_score(clusters1, clusters2)))
Out[69]:
Accuracy: 0.00
ARI: 1.00
- 在没有真实值的情况下评估聚类
我们刚刚展示了一种评估聚类算法的方法,但在实践中,使用诸如 ARI 之类的指标有一个 很大的问题。在应用聚类算法时,通常没有真实值来比较结果。如果我们知道了数据的正 确聚类,那么可以使用这一信息构建一个监督模型(比如分类器)。因此,使用类似 ARI 和 NMI 的指标通常仅有助于开发算法,但对评估应用是否成功没有帮助。
有一些聚类的评分指标不需要真实值,比如轮廓系数(silhouette coeffcient)。但它们在实 践中的效果并不好。轮廓分数计算一个簇的紧致度,其值越大越好,最高分数为 1。虽然 紧致的簇很好,但紧致度不允许复杂的形状。
下面是一个例子,利用轮廓分数在 two_moons 数据集上比较 k 均值、凝聚聚类和 DBSCAN (图 3-40):
In[70]:
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)))
图 3-40:利用无监督的轮廓分数在 two_moons 数据集上比较随机分配、k 均值、凝聚聚类和 DBSCAN(更符合直觉的 DBSCAN 的轮廓分数低于 k 均值找到的分配)
如你所见,k 均值的轮廓分数最高,尽管我们可能更喜欢 DBSCAN 的结果。对于评估聚 类,稍好的策略是使用基于鲁棒性的(robustness-based)聚类指标。这种指标先向数据中 添加一些噪声,或者使用不同的参数设定,然后运行算法,并对结果进行比较。其思想 是,如果许多算法参数和许多数据扰动返回相同的结果,那么它很可能是可信的。不幸的 是,在写作本书时,scikit-learn 还没有实现这一策略。
即使我们得到一个鲁棒性很好的聚类或者非常高的轮廓分数,但仍然不知道聚类中是否有任何语义含义,或者聚类是否反映了数据中我们感兴趣的某个方面。我们回到人脸图像的 例子。我们希望找到类似人脸的分组,比如男人和女人、老人和年轻人,或者有胡子的人 和没胡子的人。假设我们将数据分为两个簇,关于哪些点应该被聚类在一起,所有算法的 结果一致。我们仍不知道找到的簇是否以某种方式对应于我们感兴趣的概念。算法找到的 可能是侧视图和正面视图、夜间拍摄的照片和白天拍摄的照片,或者 iPhone 拍摄的照片和 安卓手机拍摄的照片。要想知道聚类是否对应于我们感兴趣的内容,唯一的办法就是对簇 进行人工分析。
- 在人脸数据集上比较算法
我们将 k 均值、DBSCAN 和凝聚聚类算法应用于 Wild 数据集中的 Labeled Faces,并查 看它们是否找到了有趣的结构。我们将使用数据的特征脸表示,它由包含 100 个成分的 PCA(whiten=True) 生成:
In[71]:
# 从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:
In[72]:
# 应用默认参数的DBSCAN
dbscan = DBSCAN()
labels = dbscan.fit_predict(X_pca)
print("Unique labels: {}".format(np.unique(labels)))
Out[72]:
Unique labels: [-1]
我们看到,所有返回的标签都是 -1,因此所有数据都被 DBSCAN 标记为“噪声”。我们可 以改变两个参数来改进这一点:第一,我们可以增大 eps,从而扩展每个点的邻域;第二, 我们可以减小 min_samples,从而将更小的点组视为簇。我们首先尝试改变 min_samples:
In[73]:
dbscan = DBSCAN(min_samples=3)
labels = dbscan.fit_predict(X_pca)
print("Unique labels: {}".format(np.unique(labels)))
Out[73]:
Unique labels: [-1]
即使仅考虑由三个点构成的组,所有点也都被标记为噪声。因此我们需要增大 eps:
In[74]:
dbscan = DBSCAN(min_samples=3, eps=15)
labels = dbscan.fit_predict(X_pca)
print("Unique labels: {}".format(np.unique(labels)))
Out[74]:
Unique labels: [-1 0]
使用更大的 eps(其值为 15),我们只得到了单一簇和噪声点。我们可以利用这一结果找出 “噪声”相对于其他数据的形状。为了进一步理解发生的事情,我们查看有多少点是噪声, 有多少点在簇内:
In[75]:
# 计算所有簇中的点数和噪声中的点数。
# bincount不允许负值,所以我们需要加1。
# 结果中的第一个数字对应于噪声点。
print("Number of points per cluster: {}".format(np.bincount(labels + 1)))
Out[75]:
Number of points per cluster: [ 27 2036]
噪声点非常少——只有 27 个,因此我们可以查看所有的噪声点(见图 3-41):
In[76]:
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)
图 3-41:人脸数据集中被 DBSCAN 标记为噪声的样本
将这些图像与图 3-7 中随机选择的人脸图像样本进行比较,我们可以猜测它们被标记为 噪声的原因:第 1 行第 5 张图像显示一个人正在用玻璃杯喝水,还有人戴帽子的图像, 在最后一张图像中,人脸前面有一只手。其他图像都包含奇怪的角度,或者太近或太宽 的剪切。
这 种 类 型 的 分 析 —— 尝 试 找 出“ 奇 怪 的 那 一 个 ” —— 被 称 为 异常值检测(outlier detection)。如果这是一个真实的应用,那么我们可能会尝试更好地裁切图像,以得到更加 均匀的数据。对于照片中的人有时戴着帽子、喝水或在面前举着某物,我们能做的事情很 少。但需要知道它们是数据中存在的问题,我们应用任何算法都需要解决这些问题。
如果我们想要找到更有趣的簇,而不是一个非常大的簇,那么需要将 eps 设置得更小,取 值在 15 和 0.5(默认值)之间。我们来看一下 eps 不同取值对应的结果:
In[77]:
for eps in [1, 3, 5, 7, 9, 11, 13]:
print("\neps={}".format(eps))
dbscan = DBSCAN(eps=eps, min_samples=3)
labels = dbscan.fit_predict(X_pca)
print("Clusters present: {}".format(np.unique(labels)))
print("Cluster sizes: {}".format(np.bincount(labels + 1)))
Out[77]:
eps=1
Clusters present: [-1]
Cluster sizes: [2063]
eps=3
Clusters present: [-1]
Cluster sizes: [2063]
eps=5
Clusters present: [-1]
Cluster sizes: [2063]
eps=7
Clusters present: [-1 0 1 2 3 4 5 6 7 8 9 10 11 12]
Cluster sizes: [2006 4 6 6 6 9 3 3 4 3 3 3 3 4]
eps=9
Clusters present: [-1 0 1 2]
Cluster sizes: [1269 788 3 3]
eps=11
Clusters present: [-1 0]
Cluster sizes: [ 430 1633]
eps=13
Clusters present: [-1 0]
Cluster sizes: [ 112 1951]
对于较小的 eps,所有点都被标记为噪声。eps=7 时,我们得到许多噪声点和许多较小的 簇。eps=9 时,我们仍得到许多噪声点,但我们得到了一个较大的簇和一些较小的簇。从 eps=11 开始,我们仅得到一个较大的簇和噪声。
有趣的是,较大的簇从来没有超过一个。最多有一个较大的簇包含大多数点,还有一些较 小的簇。这表示数据中没有两类或三类非常不同的人脸图像,而是所有图像或多或少地都 与其他图像具有相同的相似度(或不相似度)。
eps=7 的结果看起来最有趣,它有许多较小的簇。我们可以通过将 13 个较小的簇中的点全 部可视化来深入研究这一聚类(图 3-42):
In[78]:
dbscan = DBSCAN(min_samples=3, eps=7)
labels = dbscan.fit_predict(X_pca)
for cluster in range(max(labels) + 1):
mask = labels == cluster
n_images = np.sum(mask)
fig, axes = plt.subplots(1, n_images, figsize=(n_images * 1.5, 4),
subplot_kw={'xticks': (), 'yticks': ()})
for image, label, ax in zip(X_people[mask], y_people[mask], axes):
ax.imshow(image.reshape(image_shape), vmin=0, vmax=1)
ax.set_title(people.target_names[label].split()[-1])
图 3-42:eps=7 的 DBSCAN 找到的簇
有一些簇对应于(这个数据集中)脸部非常不同的人,比如 Sharon(沙龙)或 Koizumi(小泉)。在每个簇内,人脸方向和面部表情也是固定的。有些簇中包含多个人的面孔,但 他们的方向和表情都相似。
这就是我们将 DBSCAN 算法应用于人脸数据集的分析结论。如你所见,我们这里进行了 人工分析,不同于监督学习中基于 R2 分数或精度的更为自动化的搜索方法。
下面我们将继续应用 k 均值和凝聚聚类。
用 k 均值分析人脸数据集。我们看到,利用 DBSCAN 无法创建多于一个较大的簇。凝聚 聚类和 k 均值更可能创建均匀大小的簇,但我们需要设置簇的目标个数。我们可以将簇的 数量设置为数据集中的已知人数,虽然无监督聚类算法不太可能完全找到它们。相反,我 们可以首先设置一个比较小的簇的数量,比如 10 个,这样我们可以分析每个簇:
In[79]:
# 用k均值提取簇
km = KMeans(n_clusters=10, random_state=0)
labels_km = km.fit_predict(X_pca)
print("Cluster sizes k-means: {}".format(np.bincount(labels_km)))
Out[79]:
Cluster sizes k-means: [269 128 170 186 386 222 237 64 253 148]
如你所见,k 均值聚类将数据划分为大小相似的簇,其大小在 64 和 386 之间。这与 DBSCAN 的结果非常不同。
我们可以通过将簇中心可视化来进一步分析 k 均值的结果(图 3-43)。由于我们是在 PCA 生成的表示中进行聚类,因此我们需要使用 pca.inverse_transform 将簇中心旋转回到原 始空间并可视化:
In[80]:
fig, axes = plt.subplots(2, 5, subplot_kw={'xticks': (), 'yticks': ()},
figsize=(12, 4))
for center, ax in zip(km.cluster_centers_, axes.ravel()):
ax.imshow(pca.inverse_transform(center).reshape(image_shape),
vmin=0, vmax=1)
图 3-43:将簇的数量设置为 10 时,k 均值找到的簇中心
k 均值找到的簇中心是非常平滑的人脸。这并不奇怪,因为每个簇中心都是 64 到 386 张人 脸图像的平均。使用降维的 PCA 表示,可以增加图像的平滑度(对比图 3-11 中利用 100个 PCA 维度重建的人脸)。聚类似乎捕捉到人脸的不同方向、不同表情(第 3 个簇中心似 乎显示的是一张笑脸),以及是否有衬衫领子(见倒数第二个簇中心)。
图 3-44 给出了更详细的视图,我们对每个簇中心给出了簇中 5 张最典型的图像(该簇中与 簇中心距离最近的图像)与 5 张最不典型的图像(该簇中与簇中心距离最远的图像):
In[81]:
mglearn.plots.plot_kmeans_faces(km, pca, X_pca, X_people,
y_people, people.target_names)
图 3-44:k 均值为每个簇找到的样本图像——簇中心在最左边,然后是五个距中心最近的点,然后 是五个距该簇距中心最远的点
图 3-44 证实了我们认为第 3 个簇是笑脸的直觉,也证实了其他簇中方向的重要性。不过 “非典型的”点与簇中心不太相似,而且它们的分配似乎有些随意。这可以归因于以下事 实:k 均值对所有数据点进行划分,不像 DBSCAN 那样具有“噪声”点的概念。利用更多 数量的簇,算法可以找到更细微的区别。但添加更多的簇会使得人工检查更加困难。
用凝聚聚类分析人脸数据集。下面我们来看一下凝聚聚类的结果:
In[82]:
# 用ward凝聚聚类提取簇
agglomerative = AgglomerativeClustering(n_clusters=10)
labels_agg = agglomerative.fit_predict(X_pca)
print("Cluster sizes agglomerative clustering: {}".format(
np.bincount(labels_agg)))
Out[82]:
Cluster sizes agglomerative clustering: [255 623 86 102 122 199 265 26 230 155]
凝聚聚类生成的也是大小相近的簇,其大小在 26 和 623 之间。这比 k 均值生成的簇更不 均匀,但比 DBSCAN 生成的簇要更加均匀。
我们可以通过计算 ARI 来度量凝聚聚类和 k 均值给出的两种数据划分是否相似:
In[83]:
print("ARI: {:.2f}".format(adjusted_rand_score(labels_agg, labels_km)))
Out[83]:
ARI: 0.13
ARI 只有 0.13,说明 labels_agg 和 labels_km 这两种聚类的共同点很少。这并不奇怪,原 因在于以下事实:对于 k 均值,远离簇中心的点似乎没有什么共同点。
下面,我们可能会想要绘制树状图(图 3-45)。我们将限制图中树的深度,因为如果分支 到 2063 个数据点,图像将密密麻麻无法阅读:
In[84]:
linkage_array = ward(X_pca)
# 现在我们为包含簇之间距离的linkage_array绘制树状图
plt.figure(figsize=(20, 5))
dendrogram(linkage_array, p=7, truncate_mode='level', no_labels=True)
plt.xlabel("Sample index")
plt.ylabel("Cluster distance")
图 3-45:凝聚聚类在人脸数据集上的树状图
要想创建 10 个簇,我们在顶部有 10 条竖线的位置将树横切。在图 3-36 所示的玩具数据 的树状图中,你可以从分支的长度中看出,两个或三个簇就可以很好地划分数据。对于 人脸数据而言,似乎没有非常自然的切割点。有一些分支代表更为不同的组,但似乎没 有一个特别合适的簇的数量。这并不奇怪,因为 DBSCAN 的结果是试图将所有的点都聚类在一起。
我们将 10 个簇可视化,正如之前对 k 均值所做的那样(图 3-46)。请注意,在凝聚聚类中 没有簇中心的概念(虽然我们计算平均值),我们只是给出了每个簇的前几个点。我们在 第一张图像的左侧给出了每个簇中的点的数量:
In[85]:
n_clusters = 10
for cluster in range(n_clusters):
mask = labels_agg == cluster
fig, axes = plt.subplots(1, 10, subplot_kw={'xticks': (), 'yticks': ()},
figsize=(15, 8))
axes[0].set_ylabel(np.sum(mask))
for image, label, asdf, ax in zip(X_people[mask], y_people[mask],
labels_agg[mask], axes):
ax.imshow(image.reshape(image_shape), vmin=0, vmax=1)
ax.set_title(people.target_names[label].split()[-1],
fontdict={'fontsize': 9})
图 3-46:In[82] 生成的簇中的随机图像——每一行对应一个簇,左侧的数字表示每个簇中图像的数量
虽然某些簇似乎具有语义上的主题,但许多簇都太大而实际上很难是均匀的。为了得到更加均 匀的簇,我们可以再次运行算法,这次使用 40 个簇,并挑选出一些特别有趣的簇(图 3-47):
In[86]:
# 用ward凝聚聚类提取簇
agglomerative = AgglomerativeClustering(n_clusters=40)
labels_agg = agglomerative.fit_predict(X_pca)
print("cluster sizes agglomerative clustering: {}".format(np.bincount(labels_agg)))
n_clusters = 40
for cluster in [10, 13, 19, 22, 36]: # 手动挑选“有趣的”簇
mask = labels_agg == cluster
fig, axes = plt.subplots(1, 15, subplot_kw={'xticks': (), 'yticks': ()},
figsize=(15, 8))
cluster_size = np.sum(mask)
axes[0].set_ylabel("#{}: {}".format(cluster, cluster_size))
for image, label, asdf, ax in zip(X_people[mask], y_people[mask],
labels_agg[mask], axes):
ax.imshow(image.reshape(image_shape), vmin=0, vmax=1)
ax.set_title(people.target_names[label].split()[-1],
fontdict={'fontsize': 9})
for i in range(cluster_size, 15):
axes[i].set_visible(False)
Out[86]:
cluster sizes agglomerative clustering:
[ 58 80 79 40 222 50 55 78 172 28 26 34 14 11 60 66 152 27
47 31 54 5 8 56 3 5 8 18 22 82 37 89 28 24 41 40
21 10 113 69]
图 3-47:将簇的数量设置为 40 时,从凝聚聚类找到的簇中挑选的图像——左侧文本表示簇的编号 和簇中的点的总数
这里聚类挑选出的似乎是“深色皮肤且微笑”“有领子的衬衫”“微笑的女性”“萨达姆” 和“高额头”。如果进一步详细分析,我们还可以利用树状图找到这些高度相似的簇。
3.5.5 聚类方法小结
本节的内容表明,聚类的应用与评估是一个非常定性的过程,通常在数据分析的探索阶 段很有帮助。我们学习了三种聚类算法:k 均值、DBSCAN 和凝聚聚类。这三种算法都 可以控制聚类的粒度(granularity)。k 均值和凝聚聚类允许你指定想要的簇的数量,而 DBSCAN 允许你用 eps 参数定义接近程度,从而间接影响簇的大小。三种方法都可以用于 大型的现实世界数据集,都相对容易理解,也都可以聚类成多个簇。
每种算法的优点稍有不同。k 均值可以用簇的平均值来表示簇。它还可以被看作一种分解 方法,每个数据点都由其簇中心表示。DBSCAN 可以检测到没有分配任何簇的“噪声点”, 还可以帮助自动判断簇的数量。与其他两种方法不同,它允许簇具有复杂的形状,正如我 们在 two_moons 的例子中所看到的那样。DBSCAN 有时会生成大小差别很大的簇,这可能 是它的优点,也可能是缺点。凝聚聚类可以提供数据的可能划分的整个层次结构,可以通 过树状图轻松查看。
3.6 小结与展望
本章介绍了一系列无监督学习算法,可用于探索性数据分析和预处理。找到数据的正确表 示对于监督学习和无监督学习的成功通常都至关重要,预处理和分解方法在数据准备中具 有重要作用。
分解、流形学习和聚类都是加深数据理解的重要工具,在没有监督信息的情况下,也是理 解数据的仅有的方法。即使是在监督学习中,探索性工具对于更好地理解数据性质也很重 要。通常来说,很难量化无监督算法的有用性,但这不应该妨碍你使用它们来深入理解数 据。学完这些方法,你就已经掌握了机器学习从业者每天使用的所有必要的学习算法。
我们建议你在 scikit-learn 中包含的二维玩具数据和现实世界数据集(比如 digits、iris 和 cancer 数据集)上尝试聚类和分解方法。
转载请注明:xuhss » Python机器学习基础篇三《无监督学习与预处理》