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 Ollamuvllm --enable-lora— više adaptera nad jednom osnovom- Čuvaj podatke i parametre, ne samo adapter
Vežba
- Za svoj zadatak odluči da li ti treba prompt, RAG ili fine-tuning, i obrazloži.
- Pripremi bar tri stotine primera u JSONL obliku i proveri raspodelu odgovora.
- Izmeri tačnost osnovnog modela na skupu za proveru — to je tvoja polazna vrednost.
- Istreniraj adapter i izmeri tačnost istim testom.
- Postavi modelu pet pitanja van domena i proveri da li je nešto zaboravio.
- 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:
- AI na Linux serveru — pregled celog serijala
- MCP — pristup alatima — prethodni tekst
- RAG nad sopstvenom dokumentacijom — obično pravi odgovor
- Koliko VRAM-a ti stvarno treba — računica i za treniranje
- Hugging Face CLI — preuzimanje osnovnog modela
Comments
Post a Comment