Pull to refresh

Как сделать чат-бота лучше, нужен всего лишь простой советский… RAGAS

Reading time8 min
Views5.4K

В вводной части обзора мы познакомились с концепцией Retrieval Augmented Generation (RAG) и её расширением через методологию RAGAS (Retrieval Augmented Generation Automated Scoring). Мы разобрались, как RAGAS подходит к процессу оценки эффективности и точности RAG-систем.

Если после перечисления всех этих аббревиатур вам вдруг резко захотелось прекратить чтение, то не торопитесь. Всё это необходимо для разработки качественных чат-ботов. Но что на самом деле означает "качественный", и как можно измерить это качество?

Обычно оценка качества производится путём анализа обратной связи от пользователей, либо пользователь голосует рублем. Допустим, вы разработали чат-бота и обнаружили, что юзеры не в восторге от его ответов. Вы вносите изменения, например, заменяете одну LLM на другую и надеетесь, что теперь-то ответы всех устроят. Это можно сделать ещё более умно через A/B-тестирование. Но можно ли ускорить релизный цикл, заранее оценив влияние внесённых изменений? RAGAS как раз предлагает ответ на этот вопрос.

В этой части мы более подробно рассмотрим техническую сторону RAGAS. Как обычно, начнем с более простых и интуитивно понятных примеров, потом перейдем к более сложным сценариям. Статья будет сопровождаться примерами кода на python 3.11. Подразумевается, что мы стартуем в новом окружении (я использую conda).

RAG
RAG

Разбираемся в БАЗЕ

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

Для начала установим библиотеку:

pip install git+https://github.com/explodinggradients/ragas.git

Под капотом будут работать модельки OpenAI, поэтому нужно будет установить ключ, если вы работаете из России (или другого неугодного региона), то следует также прописать прокси:

import os

os.environ['http_proxy'] = "http://****:***@**.**.**.**:****"  
os.environ['HTTP_PROXY'] = "http://****:***@**.**.**.**:****"  
os.environ['https_proxy'] = "http://****:***@**.**.**.**:****"  
os.environ['HTTPS_PROXY'] = "http://****:***@**.**.**.**:****"

os.environ["OPENAI_API_KEY"] = "sk-0sE3fv***"

Далее можно скачать специально подготовленный датасет для вычисления метрик:

from datasets import load_dataset  
  
ragas_eval = load_dataset('explodinggradients/fiqa', 'ragas_eval')
ragas_eval_pd = fiqa_eval['baseline'].to_pandas()

ragas_eval_pd.head()

Датасет содержит вопросы, ответы, эталонные ответы (ground truths), а также контекст, на основе которого ваша языковая модель должна сформулировать ответ. Контекст может включать не один документ, а сразу несколько, что зависит от настроек конкретного ретривера. Основной вопрос, конечно, заключается в получении ground truths. В самом фреймворке это реализовано с использованием наиболее мощной модели OpenAI — gpt-4. То есть, эти эталонные ответы создаются синтетически (на самом деле и вопросы будут создаваться синтетически). По сути, вы будете стремиться приблизить ответы более слабой модели (например, LLaMa) к ответам более сильной. Вы также можете добавить в этот датасет примеры, размеченные вручную.

Попробуем вычислить наши метрики на этом датасете:

from ragas.metrics import (
   answer_relevancy,
   faithfulness,
   context_recall,
   context_precision,
)
from ragas import evaluate

result = evaluate(
   ragas_eval['baseline'].select(range(5)),
   metrics=[
       context_precision,
       context_recall,
       faithfulness,
       answer_relevancy,
   ]
).to_pandas()
Считаем метрики RAGAS
Считаем метрики RAGAS

Цифры получены, осталось лишь понять, что они означают.

В RAG-пайплайне условно можно выделить две базовые части: ретривер и языковую модель генерации. Ретривер отвечает за формирование контекста, ну а языковая модель уже генерирует на его основе ответ. Логично оценивать эти два этапа независимо. За качество ретривера в RAGAS отвечают метрики context precision и context recall. Давайте подробно разберемся в каждой из них.

context precision

context precision
context precision
  • Цель состоит в количественной оценке точности извлечённого контекста. Это помогает оптимизировать размер блоков данных.

  • Процесс включает в себя идентификацию и извлечение предложений из данного контекста, которые необходимы для ответа на заданный вопрос.

  • Итоговая оценка рассчитывается как отношение числа извлечённых предложений к общему числу предложений в данном контексте.

Если заглянуть под капот вычислений этой метрики, то, во первых, мы увидим, что true_positives и false_positives определяются LLM. Далее метрика учитывает не только количество правильных ответов, но и их порядок. У нас формируется verdict_list - список оценок, где 1 означает полезный контекст.

Расчет context precision:

verdict_list = [1, 0, 1, 1]

  • Первая оценка "1": Сумма до первого элемента включительно (1) / 1 (позиция) = 1.

  • Вторая оценка "0": Не учитывается, так как оценка "0".

  • Третья оценка "1": Сумма до третьего элемента включительно (2) / 3 (позиция) = 2/3.

  • Четвертая оценка "1": Сумма до четвертого элемента включительно (3) / 4 (позиция) = 3/4.

  • Числитель: 1 + 0 + 2/3 + 3/4 = 29/12

  • Знаменатель: 1 + 0 + 1 + 1 = 3

  • Значение метрики: 29/36 = 0.81

context recall

context recall
context recall
  • Эта метрика оценивает, насколько хорошо каждое предложение в ответе может быть отнесено к данному контексту.

  • Итоговая оценка рассчитывается как отношение TP к сумме TP и FN (TP / (TP + FN)).

В качестве иллюстрации расчета я возьму пример из документации

Контекст:

"Альберт Эйнштейн (14 марта 1879 года – 18 апреля 1955 года) был немецким теоретическим физиком, широко считающимся одним из величайших и самых влиятельных ученых всех времен. Наиболее известен разработкой теории относительности, он также внес важный вклад в квантовую механику и был центральной фигурой в революционном переосмыслении научного понимания природы, которое современная физика осуществила в первые десятилетия двадцатого века. Его формула эквивалентности массы и энергии E = mc², возникшая из теории относительности, была названа 'самым известным уравнением в мире'. Он получил Нобелевскую премию по физике 1921 года 'за его заслуги перед теоретической физикой и, особенно, за его открытие закона фотоэлектрического эффекта', важный этап в разработке квантовой теории. Его работа также известна своим влиянием на философию науки. В опросе 130 ведущих физиков мира, проведенном в 1999 году британским журналом Physics World, Эйнштейн был признан величайшим физиком всех времен. Его интеллектуальные достижения и оригинальность сделали Эйнштейна синонимом гения".

Вопрос:

Что вы можете рассказать мне об Альберте Эйнштейне?

GT:

"Альберт Эйнштейн, родившийся 14 марта 1879 года, был немецким теоретическим физиком, широко признанным одним из величайших и самых влиятельных ученых всех времен. Он получил Нобелевскую премию по физике в 1921 году за свои заслуги в области теоретической физики. Он опубликовал 4 статьи в 1905 году. Эйнштейн переехал в Швейцарию в 1895 году".

  1. Утверждение о дате рождения и вкладе в физику:

    • Соответствие: "1" (прямо упоминается в контексте).

  2. Утверждение о Нобелевской премии:

    • Соответствие: "1" (прямо упоминается в контексте).

  3. Утверждение о публикации работ в 1905 году:

    • Соответствие: "0" (не упоминается в контексте).

  4. Утверждение о переезде в Швейцарию:

    • Соответствие: "0" (не упоминается в контексте).

Таким образом только половина утверждений ответа соответствует информации контекста, поэтому значение метрики будет 0.5.

Теперь, когда мы разобрались с расчётом метрик, возникает вопрос об их значимости для оценки финального ответа. Означает ли низкое значение context precision, что с нашим ретривером что-то не так? На самом деле не все так однозначно. Например, низкая метрика может указывать на то, что собранный контекст слишком обширен для корректного ответа на вопрос, хотя модель всё равно выдаёт правильный ответ. В этом случае можно попытаться сократить контекст, например, выбирая не пять, а три топовых документа. Если вы используете платную модель, то таким образом можно сэкономить и на токенах. Аналогичная ситуация может быть и с context recall. Возможно, проблема кроется в неправильно размеченных ground truth данных, когда в корпусе вообще отсутствует информация, соответствующая эталонным ответам.

Давайте теперь посмотрим, как настройки ретривера могут влиять на эти метрики.

Тестируем ретривер

В качестве примеров документов я возьму свои статьи про langchain, тем более, что мы будем активно использовать эту библиотеку для тестирования элементов RAG.


from langchain.document_loaders import WebBaseLoader 

from langchain_community.chat_models import ChatOpenAI  
from langchain.chains import RetrievalQA  

from langchain.vectorstores import FAISS  
from langchain.embeddings import OpenAIEmbeddings  
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Загружаем статьи
habr_loader = WebBaseLoader(  
    ['https://habr.com/ru/articles/729664/',  
     'https://habr.com/ru/articles/733262/',  
     'https://habr.com/ru/articles/734146/',  
     'https://habr.com/ru/articles/735920/'  
     ]  
)

habr_docs = habr_loader.load()

Далее построим индекс на основе этого корпуса.

# используем вектора OpenAI
embeddings_model = OpenAIEmbeddings()  

# разбиваем документ на чанки
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500)  
chunks = text_splitter.split_documents(habr_docs)  

# индексируем в векторном хранилище
vectorstore = FAISS.from_documents(chunks, embeddings_model)

llm = ChatOpenAI(model_name='gpt-3.5-turbo-1106')

# задаем параметры ретривера
qa_chain = RetrievalQA.from_chain_type(  
    llm,  
    retriever=vectorstore.as_retriever(search_kwargs={'k': 5}),  
    return_source_documents=True  
)

result = qa_chain({'query': 'Что такое langchain?'})
RAG в действии
RAG в действии

Создаем синтетические данные

Для тестирования ретривера (а позже и языковой модели LLM), нам необходимо создать синтетические данные. Это можно сделать вручную, однако в самом фреймворке предусмотрена функция автоматической генерации таких данных. Автоматическая генерация включает создание вопросов различных типов — от простых и прямых до сложных многошаговых, требующих глубокого понимания контекста.

Запустим код генерации:

from ragas.testset import TestsetGenerator

# нужно добавить поле file_name, иначе генерация не запустится
for d in habr_docs:  
	d.metadata['file_name'] = d.metadata['title']


testsetgenerator = TestsetGenerator.from_default()  
test_size = 50  # устанавливаем небольшой размер, тк генерация ест токены

testset = testsetgenerator.generate(habr_docs, test_size=test_size)

testset_pd = testset.to_pandas()
testset_pd.head()
Генерация синтетики
Генерация синтетики

Сразу становится заметно, что все вопросы и ответы созданы на английском языке. Это обусловлено тем, что промпты для генерации оптимизированы именно под английский. Как писал классик, получилась смесь французского с нижегородским, но пока оставим параметры генерации как есть.

Если говорить про логику создания синтетики, то она примерно следующая:

  1. Из доступных узлов выбирается узел для генерации вопроса.

  2. Оценивается, подходит ли контекст для формирования вопроса.

  3. Генерируются начальные вопросы (seed question), которые затем могут быть переформулированы или изменены в соответствии с определенными правилами.

  4. Вопросы фильтруются и, при необходимости, переформулируются.

  5. Вопросы классифицируются в соответствии с их типом (например, условные вопросы, вопросы с множественным контекстом и т.д.).

  6. Сформированные вопросы и их контексты упаковываются в формат, который подходит для тестирования.

TestsetGenerator
TestsetGenerator

Можно увидеть, как были переформулированы начальные вопросы:

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

Пока этот датасет не готов для подачи в ragas, нужны небольшие корректировки.

import pandas as pd  
from datasets import Dataset  
from tqdm import tqdm  
  
from ragas.metrics import (  
    context_recall,  
    context_precision,  
    answer_relevancy,  
    faithfulness,  
)  
  
from ragas import evaluate

# Ответ нашей LLM (gpt-4) возьмем за ground truth
validation_set = testset_pd[['question', 'answer']].rename(columns={'answer' : 'ground_truth'})

# создаем датасет для подачи в ragas
def create_data_for_ragas(qa_chain, eval_dataset):  
  rag_dataset = []  
  for row in tqdm(eval_dataset):  
    answer = qa_chain({'query' : row['question']})  
    rag_dataset.append(  
        {"question" : row['question'],  
         "answer" : answer['result'],  
         "contexts" : [context.page_content for context in answer['source_documents']],  
         "ground_truths" : [row['ground_truth']]  
         }    )  rag_df = pd.DataFrame(rag_dataset)  
  rag_eval_dataset = Dataset.from_pandas(rag_df)  
  return rag_eval_dataset

qa_ragas_baseline = create_data_for_ragas(qa_chain, eval_dataset)

baseline_result = evaluate(  
  qa_ragas_baseline,  
  metrics=[  
      context_precision,  
      context_recall,  
      faithfulness,  
      answer_relevancy,  
  ]
  )
Результаты базового ретривера
Результаты базового ретривера

Что ж, теперь представим, что у нас возникла идея подавать в языковую модель более короткий контекст (не top 5 чанков, а 3.) Посмотрим, как это скажется на наших метриках.

qa_chain_short = RetrievalQA.from_chain_type(  
    llm,  
    retriever=vectorstore.as_retriever(search_kwargs={'k': 3}),  
    return_source_documents=True  
)

qa_ragas_short = create_data_for_ragas(qa_chain_short, eval_dataset)

short_retrieval_result = evaluate(  
  qa_ragas_short,  
  metrics=[  
      context_precision,  
      context_recall,  
      faithfulness,  
      answer_relevancy,  
  ]
  )
Ретривер с более коротким контекстом
Ретривер с более коротким контекстом

Теперь мы видим, что немного подрос скор по context_precision, что выглядит вполне логичным, но при этом у нас просели метрики faithfulness, answer_relevancy. Почему так произошло, обсудим уже в следующей части обзора.

Спасибо за внимание!

Пишу про AI и NLP в телеграм.

Tags:
Hubs:
Total votes 13: ↑13 and ↓0+13
Comments1

Articles