Часть 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@K | NDCG, 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 удобен, когда позитив по природе продукта один.
«Всех релевантных» — это каких? Два варианта, и оба с ценой.
- Относительно датасета. Знаменатель — позитивы из отложенной выборки. Дёшево, воспроизводимо, но позитивы в ней собраны прошлой политикой: то, что система никогда не показывала, релевантным не считается по построению. Полнота оказывается завышенной.
- Совместно со стадией ранжирования. Меряем, сколько из того, что ранкер счёл бы хорошим, дошло до него. Честнее относительно продакшена, но дороже и зависит от текущей версии ранкера — метрика перестаёт быть стабильной во времени.
Инженерный компромисс, который обычно и выбирают: базово мерить по датасету, а перед выкаткой — совместно с ранжированием.
Фиксируя \(K\), мы заранее решаем, сколько кандидатов отдаёт первая стадия. А кривые \(\mathrm{Recall}(K)\) у разных источников устроены по-разному: один быстро набирает полноту на первых десятках и выходит на плато, другой растёт медленно, но выше.
И главное: кандидатогенераторы стоят разного. Статический топ из памяти — почти ничего, поход в ANN-индекс — миллисекунды, нейросетевой источник — заметно больше.
Поэтому сравнивают не \(\mathrm{Recall}(K)\), а полноту относительно бюджета латентности: строят парето-фронт «полнота против времени» и выбирают точку под свой бюджет. Источник с чуть меньшей полнотой, но втрое дешевле, часто выигрывает — на сэкономленное время можно добавить ещё один.
- Поставьте бюджет 3 мс: выигрывает топ популярного — не потому что он лучше, а потому что остальные при таком бюджете успевают отдать слишком мало кандидатов.
- Тяните бюджет к 20 мс: победитель меняется дважды. Отсюда правило — Recall@K нельзя сравнивать при фиксированном K.
- Смотрите на форму кривых: у одних полнота набирается быстро и упирается в плато, у других растёт дольше, но выше. Это и подсказывает, сколько айтемов брать с каждого источника.
Что сказать на собесе: «Полноту кандидатогенератора сравнивают не при равном K, а при равном бюджете латентности — иначе сравниваются разные инженерные решения, а не модели».
Виджет с парето-фронтом — на странице тренажёра.
Coverage: метрика, про которую забывают
Полнота отвечает на вопрос «нашли ли нужное». Она ничего не говорит о том, сколько каталога система вообще способна показать. Для этого — покрытие: доля каталога, попавшая хоть в чью-то выдачу за период.
Источник, который всегда отдаёт топ популярного, показывает приличную полноту при копеечной стоимости — и нулевое покрытие. Формально он не плох: популярное действительно чаще релевантно.
Но именно так и замыкается контур из предыдущей главы: в симуляции покрытие вставало на 0.6% каталога, и метрики полноты этого не показывали вообще. Покрытие — самая дешёвая ранняя диагностика вырождения, и её почти никто не смотрит.
3. Метрики ранжирования
Дальше — та же выдача, посчитанная разными метриками. Слейт из десяти позиций, релевантных всего три, меняется только их расположение.
| Раскладка | Слейт | P@10 | R@10 | RR | AP | NDCG |
|---|---|---|---|---|---|---|
| хиты наверху | 1110000000 | 0.300 | 1.000 | 1.000 | 1.000 | 1.0000 |
| хиты в середине | 0001110000 | 0.300 | 1.000 | 0.250 | 0.383 | 0.5508 |
| хиты внизу | 0000000111 | 0.300 | 1.000 | 0.125 | 0.216 | 0.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} $$Три части, и каждая делает свою работу.
Метрика не просто допускает своё определение \(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.9241 | 0.6758 | 0.2484 |
| экспоненциальный | 0.9615 | 0.5336 | 0.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 и 2 | 1.0000 | 0.6309 | 0.3691 |
| позиции 2 и 3 | 0.6309 | 0.5000 | 0.1309 |
| позиции 3 и 4 | 0.5000 | 0.4307 | 0.0693 |
| позиции 9 и 10 | 0.3010 | 0.2891 | 0.0120 |
Одна и та же ошибка — одна инверсия — наверху стоит в 31 раз дороже, чем на девятом месте. Запомните это: именно из-за него попарные лоссы, считающие инверсии одинаковыми, оказываются не тем, что нужно, и появляется LambdaRank.
IDCG — это DCG идеального порядка для этого же запроса. Деление на него делает метрику сравнимой между запросами с разным числом позитивов, чего не умеет ни Precision@K, ни DCG.
Важная деталь при реализации: идеальный порядок ограничен и числом позитивов, и длиной выдачи, то есть \(\min(|R|, K)\). Без этого NDCG перестаёт быть нормированной на единицу — классическая ошибка в самописных реализациях.
- Перетаскивайте релевантные вниз: Precision не меняется вообще, а AP и NDCG падают. Это то же самое, что в таблице выше, но руками.
- Включите градуированные метки и переключите gain на экспоненциальный — разрыв между хорошей и плохой раскладкой вырастает.
- Уменьшите \(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 это невидимо. Для ранжирования не страшно. Смертельно там, где скор попадает в арифметику:
- рекламный аукцион, где ставка умножается на предсказанный CTR;
- распределение бюджета между поверхностями;
- любое правило вида «показываем, если вероятность выше порога».
Для этого — LogLoss, Brier и ECE плюс калибровочная кривая. Чинится калибровка после обучения (Platt scaling, изотоническая регрессия) и ранжированию не мешает: AUC при этом не меняется.
Оба виджета — AUC против порога и дисбаланса и калибровка — на странице тренажёра. Во втором хорошо видно главное: ползунки меняют ECE и LogLoss в разы, а AUC стоит на месте.
5. По чему усреднять
Любая из этих метрик — среднее по чему-то, и по чему именно, обычно не проговаривают. Зря: это меняет число и, что важнее, меняет то, какие регрессии вы способны заметить.
| Micro: по запросам | Macro: по пользователям | |
|---|---|---|
| как считается | среднее по всем слейтам сразу | сначала среднее внутри пользователя, потом по пользователям |
| отвечает на вопрос | как дела у среднего запроса | как дела у среднего пользователя |
| кто определяет число | активные: у них запросов в десятки раз больше | все поровну, включая тех, кто сделал один запрос |
Активность распределена так же тяжело, как популярность. Новичков по головам может быть половина аудитории, а запросов от них — считанные проценты.
Следствие: релиз, который убивает онбординг, по micro выглядит нейтральным. Уроните метрику на новичках до нуля — micro просядет на сотые доли, macro на десятые.
Обратное тоже верно: macro даёт полный голос тому, кто сделал один запрос и ушёл, и потому шумнее. Поэтому смотрят обе величины, а расхождение между ними уже само по себе диагноз.
- Посмотрите на доли: новичков по головам половина, а запросов от них — проценты. Micro почти целиком определяется активными.
- Уроните качество на новичках: micro почти не двигается, macro проседает заметно.
- Сделайте наоборот — уроните на активных: теперь падают обе, и это тот случай, когда проблему заметят и без разреза по когортам.
Что сказать на собесе: «Усреднение по запросам меряет средний запрос, по пользователям — среднего пользователя. При тяжёлом хвосте активности это разные числа, и деградацию новичков видно только во втором».
Вопросы с собеседований
Почему кандидатогенерацию меряют полнотой, а ранжирование — нет?
Потому что задачи разные. Первая стадия должна не потерять: чего она не отобрала, того вторая уже не покажет, и ошибка невосстановима. Вторая должна расставить: её ошибка восстановима переранжированием.
Мерить кандидатогенератор точностью вредно: точность толкает отбирать поменьше и понадёжнее, то есть в популярное, обрушая покрытие. А сквозное качество всё равно ограничено сверху полнотой первой стадии.
Почему 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 — средний пользователь. Деградацию новичков видно только во втором.
Первоисточники
- K. Järvelin, J. Kekäläinen. Cumulated Gain-based Evaluation of IR Techniques, TOIS 2002 — работа, в которой появился DCG и его нормированная версия.
- C. Burges. From RankNet to LambdaRank to LambdaMART, MSR 2010 — откуда берётся домножение градиента на ΔNDCG.
- A. Niculescu-Mizil, R. Caruana. Predicting Good Probabilities With Supervised Learning, ICML 2005 — Platt scaling и изотоническая регрессия.
- J. Davis, M. Goadrich. The Relationship Between Precision-Recall and ROC Curves, ICML 2006 — почему на редких событиях смотрят PR, а не ROC.
- Числа главы:
_tools/metrics_demo.pyв этом репозитории.