TSNE 에서 근사 최근접 이웃 사용하기

Beginner

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

소개

이 실습에서는 Python 의 scikit-learn 라이브러리를 사용하여 TSNE 에서 근사 최근접 이웃을 활용하는 방법을 배웁니다.

VM 팁

VM 시작이 완료되면 왼쪽 상단 모서리를 클릭하여 Notebook 탭으로 전환하여 연습용 Jupyter Notebook에 접근합니다.

때때로 Jupyter Notebook 이 로드되는 데 몇 초 정도 기다려야 할 수 있습니다. Jupyter Notebook 의 제한으로 인해 작업 검증은 자동화될 수 없습니다.

학습 중 문제가 발생하면 Labby 에게 문의하십시오. 세션 후 피드백을 제공하면 문제를 신속하게 해결해 드리겠습니다.

필요한 패키지 설치

nmslibpynndescent 패키지를 설치해야 합니다. 이 패키지는 pip 명령어를 사용하여 설치할 수 있습니다.

!pip install nmslib pynndescent

필요한 라이브러리 가져오기

필요한 라이브러리, nmslib, pynndescent, sklearn, numpy, scipy, 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

nmslib 래퍼 클래스 정의

nmslib를 위한 래퍼 클래스를 정의하여 nmslib에 scikit-learn API 를 구현하고, 로딩 함수를 포함합니다. NMSlibTransformer 클래스는 n_neighbors, metric, method, n_jobs를 매개변수로 받습니다. fit() 메서드는 nmslib를 초기화하고 데이터 포인트를 추가합니다. transform() 메서드는 가장 가까운 이웃을 찾아 희소 행렬을 반환합니다.

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]

        ## 더 많은 메트릭은 매뉴얼 참조
        ## 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]

        ## 호환성을 위해 각 샘플을 자체 이웃으로 간주하므로
        ## 하나의 추가 이웃이 계산됩니다.
        n_neighbors = self.n_neighbors + 1

        if self.n_jobs < 0:
            ## n_jobs 의 음수 값에 대한 joblib 에서 수행된 동일한 처리:
            ## 특히, `n_jobs == -1`은 "CPU 개수만큼의 스레드"를 의미합니다.
            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

MNIST 데이터셋 로드 함수 정의

MNIST 데이터셋을 로드하고, 데이터를 섞은 후 지정된 샘플 수만 반환하는 함수 load_mnist()를 정의합니다.

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]

다양한 최근접 이웃 변환기 벤치마크

다양한 정확/근사 최근접 이웃 변환기를 벤치마킹합니다. n_iter, perplexity, metric, n_neighbors 등 데이터셋, 변환기 및 매개변수를 정의합니다. 각 데이터셋에서 각 변환기를 적용하는 데 걸리는 시간을 측정하고, 각 변환기의 적합 및 변환 시간을 출력합니다.

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"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)"
            )

TSNE 임베딩 시각화

다양한 최근접 이웃 변환기를 사용하여 TSNE 임베딩을 시각화합니다. transformers는 세 가지 파이프라인 (내부 NearestNeighbors를 사용한 TSNE, KNeighborsTransformer를 사용한 TSNE, NMSlibTransformer를 사용한 TSNE) 을 포함하는 리스트로 정의합니다. 데이터셋과 변환기를 반복하며 TSNE 임베딩을 플롯합니다. 이는 여러 방법에서 유사해야 합니다. 마지막에 플롯을 표시합니다.

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()

요약

이 실험에서 Python 의 scikit-learn 라이브러리를 사용하여 TSNE 에서 근사 최근접 이웃을 활용하는 방법을 배웠습니다. 필요한 라이브러리를 가져오고 nmslib를 위한 래퍼 클래스를 정의하고 MNIST 데이터셋을 로드하는 함수를 정의하고, 다양한 최근접 이웃 변환기를 벤치마킹하고 TSNE 임베딩을 시각화했습니다. 기본 TSNE 추정기는 내부 NearestNeighbors 구현과 함께 TSNEKNeighborsTransformer를 사용하는 파이프라인과 성능 측면에서 거의 동일하다는 것을 알게 되었습니다. 또한 근사 NMSlibTransformer가 가장 작은 데이터셋에서 정확한 검색보다 약간 빠르다는 것을 알게 되었으며, 샘플 수가 더 많은 데이터셋에서는 이 속도 차이가 더욱 두드러질 것으로 예상됩니다.