Vizinhos Mais Próximos Aproximados em TSNE

Beginner

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

Introdução

Neste laboratório, aprenderemos a utilizar vizinhos mais próximos aproximados em TSNE utilizando a biblioteca scikit-learn do Python.

Dicas da Máquina Virtual

Após o arranque da máquina virtual, clique no canto superior esquerdo para mudar para a aba Notebook para aceder ao Jupyter Notebook para a prática.

Por vezes, pode ser necessário esperar alguns segundos para o Jupyter Notebook terminar o carregamento. A validação das operações não pode ser automatizada devido a limitações no Jupyter Notebook.

Se tiver problemas durante o aprendizado, não hesite em contactar o Labby. Forneça feedback após a sessão e resolveremos o problema rapidamente para si.

Instalar Pacotes Necessários

Precisamos instalar os pacotes nmslib e pynndescent. Estes pacotes podem ser instalados usando o comando pip.

!pip install nmslib pynndescent

Importar Bibliotecas Necessárias

Precisamos importar as bibliotecas necessárias, incluindo nmslib, pynndescent, sklearn, numpy, scipy e 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

Definir uma Classe Wrapper para nmslib

Definimos uma classe wrapper para nmslib para implementar a API scikit-learn no nmslib, bem como uma função de carregamento. A classe NMSlibTransformer recebe n_neighbors, metric, method e n_jobs como parâmetros. O método fit() inicializa o nmslib e adiciona os pontos de dados a ele. O método transform() encontra os vizinhos mais próximos e retorna uma matriz esparsa.

class NMSlibTransformer(TransformerMixin, BaseEstimator):
    """Wrapper for using nmslib as sklearn's KNeighborsTransformer"""

    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]

        ## veja mais métricas no manual
        ## 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]

        ## Por razões de compatibilidade, como cada amostra é considerada como seu próprio
        ## vizinho, um vizinho extra será calculado.
        n_neighbors = self.n_neighbors + 1

        if self.n_jobs < 0:
            ## Mesma manipulação feita no joblib para valores negativos de n_jobs:
            ## em particular, `n_jobs == -1` significa "tantas threads quantos os CPUs".
            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

Definir uma Função para Carregar o Conjunto de Dados MNIST

Definimos uma função load_mnist() para carregar o conjunto de dados MNIST, embaralhar os dados e retornar apenas o número especificado de amostras.

def load_mnist(n_samples):
    """Load MNIST, shuffle the data, and return only 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]

Comparação de Desempenho de Transformadores de Vizinhos Mais Próximos

Neste exemplo, comparamos o desempenho de diferentes transformadores de vizinhos mais próximos, exatos e aproximados. Definimos os conjuntos de dados, os transformadores e os parâmetros, incluindo n_iter, perplexity, metric e n_neighbors. Medimos o tempo necessário para ajustar e transformar cada transformador em cada conjunto de dados. Imprimimos o tempo gasto em cada etapa para cada transformador.

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 not supported for sparse matrices
    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"Comparando em {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 (ajuste)")
        start = time.time()
        Xt = transformer.transform(X)
        transform_duration = time.time() - start
        print(f"{transformer_name:<{longest}} {transform_duration:.3f} sec (transformação)")
        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"
                " (transformação)"
            )

Visualizar a Inclusão TSNE

Visualizamos as inclusões TSNE usando diferentes transformadores de vizinhos mais próximos. Definimos transformers como uma lista contendo três pipelines: TSNE com NearestNeighbors interno, TSNE com KNeighborsTransformer e TSNE com NMSlibTransformer. Iteramos sobre os conjuntos de dados e transformadores e plotamos as inclusões TSNE, que devem ser semelhantes entre os métodos. Mostramos o gráfico no final.

transformers = [
    ("TSNE com NearestNeighbors interno", TSNE(metric=metric, **tsne_params)),
    (
        "TSNE com KNeighborsTransformer",
        make_pipeline(
            KNeighborsTransformer(
                n_neighbors=n_neighbors, mode="distance", metric=metric
            ),
            TSNE(metric="precomputed", **tsne_params),
        ),
    ),
    (
        "TSNE com 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"Comparando em {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 + "\nem " + 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()

Resumo

Neste laboratório, aprendemos a utilizar vizinhos mais próximos aproximados em TSNE utilizando a biblioteca scikit-learn do Python. Importamos as bibliotecas necessárias, definimos uma classe wrapper para nmslib, definimos uma função para carregar o conjunto de dados MNIST, comparamos o desempenho de diferentes transformadores de vizinhos mais próximos e visualizamos as inclusões TSNE. Aprendemos que o estimador TSNE padrão com sua implementação interna de NearestNeighbors é aproximadamente equivalente ao pipeline com TSNE e KNeighborsTransformer em termos de desempenho. Também aprendemos que o NMSlibTransformer aproximado é ligeiramente mais rápido que a busca exata no conjunto de dados menor, mas essa diferença de velocidade deve se tornar mais significativa em conjuntos de dados com um número maior de amostras.