Обновление: 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 )