Salta al contenuto principale

04 - Fine-Tuning di LLM: LoRA, QLoRA e PEFT per Personalizzare i Modelli

04 - Fine-Tuning di LLM: LoRA, QLoRA e PEFT per Personalizzare i Modelli: Introduzione: Quando il Prompt Engineering Non Basta Il prompt engineering e...

Cosa ne pensi di questo articolo?

04 - Fine-Tuning di LLM: LoRA, QLoRA e PEFT per Personalizzare i Modelli
Generative AIFoto:Tara WinsteadPexels

In Breve

04 - Fine-Tuning di LLM: LoRA, QLoRA e PEFT per Personalizzare i Modelli: Introduzione: Quando il Prompt Engineering Non Basta Il prompt engineering e...

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.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti