Introduzione: Quando il Prompt Engineering Non Basta
Il prompt engineering e potente, ma ha dei limiti. Quando hai bisogno che un modello segua uno stile specifico in modo consistente, risponda in un formato proprietario, o eccella in un dominio di nicchia, il fine-tuning diventa la soluzione. Il fine-tuning adatta i pesi del modello ai tuoi dati, creando una versione specializzata che "pensa" come il tuo dominio richiede.
Ma il fine-tuning completo di un modello con miliardi di parametri e proibitivamente costoso. Tecniche come LoRA, QLoRA e PEFT hanno rivoluzionato questo processo, rendendo possibile adattare un modello da 70 miliardi di parametri su una singola GPU consumer, modificando meno dello 0.1% dei pesi totali.
Cosa Imparerai in Questo Articolo
- La differenza tra full fine-tuning e tecniche parameter-efficient
- Come funziona LoRA (Low-Rank Adaptation) e perchè e cosi efficiente
- QLoRA: combinare quantizzazione e LoRA per GPU limitate
- Preparazione del dataset per il fine-tuning
- Implementazione pratica con Hugging Face e PEFT
- Quando conviene il fine-tuning rispetto al prompt engineering
Full Fine-Tuning vs Parameter-Efficient Fine-Tuning
Nel full fine-tuning, tutti i parametri del modello vengono aggiornati durante il training. Per un modello come Llama 3 70B, questo significa aggiornare 70 miliardi di pesi, richiedendo centinaia di GB di memoria GPU e costi hardware significativi.
Le tecniche PEFT (Parameter-Efficient Fine-Tuning) risolvono questo problema aggiornando solo una piccola frazione dei parametri, ottenendo risultati comparabili al full fine-tuning con una frazione delle risorse.
Confronto Risorse: Full vs LoRA vs QLoRA
| Caratteristica | Full Fine-Tuning | LoRA | QLoRA |
|---|---|---|---|
| Parametri aggiornati | 100% | 0.1-1% | 0.1-1% |
| GPU RAM (7B model) | ~60 GB | ~16 GB | ~6 GB |
| GPU RAM (70B model) | ~500 GB | ~160 GB | ~48 GB |
| qualità risultati | Migliore | ~95-98% del full | ~93-97% del full |
| Tempo di training | Ore/Giorni | Minuti/Ore | Minuti/Ore |
| Costo stimato (7B) | $50-200 | $5-20 | $2-10 |
LoRA: Low-Rank Adaptation
LoRA (Low-Rank Adaptation) e la tecnica PEFT più popolare e si basa su un'intuizione matematica elegante: durante il fine-tuning, i cambiamenti ai pesi del modello hanno un rango basso. Invece di aggiornare la matrice dei pesi completa W (dimensione d x d), LoRA decompone l'aggiornamento in due matrici piccole A e B di rango r, dove r e molto minore di d.
In pratica, LoRA "congela" tutti i pesi originali del modello e aggiunge piccoli moduli adattatori accanto ai layer di attention. Durante il training, solo questi moduli vengono aggiornati. Durante l'inferenza, i pesi LoRA possono essere fusi con quelli originali senza costo aggiuntivo.
# Configurazione LoRA con Hugging Face PEFT
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM, AutoTokenizer
# Carica il modello base
model_name = "meta-llama/Llama-3.1-8B"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# Configura LoRA
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # Rango della decomposizione (più alto = più capacità)
lora_alpha=32, # Fattore di scala (tipicamente 2x r)
lora_dropout=0.05, # Dropout per regolarizzazione
target_modules=[ # Layer a cui applicare LoRA
"q_proj", "k_proj", # Query e Key nell'attention
"v_proj", "o_proj", # Value e Output projection
],
bias="none" # Non addestrare i bias
)
# Applica LoRA al modello
peft_model = get_peft_model(model, lora_config)
# Verifica parametri trainabili
peft_model.print_trainable_parameters()
# Output: trainable params: 6,553,600 || all params: 8,030,261,248
# Percentage: 0.082% dei parametri totali!
Come Scegliere il Rango r
Il parametro r (rango) determina la capacità espressiva dell'adattamento LoRA:
- r = 4-8: sufficiente per compiti semplici (classificazione, formato output)
- r = 16-32: buon bilanciamento per la maggior parte dei casi d'uso
- r = 64-128: per compiti complessi che richiedono cambiamenti significativi nel comportamento
QLoRA: LoRA + Quantizzazione
QLoRA combina LoRA con la quantizzazione a 4 bit del modello base. Il modello originale viene compresso da float16 (16 bit per peso) a int4 (4 bit per peso), riducendo la memoria necessaria di circa 4x. I moduli LoRA rimangono in float16 per mantenere la precisione del fine-tuning.
# QLoRA: fine-tuning con quantizzazione 4-bit
from transformers import BitsAndBytesConfig
import torch
# Configurazione quantizzazione 4-bit
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # Quantizza a 4 bit
bnb_4bit_quant_type="nf4", # NormalFloat4 (migliore per LLM)
bnb_4bit_compute_dtype=torch.bfloat16, # Compute in bfloat16
bnb_4bit_use_double_quant=True # Doppia quantizzazione per risparmiare RAM
)
# Carica modello quantizzato
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
quantization_config=bnb_config,
device_map="auto"
)
# Applica LoRA sopra il modello quantizzato
peft_model = get_peft_model(model, lora_config)
# Ora il modello 8B richiede solo ~6GB di VRAM!
print(f"Memoria GPU: ~{torch.cuda.memory_allocated() / 1e9:.1f} GB")
Preparazione del Dataset
La qualità del fine-tuning dipende direttamente dalla qualità del dataset. Un dataset ben preparato e più importante di iperparametri perfetti. Il formato standard e quello delle instruction-response pairs.
# Preparazione dataset per fine-tuning
import json
# Formato instruction-following standard
training_examples = [
{
"instruction": "Analizza il sentiment di questa recensione del ristorante",
"input": "La pasta era fredda e il servizio lentissimo. Mai più.",
"output": "NEGATIVO. La recensione esprime insoddisfazione per due aspetti: qualità del cibo (pasta fredda) e servizio (lentissimo). L'espressione 'Mai più' indica un'esperienza fortemente negativa."
},
{
"instruction": "Analizza il sentiment di questa recensione del ristorante",
"input": "Ambiente carino, cibo nella media. Prezzi un po' alti per la zona.",
"output": "NEUTRO/MISTO. La recensione presenta aspetti positivi (ambiente carino), neutri (cibo nella media) e negativi (prezzi alti). Il giudizio complessivo e bilanciato con leggera tendenza negativa."
}
]
# Formattazione per il training
def format_for_training(example: dict) -> str:
"""Formatta un esempio nel template chat standard."""
prompt = f"""### Instruction:
{example['instruction']}
### Input:
{example['input']}
### Response:
{example['output']}"""
return prompt
# Salva dataset
with open("training_data.jsonl", "w") as f:
for ex in training_examples:
f.write(json.dumps({"text": format_for_training(ex)}) + "\n")
print(f"Dataset creato con {len(training_examples)} esempi")
Best Practice per il Dataset
- qualità > Quantità: 500 esempi di alta qualità battono 5.000 mediocri
- Diversita: copri tutte le varianti del compito che il modello dovrà gestire
- Consistenza: mantieni formato e stile uniformi in tutti gli esempi
- Bilanciamento: distribuisci uniformemente le classi (positivo/negativo/neutro)
- Validazione: separa almeno il 10-20% dei dati per la valutazione
- Pulizia: rimuovi duplicati, errori grammaticali, risposte incoerenti
Training e Valutazione
Il training del fine-tuning segue lo stesso principio del training standard: minimizzare la loss sugli esempi di training. Con LoRA/QLoRA, pero, il processo e molto più veloce e richiede meno risorse.
# Training con Hugging Face Trainer
from transformers import TrainingArguments, Trainer
from datasets import load_dataset
# Carica dataset
dataset = load_dataset("json", data_files="training_data.jsonl", split="train")
dataset = dataset.train_test_split(test_size=0.1)
# Tokenizza
def tokenize(example):
return tokenizer(
example["text"],
truncation=True,
max_length=512,
padding="max_length"
)
tokenized = dataset.map(tokenize, batched=True)
# Configura training
training_args = TrainingArguments(
output_dir="./fine-tuned-model",
num_train_epochs=3, # Numero di epoch (2-5 per LoRA)
per_device_train_batch_size=4, # Batch size per GPU
gradient_accumulation_steps=4, # Simula batch size più grande
learning_rate=2e-4, # Learning rate (1e-4 - 3e-4 per LoRA)
warmup_steps=100, # Warmup graduale
logging_steps=10, # Log ogni 10 step
save_strategy="epoch", # Salva a ogni epoch
evaluation_strategy="epoch", # Valuta a ogni epoch
fp16=True, # Mixed precision per velocità
)
# Avvia training
trainer = Trainer(
model=peft_model,
args=training_args,
train_dataset=tokenized["train"],
eval_dataset=tokenized["test"],
)
trainer.train()
# Salva il modello LoRA (solo gli adapter, pochi MB)
peft_model.save_pretrained("./lora-adapters")
print("Training completato! Adapter salvati.")
Merge e Deploy
Dopo il training, gli adapter LoRA possono essere usati in due modi: caricati separatamente sopra il modello base (flessibile, puoi avere più adapter) o fusi con il modello base in un singolo modello (più semplice da deployare, nessun overhead di inferenza).
# Merge degli adapter LoRA con il modello base
from peft import PeftModel
# Carica modello base + adapter
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
torch_dtype=torch.float16,
device_map="auto"
)
merged_model = PeftModel.from_pretrained(base_model, "./lora-adapters")
# Fondi adapter con modello base
merged_model = merged_model.merge_and_unload()
# Salva modello completo
merged_model.save_pretrained("./final-model")
tokenizer.save_pretrained("./final-model")
print("Modello finale salvato (base + LoRA fusi)")
# Test del modello fine-tuned
inputs = tokenizer("### Instruction:\nAnalizza il sentiment...", return_tensors="pt")
outputs = merged_model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Decision Framework: Fine-Tuning vs Prompt Engineering
Non sempre il fine-tuning e la scelta giusta. Ecco un framework per decidere quando investire nel fine-tuning e quando il prompt engineering e sufficiente.
Quando Scegliere Cosa
| Scenario | Raccomandazione | Motivo |
|---|---|---|
| Compito generico con formato specifico | Prompt Engineering | Few-shot e sufficient |
| Dominio di nicchia con terminologia specifica | Fine-Tuning | Il modello deve apprendere il vocabolario |
| Stile di scrittura consistente | Fine-Tuning | Difficile mantenere con solo prompt |
| Budget limitato, pochi dati | Prompt Engineering | Fine-tuning richiede dati e compute |
| Latenza critica, costo per token alto | Fine-Tuning (modello piccolo) | Modello piccolo fine-tuned batte grande generico |
| Requisito privacy dati | Fine-Tuning (open source) | Nessun dato inviato a terzi |
Conclusioni
Il fine-tuning con LoRA e QLoRA ha democratizzato l'adattamento dei modelli linguistici. Quello che prima richiedeva cluster di GPU costosi ora e possibile su una singola GPU consumer, modificando meno dell'1% dei parametri totali del modello.
La chiave del successo e nella qualità del dataset: 500 esempi curati manualmente producono risultati migliori di 10.000 esempi generati automaticamente. Investi tempo nella preparazione dei dati, non solo negli iperparametri.
Nel prossimo articolo vedremo come portare gli LLM in produzione: le API di OpenAI e Anthropic, il deploy di modelli open source, strategie di caching, rate limiting, monitoring e gestione dei costi.







