RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Рантайм

Часть VI · Проектирование · глава 19 из 19

Проектирование системы

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

Что унести из главы
  • Сначала простое работающее решение, потом критика, потом полное. Сложная система, спроектированная с нуля, не работает и не чинится заплатками.
  • Целеполагание нельзя пропускать. Грубую ошибку здесь не вытащит никакая модель ниже — а пропустить его хочется всегда.
  • Размер каталога определяет архитектуру. До 75 000 кандидатов воронка не нужна вовсе, и отказ от неё — правильное решение, а не упрощение.
  • Редкое целевое событие меряют прокси не из упрямства. Поймать тот же относительный эффект на конверсии 0.5% вместо 40% стоит в 133 раза больше трафика.

1. Закон Голла

Формулировка, с которой стоит начинать ответ

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

Отсюда общая схема ответа: сначала строим простое, но работающее решение → критикуем его → строим более полное.

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

Про временные решения

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

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

2. Пять шагов

1. Целеполагание бизнес-метрика → офлайн → таргет 2. Ограничения каталог, SLA, интент, фильтры, смещения 3. Техархитектура шарды, фильтрация, доставка данных 4. ML-архитектура кандгены, модель, признаки, лосс 5. Эксперимент сплит, метрики, антагонисты критикуем → строим более полное решение порядок примерный: если целеполагание не даётся с ходу, можно начать со второго пункта — но зафиксировать его нужно в любом случае до перехода к техническим шагам
Схема разворачивается дважды: первый проход — простое решение, второй — после критики.

Шаг 1: целеполагание

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

Грубую ошибку здесь не вытащит никакая сложная модель ниже. Если вы оптимизируете не то, качество реализации не имеет значения.

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

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

Шаг 2: ограничения

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

Первое число, которое стоит посчитать вслух

Размер каталога определяет архитектуру больше, чем что-либо ещё. Пусть ранжирующая модель тратит 4 мкс на кандидата, а бюджет ответа — 300 мс:

КаталогПолный переборВлезает?Нужен отбор в
3e+0312.0 мсда
5e+052000.0 мсНЕТ7 раз
5e+082000000.0 мсНЕТ6667 раз

Порог, где перебор перестаёт влезать: 75 000 кандидатов.

Числа воспроизводятся скриптом _tools/design_demo.py в этом репозитории.

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

Шаги 3–4: техника и ML

Приём для быстрой генерации ML-идей

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

  • для каждого актора по отдельности — три ячейки;
  • для каждой пары акторов — ещё три.

Получается сетка 3 + 3, которая почти всегда покрывает всё существенное. И её же удобно проговаривать вслух как структуру ответа — интервьюер видит систему, а не перечисление.

Актор или параПримеры признаков
Пользовательчастота покупок (с ограничением по времени, чтобы признаки меньше «ехали»), средний чек, эмбеддинги любимых категорий
Айтемпродажи товара, его категории и производителя за \(X\) дней; показы, CTR, CVR и их взвешенные версии; эмбеддинг
Контекстдень недели, секунды с начала суток, эмбеддинг поверхности, эмбеддинг корзины
Пользователь × айтемскалярное произведение башен; сколько раз пользователь брал этот товар, эту категорию, этого производителя за \(X\) дней
Пользователь × контекстсколько пользователь обычно покупает в этот день и час; насколько текущая корзина для него характерна
Айтем × контексткак часто товар берут в это время; его CTR и CVR в это время; близость товара к корзине

Шаг 5: эксперимент

Проверка перед запуском

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

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

3. Кейс 1: e-grocery

Якорный пример, на фоне которого дальше проще показывать отличия.

Ограничения

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

Технические. Каталог на точке — чаще всего до 3000 айтемов, хорошо структурирован. Пользователей десятки миллионов, запросов — сотни в секунду. Бюджет ответа с учётом предзагрузок — до 300 мс. Товары становятся доступны и недоступны мгновенно, и они портятся, поэтому новинки должны появляться в выдаче быстро.

Целеполагание и его критика

Простой первый подход. Ключевая задача бизнеса — рост оборота. Офлайн проксируем его через NDCG, где gain — цена товара. Отсюда очевидный таргет — покупка, а скор собирается как

$$ \text{score} = p(\text{buy}\mid \text{show}) \cdot \text{price} $$

либо цена вшивается прямо в ранжирующий лосс. Датасет следует явно: покупка против показа в слейте.

А теперь критика — то самое второе прохождение схемы:

  • деньги в моменте могут не соответствовать деньгам в долгосроке; бизнес скорее хочет растить сумму по пользователям за длинный горизонт;
  • метрика не учитывает, что пользователи покупают несколько одинаковых товаров;
  • и она никак не учитывает каннибализацию поверхностей.

Заметьте: ни одна из трёх проблем не чинится улучшением модели. Все три — про постановку.

Почему каннибализация — не мелочь

Две поверхности, каждая изолированно даёт +10 единиц оборота:

Пересечение аудитории и ассортимента0.00.20.50.81.0
суммарный прирост+20.0+18.0+15.0+12.0+10.0
потеря против суммы0%10%25%40%50%

Числа воспроизводятся скриптом _tools/design_demo.py.

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

Это классическая ловушка отчётности: команды честно показывают приросты, а общий оборот не растёт. Лечится метриками на уровне пользователя, а не поверхности, и аплифт-постановкой — «покупка на этой поверхности относительно покупки вообще».

РешениеПочему так
Без шардированияпри таком каталоге и нагрузке в нём нет смысла
Статическое хранилище профилей, обновление раз в суткипользователи редко покупают чаще раза в сутки
Статический индекс айтемов, строится раз в суткивыход нового товара известен заранее
Кеш доступных товаров прямо на сервисе + дофильтровкафильтрация критична: наличие меняется мгновенно
Фильтр по текущей корзине и по легальным ограничениямне показываем добавленное; табак, алкоголь
Где эта схема начнёт ломаться

Полезно назвать самому, не дожидаясь вопроса.

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

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

Эксперимент: сплит пользовательский; ключевая метрика — интегральный оборот; вспомогательные — маржа и конверсия поверхности; антагонисты — оборот по категориям (не перекосили ли выдачу) и конверсия по позициям (не сломали ли верх).

4. Кейс 2: онлайн-кинотеатр

Дальше — только то, что значимо отличается от якорного кейса.

Главная проблема кейса: целевое событие слишком редкое

Бизнесу нужны подписки. Но число подписок — плохая метрика для A/B: событие очень редкое, и в коротком эксперименте оно не покрасится. Насколько именно плохая:

Базовая конверсияНужно на группуДороже, чем при 40%
0.4009.42e+031.0
0.2002.51e+042.7
0.0501.19e+0512.7
0.0106.22e+0566.0
0.0051.25e+06132.7

Числа воспроизводятся скриптом _tools/design_demo.py; относительный прирост 5%, значимость 0.05, мощность 80%.

Число наблюдений пропорционально \((1-p)/p\) при фиксированном относительном эффекте. Значит поймать тот же эффект на конверсии 0.5% вместо 40% стоит в 133 раза больше трафика.

Прокси-метрику берут не из упрямства аналитика — прямое измерение просто не помещается ни в какой разумный эксперимент.

Как строят такую прокси — рецепт целиком
  1. Выделяем список базовых метрик с правдоподобным долгосрочным эффектом: просмотр сериалов (у них лучше удержание), дискавери и разнообразие потребляемого, частота использования.
  2. Строим из них десяток комбинаций-кандидатов.
  3. Ретроспективно понимаем, какие из них лучше показывают долгосрочный эффект — сопоставлением похожих пользователей или скорректированными корреляциями. Можно и модель обучить, но нужна интерпретируемость.
  4. Проверяем на золотом наборе прошлых экспериментов — тех A/B, у которых долгосрочный исход уже известен.

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

Онбординг: приём, который стоит уметь объяснить

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

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

СхемаПро какие группы спросилиГрупп покрыто
наивно, пул не меняется0, 0, 0, 0, 01 из 5
удаляем тех, кому зашло0, 1, 2, 3, 45 из 5

Числа воспроизводятся скриптом _tools/design_demo.py.

Наивная схема пять раз подряд спрашивает про один и тот же вкус: самая большая группа никуда не девается, и каждый следующий вопрос не приносит новой информации. Удаление превращает топ в «топ для тех, кому не зашло предыдущее» — и за пять вопросов мы обходим все группы.

Формулировка, которую стоит унести: мы выбираем следующий вопрос так, чтобы он максимально делил оставшуюся неопределённость, а не так, чтобы он был самым популярным.

Карусельный интерфейс добавляет свой сюжет: выдача состоит из блоков — «продолжить просмотр», «похожие на…», жанровые подборки. Можно выбирать блоки и ранжировать внутри них, а можно сгенерировать общий пул и нарезать; на практике делают оба. Внешнее ранжирование между блоками часто бандитное из-за гетерогенности: сравнивать «продолжить просмотр» с жанровой подборкой одной моделью тяжело — у них разная природа и разный масштаб откликов.

5. Кейс 3: медиа-фид

Масштаб меняет всё.

Появляется вторая сущность: автор

Это главное отличие кейса, и оно меняет постановку. У автора свои интересы и ожидания: авторы рассчитывают на показы, их отсутствие вызывает жалобы. Система перестаёт быть двусторонней «пользователь — айтем» и становится трёхсторонней.

Типичные подходы: встроенный в модель item exploration, PID-контроллеры для стабилизации объёма показов, явные квоты.

И следствие, о котором забывают: авторам платят за показы, значит начинается фрод — крутят ботов, накручивают CTR. В хорошей системе таргет антифрод-aware, как и целевая метрика, а команды работают тесно.

Интерфейс влияет на таргет

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

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

6. Три системы рядом

E-groceryКинотеатрМедиа-фид
Каталог~3 000 на точке300–500 тыс.сотни миллионов
Кандидатогенерацияможно без неёнужна, шардов нетнужна + шардирование
Интентсильный, покупкаот одного айтемаотсутствует
Холодный стартменее актуаленкритичен для пользователейкритичен для айтемов
Бизнес-цельоборот → NDCG с gain = ценаподписки → прокси от времени просмотравнимание + счастье авторов
Ключевая больканнибализация поверхностейредкое целевое событиемасштаб, модерация, фрод

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

И одно общее ограничение: бюджет

Как разложить 300 мс по стадиям — полезно держать в голове порядок величин:

СтадиямсДоля
сеть и разбор запроса206.7%
профиль пользователя3010.0%
кандидатогенерация4013.3%
фильтрация155.0%
признаки6020.0%
ранжирование8026.7%
переранжирование и блендинг258.3%
сериализация и сеть назад206.7%
итого29096.7%

Числа воспроизводятся скриптом _tools/design_demo.py.

Запас — 10 мс, и он весь уйдёт на хвост распределения: бюджет надо держать по p99, а не по среднему. И заметьте, где деньги: признаки и ранжирование съедают 47%, поэтому ускорение ищут сначала там, а не в кандидатогенерации, где его инстинктивно ищут первым делом.

7. Чек-лист собеседования

Что спросить, прежде чем отвечать
  1. Какая бизнес-метрика? И сразу — насколько часто происходит целевое событие. Если оно редкое, разговор про прокси начинается здесь, а не в конце.
  2. Размер каталога и число пользователей? От этого зависит, нужна ли воронка вообще.
  3. Есть ли у пользователя интент? Лента и поиск — разные задачи, хотя формально одна.
  4. Бюджет ответа? 50 мс и 300 мс — это разные архитектуры.
  5. Кто известен: пользователи, айтемы, оба? Отсюда, какой холодный старт критичен.
  6. Есть ли третья сторона — авторы, продавцы, рекламодатели? Это меняет постановку целиком.
  7. Какие фильтрации обязательны? Легальные ограничения проектируются с самого начала, а не прикручиваются.
Пять ошибок, которые видно сразу
  1. Начать с архитектуры. «Возьмём двухбашенный ретривал и DCN сверху» — до того, как выяснен размер каталога. Показывает шаблон вместо мышления.
  2. Пропустить целеполагание. Самая дорогая ошибка: её не вытащит ничто ниже по стеку.
  3. Не назвать, где решение сломается. Хороший ответ сам содержит критику; ждать её от интервьюера — упущенная возможность.
  4. Забыть про эксперимент. Система, которую нельзя измерить, не спроектирована.
  5. Не назвать метрики-антагонисты. Признак того, что человек не запускал ничего в прод: там всегда что-то ломается в соседнем месте.

Вопросы с собеседований

Спроектируйте рекомендательную систему для X. С чего начнёте?

С пяти шагов в таком порядке: целеполагание (бизнес-метрика → офлайн-метрика → таргет модели), ограничения (каталог, пользователи, SLA, фильтрации, интент, смещения), техническая архитектура, ML-архитектура, сетап эксперимента. Потом критика получившегося и второй проход.

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

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

Почему целеполагание нельзя пропускать?

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

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

Как понять, нужна ли кандидатогенерация?

Посчитать. Если ранжирующая модель тратит порядка 4 мкс на кандидата, а бюджет 300 мс, то полный перебор влезает примерно до 75 тысяч кандидатов. Каталог в 3000 айтемов перебирается за 12 мс — воронка не нужна вовсе. Каталог в 500 тысяч требует отбора в 7 раз, сотни миллионов — в тысячи раз.

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

Бизнесу нужны подписки. Почему нельзя мерить подписки?

Потому что событие слишком редкое. Число наблюдений для фиксированного относительного эффекта пропорционально (1−p)/p, поэтому поймать тот же прирост на конверсии 0.5% вместо 40% стоит в 133 раза больше трафика: 1.25 млн на группу против 9.4 тысяч.

Нужна прокси-метрика — чувствительная в коротком A/B и коррелирующая с долгосрочной целью. Строят её так: выбрать базовые метрики с правдоподобным долгосрочным эффектом (просмотр сериалов, дискавери, частота использования), собрать из них комбинации-кандидаты, ретроспективно отобрать лучшие и обязательно проверить на золотом наборе прошлых экспериментов с известным исходом. Последний шаг и есть настоящая валидация.

Как быстро сгенерировать идеи для признаков?

Сеткой 3 + 3. Выписать акторов — обычно пользователь, айтем, контекст — и придумать признаки для каждого по отдельности и для каждой пары.

Например: пользователь — частота покупок и средний чек; айтем — продажи и CTR за X дней; контекст — день недели и эмбеддинг поверхности; пользователь × айтем — скалярное произведение башен и сколько раз он брал эту категорию; пользователь × контекст — сколько он обычно покупает в этот час; айтем × контекст — CTR товара в это время.

Сетка почти всегда покрывает существенное, и её удобно проговаривать вслух как структуру ответа.

Что такое каннибализация поверхностей и как с ней быть?

Ситуация, когда две поверхности показывают одному пользователю пересекающийся ассортимент, и их приросты не складываются. Если каждая изолированно даёт +10, то при половинном пересечении вместе они дадут +15, а не +20; при полном — +10, то есть вторая не добавляет ничего.

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

Как сделать онбординг холодного пользователя?

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

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

Общая формулировка: мы выбираем следующий вопрос так, чтобы он максимально делил оставшуюся неопределённость, а не так, чтобы он был самым популярным.

Что меняется, когда в системе появляются авторы?

Система перестаёт быть двусторонней и становится трёхсторонней. У авторов свои ожидания: они рассчитывают на показы, и их отсутствие вызывает жалобы. Инструменты — item exploration внутри модели, PID-контроллеры для стабилизации объёма показов, явные квоты.

И следствие, о котором забывают: авторам платят за показы, значит появляется фрод — боты и накрутка CTR. Таргет и целевая метрика должны быть антифрод-aware, иначе система будет добросовестно оптимизировать накрутку.

Как разложить бюджет ответа по стадиям?

Порядок величин при бюджете 300 мс: сеть и разбор 20, профиль 30, кандидатогенерация 40, фильтрация 15, признаки 60, ранжирование 80, переранжирование 25, ответ 20 — итого 290, запас 10 мс.

Два вывода. Первый: запас уйдёт на хвост распределения, поэтому бюджет держат по p99, а не по среднему. Второй: признаки и ранжирование съедают 47% бюджета, и ускорение искать надо сначала там, а не в кандидатогенерации, куда смотрят инстинктивно.

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

Закон Голла

Простое работающее → критика → полное. Простое решение — инструмент понимания.

Пять шагов

Цель → ограничения → техника → ML → эксперимент. И второй проход.

Первое число

Порог перебора ≈ 75 000 кандидатов. Ниже — воронка не нужна и не полезна.

Редкое событие

n ∝ (1−p)/p. 0.5% против 40% — в 133 раза больше трафика. Отсюда прокси.

Сетка 3+3

Пользователь, айтем, контекст — по отдельности и попарно. Покрывает существенное.

Каннибализация

При полном пересечении вторая поверхность даёт ноль. Мерить по пользователю.

Онбординг

Удалять тех, кому зашло: 5 групп из 5 вместо 1 из 5.

Бюджет

Признаки и ранжирование — 47%. Держать по p99, а не по среднему.

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

Это была последняя глава

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

Если из всего этого стоит унести одну мысль, то такую: в рекомендациях почти каждое техническое решение выводится из ограничения, а не из моды. Воронка — из бюджета задержки. Факторизация — из разреженности. Двухбашенность — из числа кандидатов. Стохастичность выдачи — из потребности мерить. Хороший ответ на собеседовании отличается от плохого не набором названий, а тем, называет ли человек ограничение прежде решения.

Дальше — повторение по вопросам, виджеты и разбор открытого кода реальной ленты, где всё это видно в работающем виде.