Метрики поиска: доля попаданий, MRR и NDCG
Создайте эталонный набор запросов и релевантных документов, затем вычислите долю попаданий, средний обратный ранг и NDCG, чтобы измерить, как часто система поиска находит нужные фрагменты.
«Метрики поиска: доля попаданий, MRR и NDCG» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.
Зачем нужны разные метрики поиска
Доля попаданий показывает, появился ли релевантный фрагмент где-либо среди первых K результатов, но не показывает, где именно в рейтинге он оказался. Система, которая всегда помещает лучший фрагмент на 5-е место, хуже системы, стабильно помещающей его на 1-е место, даже если у обеих одинаковая доля попаданий. Более детальные метрики, такие как MRR и NDCG, оценивают качество ранжирования и поощряют системы, помещающие наиболее релевантные фрагменты наверх, где LLM и пользователи с наибольшей вероятностью ими воспользуются.
Доля попаданий@K: повторение и реализация
Доля попаданий@K — самая простая метрика: для какой доли запросов хотя бы один релевантный фрагмент присутствует среди первых K результатов? Она даёт двоичный сигнал для каждого запроса и легко интерпретируется. Вычисляйте её, проверяя пересечение множеств идентификаторов найденных фрагментов и известных релевантных идентификаторов. По умолчанию используйте K=5, поскольку большинство систем RAG извлекают 5 фрагментов. Сравнивайте долю попаданий@1, @3 и @5, чтобы понять, как меняется чувствительность при расширении окна поиска.
def hit_rate_at_k(golden_dataset, retriever, k=5):
hits = 0
for item in golden_dataset:
retrieved = retriever.retrieve(item['question'], top_k=k)
retrieved_ids = [r['id'] for r in retrieved[:k]]
relevant_ids = set(item['relevant_chunk_ids'])
if any(rid in relevant_ids for rid in retrieved_ids):
hits += 1
return hits / len(golden_dataset)
for k in [1, 3, 5, 10]:
hr = hit_rate_at_k(golden_dataset, retriever, k=k)
print(f'Hit rate@{k}: {hr:.1%}')Средний обратный ранг (MRR)
MRR (средний обратный ранг) измеряет среднее значение, обратное рангу, на котором появляется первый релевантный фрагмент. Если релевантный фрагмент находится на 1-м месте, обратный ранг равен 1/1 = 1.0. На 2-м месте он равен 0.5, а на 5-м — 0.2. MRR усредняется по всем запросам. Более высокий MRR означает, что механизм поиска стабильно помещает релевантные фрагменты ближе к началу, что важно, поскольку LLM уделяет больше внимания контексту, расположенному в начале запроса.
def mean_reciprocal_rank(golden_dataset, retriever, top_k=10):
reciprocal_ranks = []
for item in golden_dataset:
retrieved = retriever.retrieve(item['question'], top_k=top_k)
retrieved_ids = [r['id'] for r in retrieved]
relevant_ids = set(item['relevant_chunk_ids'])
rr = 0.0
for rank, rid in enumerate(retrieved_ids, start=1):
if rid in relevant_ids:
rr = 1.0 / rank
break # only the first relevant result counts
reciprocal_ranks.append(rr)
mrr = sum(reciprocal_ranks) / len(reciprocal_ranks)
print(f'MRR@{top_k}: {mrr:.3f}')
return mrrИнтерпретация значений MRR
Значения MRR легко интерпретировать: MRR = 1.0 означает, что первый релевантный фрагмент всегда находится на 1-м месте (идеальный результат). MRR = 0.5 означает, что обычно он находится на 2-м месте. MRR = 0.25 означает, что обычно он находится на 4-м месте — релевантная информация расположена достаточно далеко, чтобы её могли обрезать из контекста. Для RAG стремитесь к MRR > 0.7, чтобы наиболее релевантный фрагмент стабильно находился в первых двух позициях.
# MRR interpretation table
mrr_interpretations = {
1.0: 'Perfect — relevant chunk always at rank 1',
0.5: 'Good — typically at rank 2',
0.33: 'Acceptable — typically at rank 3',
0.25: 'Weak — typically at rank 4',
0.1: 'Poor — relevant chunk rarely near the top'
}
for score, description in mrr_interpretations.items():
print(f'MRR {score:.2f}: {description}')Точность@K
Точность@K измеряет, какая доля из K найденных фрагментов действительно релевантна. В отличие от доли попаданий, которая является двоичной, точность@K измеряет отношение сигнала к шуму в результатах поиска. Низкая точность означает, что LLM получает вместе с релевантными фрагментами нерелевантный контекст, что повышает риск путаницы или внедрения инструкций в запрос. Для RAG хорошим целевым показателем является точность@5 > 0.6.
def precision_at_k(golden_dataset, retriever, k=5):
precisions = []
for item in golden_dataset:
retrieved = retriever.retrieve(item['question'], top_k=k)
retrieved_ids = [r['id'] for r in retrieved[:k]]
relevant_ids = set(item['relevant_chunk_ids'])
relevant_retrieved = sum(
1 for rid in retrieved_ids if rid in relevant_ids
)
precision = relevant_retrieved / k
precisions.append(precision)
mean_precision = sum(precisions) / len(precisions)
print(f'Precision@{k}: {mean_precision:.3f}')
return mean_precisionДисконтированный совокупный выигрыш (DCG)
DCG — это метрика оценки ранжированного списка, которая поощряет размещение более релевантных фрагментов выше. Она суммирует оценки релевантности найденных фрагментов, но логарифмически уменьшает их вклад в зависимости от позиции: 1-е место получает полный балл, для 2-го места применяется скидка log(2) и так далее. Более релевантные фрагменты в начале дают более высокий DCG. Нормализованная версия (NDCG) делит результат на идеальный DCG (наилучшее возможное ранжирование) и даёт значение от 0 до 1.
import math
def dcg_at_k(relevances, k):
'''relevances[i] = 1 if chunk at rank i+1 is relevant, else 0'''
dcg = 0.0
for i, rel in enumerate(relevances[:k]):
# rank is i+1, discount is log2(rank + 1)
dcg += rel / math.log2(i + 2)
return dcg
def ndcg_at_k(retrieved_ids, relevant_ids, k):
relevances = [1 if rid in relevant_ids else 0
for rid in retrieved_ids[:k]]
actual_dcg = dcg_at_k(relevances, k)
ideal_dcg = dcg_at_k([1] * min(len(relevant_ids), k), k)
return actual_dcg / ideal_dcg if ideal_dcg > 0 else 0.0Вычисление NDCG для всего набора данных
NDCG@K — эталонная метрика поиска для поисковых систем. Она одновременно учитывает релевантность и позицию в рейтинге. NDCG@5, равный 0.85, означает, что ваш механизм поиска в среднем обеспечивает 85% от теоретически наилучшего ранжирования. NDCG учитывает случаи, когда для одного запроса есть несколько релевантных фрагментов (каждому присваивается оценка релевантности), и штрафует системы, которые находят релевантные фрагменты, но помещают их слишком низко.
def mean_ndcg_at_k(golden_dataset, retriever, k=5):
ndcg_scores = []
for item in golden_dataset:
retrieved = retriever.retrieve(item['question'], top_k=k)
retrieved_ids = [r['id'] for r in retrieved[:k]]
relevant_ids = set(item['relevant_chunk_ids'])
score = ndcg_at_k(retrieved_ids, relevant_ids, k)
ndcg_scores.append(score)
mean = sum(ndcg_scores) / len(ndcg_scores)
print(f'NDCG@{k}: {mean:.4f}')
return mean
# Compute all retrieval metrics together
hit_rate = hit_rate_at_k(golden_dataset, retriever, k=5)
mrr = mean_reciprocal_rank(golden_dataset, retriever, top_k=5)
ndcg = mean_ndcg_at_k(golden_dataset, retriever, k=5)Использование RAGAS для автоматизированных метрик
Библиотека RAGAS предоставляет готовую к использованию в рабочей среде платформу оценки систем RAG. Она реализует метрики точности контекста, полноты контекста, достоверности и релевантности ответа, используя подход с LLM в роли проверяющего. Передайте в RAGAS вопросы, ответы, найденные контексты и эталонные ответы и получите полный отчёт об оценке с показателями для каждой метрики. Это самый быстрый способ настроить комплексный конвейер оценки RAG.
from ragas import evaluate
from ragas.metrics import (
context_precision,
context_recall,
faithfulness,
answer_relevancy
)
from datasets import Dataset
eval_data = Dataset.from_list([
{
'question': item['question'],
'answer': item['generated_answer'],
'contexts': item['retrieved_texts'],
'ground_truth': item['expected_answer']
}
for item in golden_dataset_with_answers
])
results = evaluate(
eval_data,
metrics=[context_precision, context_recall, faithfulness, answer_relevancy]
)
print(results)Разбиение метрик по категориям запросов
Средние метрики по всему набору скрывают важные закономерности. Разделите эталонный набор данных на категории — поиск фактов, сравнения, пошаговые вопросы «как сделать» и вопросы за пределами области применения — и вычисляйте метрики отдельно для каждой из них. Вы можете обнаружить, что доля попаданий для поиска фактов составляет 95%, а для многошаговых сравнений — всего 60%. Метрики на уровне категорий показывают конкретные режимы отказа, которые скрываются за сводными оценками.
from collections import defaultdict
def evaluate_by_category(golden_dataset, retriever):
by_category = defaultdict(list)
for item in golden_dataset:
category = item.get('category', 'unknown')
retrieved = retriever.retrieve(item['question'], top_k=5)
retrieved_ids = {r['id'] for r in retrieved}
hit = bool(retrieved_ids & set(item['relevant_chunk_ids']))
by_category[category].append(hit)
print('Hit rate by category:')
for cat, hits in sorted(by_category.items()):
hr = sum(hits) / len(hits)
print(f' {cat}: {hr:.1%} ({sum(hits)}/{len(hits)})')Когда метрики расходятся
Иногда метрики подают противоречивые сигналы. Вы можете улучшить NDCG (повысить качество ранжирования), тогда как доля попаданий останется неизменной (число промахов не изменится). Это происходит, когда оптимизация перемещает релевантные фрагменты с 6-го места на 2-е, но не возвращает ранее пропущенные запросы в первую пятёрку. В таких случаях проверьте, помогла ли оптимизация запросам, которые и так выполнялись успешно, и не оставила ли без внимания неудачные запросы. Всегда изучайте отдельные примеры ошибок наряду со сводными метриками.
def analyze_failures(golden_dataset, retriever, top_k=5):
failures = []
for item in golden_dataset:
retrieved = retriever.retrieve(item['question'], top_k=top_k)
retrieved_ids = {r['id'] for r in retrieved}
relevant_ids = set(item['relevant_chunk_ids'])
if not (retrieved_ids & relevant_ids):
failures.append({
'question': item['question'],
'expected_chunks': list(relevant_ids),
'retrieved_chunks': [r['id'] for r in retrieved],
'top_score': retrieved[0]['score'] if retrieved else None
})
print(f'Failures: {len(failures)}/{len(golden_dataset)}')
return failuresПолнота поиска@K
Полнота@K измеряет, какая доля всех релевантных фрагментов была найдена среди первых K результатов. Если для вопроса в индексе есть 3 релевантных фрагмента, а ваш механизм поиска возвращает 2 из них в первой пятёрке, полнота@5 равна 2/3 = 0.67. Высокая полнота важна, когда LLM нужны несколько фрагментов доказательств, чтобы составить полный ответ: отсутствие даже одного ключевого фрагмента может сделать ответ неполным. Балансируйте точность и полноту, настраивая K: большее значение K повышает полноту, но снижает точность.
def recall_at_k(golden_dataset, retriever, k=5):
recalls = []
for item in golden_dataset:
retrieved = retriever.retrieve(item['question'], top_k=k)
retrieved_ids = set(r['id'] for r in retrieved[:k])
relevant_ids = set(item['relevant_chunk_ids'])
if not relevant_ids:
continue # skip items with no annotated relevant chunks
retrieved_relevant = retrieved_ids & relevant_ids
recall = len(retrieved_relevant) / len(relevant_ids)
recalls.append(recall)
mean_recall = sum(recalls) / len(recalls)
print(f'Recall@{k}: {mean_recall:.3f}')
return mean_recallБыстрая проверка
Проверьте, насколько хорошо вы усвоили концепции инженерии ИИ из этого урока.
Итоги урока
В этом уроке вы узнали: о доле попаданий@K как метрике двоичного наличия; о MRR для измерения среднего положения первого релевантного фрагмента; о точности@K для оценки отношения сигнала к шуму при поиске; о NDCG@K как эталонной метрике ранжирования; а также о библиотеке RAGAS для автоматизации оценки RAG с помощью точности контекста, полноты, достоверности и релевантности ответа. Далее мы подробнее рассмотрим метрики генерации и измерение достоверности.
Часто задаваемые вопросы
Урок «Метрики поиска: доля попаданий, MRR и NDCG» бесплатный?
Да — полный текст урока «Метрики поиска: доля попаданий, MRR и NDCG» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.
Чему я научусь в уроке «Метрики поиска: доля попаданий, MRR и NDCG»?
Создайте эталонный набор запросов и релевантных документов, затем вычислите долю попаданий, средний обратный ранг и NDCG, чтобы измерить, как часто система поиска находит нужные фрагменты. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать AI Engineering Academy?
Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Метрики поиска: доля попаданий, MRR и NDCG»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке AI Engineering Academy?
Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Зачем нужна оценка RAG
- Метрики поиска: доля попаданий, MRR и NDCG
- Метрики генерации: достоверность и релевантность ответа
- Создание автоматизированной системы оценки