В первой части статьи я описывала, как мы с напарником решили выкатить модель из соревнования в онлайн рекомендации, и что из этого вышло. Напомню, что мы: Увидели проблему popularity bias и научились её диагностировать Обосновали необходимость визуального анализа при оценке алгоритмов Отказались от прямой максимизиации конкретных метрик и построили сбалансированную модель implicit bm25 Эксперименты мы строили на датасете онлайн-кинотеатра Kion. Модели рекомендуют фильмы и сериалы онлайн в нашем приложении. Построенная в прошлый раз модель неплохо умеет собирать тематичные подборки на большинство запросов, но на визуальном анализе мы увидели, что в рекомендациях могут проскакивать лишние айтемы, сильно выбивающиеся по жанру. Например, среди американских детективов неожиданно проскакивают русские комедии.  А вот проблема “Гладиатора”: Отсутствующий постер скрывает за собой фильм для взрослых. Предлагать его в топе рекомендаций на исторический боевик - это серьёзный промах. Тем более что дальше следуют вполне релевантные айтемы. Будем лечить нашу модель. Продолжаю истории по шагам о том, как мы дропаем ранжирующие метрики и улучшаем при этом качество рекомендаций. Цель градиентного бустинга в нашей рекомендательной системе - поменять порядок кандидатов от модели первого этапа для того, чтобы улучшить выдачу. При этом для кандидатов можно добавить любые фичи: популярность айтемов, их жанры, фичи юзера, для которого мы строим рекомендации. Можно добавлять и перекрёстные фичи - те, что характеризуют пару юзер-айтем. Давайте построим фичу для бустинга, которая будет решать главную проблему нашей текущей модели - не релевантные айтемы в хороших подборках. Для этого нужно научиться считать релевантность жанров айтема интересам юзера. Каждый айтем в датасете Kion описывается набором жанров и стран производства. Возьмём 12 самых частых жанров и 3 “страны” (Россия, США, остальной мир). Для всех айтемов построим векторы из dummy переменных по этим фичам - получим dummy векторы, которые характеризуют айтемы в отношении жанров и стран. Для получения вектора интересов юзера будем усреднять такие dummy векторы айтемов из истории просмотров юзера. Дальше, когда нам нужно посчитать скор релевантности айтема для юзера: мы будем считать косинус между вектором интересов юзера и dummy вектором айтема. Используя такой скор как фичу в бустинге, мы сможем продвигать в рекомендациях айтемы, более близкие юзеру по жанрам. Также у нас были замечательные фичи, проверенные ещё на модели в соревновании. Добавим айтемам фичи аудитории (доля молодой аудитории в просмотрах, доля женской аудитории), а юзерам - пол и возраст, и дадим бустингу возможность подбирать фильмы, более подходящие по соц-дему. Звучит разумно, и мы всё это пробовали. Guess what? Рекомендации не улучшились (ни по метрикам, ни визуально). И вот почему. Вспомним, какую задачу решает бустинг. Для простоты рассмотрим на примере задачи классификации, для ранжирующего лосса логика будет похожей. В задаче классификации мы подаём модели кандидатов, бывших в интеракциях в качестве позитивных таргетов, и не бывших в качестве негативных, и учим его отличать их друг от друга. Распределение популярности айтемов даже на отдельно взятом периоде у нас всё ещё следует степенному закону, и вероятность взаимодействия юзеров с хитами всё ещё значительно выше, чем с релевантными айтемами. И первое, что делает бустинг, решая свою задачу классификации - это вытягивает хиты в топ рекомендаций. И даже такие простые фичи как доля молодой или женской аудитории уже могут помочь бустингу вычленить хиты из общей массы кандидатов и продвинуть их вперёд. Результат я много показывала в прошлой части статьи - popularity bias. Модель снова рекомендует “Прабабушку лёгкого поведения” на самые разные запросы. Бороться с этим можно разными способами: hard negative sampling на популярные айтемы. Мы как можно чаще предлагаем бустингу в негативных таргетах популярные айтемы. Если в негативах становится столько же айтемов-хитов, сколько в позитивах, бустинг не имеет причин постоянно завышать для хитов вероятность взаимодействия и продвигать таким образом в рекомендациях. Например, можно семплировать негативы с вероятностью, пропорциональной популярности айтемов. Этот подход не только интуитивно понятен, но также имеет математическое обоснование как способ приближения point-wise mutual information (и нивелирования popularity bias) в процессе обучения рекомендательной модели. Подробности в этой статье. изменение веса айтемов-хитов в лосс функции. Если снизить суммарный вес позитивных таргетов для айтемов-хитов в лосс функции,  бустинг реже начинает подтягивать их в рекомендации. Мы пробовали разные варианты, popularity bias вылечивался. А вот качество рекомендаций бустинг поднять не смог ни по каким предложенным фичам. Метрики от модели первого этапа менялись незначительно и без положительной динамики, а визуальный анализ не показывал закономерностей в изменении выдачи после ре-ранжирования. Близость по жанрам не давала сильного сигнала о том, окажется ли айтем в просмотрах, ведь по факту истории просмотров юзеров в онлайн-кинотеатре имеют сильное разнообразие по жанрам: сегодня юзер смотрит завораживающий триллер, завтра - романтическую комедию. Это отлично и увлекательно, но никак не может нам помочь построить рекомендательную систему, ведь рекомендовать ромкомы после триллеров мы не можем. Почему я так подробно описала наши подходы, если они не сработали? Дело в особенностях датасета. На других данных такие же подходы помогали улучшить качество рекомендаций и полностью решить проблему не релевантных айтемов в подборках. Но в машинном обучении, как обычно, нет ни серебряной пули, ни бесплатных обедов. Для Киона нам придётся придумать новый способ улучшить рекомендации. Итак, градиентный бустинг не взлетел, и мы возвращаемся к модели, которую построили в прошлой части статьи по сбалансированным метрикам. Наша модель неплохо умеет рекомендовать, но подмешивает в подборки лишние айтемы. Также у нас есть фича, которая должна нам помочь: рассчитанный скор релевантности жанров айтема интересам юзера. Но бустинг фичу не подхватил - уж очень разнообразны бывают просмотры пользователей в онлайн-кинотеатре. Что делать? Самое простое - решать задачу в лоб. Давайте на втором этапе вместо бустинга используем content-based подход. Давайте возьмём подборку кандидатов от модели bm25 с их порядковыми рангами (1, 2, 3 и т.д.). Для всех кандидатов рассчитаем скор релевантности жанров интересам юзера. Отсортируем список кандидатов по полученным скорам релевантности и получим новые порядковые ранги. Останется решить, как формировать финальный список рекомендаций, имея два ранга на каждый айтем. Мы потестировали несколько подходов, посмотрели метрики и визуальный анализ и остановились на том, чтобы взвешивать ранг от bm25 и от скора релевантности по жанрам с весом 1:2 и сортировать айтемы по этому взвешенному рангу. Это и стало финальным сбалансированным решением нашей задачи. Как там наш “Гладиатор”? Подборки от финальной модели в целом получаются вполне душевные и тематичные. Посмотрим ещё пару примеров? Детективно-сериальная подборка: Спортивно-биографичная: Природно-документальная: Сказочная: Мультики даже показывать не буду. Все модели всегда отлично умеют рекомендовать мультики. Кто знает, возможно, мы просто недостаточно хорошо в них разбираемся, чтобы видеть проблемы. Есть ли право у нас как у исследователей утверждать, что это лучшие рекомендации из всех алгоритмов, с которыми мы экспериментировали на датасете? Мы можем делать это, помня о том, насколько субъективны любые подобные суждения. Выбирая финальную модель визуальном анализе, важно помнить о сильном влиянии bias исследователя. Личные ощущения “правильности” рекомендаций нельзя перенести на аудиторию, которая будет пользоваться сервисом и получать рекомендации. Я уже подробно писала об этом в прошлой части статьи, но здесь повторю еще раз возможные шаги для коррекции bias исследователя: привлечь к оценкам экспертов в предметной области провести user study провести АБ тест для оценки бизнес метрик А вы можете сформировать собственное мнение, просто потестив рекомендации от финального алгоритма онлайн здесь: https://recsysart.ru Все метрики считались для первых 10 айтемов в списке рекомендаций (k=10). Что максимизируем Модель MAP Recall Recall_no_pop Serendipity MIUF Popular intersection - Random 0.000 0.000 0.001 0.00001 15.6 0% - Popular 0.097 0.218 0.000 0.00004 4.5 100% MAP & Recall Competition 2-stage: cosine + catboost 0.107 0.226 0.062 0.00017 5.2 62% Баланс метрик Implicit bm25 0.046 0.116 0.091 0.00020 6.7 12% Visual winner! 2-stage: bm25 + content-based 0.034 0.103 0.083 0.00022 7.0 10% Да, мы дропнули ключевую ранжирующую метрику MAP@10 в 3 раза, пройдя путь от модели из соревнования (Competition 2-stage: cosine + catboost) до сбалансированного алгоритма (2-stage: bm25 + content-based). А также в 6 раз снизили пересечение с популярным алгоритмом, в 60 раз реже стали рекомендовать юзерам самый популярный айтем, и на 30% подняли Recall без популярного. Подняли  Serendipity и Mean Inverse User Frequency - а значит, научились больше и точнее подбирать рекомендации для юзеров вне популярных айтемов. И главное - подборки рекомендаций от финальной модели действительно могут подсказать интересный фильм на вечер. Получился отличный кейс для демонстрации того, как сильно отличаются модели, которые мы строим для data science соревнований, от моделей, которые реально применимы на практике. В общем случае проблема в том, что сложные ансамбли из нейронок с кастомными фичами, вытягивающие максимум из целевых метрик, сложно и неоправданно дорого поддерживать в production. В случае рекомендательных систем я показываю, что задача максимизации одной определённой метрики бывает далека от задачи, стоящей со стороны бизнеса. Часто предсказательная сила - совсем не то, что нужно максимизировать, чтобы строить полезные рекомендации. Поделюсь тем, как мы подошли к проблеме валидации двухэтапных моделей, чтобы не только получать красивые таблички с метриками, но ещё иметь возможность им верить. Если максимально кратко и схематично изобразить двухэтапную рекомендательную модель с градиентным бустингом (в данном случае Catboost с классификационным лоссом) для реранжирования, получим вот такую картинку: Видим, что катбуст использует определенный период в интеракциях для того, чтобы на нём учиться. Голубой отрезок таймлайна на левой картинке - это период, на котором учатся и генерируют кандидатов модели первого этапа, например, коллаборативная фильтрация. Фиолетовый отрезок таймлайна на левой картинке - это период, с которого собираются позитивные и негативные таргеты для бустинга. Катбуст учится реранжировать кандидатов. Жёлтый отрезок таймлайна на правой картинке - это полные интеракции (два прошлых периода объединены). На инференс всей двухэтапной архитектуры модели первого этапа обучаются заново, уже на полных интеракциях, и передают своих кандидатов с фичами обученному катбусту для реранжирования. Если вам не знакома концепция двухэтапных рекомендательных моделей, подробно её разбирают в этой лекции. Итак, двухэтапная архитектура имеет сложную логику работы со временем в датасете. И валидировать такую модель приходится очень аккуратно. Самое простое, что можно было бы сделать - обучать и валидировать модель лишь единожды, на одном периоде тестовых интеракций - например, на неделе сразу после периода обучения. На картинке выглядеть это будет так: Красный отрезок на правой картинке - это тестовые интеракции, на которых мы считаем метрики для оценки качества рекомендаций. Такая схема далека от идеала. Какие-то значения метрик мы получим, но насколько наши результаты можно распространять на другие временные периоды? Вы наблюдали когда-нибудь, как сильно меняется распределение просмотров айтемов в том же онлайн-кинотеатре? Давайте посмотрим на примере просмотров топ-100 фильмов в Кионе по неделям: Видите эти пики? Что будет, если мы будем обучать или валидировать модель в момент такого врыва и не проверим результат на других периодах? Как минимум, у нас будет серьёзный concept drift, потому что модель будет обучаться на совершенно нехарактерном распределении таргетов (здесь можно поспорить, к какому ещё типу дрифта отнести наш кейс). Если подбирать на таком периоде гипер-параметры или делать отбор фичей, мы рискуем сильным оверфитом на текущий период и нестабильной работой модели. Можно было бы предложить просто избегать нехарактерные по просмотрам недели при обучении и валидации бустинга, но гораздо корректнее будет сделать кросс-валидацию. Самый стабильный подход к кросс-валидации в рекомендательных системах - это схема скользящего окна, которая больше всего соответствует работе модели в реальном мире. Например, для кросс-валидации на трёх фолдах мы отрезаем от датасета 3 недели, которые последовательно выступают периодом тестовых интеракций. Модели для каждого фолда учатся только на тех интеракциях, что предшествуют тестовым, и не имеют возможности заглядывать в будущее, а валидируются на ближайшей неделе после своего обучения. Что будет, если мы попробуем соединить схему кросс-валидации скользящим окном со сложной логикой обучения двухэтапной модели? Получим вот такую цветную лесенку (каждый цвет - отдельный фолд): Самое сложное в реализации этой схемы в коде - те самые маленькие стрелочки внизу. На каждую дату, которая участвует в обучении и валидации моделей идут свои временные фичи. Мы обучили бустинг на одном периоде времени, а инференс делаем на другом, и фичи у айтемов уже поменялись. И так происходит для каждого фолда. Если проводить с моделями много экспериментов, расчёты фичей становятся очень дорогим удовольствием. В следующей части статьи я расскажу, как мы решали данную проблему и как в целом построили workflow экспериментов в проекте. А также опишу инфраструктуру нашего онлайн приложения и платформы для экспериментов. Обещаю много MLOps для RecSys. Рекомендательные модели работают онлайн здесь.Визуальный анализ в Jupyter ноутбуке можно потестировать здесь.  Наша команда ML инженер в группе рекомендательных систем MTS BigDataЛидер проекта, ведущий разработчик. Linkedin ML инженер (NLP, RecSys)Разработчик и соавтор проекта. Linkedin За ревью спасибо@asashи @egor_labintcev Другие части статьи:"Часть 1. Визуальный анализ и popularity bias""Часть 3. Платформа для экспериментов"