Voisins les plus proches approximatifs dans TSNE

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce laboratoire, nous allons apprendre à utiliser les voisins les plus proches approximatifs dans TSNE à l'aide de la bibliothèque scikit-learn de Python.

Conseils sur la machine virtuelle

Une fois le démarrage de la machine virtuelle terminé, cliquez dans le coin supérieur gauche pour basculer vers l'onglet Carnet d'étude pour accéder au carnet Jupyter pour pratiquer.

Parfois, vous devrez peut-être attendre quelques secondes pour que le carnet Jupyter ait fini de charger. La validation des opérations ne peut pas être automatisée en raison des limites du carnet Jupyter.

Si vous rencontrez des problèmes pendant l'apprentissage, n'hésitez pas à demander à Labby. Donnez votre feedback après la session, et nous réglerons rapidement le problème pour vous.

Installer les packages requis

Nous devons installer les packages nmslib et pynndescent. Ces packages peuvent être installés à l'aide de la commande pip.

!pip install nmslib pynndescent

Importer les bibliothèques requises

Nous devons importer les bibliothèques requises, y compris nmslib, pynndescent, sklearn, numpy, scipy et matplotlib.

import sys
import joblib
import numpy as np
import matplotlib.pyplot as plt
from scipy.sparse import csr_matrix
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.datasets import fetch_openml
from sklearn.utils import shuffle
from sklearn.manifold import TSNE
from sklearn.neighbors import KNeighborsTransformer
from sklearn.pipeline import make_pipeline
from pynndescent import PyNNDescentTransformer
import nmslib

Définir une classe d'emballage pour nmslib

Nous définissons une classe d'emballage pour nmslib pour implémenter l'API scikit-learn pour nmslib, ainsi qu'une fonction de chargement. La classe NMSlibTransformer prend n_neighbors, metric, method et n_jobs comme paramètres. La méthode fit() initialise nmslib et y ajoute les points de données. La méthode transform() trouve les plus proches voisins et renvoie une matrice creuse.

class NMSlibTransformer(TransformerMixin, BaseEstimator):
    """Wrapper pour utiliser nmslib comme KNeighborsTransformer de sklearn"""

    def __init__(self, n_neighbors=5, metric="euclidean", method="sw-graph", n_jobs=-1):
        self.n_neighbors = n_neighbors
        self.method = method
        self.metric = metric
        self.n_jobs = n_jobs

    def fit(self, X):
        self.n_samples_fit_ = X.shape[0]

        ## Voir plus de métriques dans le manuel
        ## https://github.com/nmslib/nmslib/tree/master/manual
        space = {
            "euclidean": "l2",
            "cosine": "cosinesimil",
            "l1": "l1",
            "l2": "l2",
        }[self.metric]

        self.nmslib_ = nmslib.init(method=self.method, space=space)
        self.nmslib_.addDataPointBatch(X.copy())
        self.nmslib_.createIndex()
        return self

    def transform(self, X):
        n_samples_transform = X.shape[0]

        ## Pour des raisons de compatibilité, comme chaque échantillon est considéré comme son propre
        ## voisin, un voisin supplémentaire sera calculé.
        n_neighbors = self.n_neighbors + 1

        if self.n_jobs < 0:
            ## Même traitement que celui effectué dans joblib pour les valeurs négatives de n_jobs :
            ## en particulier, `n_jobs == -1` signifie "autant de threads que de processeurs".
            num_threads = joblib.cpu_count() + self.n_jobs + 1
        else:
            num_threads = self.n_jobs

        results = self.nmslib_.knnQueryBatch(
            X.copy(), k=n_neighbors, num_threads=num_threads
        )
        indices, distances = zip(*results)
        indices, distances = np.vstack(indices), np.vstack(distances)

        indptr = np.arange(0, n_samples_transform * n_neighbors + 1, n_neighbors)
        kneighbors_graph = csr_matrix(
            (distances.ravel(), indices.ravel(), indptr),
            shape=(n_samples_transform, self.n_samples_fit_),
        )

        return kneighbors_graph

Définir une fonction pour charger l'ensemble de données MNIST

Nous définissons une fonction load_mnist() pour charger l'ensemble de données MNIST, mélanger les données et ne renvoyer que le nombre spécifié d'échantillons.

def load_mnist(n_samples):
    """Charge MNIST, mélange les données et ne renvoie que n_samples."""
    mnist = fetch_openml("mnist_784", as_frame=False, parser="pandas")
    X, y = shuffle(mnist.data, mnist.target, random_state=2)
    return X[:n_samples] / 255, y[:n_samples]

Benchmark des différents transformateurs de plus proches voisins

Nous effectuons un benchmark des différents transformateurs de plus proches voisins exacts/approximatifs. Nous définissons les ensembles de données, les transformateurs et les paramètres, y compris n_iter, perplexity, metric et n_neighbors. Nous mesurons le temps qu'il prend de configurer et de transformer chaque transformateur sur chaque ensemble de données. Nous affichons le temps qu'il a fallu pour configurer et transformer chaque transformateur.

datasets = [
    ("MNIST_10000", load_mnist(n_samples=10_000)),
    ("MNIST_20000", load_mnist(n_samples=20_000)),
]

n_iter = 500
perplexity = 30
metric = "euclidean"
n_neighbors = int(3.0 * perplexity + 1) + 1

tsne_params = dict(
    init="random",  ## pca non pris en charge pour les matrices creuses
    perplexity=perplexity,
    method="barnes_hut",
    random_state=42,
    n_iter=n_iter,
    learning_rate="auto",
)

transformers = [
    (
        "KNeighborsTransformer",
        KNeighborsTransformer(n_neighbors=n_neighbors, mode="distance", metric=metric),
    ),
    (
        "NMSlibTransformer",
        NMSlibTransformer(n_neighbors=n_neighbors, metric=metric),
    ),
    (
        "PyNNDescentTransformer",
        PyNNDescentTransformer(
            n_neighbors=n_neighbors, metric=metric, parallel_batch_queries=True
        ),
    ),
]

for dataset_name, (X, y) in datasets:
    msg = f"Benchmarking on {dataset_name}:"
    print(f"\n{msg}\n" + str("-" * len(msg)))

    for transformer_name, transformer in transformers:
        longest = np.max([len(name) for name, model in transformers])
        start = time.time()
        transformer.fit(X)
        fit_duration = time.time() - start
        print(f"{transformer_name:<{longest}} {fit_duration:.3f} sec (fit)")
        start = time.time()
        Xt = transformer.transform(X)
        transform_duration = time.time() - start
        print(f"{transformer_name:<{longest}} {transform_duration:.3f} sec (transform)")
        if transformer_name == "PyNNDescentTransformer":
            start = time.time()
            Xt = transformer.transform(X)
            transform_duration = time.time() - start
            print(
                f"{transformer_name:<{longest}} {transform_duration:.3f} sec"
                " (transform)"
            )

Visualiser l'imbrication TSNE

Nous visualisons les imbrications TSNE en utilisant différents transformateurs de plus proches voisins. Nous définissons transformers comme une liste contenant trois pipelines : TSNE avec NearestNeighbors interne, TSNE avec KNeighborsTransformer et TSNE avec NMSlibTransformer. Nous itérons sur les ensembles de données et les transformateurs et traçons les imbrications TSNE, qui devraient être similaires pour les différentes méthodes. Nous montrons le tracé à la fin.

transformers = [
    ("TSNE with internal NearestNeighbors", TSNE(metric=metric, **tsne_params)),
    (
        "TSNE with KNeighborsTransformer",
        make_pipeline(
            KNeighborsTransformer(
                n_neighbors=n_neighbors, mode="distance", metric=metric
            ),
            TSNE(metric="precomputed", **tsne_params),
        ),
    ),
    (
        "TSNE with NMSlibTransformer",
        make_pipeline(
            NMSlibTransformer(n_neighbors=n_neighbors, metric=metric),
            TSNE(metric="precomputed", **tsne_params),
        ),
    ),
]

nrows = len(datasets)
ncols = np.sum([1 for name, model in transformers if "TSNE" in name])
fig, axes = plt.subplots(
    nrows=nrows, ncols=ncols, squeeze=False, figsize=(5 * ncols, 4 * nrows)
)
axes = axes.ravel()
i_ax = 0

for dataset_name, (X, y) in datasets:
    msg = f"Benchmarking on {dataset_name}:"
    print(f"\n{msg}\n" + str("-" * len(msg)))

    for transformer_name, transformer in transformers:
        longest = np.max([len(name) for name, model in transformers])
        start = time.time()
        Xt = transformer.fit_transform(X)
        transform_duration = time.time() - start
        print(
            f"{transformer_name:<{longest}} {transform_duration:.3f} sec"
            " (fit_transform)"
        )

        ## plot TSNE embedding which should be very similar across methods
        axes[i_ax].set_title(transformer_name + "\non " + dataset_name)
        axes[i_ax].scatter(
            Xt[:, 0],
            Xt[:, 1],
            c=y.astype(np.int32),
            alpha=0.2,
            cmap=plt.cm.viridis,
        )
        axes[i_ax].xaxis.set_major_formatter(NullFormatter())
        axes[i_ax].yaxis.set_major_formatter(NullFormatter())
        axes[i_ax].axis("tight")
        i_ax += 1

fig.tight_layout()
plt.show()

Sommaire

Dans ce laboratoire, nous avons appris à utiliser les plus proches voisins approximatifs dans TSNE à l'aide de la bibliothèque scikit-learn de Python. Nous avons importé les bibliothèques requises, défini une classe d'emballage pour nmslib, défini une fonction pour charger l'ensemble de données MNIST, effectué un benchmark des différents transformateurs de plus proches voisins et visualisé les imbrications TSNE. Nous avons appris que l'estimateur TSNE par défaut avec son implémentation interne NearestNeighbors est approximativement équivalent au pipeline avec TSNE et KNeighborsTransformer en termes de performance. Nous avons également appris que le NMSlibTransformer approximatif est déjà légèrement plus rapide que la recherche exacte sur le plus petit ensemble de données, mais cette différence de vitesse devrait devenir plus significative sur des ensembles de données avec un plus grand nombre d'échantillons.