В двух прошлых частях статьи я описывала, как мы экспериментировали с рекомендательными моделями на датасете онлайн-кинотеатра Kion в рамках пет-проекта. Напомню, что мы:
Обсудили проблему popularity bias и обосновали необходимость визуального анализа при валидации моделей
Построили модель implicit bm25, сбалансированную по метрикам
Улучшили рекомендации с помощью двухэтапной модели
Построили схему валидации для двухэтапной модели
Показали рекомендации онлайн в нашем приложении.
Почитать можно здесь: "Часть 1. Popularity bias и визуальный анализ", "Часть 2. Двухэтапные модели".
Сегодня я опишу все MLOps составляющие нашего проекта. Кроме онлайн приложения мы построили небольшую, но цельную платформу для экспериментов с рекомендательными моделями. Сегодня я подробно на ней остановлюсь:
Расскажу о workflow экспериментов и пайплайнах обработки данных.
О том, какие инструменты мы использовали для реализации платформы.
Нарисую полную инфраструктуру проекта.
Платформа для экспериментов в RecSys
На примере Киона я показала сложности экспериментов с рекомендательными системами. Мы не можем с ходу сказать, какие метрики нужно максимизировать, и как выглядит оптимальное решение. Приходится пробовать разные подходы, сравнивать рекомендации визуально, иногда откатываться к старым моделям. В работе над RecSys в реальном сервисе мы строим гипотезы, выкатываем модели в АБ тесты, строим новые гипотезы. Нужно легко и быстро отвечать на вопросы:
“А что там было не так с матричной факторизацией месяц назад?”
“Почему мы выбрали модель с предсказательной силой в 2 раза ниже?”
“Что выйдет, если мы прикрутим вот эту фичу к нашему старому подходу?”
“АБ показал, что последняя модель не отработала. Какие ещё гипотезы у нас были?”
Расскажу наш подход к этой проблеме. Экспериментировать с разными моделями и метриками - это увлекательно, но 90% времени в проекте мы занимались не этим. Мы строили пайплайны и собирали инфраструктуру для экспериментов со сложными моделями. Часть идей взяли из курса по MLOps от ODS, часть из моего опыта разработки рекомендательных систем в МТС.
Нашей целью была инфраструктура и пайплайны для максимально быстрых и удобных экспериментов с любыми алгоритмами.
Такие задачи мы для себя поставили:
полностью воспроизводимые эксперименты
перенос на другие датасеты
поддержка кросс-валидации
поддержка визуального анализа
возможность вписать любые модели с минимальными усилиями
управление конфигами и запуск одной командой
поддержка двухэтапных моделей
поддержка эвристик
наглядные отчёты
Workflow наших экспериментов и вывода моделей в онлайн можно схематично изобразить так:
Все эксперименты и вывод в production у нас управлялись парой конфигов и простых команд. Workflow запускает пайплайны для обновления данных (если необходимо), в ходе экспериментов мы проводим модели через кросс-валидацию и визуальный анализ, а затем, если результат нас устроил - логируем обученные модели в Mlflow и выводим в онлайн.
Двухэтапную архитектуру мы реализовали отдельным классом, который проходит тот же путь обучения, валидации и вывода в онлайн, что и простые модели вроде коллаборативной фильтрации, поскольку имеет идентичное API, а вся сложная история генерации кандидатов и подбора таргетов остаётся под капотом. Позже я покажу наш подход в деталях.
В целом, схема выглядит просто. Начнём знакомиться с инструментами, которые позволили нам реализовать эту схему в проекте. Пайплайны обработки данных мы реализовали на DVC. Что за инструмент и зачем он нам нужен?
DVC магия: эксперименты одной командой
Когда узнаёшь о DVC в первый раз, обычно рядом видишь слова «версионирование данных», «автоматизация пайплайнов» - слова очень правильные, но веет чем-то них сложным и не совсем определённым. И думаешь - а нужно ли оно вообще или можно в проекте спокойно обойтись без подобных сложностей?
Я бы описала DVC при использовании в проекте как толику магии. Написать ключевые скрипты, один раз настроить конфиг с зависимостями – и дальше DVC берёт на себя заботу о том, чтобы пайплайн работал как часы, данные в экспериментах всегда были актуальные, а расчёты производились только тогда, когда действительно нужны. Звучит заманчиво, правда?
Мы использовали DVC в проекте в качестве основного workflow менеджера из-за его способности к автоматизированному отслеживанию изменений в коде, данных и конфигах. И способности запускать только те этапы пайплайна, которые затронуты этими изменениями. Опишу на простом примере.
Пайплайн в стандартном ML проекте состоит из последовательных этапов, например:
получили сырые данные
выполнили предобработку
рассчитали фичи
обучили модель
Для каждого этапа мы пишем скрипт, выполняющий нужные действия. А в dvc.yaml добавляем для этого этапа (stage) зависимости (deps), результаты (outs) и команду (cmd), которую нужно запустить, чтобы из первого получить второе - например, указываем запуск написанного скрипта. Вот кусочек из нашего dvc.yaml c этапом получения сырых данных :
stages:
get_data:
cmd: source src/data/get_data.sh
deps:
- src/data/get_data.sh
outs:
- data/raw/interactions.csv
- data/raw/items.csv
- data/raw/sample_submission.csv
- data/raw/users.csv
DVC строит пайплайн из описанных в конфиге этапов, учитывая в какой последовательности их надо выполнять по указанным deps и outs. То есть строит DAG для данных и артефактов. И дальше начинается его “магия”.
Когда мы запускаем такой пайплайн, DVC проходится по всему DAG-у и проверяет, какие из этапов нужно пересчитать, а какие можно оставить без изменений. Для каждого этапа DVC проходится по всем deps (тут могут быть и скрипты, и данные, и конфиги – всё что мы сами укажем), и если находит, что хотя бы одна из зависимостей изменилась – перезапускает этот этап. Если все зависимости у этапа остались без изменений – значит, и outs у нас актуальные, этап не будет перезапущен, готовые данные сразу пройдут дальше по пайплайну.
В результате проект с настроенным DVC пайплайном начинает «летать» на всех экспериментах, а с разработчика полностью снимается головная боль о том, какие из скриптов ему нужно перезапустить после внесённых в код изменений, чтобы получить корректный результат. Чем больше экспериментов проводится в проекте, тем больше раскрывается удобство такого подхода.
В дополнение к DVC в проекте мы использовали Snakemake, это позволило добавить параллельные вычисления на одном из этапов. Наш общий пайплайн обработки данных упрощённо можно изобразить так:
Пайплайн начинает с загрузки сырых данных датасета, проводит данные через предобработку и feature-engineering. Когда мы решаем сконструировать новые фичи, мы просто добавляем их расчёт в код скрипта на нужном этапе, и данные обновляются. В результате работы пайплайна у нас есть все данные, которые модели будут использовать для обучения: подготовленные взаимодействия юзеров с айтемами и все возможные фичи, которые мы можем использовать в моделях. Feature selection в дальнейшем мы делали через конфиги моделей, я покажу этот момент чуть позже на примере двухэтапной модели.
Вписываем рекомендательные алгоритмы через конфиги с Rectools
Если вы когда-либо занимались рекомендашками, то знаете, что самые часто используемые модели разбросаны по разным библиотекам, и каждая из них требует своего подхода в коде. Даже для простого бенчмарка бейзлайнов на новом датасете приходится предпринять массу усилий. Очень не хватает единого способа для запуска разных алгоритмов.
Чтобы решить эту проблему, мы использовали в проекте библиотеку Rectools. Преимущества:
единое API для самых часто используемых алгоритмов: Implicit ItemKNN, ALS, SVD, Lightfm, несколько эвристических моделей, пара нейронок.
функционал для расчёта метрик и кросс-валидации.
Единое API означает, что разные рекомендательные модели имеют одинаковые методы init, fit, recommend, которые скрывают под капотом всю разнообразную логику самих алгоритмов. Это позволяет легко вписать в эксперименты новые модели и вообще вывести управление моделями из кода в конфиги. Пишем в конфиге, какие модели мы хотим запустить для кросс-валидации / визуального анализа / вывода в production - запускаем одной командой. Profit!
Вот пример конфига для эксперимента с двумя моделями implicit ItemKNN с разными параметрами:
common_heuristics: &common_heuristics
INTERACTIONS_LIMIT: 20
models:
cosine_200:
model_description:
model_type: 'cosine'
params:
K: 200
heuristics: *common_heuristics
bm25_50_0.1_0.1:
model_description:
model_type: 'bm25'
params:
K: 50
K1: 0.1
B: 0.1
heuristics: *common_heuristics
“Heuristics” - это дополнительные эвристики, которые мы применяем к моделям. Например, здесь это “забывание” слишком давней истории взаимодействий юзеров (лимит на учёт только последних 20 интеракций в истории). Эвристики накладываются поверх обычной логики обучения модели и построения рекомендаций. Где-то их добавляют, чтобы повысить метрики, где-то - чтобы добавить требуемую бизнес логику. Без эвристик рекомендательные модели практически никогда не работают в реальных сервисах, и нам важно , чтобы вводить их в эксперименты было удобно.
Для моделей, которых в библиотеке нет, мы писали обёртки, которые поддерживают идентичное API. Именно так мы включили в эксперименты двухэтапные модели с градиентным бустингом. Вся сложная история генерации кандидатов и обучения бустинга остаётся под капотом, а сама модель поддерживает простые методы fit-recommend. Остановимся здесь подробнее.
Реализация двухэтапной модели: единый класс на все случаи использования
Наш способ реализации двухэтапной модели в проекте - это один класс и один конфиг на все случаи использования двухэтапной архитектуры:
кросс-валидация
обучение в Production и логирование в Mlflow
оффлайн рекомендации
онлайн рекомендации
Приведу пример конфига двухэтапной модели. Здесь на первом этапе кандидатов генерируют коллаборативная фильтрация (cosine_200) и популярный алгоритм (pop_14). Первая модель сохраняет скоры как фичу для бустинга, вторая - ранги.
first_stage_config:
cosine_200:
model_description:
model_type: 'cosine'
params:
K: 200
num_candidates: 100
keep_scores: True
keep_ranks: False
scores_fillna_value: 0
pop_14:
model_description:
model_type: 'popular'
params:
days: 14
num_candidates: 50
keep_scores: False
keep_ranks: True
ranks_fillna_value: 999
sampling:
num_neg_samples: 3
sampling_strategy: 'per_positive'
train_period_n_days: 14
random_state: 345
catboost_params:
max_depth: 9
n_estimators: 2000
learning_rate: 0.03
auto_class_weights: 'Balanced'
features:
users_static:
- 'age'
- 'sex'
users_time_based:
- 'user_watch_cnt_all'
items_static:
- 'content_type'
- 'for_kids'
- 'age_rating'
- 'release_novelty'
items_time_based:
- 'watched_in_7_days'
- 'watch_ts_std'
- 'trend_slope'
- 'watch_ts_quantile_95_diff'
- 'male_watchers_fraction'
category_cols:
- 'age'
- 'sex'
- 'content_type'
В конфиге также указаны параметры для обучения модели градиентного бустинга (в данном случае используется библиотека Catboost от Яндекса), стратегия семплирования негативных таргетов и фичи для использования бустингом. Напомню, что фичи у нас считаются в пайплайне с помощью DVC, а здесь происходит только feature selection.
Некоторые фичи зависят от даты: “users_time_based” и “items_time_based”. Пример - количество просмотров за последние 7 дней. Для любого айтема такая фича зависит от даты, на которую мы её рассчитываем. Это важно, поскольку для рекомендательной системы наиболее стабильная схема валидации - это кросс-валидация скользящим окном. Для двухэтапной модели я уже показывала её в прошлой части статьи:
Для каждого фолда в кросс-валидации мы:
Обучаем модели первого этапа на своем периоде (“Fit 1 stage”), генерируем кандидатов
Добавляем фичи, рассчитанные на дату окончания периода “Fit 1 stage”
Собираем таргеты с периода обучения модели второго этапа (“Train Catboost”), обучаем модель второго этапа
Снова обучаем модели первого этапа, на всех интеракциях до тестового периода (“Fit 1 stage” + “Train Catboost”). Генерируем кандидатов
Добавляем фичи, рассчитанные на дату окончания периода “Train Catboost”
Рассчитываем рекомендации обученной моделью второго этапа и считаем метрики на тестовом периоде (“Test interactions”)
Даже в пределах одного фолда фичи для кандидатов добавляются дважды, на разные даты, и зависящие от времени фичи уже поменялись.
Трекинг экспериментов: метрики и визуальный анализ
Трекинг экспериментов мы проводили одновременно в Mlflow и в отчётах в репозитории. Обученные модели сохраняли в Mlflow Model Registry. Оттуда же подтягивали их для онлайн рекомендаций в приложении.
Для визуального анализа мы сохраняли рекомендации от разных алгоритмов в общую базу данных. Оттуда их можно отобразить в Jupyter ноутбуке с любой машины. Функционал позволяет сравнивать рекомендации от разных алгоритмов на заданном списке реальных юзеров. Работает приложение на ipywidgets. Чтобы ещё проще было делиться результатами с командой, мы сделали деплой ноутбука с помощью Voila - теперь достаточно просто поделиться ссылкой, чтобы команда могла посмотреть на результаты экспериментов. Превью приложения с визуальным анализом (тестим здесь):
Для расчёта метрик мы использовали единую схему кросс-валидации скользящим окном для всех моделей. Пример конфига кросс-валидации:
params:
N_FOLDS: 3
N_DAYS_FOLD: 7
K_RECOS: 10
HOLDOUT_DAYS: 7
metrics:
recall_10:
metric_type: 'recall'
params:
k: 10
map_10:
metric_type: 'map'
params:
k: 10
serendipity_10:
metric_type: 'serendipity'
params:
k: 10
Здесь указана и сама схема кросс-валидации, и метрики для расчёта. Для реализации расчётов в коде мы снова обратились к RecTools.
Полная инфраструктура проекта
В целом инфраструктура проекта разбита на 2 части. На одном удалённом хосте развёрнуты микросервисы в Docker контейнерах - именно они поддерживают работу приложения и централизованно хранят результаты всех экспериментов. На другом удалённом хосте (или нескольких) запускаются DVC пайплайны для обработки данных и обучения моделей. Схема на картинке:
Жёлтые стрелки отражают запись результатов экспериментов: логирование метрик с кросс-валидации, сохранение обученных моделей в Minio и сохранение рекомендаций в базу данных для визуального анализа.
Само приложение с онлайн-рекомендациями мы строили на простой (относительно реальных production сервисов) архитектуре. Рекомендательные модели подгружаются из Mlflow Model Registry в Model service API (работает на FastAPI). Рекомендации считаются внутри API при поступлении запроса с фронтэнда. Фронтэнд приложения написан на Streamlit. Для маршрутизации и сёрвинга статичного контента используется Nginx.
Jupyter ноутбук с визуальным анализом через Voila встроен в одну из страничек Streamlit приложения. Это сделано для простой демонстрации, но также такой подход можно использовать, чтобы делиться результатами со всеми членами команды по общей ссылке, без необходимости установки окружения на машине и локального запуска ноутбука. Дополнительно мы делали парсинг постеров для фильмов с Kinopoisk API, так как постеры не были частью датасета Kion.
Заключение. Впечатления от командного пет-проекта
Над проектом мы работали вдвоём, в свободное время. Наша команда:
Тихонович Дарья
ML инженер в группе рекомендательных систем MTS BigData
Лидер проекта, ведущий разработчик. Linkedin
Гусаров Григорий
ML инженер (NLP, RecSys)
Разработчик и соавтор проекта. Linkedin
Главными мотивациями были развитие, новый опыт и желание вне рабочих проектов попробовать новые подходы к старым задачкам. Мы оба остались очень довольны полученным результатом и во многом превзошли первоначальный план.
Темп мы взяли очень бодрый. Для того, чтобы трекать задачки, я завела Kanban доску в Notion. Текущие цели по проекту разделяла на трёхнедельные спринты, в начале каждого накидывала общий пул задач и расставляла порядок выполнения, чтобы не было боттлнеков. Дальше разбирали задачки между собой по личным предпочтениям. Для такой маленькой команды стендапы не понадобились, зато постоянно держали связь в телеграм, и все вопросы решали максимально оперативно.
Получилось очень душевно, очень увлекательно, и часто - непросто. Многие задачки были нетривиальными, какие-то инструменты мы использовали впервые, а ресёрч шёл по своей, абсолютно непредвиденной логике.
Наработки по RecSys workflow я планирую развивать и дальше, уже в рабочих проектах. Пайплайны получились очень удобные, отлично подходящие для разных бизнес кейсов и разных датасетов. Возможно, что-то из наших наработок мы поконтрибьютим в open-source.
Моя огромная благодарность Григорию, который прошёл со мной весь путь, помог реализовать все задумки со здоровой долей перфекционизма и внёс вклад во все аспекты проекта, показав себя лучшим напарником!
Большое спасибо Павлу Кикину за два потока курса по MLOps, который добавил мотивации сделать отличный проект. И моим коллегам из МТС за курсы Your First RecSys и Your Second RecSys, а также за отличную библиотеку RecTools, без которой построить наши сложные пайплайны было бы близко к невозможному.
За фидбек, ревью и советы при написании статьи спасибо Александру Петрову @asash и Егору Лобынцеву @egor_labintcev
Прошлые части статьи:
"Часть 1. Popularity bias и визуальный анализ"
"Часть 2. Двухэтапные модели".
Код и инструкцию по развёртке приложения и проведению экспериментов можно найти в репозитории.
Рекомендательные модели работают онлайн здесь.
Визуальный анализ в Jupyter ноутбуке можно потестировать здесь.