От сырых данных до задачно-адаптированной модели — полный разбор: предобучение, SFT, RLHF, domain adaptation, prompt engineering, CPT, PEFT, контрастивное обучение энкодеров.
// этап 0 · данные
Качество предобученной модели на 80% определяется качеством данных. Garbage in — garbage out никогда не был актуальнее, чем для LLM. Типичный пайплайн: сбор → очистка → фильтрация → дедупликация → токенизация → упаковка в батчи.
// DATA PIPELINE · каждый этап снижает объём, повышая плотность качества
Facebook CCNet (2019) обучает 5-gram KenLM на высококачественном корпусе (Wikipedia). Документы с perplexity выше порога — скорее всего нечитаемый текст, шаблоны, спам. Perplexity отображает «насколько модель удивлена» этим текстом.
# pip install kenlm sentencepiece import kenlm, math # Загружаем 5-gram KenLM модель обученную на Wikipedia lm = kenlm.Model("wiki_lm_5gram.arpa.bin") def perplexity(text: str, model: kenlm.Model) -> float: """ Perplexity = exp(-1/N * log P(w1..wN)) Чем ниже — тем «нормальнее» текст для модели. """ words = text.split() if len(words) == 0: return float('inf') log_prob = model.score(text, bos=True, eos=True) # log10(P) log_prob_nats = log_prob * math.log(10) # → натуральный лог ppl = math.exp(-log_prob_nats / len(words)) return ppl def filter_document(doc: str, threshold: float = 1000) -> bool: """Возвращает True если документ прошёл фильтр.""" ppl = perplexity(doc, lm) return ppl < threshold # Примеры: docs = { "Wikipedia article": "The mitochondria is the powerhouse of the cell...", "Spam/boilerplate": "Click here!!! BUY NOW!!! Special offer!! 100% FREE!!!", "Code dump": "a1b2c3d4e5 0xDEADBEEF 192.168.1.1 root:x:0:0", } for name, text in docs.items(): ppl = perplexity(text, lm) passed = ppl < 1000 print(f"{name:22s} ppl={ppl:8.1f} {'✓ pass' if passed else '✗ drop'}") # Wikipedia article ppl= 45.2 ✓ pass # Spam/boilerplate ppl= 3421.8 ✗ drop # Code dump ppl= 28734.1 ✗ drop
# pip install datasketch from datasketch import MinHash, MinHashLSH import re def shingle(text: str, k: int = 5) -> set: """k-gram символьные шинглы — строительный блок MinHash.""" text = text.lower() return {text[i:i+k] for i in range(len(text) - k + 1)} def make_minhash(text: str, num_perm: int = 128) -> MinHash: """MinHash подпись документа (128 перестановок → ~1% ошибка).""" m = MinHash(num_perm=num_perm) for s in shingle(text): m.update(s.encode('utf-8')) return m # LSH: approximate nearest neighbor для быстрого поиска дублей # threshold=0.8: Jaccard similarity >= 0.8 → дублирование lsh = MinHashLSH(threshold=0.8, num_perm=128) documents = [ ("doc1", "The quick brown fox jumps over the lazy dog."), ("doc2", "The quick brown fox jumps over the lazy dog!"), # ≈ дубль ("doc3", "Machine learning is a subset of artificial intelligence."), ("doc4", "The quick brown fox jumped over the lazy dog."), # ≈ дубль ] keep = [] for doc_id, text in documents: m = make_minhash(text) duplicates = lsh.query(m) if not duplicates: lsh.insert(doc_id, m) keep.append(doc_id) print(f" ✓ keep {doc_id}: '{text[:45]}...'") else: print(f" ✗ drop {doc_id}: дубль {duplicates}") # ✓ keep doc1: 'The quick brown fox jumps over the lazy dog.' # ✗ drop doc2: дубль ['doc1'] # ✓ keep doc3: 'Machine learning is a subset of artificial...' # ✗ drop doc4: дубль ['doc1']
// этап 1 · предобучение
Предобучение — самый дорогой этап. GPT-3 (175B) обошёлся в ~$4.6M. LLaMA-3 (70B) обучался на 15 триллионах токенов. Цель: модель выучивает статистику языка, факты, код, математику.
Документы имеют разную длину. Если просто обрезать/паддить каждый до max_length=2048 — большинство батчей наполовину пустые (PAD-токены), что тратит FLOP. Packing (concatenation) объединяет несколько документов в один sequence длиной 2048, разделяя их eos_token.
from datasets import load_dataset from transformers import AutoTokenizer from itertools import chain tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf") BLOCK_SIZE = 2048 def tokenize(examples): """Токенизируем без обрезки, добавляем EOS после каждого документа.""" output = tokenizer( [t + tokenizer.eos_token for t in examples["text"]], truncation=False, # не обрезаем — объединим позже add_special_tokens=False, ) return output def pack_sequences(tokenized_dataset): """ Упаковываем токены нескольких документов в chunks по BLOCK_SIZE. Никаких PAD-токенов — 100% утилизация. """ # Flatten всех input_ids в один длинный список concatenated_ids = list(chain(*tokenized_dataset["input_ids"])) total_length = len(concatenated_ids) # Обрезаем до кратного BLOCK_SIZE total_length = (total_length // BLOCK_SIZE) * BLOCK_SIZE # Нарезаем на chunks result = { "input_ids": [ concatenated_ids[i : i + BLOCK_SIZE] for i in range(0, total_length, BLOCK_SIZE) ] } # Для CLM labels = input_ids (сдвиг делает модель автоматически) result["labels"] = result["input_ids"].copy() return result dataset = load_dataset("wikitext", "wikitext-103-raw-v1", split="train") tok_ds = dataset.map(tokenize, batched=True, remove_columns=["text"]) packed_ds = pack_sequences(tok_ds) print(f"Sequences: {len(packed_ds['input_ids'])}") print(f"Каждый sequence: {len(packed_ds['input_ids'][0])} токенов") # Sequences: 14832 # Каждый sequence: 2048 токенов (плотная упаковка)
// этап 2 · supervised fine-tuning
Base model умеет предсказывать следующий токен, но не умеет следовать инструкциям. SFT учит модель формату взаимодействия: «Получи инструкцию → выдай ответ». Это relatively дешёвый этап — тысячи примеров достаточно.
// SFT FORMAT · модель учится генерировать только ответы assistant, не повторяя инструкцию
| Формат | Описание | Пример |
|---|---|---|
| ChatML | im_start / im_end теги | GPT-4, Mistral-Instruct |
| Alpaca | instruction / input / output | Stanford Alpaca (52k) |
| ShareGPT | Human/GPT чередование | Vicuna, WizardLM |
| Llama-2 Chat | [INST] / [/INST] теги | LLaMA-2-Chat |
| Gemma | <start_of_turn> теги | Gemma-2-Instruct |
from datasets import load_dataset from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments from trl import SFTTrainer, DataCollatorForCompletionOnlyLM import torch MODEL = "mistralai/Mistral-7B-v0.1" # base model tokenizer = AutoTokenizer.from_pretrained(MODEL) tokenizer.pad_token = tokenizer.eos_token # ── Формат датасета ────────────────────────────────────────── def format_prompt(example): """ChatML формат — стандарт для SFT.""" return {"text": ( f"<|im_start|>system\n{example['system']}<|im_end|>\n" f"<|im_start|>user\n{example['instruction']}<|im_end|>\n" f"<|im_start|>assistant\n{example['output']}<|im_end|>" )} dataset = load_dataset("tatsu-lab/alpaca", split="train") dataset = dataset.map(format_prompt) # ── Label masking: считаем loss только по assistant-части ──── # DataCollatorForCompletionOnlyLM маскирует всё до response_template response_template = "<|im_start|>assistant\n" collator = DataCollatorForCompletionOnlyLM( response_template=response_template, tokenizer=tokenizer, ) # ── Модель ─────────────────────────────────────────────────── model = AutoModelForCausalLM.from_pretrained( MODEL, torch_dtype=torch.bfloat16, # BF16 на Ampere+ GPU device_map="auto", ) # ── Аргументы обучения ─────────────────────────────────────── args = TrainingArguments( output_dir="./mistral-sft", num_train_epochs=3, per_device_train_batch_size=4, gradient_accumulation_steps=8, # effective batch = 32 learning_rate=2e-5, warmup_ratio=0.03, lr_scheduler_type="cosine", bf16=True, logging_steps=10, save_strategy="epoch", ) trainer = SFTTrainer( model=model, args=args, train_dataset=dataset, dataset_text_field="text", data_collator=collator, # ← label masking! max_seq_length=2048, tokenizer=tokenizer, ) trainer.train()
// этап 3 · выравнивание
После SFT модель следует инструкциям, но не знает, что «лучше». RLHF (Reinforcement Learning from Human Feedback) добавляет человеческие предпочтения: аннотаторы сравнивают пары ответов, обучается reward model, затем policy (LLM) оптимизируется под неё через PPO.
// RLHF vs DPO · DPO убирает reward model, решает оптимизацию аналитически
from trl import DPOTrainer from datasets import load_dataset # Датасет: пары (prompt, chosen, rejected) # chosen: ответ который предпочли люди # rejected: ответ который не понравился dpo_data = load_dataset("Anthropic/hh-rlhf", split="train") # {'prompt': '...', 'chosen': '...', 'rejected': '...'} trainer = DPOTrainer( model=model, # SFT-модель ref_model=ref_model, # замороженная копия SFT (π_ref) args=training_args, beta=0.1, # KL-штраф: больше → консерват. train_dataset=dpo_data, tokenizer=tokenizer, ) trainer.train() # Loss: заставляем модель назначать # chosen > rejected по log-вероятности # относительно reference model
// Каждый пример в DPO датасете: { "prompt": "Объясни, что такое квантовая запутанность.", "chosen": "Квантовая запутанность — явление, при котором состояния двух частиц оказываются коррелированы независимо от расстояния. Если измерить спин одной частицы, состояние второй мгновенно определяется...", "rejected": "Квантовая запутанность — это когда частицы запутываются. Это очень сложная тема и лучше обратиться к специалисту." } // Ключевые датасеты для DPO: // Anthropic/hh-rlhf — 170k пар (helpfulness, harmlessness) // openbmb/UltraFeedback — 64k промптов, 4 ответа с оценками // HuggingFaceH4/ultrafeedback_binarized
// адаптация · домен и задача
Transfer learning — использование знаний, накопленных при предобучении, для новых задач. Domain adaptation — адаптация к специфическому домену (медицина, юриспруденция, финансы). Task adaptation — настройка под конкретную задачу (NER, классификация, QA).
// АДАПТАЦИЯ · три пути от базовой модели к специализированной — разный cost/benefit
| Метод | Суть | Данные | Когда использовать |
|---|---|---|---|
| Prompt Engineering | Задаём инструкцию в промпте | Не нужны | Быстрый прототип, общие задачи |
| RAG | Поиск релевантных документов + подача в контекст | Корпус для поиска | Актуальные/частые обновления данных |
| Few-shot | Примеры в контексте | 5–50 примеров | Быстрая адаптация без обучения |
| CPT (continue pretrain) | Продолжение предобучения на доменных данных | 1B–100B токенов домена | Глубокая доменная экспертиза |
| SFT на домене | Fine-tuning на instruction-пары домена | 1k–500k пар | Задачи с чёткими форматами |
| LoRA/QLoRA | Низкоранговые адаптеры поверх весов | Как SFT | Ограниченные GPU ресурсы |
from sentence_transformers import SentenceTransformer import faiss, numpy as np from transformers import pipeline # ── 1. Corpus ───────────────────────────────────────────── corpus = [ "LoRA адаптирует LLM обучая низкоранговые матрицы ΔW = A·B.", "QLoRA квантует базовую модель до 4-бит, LoRA в BF16.", "PEFT — библиотека HuggingFace для parameter-efficient FT.", "DPO оптимизирует предпочтения без reward model.", ] # ── 2. Embed corpus ──────────────────────────────────────── embedder = SentenceTransformer("BAAI/bge-small-en-v1.5") corpus_embs = embedder.encode(corpus, normalize_embeddings=True) # ── 3. FAISS index ───────────────────────────────────────── dim = corpus_embs.shape[1] index = faiss.IndexFlatIP(dim) # Inner Product ≡ cosine sim (normalized) index.add(corpus_embs) # ── 4. Retrieve ──────────────────────────────────────────── query = "Как LoRA уменьшает число параметров для обучения?" query_emb = embedder.encode([query], normalize_embeddings=True) D, I = index.search(query_emb, k=2) # top-2 похожих retrieved_context = "\n".join([corpus[i] for i in I[0]]) print("Retrieved:\n", retrieved_context) # LoRA адаптирует LLM обучая низкоранговые матрицы ΔW = A·B. # QLoRA квантует базовую модель до 4-бит, LoRA в BF16. # ── 5. Augmented generation ──────────────────────────────── prompt = f"""Используй контекст для ответа на вопрос. Контекст: {retrieved_context} Вопрос: {query} Ответ:""" gen = pipeline("text-generation", model="mistralai/Mistral-7B-Instruct-v0.2") answer = gen(prompt, max_new_tokens=150, do_sample=False)[0]["generated_text"]
// техники · prompt engineering
Prompt engineering — это искусство составления входного текста так, чтобы получить от модели нужный вывод. Не требует обучения, но требует понимания того, как модель «думает».
Чёткое описание задачи в начале промпта. Модель SFT-обучена следовать инструкциям — не стесняйся быть конкретным: формат вывода, язык, длина, ограничения.
«Let's think step by step» или показ рассуждений в примерах заставляет модель «думать» поэтапно. Значительно улучшает арифметику, логику, многошаговые задачи.
«Ты — опытный врач» или «Действуй как старший инженер Google». Сдвигает распределение ответов к экспертному домену — модель активирует нужный «регистр» знаний.
Вывод одного промпта становится входом следующего. Позволяет строить сложные пайплайны: extract → transform → validate → generate.
Добавляем 3–8 примеров (input, output) перед запросом. Модель «понимает» нужный формат и стиль без дополнительного обучения.
Генерируем несколько CoT рассуждений, берём majority vote. Улучшает accuracy на задачах с единственным правильным ответом.
# ── ПЛОХОЙ промпт (неясный, без контекста) ────────────── bad_prompt = "Напиши что-то про машинное обучение." # → модель не знает: длина? формат? аудитория? цель? # ── ХОРОШИЙ промпт (instruction-following) ────────────── good_prompt = """Ты — технический писатель с опытом в ML. Напиши краткое объяснение (3-4 предложения) понятия "градиентный спуск" для аудитории: студенты без опыта в ML. Избегай формул. Используй аналогию.""" # ── Chain-of-Thought для математики ───────────────────── cot_prompt = """Реши задачу, показывая шаги рассуждения. Вопрос: В магазине 35 яблок. Продали 1/5 от запаса, затем получили ещё 12. Сколько яблок стало? Решение (шаг за шагом):""" # → Шаг 1: 35 × 1/5 = 7 яблок продали # → Шаг 2: 35 - 7 = 28 осталось # → Шаг 3: 28 + 12 = 40 стало # → Ответ: 40 # ── Prompt Chaining пайплайн ───────────────────────────── def extract_entities(text: str) -> str: return f"""Извлеки именованные сущности (JSON). Текст: {text} Формат: {{"persons": [], "organizations": [], "locations": []}} JSON:""" def verify_entities(entities_json: str, original: str) -> str: return f"""Проверь, все ли сущности верны. Оригинал: {original} Извлечённые: {entities_json} Ответь: "ok" или укажи ошибки.""" # Шаг 1: извлечь → Шаг 2: проверить → Шаг 3: использовать # ── Persona + Few-Shot ─────────────────────────────────── persona_fewshot = """Ты — строгий код-ревьюер в Google. Оцени код и дай краткий отзыв. Пример 1: Код: for i in range(len(lst)): print(lst[i]) Отзыв: ❌ Непитоничный стиль. Используй: for x in lst: print(x) Пример 2: Код: result = [x*2 for x in items if x > 0] Отзыв: ✅ Чисто, понятно, питонично. Твой код: def get_users(db): return db.query("SELECT * FROM users")"""
// continual pretraining
CPT (Continued Pre-Training) — продолжение предобучения на доменных данных. Модель «дочитывает» специализированный корпус, адаптируя внутренние представления к домену. После CPT модель знает термины, стиль, структуры домена — но ещё не следует инструкциям.
// CPT ЦИКЛ · после CPT нужен SFT чтобы научить следовать инструкциям на доменных данных
from transformers import TrainingArguments, Trainer from torch.utils.data import ConcatDataset # ── Data mixing: domain + general ────────────────── # 85% медицинских текстов + 15% Wikipedia from torch.utils.data import WeightedRandomSampler import torch domain_ds = load_packed_dataset("medical_packed/") general_ds = load_packed_dataset("wikipedia_packed/") mixed_ds = ConcatDataset([domain_ds, general_ds]) # Веса для WeightedSampler: domain 85%, general 15% domain_weight = 0.85 / len(domain_ds) general_weight = 0.15 / len(general_ds) weights = ( [domain_weight] * len(domain_ds) + [general_weight] * len(general_ds) ) sampler = WeightedRandomSampler(weights, num_samples=len(weights)) # ── Training args для CPT ─────────────────────────── cpt_args = TrainingArguments( output_dir="./llama-medical-cpt", num_train_epochs=1, # CPT: 1-3 эпохи per_device_train_batch_size=4, gradient_accumulation_steps=16, # eff batch=64 learning_rate=1e-4, # ВЫШЕ чем в SFT! lr_scheduler_type="cosine", warmup_ratio=0.01, bf16=True, # Важно: НЕ используем label masking # Все токены = targets (как в pretraining) dataloader_num_workers=4, ) # LR для CPT: 1e-4 to 1e-5 (выше чем SFT 2e-5) # Причина: нужно перезаписать больше знаний
// parameter-efficient fine-tuning
Full fine-tuning 7B модели требует ~112 GB GPU RAM (optimizer states + gradients). PEFT позволяет обучать менее 1% параметров с сопоставимым качеством.
// LoRA · ΔW = A·B · матрица A инициализируется случайно, B — нулями · итого r×(d+d) параметров
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training from trl import SFTTrainer import torch # ── 1. QLoRA: загружаем в 4-bit ─────────────────────────── bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", # Normal Float 4 — оптимально для весов bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, # двойная квантизация: экономит ещё ~0.4 бит ) model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-2-7b-hf", quantization_config=bnb_config, device_map="auto", trust_remote_code=True, ) # VRAM: 7B в 4bit = ~4.5GB (vs ~28GB в BF16) # ── 2. Подготовка к k-bit training ──────────────────────── model = prepare_model_for_kbit_training(model) # Включает gradient checkpointing, замораживает bn слои # ── 3. LoRA конфигурация ────────────────────────────────── lora_config = LoraConfig( r=16, # rank: 4, 8, 16, 32, 64 lora_alpha=32, # scaling = alpha/r = 2.0 target_modules=[ # к каким матрицам применяем LoRA "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj" # FFN тоже ], lora_dropout=0.05, bias="none", task_type=TaskType.CAUSAL_LM, ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() # trainable params: 40,108,032 || all params: 6,778,671,104 # trainable%: 0.5916% ← обучаем МЕНЕЕ 1% параметров! # ── 4. Слияние адаптера с базовой моделью (после обучения) from peft import PeftModel base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf", torch_dtype=torch.bfloat16) merged = PeftModel.from_pretrained(base, "./lora-checkpoint") merged = merged.merge_and_unload() # W = W_frozen + A·B merged.save_pretrained("./llama-merged")
Если домен содержит много специальных терминов (медицинские аббревиатуры, химические формулы, код), стоит добавить новые токены в словарь. Это снижает среднее число токенов на термин и ускоряет инференс.
from transformers import AutoTokenizer, AutoModelForCausalLM import torch tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf") model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf") # Проверяем как сейчас токенизируются медицинские термины terms = ["myocardial", "infarction", "hyperglycemia"] for t in terms: toks = tokenizer.tokenize(t) print(f"{t}: {toks}") # myocardial: ['my', 'ocard', 'ial'] → 3 токена # infarction: ['inf', 'arc', 'tion'] → 3 токена # hyperglycemia: ['hyper', 'gly', 'cemia'] → 3 токена # ── Добавляем новые токены ──────────────────────────────── new_tokens = ["myocardial", "infarction", "hyperglycemia", "cardiomyopathy", "echocardiogram"] num_added = tokenizer.add_tokens(new_tokens) print(f"Добавлено токенов: {num_added}") # ── Resize embeddings ───────────────────────────────────── model.resize_token_embeddings(len(tokenizer)) # Новые токены инициализированы случайно — нужно обучить # ── Инициализация новых эмбеддингов средним по подтокенам # Это лучше чем random: даёт разумную начальную точку ────── old_vocab_size = len(tokenizer) - num_added with torch.no_grad(): for new_tok in new_tokens: new_id = tokenizer.convert_tokens_to_ids(new_tok) # Получаем id исходных субтокенов sub_ids = tokenizer.encode(new_tok, add_special_tokens=False) sub_ids = [i for i in sub_ids if i < old_vocab_size] if sub_ids: # Новый эмбеддинг = среднее субтокенов mean_emb = model.model.embed_tokens.weight[sub_ids].mean(0) model.model.embed_tokens.weight[new_id] = mean_emb # После этого: только новые токены требуют обучения (меньше эпох) tokenizer.save_pretrained("./llama-medical-tok")
// адаптация энкодеров
Энкодеры (BERT, RoBERTa) производят эмбеддинги — плотные векторные представления текста. Их адаптируют двумя путями: MLM (продолжение маскированного языкового моделирования на домене) и контрастивное обучение (обучение семантически близких текстов быть близко в пространстве).
// CONTRASTIVE LEARNING · anchor приближается к positive, удаляется от negatives
from transformers import ( AutoModelForMaskedLM, AutoTokenizer, DataCollatorForLanguageModeling, Trainer ) # Загружаем BERT, продолжаем MLM на медицинских текстах model = AutoModelForMaskedLM.from_pretrained("bert-base-uncased") tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") # Dynamic masking: каждый батч получает разные маски collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True, mlm_probability=0.15, # 15% токенов маскируется ) # Из 15% маскированных токенов: # 80% заменяются [MASK] # 10% заменяются случайным токеном # 10% остаются без изменений # Это не баг — это намеренная стратегия для robustness trainer = Trainer( model=model, args=mlm_training_args, train_dataset=medical_tokenized, data_collator=collator, ) trainer.train() # После: BioMedBERT-style модель # Пример результата: PubMedBERT, ClinicalBERT, BlueBERT
import torch import torch.nn as nn import torch.nn.functional as F class InfoNCELoss(nn.Module): def __init__(self, temperature: float = 0.05): super().__init__() self.T = temperature def forward(self, anchor, positive, negatives=None): """ anchor: (B, D) — базовые эмбеддинги positive: (B, D) — похожие пары negatives: (N, D) — optional явные негативы если None — используем in-batch negatives """ # L2-нормализация для cosine similarity a = F.normalize(anchor, dim=-1) p = F.normalize(positive, dim=-1) # In-batch negatives: другие примеры в батче # sim: (B, B) — попарные cosine similarities sim = torch.mm(a, p.T) / self.T # (B, B) # Positive — диагональные элементы labels = torch.arange(sim.size(0), device=sim.device) # Cross-entropy: хотим sim[i,i] >> sim[i,j] для j≠i loss = F.cross_entropy(sim, labels) return loss # SimCSE: unsupervised — один текст проходит дважды # через dropout → два разных эмбеддинга → positive пара! class SimCSE(nn.Module): def __init__(self, encoder, temperature=0.05): super().__init__() self.encoder = encoder self.loss_fn = InfoNCELoss(temperature) def forward(self, input_ids, attention_mask): # Два прохода с разными dropout masks → разные эмбеддинги out1 = self.encoder(input_ids, attention_mask).pooler_output out2 = self.encoder(input_ids, attention_mask).pooler_output return self.loss_fn(out1, out2) # Triplet Loss (альтернатива InfoNCE) triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2) # loss = max(0, ||a-p||₂ - ||a-n||₂ + margin) # p=2: L2 расстояние; margin=1.0 — минимальный зазор
| Метод | Задача | Данные | Метрика | Примеры моделей |
|---|---|---|---|---|
| Domain MLM | Доменный vocabulary, representations | Доменный текст (unlabeled) | Perplexity, downstream F1 | BioBERT, PubMedBERT, LegalBERT |
| SimCSE (unsup.) | Семантические эмбеддинги | Текст (unlabeled) | STS Spearman ρ | princeton-nlp/sup-simcse-roberta |
| SimCSE (sup.) | Семантические эмбеддинги | NLI пары (entailment/contradiction) | STS Spearman ρ | SimCSE-RoBERTa-large |
| Contrastive + Hard Neg | Поиск документов (Dense Retrieval) | Labeled query-document пары | NDCG@10, Recall@K | DPR, E5, BGE, Contriever |
| Triplet Loss | Face recognition, image retrieval | Anchor/Positive/Negative тройки | AUC, Precision@K | sentence-transformers |