Проблема: ваш поиск в документах сосёт. А данные стоят как Ferrari
Возьмем реальный сценарий. У вас 50 тысяч страниц внутренней техдокументации, медицинских исследований или спецификаций к чипам. Общая модель вроде BGE или text-embedding-3 на них спотыкается. Она не понимает ваши доменные термины, аббревиатуры, жаргон. Релевантность поиска падает до неприличных 30-40%.
Классическое решение? Дообучить эмбеддинги на своих данных. И вот тут начинается ад. Нужны тысячи, а лучше десятки тысяч размеченных пар «запрос-релевантный документ». Размечать вручную — месяцы работы и сотни тысяч рублей. Аутсорсить — дорого и долго. Парадокс: чтобы улучшить поиск, нужны данные, которые можно получить только… с помощью хорошего поиска.
Кейс Atlassian 2025 года: они дообучили эмбеддинги для поиска по своей техдокументации с помощью синтетических данных от NVIDIA. Результат — рост точности (NDCG@10) на 26%. И ни одного ручного примера. Вот о чем мы сегодня.
Решение: генерация данных нейросетью и автоматический отбор сложных примеров
NVIDIA в 2025-2026 выкатила полный стек инструментов, который превращает этот процесс из научной работы в инженерную рутину. Основа — два компонента:
- NeMo Data Designer: генерирует синтетические пары «запрос-релевантный пассаж» на основе вашего корпуса документов. По сути, заставляет большую языковую модель (вроде Nemotron-4 340B) придумывать правдоподобные вопросы к вашим текстам.
- NeMo Automodel для Embeddings: автоматизированный пайплайн для контрастивного обучения. Сам подбирает hard negatives, настраивает loss functions и управляет гиперпараметрами.
Философия проста: если нет данных — создай их. Если сложно выбрать правильные негативные примеры — поручи это алгоритму. Остается только запустить процесс и через несколько часов получить готовую модель.
1 Подготовка: ставим NeMo и добываем «сырые» документы
Забудьте про pip install nemo-toolkit. Сейчас все иначе. NVIDIA упаковывает полный стек для эмбеддингов в отдельный контейнер, который тянет за собой все зависимости. Но мы пойдем путем чистого Python, чтобы понимать, что под капотом.
# Клонируем репозиторий с официальными примерами (актуальный на 2026)
git clone https://github.com/NVIDIA/NeMo-Embeddings-Recipes.git
cd NeMo-Embeddings-Recipes
# Создаем виртуальное окружение и ставим зависимости
python -m venv nemo_env
source nemo_env/bin/activate # для Windows: nemo_env\Scripts\activate
# Устанавливаем NeMo с поддержкой эмбеддингов и синтетических данных
pip install nemo-toolkit[embedding]==1.22.0 nemo-data-designer==0.8.0
# Проверяем установку
python -c "import nemo; import nemo_data_designer; print('Всё готово')"
Ошибка номер один: попытка установить старую версию NeMo (1.19 и ниже). В них нет интеграции с Data Designer, и вы будете неделями мучиться с ручной подготовкой датасета. Не делайте так.
Теперь нужны ваши документы. Это может быть папка с PDF, Markdown, HTML или простой текстовый файл. Конвертируем все в чистый текст. Используем unstructured библиотеку — она сейчас де-факто стандарт для извлечения текста.
from unstructured.partition.auto import partition
import glob
documents = []
for file_path in glob.glob("путь/к/вашим/документам/*.pdf"):
elements = partition(filename=file_path)
text = "\n".join([str(el) for el in elements])
documents.append({"id": file_path, "text": text})
# Сохраняем в JSONL для дальнейшей обработки
import json
with open("raw_documents.jsonl", "w") as f:
for doc in documents:
f.write(json.dumps(doc, ensure_ascii=False) + "\n")
2 Магия: генерируем запросы и пассажи из воздуха
Вот сердце метода. NeMo Data Designer берет ваши документы и с помощью мощной LLM создает к каждому отрезку текста (пассажу) потенциальный запрос, который мог бы привести к этому тексту в поиске.
Под капотом используется Nemotron-4 340B Instruct (или, если у вас мало ресурсов, Nemotron-3 Nano 30B MoE), настроенная специальным промптом. Вам не нужно думать о промптах — все уже зашито в библиотеку.
from nemo_data_designer import SyntheticDataGenerator
# Инициализируем генератор. Он автоматически использует доступную LLM через NVIDIA NIM API или локально.
# Для локальной генерации нужна минимум 1x A100 80GB.
generator = SyntheticDataGenerator(
model_name="nvidia/nemotron-4-340b-instruct",
max_length=512,
num_queries_per_passage=3 # Количество разных запросов на один пассаж
)
# Загружаем наши документы, разбиваем на пассажи (чанки)
passages = []
for doc in documents:
# Простое разбиение по предложениям (в реальности лучше использовать semantic chunking)
sentences = doc["text"].split('. ')
chunk_size = 5 # 5 предложений на чанк
for i in range(0, len(sentences), chunk_size):
chunk = '. '.join(sentences[i:i+chunk_size])
if len(chunk) > 50: # отбрасываем слишком короткие
passages.append(chunk)
# Генерируем синтетические пары (запрос, релевантный пассаж)
synthetic_pairs = generator.generate(passages[:100]) # для начала берем 100 пассажей
Что происходит? Модель анализирует пассаж и придумывает к нему разнообразные запросы: прямые, перефразированные, с ошибками, на смежные темы. Это и есть тренировочные данные.
3 Hard Negative Mining: заставляем модель учиться на сложных ошибках
Если брать в качестве негативных примеров случайные пассажи из коллекции, модель научится тривиальным различиям. Это как учить ребенка отличать кошку от автомобиля — слишком просто. Нужны hard negatives: пассажи, которые семантически близки к запросу, но не релевантны.
Automodel делает это автоматически. Но чтобы понять процесс, посмотрим на код этапа mining:
from nemo.collections.nlp.models import EmbeddingModel
# Загружаем базовую модель эмбеддингов (например, NV-Embed-v2.5)
base_model = EmbeddingModel.from_pretrained("nvidia/nv-embed-v2.5-1024")
# Кодируем все пассажи в эмбеддинги
passage_embeddings = base_model.encode(passages, convert_to_tensor=True)
# Для каждого запроса находим ближайшие пассажи, которые НЕ являются релевантными
import torch
from sklearn.metrics.pairwise import cosine_similarity
hard_negatives = {}
for query, positive_passage in synthetic_pairs:
query_embedding = base_model.encode([query], convert_to_tensor=True)
# Вычисляем косинусную близость между запросом и всеми пассажами
sims = cosine_similarity(query_embedding.cpu(), passage_embeddings.cpu())
# Ищем пассажи с высокой схожестью, но это не positive_passage
similar_indices = sims.argsort()[0][-100:] # топ-100 ближайших
# Выбираем из них те, которые не являются правильным ответом
hard_negs = []
for idx in similar_indices:
if passages[idx] != positive_passage and len(hard_negs) < 5:
hard_negs.append(passages[idx])
hard_negatives[query] = hard_negs
Именно эти сложные примеры заставят модель научиться тонким различиям. Например, запрос «настройка GPU в Docker» будет иметь hard negative «установка Docker на машину с GPU». Похоже, но не то.
4 Обучение: один конфиг, одна команда, несколько часов
Раньше нужно было возиться с DataLoader, loss functions, смешанной точностью. Теперь — один YAML файл. NVIDIA за последние два года максимально упростила процесс.
Создаем конфиг config.yaml:
model:
name: nv-embed-v2.5-1024
trainable: true
pooling: cls
data:
train_file: synthetic_data_with_hard_negatives.jsonl
num_workers: 8
batch_size: 64 # для GPU с 24GB VRAM
trainer:
devices: 1 # количество GPU
max_epochs: 5
precision: bf16-mixed
gradient_clip_val: 1.0
loss:
name: MultipleNegativesRankingLoss
temperature: 0.02
scale: 20
optimizer:
lr: 2e-5
weight_decay: 0.01
А теперь запускаем обучение:
python -m nemo.collections.nlp.run
--config-path ./configs \
--config-name config.yaml \
trainer.devices=4 \
model.train_ds.file_path=synthetic_data_with_hard_negatives.jsonl
И ждем. На 4x A100 40GB обучение на 50 тысячах пар займет около 3-4 часов. Можете сходить выпить кофе. Много кофе.
Предупреждение: не увеличивайте batch_size без необходимости. Современные контрастивные loss функции (как MultipleNegativesRankingLoss) эффективно работают с умеренными размерами батча (64-128). Слишком большой батч может ухудшить качество, потому что модель будет видеть слишком много легких негативов.
5 Оценка: BEIR benchmark и ваши собственные тесты
Обучение прошло. Но как понять, что модель стала лучше? Первым делом — BEIR (Benchmarking Information Retrieval). Это стандартный набор из 18 датасетов для оценки эмбеддингов. Ваша модель должна показывать на доменном датасете результат лучше базовой.
from beir import util, evaluator
from beir.datasets.data_loader import GenericDataLoader
from beir.retrieval.evaluation import EvaluateRetrieval
# Загружаем доменный датасет (например, NFCorpus — медицинский)
dataset = "nfcorpus"
url = f"https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/{dataset}.zip"
data_path = util.download_and_unzip(url, "datasets")
corpus, queries, qrels = GenericDataLoader(data_folder=data_path).load(split="test")
# Загружаем нашу обученную модель
from nemo.collections.nlp.models import EmbeddingModel
tuned_model = EmbeddingModel.restore_from("./checkpoints/embedding_model.nemo")
# Оцениваем
retriever = EvaluateRetrieval(tuned_model, score_function="cos_sim")
results = retriever.retrieve(corpus, queries)
ndcg, _map, recall, precision = evaluator.evaluate(qrels, results, [10, 100])
print(f"NDCG@10: {ndcg['NDCG@10']:.4f}")
Если NDCG@10 вырос на 5-15 пунктов — вы на правильном пути. 20+ — отличный результат. Но BEIR не всё. Создайте свой маленький тестовый набор из реальных запросов ваших пользователей. Проверьте вручную.
Где спрятаны грабли: 3 ошибки, которые сведут на всю работу
- Слишком много синтетики низкого качества. Если ваши исходные документы — мусор, то и сгенерированные запросы будут бессмысленными. Всегда делайте префильтрацию: удаляйте слишком короткие тексты, дубликаты, шаблонный текст (типа «Copyright 2025»).
- Игнорирование доменного словаря. Базовая модель NV-Embed-v2.5 обучена на общих данных. Она может не знать ваших специфических терминов. Решение: перед генерацией данных добавьте в промпт инструкцию с глоссарием. Или используйте доменно-адаптированную LLM для генерации, как в проекте «Японский прорыв NVIDIA», где модель учили на культурных особенностях.
- Переобучение на артефакты генерации. Модель может выучить стиль синтетических запросов, а не смысл. Симптом: отличные результаты на синтетических тестах, но провал на реальных. Лекарство: добавьте в обучение хотя бы 10-20% реальных размеченных данных (если есть). Или используйте аугментацию реальных запросов.
А что насчет железа? Можно ли ускорить процесс?
Да. Если у вас нет кластера из A100, есть варианты:
- Используйте квантованную базовую модель. Например, pplx-embed от Perplexity показывает, как 4-битная квантизация ускоряет инференс в 3 раза с минимальной потерей качества. Для этапа hard negative mining это критично.
- Локальные эмбеддинги на маленькой модели. В гайде про Qwen3-0.6B INT8 описано, как получить работающие эмбеддинги на ноутбуке. Но для доменной адаптации маленькие модели слабоваты.
- Облако. Арендуйте инстанс с 4x H100 на Lambda Labs (партнерская ссылка) на 8 часов. Это обойдется около $60-80, но сэкономит дни.
И последний совет, который вы не найдете в документации NVIDIA. После обучения сделайте дополнительную тонкую настройку на реальных логах поиска. Возьмите 1000 запросов пользователей, для которых известны клики (что они выбрали из результатов). И дообучите модель на этих данных всего 1 эпоху. Это поднимет качество еще на 5-10%.
Синтетические данные — это не волшебная палочка. Это инструмент, который снимает первоначальный барьер. Дальше нужно итеративное улучшение. Но стартовать с нуля до работающей модели за день — теперь реальность. Главное — не бояться экспериментировать с генерацией и жестко тестировать на реальных сценариях.