Хабрахабр

Как я решал соревнование по машинному обучению data-like

Недавно прошло соревнование от Тинькофф и McKinsey. Привет, Хабр. отсылаешь предсказания — получаешь оценку качества предсказания; побеждает тот, у кого лучше оценка. Конкурс проходил в два этапа: первый — отборочный, в kaggle формате, т.е. В этой статье я расскажу об отборочном этапе, где мне удалось занять первое место и выиграть макбук. Второй — онсайт хакатон в Москве, на который проходит топ 20 команд первого этапа. Команда на лидерборде называлась "дети Лёши".

Я начал решать ровно за неделю до конца и решал почти фулл-тайм. Соревнование проходило с 19 сентября до 12 октября.

Краткое описание соревнования:

На story можно отреагировать лайком, дизлайком, скипнуть или просмотреть до конца. Летом в банковском приложении Тинькофф появились stories (как в Instagram). Задача предсказать реакцию пользователя на story.

Соревнование по большей части табличное, но в самих историях есть текст и картинки.

План рассказа

Метрика

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

Для проверки точности решений используется формула, нормированная на максимально возможный результат:

1} & {\text { skip }} \\ {0. $\begin{l}{\text { weight (event) }=\left\{\begin{array}{ll}{-10} & {\text { dislike }} \\ {-0. 5} & {\text { like }}\end{array}\right.} \\[15pt] {\text { Metric}\left(y_{\text {pred}}\right)=\sum_{i=1}^{n}\left(\text {weight}\left(\text {event}_{i}\right) \cdot y_{\text {pred, } i}\right)}\end{array}$ 1} & {\text { view }} \\ {0.

Какие данные есть:

  • Базовая информация о пользователе
  • Транзакции пользователей
  • Информация о story (json, из которого можно её сконструировать)
  • История реакций пользователей на stories.

Далее я подробно расскажу о каждом кусочке данных, как я его обрабатывал и какие признаки (далее фичи) извлекал.

Информация о пользователе

что есть изначально:

  • id пользователя
  • анонимизированные продукты банка, которые пользователь открыл (OPN), пользуется (UTL) или закрыл (CLS)
  • пол, бинаризованный возраст, семейное положение, время первого захода в приложение
  • job_title — то, что люди сами пишут о себе
  • job_position_cd — должность человека, как одна из 22 категорий

предполагаем, что job_position_cd нормально описывает должность человека. как фичи используем всё вышеперечисленное кроме job_title, т.к.

Транзакции

что есть изначально:

  • id пользователя
  • день, месяц транзакции
  • сумма транзакции (бинаризована с шагом 250)
  • merchant_id — внутренний банковский id кассового аппарата. В дальнейшем не используется.
  • merchant_mcc

Это стандартизованный код услуги, которую предоставляет получатель. MCC — Merchant category code. Эти коды можно удобно разбить на категории, например: entertainment, hotels и т.п. Эта информация открыта, вот расшифровка.

Для каждого customer_id сопоставим следующие фичи:

  • посчитаем сумму расходов, средний чек, стандартное отклонение
  • количество транзакций
  • разобъём mcc коды на 20 категорий, посчитаем сколько человек потратил денег на эту категорию. Получим 20 фичей
  • ещё 20 фичей получим, разделив расходы в категории на сумму расходов. Т.е. получим процент денег потраченных на категорию.

Stories

Всего историй у нас 959.
что есть изначально:

  • id истории
  • json истории.

выглядит json подобным образом:

В 'content' лежит список дочерних элементов. Это такое дерево элементов, где каждый элемент описывается ключами: ['guid', 'type', 'description', 'properties', 'content']. На страницу накиданы фон, текст, картинки. История состоит из страниц. Конструктора историй у нас не было, а самому отрисовать всё это достаточно сложно и не факт, что значительно поможет в дальнейшем.

Извлечём следующие фичи: Регулярками вытащим весь текст и соответствующий размер шрифта.

  • количество страниц, ссылок, всего элементов
  • средний размер шрифта текста
  • количество текстовых элементов
  • "объём текста" — эвристика, чтобы взвешенно учесть длину текста в зависимости от размера шрифта.

Код подсчёта объёма

def get_text_amount(all_text, font_sizes): assert len(all_text) == len(font_sizes) lengths = np.array(list(map(len, all_text))) sizes = (np.array(font_sizes) / 100)**2 return (lengths * sizes).sum()

  • Возьмём теперь весь текст, с помощью dostoevsky определим семантику текста: ['neutral', 'negative', 'skip', 'speech', 'positive']. И добавим это в качестве 5 фичей

Реакции

что есть изначально:

  • id пользователя и истории
  • время
  • реакция

Обработаем время и как фичи добавим:

  • день недели
  • час, минута

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

Какую задачу мы решаем и как формировать предсказание?

предсказываем вероятность каждой реакции. Лучший подход, который использовал весь топ, следующий: cведём задачу к многоклассовой классификации, т.е. Считаем матожидание оценки $E_i$ для данной истории $i$:

1 \cdot P_i(skip) + 0. $E_i = -10 \cdot P_i(dis) - 0. 5 \cdot P_i(like)$ 1 \cdot P_i(view) + 0.

Бинаризуем $E_i$: $\: a_i = sign(E_i)$
$a_i$ — наш ответ для объекта $i$, который может принимать значение $\pm 1$

Модель

Обусловлено это тем, что CatBoost из коробки строит полезные статистики для категориальных фичей. С самого начала и до конца я использовал CatBoost. А статистика по пользователю — то, насколько он склонен к каким реакциям, и статистика по истории — то, как чаще всего не неё реагируют, являются самыми сильными фичами в этой задаче.

Как CatBoost работает с категориальными фичами хорошо объяснено в документации.
TLDR:

  • генерирует несколько перестановок данных
  • идёт по-порядку и строит mean target encoding (mte) по тем объектам, которые он уже видел

кратко про mte на нашем примере

Получим 4 числа. берём значение признака, например, один из customer_id, считаем процент случаев, когда этот customer отреагировал лайком, дизлайком, скипнул или просмотрел. Делаем так для каждого customer_id. Заменяем customer_id на эти 4 числа и используем их как признаки.

Текущий результат

31209 С текущими фичами, с неоптимизированным катбустом, на публичном лидерборде я занимал на тот момент 11 место с результатом 0.

Киллер фичи

Давайте тогда добавим фичи, которые будут говорить: В какой-то момент появилась гипотеза, что приложение может показывать истории чаще или реже в зависимости от того, как пользователь отреагировал на неё ранее.

  • сколько раз пользователь видел соответствующую историю в прошлом/будущем, в течение месяца/дня/часа/всего
  • время, которое прошло с последнего просмотра этой же истории
  • время, через которое пользователь в следующий раз посмотрит эту же историю
  • на самом деле у пользователя в одну секунду загружается сразу несколько историй, обычно около 5-7. Назовём этот набор историй группой. Я добавил это количество историй в группе как фичу, что дало большой прирост качества.

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

Получили 0. Итак, сказано — сделано. 35657 на лидерборде.

Оптимизация модели

Перебирал параметры я с помощью байесовской оптимизации

Пример под спойлером. Из интересного можно упомянуть параметр max_ctr_complexity, который отвечает за максимальное количество категориальных фичей, которые могут быть скомбинированы.

Выдержка из документации

These features can occur in different combinations. Assume that the objects in the training set belong to two categorical features: the musical genre (“rock”, “indie”) and the musical style (“dance”, “classical”). CatBoost can create a new feature that is a combination of those listed (“dance rock”, “classic rock”, “dance indie”, or “indie classical”).

Интересные наблюдения

  1. В этой задаче обучение на GPU давало результат сильно хуже, чем на CPU. CatBoost можно обучать на GPU, это заметно ускоряет обучение, но также вводит много ограничений, особенно касательно категориальных фичей.

  2. Во многом названия фичей говорят сами за себя, но некоторые, не самые очевидные, из топа, я поясню: Важность фичей по мнению CatBoost.

  3. Давайте посмотрим на распределение реакций с течением времени:

    в какой то момент распределение по реакциям сильно меняется. Т.е.

    Зашлём как предсказание все единички, получим результат 0. Далее хочется получить какое-нибудь подтверждение, что на тесте распределение такое же, как и в конце тренировочной выборки. Предскажем все единички на последней части трейна — получим около 0. 00237. 22. 009, на первой части — около -0. Отсюда рождается гипотеза, что если подправить распределение $\pm 1$ в наших предсказаниях, то результат на лидерборде сильно улучшится, т.к. Значит распределение на тесте скорее всего такое же, как в конце трейна и точно не похоже на основную часть. распределения на трейне и на тесте отличаются.

Трешхолд предсказаний

На последнем шаге получения итоговых предсказаний добавим трешхолд: $\: a_i = sign(E_i + \text{threshold})$

Оказалось, что действительно, уменьшение количества +1 давало сильный прирост качества. В последней модели у меня было что-то около 66% единичек, если бинаризовать с трешхолдом равным 0. Оценивались только последние 3 посылки, поэтому я заслал предсказания лучшей модели с разными трешхолдами так, чтобы процент плюс единичек был примерно 62, 58 и 54.

37970. По итогу на публичном лидерборде мой лучший результат был 0.

Результаты соревнования

про публичный/приватный лидерборд

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

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

  1. 0.382 — ЗдесьМоглаБытьВашаРеклама
  2. 0.379 — дети Лёши
  3. 0.372 — Gardeners
  4. 0.35 — lazy&akulov

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

  1. 0.45807 дети Лёши
  2. 0.45264 Gardeners
  3. 0.44136 Zhuk
  4. 0.43704 ЗдесьМоглаБытьВашаРеклама
  5. 0.43474 lazy&akulov

Что не сработало

  1. Я пробовал переводить весь текст из истории в вектор с помощью fasttext, затем кластеризовал векторы и использовал номер кластера как категориальную фичу. Эта фича была топ 3 (после story_id и customer_id) в feature importance CatBoost'а, но почему-то стабильно и значительно ухудшала результат на валидации.
  2. Благодаря кластерам можно было найти истории, которые относились к чемпионату мира по футболу и присутствовали только в тренировочной выборке.
    Однако выкидывание таких объектов из датасета не улучшило результат.
  3. по дефолту CatBoost генерирует случайные перестановки объектов и считает по ним признаки для категориальных фичей. Но можно сказать катбусту, что у нас есть время в данных — has_time = True. Тогда он будет идти по порядку, не перемешивая датасет. В данной задаче, несмотря на то, что время у нас действительно есть, результат с has_time был стабильно хуже.

    В данной задаче, видимо, это не имело большого эффекта и пробежаться несколько раз по разным перестановкам было важнее. В общем случае если время есть, но его не учитывать при построении Mean Target Encoding'а, то модель будет использовать информацию о правильных ответах из будущего и может переобучиться под это.

  4. Была идея назначать больший вес объектам в конце трейна, т.е. чтобы больше учитывать объекты с правильным распределением реакций. Но и на валидации, и на публичном лидерборде это давало результат хуже.
  5. Можно учитывать разные реакции с разным весом при обучении. Хотя у меня это результат не улучшало, некоторым командам это помогло.

Выводы

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

Спасибо организаторам конкурса!

Весь код выложен на гитхабе.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть