使用Scikit-Learn的Python多维缩放指南

898 阅读8分钟

简介

在本指南中,我们将深入研究一种降维数据嵌入数据可视化技术,即多维缩放(MDS)

我们将利用Scikit-Learn来执行多维缩放,因为它有一个非常简单而强大的API。在整个指南中,我们将使用AT&T的Olivetti面孔数据集来说明数据在低维空间的嵌入。

在本指南结束时,你将对多维缩放有一个牢固的掌握,以及它的超参数和它们是如何影响该技术的。

什么是多维缩放?

MDS是一种非线性技术,用于将数据嵌入低维空间

它将居住在高维空间的点映射到低维空间,同时尽可能地保留这些点之间的距离。正因为如此,低维空间中各点之间的成对距离与它们的实际距离密切匹配。

下图是一个可能将点从三维空间映射到二维和一维空间的例子。三维空间中三个点的成对距离在二维空间中完全保留,但在一维空间中却没有保留。如果我们运行MDS,它将确保实际的成对距离和被映射点的成对距离之间的差异最小。

illustration of multidimensional scaling

MDS可以作为分类和回归问题中降维的一个预处理步骤。

MDS不仅是一种有效的降维技术,也是数据可视化的有效技术。它在低维空间中保持了高维数据的相同聚类和模式,所以你可以把,比如说,一个5维的数据集熬成一个3维的数据集,你可以更容易和自然地解释。

通常,MDS中使用的距离度量是欧几里得距离,然而,在应用MDS时,可以使用任何其他合适的差异度量。

有两种主要的方法来实现MDS。

  • 公制MDS/经典MDS:这个版本的MDS旨在尽可能地保留成对的距离/异同度量。
  • 非度量MDS:这种方法适用于只知道异同度量的等级的情况下。然后,MDS对对象进行映射,以便尽可能地保留等级。

用Scikit-Learn在Python中执行多维缩放法

Scikit-Learn库的sklearn.manifold 模块实现了流形学习和数据嵌入技术。我们将使用该模块的MDS 类。嵌入是通过以下算法确定的 ***使用主要化的压力最小化(SMACOF)***算法确定。设置MDS 对象的一些重要参数是(这并不是一个详尽的列表)。

  • n_components:将点映射到的维数。默认值是2。
  • metric:一个布尔变量,公制MDS的默认值为True ,非公制MDS的默认值为False
  • dissimilarity:默认值是euclidean ,指定欧几里得的对等距离。另一个可能的值是precomputed 。使用precomputed 需要计算成对距离矩阵,并将此矩阵作为fit()fit_transform() 函数的输入。

与一个MDS 对象相关的四个属性是。

  • embedding_:新空间中的点的位置。
  • stress_:MDS中使用的拟合度统计。
  • dissimilarity_matrix_:成对距离/相似度的矩阵。
  • n_iter_:与最佳拟合度有关的迭代次数。

像所有其他用于降维的scikit-learnMDS 类也实现了fit()fit_transform() 方法。

一个简单的插图

在本节中,我们用一个非常简单的例子来说明如何应用MDS。我们先来添加导入部分。

from sklearn.manifold import MDS
from matplotlib import pyplot as plt
import sklearn.datasets as dt
import seaborn as sns         
import numpy as np
from sklearn.metrics.pairwise import manhattan_distances, euclidean_distances
from matplotlib.offsetbox import OffsetImage, AnnotationBbox

下面的代码设置了一个MDS 对象并调用其方法fit_transform() 。这个方法返回二维空间中的嵌入点。让我们打印一下所产生的映射。

X = np.array([[0, 0, 0], [0, 0, 1], [1, 1, 1], [0, 1, 0], [0, 1, 1]])
mds = MDS(random_state=0)
X_transform = mds.fit_transform(X)
print(X_transform)
[[ 0.72521687  0.52943352]
 [ 0.61640884 -0.48411805]
 [-0.9113603  -0.47905115]
 [-0.2190564   0.71505714]
 [-0.21120901 -0.28132146]]

由于嵌入是根据应力最小化算法创建的,我们也可以看看stress 这个变量。

stress = mds.stress_
print(stress)

这样的结果是:

0.18216844548575467

另一种应用MDS的方法是通过构建一个距离矩阵,并直接对这个矩阵应用MDS,如下图代码所示。当需要使用欧氏距离以外的距离测量时,这种方法很有用。下面的代码计算了成对的 曼哈顿的距离(也称为城市街区距离或L1距离)并通过MDS对数据进行转换。

注意dissimilarity 参数已被设置为precomputed

dist_manhattan = manhattan_distances(X)
mds = MDS(dissimilarity='precomputed', random_state=0)
# Get the embeddings
X_transform_L1 = mds.fit_transform(dist_manhattan)

这样的结果是:

[[ 0.9847767   0.84738596]
 [ 0.81047787 -0.37601578]
 [-1.104849   -1.06040621]
 [-0.29311254  0.87364759]
 [-0.39729303 -0.28461157]]

虽然,这并不能帮助我们获得对刚刚发生的事情的良好直觉。人类并不那么擅长计算数字。为了更好地理解整个过程,让我们绘制原始点和通过保留欧几里得距离创建的嵌入点。一个原始点和其相应的嵌入点都显示为相同的颜色。

colors = ['r', 'g', 'b', 'c', 'm']
size = [64, 64, 64, 64, 64]
fig = plt.figure(2, (10,4))
ax = fig.add_subplot(121, projection='3d')
plt.scatter(X[:,0], X[:,1], zs=X[:,2], s=size, c=colors)
plt.title('Original Points')

ax = fig.add_subplot(122)
plt.scatter(X_transform[:,0], X_transform[:,1], s=size, c=colors)
plt.title('Embedding in 2D')
fig.subplots_adjust(wspace=.4, hspace=0.5)
plt.show()

mapping 3d to 2d with multidimensional scaling

右边的图大体上保持了相对距离--紫色、绿色和蓝色紧挨着,与青色和红色相比,它们之间的相对位置大致相同。

AT&T的Olivetti人脸数据集上的实用多维缩放法

作为MDS的一个实际例子,我们将使用来自AT&T的Olivetti面孔数据集来展示在一个低至二维的空间中的嵌入。该数据集每个人有10张64x64的位图图像,每张图像都是在不同的面部表情或照明条件下获得的。

MDS保留了数据中的模式,使同一个人的不同脸部图像在二维空间中相互靠近,而与另一个人的映射脸部相距甚远。

为了避免杂乱,我们只取4个不同的人的脸,并对其应用MDS。

在获取数据集和应用MDS之前,让我们写一个小函数,mapData() ,它接受输入参数,即成对距离矩阵dist_matrix ,原始数据矩阵X ,类变量y ,布尔变量metric ,以及图形title

该函数将MDS应用于距离矩阵,并在二维空间显示转换后的点,相同颜色的点表示同一个人的映射图像。在第二个图中,它还显示了图形上每张脸的图像在低维空间中的映射情况。

我们将用不同的距离度量与非度量MDS一起演示MDS。

def mapData(dist_matrix, X, y, metric, title):
    mds = MDS(metric=metric, dissimilarity='precomputed', random_state=0)
    # Get the embeddings
    pts = mds.fit_transform(dist_matrix)
    # Plot the embedding, colored according to the class of the points
    fig = plt.figure(2, (15,6))
    ax = fig.add_subplot(1,2,1)    
    ax = sns.scatterplot(x=pts[:, 0], y=pts[:, 1],
                         hue=y, palette=['r', 'g', 'b', 'c'])

    # Add the second plot
    ax = fig.add_subplot(1,2,2)
    # Plot the points again
    plt.scatter(pts[:, 0], pts[:, 1])
    
    # Annotate each point by its corresponding face image
    for x, ind in zip(X, range(pts.shape[0])):
        im = x.reshape(64,64)
        imagebox = OffsetImage(im, zoom=0.3, cmap=plt.cm.gray)
        i = pts[ind, 0]
        j = pts[ind, 1]
        ab = AnnotationBbox(imagebox, (i, j), frameon=False)
        ax.add_artist(ab)
    plt.title(title)    
    plt.show()

下面的代码获取了Olivetti面孔数据集并提取了标签<4的例子。

faces = dt.fetch_olivetti_faces()
X_faces = faces.data
y_faces = faces.target
ind = y_faces < 4
X_faces = X_faces[ind,:]
y_faces = y_faces[ind]

闲话少说,让我们把数据加载进去,并在上面运行我们的mapData() 函数!

使用欧几里得配对距离

使用欧氏距离对Olivetti面孔数据集进行的映射如下所示。欧氏距离是MDS的默认距离,因为它的用途很广,而且常用。

dist_euclid = euclidean_distances(X_faces)
mapData(dist_euclid, X_faces, y_faces, True, 
        'Metric MDS with Euclidean')

euclidean multidimensional scaling

我们可以看到64x64的图像被很好地映射到一个二维空间,在大多数情况下,每张图像的类别都与其他图像很好地分开。值得花点时间来欣赏这样一个事实:驻留在64x64维空间的图像可以被还原到二维空间,并且仍然保留其信息价值。

使用曼哈顿配对距离

作为比较,我们可以使用曼哈顿配对距离对相同的数据进行MDS。下面的代码使用曼哈顿距离矩阵作为输入,mapData()

dist_L1 = manhattan_distances(X_faces)
mapData(dist_L1, X_faces, y_faces, True, 
        'Metric MDS with Manhattan')

mahnattan multidimensional scaling

我们可以看到,映射与通过欧氏距离得到的映射非常相似。每个类别在低维空间中都被很好地分开,尽管它们在图上的偏移有点不同。

执行非度量的多维缩放

作为最后一个例子,我们将在同一数据集上使用欧氏距离显示非度量多维尺度,并看看它与相应的度量版本相比如何。

mapData(dist_euclid, X_faces, y_faces, False, 
        'Non-metric MDS with Euclidean')

non-metric multidimensional scaling

这里有相当多的小插曲。我们可以看到,这个版本的MDS在Olivetti面孔数据集上的表现并不是很好。

这主要是因为数据的量化性质。

非度量MDS保持了物体之间的排序距离,而不是实际距离。

MDS中的n_components参数

MDS中涉及的一个重要的超参数是点所嵌入的低维空间的大小。

当MDS被用作降维的预处理步骤时,这将是非常相关的。

问题就来了

你到底要选多少个维度,才能在不丢失重要信息的情况下最大限度地降低维度?

选择这个参数值的一个简单方法是在不同的n_components ,并绘制每个嵌入的stress_ 值的MDS。考虑到stress_ 值随着维度的增加而减少--你选择一个在stress_n_components 之间有公平权衡的点。

下面的代码通过改变1-20的维度来运行MDS,并为每个嵌入物画出相应的stress_ 属性。

stress = []
# Max value for n_components
max_range = 21
for dim in range(1, max_range):
    # Set up the MDS object
    mds = MDS(n_components=dim, dissimilarity='precomputed', random_state=0)
    # Apply MDS
    pts = mds.fit_transform(dist_euclid)
    # Retrieve the stress value
    stress.append(mds.stress_)
# Plot stress vs. n_components    
plt.plot(range(1, max_range), stress)
plt.xticks(range(1, max_range, 2))
plt.xlabel('n_components')
plt.ylabel('stress')
plt.show()

finding the right number of compo

我们可以看到,增加n_components 的值在开始时减少了压力值,然后曲线趋于平稳。18维和19维之间几乎没有差别,但1维和2维之间却有很大的差别。

曲线的肘部是n_components 的最佳值。在这种情况下,该值可以取为4,这是一个惊人的0.09%的特征/属性的减少。

结论

本指南是对使用Scikit-Learn的Python多维缩放的介绍。我们看了一下多维缩放的工作原理,它的超参数,存在哪些变种,然后在一个实际的数据集上应用了它。

我们使用了来自AT&T的Olivetti面孔数据集,并说明了驻留在64x64维空间的图像可以被映射到二维空间,并且仍然保留了图像中的单个模式或集群