Фичи и ранжирование
~20 мин

Learning to Rank

Pointwise, pairwise, listwise подходы. LambdaMART, CatBoost ranking.

Learning to Rank — когда важен не ответ, а порядок

Загрузка интерактивного виджета...

Классификация отвечает на вопрос «да или нет?» — кликнет пользователь или нет. Ранжирование отвечает на вопрос сложнее: «в каком порядке?». Два товара могут иметь одинаковый CTR 0.12, но один из них нужно показать выше другого — и эта разница стоит денег.

Аналогия: представь, что ты организуешь концерт и должен составить сет-лист из 10 песен. Знать, что каждая песня «хорошая» (классификация) — недостаточно. Тебе нужно выбрать порядок: чем открыть, чем закрыть, куда поставить хит. Learning to Rank (LTR) — это семейство методов, которые учатся именно этому: предсказывать оптимальный порядок.

LTR используется повсеместно: поисковые движки (Google, Bing, Яндекс), рекомендательные системы, e-commerce (ранжирование товаров по запросу), feed-ранжирование в соцсетях. Если где-то есть список и пользователь видит только верхнюю часть — там есть LTR.

Большая картина: от признаков к ранжированному списку

LTR-модель принимает на вход запрос (query) и список кандидатов (documents) и возвращает отсортированный список, где наиболее релевантные элементы стоят выше. Вот весь pipeline за 4 шага:

Шаг 1. Сбор кандидатов. На предыдущих этапах (retrieval, candidate generation) отобрано 100-500 документов для данного запроса. Шаг 2. Извлечение признаков. Для каждой пары (query, document) считаем фичи: текстовые (BM25, TF-IDF), поведенческие (CTR по позициям), эмбеддинговые (косинус user↔item), кросс-фичи (query×document). Шаг 3. Скоринг. LTR-модель присваивает каждому кандидату скор — число, отражающее релевантность. Чем выше скор, тем выше документ в выдаче. Шаг 4. Сортировка. Кандидаты сортируются по скору. Верхние позиции — то, что увидит пользователь.

Три подхода к LTR: pointwise, pairwise, listwise
Три подхода различаются тем, как формулируется функция потерь: по одному документу, по паре или по всему списку

Ключевой вопрос LTR: как обучить модель скорингу, чтобы итоговая сортировка была оптимальной? Три подхода — pointwise, pairwise, listwise — отличаются именно этим: как формулируется функция потерь и какую «единицу» видит модель при обучении.

Pointwise: «оцени каждый документ отдельно»

Самый простой подход: задача ранжирования сводится к обычной регрессии или классификации. Для каждого документа модель предсказывает скор (relevance grade, CTR, оценку от 0 до 4), и мы сортируем по этому скору.

Например: у тебя есть датасет, где для каждого запроса каждому документу присвоена оценка релевантности от 0 (не релевантен) до 4 (идеальный ответ). Обучаем XGBoost с MSE-лоссом предсказывать эту оценку — всё, у нас есть LTR-модель. Сортируем документы по предсказанным оценкам.

Плюсы: простота. Берёшь любую регрессию/классификацию и получаешь ранжирование. Легко интерпретировать, легко дебажить. Минусы: модель не знает, что мы хотим порядок. MSE-лосс штрафует за ошибку в абсолютном скоре, а не за неправильный порядок. Если модель предсказала [3.1, 3.0] вместо [3.0, 3.1] — порядок перевёрнут, но MSE считает это мелкой ошибкой. Pointwise не учитывает контекст запроса: обучение не видит, что два документа конкурируют за одну позицию.

Pairwise: «кто из двух лучше?»

Вместо предсказания абсолютного скора pairwise-подход учится сравнивать: для каждой пары документов (d_i, d_j) одного запроса модель учится отвечать «d_i релевантнее d_j» или наоборот. Это ближе к реальной задаче ранжирования: нам важен не точный скор, а правильный порядок.

Аналогия: вместо того чтобы ставить абсолютные оценки каждому ресторану (4.2, 4.7, 3.8), ты просто говоришь: «Ресторан A лучше B, B лучше C». Из таких попарных сравнений выстраивается ранжирование.

RankNet: нейросетевые корни

RankNet (Burges et al., 2005) — один из первых pairwise-методов. Модель предсказывает скоры s_i и s_j для двух документов, а лосс-функция — кросс-энтропия от вероятности правильного порядка:

P_ij — предсказанная вероятность того, что документ i релевантнее j. σ — параметр масштабирования

Лосс RankNet — бинарная кросс-энтропия: если d_i действительно лучше d_j, штрафуем модель за низкую P_ij. Модель учится выставлять скоры так, чтобы попарный порядок был правильным.

LambdaRank: градиенты, которые знают про NDCG

Проблема RankNet: все пары равноценны. Перепутать 1-ю и 2-ю позиции так же «плохо», как перепутать 99-ю и 100-ю. Но пользователь видит только верх списка — ошибки наверху стоят дороже.

LambdaRank решает это элегантно: к градиенту RankNet домножается |ΔNDCG| — изменение NDCG при перестановке пары. Если перестановка d_i и d_j сильно меняет NDCG (например, она перемещает документ с позиции 1 на позицию 5) — градиент большой. Если перестановка на хвосте списка — градиент маленький.

Lambda-градиент: произведение градиента RankNet и изменения NDCG при перестановке пары (i, j)

Это ключевая идея LTR: NDCG недифференцируема (это функция от порядка, а порядок дискретен), но через lambda-градиенты мы создаём «суррогатный» градиент, который направляет модель к оптимизации NDCG. На практике это работает отлично — LambdaRank стабильно обходит RankNet по NDCG.

Listwise: «оптимизируй весь список целиком»

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

Два основных направления: • ListNet — превращает скоры в распределение вероятностей (через softmax) и минимизирует KL-дивергенцию между предсказанным и истинным распределением. • Прямая оптимизация метрики (ApproxNDCG, SoftRank) — строит дифференцируемую аппроксимацию NDCG и оптимизирует её gradient descent-ом.

На практике чисто listwise-подходы не доминируют над pairwise с lambda-градиентами. Граница размыта: LambdaRank формально pairwise, но |ΔNDCG| учитывает весь список. Именно такие «гибридные» методы стали стандартом.

LambdaMART — стандарт индустрии

LambdaMART = Lambda-градиенты (из LambdaRank) + MART (Multiple Additive Regression Trees, т.е. градиентный бустинг на деревьях решений). Это, пожалуй, самый важный алгоритм LTR — он десятилетиями доминирует в поисковых системах и рекомендациях.

Как это работает: 1. Начинаем с нулевых скоров для всех документов. 2. Для каждой пары (d_i, d_j) с разной релевантностью считаем lambda-градиент: насколько нужно увеличить разницу скоров, с учётом влияния на NDCG. 3. Lambda-градиенты суммируются по всем парам для каждого документа — получаем «псевдо-остатки». 4. Строим дерево решений, предсказывающее эти остатки. 5. Добавляем дерево к ансамблю (с learning rate). 6. Повторяем 500-3000 итераций.

Почему LambdaMART — стандарт?Скорость: GBDT (XGBoost, LightGBM, CatBoost) — одни из самых быстрых моделей. Обучение — минуты, инференс — микросекунды. • Фичи: деревья нативно работают с категориальными фичами, пропусками, нелинейностями. Не нужно нормализовать, не нужен feature engineering. • Качество: lambda-градиенты позволяют оптимизировать NDCG, несмотря на его недифференцируемость. На практике — один из лучших результатов на бенчмарках LTR. • Интерпретируемость: feature importance из деревьев показывает, какие фичи важны для ранжирования. • Надёжность: десятки лет в продакшене крупнейших систем.

from catboost import CatBoostRanker

# YetiRank — реализация LambdaMART в CatBoost
model = CatBoostRanker(
    iterations=1000,
    depth=6,
    learning_rate=0.05,
    loss_function='YetiRank',     # lambda-градиенты + NDCG
    # loss_function='PairLogitPairwise',  # классический pairwise
)

# group_id — ID запроса: документы одного запроса сравниваются между собой
model.fit(X_train, y_train, group_id=query_ids_train,
          eval_set=(X_val, y_val), eval_group_id=query_ids_val)

# Инференс: скоры для ранжирования
scores = model.predict(X_test)
# Сортируем документы по убыванию скора → ранжированный список

XGBoost, LightGBM и CatBoost для LTR

XGBoost: objective="rank:ndcg" или "rank:pairwise". Классика, но медленнее на больших данных. • LightGBM: objective="lambdarank", metric="ndcg". Быстрее XGBoost благодаря histogram-based splits. • CatBoost: loss_function="YetiRank" — собственная модификация LambdaMART. Нативная работа с категориальными фичами — удобно для LTR, где много категорий (город, категория товара). Все три реализуют вариации LambdaMART.

Признаки для ранжирования: три группы

LTR-модель ранжирования — самая «тяжёлая» стадия пайплайна, но работает с маленьким набором кандидатов (100-500). Поэтому может позволить себе дорогие признаки. Все фичи делятся на три группы:

Query features — зависят только от запроса: • Длина запроса (в токенах) • Частотность запроса (популярный или редкий) • Тип интента (навигационный, информационный, транзакционный) • Эмбеддинг запроса

Document features — зависят только от документа: • Качество документа (PageRank, число просмотров, рейтинг товара) • Свежесть (дата публикации) • Длина документа • CTR документа по всем запросам • Эмбеддинг документа из Two-Tower модели

Cross features — зависят от пары (query, document). Это самая ценная группа: • BM25 / TF-IDF score (текстовое совпадение) • Косинусное сходство эмбеддингов query и document • CTR пользователя в категории этого документа • Исторический CTR документа по похожим запросам • Скор из предыдущего этапа (retrieval score) • Время с последнего взаимодействия пользователя с этой категорией

Практическое правило

Cross-фичи дают основной прирост качества. Если у тебя 50 фичей, и 40 из них — document features, а cross-фичей всего 5, — скорее всего, основной рост придёт от добавления cross-фичей, а не от улучшения document features. BM25 + косинус эмбеддингов + CTR в категории — сильный baseline.

Position bias — когда данные врут

Пользователь кликает на первый результат намного чаще, чем на пятый — даже если пятый объективно лучше. Это position bias: CTR зависит не только от релевантности, но и от позиции показа. Если обучать LTR на «кликнул / не кликнул» наивно, модель выучит: «то, что было наверху, — хорошее». Она будет закреплять текущий порядок, а не находить оптимальный.

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

Как бороться с position bias

1. Inverse Propensity Weighting (IPW). Оцениваем вероятность клика в зависимости от позиции: p(click | position). Клик на позиции 5 «весит» больше, чем клик на позиции 1, потому что пользователь должен был пролистать — значит, документ действительно релевантен. Формально: каждый пример в лоссе домножается на 1/p(examination | position).

2. Рандомизация позиций. На небольшой части трафика (1-5%) показываем документы в случайном порядке. Это даёт «чистые» данные без position bias. Дорого (ухудшает UX на рандомизированном трафике), но даёт ground truth.

3. Position as feature. Добавляем позицию показа как признак в модель при обучении. На инференсе подставляем фиксированное значение (например, позицию 1 для всех) или убираем фичу. Модель учится «вычитать» эффект позиции.

4. Unbiased LTR. Семейство методов (regression EM, dual learning), которые моделируют position bias как латентную переменную и убирают его при обучении. Самый теоретически обоснованный подход, но сложнее в реализации.

Neural LTR: когда деревьев мало

LambdaMART на ручных фичах — мощный baseline, но у него есть предел: качество ограничено фичами, которые ты придумал. Neural LTR сдвигает парадигму: вместо ручных фичей модель учится из сырых данных (текст запроса + текст документа).

Cross-encoder — основной подход для Neural LTR. Запрос и документ конкатенируются и подаются в BERT (или другой трансформер) целиком: [CLS] query [SEP] document [SEP]. На выходе [CLS]-токен → линейный слой → скор релевантности.

Почему cross-encoder, а не bi-encoder (Two-Tower)? Bi-encoder считает эмбеддинги запроса и документа отдельно — быстро, но не может уловить тонкие взаимодействия. Cross-encoder видит запрос и документ одновременно: self-attention позволяет каждому слову запроса «смотреть» на каждое слово документа. Это значительно точнее, но и дороже: нужно прогнать BERT для каждой пары (query, document).

Типичный pipeline в продакшене: 1. Retrieval (bi-encoder / BM25): быстро отбираем 1000 кандидатов 2. LTR Stage 1 (LambdaMART на фичах): ранжируем 1000 → top-100 3. LTR Stage 2 (cross-encoder / Neural): переранжируем top-100 → top-10 Cross-encoder слишком дорог для 1000 документов, но идеально подходит для финального reranking на маленьком множестве.

from sentence_transformers import CrossEncoder

# Cross-encoder для reranking
reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

query = "как работает LambdaMART"
documents = [
    "LambdaMART — алгоритм ранжирования на основе GBDT...",
    "Градиентный бустинг — семейство ансамблевых методов...",
    "BERT — языковая модель на основе трансформера...",
]

# Скоры для каждой пары (query, document)
pairs = [[query, doc] for doc in documents]
scores = reranker.predict(pairs)
# scores: [0.98, 0.23, 0.05] → первый документ наиболее релевантен

# Ранжируем
ranked = sorted(zip(documents, scores), key=lambda x: -x[1])

Pointwise vs Pairwise vs Listwise — когда что

Pointwise — когда у тебя есть абсолютные оценки релевантности (0-4) или ты решаешь задачу CTR-предсказания. Простой baseline, часто «достаточно хорошо» для MVP. Pairwise (LambdaMART) — стандарт индустрии. Работает на данных с частичной разметкой (кликнул / не кликнул, без абсолютных оценок). Lambda-градиенты с |ΔNDCG| дают лучшую оптимизацию метрик ранжирования. Listwise — теоретически элегантнее, но на практике преимущество над LambdaMART невелико. Используется в исследованиях и в нейросетевых подходах (ApproxNDCG как лосс для нейросети). Neural (cross-encoder) — максимальное качество для текстовых задач, но дорогой: подходит для финального reranking на top-100.

🎯 На собеседовании

Junior

Pointwise vs pairwise vs listwise — в чём разница? Pointwise предсказывает скор каждого документа отдельно (регрессия/классификация). Pairwise сравнивает пары: «этот документ лучше того». Listwise оптимизирует метрику (NDCG) по всему списку. • Зачем LTR, если можно сортировать по CTR? CTR — pointwise подход, не учитывает контекст запроса и position bias. LTR учитывает пары и весь список. • Что такое position bias? Пользователь чаще кликает на верхние позиции просто потому что видит их первыми, а не потому что они лучшие.

Middle

Что такое LambdaMART и почему он стандарт? GBDT + lambda-градиенты. Lambda = градиент RankNet × |ΔNDCG|. Позволяет деревьям оптимизировать NDCG, которая сама недифференцируема. Быстрый, интерпретируемый, работает с категориальными фичами. • Как бороться с position bias? IPW (взвешивание по позиции), рандомизация на части трафика, position as feature, unbiased LTR. • Cross-encoder vs bi-encoder для ранжирования? Cross-encoder: [CLS] query [SEP] doc [SEP] → BERT → скор. Точнее, но дороже (прогон для каждой пары). Bi-encoder: отдельные эмбеддинги → косинус. Быстрее, но хуже улавливает взаимодействия. • Какие фичи важны для LTR? Три группы: query (длина, интент), document (PageRank, CTR), cross (BM25, косинус эмбеддингов, CTR в категории). Cross-фичи дают основной прирост.

Senior

Почему NDCG недифференцируема и как LambdaRank обходит это? NDCG зависит от порядка (argmax), а argmax недифференцируем. LambdaRank определяет «суррогатные» градиенты: для каждой пары (i, j) gradient = sigmoid × |ΔNDCG|, где ΔNDCG — изменение NDCG при перестановке. Можно показать, что это converges к оптимуму NDCG. • Как построить multi-stage ranking pipeline? Retrieval (ANN/BM25, 10K→1K) → LTR Stage 1 (LambdaMART на фичах, 1K→100) → Reranking (cross-encoder, 100→10). Каждый этап точнее, но дороже. Важно: модель каждого этапа оптимизируется под свою метрику (recall@K для retrieval, NDCG для LTR). • Unbiased LTR — как работает? Модель P(click) = P(examine | position) × P(click | relevance). Через EM-алгоритм оцениваем propensities и relevance раздельно. Или: используем рандомизированные данные для оценки propensity, потом IPW для обучения на всех данных.

Собираем всё вместе

Learning to Rank — это задача, в которой важен порядок, а не абсолютный скор. Три подхода (pointwise → pairwise → listwise) постепенно усложняют лосс-функцию, чтобы лучше оптимизировать метрики ранжирования. LambdaMART (GBDT + lambda-градиенты с |ΔNDCG|) — рабочая лошадка индустрии: быстрый, точный, интерпретируемый.

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

В продакшене LTR-модель — часть многоступенчатого пайплайна: retrieval отбирает кандидатов, LTR ранжирует, а neural reranker (cross-encoder) переранжирует top. Position bias — главный подводный камень: без его учёта модель закрепляет статус-кво вместо нахождения оптимального порядка.