Python, dalle fondamenta Lezione 58 / 60

Modelli pre-addestrati + transfer learning + Hugging Face

Il percorso realistico da zero a un modello di deep learning funzionante nel 2026: parti da uno pre-addestrato e fanne il fine-tuning sui tuoi dati.

Nel 2017 un progetto di deep learning voleva dire scaricare ImageNet, affittare quattro GPU per una settimana e allenare una ResNet da zero. Nel 2026 vuol dire scrivere from_pretrained("...") e aver finito in venti minuti. Lo spostamento non è una piccola ottimizzazione. È la forma intera del campo.

La ragione è il transfer learning. Qualcun altro ha già bruciato un milione di GPU-ore insegnando a un modello a capire le immagini, o l’inglese, o il codice. Tu prendi quel modello, sostituisci qualche layer vicino all’output e ne fai il fine-tuning sul tuo piccolo dataset. I layer bassi della rete, quelli che hanno imparato a rilevare bordi o token sintattici, già sanno cose che si trasferiscono gratis. Non stai insegnando al modello a vedere; gli stai insegnando quali categorie ti interessano.

La lezione 57 ha attraversato l’allenamento di una piccola rete end-to-end. Questa lezione è la versione che davvero corrisponde al tuo lavoro quotidiano.

L’intuizione: i layer imparano a livelli di astrazione diversi

I primi layer di una rete convoluzionale allenata rispondono a bordi e blob di colore. I layer intermedi rispondono a texture e parti di oggetti. I layer alti rispondono a concetti interi: “questo sembra un cane, questo sembra una macchina.” Se visualizzi i filtri del layer 1 di un qualsiasi classificatore di immagini allenato su un dataset reale, sembrano sostanzialmente uguali: bordi orientati, gradienti di colore. Feature visive generiche.

Lo stesso vale per i language model. I primi layer transformer gestiscono la sintassi locale, i bigrammi comuni, la morfologia. I layer intermedi seguono la struttura della frase. I layer in cima portano segnale specifico del task: per un modello allenato su Wikipedia, è “questa è un’entità, di che tipo, come si relaziona alle altre entità.”

Il transfer learning sfrutta l’asimmetria: i layer di feature generiche non hanno bisogno di essere ri-allenati quando cambi task, ma quelli specifici del task sì. Quindi tieni il corpo del modello e sostituisci la testa.

In codice, lo schema è grosso modo:

  1. Carica un modello pre-addestrato.
  2. Opzionalmente congela i layer bassi (requires_grad = False).
  3. Sostituisci il classificatore finale con uno dimensionato per il tuo numero di classi.
  4. Allena con un learning rate basso così non spazzi via quello che già c’è.

Hugging Face: il model hub

Nel 2026, quando qualcuno dice “il modello” quasi sempre intende “un checkpoint su Hugging Face.” L’Hub ospita centinaia di migliaia di modelli: ogni variante di BERT, ogni variante di Llama, ogni fine-tune di Stable Diffusion, ogni modello audio e di visione. Il client Python è la libreria transformers e i suoi compagni datasets, tokenizers, accelerate e peft.

Il boilerplate è identico tra famiglie di modelli:

from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_id = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForSequenceClassification.from_pretrained(
    model_id, num_labels=2,
)

Tutto qui. Le classi Auto* guardano la config del checkpoint e istanziano la classe concreta giusta. Puoi sostituire distilbert-base-uncased con roberta-base o bert-base-multilingual-cased senza cambiare una riga di codice di allenamento. Questo è il regalo della libreria.

Un fine-tune vero: DistilBERT su un dataset di sentiment

Facciamo il fine-tuning di un classificatore di sentiment sul dataset di recensioni IMDB. Due classi, circa 50.000 recensioni. Il giro completo, dall’inizio alla fine.

from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer,
    DataCollatorWithPadding,
)
import numpy as np
import evaluate

ds = load_dataset("imdb")
print(ds)
# DatasetDict({ train: 25000, test: 25000, unsupervised: 50000 })

# Sotto-campiona cosi' l'esempio gira in minuti, non ore
ds["train"] = ds["train"].shuffle(seed=42).select(range(2000))
ds["test"]  = ds["test"].shuffle(seed=42).select(range(500))

model_id = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_id)

def tokenize(batch):
    return tokenizer(batch["text"], truncation=True, max_length=256)

ds_tok = ds.map(tokenize, batched=True)
collator = DataCollatorWithPadding(tokenizer=tokenizer)

model = AutoModelForSequenceClassification.from_pretrained(
    model_id, num_labels=2,
)

accuracy = evaluate.load("accuracy")

def metrics(eval_pred):
    logits, labels = eval_pred
    preds = np.argmax(logits, axis=1)
    return accuracy.compute(predictions=preds, references=labels)

args = TrainingArguments(
    output_dir="./distilbert-imdb",
    eval_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=32,
    num_train_epochs=2,
    weight_decay=0.01,
    logging_steps=50,
    report_to="none",
)

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=ds_tok["train"],
    eval_dataset=ds_tok["test"],
    tokenizer=tokenizer,
    data_collator=collator,
    compute_metrics=metrics,
)

trainer.train()
trainer.evaluate()

Su una GPU T4 di Colab gratuita questo si allena in forse quattro minuti. Otterrai un’accuratezza tra l’85 e l’89% senza altri sforzi. Per vedere cosa sta succedendo, guarda i pezzi:

  • load_dataset("imdb") fa streaming del dataset dall’Hub. La prima chiamata lo mette in cache localmente; le chiamate successive sono istantanee.
  • tokenizer(...) trasforma il testo in input_ids e attention_mask. max_length=256 tronca le recensioni lunghe: puoi salire a 512 per maggiore accuratezza a costo più alto.
  • DataCollatorWithPadding fa padding di ogni batch alla sequenza più lunga di quel batch invece che al massimo globale. Speedup gratuito.
  • AutoModelForSequenceClassification sostituisce la testa di masked-language-modeling del DistilBERT pre-addestrato con una testa di classificazione a 2 classi inizializzata casualmente. Il corpo è pre-addestrato; solo la testa è fresca.
  • Trainer impacchetta il boilerplate della lezione 57: l’optimizer, lo scheduler del learning rate, l’accumulo di gradiente, la mixed precision, il checkpointing, il logging. Configuri le manopole tramite TrainingArguments e smetti di scrivere il training loop a mano.

Due epoche a lr=2e-5 è la ricetta standard per fare il fine-tuning di un modello BERT base. Salire tende a rompere i pesi pre-addestrati; scendere sotto-allena la testa.

Salvare e ricaricare

trainer.save_model("./distilbert-imdb-final")
tokenizer.save_pretrained("./distilbert-imdb-final")

# Usalo dopo:
from transformers import pipeline
clf = pipeline("sentiment-analysis", model="./distilbert-imdb-final")
clf("This movie was beautifully shot but the script dragged.")
# [{'label': 'LABEL_1', 'score': 0.78}]

Puoi anche pusharlo direttamente sull’Hub:

trainer.push_to_hub("your-username/distilbert-imdb")

Così il checkpoint diventa condivisibile. Sei mesi dopo, chiunque (incluso te) può ricaricare il modello esatto in tre righe.

PEFT e LoRA: fine-tuning a basso costo

Il fine-tuning completo di un modello da 7 miliardi di parametri richiede hardware serio: ogni peso ha un gradiente e uno stato dell’optimizer, e questo triplica facilmente la memoria. Il parameter-efficient fine-tuning (PEFT) schiva il problema: invece di aggiornare tutti i pesi, alleni piccoli adapter addestrabili e congeli il resto.

La tecnica dominante è LoRA, Low-Rank Adaptation. Per ogni matrice di pesi che vuoi adattare, aggiungi una coppia di matrici a basso rango A e B tali che W' = W + B @ A dove A e B sono minuscole (rango 8 o 16) rispetto a W. Alleni solo A e B. I pesi originali restano congelati.

I numeri sono drammatici. Fare il fine-tuning di un modello 7B con LoRA potrebbe allenare circa 10 milioni di parametri invece di 7 miliardi. La memoria scende di 5-10x. La qualità resta competitiva.

La libreria PEFT di Hugging Face rende questo un cambiamento di poche righe:

from peft import LoraConfig, get_peft_model, TaskType

lora_cfg = LoraConfig(
    task_type=TaskType.SEQ_CLS,
    r=8,
    lora_alpha=16,
    lora_dropout=0.05,
    target_modules=["q_lin", "v_lin"],  # quali matrici adattare
)

base = AutoModelForSequenceClassification.from_pretrained(model_id, num_labels=2)
model = get_peft_model(base, lora_cfg)
model.print_trainable_parameters()
# trainable params: 296,450 || all params: 67,251,202 || trainable%: 0.44

Inserisci quel model nello stesso Trainer di prima. Stai facendo fine-tuning di meno dello 0,5% dei parametri ottenendo gran parte dell’accuratezza. Per gli LLM ormai questo è il workflow di default: una GPU consumer da 24 GB può fare il fine-tuning di un modello 7B con LoRA + quantizzazione a 4 bit, qualcosa che due anni fa avrebbe richiesto un piccolo data center.

Visione e multimodale: stesso schema

Lo schema è identico per altre modalità, solo con classi Auto* diverse:

# Computer vision: fine-tune di un ViT per classificazione di immagini
from transformers import AutoImageProcessor, AutoModelForImageClassification
processor = AutoImageProcessor.from_pretrained("google/vit-base-patch16-224")
model = AutoModelForImageClassification.from_pretrained(
    "google/vit-base-patch16-224",
    num_labels=10, ignore_mismatched_sizes=True,
)

Per progetti puramente di visione, la libreria timm (ora parte di Hugging Face) è lo standard di riferimento: ogni backbone di visione interessante, intercambiabile dietro un’unica API. Per task multimodali (immagine + testo), modelli come CLIP, SigLIP e la famiglia LLaVA sono tutti a un from_pretrained di distanza.

Dovresti fare fine-tuning?

Nel 2026 la domanda spesso non è “fine-tuning o allenamento da zero.” È “fine-tuning, o chiamare un LLM con un prompt buono?” Questo è il tema della prossima lezione. Anticipazione: per molti task NLP, fare un prompt a un modello hosted ti porta al 90% del risultato in zero tempo di allenamento, e solo quando prompt + retrieval falliscono in modo evidente conviene tirare fuori il fine-tuning.

Il modello mentale:

  • Prompt a un LLM hosted. Costo più basso per sperimentare, nessun dato di training necessario, costoso a scala.
  • Fine-tuning di un modello open-source con LoRA. Costo medio, sforzo medio, possiedi i pesi, calza stretto su un task noto.
  • Allenamento da zero. Riservato a embedding, time series, sistemi di raccomandazione, modalità nuove, oppure “abbiamo un miliardo di esempi e un budget di ricerca.”

La riga di mezzo era il default per qualsiasi progetto non banale. Nel 2026 è la seconda scelta, presa quando il prompting non basta in modo evidente.

Cosa viene dopo

Ora hai coperto lo stack pratico di deep learning del 2026: tensori e autograd (lezione 56), un training loop scritto a mano (lezione 57) e il transfer learning con Hugging Face (questa). La prossima lezione fa zoom out e pone la domanda più grossa: quando lo strumento giusto è una chiamata LLM, quando è un modello fine-tuned, e quando è una pipeline ML classica? È la domanda di design più consequente del campo in questo momento, e il costo di sbagliarla è reale.


Riferimenti: documentazione di Hugging Face transformers (https://huggingface.co/docs/transformers), documentazione di datasets (https://huggingface.co/docs/datasets), documentazione di PEFT (https://huggingface.co/docs/peft), Hu et al. “LoRA: Low-Rank Adaptation of Large Language Models” (2021), libreria timm (https://huggingface.co/docs/timm). Recupero 2026-05-01.

Cerca