Привет, Хабр. Меня зовут Алексей Жиряков, я техлид backend-команды витрины онлайн-кинотеатра KION. Этот текст написан по мотивам моего выступления на МТС True Tech Day, и это уже его вторая часть. В первой части я писал о витринах и полках в KION, о том, как мы внедряем ML в баннерную полку и просчитываем тренды. Сегодня поговорим об обучении нейронки и бизнес-правилах.
Как обучаем нейронку и готовим полку
Все просто и сложно одновременно. Мы берем тайтлы, которые пользователи посмотрели, и подаем на вход список фич: новизну, коэффициент Жаккара (о нем ниже), тренд смотрения и другие. Нейронка генерирует векторы, состоящие из 128 разрядных пространств. Мы считаем расстояние между тайтлами, которые пользователь посмотрел. То же самое делаем для рандомного тайтла, с которым он пока не ознакомился.
После обучения нейронки получается вектор из 128 измерений. Для примера я взял два: это самые близкие тайтлы к текущему. Мы знаем, к какому тайтлу он пришел, и просто берем те, что располагаются рядом на евклидовом пространстве.
На скриншоте — полка с главной страницы. Она персональная, похожа на «Смотрите также». У нее два уровня: сначала подбираются кандидаты, а потом уже из них она и строится.
Подбор организован так: приходит запрос пользователя, мы знаем, что он посмотрел, и берем похожие фильмы. Из них строим эту полку: учитывается новизна (дата выхода), рейтинг и новизна тайтла, то есть то, что пользователь еще не видел.
Прежде чем начать AB-эксперименты, мы подготавливаем модель-кандидат. Применяем различные офлайн-метрики, расскажу про них ниже. Что касается онлайна, то мы смотрим в первую очередь на TvTu или на CTR осознанного смотрения. У нас есть пирамиды метрик: если «не красят» наши основные метрики, мы оцениваем уже на более второстепенные. Если общий TvTu «не красит», смотрим тот же показатель для этой полки. Если прокрашивается, принимаем решение катить.
Как считаем и обучаем модели
Обучение проводим на промежутке времени, но не до текущего момента. Для контроля оставляем себе какой-то результат, потом прогоняем модель для пользователей, проверяем, что она рекомендует. При этом мы знаем, что зрители уже посмотрели. С помощью функции MAP Recall смотрим, насколько часто вообще модель-кандидат предлагает то, что пользователь посмотрел. И если предлагает, то на каких позициях.
В баннерной модели учитывается еще и процент новых тайтлов в рекомендациях из всех. То есть она должна рекомендовать новые тайтлы максимально близко к интересам зрителей.
В i2i примерно то же самое — есть размеченные пары. Мы обучаем модель и смотрим, насколько для таких пар совпадает ожидание. Еще учитываем метаданные — это фильмы и ключевые слова, которые заполняет редакция, актеры, режиссеры, страна. Используем коэффициент Жаккара.
Как мы считаем коэффициент Жаккара? Берем два тайтла, пересекаем множество и делим на объединение множеств. Эти метаданные имеют весомые значения.
Позитивные фичи
Позитивные фичи — это смотрение, просмотр больше, чем N минут. Не процент, а именно определенное количество минут. Все потому, что большие тайтлы досматривают реже и процент досмотра у них меньше. В команде мы условились, что если смотрят больше, чем N минут, то этот тайтл будет показываться вверху и для других. Мы знаем среднее значение повышенной конверсии в просмотр: если у тайтла она больше, он будет выше. Конечно, учитывается и новизна.
Мы не просто хотим формировать персональную витрину для каждого юзера — мы хотим, чтобы она ежедневно обновлялась. Даже если кто-то зашел на сервис, но ничего не посмотрел или посмотрел только телевизор, завтра мы предложим такому пользователю другую витрину.
В то же время мы не хотим, чтобы витрина менялась слишком часто. Например, пользователь зашел в какую-то полку или в карточку, вернулся обратно на витрину, а там все по-другому. Он паникует, а нам этого не нужно. Мы хотим, чтобы витрина перестраивалась раз в сутки — это достигается за счет рандомизации. Плюс еще каждые сутки у нас новая статистика, идет переобучение и мы учитываем новизну тайтлов.
Где спотыкаемся
Конечно, не обходится без сложностей. Например, много случаев, когда мультики, фильмы и сериалы с одного аккаунта смотрят и взрослые, и дети. Тогда нужно суметь сосчитать все это на витрине.
Еще есть такая штука, как скорость изменений. Часто меняется информационная повестка, равно как и интересы у пользователей. Например, зритель вдруг может начать смотреть комедии несколько дней подряд. Что тогда делать с витриной: показывать только комедии или поступить иначе? Мы проверяем глубину просмотра — для обучения используем 180 дней. Если сделать срок меньше, витрина будет более оперативно реагировать на изменения. Если больше — станет инертнее.
И вишенка на торте: не все пользователи одинаково полезны для обучения. Некоторые из них заходят и смотрят только популярное. Построить логическую цепочку для них сложно. Плюс еще непонятно: вот если человек посмотрел один конкретный тайтл, то рекомендовать ему сразу похожие или нет?
Главное о бизнес-правилах
Бизнес-правила мы добавили специально, чтобы наши молодые ученые не думали о бытовых вещах. ML оперирует вероятностями: он не может гарантировать, что что-то будет исполнено. Поэтому наши бизнес-правила реализованы отдельным сервисом — блендером. Он «ходит» в разные ML-ные источники (ссылки для получения персональной точки взаимодействия), компонует витрину и применяет бизнес-правила.
Какие бизнес-правила мы используем?
Бустинг. Редакция может поставить флаг у тайтла, чтобы он бустился. Тогда он будет вставать на первое место в полке, которую показывает ML и где этот тайтл есть. Это не значит, что ужасы попадут в комедии. Если тайтл бустится в этой полке, он будет ставиться на первом месте. Плюс у нас реализовано отключение. Считаем CTR у соседей тайтлов в зоне видимости — если он сильно выше, чем у бустингового фильма, мы отключаем бустинг. Тогда тайтл с бустингом будет показан на том месте, которое рекомендует ML. На скриншоте выше «Наследники» встают на первое место.
Зачем все это нужно? Во-первых, это выручает в ситуациях, когда еще нет статистики просмотров за определенный срок. Во-вторых, редакция может поставить бустинг тайтлу, который продвигается по ТВ или еще в каких-то источниках. ML про них не «знает», поэтому действовать нужно вручную.
Расстановка полок на витрине. Редакция может сформировать витрину по-разному для каждой из платформ. Это делается при помощи CMS как запись, а потом мы это реализовываем. То есть интерпретируем значения, которые нам поступают.
Есть еще и вторая система расстановки полок, реализованная на бэкенде. Этот вариант нужен специально для проведения AB-экспериментов. Там можно привязывать логику. Например, в вебе, в неавторизованной зоне на первом месте мы показывали промобаннер, а баннерную карусель разместили на третьем месте просто потому, что две большие полки некрасиво смотрятся рядом. В авторизованной зоне баннер уже не показывали, демонстрируя баннерную полку.
Дедуплицирование. Иногда модель слишком настойчиво предлагает посмотреть какой-то тайтл. Например, «Жизнь по вызову 2» располагается в разных полках в зоне видимости. Мы применяем дедуплицирование, проверяем и оставляем тайтл только в верхней полке, если он был среди первых пяти.
После дедуплицирования мы убрали «Жизнь по вызову 2» из более низкой полки.
Удаление просмотренных тайтлов. Это позволяет не выжигать показы и экономить место, чтобы пользователь видел какие-то другие тайтлы. Например, человек посмотрел «Расправляя крылья», и его больше нет на этой полке. Но если он только начал смотреть и не досмотрел, тайтл появится в полке «Продолжить просмотр».
Недавно мы сделали исключение, которого нет у конкурентов. Так, если возраст зрителя меньше 18, то мы не удаляем мультсериалы, потому что дети любят смотреть одно и то же много раз подряд.
Разнос франшиз. Правило добавили по просьбе редакции, чтобы ликвидировать баннерную слепоту. Дело в том, что у франшиз часто похожие баннеры.
На скрине выше показан «Последний богатырь». Визуалы схожи, так что у пользователя может возникнуть «баннерная слепота». Чтобы этого не случилось, франшизы редакция размечает специальными тегами. Если они совпадают и стоят рядом, то мы их разносим. Этот параметр, коэффициент разноса, можно менять в реальном времени. Он регулируется. Можно поставить 1, 2 или больше. Так что «слепота» у нас не пройдет!
Есть еще несколько параметров, которые для нас важны:
Количество платного контента. В зависимости от времени, этот параметр может регулироваться. Мы все равно его проверяем, так как не хотим, чтобы все было бесплатным или платным.
Количество бустинговых тайтлов. Если редакция ставит тайтлы вручную, есть риск, что их забудут снять. А если их станет слишком много, ML-эффект снизится. Поэтому мы проверяем количество бустинговых фильмов на витрине и внутри полок, используя разные параметры. В случае превышения мы «отстреливаем» лишнее и просим редакцию все перепроверить.
Дедупликация баннеров. Модель показывает новое, редакция ставит новое в первых местах — мы не хотим, чтобы баннеры были одинаковые. Плюс для одного и того же тайтла может быть несколько баннеров. И мы не хотим, чтобы они показывались. Поэтому мы их дедуплицируем и оставляем только более ранние версии.
Это только примеры основных бизнес-правил, на самом деле их больше 50.
Можно внедрить много отличных возможностей, но важно, чтобы они работали для всех, а не только для узкого круга пользователей. А еще — чтобы ничего не ломалось в ходе формирования витрин, например, чтобы они не начали показывать одно и то же. Для этого мы «запоминаем», что показали конкретным пользователям вчера, и отслеживаем, что показываем сегодня. Если витрины совпадают, сообщаем о проблеме. После этого чекаем ежедневную новизну тайтлов в зоне видимости. Мы хотим, чтобы полки тоже менялись еженедельно — все это тоже проверяется.
О нагрузках и стеке технологий
Формирование витрин и полок реализуется под нагрузкой в 600 RPS. Чтобы избежать проблем, мы используем Python 3.11, который получил сильный прирост производительности по сравнению с предыдущими релизами. Еще применяем FastAPI для обучения MLflow и Airflow для бэкграундных процессов. Отвечаем в 99% случаев меньше чем за 160 миллисекунд.
Благодаря выбранному стеку у нас очень маленький time-to-market. В KION действует концепция: вся логика на бэке, потому что у клиентов длиннее time-to-market.
Мы работаем над этим, но на бэке он все равно будет намного короче. К слову, один раз срочную фичу нам удалось «докатить» до прода за два с половиной часа со всеми тестами и необходимыми процедурами. Не забывайте: мы выполняем больше 50 бизнес-правил.
Пока на этом все. Если вдруг есть вопросы, оставляйте их в комментариях — постараюсь на все ответить!