
(Это не картина художника-абстракциониста, а визуализация LiveJournal-датасета с высоты птичьего полёта)
t-SNE с модификациями
Хоть t-SNE от силы лет десять, можно без опаски сказать, что он уже апробирован временем. Он пользуется популярностью не только среди матёрых адептов науки о данных, но и среди авторов научно-популярных статей. Про него уже писали на Хабре; также про этот алгоритм визуализации есть много хороших статей на просторах интернета.
Однако почти все эти статьи рассказывают про стандартный t-SNE. Напомню его вкратце:
- Посчитать для каждой пары пары точек данных
их подобие друг другу в исходном пространстве с использованием Гауссова ядра:
- Для каждой пары точек отображения выразить их подобие в пространстве визуализации с использованием ядра t-распределения
- Построить целевую функцию и минимизировать её относительно
при помощи градиентного спуска
Где
Интуитивно для трёхмерного случая это можно представить следующим образом. У вас есть
Теперь вы берёте получившуюся конструкцию и пытаетесь разместить её на столе. Даже если трёхмерный объект состоит из свободно двигающихся узлов, в общем случае его нельзя разместить на плоскости (представьте, как вы прижимаете к столу тетраэдр), так что вы просто располагаете его так, чтобы натяжение или растяжение пружин было как можно меньше. Не забываем сделать поправку на то, что в двумерном пространстве у каждого шара будет меньше ближних соседей, но больше соседей на среднем расстоянии (помним про закон куба-квадрата). Большинство пружин настолько слабые, что их можно вовсе не брать в расчёт, но элементы, связанные сильными пружинами лучше расположить рядом. Вы двигаетесь маленькими шажками, двигаясь от произвольной начальной конфигурации, потихоньку уменьшая натяжение пружин. Чем качественнее вы можете восстановить исходное натяжение пружин по их текущему натяжению, тем лучше.
(забыли на секунду, что мы знаем, что изначальное натяжение пружин равно нулю)
(я упоминал, что это волшебные шары и пружины, которые проходят друг сквозь друга?)
(зачем вообще заниматься такой ерундой? — оставлено, как упражнение читателю)
Так вот, современные data science пакеты реализуют не совсем это. Стандартный t-SNE обладает сложностью
Во-первых, считать на первом шаге все
Во-вторых…
Barnes-Hut алгоритм
Отвлечёмся немного, чтобы обсудить Barnes-Hut. Про этот алгоритм не было отдельной статьи на Хабре, а тут как раз отличный повод рассказать про него.
Наверное, каждому знакома классическая гравитационная задача N тел: по заданным массам и начальным координатам N космических тел и известному закону их взаимодействия построить функции, показывающие координаты объектов во все последующие моменты времени. Её решение не выражается через привычные нам алгебраические функции, а может быть лишь найдено численно, например, при помощи метода Рунге-Кутты. К сожалению, подобные численные методы решения дифференциальных уравнений имеют сложность
Для симуляций с таким громадным числом тел приходится идти на некоторые упрощения. Заметим, что для тройки тел A, B и C сумма сил действующих на A со стороны B и C равна силе, которую бы оказывало единое тело (BC), помещённое в точку центра масс B и C. Одного этого для оптимизации мало, поэтому применим ещё одну хитрость. Разобьём пространство на клетки. Если расстояние от тела A до клетки велико и в клетке больше одно тела, будем считать, что ровно в центре этой клетки помещён объект с суммарной массой объектов внутри. Его положение будет немного отклоняться от настоящего центра масс содержимого клетки, но чем дальше клетка, тем более незначительным будет ошибка в угле действия силы.

На картинке ниже зелёным обозначена область близости тела A. Синим обозначены направления действия сил. Тела B, C и D оказывают силу на A по-отдельности. Хоть С и D находятся в одной клетке, они в области точности. E тоже оказывает силу в одиночку — оно одно в клетке. А вот объекты F, G и H достаточно далеко от A и достаточно близко друг к другу, что взаимодействие можно приблизить. Центр клетки не всегда будет хорошо совпадать с реальным положением центра тяжести (см. K, L, M, N), но в среднем, при большом расстоянии и большом количестве тел, ошибка будет пренебрежимо мала.

Стало лучше: мы уменьшили эффективное число объектов в несколько раз. Но как же выбрать оптимальный размер разбиения? Если разбиение будет слишком мелким, толку от него не будет, а если слишком большим, увеличится ошибка и придётся поставить очень большое расстояние, на котором мы будем готовы пожертвовать точностью. Сделаем ещё шаг вперёд: вместо сетки с фиксированным шагом разобьём прямоугольник плоскости на иерархию клеток. Крупные клетки содержат в себе мелкие, те ещё более мелкие и так далее. Получившаяся структура данных является деревом, где каждая нелистовая нода, порождает четыре дочерних ноды. Такая конструкция называется «квадродерево» (quadtree). Для трёхмерного случая, соответственно, используется октодерево (octotree). Теперь при рассчёте силы, действующей на некоторое тело, можно обходить дерево до тех пор пока не встретится клетка с достаточным уровнем мелкости, после чего использовать приближение.

Ноды, в которые не попадает ни одного объекта нет смысла мельчить дальше, поэтому эта структура не такая тяжеловесная, как может показаться на первый взгляд. С другой стороны, накладные расходы определённо есть, так что я бы сказал, что пока симулируемых объектов меньше сотни, такая хитрая структура данных может и немного вредить (зависит от реализации).
Теперь более формально:
Алгоритм 1:
Пусть
- Если в текущей клетке нет ни одного элемента, пропускаем её
- Иначе, если в ноде только один элемент и это не
, напрямую добавляем его влияение к суммарной силе
- Иначе, если
, считаем эту ноду единым целым объектом, и прибавляем его суммарную силу, как описано выше
- Иначе рекурсивно запускаем эту процедуру для всех дочерних клеток текущей клетки
Здесь более подробное выполнение алгоритма по шагам. Красивая анимация Barnes-Hut симуляции галактики. Различные реализации Barnes-Hut можно найти на гитхабе.
Очевидно, что такая интересная идея не могла остаться незамеченной и в других областях науки. Barnes-Hut применяют, везде, где встречаются системы с большим количеством объектов, каждый из которых действует на другие по простому закону, несильно меняющемуся в пространстве: симуляция роя, компьютерные игры. Вот и анализ данных тоже не оказался исключением. Распишем подробнее этап градиентного спуска (3) в t-SNE:
Где
Можно ли ещё быстрее? Да. Зачем останавливаться на приближении «точка-клетка», когда можно упростить ситуацию ещё больше: если две клетки достаточно далеко друг от друга, можно приблизить не только конечный
LargeVis
Хорошо, но можно ли ещё быстрее? Jian Tang и компания, отвечают, что да!
Раз мы уже принесли в жертву точность, почему бы не принести в жертву ещё и
RT-деревья
Идея с разбиением плоскости при помощи квадродерева была неплоха. Теперь попробуем разбить плоскость на сектора при помощи чего-нибудь более простого. Как насчёт k-d дерева?
Алгоритм 2:
Параметры:
MakeTree(
- Если
вернуть текущий
, как листовую ноду
- Иначе
- Выбрать случайным образом номер переменной
- Rule(x) :=
median(
) // Предикат разбивающий подпространство данных гиперплоскостью, перпендикулярной оси координат
и проходящей через медиану значений
-той переменной элементов
.
- LeftTree := MakeTree({x
X: Rule(x) = true})
- RightTree := MakeTree({x
X: Rule(x) = false})
- Вернуть [Rule, LeftTree, RightTree]
- Выбрать случайным образом номер переменной

Как-то слишком примитивно. Перпендикулярность плоскости разбиения какой-то оси координат смотрится подозрительно. Есть более продвинутая версия: деревья случайных проекций (random projection trees, RT-деревья). Будем рекурсивно разбивать пространство, каждый раз выбирая случайный единичный вектор, перпендикулярно которому будет идти плоскость. Для лёгкой асимметричности дерева также добавим небольшой шум к точке привязки плоскости.
Алгоритм 3:
ChooseRule(X) // Мета-функция, которая возвращает предикат, по которому мы будем разбивать пространство
- Выбрать случайный единичный вектор из
- Выбрать случайный
- Пусть
— самая далёкая от него точка данных в
:= случайное число в
- Вернуть Rule(x) :=
(median(
) +
)
MakeRPTree(
- Если
вернуть текущий
, как листовую ноду
- Иначе
- Rule(x) := ChooseRule(X)
- LeftTree := MakeTree({x
X: Rule(x) = true})
- RightTree := MakeTree({x
X: Rule(x) = false})
- Вернуть [Rule, LeftTree, RightTree]

Неужели такое грубое и хаотичное разбиение способно дать хоть какие-то результаты? Оказывается, что да! В случае, когда многомерные данные на самом деле лежат вблизи не очень многомерной гиперскладки, RT-деревья ведут себя довольно-таки пристойно (см. доказательства по первой ссылке и здесь). Даже в случае, когда данные действительно имеют эффективную размерность сопоставимую с
Хм, сложность: посчитать медиану это всё же
Кажется, мы выбросили из алгоритма всё, что хоть как-то напоминает о математической строгости или точности. Как же получившиеся деревья могут помочь нам с поиском ближайших соседей? Очевидно, что соседи точки данных по листу графа — первые кандидаты в его kNN. Но это не точно: если точка лежит рядом с гиперплоскостью разбиения, настоящие соседи могут оказаться в совершенно другом листе. Получившееся приближение можно улучшить, если построить несколько RP-деревьев. Пересечение листов получившихся графов для каждого

Деревьев не нужно слишком много, это подрывает быстродействие. Получившиеся приближение доводится до ума при помощи эвристики «сосед моего соседа — скорее всего мой сосед». Утверждается, что достаточно совсем немного итераций, чтобы получить приличный результат.


Авторы статьи уверяют, что эффективная реализация всего этого работает за
Сэмплирование
После вычисление kNN Jian Tang и КО считают подобие данных в исходном пространстве по такой же формуле, как и в t-SNE.
Обратите внимание, что сумма в знаменателе считается только по kNN, остальные
После этого, предлагается хитрый теоретический манёвр: теперь мы смотрим на полученные
Снова используется t-распределение, только немного по-другому; всё ещё помним про разное количество соседей в разных слоях в пространствах с разным
Перемножим вероятности для всех рёбер и получим расположение
Где
Снова получили сумму состоящую из двух частей: одна притягивает друг к другу
Где
Так как
Ух. Что же дают нам все эти ухищрения? Примерно пятикратный прирост скорости на в меру больших датасетах по сравнению с Barnes-Hut t-SNE!





(Картинки взяты из статьи)
При этом качество визуализации примерно такое же. Внимательный читатель заметит, что на сравнительно маленьком датасете 20 Newsgroups (
Напоследок, пара практических моментов. Существует официальная имплементация LargeVis, но на мой взгляд код там слегка страшненький, используйте на свой страх и риск. Количество отрицательных сэмплов на ноду
Заключение
Спасибо за внимание. Надеюсь, теперь вам стало понятнее, что это за таинственные method='barnes_hut' и angle в параметрах у sklearn'овского TSNE, и вы не потеряетесь, когда понадобится визуализировать датасет из десяти миллионов записей. Если вы работаете с большим количеством данных, может, вы найдёте собственное применение Barens-Hut или решитесь на внедрение LargeVis. На всякий случай, вот краткие заметки по работе последнего. Хорошего дня!


