Перейти к содержимому
Ступень 22.2 Линейные модели с нуля

Метрики качества

Как честно измерить качество модели — матрица ошибок, accuracy, precision, recall, F1, ROC-AUC, MSE и MAE — и почему метрика почти никогда не совпадает с loss.

УРОКstage-2/2-2-linear-models/03-metrics.mdx1 ч 30 минУрок 3 из 3
После урока вы сможете
  • Построить матрицу ошибок и посчитать по ней accuracy, precision, recall и F1
  • Выбирать метрику и порог под задачу и замечать, когда accuracy обманывает
  • Объяснить ROC-AUC как вероятность правильно упорядочить пару примеров и посчитать её вручную
  • Объяснить, почему модель учат минимизировать одну функцию (loss), а оценивают по другой (метрике)

Функция потерьфункция потерьEN loss function нужна оптимизатору, а метрика — людям, которые принимают решения. Это разные вещи. Модель учат минимизировать cross-entropy, а в отчёте пишут что-нибудь вроде «accuracy 87% на C-Eval», «pass@1 = 71%» или «recall@5 = 0.93». Все эти слова мы разберём в уроке.

В работе с LLM метрики повсюду. Почти любой бенчмаркбенчмаркEN benchmark (стандартный набор задач для сравнения моделей) считают через accuracy или pass@k. Классификаторы, которые чистят данные для претрейна, оценивают по precision и recall. RAGгенерация с дополнением извлечёнными данными (RAG)EN retrieval-augmented generation (RAG) — схема, где модель сначала ищет подходящие документы в базе, а потом отвечает по ним (ступень 8). Ту часть, что ищет, называют ретривером, и её оценивают по recall@k. Выберете не ту метрику — будете улучшать не то. Этот урок даёт словарь и интуицию, без которых не прочитать ни один техотчёт.

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

Продолжим пример из прошлого урока. Классификатор отбирает математические страницы для претрейна. На проверку взяли 1000 страниц, из них 100 действительно математические. Модель отметила 120 страниц, и 80 из них оказались математикой. Разложим всё по четырём клеткам. Это матрица ошибокматрица ошибокEN confusion matrix:

Модель: «математика»Модель: «не математика»
Правда: математика (100)TP = 80FN = 20
Правда: не математика (900)FP = 40TN = 860

Как читать обозначения. Вторая буква — что сказала модель: P (positive, «да») или N (negative, «нет»). Первая — права ли она: T (true) или F (false). FP — ложная тревога: модель сказала «да», а там проза. FN — пропуск: модель сказала «нет», а там формулы.

Все метрики ниже — разные способы свернуть эти четыре числа в одно.

Accuracyдоля правильных ответов (accuracy)EN accuracyПо-русски accuracy иногда тоже называют «точностью» — из-за этого её путают с precision. Безопаснее писать «accuracy». — доля правильных ответов:

Accuracy=TP+TNTP+TN+FP+FN=80+8601000=0.94\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN} = \frac{80 + 860}{1000} = 0.94

Звучит отлично. Но классификатор «всегда отвечай НЕТ» получает 900/1000=0.90900 / 1000 = 0.90, ни разу не найдя математику. На реальном вебе математических страниц гораздо меньше 10%. Чем их меньше, тем ближе бесполезная константа к 100%. Это дисбаланс классовдисбаланс классовEN class imbalance, и при нём accuracy почти ничего не говорит.

Accuracy хороша, когда классы сбалансированы и ошибки одинаково дороги. Пример — тесты с выбором ответа вроде MMLU и C-Eval: четыре варианта, случайное угадывание даёт 25%, и accuracy честно показывает, насколько модель лучше.

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

Precision=TPTP+FP=80120≈0.667,Recall=TPTP+FN=80100=0.8\begin{gathered} \text{Precision} = \frac{TP}{TP + FP} = \frac{80}{120} \approx 0.667, \\ \text{Recall} = \frac{TP}{TP + FN} = \frac{80}{100} = 0.8 \end{gathered}
  • Precisionточность (precision)EN precisionTP / (TP + FP). У Чжоу Чжихуа термин буквально означает «доля точных среди найденного». (точность) — «из отобранного, сколько правда математика?» Это чистота корпуса: треть отобранных страниц — мусор.
  • Recallполнота (recall)EN recallTP / (TP + FN). Recall — буквально «отозвать, вернуть»: та же метрика recall@k в поиске и RAG. (полнота) — «из всей математики, сколько нашли?» Это охват: пятая часть математики потерялась.

Китайские названия запоминаются легко: в учебниках precision буквально называют «долей точно найденного», а recall — «долей найденного из всего». В статьях recall чаще называют «долей отозванного». Слово «отозвать, вернуть» вы ещё встретите в поиске и RAG: так по-китайски называют этап, на котором отбирают кандидатов.

ЗадачаЧто дорожеГлавная метрика
Спам-фильтр почтыПотерять важное письмо (FP)Precision
Поиск уязвимостей в кодеПропустить уязвимость (FN)Recall
Фильтр токсичных текстов для претрейнаПропустить токсичный текст в корпусRecall по токсичным
Первый этап RAG (ретривер)Не найти нужный документ — LLM его не увидитRecall@k
Второй этап RAG (реранкер)Засорить контекст лишнимPrecision в top-k

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

В поиске метрики считают по первым kk результатам выдачи. Recall@k — какая доля всех нужных документов попала в top-kk. Precision в top-k (precision@k) — какая доля из этих kk документов действительно нужна.

Модель выдаёт не метку, а скор — вероятность. Метку получают сравнением с порогом: скор ≥ порога — «математика». Порог классификациипорог классификацииEN classification threshold — главный рычаг. Возьмём 10 страниц: 4 математические со скорами 0.95, 0.80, 0.60, 0.35 и 6 обычных со скорами 0.70, 0.45, 0.30, 0.20, 0.10, 0.05.

ПорогTPFPFNPrecisionRecallF1
0.304300.5711.000.727
0.503110.7500.750.750
0.752021.0000.500.667

Порог выше — модель осторожнее: precision растёт, recall падает. Порог ниже — наоборот. Порог не обучают вместе с весами. Его выбирают после обучения на валидационной выборке под нужный баланс. Например, «максимальный recall при precision не ниже 0.9».

Сравнивать модели удобнее по одному числу. F1-мераF1-мераEN F1 scoreГармоническое среднее precision и recall. — гармоническое среднее precision и recall. Гармоническое среднее двух чисел — это «удвоенное произведение, делённое на сумму»:

F1=2⋅P⋅RP+R=2⋅0.667⋅0.80.667+0.8≈0.727F_1 = \frac{2 \cdot P \cdot R}{P + R} = \frac{2 \cdot 0.667 \cdot 0.8}{0.667 + 0.8} \approx 0.727

Почему не обычное среднее? Возьмём модель, которая отметила 2 страницы из 100 математических и не ошиблась: P=1P = 1, R=0.02R = 0.02. Среднее арифметическое — 0.51, будто модель «наполовину хороша». F1 =2⋅0.02/1.02≈0.039= 2 \cdot 0.02 / 1.02 \approx 0.039. Гармоническое среднее тянется к меньшему из чисел и не даёт одной метрике спрятать провал другой.

Если recall важнее precision, берут FβF_\beta с β>1\beta > 1:

Fβ=(1+β2)⋅P⋅Rβ2P+RF_\beta = \frac{(1 + \beta^2) \cdot P \cdot R}{\beta^2 P + R}

Число β\beta («бета») говорит, во сколько раз recall важнее precision. При β=1\beta = 1 формула превращается в обычную F1F_1. Для нашего фильтра при β=2\beta = 2: F2=5⋅0.667⋅0.84⋅0.667+0.8=2.6673.467≈0.769F_2 = \frac{5 \cdot 0.667 \cdot 0.8}{4 \cdot 0.667 + 0.8} = \frac{2.667}{3.467} \approx 0.769 — выше, чем F1F_1, потому что recall у фильтра (0.8) лучше precision (0.667) и теперь весит больше. А F0.5≈0.690F_{0.5} \approx 0.690: больше весит precision, и оценка ниже.

Precision, recall и F1 зависят от порога. А как сравнить две модели «вообще», до выбора порога? Будем двигать порог от 1 до 0 и следить за двумя величинами:

  • TPR (true positive rate) — это recall: TPTP+FN\frac{TP}{TP + FN};
  • FPR (false positive rate) — доля ложных тревог среди отрицательных: FPFP+TN\frac{FP}{FP + TN}.

Когда порог выше всех скоров, модель ничего не отмечает: (FPR,TPR)=(0,0)(\text{FPR}, \text{TPR}) = (0, 0). При пороге 0 она отмечает всё: (1,1)(1, 1). Кривая между ними — ROC-кривая. Площадь под ней — ROC-AUCROC-AUC (площадь под ROC-кривой)EN ROC AUC (area under the ROC curve)Вероятность того, что случайный положительный пример получит скор выше случайного отрицательного..

Построим кривую для наших 10 страниц. Будем ставить порог по очереди на каждый скор, от большего к меньшему. Каждый раз ответ «да» (скор ≥ порога) получает ровно одна новая страница:

ПорогКто добавилсяTPR (recall)FPR
0.95математика1/4 = 0.250
0.80математика2/4 = 0.500
0.70обычная0.501/6 ≈ 0.17
0.60математика3/4 = 0.750.17
0.45обычная0.752/6 ≈ 0.33
0.35математика4/4 = 1.000.33
0.30обычная1.003/6 = 0.50
0.20, 0.10, 0.05обычные1.000.67, 0.83, 1.00

Математическая страница поднимает кривую на 1/4 (их четыре), обычная сдвигает вправо на 1/6 (их шесть). Получается лесенка:

01/62/63/64/65/6101/42/43/41FPR — доля ложных тревогTPR = recallслучайное угадывание(AUC = 0.5)0.950.800.700.600.450.352/43/44/44/44/44/4Порог опускается от 1 к 0:положительный пример —шаг вверх на 1/4,отрицательный — шагвправо на 1/6.Подписи у точек — порог.Площадь = 6 столбцовшириной 1/6, высотыподписаны внизу:(2+3+4+4+4+4) / 24= 21/24 = 0.875
ROC-кривая десяти страниц из примера. Каждый столбец под ступенькой соответствует одной обычной странице, а его высота — доле математических страниц со скором выше её. Отсюда площадь = доля правильно упорядоченных пар.

Площадь под лесенкой не нужно интегрировать. Это шесть столбцов шириной 1/6 — по одному на каждую обычную страницу. Высота столбца — доля математических страниц, которые обогнали эту обычную по скору: 2/4, 3/4 и четыре раза по 4/4. Площадь равна 2+3+4+4+4+44⋅6=2124=0.875\frac{2 + 3 + 4 + 4 + 4 + 4}{4 \cdot 6} = \frac{21}{24} = 0.875. В числителе стоит число пар «математическая × обычная», в которых математическая страница получила скор выше. Отсюда главная интерпретация AUC:

AUC=P(скор случайного положительного>скор случайного отрицательного)\begin{gathered} \text{AUC} = P\big(\text{скор случайного положительного} \\ > \text{скор случайного отрицательного}\big) \end{gathered}

Проверим с другой стороны — пройдём по математическим страницам. Всего пар 4⋅6=244 \cdot 6 = 24.

  • 0.95 и 0.80 выше всех шести обычных: 6+6=126 + 6 = 12 пар.
  • 0.60 проигрывает только обычной странице со скором 0.70: 5 пар.
  • 0.35 выше четырёх обычных (0.30, 0.20, 0.10, 0.05): 4 пары.

Итого 21/24=0.87521 / 24 = 0.875. Ничьи (равные скоры) считаются за половину пары. Перебор всех пар на миллионе объектов слишком медленный; как посчитать AUC за O(nlog⁡n)O(n \log n) через сортировку и средние ранги — классическая задача собеседования из урока 11.3.4.

Что из этого следует:

  • AUC = 0.5 — случайное угадывание, 1.0 — идеальное ранжирование. AUC меньше 0.5 значит, что модель перепутала классы: переверните её ответы.
  • AUC зависит только от порядка скоров. Логиты и вероятности после сигмоиды дают одинаковый AUC.
  • AUC не зависит от порога и от доли классов. Это плюс для сравнения моделей и минус при сильном дисбалансе. Когда положительных доли процента, даже малый FPR — это тысячи ложных тревог. Тогда смотрят на PR-кривую (precision от recall) и площадь под ней — average precision (AP). AP — это средняя precision в моменты, когда порог «захватывает» очередной положительный пример. У наших 10 страниц это (1/1+2/2+3/4+4/6)/4≈0.85(1/1 + 2/2 + 3/4 + 4/6) / 4 \approx 0.85.
flowchart TD
Q["Какую метрику выбрать?"] --> R{"Регрессия или<br/>классификация?"}
R -->|"регрессия"| M{"Важны ли<br/>редкие большие ошибки?"}
M -->|"да"| MSE["MSE / RMSE"]
M -->|"нет, нужна устойчивость к выбросам"| MAE["MAE, медиана, квантили"]
R -->|"классификация"| B{"Классы<br/>сбалансированы?"}
B -->|"да, ошибки равноценны"| ACC["Accuracy"]
B -->|"нет"| T{"Порог уже выбран?"}
T -->|"да"| PRF["Precision, recall, F1 / Fβ"]
T -->|"нет, сравниваем модели"| AUC["ROC-AUC или PR-AUC"]
Шпаргалка по выбору метрики

MSEсреднеквадратичная ошибка (MSE)EN mean squared error (MSE)Корень из MSE — RMSE (root mean squared error), измеряется в тех же единицах, что и целевая переменная. вы знаете по прошлым урокам. Её ближайшая родственница — средняя абсолютная ошибка (MAE)средняя абсолютная ошибка (MAE)EN mean absolute error (MAE):

MAE=1n∑i∣y^i−yi∣,RMSE=MSE\text{MAE} = \frac{1}{n} \sum_i \lvert \hat y_i - y_i \rvert, \qquad \text{RMSE} = \sqrt{\text{MSE}}

Вернёмся к прогнозу времени ответа LLM. Ошибки на пяти запросах: 0.1,−0.2,0.1,0.00.1, -0.2, 0.1, 0.0 и 3.03.0 секунды. Последний запрос «завис» в очереди.

MAEMSERMSE
Без зависшего запроса0.100.0150.12
С зависшим запросом0.681.8121.35

Один выброс увеличил MAE в 6.8 раза, а MSE — в 120 раз. MSE «паникует» из-за больших ошибок, MAE спокойнее.

Разница глубже, чем кажется. Если предсказывать одну константу, то лучшая константа для MSE — среднее, а для MAE — медиана (число посередине упорядоченного списка, урок 0.3.3). Почему: производная ∑i(c−yi)2\sum_i (c - y_i)^2 по cc равна 2∑i(c−yi)2 \sum_i (c - y_i) и обнуляется, когда cc — среднее. А производная ∑i∣c−yi∣\sum_i \lvert c - y_i \rvert равна «число точек ниже cc минус число точек выше cc». Она обнуляется, когда по обе стороны от cc точек поровну, то есть в медиане. Пусть времена ответа равны 1, 1, 1, 1 и 10 секунд. Среднее — (1+1+1+1+10)/5=2.8(1 + 1 + 1 + 1 + 10) / 5 = 2.8 с, медиана — 1 с (третье из пяти упорядоченных чисел). Какое число вы покажете пользователю как «типичное время ответа»?

Поэтому в инференсе (так называют работу уже обученной модели — ответы на запросы) задержку описывают квантилями — перцентилями из урока 0.3.3. p50 — медиана: половина запросов обслужена быстрее. p99 — «хвост»: быстрее этого времени обслужено 99% запросов, и только 1% ждал дольше. Это тоже метрики, и они устойчивы к выбросам.

Если в итоге нас интересует accuracy, почему бы не учить модель прямо на ней? Есть четыре причины.

  1. Метрика не дифференцируема. Accuracy — ступенчатая функция весов. Сдвиньте веса чуть-чуть, и ни один ответ, скорее всего, не поменяется: accuracy та же, градиент ноль. Градиентному спуску не за что зацепиться. В PyTorch argmax просто рвёт вычислительный граф (цепочку операций, по которой считаются градиенты, урок 1.2.2).

  2. Loss — гладкий суррогат (заменитель). Cross-entropy реагирует на любое изменение вероятностей. К тому же она связана с долей ошибок: если модель ошиблась, вероятность правильного класса p≤0.5p \le 0.5 (иначе он победил бы в argmax), и −log⁡2p≥1-\log_2 p \ge 1. Значит, средняя cross-entropy в битах (с log⁡2\log_2) не меньше доли ошибок: уменьшая loss, мы опускаем потолок для числа ошибок.

  3. Loss видит уверенность, метрика — нет. Две модели решают задачу с двумя классами. На четырёх примерах они дают правильному классу такие вероятности (cross-entropy — в натах, с ln⁡\ln; биты и наты — просто логарифмы по разным основаниям, урок 1.3.3):

    Вероятности правильного классаAccuracyCross-entropy
    Модель A0.51, 0.51, 0.51, 0.4975%0.683
    Модель B0.99, 0.99, 0.99, 0.0175%1.159

    Accuracy одинаковая. Но B один раз уверенно ошиблась, и cross-entropy это видит. Если LLM так же уверенно ошибается, при сэмплировании она почти наверняка выдаст неверный токен и продолжит уверенно неверный ответ.

  4. Метрику выбирает задача, а не математика. Порог, β\beta в FβF_\beta, цена ложной тревоги — бизнес-решения. Их принимают после обучения, на валидации.

С LLM то же самое, только на другом уровне. Претрейн минимизирует cross-entropy следующего токена (её экспонента — перплексияперплексияEN perplexity из урока 1.3.3). А модели сравнивают по accuracy на бенчмарках, pass@k на задачах с кодом (доля задач, где хотя бы одна из kk попыток прошла тесты), доле побед в Chatbot Arena (сайт, где люди вслепую сравнивают ответы двух моделей). Связь между ними есть, но не жёсткая. И есть закон Гудхартазакон ГудхартаEN Goodhart's law«Когда мера становится целью, она перестаёт быть хорошей мерой».: когда мера становится целью, она перестаёт быть хорошей мерой. Для подгонки под конкретный бенчмарк в китайском AI-сообществе есть жаргонное словечко «шуабан» («накручивать рейтинг»). Подробно об оценке LLMоценка (эвалы)EN evaluation (evals) — в уроке 7.3.1.

Реализуйте метрики на чистом Python: матрицу ошибок, precision, recall, F1 (без деления на ноль) и ROC-AUC через подсчёт пар. NumPy не нужен: хватит циклов, словаря и zip из модуля 0.2. Тесты используют примеры из этого урока, поэтому ответы можно сверить с таблицами выше.

Precision, recall, F1 и ROC-AUC7 тестов
def confusion_counts(y_true, y_pred):
    """Матрица ошибок для бинарной классификации.

    y_true, y_pred — списки из 0 и 1 одинаковой длины (1 = положительный класс).
    Возвращает словарь {'tp': ..., 'fp': ..., 'fn': ..., 'tn': ...}.
    """
    # TODO
    ...


def precision(y_true, y_pred):
    """Precision = TP / (TP + FP). Если модель не назвала ни одного положительного — вернуть 0.0."""
    # TODO
    ...


def recall(y_true, y_pred):
    """Recall = TP / (TP + FN). Если положительных в y_true нет — вернуть 0.0."""
    # TODO
    ...


def f1_score(y_true, y_pred):
    """F1 = 2·P·R / (P + R) — гармоническое среднее. Если P + R = 0 — вернуть 0.0."""
    # TODO
    ...


def roc_auc(y_true, scores):
    """ROC-AUC через пары: доля пар (положительный, отрицательный), где положительный
    получил БОЛЬШИЙ скор. Ничья (равные скоры) засчитывается как 0.5.

    scores — список чисел (вероятностей или любых «баллов» модели).
    """
    # TODO
    ...


# Проверьте себя кнопкой «Запустить» — пример из урока (порог 0.5):
y_true = [1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
y_pred = [1, 1, 1, 0, 1, 0, 0, 0, 0, 0]
print(confusion_counts(y_true, y_pred))  # {'tp': 3, 'fp': 1, 'fn': 1, 'tn': 5}
print(precision(y_true, y_pred), recall(y_true, y_pred), f1_score(y_true, y_pred))  # 0.75 0.75 0.75

Убедимся, что accuracy не пропускает градиент, а cross-entropy пропускает:

import torch
import torch.nn.functional as F
# 4 примера, 2 класса; правильные классы y
# requires_grad=True — «PyTorch, запоминай операции с этим тензором, потом посчитаешь по нему градиент»
logits = torch.tensor([[2.0, 0.5], [0.3, 0.1], [-1.0, 1.0], [0.2, -0.4]], requires_grad=True)
y = torch.tensor([0, 1, 1, 0])
ce = F.cross_entropy(logits, y) # loss: softmax + минус лог правильного класса, среднее
acc = (logits.argmax(dim=1) == y).float().mean() # метрика: доля примеров, где argmax совпал с y
ce.backward() # градиенты CE по логитам → в logits.grad
print(f"CE = {ce.item():.3f}, accuracy = {acc.item():.2f}")
print("accuracy.requires_grad:", acc.requires_grad) # argmax рвёт граф: градиента у accuracy нет
print("dCE/dlogits:\n", logits.grad)
# Чуть-чуть сдвинем логиты второго примера к правильному классу
with torch.no_grad(): # здесь градиенты не нужны
logits2 = logits.clone() # копия, чтобы не испортить исходный тензор
logits2[1, 1] += 0.1 # второй пример (строка 1), правильный класс 1 (столбец 1)
print(f"после сдвига: CE = {F.cross_entropy(logits2, y).item():.3f}, "
f"accuracy = {(logits2.argmax(1) == y).float().mean().item():.2f}")

Ожидаемый вывод:

CE = 0.391, accuracy = 0.75
accuracy.requires_grad: False
dCE/dlogits:
tensor([[-0.0456, 0.0456],
[ 0.1375, -0.1375],
[ 0.0298, -0.0298],
[-0.0886, 0.0886]])
после сдвига: CE = 0.378, accuracy = 0.75

Градиент cross-entropy указывает, куда двигать каждый логит. Сильнее всего «просит» второй пример — тот, на котором модель ошибается. Сдвиг в нужную сторону уменьшил CE, а accuracy даже не заметила. Градиенты здесь равны (p−onehot(y))/n(p - \text{onehot}(y)) / n — та же формула из прошлого урока, делённая на размер батча.

Проверь себя6 вопросов
1 TP = 30, FP = 10, FN = 20. Чему равна F1? (округлите до двух знаков)
2 Мошеннических транзакций 1%. Модель всегда отвечает «не мошенничество». Какая метрика сразу покажет, что модель бесполезна?
3 Какие утверждения о ROC-AUC верны?Можно выбрать несколько ответов
4 Первый этап RAG: ретривер отбирает 20 документов из миллиона, дальше их читает реранкер и LLM. Какая метрика ретривера важнее всего?
5 Почему LLM учат на cross-entropy, а не прямо на accuracy?
6 Ошибки модели на пяти примерах: 1,−1,2,−2,91, -1, 2, -2, 9. Чему равна MAE?
  • Матрица ошибок (TP, FP, FN, TN) — основа всех метрик классификации. Accuracy обманывает при дисбалансе классов.
  • Precision — чистота найденного, recall — полнота. Их баланс задаёт порог, а выбирают его на валидации. F1 — гармоническое среднее, FβF_\beta — со смещением в нужную сторону. Как выбрать порог по цене ошибок, а не по F1, — в разборе модерации из урока 11.2.4.
  • ROC-AUC — вероятность правильно упорядочить пару «положительный/отрицательный». Не зависит от порога. При сильном дисбалансе дополняйте её PR-кривой.
  • MSE чувствительна к выбросам и ведёт к среднему, MAE устойчива и ведёт к медиане.
  • Loss ≠ метрика: учим гладкий суррогат (cross-entropy), отчитываемся задачной метрикой. И помним про закон Гудхарта и «шуабан» — накрутку рейтинга.

Дальше: модуль 2.3 — kNN, деревья решений и бустинг. Это классические алгоритмы, которые на табличных данных до сих пор часто обходят нейросети. Метрики из этого урока понадобятся, чтобы честно их сравнить.