Poradnik 13: Lokalny fine-tuning LLM-ów — przepływy SFT, LoRA i DPO

Autor: Jakub RusinowskiOstatnia aktualizacja 11 min czytaniaZaawansowany

Fine-tuning dostosowuje wytrenowany wcześniej model do Twojego konkretnego zadania. Ten poradnik omawia, kiedy warto go stosować, trzy główne techniki oraz działający kod do każdej z nich.

Pierwszy raz? Zacznij od Poradnika 14: Fine-tuning pierwszego LLM-a w godzinę — to praktyczna karta przepisu. Ten poradnik jest pełnym materiałem źródłowym, do którego tamten odsyła.


Kiedy stosować fine-tuning?

Fine-tuning jest potężny, ale nie zawsze właściwy. Kieruj się tą listą decyzyjną:

  1. Najpierw spróbuj promptowania — prompty systemowe i przykłady few-shot rozwiązują 80% przypadków
  2. Potem spróbuj RAG — jeśli problemem jest wiedza (fakty, dokumenty, dane prywatne), sięgnij po RAG
  3. Fine-tuning stosuj, gdy: potrzebujesz spójnego stylu lub formatu, języka specyficznego dla dziedziny, masz ograniczenia opóźnień (brak miejsca na długie prompty systemowe) albo chcesz zachowania, którego nie da się wypromptować

Dobre zastosowania fine-tuningu:

  • Bot obsługi klienta w tonie Twojej firmy
  • Asystent programisty trzymający się konwencji Twojego zespołu
  • Specjalista z dziedziny medycznej lub prawnej
  • Ekstrakcja ustrukturyzowanego wyjścia (zawsze zwracaj JSON)

Słabe zastosowania fine-tuningu:

  • „Niech będzie mądrzejszy” — fine-tuning nie dodaje wiedzy o świecie
  • Zadania jednorazowe — promptowanie jest szybsze
  • Bardzo małe zbiory danych (poniżej 100 przykładów) — model się przeuczy

Trzy techniki

1. Nadzorowany fine-tuning (SFT)

Trening na parach wejście/wyjście. Model uczy się generować wyjście na podstawie wejścia. To fundament modeli śledzących instrukcje.

  • Format danych: prompt + wzorcowa odpowiedź
  • Minimalny rozmiar zbioru: 200–500 przykładów
  • Kluczowe parametry: współczynnik uczenia (2e-4), liczba epok (1–3), maksymalna długość sekwencji

2. LoRA / QLoRA (fine-tuning oszczędny parametrowo)

Zamiast aktualizować wszystkie wagi modelu (pełny fine-tuning wymaga dwukrotnie więcej VRAM), LoRA dodaje do warstw uwagi małe, trenowalne macierze rozkładu niskiego rzędu. QLoRA = LoRA na modelu bazowym skwantyzowanym do 4 bitów — pozwala prowadzić fine-tuning modeli 7B na pojedynczym GPU z 6 GB VRAM.

  • r (rank): 8–64 — wyższy = większa pojemność, więcej pamięci
  • alpha: zwykle dwukrotność rangi
  • target_modules: q_proj,v_proj (minimum), k_proj,o_proj,gate_proj,up_proj,down_proj (pełny zestaw)
  • VRAM: 7B QLoRA ≈ 6 GB, 13B QLoRA ≈ 10 GB, 70B QLoRA ≈ 48 GB

3. DPO (Direct Preference Optimization)

Dostraja model do ludzkich preferencji bez osobnego modelu nagrody. Wymaga danych w parach: odpowiedzi „wybranej” i „odrzuconej” dla tego samego promptu. Zwykle uruchamiany po SFT.

  • Format danych: prompt + odpowiedź wybrana + odpowiedź odrzucona
  • Kluczowy parametr: beta (0,1–0,5) — kontroluje, jak bardzo model odbiega od bazowego
  • Kiedy stosować: gdy SFT daje odpowiedzi poprawne, lecz mdłe i mało pomocne

Przygotowanie zbioru danych

Format SFT Alpaca (najprostszy)

python
import json

# Write your training data as JSONL
examples = [
    {"instruction": "Summarize this in one sentence", "input": "Long text...", "output": "Short summary."},
    {"instruction": "Classify the sentiment", "input": "I love this product!", "output": "Positive"},
]

with open('train.jsonl', 'w') as f:
    for ex in examples:
        f.write(json.dumps(ex) + '\n')

Format ChatML z wieloma turami

jsonl
{"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi! How can I help?"}]}

Format DPO

jsonl
{"prompt": "Explain photosynthesis", "chosen": "Photosynthesis is the process by which plants convert sunlight into sugar using CO2 and water...", "rejected": "Plants make food from sun."}

SFT z Unsloth (najszybsza metoda)

Unsloth jest dwukrotnie szybszy od standardowego treningu przy 60% mniejszym zużyciu VRAM.

bash
pip install unsloth
python
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset

# Load model with QLoRA config
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Llama-3.2-3B-Instruct",
    max_seq_length=2048,
    load_in_4bit=True,  # QLoRA
)

# Add LoRA adapters
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    lora_alpha=32,
    lora_dropout=0,
    bias="none",
)

# Load your dataset (Alpaca format)
dataset = load_dataset("tatsu-lab/alpaca", split="train[:1000]")  # first 1k for demo

# Format as instruction prompt
def format_prompt(example):
    return {"text": f"### Instruction:\n{example['instruction']}\n\n### Input:\n{example['input']}\n\n### Response:\n{example['output']}"}

dataset = dataset.map(format_prompt)

# Train
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    args=TrainingArguments(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        num_train_epochs=2,
        learning_rate=2e-4,
        output_dir="./output",
        save_strategy="epoch",
    ),
)
trainer.train()

# Save merged model for Ollama
model.save_pretrained_merged("merged_model", tokenizer, save_method="merged_16bit")

Trening DPO z TRL

bash
pip install trl transformers peft
python
from trl import DPOTrainer, DPOConfig
from peft import LoraConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset

model_name = "meta-llama/Llama-3.2-3B-Instruct"  # your SFT checkpoint
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name)

dataset = load_dataset("argilla/dpo-mix-7k", split="train")
# dataset columns: prompt, chosen, rejected

lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"])

training_args = DPOConfig(
    beta=0.1,                    # preference strength
    max_length=1024,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    num_train_epochs=1,
    learning_rate=5e-5,
    output_dir="./dpo_output",
)

trainer = DPOTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    tokenizer=tokenizer,
    peft_config=lora_config,
)
trainer.train()

LLaMA-Factory (alternatywa z interfejsem graficznym)

Dla tych, którzy wolą interfejs wizualny od kodu:

bash
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e '.[torch,metrics]'
llamafactory-cli webui

Następnie otwórz w przeglądarce http://localhost:7860. LLaMA-Factory obsługuje:

  • Metody treningu SFT, RLHF, DPO i PPO
  • Ponad 100 rodzin modeli, w tym Llama, Qwen, Gemma i Mistral
  • Wbudowaną przeglądarkę zbiorów danych (obsługuje wszystkie formaty)
  • Scalanie LoRA i eksport do GGUF (zgodny z Ollama)

Ewaluacja dostrojonego modelu

Porównanie z punktem odniesienia:

python
# Before vs After test
prompt = "[YOUR TASK-SPECIFIC PROMPT HERE]"
print("BASE:", base_model.generate(prompt))
print("FINE-TUNED:", finetuned_model.generate(prompt))

Dla modeli kodujących: uruchom HumanEval Pass@1 przed i po

Dla modeli czatowych: użyj MT-Bench z GPT-4 w roli sędziego

Dla zadań specyficznych dla dziedziny: zbuduj przed treningiem wydzielony zbiór testowy (10–20% Twoich danych)

Uważaj na katastrofalne zapominanie: przetestuj zadania ogólne (matematyka, rozumowanie), aby upewnić się, że model nie stracił zdolności bazowych.


Typowe błędy, których warto unikać

BłądRozwiązanie
Za mało danych (poniżej 100 przykładów)Minimum 200–500 dla sensownego fine-tuningu
Za dużo epok (powyżej 3)Zwykle 1–3 epoki; więcej prowadzi do przeuczenia
Zły format JSONLZwaliduj poleceniem python -c "import json; [json.loads(l) for l in open('data.jsonl')]"
Użycie złego modelu bazowegoWybierz model już bliski Twojemu zadaniu (zadanie programistyczne → CodeLlama)
Zapomnienie o scaleniu adapteraPrzed importem do Ollama wymagane jest save_pretrained_merged() z Unsloth
Brak zbioru ewaluacyjnegoZawsze wydziel 10% danych, by zmierzyć poprawę