RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Конфигурация Обзор

Дополнение · X-алгоритм · страница 11 из 11

Чему этот код учит о теории

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

Коротко

  • Почти вся теория встретилась в живом коде — двухбашенный retrieval, semantic IDs, хеш-эмбеддинги, мультизадачность, DPP, фильтр Блума, PageRank, сглаженные отношения.
  • Главное расхождение — про эмбеддинги. Курс учит представлять пользователя обучаемым вектором; здесь его нет вообще.
  • Второе — про списочность. Курс говорит, что видеть весь слейт полезно; здесь это намеренно запрещено ради кэшируемости.
  • Третье — про LogQ. Теория даёт масштаб коррекции 1, в проде стоит 2.
  • Чего в учебниках нет вовсе: контур безопасности, отчёт пользователю, синхронизация конфигурации с открытым кодом.

1. Тема за темой

ТемаКак в XГде разбираем
Многостадийная воронкаИсточники → гидратация → фильтры → скоринг → отбор 50 → фильтры → 35 на экранx01, x06
MIPS и меры похожестиСкалярное произведение нормированных векторов, то есть косинусx03
Матричная факторизацияНе используется для ленты; её роль занял двухбашенный трансформер. Родственная идея живёт в SimClusters — как разреженная бинарная факторизация графаx02
Виды кандидатогенераторовПять источников с разными слепыми зонами: память подписок, retrieval, кластеры, темы, кешx02
Recall@K кандидатогенерацииЯвно не измеряется в открытом коде; вместо этого лимиты источников как продуктовое решениеx02
Метрики ранжированияВ коде обучения есть NDCG, AUC, калибровка, относительная кросс-энтропияx04
Проблемы implicit-данныхПозитив засчитывается только при отсутствии негатива. Кликбейт отсекается меткой «не задержался»x03, x04
MMR и DPPDPP по эмбеддингам в отдельном сервисе, θ = 0.65, переставляются первые 150 позицийx05
Смещения и feedback loopПерекоррекция LogQ, дисконт за пределы подписок, буст малоизвестного автораx03, x05
Двухбашенные моделиРовно так, включая нормировку и косинус. Корпус 28.7 млн, индекс внутри чекпоинтаx03
Сэмплирование негативовIn-batch плюс 64 глобальных на пример, у каждого вида своя поправкаx03
LogQ-коррекцияЕсть, с масштабом 2.0 вместо теоретической единицыx03
Обучаемые эмбеддингиОтсутствуют и у пользователя, и у поста. Только история и semantic IDx03
Индуктивный bias, холодный стартМодель индуктивна по построению; плюс в 10% примеров историю обнуляют намеренноx03
Хеширование категорийДве хеш-функции на сущность, общая таблица со смещениями зонx03
Unified EmbeddingЭто она и есть: неразличимость требует коллизии в обеих таблицахx03
МультизадачностьГоловы на каждое действие, сведение весами вне моделиx05
ESMM и сдвиг выборкиМаска лосса: конверсионные головы учатся только на кликнутых постахx04
Позиционный дебиасингВ открытом коде явной поправки не видно; зато есть дисконт за пределы подписок и затухание по авторуx05
Два контура, лямбда-архитектураОнлайн: память подписок из Kafka. Офлайн: кластеры и репутация раз в неделюx02, x07
Логирование и его разрывыПобочные эффекты в фоне без проверки результата — отсюда четырёхкратная защита от повторовx01, x08
Архитектура рантаймаRust на дорожке запроса, Python для модели, Scala для батчейx00
Порядок фильтрации18 дешёвых до скоринга, 3 дорогих после отбораx06
Фильтр БлумаПриезжает с запросом как гидратор, вместе с ещё тремя механизмамиx01, x06
ОтказоустойчивостьОшибки источников проглатываются, лента собирается из выжившихx01
Трансформеры над историейПоследовательность профиль + 1022 события + 64 кандидата, 8 слоёв, ширина 2560x04
Бандиты и Thompson samplingЕсть в буcте новичка с априором Beta(0.75, 49.25), но по умолчанию выключенx05
Эксперименты191 параметр, выключатель у каждой стадии, история изменения по днямx09
Слои с разными целямиРеклама и модули — отдельная труба, правила вместо общего скораx08
Semantic IDsОстаточное квантование 6 уровней по 256 кодов поверх мультимодального эмбеддингаx03

2. Пять мест, где практика расходится с теорией

Совпадения полезны, но ничему не учат. Разберём расхождения — в них видно, чем практика отличается от изложения.

Расхождение 1. У пользователя нет эмбеддинга

Курс говорит: представляем пользователя обучаемым вектором, айтем — обучаемым вектором, скор — их произведение.

В проде: ни того, ни другого. Пользователь — это выход трансформера над его историей, пост — набор кодов из остаточного квантования.

Почему разошлось

Курс идёт от матричной факторизации, где обучаемые векторы — сама суть метода. Прод идёт от свойств домена: контент живёт часы, пользователи приходят каждый день новые, каталог не помещается в таблицу.

Оба правы в своих рамках. Обучаемый эмбеддинг лучше, когда объекты долгоживущие и их немного — каталог фильмов, товарные категории. Он проигрывает, когда объекты эфемерны: пока таблица обновится, пост уже неактуален.

Что унести: вопрос «обучаемый вектор или вычисляемое представление» решается не теорией, а временем жизни объекта относительно периода переобучения. Если объект живёт меньше — обучаемый вектор бесполезен.

Расхождение 2. Списочность запрещена намеренно

Курс говорит: listwise-подходы видят весь слейт и потому сильнее pointwise.

В проде: кандидаты физически не видят друг друга — маска внимания это запрещает.

Почему разошлось

Курс сравнивает по качеству при прочих равных. Прод учитывает то, чего в сравнении нет: кэшируемость, воспроизводимость и объяснимость.

Обратите внимание, что списочные эффекты не потеряны — они вынесены в отдельный шаг переранжирования. То есть выбран не «pointwise вместо listwise», а разделение: модель отвечает за пару «пользователь и пост», отдельный сервис — за то, как посты выглядят вместе.

Что унести: у архитектурного решения есть измерения помимо качества. Скор, зависящий от состава батча, нельзя закэшировать — а кеш здесь стал полноценным источником кандидатов.

Расхождение 3. LogQ с масштабом 2

Курс говорит: вычитаем \(\log Q\) и получаем несмещённую оценку.

В проде: вычитается \(2\log Q\).

Почему разошлось

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

Что унести: «несмещённая оценка» всегда несмещена относительно чего-то. В рекомендациях это «относительно того, что мы показывали вчера», и если вчерашняя политика была смещена, формальная корректность не спасает.

Расхождение 4. Разнообразие наводится дважды

Курс говорит: для разнообразия берут MMR или DPP.

В проде: и то, и другое, но по разным осям — затухание по повторному автору отдельно, DPP по эмбеддингам отдельно.

Почему разошлось

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

Что унести: прежде чем выбирать метод разнообразия, надо назвать ось. «Разнообразная выдача» — не постановка задачи.

Расхождение 5. Буст новичка — не множитель

Курс говорит: холодный старт лечат приоритетом — подмешиванием, бустом, exploration.

В проде: берётся ровно один подходящий пост и его скор приравнивается к скору позиции 15–16.

Почему разошлось

Множитель даёт непредсказуемый результат: где окажется пост, зависит от того, кто ещё в выдаче. Приравнивание к скору целевой позиции даёт гарантию места, а не гарантию прибавки. Продуктовое обещание формулируется как «одна позиция в середине ленты», и код выражает ровно его.

Плюс «ровно один» — это ограничение сверху на цену механизма: сколько бы новичков ни было, лента теряет одну позицию, а не десять.

Что унести: когда требование формулируется в позициях, реализуйте его в позициях, а не в множителях к скору.

3. Чего в учебниках нет вовсе

ЧтоПочему это важно
Контур безопасности — классификаторы, репутация аккаунтов, движок правил, три вида ответаПо объёму кода сопоставим с рекомендательным ядром. В любой публичной системе это половина работы, и на собеседовании про неё спрашивают
Отчёт пользователю о лейблахПрозрачность как часть продукта, а не как документация. Технически держится на сохранении причины вердикта
Синхронизация конфигурации с открытым кодомОткрыть код и не открыть значения — почти бессмысленно. Отдельный механизм решает эту задачу
Измерение ценности инвентаря холдаутом по паре «пользователь и объект»Причинное измерение с единицей рандомизации, отличной от пользователя
Ограничение рекламы качеством выдачиЭкономический контур, встроенный в код: плохой контент уменьшает рекламный инвентарь

Вопросы с собеседований по системе целиком

Вопросы, на которые эта система даёт готовый и конкретный ответ. Ценность в том, что вместо «наверное, делают как-то так» можно сказать «в открытом коде X это сделано вот так, и вот почему».

Спроектируйте ленту социальной сети. С чего начнёте?

С двух разделений, которые определяют всё остальное.

Первое: ранжирование отдельно от видимости. Порядок решает модель, право на показ — отдельный сервис с правилами. Разная цена ошибки, разная скорость изменений, разная проверяемость. И главное: понижение скора не гарантирует, что пост не покажут.

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

Дальше стандартная воронка: несколько источников с разными слепыми зонами → гидратация → дешёвые фильтры → модель → отбор с запасом → дорогие фильтры → блендинг. И побочные эффекты после ответа.

Как представить пользователя и айтем, если контент живёт часы?

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

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

Модель предсказывает десяток разных действий. Как свести их в один скор?

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

Три вещи, о которых надо сказать отдельно. Веса умножаются на вероятности, а не на счётчики, поэтому из отношения весов нельзя делать вывод о влиянии. Результат стоит приводить к неотрицательной области, если дальше он умножается на поправки меньше единицы. И веса нельзя вывести теоретически — только экспериментом, что и является главной ценой схемы.

Как не дать одному автору занять всю ленту?

Мягким множителем с полом, а не квотой: \(m(k) = (1-\text{floor})\cdot\text{decay}^k + \text{floor}\), где \(k\) — сколько постов того же автора уже стоит выше. При значениях 0.5 и 0.25 получается 1.0 → 0.625 → 0.438 → 0.344 с полом 0.25.

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

И отдельно стоит сказать, что это только одна ось разнообразия. Однообразие по темам решается другим механизмом — отбором через детерминантный процесс после скоринга.

Как поддержать новых авторов, не сломав ленту?

Дать ровно одному их посту гарантированную позицию в середине выдачи, а не множитель ко всем. Реализация: из подходящих постов (мало показов, мало подписчиков у автора, свежий) берётся лучший по скору, и его скор приравнивается к скору позиции 15–16.

Два свойства делают это управляемым. Гарантия позиции, а не прибавки: результат предсказуем и не зависит от того, кто ещё в выдаче. Ровно один пост: цена механизма ограничена сверху одной позицией независимо от числа новичков.

По смыслу это exploration: тратим позицию, чтобы получить сигнал о посте, о котором данных нет. В коде рядом лежит и байесовский вариант выбора через сэмплирование Томпсона с априором Beta(0.75, 49.25), выключенный по умолчанию.

Пользователь жалуется, что видит один и тот же пост дважды. Где искать?

Начать надо с того, что запись показов почти наверняка происходит после отправки ответа, в фоне, и её результат не проверяется. Значит, показ мог не записаться: перезапуск сервиса, переполнение очереди, отказ хранилища.

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

Диагностика соответственно: сравнить, что записалось в каждый из журналов, и искать расхождение, а не искать баг в фильтре.

Как измерить, сколько ценности приносит определённый тип контента?

Только экспериментом, наблюдением нельзя: убрав тип контента, вы освободите позиции, которые займут другие посты, и часть вовлечения перетечёт туда. Наблюдаемая доля завышает вклад.

Механизм: детерминированно скрывать заданный процент таких постов и сравнивать. Две важные детали — решение должно зависеть от хеша пары «объект и пользователь», чтобы быть стабильным между запросами, и должно быть персональным, чтобы не искажать общую статистику объекта.

Единица рандомизации здесь пара «пользователь и объект», а не пользователь. Это позволяет мерить ценность инвентаря, а не функциональности.

Метрики выросли, а пользователи жалуются. Что делать?

Признать, что метрика измеряет не то, ради чего продукт существует, и искать, какая функция продукта деградировала.

Конкретный случай из этого репозитория: усиление веса ответа для взаимных подписок дало хорошие метрики вовлечения — люди активнее общались со знакомыми. А жалобы были на другое: во время крупного спортивного события лента показывала мало обсуждения, потому что релевантные посты писали неподписанные аккаунты, и усиление одной группы автоматически ослабило остальные. Значение снизили с 20 до 15.

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

Что бы вы сделали иначе, чем сделано в X?

Вопрос на зрелость суждения, и отвечать «всё правильно» плохо. Три содержательных направления.

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

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

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

Шпаргалка одним экраном

Совпало с теорией

Двухбашенность, semantic IDs, хеш-эмбеддинги, мультизадачность, DPP, Блум, PageRank.

Разошлось: эмбеддинги

Обучаемых векторов нет ни у пользователя, ни у поста. Причина — время жизни объекта.

Разошлось: списочность

Запрещена намеренно ради кэшируемости; вынесена в отдельный шаг переранжирования.

Разошлось: LogQ

Масштаб 2 вместо 1 — против смещения, унаследованного от логирующей политики.

Разошлось: разнообразие

Две оси, два механизма: по авторам множителем, по темам определителем.

Разошлось: холодный старт

Гарантия позиции для одного поста вместо множителя для всех.

Нет в теории

Контур безопасности, прозрачность как продукт, синхронизация конфигурации.

Главный урок

Архитектурные решения выбираются не только по качеству: кэшируемость и объяснимость весят не меньше.

Второй урок

Усиление любой группы есть ослабление остальных, и видно это там, где метрик нет.

Первоисточники