
Привет! Это Иван Воробьев, ведущий инженер отдела базового качества рекомендаций в VK Видео (AI VK).
Эта статья — про механизм рекомендаций, который мы называем стримами. Он появился из довольно обычной инженерной ситуации: рекомендательный пайплайн решал сразу несколько разных задач, что приводило к нетривиальным взаимосвязям, и со временем улучшать элементы системы по отдельности стало трудно. Продуктовой фичей стримы при этом не назовёшь: для пользователя изменения не должны быть видны. Ценность другая — командам становится проще дорабатывать отдельные составляющие рекомендаций, не задевая соседние.
С помощью стримов мы разделили один большой пайплайн на несколько специализированных поменьше, у каждого из которых своя задача и своя конфигурация. В рекомендациях VK Видео этот подход уже внедрили, а затем через платформу сделали доступным и для других продуктов.
Пайплайн рекомендаций
Начнём с того, что происходит, когда человек открывает ленту. Сервис идёт в рекомендер за порцией контента, и снаружи это выглядит как одна операция: пришёл запрос — вернулся список объектов. Внутри же проходит несколько этапов, и они примерно одинаковы во всех рекомендательных системах:
-
сбор данных о пользователе;
-
подбор документов-кандидатов;
-
фильтрация;
-
ранжирование;
-
блендинг.
До внедрения стримов все этапы были общими. Кандидаты подбирались сразу для всего, что рекомендер должен сделать в запросе, дальше они проходили через одни и те же фильтры, ранжировались одним и тем же образом, и только в самом конце документы разбивались по меткам и смешивались в выдачу по квотам. То есть разделение задач в системе было, но происходило на последнем шаге.
Стримы переносят это разделение на этап, следующий за сбором данных о пользователе. Запрос по-прежнему приходит в рекомендер один, но дальше разбивается на несколько разных стримов. В рамках каждого из них эффективно независимо подбираются, фильтруются и ранжируются документы. Под капотом часть работы переиспользуется между стримами, но на уровне конфигурации их исполнение можно считать независимым. Блендинг по квотам в конце никуда не делся, а был дополнен возможностью работать не только по меткам, но и по стримам.
Задачи внутри одного пайплайна
Среди задач, решаемых в рамках запроса к рекомендеру, есть следующие:
1. Подбор по известным интересам. Показать то, что должно понравиться человеку прямо сейчас. Для этого система использует уже собранные знания о его предпочтениях.
2. Исследование интересов пользователя. Показать что-то новое, чтобы открыть новые темы. Например, если он лайкнул котика, это не повод показывать дальше одних котиков: стоит попробовать автомобили, спорт и другие темы, и посмотреть на реакцию.
3. Исследование документов. Показать пользователям новый или малоизвестный объект нужно, чтобы понять, кому его стоит рекомендовать дальше.
4. Поддержка авторов. Помочь новым и небольшим авторам найти свою аудиторию, чтобы платформа не замыкалась на тех, кто уже набрал охваты.
Разберём подробнее пару из них: подбор по известным интересам и изучение новых. В рекомендательных системах это классическая пара: система применяет то, что уже знает про пользователя и про документы, и параллельно собирает новые данные. Решение первой задачи опирается на то, в чём система уверена, а вторая решается ради сбора дополнительных данных для решения первой.
Пока обе задачи решаются одним пайплайном, настраивать их одновременно проблематично. Подбор кандидатов общий, фильтры общие, ранжирование общее, а цели у задач разные — и значит, каждая настройка становится компромиссом. Хочется развести эти конфигурации и двигать каждую отдельно, не оглядываясь на соседнюю, а замешивание по квотам оставить там же, где оно и было, — в самом конце.
Связность и связанные с ней проблемы
До стримов вышеперечисленные задачи решались одним пайплайном, с помощью квотирования по меткам документов. Проблема была в том, что они оказались сцеплены общей конфигурацией — пользовались общим ранжированием, делили параметры, и одна настройка могла влиять сразу на несколько смысловых задач. Из-за этого их было трудно развивать и проверять независимо друг от друга.
На практике это выражалось так:
1. Конфигурация ветвилась по каждой настройке в отдельности. Когда один запрос обслуживает несколько задач, настройки обрастают условиями: в таком-то случае применить фильтр, в другом не применять. Ветвилась при этом не конфигурация целиком, а каждый параметр сам по себе, и понять, что получается в сумме, становилось всё труднее.
2. Изменения было сложно изолировать, а вклад конкретной механики — оценить. Разные задачи решались в рамках общего процесса: использовали одних и тех же кандидатов, общее ранжирование и общую выдачу. Поэтому при изменении одной механики приходилось отдельно проверять, как оно повлияло на остальные. По той же причине было сложнее понять, какой именно механизм повлиял на результат. Метки и информация об источнике кандидата помогали отслеживать, откуда появился документ, но один и тот же документ мог попасть в выдачу разными путями и пройти общие этапы обработки. При этом сбои обычно удавалось находить по мониторингам, но проблема одного механизма могла затронуть весь запрос.
3. Эксперименты конкурировали за общие слоты. Разводить A/B по слоям так, чтобы они не пересекались на пользователях, умели и раньше. Проблема была в другом: один крупный параметр или флаг мог одновременно менять несколько смысловых частей рекомендера, поэтому эксперимент по одной задаче занимал слот в том же слое, что и эксперимент по другой.
Разделение пайплайнов с помощью стримов
Чтобы разделить части пайплайна, решающие разные задачи, были реализованы стримы. Стрим — это отдельный пайплайн подбора рекомендаций со своей задачей и своей конфигурацией.
Запрос приходит в рекомендер и разбивается на стримы, каждый из которых независимо решает свою подзадачу и возвращает свои документы. На выходе всё равно получается одна выдача — результаты стримов объединяются по квотам тем же замешиванием, что было и раньше.
Разделить общий пайплайн на стримы можно по-разному, в частности:
1. Вынести в отдельные стримы функциональные части. Например, упомянутые подбор по известным интересам и исследование интересов пользователя.
2. Вынести в отдельные стримы конфигурации отдельных поверхностей. Тогда различия между поверхностями живут не в общих настройках, а в том, какой набор стримов для поверхности запрашивается.
Подходы не исключают друг друга: для одной поверхности может вызываться набор стримов, решающих разные задачи. Работать стримы могут одновременно, а могут один вместо другого, в зависимости от задач, ими решаемых.
Конфигурация стримов
Чтобы не конфигурировать каждый стрим с нуля, внутри рекомендера есть набор «кубиков» — отдельных частей пайплайна. Кубиками являются методы подбора кандидатов, фильтрации, вычисления признаков и некоторые другие инструменты. Конфигурация стрима во многом сводится к тому, какие кубики он берёт — настройки самих кубиков общие для всех стримов. Существующих кубиков хватает не всегда: если новая задача потребовала кубика, которого в наборе нет, его придётся написать — зато дальше он доступен всем остальным стримам.
Отсюда следует важное свойство: два стрима — это не две отдельные кодовые базы. Они выбирают разные компоненты общего кода, и новый сценарий часто можно собрать из существующих компонентов вместо разработки отдельного рекомендера или ветвления общей логики через конфигурации. У конфигурации тоже есть свой релизный процесс, просто он легче, чем изменение общей логики рекомендера.
Польза стримов
Новую механику стало проще выделить и настраивать отдельно: она заводится своим стримом и в типичном случае собирается из существующих кубиков. Проще стало и выделять вклад конкретного механизма. Отказ одного стрима больше не роняет остальные. И для работы с конкретной задачей теперь достаточно держать в голове локальную конфигурацию, а не весь огромный конфиг целиком. Кроме того, часть логов и метрик сейчас пишется с указанием конкретного стрима, так что в значимых местах можно смотреть показатели по отдельным стримам, а не только по рекомендеру целиком. Команды при этом меньше влияют друг на друга: изменение внутри стрима остаётся локальным, но общая выдача у продукта по-прежнему одна.
Параллельных экспериментов стало больше: настройки локальнее, поэтому команды реже конкурируют за одни и те же слоты. Полной независимости всё равно не возникает, крупные общие изменения вроде блендинга по-прежнему требуют общих слотов. Важная оговорка: общий объём конфигурации не уменьшился. Изменилось не количество настроек, а их разделённость — конфигурация стала более структурированной и простой для локальной работы.
Цена разделения
У разделения есть цена, и на неё стоит посмотреть отдельно. Несколько стримов потенциально создают дополнительные расходы: часть структур приходится создавать отдельно для каждого, а выполняются стримы последовательно.
Чтобы это компенсировать, под капотом сделали два изменения:
1. Кубики перестали пересоздаваться. Большая часть кубиков не создаётся заново ни между запросами, ни между стримами. Эта оптимизация полезна и без стримов, но со стримами она становится критичной.
2. Часть вычислений переиспользуется между стримами внутри запроса. Например, признаки: посчитанное для одного стрима не считается заново для другого. Это уже прямое сокращение накладных расходов на стримы.
Суммарно по рекомендеру переход на стримы вышел примерно в ноль по ресурсам: то, что терялось на накладных расходах в случае нескольких стримов, компенсировало кеширование кубиков между запросами. Это было существенным требованием: независимость логики не должна означать дублирование всей работы под капотом. Удобство разработки и конфигурации само по себе не может оправдать кратный рост потребления вычислительных ресурсов.
Стримы на практике
В рекомендациях VK Видео в отдельные стримы выделили подбор по известным интересам, исследование интересов пользователя, прогрев документов, фильмы и выдачу для новых пользователей. Это не весь набор — здесь перечислена лишь часть.
В отдельные стримы вынесли и конфигурации отдельных табов — например, детского или политики. Раньше такие различия приходилось задавать, варьируя каждую настройку по отдельности, а теперь они группируются по стримам, и для конкретного запроса вызываются нужные. Менять их и ставить на них эксперименты тоже можно по отдельности.
Кроме того, новые механики в VK Видео теперь заводят сразу стримами. Вот несколько примеров.
1. Стрим фильмов. Для фильмов собрали свою конфигурацию подбора и ранжирования, не добавляя ветвлений в общий код рекомендера. Это позволило показывать фильмы только тем пользователям, которым они интересны, и за счёт этого нарастить суммарное время просмотра на пару процентов.
2. Выдача для новых пользователей. Механику завели сразу отдельным стримом: у прогретых и новых доступны разные источники кандидатов — нельзя, например, искать контент, похожий на историю просмотров, если этой истории ещё нет. Значит, нужен собственный набор источников, и держать его отдельной конфигурацией удобнее, чем набором исключений внутри общей.
3. Персональный стрим в рекомендациях к видео. Это рекомендации, которые человек видит при просмотре конкретного видео. Сначала добавили оценку, нужен ли персональный подбор для конкретного запроса, и вынесли его отдельным стримом. Реализация прошла в основном по конфигурации, хотя пару новых механик всё же пришлось добавить, а под задачу дополнительно обучили модели. По результатам экспериментов получили около 3% суммарного времени просмотра. Весь путь, вместе с несколькими итерациями экспериментов, занял квартал.
4. Стрим мягких подписок на главной. Задача продуктовая — помочь небольшим авторам находить свою аудиторию. Здесь тоже почти всё решилось конфигурацией, дописать пришлось только подготовку данных. В результате число авторов, у которых набралась аудитория хотя бы в тысячу мягких подписок, выросло на 15%, и на столько же выросло число уникальных авторов VK Видео.
Стримы изначально появились как механизм внутри отдельного рекомендера VK Видео, а затем были перенесены в Discovery-платформу. Благодаря этому механизм в принципе доступен рекомендерам других продуктов платформы: подключение не требует отдельной интеграции, для использования стримов достаточно сконфигурировать их. Обратная совместимость при этом сохраняется: конфигурации без стримов продолжают работать как прежде.
В VK Клипах конфигурации для отдельных поверхностей тоже начали выносить в отдельные стримы. Разнообразие задач, решаемых в рамках одной поверхности VK Клипов, не столь велико, но различие конфигураций между поверхностями весьма значительно. Следующим на очереди стоит Дзен, а за ним и некоторые другие продукты.
Итоги стримов
Все изменения можно свести к двум состояниям:
1. Было. Один запрос, внутри которого общий подбор кандидатов, общие фильтры и общее ранжирование использовались для решения разных по смыслу задач, разделяемых только в конце, при замешивании по квотам.
2. Стало. Несколько стримов внутри того же рекомендера поверх общего набора кубиков, а результаты по-прежнему собираются в одну выдачу: в рамках каждого из них кандидаты подбираются, фильтруются и ранжируются под конкретную задачу.
Кроме того, из всей проделанной работы можно сделать такие выводы:
1. Сцепленность набирается незаметно. Решение встроить очередную механику в существующий пайплайн почти никогда не выглядит ошибкой в тот момент, когда его принимают, поскольку так действительно дешевле. Цена появляется только в сумме и только через годы, поэтому предъявить её на ревью одного изменения не получается — её видно лишь по тому, что менять систему становится все более трудоемко.
2. Граница разделения проходит по задаче, а не по коду. Разводить стоит не компоненты и не сервисы — они как раз остаются общими, иначе разделение превращается в дублирование. Разводить стоит то, что система пытается сделать в конкретном запросе, и дальше уже конфигурировать отдельные пайплайны под конкретные задачи.
3. Разделение ускоряет разработку, а не улучшает метрики. Качество растят механики, а не сама возможность их развести. Смысл разделения в другом: за тот же срок этих механик успевает появиться и доехать до продакшена заметно больше, и их становится проще мониторить и изменять.
А у вас внутри одного пайплайна тоже накопились разные по смыслу задачи? Расскажите в комментариях, как вы их разделяете и что при этом оставляете общим.
Автор: vorobev-iv


