Всем привет! В нашем курсе частично поднималась тема Manifold Learning, но для большей ясности думаю стоит еще раз вспомнить про эту область машинного обучения и немного глубже затронуть ее аспекты.
Manifold Learning – метод нелинейного уменьшения размерности, при котором обычно размерность настолько большая, что интерпретировать данные становится сложно.
К таким алгоритмам относятся t-SNE и uMAP, а к алгоритмам линейного уменьшения размерности – PCA, SVD. Часто размерность уменьшают до 2 или 3, чтобы данные целиком можно было бы отобразить на двумерное или трехмерное пространство.
Тут надо подметить, что если размерность экстремально большая и исчисляется десятками тысяч, то нужно использовать более эффективные методы уменьшения размерности, например линейные PCA и SVD (но никто не запрещает использовать сначала PCA, а потом TSNE)
Импортируем нужные библиотеки
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
%matplotlib inline
import os
from sklearn.decomposition import TruncatedSVD
from sklearn.manifold import TSNE
Для установки Multicore TSNE может понадобиться cmake и gcc
На Windows склонировать репозиторий куда угодно и запустить pip install на эту директорию
Возможно придется заменить pip на pip3, если запускаете не из virtualenv
!git clone https://github.com/DmitryUlyanov/Multicore-TSNE.git && pip install Multicore-TSNE/ && rm -rf Multicore-TSNE/
from MulticoreTSNE import MulticoreTSNE
Установим uMAP, для установки может потребоваться установить numba вручную, но все должно встать автоматически
!pip install umap-learn
# так же, возможно придется заменить на pip3
from umap import UMAP
Загрузим первый датасет 20_newsgroups
# from sklearn.datasets import fetch_20newsgroups
# newsgroups = fetch_20newsgroups('all')
# Можно было бы их скачать таким образом, но из-за блокировки серверов AWS РКН ссылка не работает
Поэтому я нашел оригинальную ссылку, на Windows придется скачать и распаковать вручную и закинуть папку 20news-18828 в /data
!wget -O ../../data/20news-18828.tar.gz http://qwone.com/~jason/20Newsgroups/20news-18828.tar.gz && tar -xzf ../../data/20news-18828.tar.gz -C ../../data && rm ../../data/20news-18828.tar.gz
Текст нужно препроцессить, используем TfIdfVectorizer со следующими параметрами:
input='filename'
: использует напрямую пути к файлам, векторизатор умеет их читать сам.
decode_error='ignore'
: пропускать проблемные участки текстов (не все из них закодированы удачно, т.к. мы скачали датасет напрямую, а проводить тщательный анализ текстов не входит в цель данного урока) Мы увидим, что так мы не пропустим ни один файл.
max_features=100000
: Для наглядности ограничим размерность X 100000, вообще полная размерность была бы 170000
from sklearn.feature_extraction.text import TfidfVectorizer
# названия классов = названия директорий, которые содержат файлы с текстами
labels = [a for a in os.listdir("../../data/20news-18828/") if a != ".DS_Store"]
X = []
y = []
for label in labels:
base_path = f"../../data/20news-18828/{label}"
filepaths = os.listdir(base_path)
X.extend([os.path.join(base_path, filepath) for filepath in filepaths])
y.extend([label] * len(filepaths))
X_trans = TfidfVectorizer(input='filename', decode_error='ignore', max_features=100000).fit_transform(X)
# Проверяем, что не потеряли никаких файлов
assert len(X) == X_trans.shape[0] == len(y)
X_trans.shape
Наша исходная размерность – 100000 и 20 классов, данные разреженые
# цвета для каждого класса – будет использоваться на графике
from matplotlib.cm import get_cmap
cm = get_cmap("gist_rainbow")
unique_y = list(np.unique(y))
N_COLORS = len(unique_y)
colors = dict(zip(np.unique(y), [cm(unique_y.index(l) / N_COLORS) for l in unique_y]))
# для отображения легенды
import matplotlib.patches as mpatches
patches = [mpatches.Patch(color=colors[l], label=l) for l in unique_y]
Сначала попробуем просто отобразить данные на плоскость с помощью линейных методов.
Может весь туториал зря и можно просто было сделать по-простому?
X_svd_2 = TruncatedSVD(2).fit_transform(X_trans)
plt.figure(figsize(18, 14))
plt.scatter(X_svd_2[:, 0], X_svd_2[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Не зря. Зато выглядит симпатично.
Казалось бы: ну все, теперь мы преодбработали данные, имеем вектора, размерность большая: закинем все в TSNE и получим красивую визуализацию, но нет.
Как было написано в начале, нелинейные методы не способны обработать экстремально большие размерности. t-SNE уменьшает размерность данных опираясь на локальные свойства, поэтому он не сработает на данных с очень большой размерностью.
Решение – использовать линейное уменьшение размерности и уже на нем прогонять все остальные алгоритмы.
# Для sparse данных идеально подойдет TruncatedSVD
X_svd = TruncatedSVD(100).fit_transform(X_trans)
Теперь мы готовы к уменьшению размерности
Рассмотрим некоторые параметры, которые есть у TSNE:
n_components
: Результирующая размерность. TSNE устроен так, что результаты
будут плохо интерпретироваться при значениях больше 3 (а некоторые реализации и
вовсе не поддерживают эти значения) Поэтому TSNE используется больше для визуализации.По умолчанию – 2
perplexity
: Количество соседних точек, будет определять качество
представления данных. Большие датасеты требуют большого значения,
Рекомендуется ставить значение от 5 до 50, но TSNE не чуствителен к этому параметру.По умолчанию – 30
metric
: Выбор метрики для подсчета расстояния между точками. Они есть здесь.По умолчанию - euclidean
У нашей "быстрой" реализации TSNE есть параметр n_jobs, который сильно выручает, ставим его -1 (использовать все ядра).
P.S. Было решено даже не использовать обычную версию TSNE из sklearn, предполагается, что читателю известно, что выполнение не завершится за приемлемое время, так что я даже решил не ждать.
%%time
X_tsne = MulticoreTSNE(n_jobs=-1).fit_transform(X_svd)
plt.figure(figsize(18, 14))
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Видно, что евклидова метрика (по умолчанию) справляется плохо. Для векторов, получаемых из текста обычно используется косинусная мера.
%%time
X_tsne_cosine = MulticoreTSNE(metric='cosine', n_jobs=-1).fit_transform(X_svd)
plt.figure(figsize(18, 14))
plt.scatter(X_tsne_cosine[:, 0], X_tsne_cosine[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Уже лучше, хоть в центре все равно не разобрать происходящее
Попробуем увеличить perplexity
%%time
X_tsne_cosine_perplexity = MulticoreTSNE(perplexity=40, metric='cosine', n_jobs=-1).fit_transform(X_svd)
plt.figure(figsize(18, 14))
plt.scatter(X_tsne_cosine_perplexity[:, 0], X_tsne_cosine_perplexity[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Попробуем уменьшить perplexity
%%time
X_tsne_cosine_perplexity2 = MulticoreTSNE(perplexity=12, metric='cosine', n_jobs=-1).fit_transform(X_svd)
plt.figure(figsize(18, 14))
plt.scatter(X_tsne_cosine_perplexity2[:, 0], X_tsne_cosine_perplexity2[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Мы убедились, что параметр perplexity по большому счету ни на что не влияет и TSNE почти не конфигурируется
Рассмотрим параметры, которые есть у uMAP:
n_neighbors
: Количество соседних точек, будет определять качество
представления данных. Большое значение повлечет за собой более глобальную структуру,
потеряются особенности данных в небольшой окрестности. Рекомендуется ставить значение
от 5 до 50, подстраивая под свои нужды. Стандартно от 10 до 15.
min_dist
: Минимальная дистанция между точками в итоговом представлении.
Большие значения более равномерно распределят точки, когда как маленькие
восстановят зависимости в локальной окрестности. Рекомендуется изменять от
0.001 до 0.5. Стандартно – 0.1.
metric
: Выбор метрики для подсчета расстояния между точками. Можно задать свою
функцию, предварительно оформив ее декоратором numba.jit
%%time
X_umap = UMAP(metric='cosine').fit_transform(X_svd)
Уже сейчас заметно ускорение в ~10 раз!
plt.figure(figsize(18, 14))
plt.scatter(X_umap[:, 0], X_umap[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Выглядит тоже довольно симпатично, но вот какие-то отдаленные точки уж очень напоминают аутлаеры, а внутри наоборот все довольно плотно. Поставим минимальную дистанцию в 2 раза больше и кол-во соседей поменьше
%%time
X_umap_tuned = UMAP(n_neighbors=8, min_dist=0.2, metric='cosine').fit_transform(X_svd)
plt.figure(figsize(18, 14))
plt.scatter(X_umap_tuned[:, 0], X_umap_tuned[:, 1], s=3, c=[colors[l] for l in y])
plt.legend(handles=patches)
Вот такое представление данных мне нравится больше! Но что если данные в центре просто не способны отобразиться на плоскость?
Отобразим на 3-мерное пространство
%%time
X_umap_tuned_3d = UMAP(n_components=3, n_neighbors=8, min_dist=0.2, metric='cosine').fit_transform(X_svd)
from mpl_toolkits.mplot3d import Axes3D
ax = plt.subplot(111, projection='3d')
plt.figure(figsize(18, 14))
ax.scatter(X_umap_tuned_3d[:, 0], X_umap_tuned_3d[:, 1], X_umap_tuned_3d[:, 2], s=3, c=[colors[l] for l in y])
ax.set_zlim3d(-5, 4)
ax.set_ylim3d(-5, 5)
ax.set_xlim3d(-4, 4)
ax.legend(handles=patches, loc='upper left', numpoints=1, ncol=3, fontsize=8, bbox_to_anchor=(0, 0))
Теперь, когда мы построили неплохое распределение мы можем обучить логистическую регрессию на исходных (не уменьшенных) векторах и попытаться сопоставить те, которые были плохо выделены в кластер с их итоговым скором.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
X_train, X_test, y_train, y_test = train_test_split(X_trans, LabelEncoder().fit_transform(y))
y_predicted = LogisticRegression().fit(X_train, y_train).predict(X_test)
print(classification_report(y_test, y_predicted, target_names=unique_y))
Все точки из категории 'rec' (зеленого цвета) хорошо выделены в кластеры, это подтверждается большим скором в таблице
'talk.religion.misc' определяется плохо, оно и понятно, ведь это категория "прочее"
Мы еще раз за время курса попробовали уменьшить размерность данных и визуализировать данные, познакомились с библиотекой UMAP, которая (на мой взгляд) строит более совершенные визуализации с возможностью настройки, да еще и намного быстрее.