Re-rank: ограничения и продуктовые правила (B6)

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

⚠ Слейты посчитаны офлайн ранкером Фазы 8 на реальном логе Brightkite и приезжают готовым JSON. А вот сам re-rank считается на лету: ползунки пересчитывают настоящие метрики по всем eval-пользователям, а не симуляцию — продуктовый слой на то и дешёвый, что не требует переобучения.

загрузка слейтов…

Теория простым языком

Модель упорядочивает кандидатов по ranking score. Витрину показывает продукт — и у продукта есть обязательства, которых нет в функции потерь: доступность, квоты, разнообразие, обязательный слот под новое. Re-rank — это последняя ступень воронки, где обучение уже закончилось и начинаются правила.

▸Зачем править выдачу после ранкера, а не учить модель сразу правильно
БылоРанкер из Фазы 8 сортирует кандидатов по ranking score — относительной оценке предпочтительности. Это не откалиброванная вероятность чек-ина: лог positive-only, негативы сэмплированы, калибровки нет — и всё.
ПроблемаОн не знает, что место закрыто, что десять позиций из одного района — плохая выдача, что показывать одному продавцу всю полку нельзя, и что каталог не изучится, если никогда не показывать новое. Ни одно из этих требований не выражается через ranking score — он упорядочивает айтемы, а не описывает состав витрины.
ИдеяОтделить предсказание от политики показа. Ранкер даёт порядок, а тонкий продуктовый слой поверх него применяет жёсткие фильтры и мягкие квоты — жадно, по убыванию скора, с явным добором, если правила не дают набрать K.
Стало лучшеПравила меняются за минуты и без переобучения: наш замер — квота 2 на голову каталога стоит 0.26 п.п. Recall@10, срезает долю популярного примерно вдвое (0.353 → 0.181) и ПОДНИМАЕТ покрытие каталога с 0.571 до 0.598; один слот под новое место стоит 0.13 п.п. Модель при этом не тронута.
Осталось слабымСлой жадный и близорукий: он оптимизирует одну витрину, а не сессию и не долгосрочную ценность. Квоты подобраны человеком, а не выучены. Эффект правила бывает отрицательным, нулевым и положительным — смотря какая метрика и какой срез, — поэтому его надо мерить, а не постулировать.
ДальшеУчить ограничения внутрь модели (multi-task, MMoE/PLE — Фаза 11), решать распределение показов как задачу оптимизации (slate optimization, определяющая diversity через submodular-функции), а exploration отдать бандитам (Фаза 12).

Два типа правил, которые нельзя путать

Жёсткий фильтр (eligibility) — то, что нарушать нельзя никогда: товара нет на складе, доставки в этот регион нет, возрастное ограничение, юридический запрет, заблокированный автор. Про место такого фильтра принято говорить «до retrieval», и для глобальных, достаточно свежих ограничений это верно: их выгодно push-down внутрь поиска кандидатов. Персонализированные или быстро меняющиеся ограничения нередко применяют после retrieval — с overfetch и явным fallback. Неизменно другое: финальная проверка перед выдачей нужна всегда, и требование не «блок стоит в определённом месте», а «недопустимый айтем не будет показан, а витрина не схлопнется незаметно».

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

Мягкая квота (cap) — цель, а не закон: «не больше 3 товаров одного продавца», «не больше 2 повторов», «не больше 5 из одной категории». Квоты конфликтуют друг с другом и с размером витрины, поэтому у них обязан быть определён порядок применения и поведение при недоборе.

Backfill — самая частая необъявленная деталь — Если квоты не дают набрать K позиций, показать полупустую полку почти всегда хуже, чем нарушить квоту. Поэтому реализации добирают выдачу, игнорируя caps, — и почти всегда молча. Отсюда берётся классическое «правило вроде работает, но не всегда»: оно работает ровно до того момента, когда пул перестаёт его выдерживать. В нашем замере квота «не больше одного места из гео-ячейки» вызывает добор у 10.3% пользователей — то есть у каждого десятого правило де-факто отключено. Наблюдаемость его не устраняет, а делает видимым: доля backfill — такая же продуктовая метрика, как coverage, и место ей на дашборде (B7).
Разнообразие ≠ новизна ≠ покрытие — Три разные величины, которые постоянно склеивают в слово «diversity». Новизна — доля показанного, чего этот пользователь не видел. Внутрислейтовое разнообразие — насколько непохожи позиции внутри одной витрины (жанры, районы, продавцы). Покрытие каталога — сколько разных айтемов система показывает вообще, по всем пользователям. Наш замер показывает, что они могут двигаться в разные стороны: квота «не больше трёх повторов» поднимает новизну с 0.587 до 0.771 и при этом снижает покрытие каталога (0.571 → 0.519). Причина: вытесняются личные хвостовые места (у каждого свои), а на их место приходят одни и те же новые кандидаты для всех. Отсюда правило: guardrail-метрики измеряют по отдельности, а «повысить разнообразие» — не постановка задачи.
MMR и почему его здесь нет — Maximal Marginal Relevance — классический способ диверсификации: каждый следующий айтем выбирается по λ·релевантность − (1−λ)·похожесть на уже выбранных. Он у нас уже построен и живёт в Фазе 7 на MovieLens, где есть жанры и, значит, осмысленная мера похожести. Здесь другой инструмент осознанно: MMR оптимизирует непохожесть внутри витрины по некоторой метрике, а квоты выражают продуктовые обязательства в счётных единицах («не больше трёх от одного продавца»). В проде обычно работают оба: MMR формирует разнообразие, квоты гарантируют границы. Квоты при этом проще объяснить бизнесу и проверить в тесте — у них нет λ, который надо подбирать.

Порядок применения — это тоже дизайн

Слои не коммутируют, и порядок надо выбирать сознательно:
1. eligibility — настолько рано, насколько позволяют свежесть данных и архитектура, плюс обязательная финальная проверка перед выдачей;
2. ранжирование — модель, без правил;
3. квоты — жадно по убыванию скора;
4. слоты под новое место — после квот и после eligibility, с их соблюдением: показывать новое из недоступного или запрещённого нельзя;
5. backfill — последний, с явным учётом.

Важно не перепутать этот список с тем, что делает экспонат. Здесь retrieval и ранжирование уже посчитаны офлайн, поэтому eligibility применяется к фиксированному пулу top-50, а дальше идут квоты, слот и backfill. Значит, экспонат измеряет поведение позднего фильтра внутри готового пула и не сравнивает позиции фильтра в воронке. Сам порядок шагов виден в коде app/models/rerank.py: одна функция используется и офлайн-билдером для кривых, и живым эндпоинтом — чтобы картинка и замер не разошлись.

Как это связано с остальным учебником

Это третья и последняя ступень перед сборкой полного capstone: retrieval (E3) → ranking (Фаза 8) → фичи под этим всем (B4) → re-rank. Отсюда прямые мосты: guardrail-метрики и «качество ≠ бизнес-метрика» — Фаза 14; exploration-слоты как fallback-механика — B5; наблюдаемость квот и backfill — B7; петля «показали → залогировали → научились» — B1.

⚠️ Что может пойти не так

  • Считать поздний жёсткий фильтр безопасным, потому что «на наших данных всё влезло»: он безопасен ровно пока доступных кандидатов в пуле хватает на K. Замер показывает, где эта граница — и что за ней витрина укорачивается молча.
  • Считать, что backfill «лечится наблюдаемостью»: счётчик обнаруживает проблему, но не решает её. Решения — увеличить пул, добавить источники кандидатов, задать порядок ослабления квот, разделить обязательные и желательные caps, считать добор по каждой квоте отдельно или заменить жадный выбор на constrained optimization.
  • Считать «повысить разнообразие» задачей: новизна, внутрислейтовое разнообразие и покрытие каталога — разные метрики, и квота может двигать их в противоположные стороны.
  • Подбирать квоты по офлайн-метрике: ограничение может её ухудшить, не изменить или даже улучшить, если правило коррелирует с релевантностью — у нас квоты поднимают recall на новых местах. Само по себе это ничего не доказывает: офлайн меряет размен качества и guardrails, бизнес-эффект проверяется онлайн.
  • Называть exploration детерминированный слот: без рандомизации и логирования propensity данные из него непригодны для IPS/DR (Фаза 14). И ставить такой слот надо после eligibility — «исследовать» можно ровно то, что вообще разрешено показывать.
  • Ставить cap, не посмотрев на распределение группы: квота «одно место из района» конфликтует с тем, что люди живут в одном районе, и просто конвертируется в backfill.
  • Забывать, что правило не пробивает потолок retrieval: если кандидата нет в пуле, никакая перестановка его не покажет.

🧠 Проверь себя: Продакт просит: «в топ-10 должно быть не больше двух товаров одного продавца». Как это внедрять?

Где это в дорожной карте

B6 — Re-ranking Constraints & Product Rules, ступень 3 из 3 к полному capstone (Фаза 8 → B4 → B6 → сборка). Главный урок: витрина — это не отсортированный список, а политика показа, и цена каждого её правила измерима.

Что дальше

Всё, на чём стоит финальная сборка, уже пройдено — можно собирать воронку целиком.

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

Источники