声明: 本文转译自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-9
10个标签组成
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
算法试图将不同点区分且将相同的点聚集起来。因此,这个图也可以用来做数据探索性分析。当然主成分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()
正如你所见,这次的降维结果比刚才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] 个人感觉这里分别尝试PCA
与TruncatedSVD
这两中方法会更加合理一些。
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
可以看出经过以上步骤后的执行时间大幅度缩减。
从图上可以看出来,几乎非常接近于直接运用t-SNE
的图,但是还是有一些地方不同,在0
标签或者说在T-shirt/top
这一簇被更加紧密的聚集在一起了。
PCA 与 t-SNE 的区别
尽管PCA
与t-SNE
各有优劣,但是它们之间还是有一些非常重要的区别:
t-SNE
的计算成本非常高,相同数量级的数据集在PCA
上的计算速度会比t-SNE
要快许多。- 从理论上来说,
PCA
是一种矩阵分解技术,而t-SNE
是一种概率方法。 - 在类似
PCA
一样的线性降维算法中,会将不同的数据点置于距离较远的低维空间中。但是,为了在低维非线性流行上表示高维数据,必须将相似的数据点紧密的表示在一起,这也是t-SNE
与PCA
应用场景不同之处。 - 在使用
t-SNE
的时候,即使是相同的超参数但是由于在不同时期运行的结果可能不尽相同,因此在使用t-SNE
时必须观察许多图,而PCA
则是稳定的。 - 由于
PCA
是一种线性的算法,它无法解释特征之间的复杂多项式关系也即非线性关系,而t-SNE
可以获知这些信息。
结论
恭喜!你通过本教程已经对降维技术有了一定的了解,并且知道如何使用PCA
和t-SNE
这两种主流的降维技术,并且了解了如何创建一些漂亮的图来比较它们之间的结果。但是,这些仅仅是非常基础的知识,在t-SNE
的原理中还有许多知识可以去深究,希望通过本教程可以让您在您的日常工作中更好的使用t-SNE
。