Как оценить качество извлечения в трубопроводах RAG: Точность @k, отзыв@k и F1@k

Автор: Дмитрий Иванов [Команда P9X]

~8 минут чтения

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

Итак, теперь, когда мы получили соответствующие документы, пришло время передать их в LLM для этапа генерации. Но прежде важно уметь определять, хорошо ли работает механизм извлечения и может ли он успешно идентифицировать релевантные результаты. В конце концов, извлечение фрагментов, содержащих ответ на запрос пользователя, — это первый шаг к созданию осмысленного ответа.

Поэтому именно это мы и рассмотрим в сегодняшнем посте. В частности, мы рассмотрим некоторые из наиболее популярных метрик для оценки эффективности извлечения и переранжирования.

Почему важно измерять производительность извлечения

Наша цель — оценить, насколько хорошо наша модель встраивания и векторная база данных возвращают фрагменты текста-кандидата. По сути, мы пытаемся выяснить: «Находятся ли нужные документы где-то в топ-k извлечённых наборах?» или «Возвращает ли наш векторный поиск полный мусор?» 😛 Существует несколько различных мер, которые мы можем использовать, чтобы ответить на этот вопрос. Большинство из них родом из области информационного поиска.

Прежде чем начать, полезно различать два типа мер — бинарные и градуированные релевантности. Более конкретно, бинарные меры характеризуют извлечённый текстовый фрагмент либо как релевантный, либо как нерелевантный для ответа на запрос пользователя — среднего не дано. Напротив, градуированные меры присваивают значение релевантности каждому извлечённому текстовому фрагменту, где-то в спектре от полной нерелевантности до полной релевантности.

Некоторые бинарные меры, не учитывающие порядок

Бинарные меры, не учитывающие порядок, для оценки извлечения являются наиболее простыми и интуитивно понятными. Таким образом, они являются отличной отправной точкой для понимания того, что именно мы пытаемся измерить и оценить. Некоторые распространённые и полезные бинарные, не учитывающие порядок меры — это HitRate@k, Recall@k, Precision@k и F1@k.

🎯 HitRate@K

HitRate@K — это простейшая из всех мер для оценки извлечения. Это бинарная мера, указывающая, существует ли хотя бы один релевантный результат в топ-k извлечённых фрагментах или нет. Таким образом, он может принимать только два значения: либо 1 (если в извлечённом наборе существует хотя бы один релевантный документ), либо 0 (если ни один из извлечённых документов на самом деле не является релевантным).

🎯 Recall@K

Recall@K выражает, как часто релевантные документы появляются в топ-k извлечённых документах. По сути, он оценивает, насколько хорошо нам удалось избежать ложных отрицательных результатов. Recall@k рассчитывается следующим образом:

🎯 Precision@k

Precision@k указывает, сколько из топ-k извлечённых документов действительно релевантны. По сути, он оценивает, насколько хорошо нам удалось избежать ложных положительных результатов. Precision@k можно рассчитать следующим образом:

🎯 F1@K

Но что, если нам нужны как правильные, так и полные результаты — что, если нам нужен извлечённый набор, чтобы иметь высокие значения как Recall, так и Precision? Чтобы достичь этого, Recall@K и Precision@K можно объединить в единую меру, называемую F1@K, что позволит нам создать показатель, одновременно балансирующий достоверность и полноту извлечённых результатов.

Так хорош ли наш векторный поиск?

Теперь давайте посмотрим, как всё это работает на примере «Войны и мира», ответив ещё раз на мой любимый вопрос — «Кто такая Анна Павловна?». Как и в моих предыдущих постах, я снова буду использовать текст «Войны и мира» в качестве примера, лицензированный как общественное достояние и легко доступный через Project Gutenberg.

Определение метрик оценки извлечения

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

def normalize_text(text):
    return " ".join(text.lower().split())

def hit_rate_at_k(retrieved_docs, ground_truth_texts, k):
    for doc in retrieved_docs[:k]:
        doc_norm = normalize_text(doc.page_content)
        if any(normalize_text(gt) in doc_norm or doc_norm in normalize_text(gt) for gt in ground_truth_texts):
            return True
    return False

def precision_at_k(retrieved_docs, ground_truth_texts, k):
    hits = 0
    for doc in retrieved_docs[:k]:
        doc_norm = normalize_text(doc.page_content)
        if any(normalize_text(gt) in doc_norm or doc_norm in normalize_text(gt) for gt in ground_truth_texts):
            hits += 1
    return hits / k

def recall_at_k(retrieved_docs, ground_truth_texts, k):
    matched = set()
    for i, gt in enumerate(ground_truth_texts):
        gt_norm = normalize_text(gt)
        for doc in retrieved_docs[:k]:
            doc_norm = normalize_text(doc.page_content)
            if gt_norm in doc_norm or doc_norm in gt_norm:
                matched.add(i)
                break
    return len(matched) / len(ground_truth_texts) if ground_truth_texts else 0

def f1_at_k(precision, recall):
    return 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0

Чтобы рассчитать любую из этих оценочных метрик, нам нужно сначала определить набор запросов и соответствующие им действительно релевантные фрагменты. Это довольно объёмная работа; поэтому я продемонстрирую процесс только для одного запроса — «Кто такая Анна Павловна?» — и соответствующих релевантных текстовых фрагментов, которые должны быть извлечены.

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

В частности, мы можем считать, что релевантными фрагментами, которые должны быть включены в словарь истинной информации для нашего запроса «Кто такая Анна Павловна?», являются следующие:

  1. «Это было в июле 1805 года, и оратором была известная Анна Павловна Шерер, фрейлина и любимица императрицы Марии Фёдоровны. С этими словами она приветствовала князя Василия Курагина, человека высокого ранга и важности, который первым прибыл на её приём. У Анны Павловны несколько дней был кашель. Она, как сказала, болела гриппом; грипп тогда был новым словом в Санкт-Петербурге, используемым только элитой. Все её приглашения без исключения, написанные по-французски и доставленные лакеем в алой ливрее в то утро, выглядели так: „Если у вас нет ничего более интересного, граф (или князь), и если перспектива провести вечер с бедной больной не слишком ужасна, я буду очень рада видеть вас сегодня между 7 и 10 — Аннет Шерер“».
  2. «Приём Анны Павловны был похож на предыдущий, только новинкой, которую она предложила своим гостям на этот раз, был не Мортемарт, а дипломат из Берлина с самыми свежими подробностями о визите императора Александра в Потсдам и о том, как два августейших друга поклялись в неразрывном союзе поддерживать дело справедливости против врага рода человеческого. Анна Павловна приняла Пьера с оттенком меланхолии, очевидно относящимся к недавней потере молодого человека смертью графа Безухова (все постоянно считали своим долгом заверить Пьера, что он был глубоко огорчён смертью отца, которого едва знал), и её меланхолия была такой же, как августейшая меланхолия, которую она показывала при упоминании её августейшего Величества императрицы Марии Фёдоровны. Пьер был польщён этим. Анна Павловна устроила разные группы в своей гостиной со своим обычным мастерством».
  3. «Гостиная с её обычным мастерством. Большая группа, в которой были принц Василий и генералы, получила пользу от дипломата. Другая группа была за чайным столом. Пьер хотел присоединиться к первой, но Анна Павловна — которая была в возбуждённом состоянии, как командир на поле боя, в голове которого тысячи новых и блестящих идей, которые едва ли успевают воплотить в жизнь, — увидев Пьера, коснулась его рукава пальцем, говоря:»

В этом случае мы можем определить истинную информацию для этого запроса и соответствующих фрагментов, содержащих информацию, которая может ответить на вопрос, следующим образом:

query = "Who is Anna Pávlovna?"

ground_truth_texts = [
    "It was in July, 1805, and the speaker was the well-known Anna Pávlovna Schérer, maid of honor and favorite of the Empress Márya Fëdorovna. With these words she greeted Prince Vasíli Kurágin, a man of high rank and importance, who was the first to arrive at her reception. Anna Pávlovna had had a cough for some days. She was, as she said, suffering from la grippe; grippe being then a new word in St. Petersburg, used only by the elite. All her invitations without exception, written in French, and delivered by a scarlet-liveried footman that morning, ran as follows: “If you have nothing better to do, Count (or Prince), and if the prospect of spending an evening with a poor invalid is not too terrible, I shall be very charmed to see you tonight between 7 and 10 — Annette Schérer.”",
    "Anna Pávlovna’s “At Home” was like the former one, only the novelty she offered her guests this time was not Mortemart, but a diplomatist fresh from Berlin with the very latest details of the Emperor Alexander’s visit to Potsdam, and of how the two august friends had pledged themselves in an indissoluble alliance to uphold the cause of justice against the enemy of the human race. Anna Pávlovna received Pierre with a shade of melancholy, evidently relating to the young man’s recent loss by the death of Count Bezúkhov (everyone constantly considered it a duty to assure Pierre that he was greatly afflicted by the death of the father he had hardly known), and her melancholy was just like the august melancholy she showed at the mention of her most august Majesty the Empress Márya Fëdorovna. Pierre felt flattered by this. Anna Pávlovna arranged the different groups in her drawing room with her habitual skill. The large group, in which were",
    "drawing room with her habitual skill. The large group, in which were Prince Vasíli and the generals, had the benefit of the diplomat. Another group was at the tea table. Pierre wished to join the former, but Anna Pávlovna—who was in the excited condition of a commander on a battlefield to whom thousands of new and brilliant ideas occur which there is hardly time to put in action—seeing Pierre, touched his sleeve with her finger, saying:"
]

Наконец, мы также можем добавить следующий раздел в нашу функцию main(), чтобы соответствующим образом рассчитать и отобразить оценочные показатели:

...

k_ = 10

D, I = index.search(query_embedding, k=k_)

relevant_docs = [vector_store.docstore[i] for i in I[0]]

reranked_docs, reranked_scores = rerank_with_cross_encoder(user_input, relevant_docs)

top_k_docs = reranked_docs[:k_]  
precision = precision_at_k(top_k_docs, ground_truth_texts, k=k_)
recall = recall_at_k(top_k_docs, ground_truth_texts, k=k_)
f1 = f1_at_k(precision, recall)
hit = hit_rate_at_k(top_k_docs, ground_truth_texts, k=k_)

print("\n--- Retrieval Evaluation Metrics ---")
print(f"Hit@6: {hit}")
print(f"Precision@6: {precision:.2f}")
print(f"Recall@6: {recall:.2f}")
print(f"F1@6: {f1:.2f}")
print("-" * 40)

...

Обратите внимание, что мы запускаем оценку после переранжирования. Поскольку метрики, которые мы рассчитываем — такие как Precision@K, Recall@K и F1@K — не учитывают порядок, оценка их на топ-k извлечённых фрагментах до или после переранжирования даёт одинаковые результаты, если набор топ-k элементов остаётся прежним.

Итак, для нашего вопроса «Кто такая Анна Павловна?» и @k = 10 мы получаем следующие оценки:

  • @k = 10, что означает, что мы рассчитываем все оценочные метрики на топ-10 извлечённых фрагментах.
  • Hit@10 = True, что означает, что хотя бы один из правильных (истинных) фрагментов был найден в топ-10 извлечённых фрагментах.
  • Precision@10 = 0,20, что означает, что из 10 извлечённых фрагментов только 2 были правильными (0,20 = 2/10). Другими словами, искатель также принёс ненужную информацию; только 20% из того, что он извлёк, было действительно полезно.
  • Recall@10 = 0,67, что означает, что мы извлекли 67% всех релевантных фрагментов, доступных в истинной информации, в топ-10 документах.
  • F1@10 = 0,31, что указывает на общее качество извлечения, сочетающее как точность, так и отзывчивость. Оценка F1 в 0,31 указывает на умеренную производительность, и мы знаем, что это связано с приличной отзывчивостью, но низкой точностью.

Как упоминалось ранее, мы можем рассчитать эти метрики для любого k — имеет смысл рассчитывать только для значений k, превышающих количество фрагментов на запрос в истинной информации. Таким образом, мы можем экспериментировать с различными значениями k и понимать, как наша система извлечения работает по мере расширения или сужения области извлечённых результатов.

В заключение

Пока такие метрики, как Precision@K, Recall@K и F1@K, могут быть вычислены для одиночного запроса и соответствующего набора извлечённых фрагментов (как мы это сделали здесь), в реальной оценке они обычно оцениваются по коллекции запросов, известной как тестовый набор. Более точно, каждому запросу в тестовом наборе соответствует свой набор фрагментов истинной информации. Мы затем рассчитываем метрики извлечения индивидуально для каждого запроса, а затем усредняем результаты по всем запросам.

В конечном счёте, понимание значения различных метрик извлечения, которые можно рассчитать, действительно важно для эффективной оценки и настройки конвейера RAG. Самое главное, что эффективный механизм извлечения — поиск соответствующих документов — является основой для генерации осмысленных ответов с помощью настройки RAG.