Fine-tuning LLM: дообучение модели на своих данных
Рано или поздно, работая с LLM, вы упираетесь в потолок: модель не знает ваш внутренний стек, отвечает не в том формате, путает терминологию вашего домена. И тут возникает вопрос — fine-tuning? RAG? Или достаточно лучше написать промпт? Давайте разберёмся, когда действительно стоит дообучать модель и как это сделать, не продавая почку для оплаты облачных GPU.
Когда нужен fine-tuning (а когда нет)
Прежде чем бросаться обучать модель, пройдите по дереву решений:
Prompt Engineering — первая линия. Если задача решается хорошим system prompt’ом и few-shot примерами, дообучать модель не нужно. Это 80% случаев.
RAG (Retrieval-Augmented Generation) — когда модели не хватает знаний. Ваша документация, база знаний, кодовая база — всё это можно подавать в контекст через RAG. Модель при этом остаётся базовой, но получает релевантный контекст.
Fine-tuning — когда нужно изменить поведение модели. Конкретно:
- Специфический стиль или формат ответов, который не получается задать промптом
- Доменная терминология, которую модель постоянно путает
- Задача, где скорость критична (fine-tuned модель не нужен длинный промпт с примерами)
- Маленькая модель должна работать как большая на узкой задаче
Золотое правило: если можно решить промптом — решайте промптом. Если нужны данные — используйте RAG. Fine-tuning — последнее средство, самое мощное, но и самое дорогое.
LoRA и QLoRA: обучение без боли
Полный fine-tuning LLM — это обновление всех параметров модели. Для 8B-модели это значит ~16 ГБ весов в fp16, плюс оптимизатор, плюс градиенты — итого ~80-100 ГБ VRAM. Нереалистично для обычного железа.
LoRA (Low-Rank Adaptation) решает это элегантно: вместо обновления всех весов, мы «замораживаем» оригинальную модель и обучаем маленькие адаптерные матрицы, которые вставляются в нужные слои. Вместо миллиардов параметров обучаем миллионы — это 0.1-1% от общего числа.
QLoRA идёт ещё дальше: базовая модель квантизуется до 4 бит, а адаптеры обучаются в 16-битном формате. Это позволяет дообучать 8B-модель на GPU с 24 ГБ VRAM (RTX 3090, RTX 4090) и даже на 16 ГБ с маленьким batch size.
Подготовка датасета
Качество данных — это 90% успеха fine-tuning. Garbage in, garbage out — тут это работает буквально.
Стандартный формат — instruction-following (Alpaca-style):
[
{
"instruction": "Напиши Ansible task для установки nginx на Ubuntu",
"input": "",
"output": "```yaml\n- name: Install nginx\n ansible.builtin.apt:\n name: nginx\n state: present\n update_cache: true\n become: true\n```"
},
{
"instruction": "Объясни, что делает этот Ansible task",
"input": "- name: Copy config\n ansible.builtin.template:\n src: nginx.conf.j2\n dest: /etc/nginx/nginx.conf\n notify: restart nginx",
"output": "Этот task копирует шаблон конфигурации nginx из файла nginx.conf.j2 на управляемый хост в /etc/nginx/nginx.conf. Используется модуль template, который обрабатывает Jinja2-переменные в файле перед копированием. При изменении файла срабатывает handler 'restart nginx', который перезапустит сервис."
}
] Или chat-формат (предпочтительный для современных моделей):
{"messages": [{"role": "system", "content": "Ты — Ansible-эксперт"}, {"role": "user", "content": "Как создать роль?"}, {"role": "assistant", "content": "Для создания роли используйте..."}]}
{"messages": [{"role": "user", "content": "Напиши playbook для деплоя Docker"}, {"role": "assistant", "content": "```yaml\n---\n- name: Deploy with Docker..."}]} Рекомендации по данным
- Минимум 100 примеров для заметного эффекта. Оптимально — 500-2000.
- Качество важнее количества. 200 отличных примеров лучше 2000 посредственных.
- Разнообразие. Включайте разные формулировки одного вопроса, разные уровни сложности.
- Консистентный формат. Все ответы должны следовать одному стилю.
- Валидация. Проверьте каждый пример вручную. Ошибка в данных — ошибка в модели.
Инструменты: Unsloth
Unsloth — это библиотека, которая делает fine-tuning доступным на consumer-grade GPU. Обещает 2x ускорение и 60% снижение потребления памяти по сравнению с vanilla HuggingFace — и в моём опыте это близко к правде.
Установка:
pip install unsloth Пошаговый пример: дообучение Llama 3 8B
Вот полный пример fine-tuning с Unsloth:
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
# 1. Загрузка модели с 4-bit квантизацией
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Meta-Llama-3.1-8B-Instruct",
max_seq_length=2048,
dtype=None, # auto-detect
load_in_4bit=True,
)
# 2. Настройка LoRA-адаптеров
model = FastLanguageModel.get_peft_model(
model,
r=16, # ранг адаптера (8-64, больше = лучше, но дороже)
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_alpha=16, # scaling factor
lora_dropout=0, # Unsloth оптимизирован для dropout=0
bias="none",
use_gradient_checkpointing="unsloth", # экономит 30% VRAM
)
# 3. Подготовка датасета
dataset = load_dataset("json", data_files="training_data.jsonl", split="train")
def format_prompt(example):
"""Форматирование в chat template Llama 3"""
messages = example["messages"]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=False
)
return {"text": text}
dataset = dataset.map(format_prompt)
# 4. Настройка обучения
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=2048,
dataset_num_proc=2,
packing=True, # упаковка коротких примеров — экономит время
args=TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4, # эффективный batch = 8
warmup_steps=10,
num_train_epochs=3,
learning_rate=2e-4,
fp16=True,
logging_steps=10,
output_dir="outputs",
optim="adamw_8bit",
seed=42,
),
)
# 5. Запуск обучения
trainer.train()
# 6. Сохранение
model.save_pretrained("my-finetuned-model")
tokenizer.save_pretrained("my-finetuned-model") На RTX 4090 (24 ГБ) обучение на 1000 примеров занимает ~15-30 минут. На RTX 3090 — примерно столько же. На RTX 3060 12GB получится, но с batch_size=1 и gradient_accumulation_steps=8.
Оценка результатов
После обучения нужно проверить, что модель стала лучше, а не хуже (да, такое бывает):
# Быстрый тест генерации
FastLanguageModel.for_inference(model)
messages = [
{"role": "user", "content": "Напиши Ansible playbook для настройки firewall"}
]
inputs = tokenizer.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True,
return_tensors="pt"
).to("cuda")
outputs = model.generate(
input_ids=inputs,
max_new_tokens=512,
temperature=0.3,
)
print(tokenizer.decode(outputs[0], skip_special_tokens=True)) Метрики, на которые стоит смотреть:
- Training loss — должен стабильно снижаться, но не до нуля (это переобучение)
- Eval loss — если растёт при снижении training loss — переобучение
- Ручная проверка — самый надёжный способ. Подготовьте 20-30 тестовых вопросов и проверьте ответы вручную
Экспорт в GGUF для Ollama
Обученную модель можно конвертировать в GGUF-формат и запустить через Ollama:
# Экспорт в GGUF с квантизацией Q4_K_M
model.save_pretrained_gguf(
"my-model-gguf",
tokenizer,
quantization_method="q4_k_m"
) Затем создаём Modelfile для Ollama:
FROM ./my-model-gguf/unsloth.Q4_K_M.gguf
PARAMETER temperature 0.3
PARAMETER num_ctx 4096
SYSTEM """
Ты — специализированный Ansible-ассистент.
""" И регистрируем модель:
ollama create my-ansible-expert -f Modelfile
ollama run my-ansible-expert Теперь у вас есть кастомная модель, которая работает локально через Ollama, знает вашу специфику и отвечает в нужном формате.
Типичные ошибки
Переобучение (overfitting). Самая частая проблема. Модель наизусть запоминает обучающие примеры и теряет способность к обобщению. Решение: меньше эпох (1-3), больше данных, eval-set для мониторинга.
Плохие данные. Если в ваших примерах есть ошибки, противоречия, или непоследовательный формат — модель усвоит все эти проблемы. Потратьте 80% времени на подготовку данных.
Неправильные гиперпараметры. Начните с проверенных значений: lr=2e-4, r=16, epochs=3, batch_size=2-4. Не крутите всё сразу — меняйте один параметр за раз.
Слишком маленькая модель. Fine-tuning 1B-модели не превратит её в GPT-4. Если базовая модель не справляется с вашей задачей в режиме few-shot, fine-tuning скорее всего тоже не поможет.
Catastrophic forgetting. При агрессивном обучении модель может «забыть» общие знания. Используйте маленький learning rate и мало эпох.
Когда это имеет смысл
В моей практике fine-tuning оправдал себя в нескольких случаях: модель для генерации Ansible-плейбуков в строго определённом формате (с FQCN, become, конкретной структурой ролей), модель для классификации тикетов по внутренней таксономии, и модель для генерации ответов в стиле внутренней документации. Везде ключевым было не столько «добавить знания», сколько «зафиксировать формат и стиль».
Если ваша задача — просто дать модели доступ к информации — смотрите в сторону RAG. Fine-tuning — для изменения поведения, а не для добавления знаний.