Реализовать hard negative mining для retrieval

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Реализовать hard negative mining для retrieval

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

Разработать автоматизированный пайплайн hard negative mining на основе retrieval логов, который извлекает top-10 нерелевантных документов для каждого запроса. Полученные hard negatives используются для дообучения (fine-tune) эмбеддинговой модели, что должно повысить качество поиска. Ключевой результат Recall@10 увеличивается минимум на 10% после fine-tune на валидационном наборе.

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

Что нужноОткуда взять
Retrieval логи (исторические запросы и ранжированные документы с релевантностью)API существующей поисковой системы; при отсутствии — синтетическая генерация
Датасет пар (запрос — релевантный документ) для обученияИз тех же логов или публичный датасет (например, MS MARCO, Natural Questions)
База документов для поиска (корпус)Из логов или датасета
Baseline эмбеддинговая модельSentence-BERT (all-MiniLM-L6-v2) или любая другая из HuggingFace
Валидационный набор с релевантностьюОтложенные запросы из логов (не менее 500)

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

  1. Взять публичный датасет ms-marco (TREC 2019 Deep Learning Track) – скачать коллекцию и запросы.
  2. Создать синтетические логи: закодировать все запросы и документы baseline моделью, выполнить ANN поиск (FAISS), записать топ-100 для каждого запроса с бинарной меткой релевантности (из датасета).
  3. Сгенерировать «логи» в формате CSV: query_id, doc_id, relevance_score(0/1).
  4. Разбить на train/val по запросам (80/20).

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

КомпонентИнструментыНазначение
Эмбеддинговая модельsentence-transformers (all-MiniLM-L6-v2)Получение векторных представлений запросов и документов
Поиск ближайших соседейfaiss (IndexFlatIP)Быстрый ANN поиск для эмуляции retrieval логов
Обработка данныхpandas, numpyРабота с таблицами, метриками
Fine-tuningsentence-transformers + torchДообучение модели с triplet loss (hard negatives)
Логированиеwandb или tensorboardОтслеживание метрик обучения
Оценкаpytrec-eval или самописный скриптВычисление Recall@k

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

Этап 1: Подготовка данных и симуляция логов (оценка времени: 4 ч)

Действия

  1. Установить зависимости: pip install sentence-transformers faiss-cpu pandas numpy torch.
  2. Скачать датасет MS MARCO (passage ranking): wget коллекцию документов и запросов с релевантностями.
    • Использовать только один сплит (train/val).
  3. Загрузить baseline модель: model = SentenceTransformer('all-MiniLM-L6-v2').
  4. Закодировать все документы и запросы (batch size 512, show_progress).
  5. Построить FAISS индекс: index = faiss.IndexFlatIP(384) (размерность модели), добавить эмбеддинги документов.
  6. Для каждого запроса из тренировочной выборки выполнить поиск по всем документам, получить топ-100 индексов и расстояний.
  7. Построить датасет retrieval логов: для каждого запроса определить релевантные документы (из ground truth). Остальные — нерелевантные. Создать CSV:
    logs = []
    for qid, q_emb in zip(query_ids, query_embs):
        scores, idxs = index.search(q_emb[np.newaxis,:], 100)
        for score, doc_idx in zip(scores[0], idxs[0]):
            rel = 1 if doc_idx in relevant[qid] else 0
            logs.append({'query_id': qid, 'doc_id': doc_idx, 'score': float(score), 'relevance': rel})
    pd.DataFrame(logs).to_csv('retrieval_logs.csv', index=False)
    
  8. Выделить валидационную выборку: случайно выбрать 20% запросов, их логи переместить в отдельный файл val_logs.csv.

Ожидаемый результат этапа файлы retrieval_logs_train.csv и retrieval_logs_val.csv, готовые к анализу hard negatives.

Этап 2: Реализация hard negative mining (оценка времени: 3 ч)

Действия

  1. Загрузить тренировочные логи и сгруппировать по query_id.
  2. Для каждого запроса из логов, где есть хотя бы один релевантный документ:
    • Отфильтровать нерелевантные (relevance == 0).
    • Отсортировать по возрастанию score (чем выше score, тем «ближе» нерелевантный документ к запросу в embedding space). Нам нужны top-10 нерелевантных с наибольшим сходством (hard negatives), поэтому сортируем по убыванию score.
    • Взять первые 10 (если меньше – все доступные).
  3. Сохранить тройки (query_id, positive_doc_id, hard_negative_doc_id) в CSV:
    hard_neg_pairs = []
    for qid, group in logs.groupby('query_id'):
        rel_docs = group[group['relevance'] == 1]['doc_id'].tolist()
        hard_neg = group[group['relevance'] == 0].nlargest(10, 'score')['doc_id'].tolist()
        for pos in rel_docs:
            for neg in hard_neg:
                hard_neg_pairs.append({'query_id': qid, 'positive': pos, 'hard_negative': neg})
    pd.DataFrame(hard_neg_pairs).to_csv('hard_negatives.csv', index=False)
    
  4. Дополнительно сэмплировать случайные negatives (easy negatives) для каждого запроса (например, 5 штук) – для стабильности обучения.
  5. Объединить hard и easy negatives в итоговый датасет fine-tune.

Ожидаемый результат этапа файл training_triplets.csv с колонками query_id, positive, hard_negative, и дополнительный файл с easy negatives.

Этап 3: Fine-tune эмбеддинговой модели (оценка времени: 5 ч)

Действия

  1. Загрузить обучение данные: train_df = pd.read_csv('training_triplets.csv').
  2. Использовать sentence-transformers модель all-MiniLM-L6-v2 как базовую.
  3. Создать датасет TripletDataset:
    from torch.utils.data import DataLoader
    class TripletDataset(torch.utils.data.Dataset):
        def __init__(self, df, queries, docs):
            self.queries = queries  # dict {id: text}
            self.docs = docs
            self.triplets = [(row['query_id'], row['positive'], row['hard_negative']) for _, row in df.iterrows()]
        def __getitem__(self, idx):
            q_id, p_id, n_id = self.triplets[idx]
            return self.queries[q_id], self.docs[p_id], self.docs[n_id]
    
  4. Настроить loss: TripletLoss (distance metric = cosine), margin=0.5.
  5. Обучать 2 эпохи, batch size 32, learning rate 2e-5, веса адаптера сохранять после каждой эпохи.
    • Использовать wandb для логирования triplet loss.
  6. После финальной эпохи загрузить лучшую модель (по loss на валидации, если есть) и сохранить: model.save('fine_tuned_model').

Ожидаемый результат этапа папка fine_tuned_model с конфигом и весами, файл training_metrics.json с loss.

Этап 4: Оценка Recall@10 и сравнение с baseline (оценка времени: 2 ч)

Действия

  1. Загрузить baseline модель и валидационные данные.
  2. Закодировать все документы и запросы из val двумя моделями (baseline и fine-tuned).
  3. Построить два FAISS индекса и выполнить search топ-10 для каждого валидационного запроса.
  4. Вычислить Recall@10:
    def recall_at_k(q_emb, doc_embs, ground_truth, k=10):
        index = faiss.IndexFlatIP(doc_embs.shape[1])
        index.add(doc_embs)
        _, I = index.search(q_emb, k)
        recall = 0
        for i, (gt, preds) in enumerate(zip(ground_truth, I)):
            relevant = len(set(gt) & set(preds))
            recall += relevant / len(gt)
        return recall / len(ground_truth)
    
  5. Сравнить Recall@10 для baseline и fine-tuned модели.
  6. Убедиться, что улучшение >= 10% (относительно baseline). Если нет — повторить обучение с другими hyperparams.

Ожидаемый результат этапа таблица сравнения метрик, вывод о достижении +10%.

Этап 5: Документирование и упаковка (оценка времени: 2 ч)

Действия

  1. Написать README.md с описанием пайплайна, инструкцией по запуску, результатами.
  2. Упаковать код в структуру:
    hard_neg_mining/
    ├── data/ (исключено .gitkeep)
    ├── scripts/
    │   ├── simulate_logs.py
    │   ├── mine_hard_negatives.py
    │   ├── train.py
    │   └── evaluate.py
    ├── requirements.txt
    └── README.md
    
  3. Создать requirements.txt.
  4. Убедиться, что все скрипты запускаются из корня (python scripts/simulate_logs.py).

Ожидаемый результат этапа репозиторий с кодом, документацией и финальной моделью.

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

  • Retrieval логи сгенерированы (или использованы реальные) и содержат минимум 10 000 запросов.
  • Датасет hard negatives содержит не менее 100 000 триплетов (запрос‑позитив‑харднегатив).
  • Baseline модель способна выдать Recall@10 на валидации (зафиксировано значение).
  • Fine-tuned модель обучена (2 эпохи) и сохранена.
  • Recall@10 после fine-tune улучшился не менее чем на 10% относительно baseline.
  • Результаты измерений оформлены в виде таблицы и графика (loss, recall).
  • Код читаемый, с комментариями, воспроизводится с нуля через pip install -r requirements.txt && python scripts/*.py.

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

  • Файлы hard_negatives.csv, training_triplets.csv (с easy negatives), fine_tuned_model/ , scripts/, README.md.
  • Содержание полный пайплайн hard negative mining + fine-tuning + evaluation.
  • Опционально веса модели загружены на HuggingFace Hub, скрипт для инференса.

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

СложностьРешение
Нет реальных логовСимуляция с FAISS и публичным датасетом, как описано в Этапе 1.
Hard negatives оказываются слишком «легкими» (низкое сходство)Проверить распределение score; если среднее сходство < 0.3 — увеличить число top-k (брать не 10, а 50) и отбирать по порогу. Либо использовать max_score - 0.2 динамический порог.
Triplet loss не сходится или loss растетУменьшить learning rate, увеличить margin, добавить warmup. Проверить, что hard negatives действительно hard (среднее сходство >= 0.7 от сходства позитива).
Долгое индексирование при большом корпусеИспользовать FAISS с IVF или HNSW, уменьшить размерность (PCA), или работать с репрезентативным подмножеством.
Discrepancy между эмуляцией и реальным retrievalПосле внедрения на реальные логи перекалибровать параметры mining на реальных данных.

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

ЭтапВремя
Этап 1: Подготовка данных и симуляция логов4 ч
Этап 2: Реализация hard negative mining3 ч
Этап 3: Fine-tune модели5 ч
Этап 4: Оценка Recall@10 и сравнение2 ч
Этап 5: Документирование и упаковка2 ч
Итого16 ч

Примечание Для первого выполнения рекомендуется заложить +20% времени на отладку и итерации по подбору параметров (всего ~20 ч).

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

ВопросТема
12Что такое hard negative mining и зачем он нужен в retrieval?
47Как оценить качество retrieval (Recall@k, MRR)?
88Sentence-BERT: архитектура и варианты fine-tuning
123Triplet loss vs Cross-Entropy loss для обучения эмбеддингов
156Использование FAISS для быстрого ANN поиска
189Методы аугментации данных для улучшения robustness модели
234Подготовка triplet-датасета из логов поиска
289Влияние hard negatives на качество fine-tuning: эмпирические наблюдения
315Bias в retrieval логах и как его компенсировать
398Сравнение онлайн и оффлайн hard negative mining

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

  • Я сгенерировал/получил retrieval логи в формате CSV с полями query_id, doc_id, score, relevance.
  • Я убедился, что в логах для каждого запроса есть хотя бы один релевантный документ.
  • Я написал скрипт mine_hard_negatives.py, который корректно выбирает топ-10 нерелевантных по убыванию score и формирует триплеты.
  • Я провёл fine-tune модели с TripletLoss и проверил, что loss снижается на протяжении обучения.
  • Я вычислил Recall@10 для baseline и fine-tuned модели на одном и том же наборе запросов и убедился, что улучшение ≥ 10% (или понял, почему нет, и изменил параметры).