Метод t-SNE – стохастическое соседское вложение с распределением Стьюдента

Метод t-SNE. Теория

Здравствуйте и вновь добро пожаловать на занятия по теме «Глубокое обучение без учителя на языке Python».

В этой лекции мы поговорим о другом методе уменьшения размерности и визуализации данных, называющийся методом t-SNE. «t» в данном случае значит, что будет рассматриваться распределение Стьюдента, а «SNE» можно расшифровать как «стохастическое вложение соседей» (stohastic neighbor embedding). Большое преимущество метода t-SNE состоит в том, что он является нелинейным, а потому и более наглядным, нежели метод основных компонент.

Итак, зачем же нужно знать метод t-SNE?

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

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

В исходных данных X определим общее распределение вероятностей p(i,j), равное следующему:

Заметьте, что оно похоже на гауссово распределение; при этом σ можно рассматривать как гиперпараметр.

Далее мы получаем низкоразмерное отображение Y, которое мы определим таким же образом, но уже без σ:

Обратите внимание, что при этом мы считаем, что p(i,i)=q(i,i)=0, поскольку для нас не имеет смысла вопрос, насколько далеко что-то отстоит от самого себя.

Далее, если не знаете, как сравнивать распределения вероятностей, то отмечу, что, как правило, для этого используется так называемое расстояние Кульбака-Лейблера, которое и будет нашей функцией затрат:

C=KL(P|\;|Q)=\sum_{I} \sum_{J} p_{ij} \; log \frac{p_{ij}}{q_{ij}}.

Оно будет равно нулю, когда два распределения вероятностей в точности одинаковы.

И наконец, как нам этого добиться? Да точно так же, как мы до этого решали задачи подобного рода, – берём производную функции затрат и используем градиентный спуск. Обратите внимание, что в данной модели у нас нет никаких весовых коэффициентов – у нас есть лишь Y, так что находим градиент относительно Y, которое и является собственно исходящим отображением.

Одна из проблема симметричного t-SNE, существовавшая, впрочем, и до него, – так называемая проблема скученности, заключающаяся в препятствии образованию интервалов вокруг естественных кластеров. Для её предотвращения в методе t-SNE используются несколько различные распределения для q и p, что позволяет разместить кластеры несколько удачнее:

Функция стоимости при этом остаётся прежней.

Мы не будем здесь находить производную, поскольку библиотека Theano позволяет сделать это автоматически.

При этом прошу заметить, что на самом деле мы не будем реализовывать метод t-SNE, но по другим причинам, нежели те, по которым мы не реализовали метод главных компонент. Как вы могли убедиться, формулировка метода t-SNE на самом деле весьма проста и интуитивно понятна, при этом легко определить функцию стоимости и её производную. Но дело в том, что он крайне медлителен и для своей работы требует огромного количества оперативной памяти. В действительности при использовании метода t-SNE с полным набором данных ваш компьютер, скорее всего, просто зависнет. Во-первых, потому что нам необходимо вычислить q(i,j) и p(i,j) для i и j, пробегающих значения от 1 до N – то есть фактически мы имеем квадратичную сложность алгоритма.

Есть, правда, вариант метода t-SNE, называющийся алгоритмом Барнса-Хата и имеющий лишь логарифмическую сложность, но и он требует очень большой оперативной памяти. Хотя этот метод используется по умолчанию в библиотеке Sci-Kit Learn.

Таким образом, даже если мы попытаемся использовать метод t-SNE напрямую, он не будет работать, так что нам остаётся лишь пользоваться тем, что предоставляют нам соответствующие библиотеки.

Во-вторых же, даже при использовании библиотек метод всё равно остаётся крайне медленным и ресурсоёмким, поэтому мы будем использовать наборы данных лишь в несколько сотен примеров и применять метод t-SNE на них. Хотя, разумеется, вы можете использовать наборы данных и побольше, если готовы ждать и у вас есть достаточно оперативной памяти.

Метод t-SNE и «проблема пончика»

Сейчас мы применим метод t-SNE на нашей любимой шутливой «проблеме пончика».

Если вы не хотите сами писать код, вы можете перейти по адресу https://github.com/lazyprogrammer/machine_learning_examples, зайти в папку unsupervised_class2 и открыть файл tsne_donut.py

Итак, первое, что мы делаем, – импортируем обычные библиотеки, а также функцию TSNE Sci-Kit Learn:

import numpy as np

import matplotlib.pyplot as plt

from sklearn.manifold import TSNE

Далее создадим функцию для получения данных с «проблемой пончика». У нас будет лишь 600 примеров, поскольку метод t-SNE с большим количеством может и не справиться. Внутренний радиус установим равным 10, а внешний – 20.

def get_donut_data():

    N = 600

    R_inner = 10

    R_outer = 20

Внутреннее кольцо будет представлять собой случайно и равномерно распределённые точки плюс внутренний радиус:

R1 = np.random.randn(N/2) + R_inner

theta = 2*np.pi*np.random.random(N/2)

X_inner = np.concatenate([[R1 * np.cos(theta)], [R1 * np.sin(theta)]]).T

Теперь то же самое с внешним:

R2 = np.random.randn(N/2) + R_outer

theta = 2*np.pi*np.random.random(N/2)

X_outer = np.concatenate([[R2 * np.cos(theta)], [R2 * np.sin(theta)]]).T

И сводим всё вместе. Не забывайте, что в методе t-SNE мы не используем метки, они нам нужны лишь для прорисовки в отдельные цвета.

X = np.concatenate([ X_inner, X_outer ])

Y = np.array([0]*(N/2) + [1]*(N/2))

return X, Y

Теперь запишем функцию main. При этом сначала выведем график необработанных данных.

def main():

    X, Y = get_donut_data()

    plt.scatter(X[:,0], X[:,1], s=100, c=Y, alpha=0.5)

    plt.show()

Теперь создаём объект t-SNE и график результата работы.

    tsne = TSNE(perplexity=40)

    Z = tsne.fit_transform(X)

    plt.scatter(Z[:,0], Z[:,1], s=100, c=Y, alpha=0.5)

    plt.show()

и код для вызова функции main.

if __name__ == ‘__main__’:

main()

Итак, попробуем всё это запустить.

Вначале мы видим обычную диаграмму «проблемы пончика».

Вначале мы видим обычную диаграмму «проблемы пончика».

А затем то, что сделал метод t-SNE – как видим, он обеспечивает неплохое разделение данных.

А затем то, что сделал метод t-SNE – как видим, он обеспечивает неплохое разделение данных.

Попробуем запустить его снова и посмотреть, что получится на этот раз.

Получилось, что иногда метод t-SNE может даже выдавать результат, который почти линейно разделяем.

Получилось, что иногда метод t-SNE может даже выдавать результат, который почти линейно разделяем.

Метод t-SNE и проблема XOR

Если вы не хотите сами писать код, вы можете перейти по адресу https://github.com/lazyprogrammer/machine_learning_examples и зайти в папку unsupervised_class2. Нужный файл называется tsne_xor.py

Сначала всё как обычно:

import numpy as np

import matplotlib.pyplot as plt

from sklearn.manifold import TSNE

Поскольку в проблеме XOR есть только четыре точки данных, то в данном случае нам придётся несколько расширить эту задачу, создав группы с равномерно распределёнными точками, но в том же порядке, что и в исходном XOR. Так и сделаем, для чего напишем функцию get_xor_data.

def get_xor_data():

    X1 = np.random.random((100, 2))

    X2 = np.random.random((100, 2)) – np.array([1, 1])

    X3 = np.random.random((100, 2)) – np.array([1, 0])

    X4 = np.random.random((100, 2)) – np.array([0, 1])

    X = np.vstack((X1, X2, X3, X4))

И метки – с 200 нулей и 200 единиц. Поскольку я уже всё протестировал, тут всё правильно.

    Y = np.array([0]*200 + [1]*200)

    return X, Y

Теперь напишем функцию main. Сначала – график исходных данных, а затем метод t-SNE и результаты его работы.

def main():

    X, Y = get_xor_data()

    plt.scatter(X[:,0], X[:,1], s=100, c=Y, alpha=0.5)

    plt.show()

    tsne = TSNE(perplexity=40)

    Z = tsne.fit_transform(X)

    plt.scatter(Z[:,0], Z[:,1], s=100, c=Y, alpha=0.5)

    plt.show()

if __name__ == ‘__main__’:

main()

Теперь запустим этот файл.

Сначала видим исходную проблему XOR

Сначала видим исходную проблему XOR.

Перейдем к следующему графику, полученный основываясь на знании того, как работает метод t-SNE.

Перейдем к следующему графику, полученный основываясь на знании того, как работает метод t-SNE.

Получилась интересная вещь – ничего особого не произошло, разве что группы несколько перекосились, но общая картина осталась прежней.

Почему же так произошло?

Не забывайте, что метод t-SNE – из области обучения без учителя. Он ничего не «знает» о метках данных, а потому и наши данные для него выглядят лишь одной большой однородной группой. Более того, поскольку в методе t-SNE используются расстояния, а наши точки были равномерно распределены, то нельзя было ожидать, что получится что-то особенное, разве что в результате работы точки будут распределены так же, как и прежде.

Метод t-SNE и база данных MNIST

Если вы не хотите сами писать код, а лишь его просмотреть, вы можете перейти по адресу https://github.com/lazyprogrammer/machine_learning_examples и зайти в папку unsupervised_class2. Соответствующий файл называется tsne_mnist.py

Он будет небольшим, ведь у нас уже есть функция для загрузки данных из базы MNIST.

Итак, начнём. Не будем использовать чересчур много точек данных, поскольку в противном случае метод может не сработать. Установим их количество равным 1000.

import numpy as np

import matplotlib.pyplot as plt

from sklearn.manifold import TSNE

from util import getKaggleMNIST

def main():

    Xtrain, Ytrain, _, _ = getKaggleMNIST()

    sample_size = 1000

    X = Xtrain[:sample_size]

    Y = Ytrain[:sample_size]

    tsne = TSNE()

    Z = tsne.fit_transform(X)

    plt.scatter(Z[:,0], Z[:,1], s=100, c=Y, alpha=0.5)

    plt.show()

if __name__ == ‘__main__’:

main()

Запустим наш код.

Итак, мы видим результат работы метода t-SNE на данных базы MNIST.

Итак, мы видим результат работы метода t-SNE на данных базы MNIST.

Возникает вопрос: лучшим ли получился результат, чем при использовании метода главных компонент?

Можно видеть, что разделение между классами выглядит несколько лучше, притом что оно произошло полностью без учителя, что хорошо. Впрочем, тут есть и недостаток, так как мы не можем использовать ту же модель, но уже применительно к новым данным.

Андрей Никитенко
Андрей Никитенко
Задать вопрос эксперту
Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Share via
Copy link