Привет, Хабр! Меня зовут Иван Воробьев, я работаю в команде рекомендаций VK Видео, AI VK. В данной статье хочу рассказать, как и зачем я переделывал систему построения I2I-рекомендаций. Поговорим о том, какие решения были поставлены в её основу, насколько они оправдались, а также причём тут якори и как они связаны со свежестью рекомендаций. 

В первую очередь сосредоточимся на бэкенде: организации работы с данными и вычислений, их конфигурациях, и требуемых вычислительных ресурсах. Основной эффект от внедрения новой системы кроется именно тут: удалось обеспечить свежесть I2I-рекомендаций примерно в час, сократив при этом количество используемых ядер в три раза. Саму логику построения рекомендаций тоже затронем, но в меньшей мере.

Что такое I2I, или как отделить машины от котиков  

Начнем со способов подбора кандидатов для ранжирования. В принципе, стандартным способом персонализированного подбора кандидатов является поиск документов, похожих по эмбеддингу на пользователя. Однако, у этого способа есть  ограничение: если строить один эмбеддинг на пользователя, то в нём будут каким-то образом будут усреднены разные интересы этого пользователя. Это не мешает ранжированию, поскольку там нужны относительные величины, но в случае подбора кандидатов такое усреднение приводит к парадоксальным результатам.

Профиль пользователя и ближайшие к нему документы
Профиль пользователя и ближайшие к нему документы

Предположим, что пользователь провзаимодействовал с двумя видео, одно из которых - про котиков, а второе – про машины. Если построить эмбеддинг пользователя как среднее между эмбеддингами этих видео, и потом найти к нему ближайшее видео, то им может оказаться, например, произвольное юмористическое видео. Так получается, поскольку тот самый эмбеддинг пользователя оказался в достаточно произвольной точке векторного пространства, и конкретно это видео оказалось ближе к ней, чем оба исходных или им подобные. С тем же успехом там могли попасться, например, новости. Как же тогда подобрать что-то более релевантное?

Например, можно искать видео, похожие на просмотренные пользователем документы-якори. Но искать похожие по эмбеддингу для каждого просмотренного пользователем видео будет достаточно долго. Впрочем, тут можно воспользоваться тем, что список этих похожих не зависит от самого пользователя. Значит, что такие списки (далее – I2I-рекомендации) можно построить оффлайн, и в рантайме просто доставать готовые результаты.

Нюансы

Отмечу, что как якори можно использовать не только сами документы, но и, например, кластеры документов. Главное, чтобы сами эти якори были статическими, не зависящими от конкретного пользователя.

Общая схема: было и стало 

Схема старых I2I-рекомендаций
Схема старых I2I-рекомендаций

Подобные I2I-рекомендации уже использовались в Дзене, и я внедрял их в VK Видео и VK Клипы, но они имели несколько существенных недостатков. Например, эти рекомендации строились для небольшого множества наиболее популярных якорей, долго доставлялись до рантайма, неэффективно использовали ресурсы, да и в принципе сильно отличался от основных рекомендательных технологий. Для исправления этих недостатков и было принято решение сделать новую систему построения I2I-рекомендаций.

Схема новых I2I-рекомендаций
Схема новых I2I-рекомендаций

Я приступил к реализации следующей схемы: планировщик запрашивает I2I-рекомендации для нужных якорей из рекомендера, и пишет результаты в хранилище данных, используемое затем в рантайме. В свою очередь, I2I-рекомендер подобен рантаймовому: он представляет из себя граф в Сервисхосте, который состоит из обогащения запроса данными, шардированного базового рекомендера для подбора кандидатов и их ранжирования, и метарекомендера для замешивания документов в готовые рекомендации.

Документы, похожие и интересные

Реализацию я начал с базового рекомендера. Как и было описано выше, для подбора кандидатов можно использовать поиск похожих по разным эмбеддингам. Но поскольку этот процесс я выношу в оффлайн, можно сделать подбор кандидатов эффективнее. Пожалуй, эта часть была самой простой, так как была возможность переиспользовать логику из старого I2I-контура. В целом, отбор кандидатов в I2I можно свести к двум ключевым частям: фильтрации по релевантности и ранжированию по аттрактивности. 

Релевантность – это метрика схожести самих документов, без учёта взаимодействий с ними. Модель для оценки релевантности обучается на специальной разметке, а порог для неё подбирается такой, чтобы релевантность итоговой выдачи была достаточной (обычно - в районе 80%-90%, но может отличаться для разных задач). Собственно, фильтрация по релевантности выполняет роль регуляризации, не позволяющей ранжированию вытащить произвольные документы с хорошей статистикой для неподходящих якорей.

Нюансы

С моделью релевантности связана пара курьёзов. Когда она обучалась на ручной разметке, очень большой вес получил эмбеддинг по картинке на превью, так что картинка для привлечения внимания и правда привлекает внимание! Правда, тех людей, которые совершенно не хотят смотреть содержимое…

Другой курьёз больше проявился для разметки с помощью LLM. Так получилось, что в модели, обученной на этом датасете, непропорционально большой вес получил эмбеддинг по заголовку. Это привело к тому, что модель посчитала похожими мультфильм “Русалочка” и видео по приготовлению салата “Русалочка”…

Для ранжирования применять релевантность можно, и порой даже оправдано (для свежих документов, например), но в большинстве случаев эффективность этого подхода достаточно сомнительна. Например, наиболее релевантной к какому-то видео будет его перезаливка, но маловероятно, что пользователю захочется эту перезаливку смотреть. Поэтому для ранжирования используется модель аттрактивности, обученная на взаимодействиях пользователя с рекомендованным по I2I документами. 

Нюансы

У этого подхода есть свои проблемы. Например, он требует включения хоть каких‑то I2I‑рекомендаций для того, чтобы датасет собрать. В целом, более оптимально было бы собирать датасет по документам, совместно встречающимся в историях взаимодействий пользователей, но с этим ещё предстоит экспериментировать.

Зависимость аттрактивности от релевантности для некоторого набора I2I-рекомендаций
Зависимость аттрактивности от релевантности для некоторого набора I2I-рекомендаций

На графике выше, изображающем зависимость аттрактивности от релевантности, можно наблюдать точку максимума аттрактивности при релевантности примерно в 0.91. Это в какой-то мере обобщает пример с перезаливкой видео: слишком релевантные документы становятся менее аттрактивными для пользователей. 

Быстро работает, удобно настраивается

Отмечу, что важной составляющей новой схемы была возможность гибко конфигурировать I2I-рекомендации. Общий подход у меня получился такой: в снапшотах и запросах для I2I-рекомендера содержатся различные данные (эмбеддинги, счётчики, и прочее) по отдельным ключам, и в конфигурации рекомендера указано, какие и где данные использовать. В целом, такого хватило, чтобы можно было легко добавлять использование типовых признаков (счётчиков, произведений эмбеддингов, и прочих), фильтров или методов отбора кандидатов (вроде поиска по эмбеддингам). Для чего-то более специфичного (например, признаков по условным счётчикам) всё равно приходилось дописывать код, но для большей части применений такого инструментария вполне хватало.

Таким образом, многие параметры I2I-рекомендаций я мог менять достаточно просто, что оказалось весьма полезным при проведении эксперимента с переходом на новую систему построения I2I-рекомендаций. Так, например, в какой-то момент оказалось, что в снапшоты попали данные для неправильного множества кандидатов. Для исправления этой проблемы было достаточно немного поменять конфигурацию сбора снапшотов, что без подобной гибкости в настройке потребовало бы релиза базового рекомендера, и скорее всего не его одного. Да и впоследствии, при обучении новых моделей для ранжирования, добавлять новые признаки и вычищать старые оказалось достаточно просто. 

Похожесть бывает разная, или как смешать несмешиваемое

Теперь, когда документы пофильтрованы и отранжированы, осталось только замешать их в метарекомендере. Однако, вместо замешивания I2I-рекомендаций по множеству разных критериев, удобнее строить отдельные рекомендации по разным критериям, и по каждому критерию замешивать их тривиально, по наибольшей аттрактивности. Таким образом, выбор наиболее полезных I2I-рекомендаций будет происходить уже в рантайме, с учётом пользовательской информации. 

Например, для VK Видео я сделал отдельные I2I-рекомендации по контентным и коллаборативным данным. Кроме того, для свежих документов нужно набирать коллаборативные данные, для чего я добавлял I2I-рекомендации по отдельному критерию. Так и достигается необходимая гибкость - для разных случаев можно использовать разные конфигурации I2I-рекомендера. 

Якори, полезные и не очень

И вот, реализован I2I-рекомендер, осталось только запросить из него рекомендации к подходящим якорям. Но VK Видео есть более миллиарда разных видео. Полный цикл на них займет почти неделю с целевой скоростью обработки в 2000 якорей в секунду, что как-то не очень вяжется с требованием к реактивности обновления рекомендаций. Чтобы достичь нужной реактивности, я добавил якорям приоритеты, с целью строить I2I-рекомендации для этих якорей с пропорциональной их приоритету частотой.

Какие же якори нужно обрабатывать в первую очередь? Я вижу два основных повода перестроить I2I-рекомендации для якоря: изменились данные по якорю, и изменились данные по кандидатам. Из этой мысли родилась простая формула для приоритетов:

p_i = \alpha \frac{T}{\max (T, t^c - t_i) } + \sum_j \beta_j C_{ij}

Тут T – характерный период свежести документа, t^c – текущее время, t_i – время публикации документа-якоря, C_{ij} – различные счётчики по якорю (в частности, показы за последний день для учёта изменения данных якоря и популярность за большой срок для учёта изменений кандидатов), а \alpha и \beta_j – это настраиваемые коэффициенты. Таким образом, оба фактора учтены: рекомендации для популярных якорей обновляются часто, и для свежих тоже достраиваются реактивно.  

Нюансы

Отмечу, что в зависимости от задачи оптимальные для расчёта приоритетов параметры могут отличаться. В некоторых случаях несущественна свежесть, а иногда важна популярность в данный момент.

Собираем патроны, обстреливаем рекомендер

Теперь, когда якорям в соответствие поставлены приоритеты, осталось научиться запрашивать для них построение I2I-рекомендаций с необходимым соотношением частот. Для этого нужно выбирать набор лучших по финальному весу якорей:

s_i = p_i \min(T_c, t^c – t^r_i)

Тут p_i – уже приоритет якоря, T_c – ограничение на учитываемое время с последнего пересчета, t^c – текущее время, и t^r_i – время последней обработки якоря. Таким образом, построение I2I-рекомендаций производится по своеобразной спирали: раз за разом она будет проходить по наиболее приоритетным якорям, но каждый раз заходить дальше и дальше…

Чтобы не вычислять эти веса для сотен миллионов якорей каждый раз, я разбил всё множество документов на большое число виртуальных шардов, и реализовал распределение этих шардов по отдельным серверам планировщика. Таким образом, для каждого шарда якори выбираются весьма быстро, и достаточно близко к оптимуму. Дальше всё просто – для якорей запрашиваются I2I-рекомендации, полученные результаты записываются в хранилище данных, а время последнего обновления сохраняется для последующих расчётов.

Нюансы

С масштабированием связан интересный эффект: из-за наличия в формуле T_c с ростом пропускной способности будет расти и множество обрабатываемых якорей. Из-за этой особенности нет смысла хранить I2I-рекомендации в хранилище дольше T_c, поскольку к тому времени они либо будут обновлены, либо уже выпадут из множества активных якорей.

Каждому – по списку

И вот, получилась готовая система построения I2I-рекомендаций, достаточно реактивная, легко масштабируемая, и весьма гибкая, чего и хотелось достичь. Осталось самое главное – построенные рекомендации отдавать в рантайме. Чтобы доставать миллионы раз в секунду списки документов для сотен миллионов разных якорей, надо хранить в памяти каждого сервера I2I-рекомендации для наиболее нужных якорей, и недостающее запрашивать из внешних хранилищ. Для этого я тоже приспособил приоритеты – по ним определяются наилучшие якори для хранения в памяти. Практика показала, что пары десятков миллионов якорей хватает, чтобы покрыть 97% позитивных взаимодействий пользователей в VK Видео, оставшиеся же 3% можно получить и по сети.

Нюансы

Отмечу, что тут я использовал не те же самые приоритеты, что и для планировщика. Свежие якори в памяти не особо нужны, так что приоритеты для построения снапшотов я сконфигурировал для учёта только популярности.

Внезапные проблемы на финишной прямой

Казалось бы, на этом проект и заканчивается, осталось лишь эксперимент провести. Тем не менее, внезапно образовалась проблема с обогащением якорей данными. Для этого используется фактор-прокси, отдельный конфигурируемый сервис для унификации запроса данных из разных источников, и его пропускная способность составила с десяток запросов (по одному якорю!) в секунду, что было сильно меньше целевого показателя в 2000 якорей в секунду. Поэтому мне пришлось всё же отойти от изначальной концепции с запросом данных по каждому якорю, и реализовать запрос данных для набора якорей из планировщика, нарезку данные на отдельные запросы, и последующую отправку их в рекомендер по одному. Для достижения целевой пропускной способности этого оказалось достаточно. Правда, поякорный запрос данных я всё же оставил, и он пригодился несколько раз во время проблем с хранилищами данных. 

Нюансы

Кроме того, запрошенные планировщиком данные играют роль буфера. В случае проблем с хранилищами данных этого буфера хватит на несколько дополнительных минут работы рекомендера.

Не только свежесть, но и экономия

Суммарно, на реализацию и внедрение в VK Видео всего вышеописанного ушло у меня около полугода, с учётом проведения экспериментов и полного перехода на новую систему. Существенного изменения продуктовых метрик не было, но такая и была цель. Зато получилось сэкономить приличные вычислительные ресурсы. Новая схема потребляет примерно 2000 ядер базового рекомендера (и пару сотен на остальные сервисы) для обеспечения целевой пропускной способности I2I-рекомендера в 2000 якорей в секунду для VK Видео, чего оказалось достаточно для обеспечения свежести I2I-рекомендаций порядка часа. Для сравнения, старая схема для VK Видео потребляла примерно 6000 ядер, чтобы обеспечивать примерно трехчасовую свежесть I2I-рекомендаций.

Помимо этого, новая систем позволила заметно упростить добавление новых типов I2I-рекомендаций, а также внедрение I2I в другие продукты. В частности, мне потребовалось порядка недели, чтобы внедрить новый I2I-контур в VK Клипы, так что цели по гибкости получившегося решения можно в целом считать достигнутыми. Впоследствии, новая система внедрялась и в другие продукты (например, в VK Музыку) моими коллегами, но это уже совсем другая история. 

Комментарии (2)


  1. iBolitt
    29.05.2026 18:24

    Это все хорошо. Но есть несколько вопросов, как пользователя вк видео.

    Как вы отделяете оригинальное видео от перезаливов? Часто вижу в рекомендациях не оригинальные видео, а копии. Иногда даже при реальном существовании на платформе оригинальных роликов. Почему вы их не пессимизируете при ранжировании и в рекомендациях?

    Зачем в рекомендациях предлагать ролики, которые уже просмотрены? Да еще и достаточно старые и неактуальные. Почти все рекомендации из старых роликов. Да еще и с каналов, на которые я подписан.

    В рекомендациях хочется видеть больше новых роликов, а не годовалой давности.

    Например, если есть что-то новое по той теме, что я смотрю, то хотелось бы в рекомендациях это увидеть.

    Почему не предлагаете контент новых авторов, который похож на то, что я в последнее время смотрел. Новых авторов можно сказать нету в реках. Только по кругу крутятся ролики каналов, на которые подписан.


  1. iBolitt
    29.05.2026 18:24

    Еще интересует вопрос. А что насчет рекомендаций стримов? В вк видео и вк видео лайв вообще не вижу, чтобы работал механизм рекомендаций трансляций/игровых стримов. В вк видео лайв думаю это будет проще сделать, так как там есть категории. У каждого стрима есть указание название игр. Можно хотя бы рекомендовать по жанру игры. Чтобы зрителям, которые смотрят например гоночные стримы, предлагались другие стримы по играм из жанра гонки. Хотя бы на таком базовом уровне.

    Сейчас без рекомендаций в стримах практически не реально что-то новое найти. Только ручной поиск того, чего заранее знаешь, что ищешь. Если я например не знаю название игры, но люблю смотреть гонки, то я не смогу найти другие, новые стримы по гонкам от других стримеров. А стримеры наоборот не найдут новых зрителей.