Обновление: 29 апреля 2019 г. Обновлена ​​часть кода, чтобы не использовать ggplot, а вместо этого использовать seaborn и matplotlib. Я также добавил пример для 3D-графика. Я также изменил синтаксис для работы с Python3.

Первый шаг к решению любой проблемы, связанной с данными, - это начать с изучения самих данных. Это может быть, например, рассмотрение распределения определенных переменных или изучение потенциальных корреляций между переменными.

В настоящее время проблема заключается в том, что большинство наборов данных содержат большое количество переменных. Другими словами, они имеют большое количество измерений, по которым распределяются данные. Визуальное изучение данных может стать сложной задачей, и в большинстве случаев это практически невозможно сделать вручную. Однако такое визуальное исследование невероятно важно в любой проблеме, связанной с данными. Поэтому очень важно понимать, как визуализировать многомерные наборы данных. Это может быть достигнуто с помощью методов, известных как уменьшение размерности. Этот пост будет посвящен двум методам, которые позволят нам это сделать: PCA и t-SNE.

Подробнее об этом позже. Давайте сначала получим некоторые (многомерные) данные для работы.

Набор данных MNIST

В этой статье мы будем использовать MNIST-dataset. Нет необходимости загружать набор данных вручную, так как мы можем получить его с помощью Scikit Learn.

Сначала давайте разместим все библиотеки.

from __future__ import print_function
import time
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_mldata
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
%matplotlib inline
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns

а затем начнем с загрузки данных

mnist = fetch_mldata("MNIST original")
X = mnist.data / 255.0
y = mnist.target
print(X.shape, y.shape)
[out] (70000, 784) (70000,)

Мы собираемся преобразовать матрицу и вектор в DataFrame Pandas. Это очень похоже на DataFrames, используемые в R, и облегчит нам построение графика позже.

feat_cols = [ 'pixel'+str(i) for i in range(X.shape[1]) ]
df = pd.DataFrame(X,columns=feat_cols)
df['y'] = y
df['label'] = df['y'].apply(lambda i: str(i))
X, y = None, None
print('Size of the dataframe: {}'.format(df.shape))
[out] Size of the dataframe: (70000, 785)

Поскольку мы не хотим использовать 70 000 цифр в некоторых вычислениях, мы возьмем случайное подмножество цифр. Рандомизация важна, поскольку набор данных сортируется по его метке (т.е. первые семь тысяч или около того - нули и т. Д.). Чтобы обеспечить рандомизацию, мы создадим случайную перестановку числа от 0 до 69 999, что позволит нам позже выбрать первые пять или десять тысяч для наших расчетов и визуализаций.

# For reproducability of the results
np.random.seed(42)
rndperm = np.random.permutation(df.shape[0])

Теперь у нас есть фрейм данных и вектор рандомизации. Давайте сначала проверим, как на самом деле выглядят эти числа. Для этого мы сгенерируем 30 графиков случайно выбранных изображений.

plt.gray()
fig = plt.figure( figsize=(16,7) )
for i in range(0,15):
    ax = fig.add_subplot(3,5,i+1, title="Digit: {}".format(str(df.loc[rndperm[i],'label'])) )
    ax.matshow(df.loc[rndperm[i],feat_cols].values.reshape((28,28)).astype(float))
plt.show()

Теперь мы можем начать думать о том, как на самом деле отличить нули от единиц, двоек и так далее. Если бы вы были, например, почтовым отделением, такой алгоритм мог бы помочь вам читать и сортировать рукописные конверты с помощью машины, а не людей. Очевидно, что в настоящее время у нас есть очень продвинутые методы для этого, но этот набор данных по-прежнему является очень хорошей площадкой для тестирования, чтобы увидеть, как работают конкретные методы уменьшения размерности и насколько хорошо они работают.

Все изображения, по сути, представляют собой изображения размером 28 на 28 пикселей и, следовательно, имеют в общей сложности 784 «размера», каждое из которых содержит значение одного конкретного пикселя.

Что мы можем сделать, так это резко уменьшить количество измерений, пытаясь сохранить как можно больше «вариаций» в информации. Вот где мы подходим к уменьшению размерности. Давайте сначала взглянем на что-то, известное как Анализ главных компонентов.

Снижение размерности с помощью PCA

PCA - это метод уменьшения количества измерений в наборе данных при сохранении большей части информации. Он использует корреляцию между некоторыми измерениями и пытается предоставить минимальное количество переменных, которые сохраняют максимальное количество вариаций или информацию о том, как распределяются исходные данные. Он делает это не наугад, а на основе точной математики и использует нечто, известное как собственные значения и собственные векторы матрицы данных. Эти собственные векторы ковариационной матрицы обладают тем свойством, что они указывают вдоль основных направлений изменения данных. Это направления максимальных вариаций в наборе данных.

Я не собираюсь вдаваться в фактический вывод и расчет основных компонентов - если вы хотите углубиться в математику, посмотрите эту отличную страницу - вместо этого мы воспользуемся реализацией Scikit-Learn PCA.

Поскольку нам, людям, нравятся наши двух- и трехмерные графики, давайте начнем с них и сгенерируем из исходных 784 измерений первые три основных компонента. И мы также увидим, сколько вариаций в общем наборе данных они фактически учитывают.

pca = PCA(n_components=3)
pca_result = pca.fit_transform(df[feat_cols].values)
df['pca-one'] = pca_result[:,0]
df['pca-two'] = pca_result[:,1] 
df['pca-three'] = pca_result[:,2]
print('Explained variation per principal component: {}'.format(pca.explained_variance_ratio_))
Explained variation per principal component: [0.09746116 0.07155445 0.06149531]

Теперь, учитывая, что на первые два компонента приходится около 25% вариации всего набора данных, давайте посмотрим, достаточно ли этого, чтобы визуально разделить разные цифры. Что мы можем сделать, так это создать диаграмму рассеяния первого и второго главных компонентов и раскрасить каждый из различных типов цифр разным цветом. Если нам повезет, цифры одного и того же типа будут расположены (т. Е. Сгруппированы) вместе в группы, что будет означать, что первые два основных компонента на самом деле многое говорят нам о конкретных типах цифр.

plt.figure(figsize=(16,10))
sns.scatterplot(
    x="pca-one", y="pca-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df.loc[rndperm,:],
    legend="full",
    alpha=0.3
)

На графике мы видим, что эти два компонента определенно содержат некоторую информацию, особенно для конкретных цифр, но явно недостаточно, чтобы разделить их все. К счастью, есть еще один метод, который мы можем использовать для уменьшения количества измерений, который может оказаться более полезным. В следующих нескольких абзацах мы рассмотрим эту технику и исследуем, дает ли она нам лучший способ уменьшения размеров для визуализации. Метод, который мы будем исследовать, известен как t-SNE (t-распределенные стохастические соседние объекты).

За 3d-версию того же сюжета

ax = plt.figure(figsize=(16,10)).gca(projection='3d')
ax.scatter(
    xs=df.loc[rndperm,:]["pca-one"], 
    ys=df.loc[rndperm,:]["pca-two"], 
    zs=df.loc[rndperm,:]["pca-three"], 
    c=df.loc[rndperm,:]["y"], 
    cmap='tab10'
)
ax.set_xlabel('pca-one')
ax.set_ylabel('pca-two')
ax.set_zlabel('pca-three')
plt.show()

T-распределенные стохастические соседние объекты (t-SNE)

t-распределенное стохастическое соседнее вложение (t-SNE) - это еще один метод уменьшения размерности, который особенно хорошо подходит для визуализации многомерных наборов данных. В отличие от PCA это не математический метод, а вероятностный. Оригинальный документ описывает работу t-SNE как:

«t-распределенное стохастическое вложение соседей (t-SNE) минимизирует расхождение между двумя распределениями: распределением, которое измеряет попарное сходство входных объектов, и распределением, которое измеряет попарное сходство соответствующих низкоразмерных точек во встраивании» .

По сути, это означает, что он смотрит на исходные данные, которые вводятся в алгоритм, и смотрит, как лучше всего представить эти данные с использованием меньших измерений, сопоставив оба распределения. Способ, которым он это делает, довольно сложен в вычислительном отношении, и поэтому существуют некоторые (серьезные) ограничения на использование этого метода. Например, одна из рекомендаций заключается в том, что в случае данных очень высокой размерности вам может потребоваться применить другой метод уменьшения размерности перед использованием t-SNE:

|  It is highly recommended to use another dimensionality reduction
|  method (e.g. PCA for dense data or TruncatedSVD for sparse data)
|  to reduce the number of dimensions to a reasonable amount (e.g. 50)
|  if the number of features is very high.

Другой ключевой недостаток заключается в том, что он:

«Поскольку t-SNE квадратично масштабируется по количеству объектов N, его применимость ограничена наборами данных с несколькими тысячами входных объектов; кроме того, обучение становится слишком медленным, чтобы быть практичным (а требования к памяти становятся слишком большими) ».

В оставшейся части этой статьи мы будем использовать Реализацию Scikit-Learn алгоритма.

Вопреки приведенной выше рекомендации мы сначала попробуем запустить алгоритм на фактических размерах данных (784) и посмотрим, как это работает. Чтобы не перегружать нашу машину памятью и мощностью / временем, мы будем использовать только первые 10 000 выборок для запуска алгоритма. Для сравнения позже я также снова запущу PCA на подмножестве.

N = 10000
df_subset = df.loc[rndperm[:N],:].copy()
data_subset = df_subset[feat_cols].values
pca = PCA(n_components=3)
pca_result = pca.fit_transform(data_subset)
df_subset['pca-one'] = pca_result[:,0]
df_subset['pca-two'] = pca_result[:,1] 
df_subset['pca-three'] = pca_result[:,2]
print('Explained variation per principal component: {}'.format(pca.explained_variance_ratio_))
[out] Explained variation per principal component: [0.09730166 0.07135901 0.06183721]

x

time_start = time.time()
tsne = TSNE(n_components=2, verbose=1, perplexity=40, n_iter=300)
tsne_results = tsne.fit_transform(data_subset)
print('t-SNE done! Time elapsed: {} seconds'.format(time.time()-time_start))
[out] [t-SNE] Computing 121 nearest neighbors...
[t-SNE] Indexed 10000 samples in 0.564s...
[t-SNE] Computed neighbors for 10000 samples in 121.191s...
[t-SNE] Computed conditional probabilities for sample 1000 / 10000
[t-SNE] Computed conditional probabilities for sample 2000 / 10000
[t-SNE] Computed conditional probabilities for sample 3000 / 10000
[t-SNE] Computed conditional probabilities for sample 4000 / 10000
[t-SNE] Computed conditional probabilities for sample 5000 / 10000
[t-SNE] Computed conditional probabilities for sample 6000 / 10000
[t-SNE] Computed conditional probabilities for sample 7000 / 10000
[t-SNE] Computed conditional probabilities for sample 8000 / 10000
[t-SNE] Computed conditional probabilities for sample 9000 / 10000
[t-SNE] Computed conditional probabilities for sample 10000 / 10000
[t-SNE] Mean sigma: 2.129023
[t-SNE] KL divergence after 250 iterations with early exaggeration: 85.957787
[t-SNE] KL divergence after 300 iterations: 2.823509
t-SNE done! Time elapsed: 157.3975932598114 seconds

Теперь, когда у нас есть два результирующих измерения, мы можем снова визуализировать их, создав диаграмму рассеяния двух измерений и раскрасив каждый образец соответствующей меткой.

df_subset['tsne-2d-one'] = tsne_results[:,0]
df_subset['tsne-2d-two'] = tsne_results[:,1]
plt.figure(figsize=(16,10))
sns.scatterplot(
    x="tsne-2d-one", y="tsne-2d-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df_subset,
    legend="full",
    alpha=0.3
)

Это уже значительное улучшение по сравнению с визуализацией PCA, которую мы использовали ранее. Мы можем видеть, что цифры очень четко сгруппированы в свои собственные подгруппы. Если бы мы теперь использовали алгоритм кластеризации для выделения отдельных кластеров, мы, вероятно, могли бы довольно точно назначить новые точки метке. Просто для сравнения PCA и T-SNE:

plt.figure(figsize=(16,7))
ax1 = plt.subplot(1, 2, 1)
sns.scatterplot(
    x="pca-one", y="pca-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df_subset,
    legend="full",
    alpha=0.3,
    ax=ax1
)
ax2 = plt.subplot(1, 2, 2)
sns.scatterplot(
    x="tsne-2d-one", y="tsne-2d-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df_subset,
    legend="full",
    alpha=0.3,
    ax=ax2
)

Теперь мы примем рекомендации близко к сердцу и фактически уменьшим количество измерений, прежде чем вводить данные в алгоритм t-SNE. Для этого мы снова будем использовать PCA. Сначала мы создадим новый набор данных, содержащий пятьдесят измерений, сгенерированных алгоритмом редукции PCA. Затем мы можем использовать этот набор данных для выполнения t-SNE на

pca_50 = PCA(n_components=50)
pca_result_50 = pca_50.fit_transform(data_subset)
print('Cumulative explained variation for 50 principal components: {}'.format(np.sum(pca_50.explained_variance_ratio_)))
[out] Cumulative explained variation for 50 principal components: 0.8267618822147329

Удивительно, но на первые 50 компонентов приходится примерно 85% общего разброса данных.

Теперь давайте попробуем передать эти данные в алгоритм t-SNE. На этот раз мы будем использовать 10 000 сэмплов из 70 000, чтобы убедиться, что алгоритм не занимает слишком много памяти и ЦП. Поскольку код, используемый для этого, очень похож на предыдущий код t-SNE, я переместил его в раздел Приложение: Код внизу этого сообщения. Сюжет, который он произвел, следующий:

На этом графике мы можем ясно видеть, как все образцы хорошо разнесены и сгруппированы вместе с соответствующими цифрами. Это может быть отличной отправной точкой для последующего использования алгоритма кластеризации и попытки идентифицировать кластеры или фактически использовать эти два измерения в качестве входных данных для другого алгоритма (например, чего-то вроде нейронной сети).

Итак, мы исследовали использование различных методов уменьшения размерности для визуализации многомерных данных с помощью двухмерной диаграммы рассеяния. Мы не вдавались в фактическую математику, а вместо этого полагались на реализации всех алгоритмов Scikit-Learn.

Обзорный отчет

Прежде чем закончить с приложением…

Вместе с друзьями-единомышленниками мы рассылаем еженедельные информационные бюллетени с некоторыми ссылками и заметками, которыми мы хотим поделиться между собой (почему бы не позволить другим читать их?).

Приложение: Код

Код: t-SNE на данных с сокращенным PCA

time_start = time.time()
tsne = TSNE(n_components=2, verbose=0, perplexity=40, n_iter=300)
tsne_pca_results = tsne.fit_transform(pca_result_50)
print('t-SNE done! Time elapsed: {} seconds'.format(time.time()-time_start))
[out] t-SNE done! Time elapsed: 42.01495909690857 seconds

А для визуализации

df_subset['tsne-pca50-one'] = tsne_pca_results[:,0]
df_subset['tsne-pca50-two'] = tsne_pca_results[:,1]
plt.figure(figsize=(16,4))
ax1 = plt.subplot(1, 3, 1)
sns.scatterplot(
    x="pca-one", y="pca-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df_subset,
    legend="full",
    alpha=0.3,
    ax=ax1
)
ax2 = plt.subplot(1, 3, 2)
sns.scatterplot(
    x="tsne-2d-one", y="tsne-2d-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df_subset,
    legend="full",
    alpha=0.3,
    ax=ax2
)
ax3 = plt.subplot(1, 3, 3)
sns.scatterplot(
    x="tsne-pca50-one", y="tsne-pca50-two",
    hue="y",
    palette=sns.color_palette("hls", 10),
    data=df_subset,
    legend="full",
    alpha=0.3,
    ax=ax3
)