Fine-tuning LLM: дообучение модели на своих данных

fine-tuningllmaipython

Рано или поздно, работая с LLM, вы упираетесь в потолок: модель не знает ваш внутренний стек, отвечает не в том формате, путает терминологию вашего домена. И тут возникает вопрос — fine-tuning? RAG? Или достаточно лучше написать промпт? Давайте разберёмся, когда действительно стоит дообучать модель и как это сделать, не продавая почку для оплаты облачных GPU.

Когда нужен fine-tuning (а когда нет)

Прежде чем бросаться обучать модель, пройдите по дереву решений:

Дерево решений: промпт-инжиниринг, RAG или 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.

Архитектура LoRA — замороженные веса базовой модели и обучаемые низкоранговые адаптерные матрицы

Подготовка датасета

Качество данных — это 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 — для изменения поведения, а не для добавления знаний.

© 2026 Terminal Notes. Built with SvelteKit.