Как обучить LoRA-адаптер для своей задачи
Как обучить LoRA-адаптер для языковой модели на одном GPU — от подготовки данных до оценки результата.
LoRA (Low-Rank Adaptation) — метод дообучения языковых моделей, который обучает только 0.1–1% параметров вместо всех миллиардов. Это сокращает требования к GPU с кластера до одной видеокарты: адаптер для Llama 3.1 8B обучается на RTX 3090 (24 ГБ VRAM) за 1–4 часа. Принцип работы fine-tuning мы разбирали ранее — здесь переходим к практике.
Когда нужен LoRA
LoRA оправдан в трёх сценариях. Первый — адаптация стиля: модель должна отвечать в тоне компании, использовать специфическую терминологию. Второй — обучение на закрытых данных: модель должна знать внутреннюю документацию, продуктовые спецификации. Третий — специализация: модель для узкой задачи (классификация обращений, генерация SQL, медицинские ответы) работает точнее универсальной.
Если задача решается промпт-инжинирингом или RAG, LoRA не нужен. Дообучение — тяжёлая артиллерия для случаев, когда промпт не справляется.
Подготовка данных
Качество данных определяет качество адаптера. Минимальный датасет — 100–500 примеров для простых задач, 1000–5000 для сложных. Формат — JSONL с парами инструкция-ответ:
{"instruction": "Классифицируй обращение", "input": "Не могу оплатить заказ картой", "output": "Категория: оплата. Приоритет: высокий."}
{"instruction": "Классифицируй обращение", "input": "Когда приедет курьер?", "output": "Категория: доставка. Приоритет: средний."}Правила подготовки данных:
Разнообразие. Покрывайте все типы запросов, которые модель будет получать. 100 разнообразных примеров лучше 1000 однотипных.
Качество. Каждый пример — эталонный ответ. Ошибки в данных учатся моделью буквально.
Формат. Используйте тот же шаблон (chat template), что и базовая модель. Для Llama 3 — формат с тегами <|begin_of_text|>, для Mistral — формат [INST].
Синтетические данные. Если реальных примеров мало, сгенерируйте дополнительные через GPT-4 или Claude. Схема: дайте модели 10–20 реальных примеров, попросите сгенерировать ещё 200 в том же формате. Затем вручную проверьте и отфильтруйте — синтетические данные часто содержат повторы и неточности. Такой подход работает для классификации и стилистических задач, но хуже — для задач с точными фактическими знаниями.
Установка инструментов
pip install torch transformers peft trl datasets bitsandbytes accelerateКлючевые библиотеки: peft (Parameter-Efficient Fine-Tuning, реализация LoRA), trl (Transformer Reinforcement Learning, удобные тренеры), bitsandbytes (квантизация для экономии памяти).
Код обучения
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
import torch
# Загрузка модели в 4-bit для экономии памяти
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model_name = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name, quantization_config=bnb_config, device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# Настройка LoRA
lora_config = LoraConfig(
r=16, # Ранг адаптера (8-64, выше = больше параметров)
lora_alpha=32, # Масштабирующий коэффициент
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # ~0.5% от всех параметров
# Загрузка данных
dataset = load_dataset("json", data_files="training_data.jsonl", split="train")
# Обучение
training_config = SFTConfig(
output_dir="./lora-adapter",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
warmup_steps=10,
logging_steps=10,
save_strategy="epoch",
bf16=True
)
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=training_config,
tokenizer=tokenizer
)
trainer.train()
trainer.save_model("./lora-adapter")Гиперпараметры LoRA
Rank (r) — размерность адаптера. r=8 — минимум, r=16 — стандарт, r=64 — для сложных задач. Больший ранг = больше обучаемых параметров = лучше результат, но медленнее обучение.
Alpha — масштабирующий коэффициент. Правило: alpha = 2×r. Alpha/r определяет «силу» адаптера.
Target modules — какие слои модели адаптировать. Минимум: q_proj и v_proj (проекции запроса и значения в механизме внимания). Стандарт для Llama: q_proj, v_proj, k_proj, o_proj. Максимум: все линейные слои, включая gate_proj, up_proj, down_proj (MLP-блоки). Адаптация только attention-проекций обходится дешевле, но для сложных задач включение MLP-блоков даёт прирост качества до 3–5%.
Learning rate. 1e-4 — 3e-4 для большинства задач. Слишком высокий — модель «забывает» базовые навыки. Слишком низкий — адаптер не обучается.
LoRA vs QLoRA vs DoRA
| Метод | Обучаемые параметры | VRAM (7B) | Время (1K примеров) | Качество |
|---|---|---|---|---|
| Full Fine-tuning | 100% (~7B) | 80GB+ | 8+ ч на A100 | ★★★★★ |
| LoRA (rank=16) | ~0.5% (~35M) | 16–24GB | 1–2 ч на RTX 3090 | ★★★★☆ |
| QLoRA (4bit + LoRA) | ~0.5% | 6–10GB | 2–3 ч на RTX 3090 | ★★★★☆ |
| DoRA (magnitude + direction) | ~0.6% | 16–24GB | 1.5–2.5 ч на RTX 3090 | ★★★★★ |
QLoRA — комбинация 4-битной квантизации (NF4) с LoRA. Базовая модель загружается в 4 бита, а адаптер обучается в float16/bfloat16. Потеря качества минимальна, а VRAM сокращается вдвое. Код обучения выше уже использует QLoRA — за это отвечает BitsAndBytesConfig с load_in_4bit=True.
DoRA (Weight-Decomposed Low-Rank Adaptation) — метод от NVIDIA, представленный на ICML 2024. DoRA раскладывает веса модели на два компонента: магнитуду (длина вектора) и направление. LoRA применяется только к направлению, а магнитуда обучается отдельно. Результат: DoRA опережает LoRA на 22–37% при низких рангах (r=4, r=8) и не добавляет накладных расходов при инференсе. В PEFT DoRA включается одним параметром:
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
use_dora=True # Включает DoRA
)Типичные ошибки при обучении
OOM (Out of Memory). Самая частая проблема. Решения по нарастающей: уменьшите per_device_train_batch_size до 1, включите gradient_checkpointing=True в конфигурации обучения, переключитесь на QLoRA (load_in_4bit=True), снизьте ранг адаптера. Если и это не помогает — берите модель меньше. На RTX 3060 (12 ГБ) реалистично обучить QLoRA для моделей до 7B параметров.
Переобучение (overfitting). Лосс на обучении падает, но модель начинает дословно повторять примеры из датасета вместо обобщения. Признаки: ответы слишком шаблонные, модель «заучила» данные. Лечение: сократите число эпох до 1–2, увеличьте lora_dropout до 0.1, добавьте разнообразия в датасет. При 200–500 примерах 3 эпохи — это часто уже слишком много.
Плохой датасет. Если модель после обучения стала хуже — проблема почти всегда в данных. Проверьте: нет ли противоречивых примеров (одинаковый input, разный output), правильно ли применяется chat template, нет ли пустых или обрезанных ответов. Полезно начать обучение на 50 примерах и проверить, что модель способна их воспроизвести — если нет, проблема в формате данных.
Catastrophic forgetting. Модель забывает базовые навыки. Чаще всего это результат слишком высокого learning rate или слишком большого числа эпох. Снизьте LR до 5e-5, уменьшите эпохи и проверьте модель на общих задачах (не только на задачах из датасета).
Мониторинг обучения
Следите за training loss — он должен стабильно снижаться. Если лосс прыгает или не падает после первых 50 шагов, проблема в learning rate или данных. Для удобного мониторинга подключите Weights & Biases:
pip install wandb
wandb login# Добавьте в SFTConfig:
training_config = SFTConfig(
...
report_to="wandb",
run_name="lora-llama3-classifier"
)WandB покажет графики лосса, learning rate, GPU utilization и скорость обучения в реальном времени. Если лосс падает до ~0.01 и ниже — модель переобучилась, остановите обучение раньше.
Использование адаптера
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")
model = PeftModel.from_pretrained(base_model, "./lora-adapter")
# Инференс как обычно
inputs = tokenizer("Классифицируй: не работает оплата", return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))Адаптер занимает 10–50 МБ — в тысячи раз меньше базовой модели. Можно хранить десятки адаптеров для разных задач и переключаться между ними на лету. Для продакшен-деплоя используйте vLLM с поддержкой LoRA.
Слияние адаптера с базовой моделью
Для деплоя без дополнительной задержки адаптер можно «вшить» в базовую модель. После слияния модель работает с обычной скоростью инференса, не загружая адаптер отдельно:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
# Загружаем базовую модель в полной точности
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
torch_dtype=torch.float16,
device_map="auto"
)
# Подключаем адаптер и сливаем
model = PeftModel.from_pretrained(base_model, "./lora-adapter")
merged_model = model.merge_and_unload()
# Сохраняем как обычную модель
merged_model.save_pretrained("./merged-model")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
tokenizer.save_pretrained("./merged-model")После слияния получается полноценная модель, которую можно загрузить в vLLM, Ollama или любой другой фреймворк без поддержки PEFT. Для Ollama сконвертируйте через llama.cpp в формат GGUF, для vLLM — загрузите директорию напрямую. Минус слияния — теряется возможность быстро переключаться между адаптерами. Если у вас десяток адаптеров для разных клиентов, выгоднее загружать их динамически: vLLM и TGI поддерживают подключение LoRA-адаптеров к запущенной модели без перезагрузки.
Оценка результата
Сравните ответы модели до и после адаптации на тестовой выборке (10–20% от данных, не использованных в обучении). Метрики зависят от задачи: accuracy для классификации, BLEU/ROUGE для генерации, экспертная оценка для стилистических задач.
Практический подход: составьте набор из 30–50 тестовых запросов, прогоните через базовую модель и через модель с адаптером, сравните результаты в таблице. Для стилистических задач часто хватает ручной оценки — просто прочитайте оба ответа и выберите лучший. Для классификации считайте accuracy: адаптер должен давать минимум 85–90% на тестовой выборке, иначе стоит пересмотреть данные или гиперпараметры.
Если адаптер ухудшил общие навыки модели (отвечает хуже на вопросы не по теме обучения) — это catastrophic forgetting. Снизьте learning rate до 5e-5, сократите число эпох до 1–2 или уменьшите ранг адаптера. Чем специфичнее задача и чем меньше данных, тем ниже должен быть learning rate.
Где запускать дообучение
Google Colab Pro (~$10/мес) — A100 40GB, достаточно для QLoRA на Llama 3.1 8B.
Kaggle Notebooks — бесплатно, 2×T4 GPU, 30 часов/неделю.
Modal.com — serverless GPU, платите только за вычисления (~$1.50/час A100).
RunPod / Vast.ai — аренда GPU по требованию, A100 от $1.50/час.
Локальный GPU. RTX 3090 / 4090 (24 ГБ VRAM) — хватает для QLoRA на моделях до 13B. RTX 3060 (12 ГБ) — для моделей до 7B с QLoRA. Для ускорения обучения на потребительских GPU попробуйте Unsloth — он оптимизирует тренировочный цикл и сокращает потребление памяти на 30–50%, что позволяет обучать модели крупнее или с большим batch size на том же железе.
Итог
LoRA — доступный способ специализировать LLM под конкретную задачу. На одном GPU (24 ГБ VRAM) за несколько часов можно обучить адаптер, который превратит универсальную модель в специалиста по вашей предметной области. Начните с 200–500 качественных примеров, стандартных гиперпараметров (r=16, alpha=32) и трёх эпох — и итерируйте от результата. Если LoRA не даёт нужного качества — попробуйте DoRA при низких рангах или увеличьте датасет. А если нужна максимальная экономия памяти — QLoRA позволит обучить адаптер даже на видеокарте с 6 ГБ VRAM.