Как вы 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-7b | 7B | EN | Очень сильная, но требует много VRAM (GPU 24+ GB). |
BAAI/bge-large-en-v1.5 | 0.33B | EN | Хороший баланс качества и скорости, batch size большой. |
intfloat/multilingual-e5-large | 0.33B | 100+ языков | Для мультиязычных доменов. |
sentence-transformers/all-MiniLM-L6-v2 | 0.08B | EN | Лёгкая, быстрая, но базовая точность ниже. |
Для старта рекомендую 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@5 | MRR@10 | Recall@10 |
|---|---|---|---|
Базовая bge-large-en-v1.5 | 0.72 | 0.38 | 0.60 |
| Fine-tuned (2 эпохи, InfoNCE) | 0.86 | 0.52 | 0.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.
Результат:
Почему +15%: модель научилась различать близкие по формулировкам, но различные по сути статьи (например, «гипертония и диабет» vs «гипертоническая болезнь с диабетической нефропатией»).
9. Возможные проблемы и решения
| Проблема | Причина | Решение |
|---|---|---|
| Overfitting | Слишком мало данных, много эпох | Early stopping, dropout 0.1, увеличение датасета аугментациями |
| Data leakage | Positive документ случайно попал в 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. Инструменты и библиотеки
- Sentence-Transformers — основной фреймворк для fine-tuning embedding моделей.
- Hugging Face Transformers — прямой доступ к моделям, кастомные loss.
- PyTorch + Transformers — для продвинутых (e5-mistral-7b).
- Weights & Biases — логирование метрик.
- FAISS — для быстрого поиска после fine-tuning.
Пет-проект для закрепления
Задача: Fine-tune embedding модель под научные статьи (ArXiv) и сравнить с базовой.
Инструменты:
- Python, Sentence-Transformers, Hugging Face.
- Датасет: 10 000 пар (title/abstract — релевантная статья) из kaggle’s ArXiv dataset.
- Hard negatives: baseline
all-MiniLM-L6-v2top-20 нерелевантных.
Шаги:
- Загрузить базовую модель
BAAI/bge-base-en-v1.5. - Подготовить тройки (query = title статьи, positive = abstract, negative = abstract другой статьи, не связанной по категориям).
- Обучить 2 эпохи, batch 64, lr=3e-5, loss = MultipleNegativesRankingLoss.
- Оценить на тестовом сете (1000 запросов): Hit Rate@10, MRR@10.
- Сравнить с базовой моделью.
Ожидаемый результат: улучшение 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? |
Навигация
- Предыдущий: 34
- Следующий: 36
- Индекс: 00. Индекс разборов