Как вы fine-tune embedding модель под свой домен (а не используете готовую)?

Краткий тезис

Fine-tuning embedding модели под узкий домен (медицина, юриспруденция, научные статьи) позволяет существенно повысить качество поиска (recall@k на 10–20%) за счёт адаптации векторных представлений к специфической терминологии и стилю текстов. Основной подход — собрать парный датасет (query, positive, hard negative) и обучить модель с contrastive loss (InfoNCE или Triplet). Ключевые инструменты — библиотеки Sentence-Transformers и Hugging Face, базовые модели — семейства intfloat/e5 или BAAI/bge.


1. Зачем fine-tune embedding модель?

Готовая общая модель (например, all-MiniLM-L6-v2) обучена на разноплановых текстах (Wikipedia, Reddit, новости). В специализированном домене:

  • Терминология отличается: “myocardial infarction” вместо “heart attack”, “res ipsa loquitur” в юриспруденции.
  • Стиль формальный, много аббревиатур (MRI, ECG, GDPR).
  • Семантическое расстояние между похожими по смыслу, но разными по форме документами может быть большим для общей модели.

Fine-tuning «поджимает» эмбеддинги так, что релевантные query–document оказываются ближе в векторном пространстве, а нерелевантные — дальше. Результат — retrieval с меньшим числом пропусков (higher recall) и более точной ранжировкой (higher MRR).


2. Сбор датасета (query, positive, negative)

Для обучения с контрастивными потерями нужны тройки: (anchor, positive, negative). Anchor — это запрос, positive — релевантный документ, negative — нерелевантный (желательно «hard negative», похожий на positive, но не подходящий).

2.1 Источники данных

  • Логи поиска реальной системы: запросы пользователей и клики по документам (implicit feedback).
  • Аннотация экспертами: 100–1000 пар запрос–документ с разметкой релевантности (expensive, но gold standard).
  • Synthetic generation через LLM: “for the query ‘...’, generate a relevant and a non-relevant document”. (осторожно — шум).

2.2 Формат датасета (CSV/JSON)

{"query": "Что такое гибридный артрит?", "positive": "Текст документа про гибридный артрит...", "negative": "Текст про остеоартроз..."}

2.3 Hard negative mining

Используем базовую модель: для каждого запроса берём top-50 результатов, из них выбираем те, которые нерелевантны, но косинусная близость > 0.5 (hard negatives). Можно комбинировать с random negatives (произвольные документы).


3. Выбор базовой модели

МодельРазмерЯзыкиПреимущества
intfloat/e5-mistral-7b7BENОчень сильная, но требует много VRAM (GPU 24+ GB).
BAAI/bge-large-en-v1.50.33BENХороший баланс качества и скорости, batch size большой.
intfloat/multilingual-e5-large0.33B100+ языковДля мультиязычных доменов.
sentence-transformers/all-MiniLM-L6-v20.08BENЛёгкая, быстрая, но базовая точность ниже.

Для старта рекомендую BAAI/bge-large-en-v1.5 (до 512 токенов) или intfloat/e5-mistral-7b если есть ресурсы.


4. Метрика/функция потерь

4.1 Contrastive loss (InfoNCE)

Используется в современном fine-tuning. Для каждой тройки (query, positive, negative) максимизируем вероятность положительной пары:

$$ \mathcal{L} = -\log \frac{ \exp([text](/wiki/text){sim}(q, p) / \tau) }{ \exp([text](/wiki/text){sim}(q, p) / \tau) + \sum_{j=1}^N \exp([text](/wiki/text){sim}(q, n_j) / \tau) } $$

где $\tau$ — temperature (обычно 0.01–0.05), $[text](/wiki/text){sim}$ — косинусное сходство.

4.2 Triplet loss

$$\mathcal{L} = \max(0, [text](/wiki/text){sim}(q, n) - [text](/wiki/text){sim}(q, p) + [text](/wiki/text){margin})$$ Margin (0.2–0.5) — минимальный зазор между сходством положительной и отрицательной пары.

Сравнение:

LossПлюсыМинусы
InfoNCEИспользует все негативы в батче, даёт более стабильное обучениеЧувствителен к размеру батча
TripletПростая реализация, легко интерпретироватьПри фиксированном margin может не обучаться, если негативы слишком лёгкие

На практике чаще выбирают InfoNCE с in-batch negatives (негативы — все positive документы других запросов в батче).


5. Процесс обучения (гиперпараметры, батчи, эпохи)

5.1 Код на Python (Hugging Face + Sentence-Transformers)

from sentence_transformers import SentenceTransformer, losses, InputExample
from torch.utils.data import DataLoader
import torch

# 1. Загрузка базовой модели
model = SentenceTransformer('BAAI/bge-large-en-v1.5')

# 2. Подготовка датасета
train_examples = []
for row in dataset:  # dataset — список словарей
    train_examples.append(InputExample(texts=[row['query'], row['positive'], row['negative']]))
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=32)

# 3. Выбор loss (InfoNCE с in-batch negatives)
train_loss = losses.MultipleNegativesRankingLoss(model)

# 4. Fine-tuning (1-3 эпохи, low learning rate)
model.fit(
    train_objectives=[(train_dataloader, train_loss)],
    epochs=2,
    warmup_steps=100,
    optimizer_params={'lr': 2e-5, 'weight_decay': 0.01},
    show_progress_bar=True,
    fp16=True  # mixed precision для экономии памяти
)

5.2 Ключевые гиперпараметры

  • batch size — как можно больше (ограничение VRAM). 32–128 для bge-large, 16–32 для e5-mistral-7b.
  • learning rate — 1e-5 до 5e-5 (AdamW).
  • warmup steps — 5–10% от общего числа шагов.
  • fp16 — обязателен для больших моделей, снижает потребление памяти вдвое.
  • gradient checkpointing — если не хватает VRAM.

5.3 Число эпох

1–3 эпохи; больше — риск переобучения (overtraining). Используйте early stopping по метрике на валидации.


6. Оценка модели (офлайн метрики)

Оценка проводится на отложенном тестовом наборе с ground truth (релевантные документы для каждого запроса).

МетрикаФормулаСуть
Hit Rate@kДоля запросов, для которых хотя бы один релевантный документ в top-kПроверяет, находит ли retrieval хоть что-то
MRRСреднее обратных рангов первого релевантного документаКак высоко релевантный документ в выдаче
Recall@kДоля найденных релевантных документов от всех релевантныхПолнота поиска
NDCG@kНормализованный дисконтированный кумулятивный выигрышУчитывает порядок и множественную релевантность

Пример оценки (Python):

from sentence_transformers.util import cos_sim, semantic_search
import numpy as np

# Кодируем все документы
doc_emb = model.encode(documents, convert_to_tensor=True)
query_emb = model.encode(queries, convert_to_tensor=True)

hits = []
for q_emb in query_emb:
    results = semantic_search(q_emb, doc_emb, top_k=10)
    hits.append(results[0])
# далее считаем метрики по ground_truth

7. Сравнение: базовая vs fine-tuned

Метод — замер метрик на тестовом сете до и после fine-tuning.

МодельHit Rate@5MRR@10Recall@10
Базовая bge-large-en-v1.50.720.380.60
Fine-tuned (2 эпохи, InfoNCE)0.860.520.75
Прирост+19%+37%+25%

Конкретный пример: медицинский датасет (5000 запросов, 50 000 документов). Fine-tuning на 1000 экспертно размеченных пар дал +15% recall@5.

Вывод: fine-tuning даёт значимый прирост именно в узком домене. Без такой адаптации общая модель может «не понимать» специфичные синонимы и контексты.


8. Практический пример: медицинские документы

Задача: поиск клинических рекомендаций по симптомам.

Датасет: 2000 пар (запрос врача — подходящая статья), hard negatives из того же корпуса.

Базовое решение: intfloat/e5-mistral-7b с zero-shot.

Fine-tuned: 1 эпоха, batch 16, lr=2e-5, InfoNCE.

Результат:

  • Recall@5 вырос с 0.55 до 0.70 (+15%).
  • MRR@10 с 0.32 до 0.44.

Почему +15%: модель научилась различать близкие по формулировкам, но различные по сути статьи (например, «гипертония и диабет» vs «гипертоническая болезнь с диабетической нефропатией»).


9. Возможные проблемы и решения

ПроблемаПричинаРешение
OverfittingСлишком мало данных, много эпохEarly stopping, dropout 0.1, увеличение датасета аугментациями
Data leakagePositive документ случайно попал в negative setТщательная проверка: удалить дубликаты, разбивать по документам, а не по чанкам
High VRAM usageБольшая модель (7B)Использовать LoRA (Low-Rank Adaptation) для embedding моделей (ещё не стандарт, но возможно) или gradient checkpointing
Bad negativesСлишком лёгкие или случайные негативыHard negative mining через базовую модель; доля hard neg в батче 30–50%
Long textsПревышение лимита токенов (512)Chunking документа до 512 токенов и fine-tune на уровне чанков; или использовать LongFormer

10. Инструменты и библиотеки


Пет-проект для закрепления

Задача: Fine-tune embedding модель под научные статьи (ArXiv) и сравнить с базовой.

Инструменты:

  • Python, Sentence-Transformers, Hugging Face.
  • Датасет: 10 000 пар (title/abstract — релевантная статья) из kaggle’s ArXiv dataset.
  • Hard negatives: baseline all-MiniLM-L6-v2 top-20 нерелевантных.

Шаги:

  1. Загрузить базовую модель BAAI/bge-base-en-v1.5.
  2. Подготовить тройки (query = title статьи, positive = abstract, negative = abstract другой статьи, не связанной по категориям).
  3. Обучить 2 эпохи, batch 64, lr=3e-5, loss = MultipleNegativesRankingLoss.
  4. Оценить на тестовом сете (1000 запросов): Hit Rate@10, MRR@10.
  5. Сравнить с базовой моделью.

Ожидаемый результат: улучшение Hit Rate@10 на 10–15% для запросов, содержащих специфичные термины (например, "transformer attention mechanism" → находит именно ту статью, а не общую про нейросети).


Связь с другими вопросами

ВопросТема
34Как выбрать embedding модель для RAG?
36Что такое contrastive learning в контексте эмбеддингов?
37Как оценить качество эмбеддингов без ground truth?
38Как использовать LLM для генерации синтетических датасетов для fine-tuning?
39Как работает SGPT (Sentence GPT) для поиска?
40Как сделать мультиязычный embedding для RAG?

Навигация