Fine-tuning za administratore: kada ima smisla, a kada ne

Kad neko kaže „hoću da model nauči našu dokumentaciju", misli na fine-tuning. Skoro uvek greši — odgovor je RAG, i to je bio jedan od prvih zaključaka u ovom serijalu.

Ali postoje slučajevi u kojima fine-tuning jeste pravi alat. Ovaj tekst objašnjava koji su, šta postupak stvarno zahteva i kako se izvodi na jednoj kartici.

Šta fine-tuning menja

Menja težine modela. Ne dodaje znanje na neki uredan način koji možeš da pregledaš — pomera brojeve tako da model drugačije reaguje na slične ulaze.

Menja Ne menja
Stil i ton odgovora Činjenice koje pouzdano zna
Oblik izlaza Sposobnost rasuđivanja
Poznavanje uskog domena Podatke koji se često menjaju
Doslednost u ponašanju Veličinu modela i brzinu

Ključna razlika: fine-tuning uči model kako da odgovara, ne šta da zna.

Ako mu daš stotinu dokumenata o vašim procedurama, neće ih zapamtiti kao činjenice. Naučiće da zvuči kao neko ko ih poznaje — i onda samouvereno izmisliti detalj koji nikad nije video. To je gore od modela koji prizna da ne zna.

Izbor pristupa

Zadatak Rešenje
Model treba da zna našu dokumentaciju RAG
Odgovori treba da budu kraći i drugačijeg tona Sistemski prompt
Izlaz mora biti JSON određenog oblika response_format
Treba pristup stanju sistema Alati, MCP
Loše piše na srpskom u našem domenu Fine-tuning
Mali model treba da radi jedan zadatak odlično Fine-tuning
Ne poštuje naš format uprkos uputstvima Fine-tuning

Redosled kojim se ide: prvo sistemski prompt, pa nekoliko primera u promptu, pa RAG, i tek onda fine-tuning. Devet od deset zahteva se zaustavi pre poslednjeg koraka, uz mnogo manje truda.

Slučaj u kome se zaista isplati

Model klase 3B koji radi jedan zadatak — recimo klasifikaciju tiketa u dvanaest kategorija — posle fine-tuninga ume da nadmaši model klase 27B sa promptom. Radi deset puta brže, staje u malu karticu i ne greši u obliku izlaza.

To je pravi argument: nije bolji kvalitet, nego isti kvalitet na desetostruko manjem modelu.

LoRA

Puni fine-tuning menja sve težine i traži memoriju kakvu nemaš. LoRA umesto toga dodaje male matrice pored postojećih slojeva i trenira samo njih.

Pun fine-tuning modela 8B:  oko 80 GB VRAM
LoRA nad istim modelom:     oko 16 GB
QLoRA (4-bitna osnova):     oko 8 GB

Rezultat je adapter od nekoliko desetina megabajta, a ne kopija celog modela. Prednost: možeš ih držati više za različite zadatke i menjati ih nad istom osnovom.

Model QLoRA LoRA
3B 6 GB 10 GB
8B 10 GB 18 GB
27B 24 GB 48 GB

Kartica od 24 GB nosi QLoRA nad modelima do 27B — što pokriva praktično sve što bi administrator radio.

Podaci

Ovde se odlučuje ishod. Sve ostalo je tehnikalija.

Koliko primera: ispod stotinu nema efekta; pet stotina do hiljadu je razuman minimum za uzak zadatak; nekoliko hiljada je već ozbiljno.

Kvalitet nad količinom. Tri stotine pažljivo pripremljenih primera daju bolji rezultat od tri hiljade natrpanih. Model uči i greške koje mu pokažeš.

Format je JSONL, jedan primer po redu:

{"messages": [{"role": "system", "content": "Klasifikuješ tikete."}, {"role": "user", "content": "Ne mogu da se prijavim na VPN, traži novu lozinku"}, {"role": "assistant", "content": "{\"kategorija\": \"pristup\", \"prioritet\": 2}"}]}
{"messages": [{"role": "system", "content": "Klasifikuješ tikete."}, {"role": "user", "content": "Ceo sajt je pao, ništa ne radi"}, {"role": "assistant", "content": "{\"kategorija\": \"ispad\", \"prioritet\": 5}"}]}

Sistemski prompt mora biti isti u svakom primeru i isti onaj koji ćeš koristiti posle. Ako se razlikuje, model ne prepozna situaciju za koju je učen.

Provera pre treniranja:

$ wc -l podaci.jsonl
$ jq -e . podaci.jsonl > /dev/null && echo "JSON ispravan"
$ jq -r '.messages[-1].content' podaci.jsonl | sort | uniq -c | sort -rn | head

Poslednja komanda je najvažnija. Ako je jedna kategorija zastupljena deset puta više od ostalih, model će je predviđati uvek. Neuravnotežen skup je najčešći uzrok neuspeha.

Podeli podatke pre treniranja:

$ shuf podaci.jsonl > izmesano.jsonl
$ head -n -100 izmesano.jsonl > trening.jsonl
$ tail -n 100 izmesano.jsonl > provera.jsonl

Sto primera koje model nikad nije video su jedini način da znaš da li je nešto naučio ili samo zapamtio.

Treniranje

Najjednostavniji put je Unsloth — brži je i troši manje memorije od osnovnih biblioteka:

$ python3 -m venv ~/.venv/ft
$ source ~/.venv/ft/bin/activate
$ pip install unsloth
#!/usr/bin/env python3
from unsloth import FastLanguageModel
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig

MODEL = "unsloth/Qwen3-8B-bnb-4bit"
IZLAZ = "./adapter-tiketi"

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name=MODEL,
    max_seq_length=2048,
    load_in_4bit=True,
)

model = FastLanguageModel.get_peft_model(
    model,
    r=16,                    # veličina adaptera
    lora_alpha=32,
    lora_dropout=0,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    use_gradient_checkpointing="unsloth",
    random_state=42,
)

podaci = load_dataset("json", data_files="trening.jsonl", split="train")

def oblikuj(primer):
    return {"text": tokenizer.apply_chat_template(
        primer["messages"], tokenize=False)}

podaci = podaci.map(oblikuj)

trener = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=podaci,
    args=SFTConfig(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        num_train_epochs=3,
        learning_rate=2e-4,
        logging_steps=10,
        output_dir=IZLAZ,
        optim="adamw_8bit",
        seed=42,
    ),
)

trener.train()
model.save_pretrained(IZLAZ)
tokenizer.save_pretrained(IZLAZ)
$ python3 treniraj.py

Za hiljadu primera na kartici od 24 GB računaj petnaest do trideset minuta.

Vrednosti koje se diraju

Parametar Uticaj
r 8 do 32; više uči više, ali lakše prenauči
num_train_epochs 2 do 4; preko toga model počne da pamti
learning_rate 1e-4 do 3e-4 za LoRA
max_seq_length Dovoljno za najduži primer; troši memoriju
batch_size Koliko memorija dozvoli

Ako gubitak pada ispod 0.1, model verovatno pamti umesto da uči. Smanji broj prolaza ili r.

Prati karticu u drugom terminalu, kako je opisano u tekstu o nvidia-smi:

$ watch -n 2 nvidia-smi

Provera

Nikad ne ocenjuj po utisku. Pusti sto primera koje model nije video i prebroj:

#!/usr/bin/env python3
import json
from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="./adapter-tiketi", max_seq_length=2048, load_in_4bit=True)
FastLanguageModel.for_inference(model)

tacno = ukupno = 0

for red in open("provera.jsonl"):
    primer = json.loads(red)
    ulaz = primer["messages"][:-1]
    ocekivano = primer["messages"][-1]["content"]

    tokeni = tokenizer.apply_chat_template(
        ulaz, add_generation_prompt=True, return_tensors="pt").to("cuda")
    izlaz = model.generate(input_ids=tokeni, max_new_tokens=64,
                           temperature=0.0, do_sample=False)
    dobijeno = tokenizer.decode(
        izlaz[0][tokeni.shape[1]:], skip_special_tokens=True).strip()

    ukupno += 1
    try:
        if json.loads(dobijeno) == json.loads(ocekivano):
            tacno += 1
    except json.JSONDecodeError:
        pass

print(f"Tačnost: {tacno}/{ukupno} ({100*tacno/ukupno:.1f}%)")

Isti test pusti i nad osnovnim modelom, pre fine-tuninga. Ako razlika nije jasna, fine-tuning nije vredeo truda — i to je sasvim mogući ishod.

Proveri i da model nije zaboravio ostalo. Postavi mu nekoliko pitanja van domena; ako više ne ume ništa osim klasifikacije tiketa, prenaučio je.

Upotreba

Za Ollamu i llama.cpp adapter treba spojiti sa osnovom i pretvoriti u GGUF:

from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="./adapter-tiketi", max_seq_length=2048, load_in_4bit=True)

model.save_pretrained_gguf("./model-gguf", tokenizer,
                           quantization_method="q4_k_m")
$ cat > Modelfile <<'EOF'
FROM ./model-gguf/unsloth.Q4_K_M.gguf

PARAMETER temperature 0
PARAMETER num_ctx 4096

SYSTEM """Klasifikuješ tikete."""
EOF

$ ollama create tiketi -f Modelfile
$ ollama run tiketi "Ne mogu da se prijavim na VPN"

Sistemski prompt mora biti identičan onom iz podataka za treniranje.

vLLM ume da učita adapter bez spajanja, što je zgodno kad ih imaš više:

$ vllm serve Qwen/Qwen3-8B \
    --enable-lora \
    --lora-modules tiketi=./adapter-tiketi
$ curl http://localhost:8000/v1/chat/completions \
  -d '{"model": "tiketi", "messages": [...]}'

Jedna osnova u memoriji, više adaptera nad njom — znatno jeftinije od više punih modela.

Šta obično pođe naopako

Premalo podataka. Sto primera ne menja ništa. Rezultat izgleda kao slučajnost jer to i jeste.

Neuravnotežen skup. Model uvek predviđa najčešću kategoriju. Proveri raspodelu pre nego što pokreneš treniranje.

Prenaučenost. Savršeno na trening podacima, loše na novim. Manje prolaza, više raznovrsnosti.

Zaboravljanje. Model nauči zadatak i izgubi opšte sposobnosti. Kod uskih zadataka to je prihvatljiv kompromis; kod opšteg asistenta nije.

Različiti sistemski promptovi. Trenirano sa jednim, korišćeno sa drugim — model ne prepozna situaciju.

Rešavan pogrešan problem. Najčešća greška od svih. Treba mu bio RAG, a dobio je fine-tuning.

Održavanje

Adapter je vezan za tačnu verziju osnovnog modela. Kad izađe nova generacija, treniranje ide iznova.

Zato čuvaj podatke, ne samo rezultat:

$ ls fine-tuning/
podaci/trening.jsonl
podaci/provera.jsonl
treniraj.py
proveri.py
rezultati.md

U rezultati.md zapiši verziju osnovnog modela, parametre i izmerenu tačnost. Za pola godine to je jedino što ti omogućava da ponoviš posao.

Praktični scenariji

Scenario 1: nema dovoljno memorije

Smanji max_seq_length, pa batch_size na 1 uz veći gradient_accumulation_steps. Ako i dalje ne staje, uzmi manji model.

Scenario 2: gubitak ne opada

Podaci su neuredni ili je stopa učenja premala. Prvo pogledaj nekoliko primera ručno.

Scenario 3: gubitak je skoro nula

Model pamti. Manje prolaza ili više podataka.

Scenario 4: dobro na treningu, loše u praksi

Sistemski prompt se razlikuje, ili trening podaci ne liče na stvarne ulaze.

Scenario 5: model ne ume ništa osim jednog zadatka

Za uzak zadatak je to u redu. Za asistenta smanji r i broj prolaza.

Scenario 6: rezultat nije bolji od osnovnog modela

Mogući i pošten ishod. Zapiši ga i vrati se na prompt ili RAG — to je i dalje uspešno rešen zadatak.

Kratka referenca

  • Fine-tuning uči kako, ne šta — za znanje ide RAG
  • Redosled: prompt → primeri u promptu → RAG → fine-tuning
  • LoRA — mali adapter umesto celog modela
  • QLoRA — 4-bitna osnova, model 8B staje u 10 GB
  • Najmanje 500 primera; kvalitet nad količinom
  • JSONL, isti sistemski prompt u svakom primeru
  • jq -r '.messages[-1].content' | uniq -c — provera raspodele
  • Odvoji 100 primera za proveru pre treniranja
  • r=16, 2–4 prolaza, stopa učenja oko 2e-4
  • Gubitak ispod 0.1 znači pamćenje
  • Meri tačnost, ne utisak; poredi sa osnovnim modelom
  • save_pretrained_gguf — za Ollamu
  • vllm --enable-lora — više adaptera nad jednom osnovom
  • Čuvaj podatke i parametre, ne samo adapter

Vežba

  1. Za svoj zadatak odluči da li ti treba prompt, RAG ili fine-tuning, i obrazloži.
  2. Pripremi bar tri stotine primera u JSONL obliku i proveri raspodelu odgovora.
  3. Izmeri tačnost osnovnog modela na skupu za proveru — to je tvoja polazna vrednost.
  4. Istreniraj adapter i izmeri tačnost istim testom.
  5. Postavi modelu pet pitanja van domena i proveri da li je nešto zaboravio.
  6. Spoji adapter u GGUF i pokreni ga kroz Ollamu.

Sledeći tekst u serijalu: Praktičan primer od A do Š: AI asistent za sopstveni server

Povezano:

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd