Мое первое серебро на Kaggle или как стабилизировать ML модель и подпрыгнуть на 700 мест вверх
Привет, чемпион!
Летом прошел очередной чемпионат на Kaggle - "American Express - Default Prediction", где требовалось предсказывать - выйдет ли пользователь в дефолт или нет. Табличное соревнование на 5К участников с очень плотным лидербордом.
И вот ведь парадокс! Все умеют решать табличные соревнования, все знают, что бустинги "стреляют" точнее всех, но почему-то всё равно не все могут забраться в топ лидерборда. В чем проблема?! Мы с командой все-таки смогли забрать серебро и сейчас я расскажу свое видение, как можно было выиграть медаль в этом чемпионате.
?♂️ Схема решения (упрощенная версия)
Как видите, схема решения очень проста. Всего три шага отделяли тебя от бронзовой медали. Генерируем признаки - фильтруем признаки - учим модель и залетаем в бронзу. Делов то на один вечер! Верно? Эх, было бы все так просто. Даже в таком классическом подходе возникают десятки технических тонкостей, которые "сольют" тебя по пути к медалям. Однако, не в мою смену! Я расскажу, как можно было обойти все технические сложности. Начнем решать проблемы поэтапно!
? Датасет в 50 гигов данных, а хватит ли оперативки открыть?
Первое, что выводило людей из игры, это то как люди импортивали датасет. Если попытаться открыть датасет в лоб, то в оперативную память подгружалось 50 гигов и среднестатистическое железо отрубалось с ошибкой по памяти.
Решение: заранее подаем в Pandas словарь типов, с которыми надо импортировать столбцы.
По умолчанию Pandas подгружает данные в самом "тяжелом" формате - float64
. Поэтому банальная смена типа на float16
уменьшала размер датасета в 10 раз. Конечно, чтоб узнать минимальный тип колонок, надо все-таки один раз открыть датасет. Однако, для этого можно было ограничить число строк и столбцов, подгружаемых в память. Аргументы в помощь nrows
и usecols
. Гениально просто. Код для автоматической минимизации типов в Pandas
??? Генерация признаков - хотя данных уже и так много ...
Я уже не первый раз решаю задачу классификацию временных рядов. В этой задаче с одной стороны, мы имеем очень короткие временные ряды по 12 значений (12 месяцев), но таких рядов для каждого пользователя 190 штук (190 столбцов). И таких пользователей у нас 450К. Стало быть, данных много и при добавлении еще признаков, мы раздуем датасет в разы. Как следствие, будут встречаться еще разные сложности. Что делать?!
Было решено разделиться и сгенерировать признаки двумя путями. Первой способ - это извлечь эмбеддинги из временных рядов нейронкой, а второй способ - добавить статистических признаков библиотекой tsfresh. И да, конечно, нельзя отказать себе в том, чтобы содрать все идеи признаков еще и с форума на Kaggle.
?♂️ Извлекаем ембеддинги и укращаем pytorch-lifestream
В качестве основы для ембеддингов решено было попробовать библиотеку pytorch-lifestream от разработчиков из Сбера, которая хорошо себя зарекомендовала (1 и 3 места), на проходившем недавно Data Fusion Contest, тоже на данных банковских транзакций.
Подробнее о библиотеке
Open-source библиотека, предназначенная для работы со сложноструктурированными последовательными данными (банковские транзакции, clickstream интернет-провайдеров и.т.п), с помощью нейросетевых методов, реализованных в ней, событийные данные можно преобразовывать в эмбеддинги и подавать на вход моделей. Или использовать нейросетевые архитектуры сразу для предсказаний.
Воспользовавшись туториалом из официального репозитория удалось получить эмбеддинги, которые смешивались с другими признаками и подавались на вход бустингу. Как видно на изображении, они оказались достаточно высоко по feature importance относительно других признаков.
Так же попытались обучить автоэнкодер и трансформер из этой же библиотеки, но не удалось выбить из них большой скор (0.789 и 0.791). Однако, мы использовали их предсказания в итоговом ансамбле, что увеличило устойчивость модели.
??Извлекаем ембеддинги и укрощаем tsfresh:
Несмотря на то, что tsfresh достаточно медленно генерирует признаки, зато это сразу и разнообразный и достаточно боевой набор признаков. Но вот ведь незадача, если запустить генерацию на всех сырых данных, то есть два исхода. Либо генерация будет длится несколько суток, либо в один момент объем накопленных признаков привысит лимит по памяти. И даже если распраллелить на на несколько CPU, все равно придется ждать около 4 дней.
Решение: посчитать список признаков на 10% данных, далее отфильтровать топ-2000 самых релевантных. Так и сделали. Как итог, распараллелив процесс на 4 CPU мы ускорили генерацию до нескольких дней, получая на выходе полный набор релевантных признаков. Для большей стабильности генерации признаков - сохраняли промежуточные результаты на каждой итерации. Вышло примерно 4К фичей, а это все еще много, но уже терпимо. Примерный код реализации.
Замечание: Более умным путем было бы использовать вместо tsfresh ускоренный аналог - tsfel. Однако, у нас на сервере tsfel запускаться отказывался =(.
? Фильтрация признаков или как убрать 50% мусора?!
Из полученных 4К фичей надо было как-то отсеять еще половину. И казалось бы, в чем сложность?! Глабольно, если посмотреть
Метод | Преимущество | Проблема/Преимущество |
Feature Importance | Быстрый расчет | Неточный. Отсеивание "неважных" признаков не реже помогает поднять скор |
Permutation Importance, | Очень долгий расчет | Как правило, помогает убрать мусорные фичи |
Корреляции, Критерии | Быстрый расчет | Неточный. Не ловит сложные зависимости. |
Рекурсивное удаление | Долго | Точно |
Вообще говоря, методов фильтации признаков много, но все упиралось во время. На этап фильтрации признаков и борьбу за последние доли точности у нас уже не оставалось достаточного колличества времени.
Помогло три трюка. Первое - взять для фильтрации признаков 10% данных, второе - отсеивать признаки не по одному, а группами. Третье - если удаление группы признаков поднимало скор на лидерборде (не локально) - то мы выбрасывали все признаки этой группы без исключения. Так всего через сутки мы смогли ужать 4 тысячи признаков до 2 тысяч. При этом новый новый скор поднял нас всего лишь на несколько десятков мест вверх на бронзовый порог. (Напомню, что всего в чемпионате было почти 5 000 участников).
? Модели - смешать, но не взбалтывать!
Финальный пайлайн со всеми признаками и моделями выглядел примерно так.
Основной частью решения тут является смесь градиентных бустингов Catboost и LightGBM. Только бустингов для конкурентноспособного скора не хватало. Однако, смешивание c диверсифицированной моделью RNN Transformer'а добавило нам в точности. Замечу, что lightGBM тут работал в режиме dart (это такой режим, где есть dropout'ы по аналогии с нейронками)
✈️Стабилизация моделей. Притегнись, мы летим вверх!
Как видите, подняло неплохо. Мои решения достаточно часто подкидывает вверху. Добиться такого несложно.. Теперь давайте попробуем разобраться в чем секрет такого шейк-апа на 700 мест вверх? Есть три причины.
Как видно из решения - финальная модель состояла из ансабля уже нескольких сильных моделей, что хорошо стабилизирует модели.
Обучение бустингов происходило по фолдам, т.е. финальный прогноз стабилизировался несколькими моделями. Так можно использовать все данные при обучении, и это не мешает валидации. Писал об этом в предыдушей статье.
Важная фишка - это декомпозиция модели по пользователям, у которых представлены не все признаки. Дело в том, что 10% пользователей в датасете имели не полный набор данных (всего пару месяцев вместо 12). Логично было при обучении натравить на такие случаи отдельную урезанную модель на основе 500 признаков. Далее, для таких пользователей ответы моделей не смешивались. Бралась только одна из моделей в зависимости от числа имеющегося числа признаков.
? Что еще помогло?
Как всегда хорошей практикой было удаление синтетического шума. (Хотя нас это обошло стороной т.к мы обрезали тип данных до
float16
).Разбор решения с 2-го места схожего с нашим можно посмотреть тут. Хорошие признаки и грамотное обучении бустингов принесло ребятам медали? и призы?
Пример трансформера для смешивания можно найти тут.
? Что не помогло?!
Как это часто бывает, многие гениальные идеи могут не показать хорошего перфоманса на лидерборде. Мы при решении этой задачи успели перебрать большой зоопарк моделей. Даже из трех бустингов хорошо зашли не все. Удивительно было, что зашел dart режим из lightGBM, обычно он всегда хуже остальных, но в этот раз дал очень сильный прирост. Возможно он хорошо показывает себя на временных рядах. Классические нейронные сетки уже редко побеждают в табличных соревнованиях. Даже Tabnet, при своем высоком скоре, в ансамбле не увеличивал точность (хотя возможно мы плохо старались при его обучении).
⚙️Что могло технически помочь, но мы не попробовали
Библиотека для работы с датафреймами (и не только) на GPU - rapids (rapids.ai)
cudf - это эквивалент Pandas для GPU
FIL - библиотека для инференса моделей из
sklearn
, бустингов типо XGBoost / LightGBM на GPU с кучкой «хаков» для ускорения.Умение параллелить процессы на CPU (
n_jobs
/n_threads
)Все это продукты Nvidia - не просто сторонний софт
? Выводы и мысли про секрет успеха
?️ Не брать решения с форума на Kaggle
? Умение писать хороший код
??? Feature Engineering
? Навык тюнить бустинги
? Качественная валидация
Спасибо, что дочитал до конца. Следи за нашими следующими победами в телеграмм канале. Пока ты читаешь эту статью, возможно мы выиграли еще что-то, завоевав новые медали на Kaggle.
Авторы статьи:
Александр Миленькин (@Aleron75)
Иван Александров (@ivanich_spb)