Адаптация моделей под домен: full fine-tuning vs LoRA vs QLoRA, PEFT методы, подготовка датасетов
Адаптация предобученных моделей: Full FT, LoRA, QLoRA, PEFT, DPO, case studies
Fine-tuning (дообучение) — процесс адаптации предобученной языковой модели под специфичные задачи или домен. Вместо обучения модели с нуля (что требует миллионов долларов и датасетов), вы берёте готовую модель (Llama, Mistral, GPT) и дообучаете её на своих данных.
Зачем fine-tuning:
Fine-tuning vs RAG:
| Критерий | Fine-Tuning | RAG | Когда использовать |
|---|---|---|---|
| Cost | $$$ upfront (обучение) | $ per token (inference) | FT при стабильных данных |
| Обновление данных | Требуется переобучение | Обновление документов | RAG для частых обновлений |
| Factual knowledge | Ограничено training data | Актуальные данные из БД | RAG для фактов |
| Style/Format | Отлично | Требует промптов | FT для стиля |
| Citations | Нет | Есть (источники) | RAG для ссылок |
| Latency | Низкая | Выше (retrieval + generation) | FT для speed |
| Hallucinations | Возможны | Меньше (grounded в контексте) | RAG для точности |
Гибридный подход (Best Practice):
Fine-tuning для: style, format, domain terminology, task patterns
+
RAG для: factual knowledge, frequently changing data, citations
=
Production-ready система с максимальной accuracy
Обновление всех весов модели.
Характеристики:
Когда использовать:
Cost estimate (AWS):
PEFT метод: замораживает веса модели, обучает только low-rank матрицы (adapter).
Характеристики:
Математика:
W' = W + ΔW = W + A × B
где:
W ∈ R^(d×k) — оригинальные веса (frozen)
A ∈ R^(r×k), B ∈ R^(d×r) — обучаемые матрицы
r << d,k (rank, обычно 8-64)
Когда использовать:
LoRA + 4-bit quantization — обучение на потребительском GPU.
Характеристики:
Cost estimate:
Когда использовать:
RLHF альтернатива — обучение на preference data без reward model.
Traditional RLHF: SFT → Reward Model → PPO → Policy
DPO: SFT → DPO (на preference pairs) → Policy
Характеристики:
Когда использовать:
DPO + SFT в одном этапе — объединяет SFT и preference optimization.
Характеристики:
Когда использовать:
Instruction формат (наиболее распространённый):
[
{
"instruction": "Переведи на французский",
"input": "Hello, world!",
"output": "Bonjour, le monde!"
},
{
"instruction": "Классифицируй тональность текста",
"input": "Отличный продукт, очень доволен!",
"output": "Позитивная"
},
{
"instruction": "Напиши SQL запрос для получения пользователей",
"input": "Таблица users с полями id, name, email",
"output": "SELECT id, name, email FROM users;"
}
]Chat формат (для диалоговых моделей):
[
{
"messages": [
{"role": "system", "content": "Ты — помощник для Python разработчиков."},
{"role": "user", "content": "Как создать FastAPI приложение?"},
{"role": "assistant", "content": "from fastapi import FastAPI\napp = FastAPI()"}
]
}
]import json
from typing import List
def create_medical_dataset() -> List[dict]:
"""Создание датасета для медицинской Q&A системы"""
data = [
{
"instruction": "Объясни диагноз пациенту простым языком",
"input": "Диагноз: артериальная гипертензия 2 степени, риск 3",
"output": "У вас повышенное артериальное давление второй степени. "
"Это означает, что давление обычно в диапазоне 160-179 на 100-109. "
"Риск 3 означает повышенную вероятность осложнений на сердце и сосуды."
},
{
"instruction": "Рекомендуй лечение при симптомах",
"input": "Головная боль, температура 38.5°C, кашель",
"output": "Рекомендации:\n1. Парацетамол 500мг при температуре выше 38.5°C\n"
"2. Обильное тёплое питьё\n3. Постельный режим\n"
"4. Обратиться к врачу при ухудшении"
}
]
return data
# Сохранение в формат для обучения
dataset = create_medical_dataset()
with open("medical_dataset.json", "w", encoding="utf-8") as f:
json.dump(dataset, f, ensure_ascii=False, indent=2)from datasets import Dataset, DatasetDict
def validate_dataset(file_path: str) -> DatasetDict:
"""Загрузка и валидация датасета"""
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
# Проверка структуры
for i, item in enumerate(data):
if "instruction" not in item or "output" not in item:
raise ValueError(f"Item {i} missing required fields")
if not item["instruction"].strip():
raise ValueError(f"Item {i} has empty instruction")
if not item["output"].strip():
raise ValueError(f"Item {i} has empty output")
# Разделение на train/validation
from sklearn.model_selection import train_test_split
train_data, val_data = train_test_split(
data, test_size=0.1, random_state=42
)
train_dataset = Dataset.from_list(train_data)
val_dataset = Dataset.from_list(val_data)
dataset_dict = DatasetDict({
"train": train_dataset,
"validation": val_dataset
})
print(f"Train: {len(train_dataset)} examples")
print(f"Validation: {len(val_dataset)} examples")
return dataset_dictpip install transformers datasets peft accelerate bitsandbytes
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from datasets import load_dataset
# Загрузка модели и токениизатора
model_name = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto"
)
# Загрузка датасета
dataset = load_dataset("json", data_files="medical_dataset.json")
# Токенизация
def tokenize_function(examples):
texts = [
f"### Instruction: {inst}\n### Input: {inp}\n### Output: {out}"
for inst, inp, out in zip(
examples["instruction"],
examples["input"],
examples["output"]
)
]
return tokenizer(texts, truncation=True, max_length=512, padding="max_length")
tokenized_dataset = dataset.map(tokenize_function, batched=True)
# Training arguments
training_args = TrainingArguments(
output_dir="./medical-llama",
num_train_epochs=3,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
warmup_steps=100,
weight_decay=0.01,
logging_dir="./logs",
logging_steps=10,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
# Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"],
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
# Обучение
trainer.train()
# Сохранение
model.save_pretrained("./medical-llama-final")
tokenizer.save_pretrained("./medical-llama-final")from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
model_name = "meta-llama/Llama-2-7b-hf"
# Загрузка модели
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
# LoRA конфигурация
lora_config = LoraConfig(
r=8, # rank
lora_alpha=32, # scaling factor
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj"
],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# Применение LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 8,388,608 || all params: 6,742,511,616 || trainable%: 0.1244%
# Training arguments (аналогично full fine-tuning)
training_args = TrainingArguments(
output_dir="./medical-llama-lora",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4, # LoRA использует higher LR
fp16=True,
logging_steps=10,
eval_strategy="epoch",
save_strategy="epoch",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
eval_dataset=tokenized_dataset["validation"],
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
trainer.train()
# Сохранение только adapter (не всей модели!)
model.save_pretrained("./medical-llama-lora-adapter")from transformers import BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 4-bit quantization config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # normal float 4-bit
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True, # double quantization
)
# Загрузка модели в 4-bit
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
# Подготовка модели для k-bit training
model = prepare_model_for_kbit_training(model)
# LoRA конфигурация
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora_config)
# Обучение (аналогично LoRA)
training_args = TrainingArguments(
output_dir="./medical-llama-qlora",
num_train_epochs=3,
per_device_train_batch_size=2, # меньше batch size из-за memory
gradient_accumulation_steps=8,
learning_rate=2e-4,
fp16=True,
optim="paged_adamw_32bit", # paged optimizer для stability
)
trainer = Trainer(model=model, args=training_args, ...)
trainer.train()Создадим модель, специализированную на генерации FastAPI кода.
fastapi_dataset = [
{
"instruction": "Создай FastAPI endpoint для получения пользователя по ID",
"input": "Таблица users с полями id, name, email",
"output": """from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user"""
},
{
"instruction": "Добавь валидацию запроса с помощью Pydantic",
"input": "Endpoint POST /users для создания пользователя",
"output": """from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
password: str = Field(..., min_length=8)
@app.post("/users")
async def create_user(user: UserCreate):
# Валидация автоматически применена
hashed_pw = hash_password(user.password)
new_user = User(name=user.name, email=user.email, password=hashed_pw)
db.add(new_user)
db.commit()
return {"id": new_user.id}"""
}
]# train_fastapi_model.py
import torch
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
BitsAndBytesConfig
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from datasets import Dataset
# Загрузка данных
with open("fastapi_dataset.json", "r") as f:
data = json.load(f)
dataset = Dataset.from_list(data)
# Токенизация
model_name = "mistralai/Mistral-7B-Instruct-v0.2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
def format_example(example):
return f"""[INST] {example['instruction']}
Input: {example['input']}
Output: [/INST] {example['output']}"""
def tokenize(example):
text = format_example(example)
return tokenizer(text, truncation=True, max_length=1024)
tokenized_dataset = dataset.map(tokenize)
# QLoRA setup
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# Training
training_args = TrainingArguments(
output_dir="./fastapi-mistral",
num_train_epochs=5,
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
learning_rate=2e-4,
fp16=True,
optim="paged_adamw_32bit",
logging_steps=5,
save_strategy="epoch",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
trainer.train()
model.save_pretrained("./fastapi-mistral-adapter")from peft import PeftModel
from transformers import AutoTokenizer, AutoModelForCausalLM
base_model_name = "mistralai/Mistral-7B-Instruct-v0.2"
adapter_path = "./fastapi-mistral-adapter"
# Загрузка базовой модели
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.float16,
device_map="auto"
)
# Загрузка adapter
model = PeftModel.from_pretrained(base_model, adapter_path)
# Инференс
def generate_code(instruction: str, input: str = "") -> str:
prompt = f"[INST] {instruction}\n\nInput: {input}\n\nOutput: [/INST]"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(
**inputs,
max_new_tokens=512,
temperature=0.2, # низкая температура для кода
do_sample=True,
top_p=0.95,
)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
# Пример
code = generate_code(
"Создай FastAPI endpoint с JWT аутентификацией",
"Используй Pydantic для валидации"
)
print(code)from fastapi import FastAPI
from pydantic import BaseModel
import torch
app = FastAPI()
class CodeGenerationRequest(BaseModel):
instruction: str
input: str = ""
temperature: float = 0.2
max_tokens: int = 512
class CodeGenerationResponse(BaseModel):
code: str
model: str
@app.post("/generate", response_model=CodeGenerationResponse)
async def generate_code(request: CodeGenerationRequest):
prompt = f"[INST] {request.instruction}\n\nInput: {request.input}\n\nOutput: [/INST]"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=request.max_tokens,
temperature=request.temperature,
do_sample=True,
top_p=0.95,
)
code = tokenizer.decode(outputs[0], skip_special_tokens=True)
return CodeGenerationResponse(code=code, model="fastapi-mistral-7b")from evaluate import load
import torch
perplexity = load("perplexity")
bleu = load("bleu")
rouge = load("rouge")
# Perplexity на validation set
def evaluate_perplexity(model, tokenizer, val_dataset):
texts = [format_example(ex) for ex in val_dataset]
results = perplexity.compute(
model_id=model.config._name_or_path,
predictions=texts,
)
print(f"Perplexity: {results['perplexity']:.2f}")
return results
# BLEU/ROUGE для генерации
def evaluate_generation(model, tokenizer, test_examples):
predictions = []
references = []
for ex in test_examples:
prompt = f"[INST] {ex['instruction']}\n\nInput: {ex['input']}\n\nOutput: [/INST]"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=512)
pred = tokenizer.decode(outputs[0], skip_special_tokens=True)
predictions.append(pred)
references.append(ex['output'])
bleu_score = bleu.compute(predictions=predictions, references=[[r] for r in references])
rouge_score = rouge.compute(predictions=predictions, references=references)
print(f"BLEU: {bleu_score['bleu']:.4f}")
print(f"ROUGE-L: {rouge_score['rougeL']:.4f}")# Шаблон для human evaluation
evaluation_template = """
Оцени сгенерированный код по критериям:
Instruction: {instruction}
Input: {input}
Generated Code:
{generated}
Reference Code:
{reference}
Оценки (1-5):
- Correctness (код работает): _
- Completeness (все требования): _
- Code Quality (стиль, best practices): _
- Efficiency (производительность): _
Comments:
"""| Метод | GPU Memory | Время | Точность | Cost | Когда использовать |
|---|---|---|---|---|---|
| Full FT (7B) | 80GB | 10+ часов | 100% | $$$$ | Максимальная accuracy, большой бюджет |
| LoRA (7B) | 24GB | 2-3 часа | 95-98% | $$ | Быстрое итерирование, multiple tasks |
| QLoRA (7B) | 16GB | 3-4 часа | 93-97% | $ | Consumer GPU, ограниченный бюджет |
| DPO | 24GB | 4-6 часов | 95-98% | $$ | Есть preference data, RLHF альтернатива |
| RAG | 8GB | N/A | Зависит | $/token | Factual knowledge, частые обновления |
Рекомендации:
Контекст: Телемедицинский стартап, 50K+ пользователей.
Проблема:
Решение:
Training setup:
Результаты:
| Метрика | Base Llama-3-8B | Fine-tuned | GPT-4 |
|---|---|---|---|
| Medical QA Accuracy | 62% | 84% | 89% |
| Perplexity | 18.5 | 8.2 | 6.1 |
| Cost per 1K tokens | $0.0001 | $0.0001 | $0.03 |
| Latency (p95) | 1.2s | 1.2s | 2.8s |
ROI:
Контекст: FinTech компания, 200+ разработчиков.
Проблема:
Решение:
Training setup:
Результаты:
| Метрика | До | После |
|---|---|---|
| Code Acceptance Rate | 23% | 67% |
| Time Saved | — | 2.5 часа/неделю на разработчика |
| Boilerplate Reduction | — | 45% |
| Developer Satisfaction | 2.8/5 | 4.3/5 |
ROI:
Контекст: Юридическая фирма, автоматизация document review.
Проблема:
Решение:
Training setup:
Результаты:
| Метрика | До | После |
|---|---|---|
| Document Review Time | 45 мин/документ | 12 мин/документ |
| Accuracy (clause detection) | 71% | 94% |
| False Negative Rate | 12% | 3% |
| Lawyer Satisfaction | 3.1/5 | 4.6/5 |
ROI:
Симптомы:
Решения:
Симптомы:
Решения:
Симптомы:
Решения:
Симптомы:
Решения:
| Инструмент | Назначение | Сложность |
|---|---|---|
| Hugging Face PEFT | LoRA/QLoRA training | Средняя |
| Axolotl | Production FT pipeline | Средняя |
| LLaMA Factory | Unified FT framework | Низкая |
| Unsloth | Optimized QLoRA (2x быстрее) | Низкая |
| TRL | DPO, PPO, RLHF | Высокая |
| Платформа | GPU | Cost/час | Когда использовать |
|---|---|---|---|
| AWS SageMaker | A100, V100 | $3-30 | Enterprise, production |
| Google Cloud | A100, V100 | $3-30 | GCP ecosystem |
| RunPod | RTX 4090, A100 | $0.7-2 | Budget training |
| Lambda Labs | A100, A10 | $1-2 | Cost-effective |
| Paperspace | RTX 6000, A40 | $1-3 | Easy to use |
Fine-tuning позволяет адаптировать LLM под ваш домен с минимальными ресурсами благодаря QLoRA и PEFT.
Ключевые выводы:
Next: Квантование — как уменьшить размер модели и ускорить inference.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.