«От категорий к векторам» или нестандартное кодирование категориальных данных. Часть 1

Привет, Хабр! С вами Артём, аналитик больших данных МегаФона. На работе занимаюсь рекомендательными системами и интересуюсь NLP. Эти две вещи и привели меня к рассматриваемой тут теме, так что садитесь поудобнее, и поехали. Кстати, к статье прилагается код, ищите ссылки внутри.

Интро или «откуда ноги растут»

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

Но как это все удобнее делать, ведь у категориальных признаков особый характер?

Примерно из такого праздного вопроса родилась идея провести несколько экспериментов с кодом, а потом появилась возможность выступить на Data Fest 2021 с этой темой. Чтобы помочь всем заинтересованным, мне захотелось оформить результаты в виде статьи: при желании можете воспользоваться наработками и развить их во что-то большее.

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

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

Надеюсь, вам еще не наскучила статья, перейдем же к делу.


Завязка или «жила-была модельная задача»

Рассмотрим задачу, которую вы, при желании, можете транслировать на свои кейсы:

У нас есть датасет, который полностью состоит из категориальных признаков. В данном случае использовались материалы одного из соревнований на Kaggle. Датасет имеет следующий вид:

Что же мы хотим и зачем задумались об этой проблеме? Мы хотим получить числовые признаки, полезные для решения задач supervised learning (классификация, регрессия) и unsupervised learning (кластеризация, визуализация, простейшие рекомендательные системы). Мы сможем заменить ими исходные и неудобные представления объектов, ну или дополнить такими признаками нашу базу кодировок. Было бы неплохо, чтобы данные признаки были доступны в виде числовых векторов, с которыми мы все уже умеем работать, к примеру так:

Простор для фантазии, главное, чтобы хватило времени на эксперименты.

Также немного ограничим себя и не будет использовать информацию о target, только сами признаки. То есть получать мы будем наши векторы в unsupervised манере.

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

Схема кодирования будет выглядеть примерно так:

Качество полученных числовых векторов, которые будем называть векторными представлениями объектов, или эмбеддингами, проверим на задаче классификации: посмотрим на метрику площади под кривой AUC ROC и оценим время инференса.

Высокие значения метрики AUC ROC на тесте будут, по-видимому, говорить о высокой информативности эмбеддингов. Методы же будем сравнивать по качеству как между собой, так и с несколькими обыденными подходами обработки категориальных данных.

Сразу отмечу, что гнаться за качеством и пытаться «доминировать» над привычными техниками мы здесь не планируем, основная задача — получить сравнимое качество, что и будет говорить о сохранении «достаточного» количества исходной информации в наших векторах, а значит – и о положительном результате наших опытов.

Схема проверки качества на модельной задаче выглядит примерно так:


Сомнения или «Почему же не one-hot?!»

Но возникает вопрос, а зачем пробовать что-то нестандартное, когда существует множество общепринятых методик обработки категориальных признаков. Вдруг, искомые числовые векторы можно легко получить как-то стандартно, не напрягаясь?

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

Сначала оценим, наверное, самый известных метод — one-hot кодирование. Вот простой пример векторов на выходе кодировщика в данном случае:

Id

V11

V12

V21

V22

V23

V24

V31

V32

1

0

1

0

0

0

1

0

1

2

1

0

0

0

1

0

1

0

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

Есть еще некоторые более продвинутые техники, к примеру, Helmert coding. Тут ведь новые признаки уже больше похожи на эмбеддинги? Но длительность работы данных методов на большом количестве категорий часто велика, а памяти не всегда хватает (по крайней мере в моих экспериментах). В результате же количество признаков снова очень велико. Да, честно сказать, и к содержанию этих длинных векторов тоже есть вопросы… Только взгляните на эту «ступенчатую» структуру и обилие повторяющихся значений и нулей.

Cat. new_vars

New_var_1

New_var_2

New_var_3

Category 1

0.75

0

0

Category 2

-0.25

0.67

0

Category 3

-0.25

-0.33

0.5

Category 4

-0.25

-0.33

-0.5

Теперь посмотрим в сторону кодирования целыми числами. Ведь, кажется, что это те самые векторы, которые мы ищем.

Id

V1

V2

V3

1

1

4

2

2

2

3

1

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

А что же до методик обработки, встроенных в такие мощные и модные алгоритмы, как LightGBM и CatBoost? Они ведь дают прекрасные результаты на различных метриках? Ответ прост — здесь результаты сложно или даже невозможно «вынуть» из алгоритмов и потом использовать, к примеру, для задачи определения степени похожести объектов, а также для визуализации. Но ведь есть же CatBoostEncoder, скажете вы, и будете правы! Но давайте вспомним, что здесь мы решили не использовать информацию о target, поэтому для наших эмбеддингов данный интересный кодировщик, к сожалению, не подходит.

Таким образом, наша задача становится весьма нестандартной, но интересной.


Кульминация или «как получить векторы, имея только категории»

Мои эксперименты затронули:

— Кодирование на основе методик обработки естественного языка.

— Кодирование с использованием элементов теории вероятностей.

Окей, я надеюсь, что мне удалось создать интригу этими немного громкими заголовками, и предлагаю поглубже погрузиться в алгоритмическую часть.

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

Давайте же взглянем на объекты, выраженные в категориальных переменных, как на тексты, а далее – применим что-то из богатого арсенала NLP методов. Вдруг получится что-то интересное?

Метод заключается в следующем:

1. Преобразуем каждый объект в строку определенного вида.

2. Затем, рассматривая полученные строки как некоторые тексты, будем искать их тематические профили. Здесь мы используем подход LSA, сущность которого заключается в матричном разложении.

где k — количество тем, то есть размерность наших векторов.

3. С таким профилями (VkDk) уже можно работать, как с векторами, они и будут нашими эмбеддингами объектов.


Проверка качества или «Удалось ли сохранить информацию?»

Какое же качество на модельной задаче классификации дает описанный подход? Для сравнения используются результаты классификации привычными методами.

Оценим сначала время обучения и инференса кодировщика в ноутбуке Google Colab:

  • Время обучения для размерности 700 (230 тыс. образцов): 18 мин.

  • Время кодирования тестовых данных (70 тыс. образцов): 34 сек.

Теперь посмотрим на результаты классификации с использованием полученных векторных представлений размерности 700.

Алгоритм

AUC ROC

LightGBM

0.751

LogisticRegression

0.772

Как я и обещал, для сравнения ниже представлены результаты классификации с использованием привычных методов обработки.

Алгоритм

AUC ROC

LightGBM (встроенный обработчик)

0.783

LightGBM (ohe) *

0.756

LogisticRegression (ohe) *

0.800

LogisticRegression (ordinal enc., scaled)

0.719

* Количество бинарных признаков после one-hot кодирования — более 16 тыс.

Можно заметить, что хоть мы и существенно сокращаем размерность признакового пространства по сравнению с one-hot кодированием (более чем на порядок), качество же довольно высокое.

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

Использовался метод t-SNE*
Использовался метод t-SNE*
Использовался метод MDS**
Использовался метод MDS**

* метод t-SNE

** метод MDS

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

Спойлер: более сложные методы все же покажут нам кластеры получше, но об этом — в следующей статье.


Пролог или «вместо заключения»

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

Хочется отметить, что данный подход можно расширять за счёт других методов NLP: доставать из объектов n-граммы и skip-граммы (то есть несколько лучше учитывать взаимосвязь переменных), выбирать между CountVectorizer и Tfidf при формировании входа LSA. Также можно попробовать получать сами признаки на основе Tfidf, но это не совсем относится к эмбеддингам, а просто иной вариант кодирования.

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

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

Напоследок я подсвечу немного очевидную мысль, которая, возможно, вдохновит кого-то на новые эксперименты:

Искать что-то новое и полезное можно не только там, где еще ничего не открыто, а еще и там, где сначала казалось, что открывать уже нечего.


А какие эксперименты с категориальными данными проводили вы? И каков результат? Пишите в комментариях, интересно обсудить) А ниже для вас я подготовил небольшой опрос.

Источник 📢