Power by GeekHades

[译]浅析t-SNE原理及其应用

声明: 本文转译自Data Camp上Manish Pathak的文章《Introduction to t-SNE》原文地址

译者注: 本文言简意赅的阐述了数据降维( Dimensionality Reduction technique)技术中PCA以及t-Distributed Stochastic Neighbor Embedding(t-SNE)算法的相关实现原理以及利弊,并且使用Python基于Fashion-MNIST数据集描述了对PCA以及t-SNE算法的基本应用。本人觉得相关概念阐述的比较清晰因此特别转译在此博客,但如果读完本文后想深究t-SNE背后的数学原理还是推荐看原论文,论文地址会在附录给出。本人会在原文基础上添加一些相关注释(本人学习和相关工作中的一些理解)以[注]为标记。原文在实现的过程中使用的是Python2,为了贴合当下Python生态体系,因此本人使用Python3重新复现。由于本人英文水平以及技术水平有限,我会尽最大的能力确保翻译得当,倘若文中出现翻译不恰当的地方还请诸位海涵且帮助我纠正错误,可通过文章底部的邮箱地址联系我。感谢!

以下为译文内容 。

通过本教程,你将会对当下较为流行的t-SNE降维技术有一定的认知并能掌握其基础应用。

本文涉及内容如下:

  • 了解数据降维以及常用手段类型。
  • 了解主成分分析(Principal Component Analysis (PCA))技术原理以及如何在Python上应用。
  • 了解t分布随机邻近嵌入(t-Distributed Stochastic Neighbor Embedding (t-SNE))原理以及如何在Python上应用。
  • 可视化这两种算法的降维结果。
  • 比较这两种算法之间的优缺点

数据降维

如果你已经有过在大型数据集(包含非常多的特征)上进行相关工作的经历,你应该可以深刻(fathom)的体会到想要理解或者挖掘特征间关系是非常困难的。这一问题不仅体现在数据探索性分析(EDA)上,而且还会影响机器学习模型的性能,因此你可能会因此让你的模型过拟合或者破坏了你对模型的某些假设,例如独立特征对线性回归模型的影响[1]。这时数据降维就显得格外重要。在机器学习中,降维技术是减少随机变量个数,得到一组“不相关”主变量的过程。通过减少特征空间的维数,你仅需要考虑一小部分特征之间的关系,而且你可以很轻易地在这部分特征上做探索或者可视化,同时也可以减少你的模型出现过拟合的可能。

[注1] 考虑线性相关的两个变量以及线性无关的两个变量对整个模型的影响。

降维通常可以通过以下方式实现:

  • 特征限制:通过限制特征来缩减特征空间。虽然可以达到降维目的,但其缺点就是你会丢失一部分被你删掉的特征所蕴含的信息。
  • 特征选择:应用一些统计检验的方法去根据它们的重要程度进行排序,然后选择其中最主要的几个特征子集。这类方法依然会面临信息丢失的风险,而且还会因为不同的检验方法会导致出现不同的特征排序方式,因此是不稳定的。
  • 特征提取:通过创建一些融合了若干旧特征的独立特征。这类技术可以很好的应用在线性或非线性的降维技术中。

主成分分析(PCA)

主成分分析是一种线性的特征提取技术,它将数据通过线性映射的方式投影到低维空间中,通过这样的方式能够确保原数据在低维空间中方差最大。它通过计算其特征的协方差矩阵中的特征向量来实现这一目的。与最大的特征值(主成分)一一对应的特征向量会被用于重建成新的数据,并且保证这些数据在该特征向量方向上的方差最大。

简单来说,PCA以特定的方式融合了你所有的输入属性(特征)值,这样你就可以在删除不重要特征的同时不必担心丢失最有价值的部分。还有一个更为显著的好处,经过PCA处理之后的每一个新特征都是独立于其他特征的[2]

[注2] 矩阵分解中的所有特征向量都是线性无关的。

t分布随机邻近插入(t-SNE)

t-SNE是一种非线性的降维技术,非常适合用于高维数据的可视化。广泛应用于图像处理、自然语言处理,基因数据以及语音处理。为了保证足够浅显易懂,这里仅对t-SNE的工作原理做简要介绍:

  • 该算法一开始通过计算在高维空间中的数据点的相似度概率和与其对应的低维空间中的点的相似度的概率。点的相似度计算方法是[3]:以A为中心的高斯分布中,如果按概率密度的比例选取相邻点,则点A将选择点B作为其相邻点的条件概率,以此计算点A的相似性。
  • 为了更好将数据投影至低维空间中,算法尝试去最小化高维数据空间和低维数据空间之间的条件概率(相似度)之差。
  • 为了评估t-SNE条件概率差和的最小化,使用梯度下降的方法最小化原分布中数据与映射分布中的对应数据的KL散度[4](Kullback-Leibler divergence)的总和。

[注3] 更容易理解的方式是通过欧式距离算出的AB两点的距离转换为条件概率以此来表达点与点之间的相似度,此概率与大,AB两点的相似度就越高。

[注4] 即相对熵或称信息增益。让其值变小也就是为了让相似度更高的数据点聚集在一起。信息增益小则说明区分该实例的难度大,换个角度来说就是这两个实例非常相似。关于信息增益相关概念可以浏览一下我的另一篇博文

如果有兴趣更进一步研究t-SNE的同学可以查看附录中的论文1。

简单来说,t-SNE主要目的在最小化两种分布的差异性:第一个是度量输入实例成对相似性,另一种是嵌入在相应低维空间中的成对点的相似性。

通过上述方式,t-SNE映射多维数据到低维空间中并且试图找到基于多个特征的数据点的相似性来区分观测数据群,从而发现数据中的模式。然而,经过这一过程后,输入特征开始变得模糊,并且你无法仅基于t-SNE的结果进行任何推断[5]。因此这也是为什么t-SNE主要还是用来做EDA和可视化的原因。

[注5]PCA比较就可以很显然的看出,经过PCA处理过后的结果能够得知每一个成分的方差贡献度(解释方差),然后t-SNE仅仅是基于相似度进行判定,没办法从其结果推断类似的信息。

t-SNE应用 Python实现

现在你可以在Python中基于开源数据集应用t-SNE算法并且将其降维结果可视化。与此同时,你也要在相同数据集应用PCA算法并可视化结果,然后与t-SNE进行比较。

本次的数据集使用Fashion-MNIST并且您可以点击此处进行下载。

Fashion-MNIST是类似MNIST手写图像数据集的公共数据集,由70,000条已标注为10种类别的时尚服装数据,每一个实例都是由28x28的灰度图像组成,其中训练数据集含有60,000条,测试数据集有10,000条,用此数据集比用原MNIST数据集能更好的对比结果。Fashion-MNIST的标签与MNIST一样是0-9,但是不同的是这是个数字代表的是对应的时尚服装产品,下面对每一个数字对应的含义进行解释说明:

标注编号 描述
0 T-shirt/top(T恤)
1 Trouser(裤子)
2 Pullover(套衫)
3 Dress(裙子)
4 Coat(外套)
5 Sandal(凉鞋)
6 Shirt(汗衫)
7 Sneaker(运动鞋)
8 Bag(包)
9 Ankle boot(踝靴)

之后你可以在Fashion-MNIST官方仓库的utils文件下的mnist_reader.py找到读取该数据集的特定方法,即load_mnist(),如下:

def load_mnist(path, kind='train'):
    import os
    import gzip
    import numpy as np

    """Load MNIST data from `path`"""
    labels_path = os.path.join(path,
                               '%s-labels-idx1-ubyte.gz'
                               % kind)
    images_path = os.path.join(path,
                               '%s-images-idx3-ubyte.gz'
                               % kind)

    with gzip.open(labels_path, 'rb') as lbpath:
        labels = np.frombuffer(lbpath.read(), dtype=np.uint8,
                               offset=8)

    with gzip.open(images_path, 'rb') as imgpath:
        images = np.frombuffer(imgpath.read(), dtype=np.uint8,
                               offset=16).reshape(len(labels), 784)

    return images, labels

之后你可以通过load_mnist()方法读取训练和测试数据集并将其应用在你的算法上即可,只要将数据集的.gz文件路径作为第一个参数以及数据类型kind作为第二个参数传入该函数即可,如

X_train, y_train = load_mnist('.', kind='train')  # 我的.gz文件就放在当前路径下

读取数据之后,你可以检查一下训练数据的基本属性,如shape,你会看到训练数据由60,000个实例以及784个特征组成

X_train.shape
(60000, 784)

同样的标注数据也可以看到是由0-910个标签组成

y_train
array([9, 0, 0, ..., 3, 0, 5], dtype=uint8)

接下来,为了保证能够代码的完整性比如引入所需要的第三方库,同时为了确保可复现,还需要设置Random State参数[6]为123。代码如下:

[注6] 我们知道计算机的随机都是伪随机,因此为了确保代码结果是可复现的都会设置一个随机因子,至于这个值是多少并没有规定,例如我本人就喜欢设置成42,原因是在《银河系漫游指南》中42是超级计算机得出的生命终极答案。

import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as PathEffects
%matplotlib inline

import seaborn as sns
sns.set_style('darkgrid')
sns.set_palette('muted')
sns.set_context("notebook", font_scale=1.5,
                rc={"lines.linewidth": 2.5})
RS = 123

为了能够兼容两个算法结果的可视化展示,需要创建一个fashion_scatter()函数。该函数接受两个参数:参数1 x 接受一个算法结果的2维矩阵输入;参数2 colors接受一个1维的标签数组。该函数会根据colors中的标签对x散点数据继续染色。

fashion_scatter()定义如下:

# Utility function to visualize the outputs of PCA and t-SNE

def fashion_scatter(x, colors):
    # choose a color palette with seaborn.
    num_classes = len(np.unique(colors))
    palette = np.array(sns.color_palette("hls", num_classes))

    # create a scatter plot.
    f = plt.figure(figsize=(8, 8))
    ax = plt.subplot(aspect='equal')
    sc = ax.scatter(x[:,0], x[:,1], lw=0, s=40, c=palette[colors.astype(np.int)])
    plt.xlim(-25, 25)
    plt.ylim(-25, 25)
    ax.axis('off')
    ax.axis('tight')

    # add the labels for each digit corresponding to the label
    txts = []

    for i in range(num_classes):

        # Position of each label at median of data points.

        xtext, ytext = np.median(x[colors == i, :], axis=0)
        txt = ax.text(xtext, ytext, str(i), fontsize=24)
        txt.set_path_effects([
            PathEffects.Stroke(linewidth=5, foreground="w"),
            PathEffects.Normal()])
        txts.append(txt)

    return f, ax, sc, txts

为了不让你的机器承担过重的内存与运行时间压力,在本次实践中我们只取训练集中前20,000条作为训练数据,同时你要确保这20,000条数据一定要涵盖10个标签。

x_subset = X_train[0:20000]
y_subset = y_train[0:20000]

np.unique(y_subset)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

现在可以在训练子集上使用PCA算法并可视化其结果了,为了方便我们直接使用scikit-learn提供的PCA算法,将n_components设置为4,这个参数决定了最后输出的数据维度。关于PCA的更多用法可以自行前往scikit-learn的官网查看

from sklearn.decomposition import PCA

time_start = time.time()

pca = PCA(n_components=4)
pca_result = pca.fit_transform(x_subset)

print(f'PCA done! Time elapsed: {time.time()-time_start} seconds')
PCA done! Time elapsed: 0.6091551780700684 seconds

现在可以将pca_reuslt存入DataFrame中,之后我们可以检查这四个主成分的各自的数据方差。

pca_df = pd.DataFrame(columns = ['pca1','pca2','pca3','pca4'])

pca_df['pca1'] = pca_result[:,0]
pca_df['pca2'] = pca_result[:,1]
pca_df['pca3'] = pca_result[:,2]
pca_df['pca4'] = pca_result[:,3]

print(f'Variance explained per principal component: {pca.explained_variance_ratio_}')
Variance explained per principal component: [0.29021329 0.1778743  0.06015076 0.04975864]

注意:训练子集中的第一和第二主成分的解释方差之和几乎达到了48%。因此我们仅对这两个主成分的数据进行可视化即可。

top_two_comp = pca_df[['pca1','pca2']] # taking first and second principal component

f, ax, sc, txts = fashion_scatter(top_two_comp.values,y_subset) # Visualizing the PCA output
f.show()

pca_top_2_result.png

如图中所示,PCA算法试图将不同点区分且将相同的点聚集起来。因此,这个图也可以用来做数据探索性分析。当然主成分1与主成分2还可以在分类和聚类算法上直接充当特征。

现在我们使用t-SNE来做相同的事情,当然t-SNE也在被scikit-learn实现了,我们直接使用即可,在此我们提供一些t-SNE常用的参数说明,更多关于t-SNE的说明可以参考scikit-learn的官方文档

  • n_components (默认为2): 嵌入空间的维数。
  • perplexity (默认为30): 复杂度的含义与其他流行学习方法[7]中的最邻近个数的含义相似,通常在5-50之间考虑。
  • early_exaggeration (默认为12.0): 控制原始空间在嵌入空间中的密度集大小。
  • learning_rate (默认为200.0): 学习率的常见范围在10.0~1000.0之间。
  • n_iter (默认为1000): 算法优化的的最大迭代次数,至少应该设置为250次。
  • method (默认为 ‘barnes_hut’): 学习方法,Barnes-Hut方法运行时间为O(NlogN)。method='exact'会比较慢,算法执行时间为O(N2)。

[注7] 流行学习方法是指从高维采样数据中恢复低维流形结构,即找到高维空间中的低维流形。引自简书《流行学习-实现高维数据的降维与可视化》。

在此我们先以默认参数运行t-SNE算法:

from sklearn.manifold import TSNE
import time
time_start = time.time()

fashion_tsne = TSNE(random_state=RS, n_jobs=-1).fit_transform(x_subset)

print(f't-SNE done! Time elapsed: {time.time()-time_start} seconds')
t-SNE done! Time elapsed: 882.41050598 seconds

很显然,t-SNE在相同数据集上的执行时间要比PCA长太多[8]

[注8]sklearn 0.22版本更新了n_jobs参数,可以使用多个CPU并行计算,会对执行时间有所改观。代码如下:

fashion_tsne = TSNE(random_state=RS, n_jobs=-1).fit_transform(x_subset)

因机器而异,在我本地跑完整个算法耗时223秒。

现在可视化t-SNE的结果:

f, ax, sc, txts = fashion_scatter(fashion_tsne, y_subset)
f.show()

tsne_nc2_result.png

正如你所见,这次的降维结果比刚才PCA的结果好了许多,基本可以清晰的看到每个标签都被聚集在它们各自的簇中,如果你将此数据应用于聚类算法中,应该会更精确的描绘每一个簇。

sklearn关于t-SNE的文档中有提到:

It is highly recommended to use another dimensionality reduction method (e.g., PCA for dense data or TruncatedSVD for sparse data) to reduce the number of dimensions to a reasonable amount (e.g., 50) if the number of features is very high. This will suppress some noise and speed up the computation of pairwise distances between samples.

当如果维数非常高时,强烈建议使用其他的降维方式(如稠密矩阵使用PCA或者稀疏矩阵使用TruncatedSVD)去降低至一个合理的维度(如50),这样做会极大的抑制数据噪声,加快样本间欧氏距离的计算时间。

现在我们可以根据官方给出的这个建议,先使用PCA算法[9]将数据维数降至50维,然后再在t-SNE上使用此数据,并可视化其结果。

[注9] 个人感觉这里分别尝试PCATruncatedSVD这两中方法会更加合理一些。

time_start = time.time()

pca_50 = PCA(n_components=50)
pca_result_50 = pca_50.fit_transform(x_subset)

print(f'PCA with 50 components done! Time elapsed: {time.time()-time_start} seconds')

print(f'Cumulative variance explained by 50 principal components: {np.sum(pca_50.explained_variance_ratio_)}')
PCA with 50 components done! Time elapsed: 1.1455249786376953 seconds
Cumulative variance explained by 50 principal components: 0.8624682420611026

现在将pca_result_50应用在t-SNE上:

import time
time_start = time.time()

# 这里直接应用 n_jobs=-1 参数,启用所有cpu进行计算
fashion_pca_tsne = TSNE(random_state=RS, n_jobs=-1).fit_transform(pca_result_50)

print(f't-SNE done! Time elapsed: {time.time()-time_start} seconds')
t-SNE done! Time elapsed: 93.07448196411133 seconds

可以看出经过以上步骤后的执行时间大幅度缩减。

tsne_pca50_result.png

从图上可以看出来,几乎非常接近于直接运用t-SNE的图,但是还是有一些地方不同,在0标签或者说在T-shirt/top这一簇被更加紧密的聚集在一起了。

PCA 与 t-SNE 的区别

尽管PCAt-SNE各有优劣,但是它们之间还是有一些非常重要的区别:

  • t-SNE的计算成本非常高,相同数量级的数据集在PCA上的计算速度会比t-SNE要快许多。
  • 从理论上来说,PCA是一种矩阵分解技术,而t-SNE是一种概率方法。
  • 在类似PCA一样的线性降维算法中,会将不同的数据点置于距离较远的低维空间中。但是,为了在低维非线性流行上表示高维数据,必须将相似的数据点紧密的表示在一起,这也是t-SNEPCA应用场景不同之处。
  • 在使用t-SNE的时候,即使是相同的超参数但是由于在不同时期运行的结果可能不尽相同,因此在使用t-SNE时必须观察许多图,而PCA则是稳定的。
  • 由于PCA是一种线性的算法,它无法解释特征之间的复杂多项式关系也即非线性关系,而t-SNE可以获知这些信息。

结论

恭喜!你通过本教程已经对降维技术有了一定的了解,并且知道如何使用PCAt-SNE这两种主流的降维技术,并且了解了如何创建一些漂亮的图来比较它们之间的结果。但是,这些仅仅是非常基础的知识,在t-SNE的原理中还有许多知识可以去深究,希望通过本教程可以让您在您的日常工作中更好的使用t-SNE

附录



* 如果你对文章有任何意见或建议请发 邮件 给我!
* if you have any suggestion that you could send a E-mail to me, Please!