← к модулям

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

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

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

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

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

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

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

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

Жёсткий фильтр (eligibility) — то, что нарушать нельзя никогда: товара нет на складе, доставки в этот регион нет, возрастное ограничение, юридический запрет, заблокированный автор. Правильное место такого фильтра — до retrieval (или сразу после): если фильтровать в самом конце, из top-10 могут выпасть 4 позиции, и витрина придёт короче K. Наш замер это подтверждает: отфильтровать 8% каталога стоит 6.7 п.п. Recall@10 — почти вдвое больше доли отфильтрованного, потому что выбывают высоко отранжированныеместа, а не случайные.

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

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

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

Слои не коммутируют, и порядок надо выбирать сознательно:
1. eligibility — до retrieval, чтобы недоступное не занимало место в пуле;
2. ранжирование — модель, без правил;
3. квоты — жадно по убыванию скора;
4. слоты под исследование — после квот, но с их соблюдением: «исследовать» недоступное или запрещённое нельзя;
5. backfill — последний, с явным учётом.
Наш экспонат следует ровно этому порядку, и его видно в коде app/models/rerank.py: одна функция используется и офлайн-билдером для кривых, и живым эндпоинтом — чтобы картинка и замер не разошлись.

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

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

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

  • Ставить жёсткий фильтр в конец воронки: пул схлопывается, витрина приходит короче K — фильтровать надо до retrieval.
  • Не считать backfill: при конфликтующих квотах правило де-факто отключается у части пользователей, и об этом никто не узнаёт.
  • Считать «повысить разнообразие» задачей: новизна, внутрислейтовое разнообразие и покрытие каталога — разные метрики, и квота может двигать их в противоположные стороны.
  • Подбирать квоты по офлайн-метрике: она по определению падает от любого ограничения. Квота обосновывается продуктом и проверяется онлайн-экспериментом, а офлайн считает только цену.
  • Ставить exploration до eligibility — «исследовать» можно ровно то, что вообще разрешено показывать.
  • Ставить cap, не посмотрев на распределение группы: квота «одно место из района» конфликтует с тем, что люди живут в одном районе, и просто конвертируется в backfill.
  • Забывать, что правило не пробивает потолок retrieval: если кандидата нет в пуле, никакая перестановка его не покажет.

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

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

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