降维实现的性能比较
首先,让我们加载所需的基本工具——显然包括numpy和pandas,还有获取和重新采样数据的工具,以及时间模块,以便我们可以进行一些基本的基准测试。
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_openml
from sklearn.utils import resample
import time
接下来我们需要实际的降维实现。为了解释的目的,我们将主要使用scikit-learn,但为了比较,我们还将包括MulticoreTSNE的t-SNE实现和openTSNE,这两者在历史上都比scikit-learn的t-SNE性能显著更好(scikit-learn的较新版本已经改进了t-SNE性能)。
from sklearn.manifold import TSNE, LocallyLinearEmbedding, Isomap, MDS, SpectralEmbedding
from sklearn.decomposition import PCA
from MulticoreTSNE import MulticoreTSNE
from openTSNE import TSNE as OpenTSNE
from umap import UMAP
接下来我们需要绘图工具,当然,还需要一些数据来进行操作。对于这个性能比较,我们将默认使用现在标准的流形学习基准:MNIST数字数据集。我们可以使用scikit-learn的fetch_openml来获取它。
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set(context='notebook',
rc={'figure.figsize':(12,10)},
palette=sns.color_palette('tab10', 10))
mnist = fetch_openml('mnist_784', version=1, return_X_y=True)
mnist_data = mnist[0]
mnist_labels = mnist[1].astype(int)
现在是时候开始关注性能了。首先,让我们看看性能如何随着数据集大小的增加而扩展。
按数据集大小进行性能扩展
随着数据集大小的增加,给定降维算法的运行时间将以不同的速率增加。如果您希望在更大的数据集上运行您的算法,您不仅会关心在单个小数据集上的比较运行时间,还会关心在移动到更大数据集时性能的扩展情况。我们可以通过从MNIST数字中进行子采样(通过scikit-learn的便捷resample工具)并查看不同大小子样本的运行时间来模拟这一点。由于这里涉及一些随机性(在子样本选择中,以及在一些具有随机性的算法中),我们将希望为每个数据集大小运行几个示例。我们可以轻松地将所有这些打包到一个简单的函数中,该函数将返回一个方便的pandas数据框,其中包含给定算法的数据集大小和运行时间。
def data_size_scaling(algorithm, data, sizes=[100, 200, 400, 800, 1600], n_runs=5):
result = []
for size in sizes:
for run in range(n_runs):
subsample = resample(data, n_samples=size)
start_time = time.time()
algorithm.fit(subsample)
elapsed_time = time.time() - start_time
del subsample
result.append((size, elapsed_time))
return pd.DataFrame(result, columns=('dataset size', 'runtime (s)'))
现在我们只想为每种不同的降维实现运行这个,以便我们可以查看结果。由于我们不知道这些运行可能需要多长时间,我们将从非常小的样本集开始,最多只扩展到1600个样本。
all_algorithms = [
PCA(),
UMAP(),
MulticoreTSNE(),
OpenTSNE(),
TSNE(),
LocallyLinearEmbedding(),
SpectralEmbedding(),
Isomap(),
MDS(),
]
performance_data = {}
for algorithm in all_algorithms:
if 'openTSNE' in str(algorithm.__class__):
alg_name = "OpenTSNE"
elif 'MulticoreTSNE' in str(algorithm.__class__):
alg_name = "MulticoreTSNE"
else:
alg_name = str(algorithm).split('(')[0]
performance_data[alg_name] = data_size_scaling(algorithm, mnist_data, n_runs=5)
print(f"[{time.asctime(time.localtime())}] Completed {alg_name}")
[Sat Feb 22 09:50:24 2020] Completed PCA
[Sat Feb 22 09:51:23 2020] Completed UMAP
[Sat Feb 22 09:53:24 2020] Completed MulticoreTSNE
[Sat Feb 22 10:00:50 2020] Completed OpenTSNE
[Sat Feb 22 10:02:22 2020] Completed TSNE
[Sat Feb 22 10:02:44 2020] Completed LocallyLinearEmbedding
[Sat Feb 22 10:03:06 2020] Completed SpectralEmbedding
[Sat Feb 22 10:03:31 2020] Completed Isomap
[Sat Feb 22 10:11:45 2020] Completed MDS
现在让我们绘制结果,以便我们可以看到发生了什么。我们将使用 seaborn的回归图来插值有效的缩放比例。对于某些 算法,这可能会有些嘈杂,特别是在这个相对 较小的数据集情况下,但它会给我们一个很好的了解发生了什么。
for alg_name, perf_data in performance_data.items():
sns.regplot('dataset size', 'runtime (s)', perf_data, order=2, label=alg_name)
plt.legend()
我们可以立即看出这里有一些异常值。值得注意的是,openTSNE 在小数据集上表现不佳。然而,它不具备 MDS 的扩展性;对于更大的数据集,MDS 将很快变得完全无法管理,而 openTSNE 的扩展性相对平稳。同时,MulticoreTSNE 展示了 t-SNE 可以相当高效地运行。除了 PCA 明显是最快的选择之外,很难对其他实现做出太多判断。为了了解更多,我们将不得不查看更大数据集上的运行时间。MDS、Isomap 和 SpectralEmbedding 实际上会花费太长时间来运行,因此让我们限制自己只使用表现最快的实现,并看看当我们扩展到更大的数据集时会发生什么。
fast_algorithms = [
PCA(),
UMAP(),
MulticoreTSNE(),
OpenTSNE(),
TSNE(),
LocallyLinearEmbedding(),
]
fast_performance_data = {}
for algorithm in fast_algorithms:
if 'openTSNE' in str(algorithm.__class__):
alg_name = "OpenTSNE"
elif 'MulticoreTSNE' in str(algorithm.__class__):
alg_name = "MulticoreTSNE"
else:
alg_name = str(algorithm).split('(')[0]
fast_performance_data[alg_name] = data_size_scaling(algorithm, mnist_data,
sizes=[1600, 3200, 6400, 12800, 25600], n_runs=4)
print(f"[{time.asctime(time.localtime())}] Completed {alg_name}")
[Sat Feb 22 10:12:15 2020] Completed PCA
[Sat Feb 22 10:14:51 2020] Completed UMAP
[Sat Feb 22 11:16:05 2020] Completed MulticoreTSNE
[Sat Feb 22 11:50:17 2020] Completed OpenTSNE
[Sat Feb 22 13:06:38 2020] Completed TSNE
[Sat Feb 22 14:14:36 2020] Completed LocallyLinearEmbedding
for alg_name, perf_data in fast_performance_data.items():
sns.regplot('dataset size', 'runtime (s)', perf_data, order=2, label=alg_name)
plt.legend()
在这一点上,我们开始看到不同实现之间的一些显著差异。在早期的图表中,OpenTSNE看起来表现相对较差,但现在缩放效果开始显现,我们看到它比大多数方法更快。同样,MulticoreTSNE在早期的图表中看起来比其他一些算法慢,但随着我们扩展到更大的数据集,我们看到它的相对缩放性能优于scikit-learn实现的TSNE和局部线性嵌入。
可能值得进一步扩展——直到完整的MNIST数字数据集。为了在合理的时间内完成这一任务,我们将不得不将注意力限制在更小的实现子集上。我们将把内容缩减到仅包括OpenTSNE、MulticoreTSNE、PCA和UMAP。
very_fast_algorithms = [
PCA(),
UMAP(),
MulticoreTSNE(),
OpenTSNE(),
]
vfast_performance_data = {}
for algorithm in very_fast_algorithms:
if 'openTSNE' in str(algorithm.__class__):
alg_name = "OpenTSNE"
elif 'MulticoreTSNE' in str(algorithm.__class__):
alg_name = "MulticoreTSNE"
else:
alg_name = str(algorithm).split('(')[0]
vfast_performance_data[alg_name] = data_size_scaling(algorithm, mnist_data,
sizes=[3200, 6400, 12800, 25600, 51200, 70000], n_runs=2)
print(f"[{time.asctime(time.localtime())}] Completed {alg_name}")
[Sat Feb 22 14:15:22 2020] Completed PCA
[Sat Feb 22 14:18:59 2020] Completed UMAP
[Sat Feb 22 17:04:58 2020] Completed MulticoreTSNE
[Sat Feb 22 17:54:14 2020] Completed OpenTSNE
for alg_name, perf_data in vfast_performance_data.items():
sns.regplot('dataset size', 'runtime (s)', perf_data, order=2, label=alg_name)
plt.legend()
在这里,我们看到UMAP相对于t-SNE的优势真正显现出来。 虽然UMAP明显比PCA慢,但其扩展性能 明显优于MulticoreTSNE,而且尽管openTSNE的扩展性能令人印象深刻, UMAP仍然表现更优。根据线条的斜率,对于更大的数据集, UMAP和t-SNE之间的差异只会越来越大。
这结束了我们对数据集大小缩放的探讨。简而言之,PCA 是目前为止最快的选择,但你可能为了速度而放弃了很多。UMAP 虽然无法与 PCA 竞争,但在我们探讨的实现中,它显然是性能方面次佳的选择。考虑到 UMAP 能够提供的结果质量,我们认为它显然是降维的一个好选择。