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

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

Метрики: чем меряют кандидатогенерацию и ранжирование

У двух стадий воронки разные задачи, поэтому и метрики разные: первую меряют полнотой, вторую — порядком. Дальше начинаются тонкости, на которых чаще всего и ловят: Recall нельзя сравнивать при фиксированном K, Precision не видит перестановок, AUC ничего не знает о калибровке, а усреднение по запросам скрывает деградацию новичков. Все примеры посчитаны и воспроизводимы.

Что унести из главы
  • Recall@K сравнивают при равном бюджете, а не при равном K. Иначе сравниваются не модели, а инженерные решения о размере выдачи.
  • Precision@k слеп к порядку. Три раскладки одного слейта дают одинаковые 0.300, а NDCG — 1.0000, 0.5508 и 0.4250.
  • Одна и та же перестановка соседей стоит по-разному. Наверху ΔNDCG = 0.3691, на девятом месте 0.0120 — разница в 31 раз. Отсюда вырастут ранжирующие лоссы.
  • AUC инвариантен к любому монотонному преобразованию скора и потому ничего не говорит о калибровке. Там, где скор умножается на деньги, нужны LogLoss и ECE.

1. Две стадии — две метрики

Задача кандидатогенерации — не потерять. Задача ранжирования — расставить. Из этого следует всё остальное.

КандидатогенерацияРанжирование
вопросесть ли нужное среди отобранногов правильном ли оно порядке
метрикаRecall@K, HitRate@KNDCG, MRR, MAP
Kсотни–тысячиединицы–десятки
цена ошибкиневосстановима: чего нет, того не покажутвосстановима переранжированием

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

2. Метрики кандидатогенерации

Recall@K и HitRate@K

$$ \mathrm{Recall@}K = \frac{\#\{\text{релевантных в топ-}K\}}{\#\{\text{всех релевантных}\}}, \qquad \mathrm{HitRate@}K = \frac{\#\{\text{запросов, где есть хотя бы один релевантный в топ-}K\}}{\#\ \text{запросов}} $$

Обе отвечают на один вопрос — «дошло ли нужное до второй стадии» — с разной детализацией: у HitRate под знаком суммы стоит 0/1 вместо самой полноты. HitRate удобен, когда позитив по природе продукта один.

Проблема 1: относительно чего считать знаменатель

«Всех релевантных» — это каких? Два варианта, и оба с ценой.

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

Инженерный компромисс, который обычно и выбирают: базово мерить по датасету, а перед выкаткой — совместно с ранжированием.

Проблема 2: K — это уже инженерное решение

Фиксируя \(K\), мы заранее решаем, сколько кандидатов отдаёт первая стадия. А кривые \(\mathrm{Recall}(K)\) у разных источников устроены по-разному: один быстро набирает полноту на первых десятках и выходит на плато, другой растёт медленно, но выше.

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

Поэтому сравнивают не \(\mathrm{Recall}(K)\), а полноту относительно бюджета латентности: строят парето-фронт «полнота против времени» и выбирают точку под свой бюджет. Источник с чуть меньшей полнотой, но втрое дешевле, часто выигрывает — на сэкономленное время можно добавить ещё один.

Что здесь надо увидеть
  1. Поставьте бюджет 3 мс: выигрывает топ популярного — не потому что он лучше, а потому что остальные при таком бюджете успевают отдать слишком мало кандидатов.
  2. Тяните бюджет к 20 мс: победитель меняется дважды. Отсюда правило — Recall@K нельзя сравнивать при фиксированном K.
  3. Смотрите на форму кривых: у одних полнота набирается быстро и упирается в плато, у других растёт дольше, но выше. Это и подсказывает, сколько айтемов брать с каждого источника.

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

Виджет с парето-фронтом — на странице тренажёра.

Coverage: метрика, про которую забывают

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

Почему её надо смотреть рядом с полнотой

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

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

3. Метрики ранжирования

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

Одна выдача, три раскладки
РаскладкаСлейтP@10R@10RRAPNDCG
хиты наверху11100000000.3001.0001.0001.0001.0000
хиты в середине00011100000.3001.0000.2500.3830.5508
хиты внизу00000001110.3001.0000.1250.2160.4250

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

Precision и Recall одинаковы во всех трёх строках. Они не видят порядка вообще — а именно порядок и есть продукт. Отсюда и нужда во всём остальном списке.

MRR — когда позитив ровно один

$$ \mathrm{RR} = \frac{1}{\mathrm{rank}}, \qquad \mathrm{MRR@}K = \frac{1}{|Q|}\sum_{q} \mathrm{RR}_q $$

где \(\mathrm{rank}\) — позиция первого релевантного; если его нет в топ-\(K\), обычно \(\mathrm{RR} = 0\).

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

MAP — и почему её вытеснила NDCG

$$ \mathrm{AP} = \frac{1}{|R|}\sum_{k=1}^{K} \mathrm{Precision@}k \cdot rel_k, \qquad \mathrm{MAP} = \frac{1}{|Q|}\sum_{q}\mathrm{AP}_q $$

Позиция здесь учитывается косвенно: в нормальной системе \(\mathrm{Precision@}k\) убывает с ростом \(k\), поэтому позитив наверху вносит больше.

Две оговорки, о которых спрашивают
  • Нормировка неоднозначна. Здесь знаменатель \(|R|\) — все релевантные; встречается и \(\min(|R|, K)\), и тогда метрика достижима до единицы даже при \(|R| > K\). Это стоит уточнять, а не предполагать.
  • Метрика бинарная по природе. AP не умеет градуированную релевантность: «отлично», «сойдёт» и «плохо» для неё либо 1, либо 0. NDCG умеет, и это главная причина, по которой в индустрии победила она.

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

$$ \mathrm{DCG@}K = \sum_{k=1}^{K} \frac{g(rel_k)}{\log_2(k+1)}, \qquad \mathrm{NDCG@}K = \frac{\mathrm{DCG@}K}{\mathrm{IDCG@}K} $$

Три части, и каждая делает свою работу.

Gain: чем измеряется ценность попадания

Метрика не просто допускает своё определение \(g(rel)\) — она его требует. Для бинарной релевантности \(g(rel) = rel\). Для градуированной популярен экспоненциальный вариант \(g(rel) = 2^{rel} - 1\), который сильнее разводит «отлично» и «сойдёт».

Насколько сильнее — считается. Две раскладки с метками 3, 1, 1 и нулями:

Gain[3,0,0,1,1][1,1,0,0,3]разрыв
линейный0.92410.67580.2484
экспоненциальный0.96150.53360.4278

Экспоненциальный gain почти вдвое сильнее наказывает за то, что самый ценный айтем уехал вниз. А для e-commerce в gain можно положить цену позитива — и тогда NDCG начинает измерять деньги, а не клики.

Дисконт: почему именно логарифм

Множитель \(1/\log_2(k+1)\) по позициям: 1-я — 1.00, 2-я — 0.63, 3-я — 0.50, 5-я — 0.39, 10-я — 0.29.

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

Число, из которого вырастут ранжирующие лоссы

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

Меняем местамиNDCG былосталоΔNDCG
позиции 1 и 21.00000.63090.3691
позиции 2 и 30.63090.50000.1309
позиции 3 и 40.50000.43070.0693
позиции 9 и 100.30100.28910.0120

Одна и та же ошибка — одна инверсия — наверху стоит в 31 раз дороже, чем на девятом месте. Запомните это: именно из-за него попарные лоссы, считающие инверсии одинаковыми, оказываются не тем, что нужно, и появляется LambdaRank.

Нормировка на IDCG: зачем она

IDCG — это DCG идеального порядка для этого же запроса. Деление на него делает метрику сравнимой между запросами с разным числом позитивов, чего не умеет ни Precision@K, ни DCG.

Важная деталь при реализации: идеальный порядок ограничен и числом позитивов, и длиной выдачи, то есть \(\min(|R|, K)\). Без этого NDCG перестаёт быть нормированной на единицу — классическая ошибка в самописных реализациях.

Что здесь надо увидеть
  1. Перетаскивайте релевантные вниз: Precision не меняется вообще, а AP и NDCG падают. Это то же самое, что в таблице выше, но руками.
  2. Включите градуированные метки и переключите gain на экспоненциальный — разрыв между хорошей и плохой раскладкой вырастает.
  3. Уменьшите \(K\): метрики перестают видеть то, что ниже отсечки, и начинают вести себя иначе. \(K\) — часть определения метрики, а не деталь реализации.

Что сказать на собесе: «Precision@k не видит порядка, MRR годится, когда позитив один, MAP бинарна по природе, NDCG умеет градуированную релевантность и явный дисконт по позиции — поэтому она и стала стандартом».

4. AUC и то, чего он не видит

$$ \mathrm{AUC} \;=\; P\bigl(f(u, i^{+}) > f(u, i^{-})\bigr) \;=\; \frac{1}{|P||N|}\sum_{i^{+} \in P}\ \sum_{i^{-} \in N} \mathbb{1}\bigl[f(u,i^{+}) > f(u,i^{-})\bigr] $$

Доля правильно упорядоченных пар «позитив–негатив». Удобен тем, что не зависит ни от калибровки скоров, ни от порога, ни (почти) от дисбаланса классов.

Чем он неудобен

Все пары равноценны. Перестановка первой и второй позиции стоит ровно столько же, сколько двухсотой и двести первой, — а пользователю видна только первая. Для ранжирования выдачи это ровно та слепота, из-за которой существует NDCG.

Отсюда практическое: AUC хорош как метрика модели (умеет ли она вообще отличать позитив от негатива) и плох как метрика продукта.

AUC ничего не знает о калибровке

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

Значит, модель с прекрасным AUC может систематически завышать вероятности вдвое, и по AUC это невидимо. Для ранжирования не страшно. Смертельно там, где скор попадает в арифметику:

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

Для этого — LogLoss, Brier и ECE плюс калибровочная кривая. Чинится калибровка после обучения (Platt scaling, изотоническая регрессия) и ранжированию не мешает: AUC при этом не меняется.

Оба виджета — AUC против порога и дисбаланса и калибровка — на странице тренажёра. Во втором хорошо видно главное: ползунки меняют ECE и LogLoss в разы, а AUC стоит на месте.

5. По чему усреднять

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

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

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

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

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

Что здесь надо увидеть
  1. Посмотрите на доли: новичков по головам половина, а запросов от них — проценты. Micro почти целиком определяется активными.
  2. Уроните качество на новичках: micro почти не двигается, macro проседает заметно.
  3. Сделайте наоборот — уроните на активных: теперь падают обе, и это тот случай, когда проблему заметят и без разреза по когортам.

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

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

Почему кандидатогенерацию меряют полнотой, а ранжирование — нет?

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

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

Почему Recall@K нельзя сравнивать при фиксированном K?

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

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

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

Чем NDCG лучше MAP и Precision@k?

Precision@k вообще не видит порядка: три раскладки одного слейта с хитами наверху, в середине и внизу дают одинаковые 0.300, тогда как NDCG — 1.0000, 0.5508 и 0.4250.

MAP порядок учитывает, но бинарна по природе: градуированную релевантность она не выражает. NDCG умеет и градуированный gain (включая экспоненциальный \(2^{rel}-1\) или прямо цену позитива), и явный дисконт по позиции, и нормировку на идеальный порядок, которая делает запросы с разным числом позитивов сравнимыми.

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

Почему одна инверсия не равна другой инверсии?

Из-за дисконта. Перестановка соседних позиций 1 и 2 меняет NDCG на 0.3691, та же перестановка на позициях 9 и 10 — на 0.0120. Разница в 31 раз.

Отсюда следует, что попарный лосс, считающий все инверсии одинаковыми (RankNet), оптимизирует не то, что мы меряем. Исправление — домножить градиент пары на то, насколько её перестановка сдвинет метрику; так устроен LambdaRank.

Что AUC не показывает и когда это критично?

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

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

Критично там, где скор попадает в арифметику: рекламный аукцион со ставкой × CTR, распределение бюджета, пороговые правила. Там смотрят LogLoss, Brier, ECE и калибровочную кривую. Калибровка чинится после обучения и AUC не портит.

Micro или macro: по чему усреднять метрику?

По запросам (micro) — измеряется средний запрос, и число почти целиком определяется активными пользователями, у которых запросов в десятки раз больше. По пользователям (macro) — измеряется средний пользователь, все голоса равны.

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

Ответ «смотрю обе и отдельно режу по когортам» — правильный: расхождение между micro и macro само по себе диагноз.

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

Кандген

Recall@K, HitRate@K. Сравнивать при равном бюджете, не при равном K. Рядом смотреть coverage.

Precision слеп

Три раскладки одного слейта — одинаковые 0.300. Порядка он не видит.

NDCG

Gain (линейный, \(2^{rel}-1\) или цена) × дисконт \(1/\log_2(k+1)\), делённое на идеал. Дисконт: 1.00, 0.63, 0.50, 0.29.

Инверсии неравны

Наверху ΔNDCG 0.3691, на девятом месте 0.0120 — в 31 раз. Отсюда LambdaRank.

AUC

Вероятность, что позитив выше негатива. Инвариантен к монотонным преобразованиям → о калибровке молчит.

Усреднение

Micro — средний запрос, macro — средний пользователь. Деградацию новичков видно только во втором.

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