降维实现的性能比较

不同的降维技术可能具有截然不同的计算复杂度。除了算法本身,实现方式也会产生影响。这两个因素对实际运行给定降维算法所需的时间有着重要作用。此外,您尝试处理的数据性质也会产生影响——这主要涉及原始数据的维度。在这里,我们将简要介绍一些降维实现的性能特征。

首先,让我们加载所需的基本工具——显然包括 numpy 和 pandas,但也需要用于获取和重采样数据的工具,以及 time 模块以便进行一些基础的性能测试。

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 的实现,其性能明显优于当前的 scikit-learn t-SNE。

from sklearn.manifold import TSNE, LocallyLinearEmbedding, Isomap, MDS, SpectralEmbedding
from sklearn.decomposition import PCA
from MulticoreTSNE import MulticoreTSNE
from umap import UMAP

接下来,我们需要绘图工具,当然还有一些数据来处理。对于这次性能比较,我们将默认使用流形学习的现行标准基准测试:MNIST 数字数据集。我们可以使用 scikit-learn 的 fetch_mldata 来为我们获取数据。

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('Fashion-MNIST', version=1)

现在是时候开始考察性能了。首先,让我们看看性能如何随数据集大小的增加而变化。

性能随数据集大小的变化

随着数据集大小的增加,给定降维算法的运行时间会以不同的速率增加。如果您想在更大的数据集上运行您的算法,您不仅会关心在单个小型数据集上的比较运行时间,还会关心随着数据集变大,性能如何扩展。我们可以通过从 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(),
    LocallyLinearEmbedding(),
    SpectralEmbedding(),
    Isomap(),
    TSNE(),
    MDS(),
]
performance_data = {}
for algorithm in all_algorithms:
    alg_name = str(algorithm)
    if 'MulticoreTSNE' in alg_name:
        alg_name = 'MulticoreTSNE'
    else:
        alg_name = alg_name.split('(')[0]

    performance_data[alg_name] = data_size_scaling(algorithm, mnist.data, n_runs=3)

现在让我们绘制结果,以便了解情况。我们将使用 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();
_images/performance_14_1.png

我们可以立即看到这里有一些异常值。scikit-learn 的 t-SNE 明显比大多数其他算法慢得多。然而,它不像 MDS 那样具有可伸缩性;对于更大的数据集,MDS 将很快变得完全无法处理。同时,MulticoreTSNE 表明 t-SNE 可以相当有效地运行。除了 PCA 是迄今为止最快的选项之外,很难判断其他实现的情况。要了解更多,我们需要查看在更大数据集上的运行时间。MDS 和 scikit-learn 的 t-SNE 都需要太长时间运行,所以让我们将范围限制在性能最快的实现,看看当我们扩展到更大数据集时会发生什么。

fast_algorithms = [
    PCA(),
    UMAP(),
    MulticoreTSNE(),
    LocallyLinearEmbedding(),
    SpectralEmbedding(),
    Isomap(),
]
fast_performance_data = {}
for algorithm in fast_algorithms:
    alg_name = str(algorithm)
    if 'MulticoreTSNE' in alg_name:
        alg_name = 'MulticoreTSNE'
    else:
        alg_name = alg_name.split('(')[0]

    fast_performance_data[alg_name] = data_size_scaling(algorithm, mnist.data,
                                                   sizes=[800, 1600, 3200, 6400, 12800], n_runs=3)
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();
_images/performance_17_1.png

在这一点上,我们开始看到不同实现之间的一些显著差异。在之前的图中,MulticoreTSNE 看起来比其他一些算法要慢,但当我们扩展到更大的数据集时,我们看到它的相对缩放性能远优于 scikit-learn 中 Isomap、谱嵌入和局部线性嵌入的实现。

也许值得进一步扩展——直到完整的 MNIST 数字数据集。为了在合理的时间内完成,我们将不得不将注意力限制在更小的实现子集上。我们将精简到只剩下 MulticoreTSNE、PCA 和 UMAP。

very_fast_algorithms = [
    PCA(),
    UMAP(),
    MulticoreTSNE(),
]
vfast_performance_data = {}
for algorithm in very_fast_algorithms:
    alg_name = str(algorithm)
    if 'MulticoreTSNE' in alg_name:
        alg_name = 'MulticoreTSNE'
    else:
        alg_name = alg_name.split('(')[0]

    vfast_performance_data[alg_name] = data_size_scaling(algorithm, mnist.data,
                                                    sizes=[3200, 6400, 12800, 25600, 51200, 70000], n_runs=2)
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();
_images/performance_20_1.png

在这里,我们看到 UMAP 相对于 t-SNE 的优势真正凸显出来。虽然 UMAP 明显比 PCA 慢,但其缩放性能显著优于 MulticoreTSNE,对于更大的数据集,这种差异只会继续扩大。

关于性能随数据集大小变化的探讨到此结束。简短总结是:PCA 是迄今为止最快的选择,但为了这种速度,您可能会牺牲很多。UMAP 虽然在速度上无法与 PCA 竞争,但在此处探讨的实现中,它在性能方面显然是次优的选择。考虑到 UMAP 能够提供的结果质量,我们认为它显然是降维的一个好选择。