Создать benchmark для retrieval

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Создать benchmark для retrieval

1. Цель задачи

Спроектировать и реализовать эталонный набор данных (benchmark) для оценки качества retrieval-системы в предметной области (например, техническая документация или научные статьи). Необходимо подготовить 500 запросов (query) с эталонными документами (gold documents) и запустить baseline-модель (BM25) для подтверждения метрики MRR@10 > 0.85. Benchmark должен быть пригоден для автоматического прогона и сравнения разных retrieval-методов.

Ключевой результат файл benchmark.parquet с колонками query_id, query_text, gold_doc_ids (список ID релевантных документов) и скрипт оценки, который воспроизводит MRR@10 на baseline.

2. Исходные данные

Что нужноОткуда взять
Корпус документов (минимум 10 000 шт.)Открытый датасет: wiki_corpus из библиотеки datasets (subset wikitext-2-raw-v1) или предварительно собранная коллекция технических статей (например, c4 с фильтрацией по домену en.wikipedia.org).
Генерация запросовИспользовать LLM (GPT-4o mini или локальную модель типа mistral-7b) для синтеза запросов на основе случайных документов.
Эталонные документыДля каждого запроса взять 1-3 исходных документа, из которых сгенерирован запрос, и добавить до 5 документов, семантически близких (находятся в одном кластере эмбеддингов).

Если нет реального инструмента — симулируем:

  1. Скачайте датасет wikitext через datasets.load_dataset("wikitext", "wikitext-2-raw-v1").
  2. Выберите 10 000 статей (текст в text), очистите от спецсимволов, нормализуйте пробелы.
  3. Разбейте каждую статью на чанки по 150-200 токенов (через nltk.tokenize). Получится ~1000 документов (можно увеличить повторным чанкингом).
  4. Для генерации запросов используйте OpenAI API (или заглушку: из каждого документа берите первые 20 токенов как «золотой запрос» — этого достаточно для проверки метрик, но в реальной задаче лучше синтезировать).

3. Технологический стек

КомпонентИнструментыНазначение
ЯзыкPython 3.10+Основной язык разработки
Работа с даннымиpandas, numpy, datasetsЗагрузка, обработка, хранение benchmark
NLPnltk, transformers, sentence-transformersТокенизация, эмбеддинги для кластеризации
Retrieval baselinerank_bm25 (BM25 Okapi)Базовая модель для верификации метрики
Метрикиranx или собственный расчёт MRR@10Оценка качества retrieval
Генерация запросовOpenAI API (gpt-4o-mini) или локальная модель (mistral-7b через llama.cpp)Синтез запросов на основе документов
Версионированиеgit, dvc (опционально)Версии данных и кода
Визуализацияmatplotlib, seabornГрафики распределения длин запросов / документов

4. Этапы выполнения

Этап 1: Подготовка корпуса документов (2 часа)

Действия

  1. Загрузите датасет wikitext-2-raw-v1 с помощью datasets.load_dataset.
    from datasets import load_dataset
    dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
    
  2. Отфильтруйте строки text длиной менее 50 символов (очистка).
  3. Разбейте каждую строку на чанки по 150-200 токенов (используйте nltk.word_tokenize). Сохраните метаданные: doc_id, chunk_index, text.
  4. Объедините чанки в финальные документы (каждый чанк = отдельный документ). Получите N документов (минимально 10 000, но для экономии времени можно 5 000).
  5. Сохраните корпус в формате Parquet: corpus.parquet со столбцами doc_id, text.

Ожидаемый результат этапа corpus.parquet с 5 000+ документов, размером ~20 MB.

Этап 2: Генерация 500 запросов с gold документами (4 часа)

Действия

  1. Из корпуса случайно выберите 500 документов (или 500 групп по 2-3 документа).
  2. Для каждого выбранного документа (группы) сформируйте запрос с помощью LLM. Пример промпта:

    "Based on the following text, generate a natural language query that a user would ask to retrieve this exact text. Output only the query." Text: <...>

  3. Используйте API OpenAI: openai.ChatCompletion.create(...) с моделью gpt-4o-mini, temperature=0.3, max_tokens=50.
  4. Для экономии токенов можно сгенерировать запросы батчами по 20 штук (конкатенация).
  5. Для каждого запроса определите gold документы:
    • Основной: документ, из которого сгенерирован запрос.
    • Дополнительные: 2-5 документов, найденных по эмбеддинговой близости (используйте sentence-transformers/all-MiniLM-L6-v2, k-means кластеризация с k=100; для каждого запроса выберите документы из того же кластера).
  6. Проверьте, что для каждого запроса есть хотя бы 1 gold документ. При необходимости повторите генерацию.
  7. Сформируйте таблицу benchmark.parquet:
    df = pd.DataFrame({
        "query_id": range(1, 501),
        "query_text": queries,
        "gold_doc_ids": gold_ids  # список строковых ID
    })
    df.to_parquet("benchmark.parquet")
    

Ожидаемый результат этапа benchmark.parquet с 500 запросами и списками gold документов.

Этап 3: BaselineBM25 и расчёт MRR@10 (2 часа)

Действия

  1. Установите rank_bm25 и nltk.

  2. Токенизируйте корпус документов (все тексты) через nltk.word_tokenize.

  3. Постройте BM25 индекс:

    from rank_bm25 import BM25Okapi
    corpus_tokenized = [nltk.word_tokenize(doc) for doc in corpus['text']]
    bm25 = BM25Okapi(corpus_tokenized)
    
  4. Для каждого запроса из benchmark получите топ-10 документов по BM25:

    query_tokenized = nltk.word_tokenize(query)
    scores = bm25.get_scores(query_tokenized)
    top10 = np.argsort(scores)[-10:][::-1]  # индексы документов
    
  5. Рассчитайте MRR@10:

    mrr = 0.0
    for i in range(len(queries)):
        gold_set = set(gold_doc_ids[i])
        for rank, doc_id in enumerate(top10_doc_ids[i], start=1):
            if doc_id in gold_set:
                mrr += 1.0 / rank
                break
    mrr /= len(queries)
    
  6. Выведите MRR@10. Если значение > 0.85 — цель достигнута. Если нет — проверьте качество генерации запросов (возможно, gold документы слишком специфичны).

Ожидаемый результат этапа значение MRR@10 > 0.85 и скрипт evaluate_bm25.py, который воспроизводит расчёт.

Этап 4: Документирование и упаковка (1 час)

Действия

  1. Создайте в корне репозитория файл README.md с описанием benchmark: формат данных, способ генерации, метрики.
  2. Напишите скрипт run_benchmark.sh, который последовательно запускает: загрузку корпуса, генерацию (или загрузку готового benchmark), оценку BM25.
  3. Добавьте requirements.txt с зависимостями: pandas, datasets, rank_bm25, nltk, openai, sentence-transformers.
  4. Убедитесь, что все этапы воспроизводимы (зафиксируйте случайные seed random.seed(42)).

Ожидаемый результат этапа README.md, requirements.txt, run_benchmark.sh.

Этап 5: Валидация на альтернативной модели (опционально, 1 час)

Действия

  1. Возьмите простую Dense Retrieval модель (например, all-MiniLM-L6-v2 через sentence-transformers).
  2. Для каждого запроса вычислите эмбеддинг и получите косинусную близость ко всем документам.
  3. Рассчитайте MRR@10.
  4. Сравните с BM25. Ожидается, что dense модель покажет MRR@10 > 0.9 (из-за кластеризации).

Ожидаемый результат этапа Сравнительная таблица метрик (BM25 vs Dense).

5. Критерии приемки (Definition of Done)

  • Собран корпус из минимум 5 000 документов (загружен и обработан).
  • Сгенерировано ровно 500 уникальных запросов, каждый содержит список gold документов (1-5 шт).
  • Все данные сохранены в Parquet с чёткой схемой (query_id, query_text, gold_doc_ids; doc_id, text).
  • Baseline BM25 воспроизводимо даёт MRR@10 > 0.85 (зафиксирован seed).
  • Скрипт evaluate_bm25.py принимает пути к корпусу и benchmark и выводит MRR@10.
  • Документация (README) описывает формат benchmark, способ генерации и пример использования.
  • Репозиторий содержит requirements.txt и run_benchmark.sh для полного воспроизведения.

6. Ожидаемый результат

Основной артефакт каталог benchmark/ с файлами:

  • corpus.parquet — корпус документов.
  • benchmark.parquet — 500 запросов с gold документами.
  • evaluate_bm25.py — скрипт расчёта MRR@10.
  • run_benchmark.shshell-скрипт полного пайплайна.
  • README.md — документация.
  • requirements.txt.

Дополнительные результаты (опционально)

  • Сравнительная таблица MRR@10 для BM25 и Dense (в results/).
  • Графики распределения длины запросов и количества gold документов.

7. Возможные сложности и их решение

СложностьРешение
LLM генерирует запросы, не соответствующие исходному документу (халлюцинации)Использовать промпт с инструкцией «Только запрос, без дополнительного текста»; добавить пост-проверку: запрос не должен содержать прямых цитат длиннее 10 слов.
Gold документы в benchmark могут не находиться BM25 (например, из-за разной лексики)Уменьшить количество золотых документов до 1 (самого исходного), либо генерировать запросы с включением ключевых слов из документа.
Не хватает документов в корпусе для 500 запросовИспользовать дополнительный датасет (например, squad контексты) или генерировать запросы из одного документа с разными формулировками.
Высокая стоимость API OpenAIИспользовать локальную модель (например, mistral-7b через llama-cpp-python); или сгенерировать синтетические запросы путём извлечения первых 10-20 слов документа.
BM25 не даёт MRR@10 > 0.85Проверить, не слишком ли широкая формулировка запросов; увеличить число gold документов до 3-5 (BM25 лучше находит поверхностные совпадения).

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Подготовка корпуса2 часа
Этап 2: Генерация 500 запросов4 часа
Этап 3: Baseline BM25 + MRR2 часа
Этап 4: Документирование1 час
Этап 5: Валидация (опционально)1 час
Итого10 часов

Примечание Для первого выполнения (с настройкой окружения, отладкой) закладывайте дополнительно 2-3 часа.

9. Связанные вопросы из базы знаний

ВопросТема
12Как выбрать метрику для задачи retrieval?
47Принципы построения benchmark-датасетов
103Разница между BM25 и Dense Retrieval
201Работа с библиотекой datasets (Hugging Face)
275Расчёт MRR и MAP в ранжировании
310Оптимизация индексации для больших корпусов
412Генерация синтетических данных с помощью LLM
589Проверка воспроизводимости экспериментов
703Форматы хранения данных: Parquet vs JSON
888A/B тестирование retrieval-моделей

10. Чек-лист самопроверки

  • Я загрузил и подготовил корпус документов (проверил количество и размер чанков).
  • Для каждого из 500 запросов есть хотя бы один gold документ, уникальный ID запроса.
  • Я зафиксировал seed 42 во всех местах, где есть случайность (выбор документов, split).
  • Скрипт evaluate_bm25.py запускается без ошибок и выдаёт MRR@10 > 0.85.
  • README содержит инструкцию по запуску с нуля (включая установку зависимостей).