Ti è mai capitata questa scena? Passi un pomeriggio ad addestrare un modello, la grid search finalmente converge, l’accuratezza è quella che speravi. Chiudi il laptop soddisfatto. Il giorno dopo riapri il notebook e ti accorgi che il modello… non c’è più. Era in memoria, e la memoria è volatile: per riaverlo devi rieseguire tutto, magari un’ora di addestramento. Se ti è successo almeno una volta, questo tutorial è per te. Oggi impariamo a salvare un modello su disco, a ricaricarlo quando serve e — passo che quasi tutti saltano — a confrontare versioni diverse in modo ordinato, senza affidarci alla memoria (la nostra, stavolta).
Serializzare: cucinare una volta, scongelare per sempre
Il termine tecnico è serializzazione: trasformare un oggetto vivo in memoria — il tuo modello addestrato, con tutti i suoi coefficienti — in una sequenza di byte scrivibile su file. L’operazione inversa, la deserializzazione, ricostruisce l’oggetto identico a com’era.
L’analogia che preferisco è la lasagna della domenica. Cucinarla richiede ore: è l’addestramento. Ma nessuno cucina una lasagna nuova ogni volta che ha fame: la prepari una volta, la porzioni, la surgeli. Quando serve, la scongeli in cinque minuti ed è (quasi) come appena fatta. Serializzare un modello è esattamente questo: paghi il costo dell’addestramento una sola volta, poi il file sul disco è la tua porzione pronta. E come per la lasagna, sul contenitore va l’etichetta con la data — ci torniamo tra poco, perché è il punto dove i progetti amatoriali si distinguono da quelli professionali.
Un richiamo veloce prima di partire: se non ricordi cos’è una Pipeline di scikit-learn, ti basta sapere che è una catena che unisce preprocessing e modello in un unico oggetto, così fit e predict attraversano tutti i passaggi in ordine. Ci servirà subito.
Salvare e ricaricare con joblib
Lo strumento standard nell’ecosistema scikit-learn è joblib, una libreria che serializza oggetti Python ed è particolarmente efficiente con gli array NumPy (cioè con quasi tutto quello che c’è dentro un modello). Prepariamo l’ambiente e proviamo:
# ambiente 2026: Python 3.12 + uv, editor VS Code o PyCharm
# uv venv .venv && source .venv/bin/activate
# uv pip install scikit-learn joblib
import joblib
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# Dataset incluso in scikit-learn: diagnosi di tumori (binario)
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Addestriamo una pipeline completa: scaling + modello
modello = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
modello.fit(X_train, y_train)
print(f"Accuratezza prima del salvataggio: {modello.score(X_test, y_test):.4f}")
# SALVATAGGIO: una riga. compress=3 riduce la dimensione del file
joblib.dump(modello, "modello.joblib", compress=3)
# RICARICAMENTO: in un altro script, un altro giorno, un altro computer
modello_ricaricato = joblib.load("modello.joblib")
print(f"Accuratezza dopo il ricaricamento: {modello_ricaricato.score(X_test, y_test):.4f}")
# I due valori sono IDENTICI: l'oggetto è lo stesso, byte per byte
Nota il dettaglio che fa la differenza: abbiamo serializzato la pipeline intera, non solo la regressione logistica. È l’errore classico dei principianti: salvare il modello ma non lo StandardScaler addestrato insieme a lui. Risultato: al momento di predire su dati nuovi, i numeri arrivano al modello senza la scalatura che si aspetta, e le predizioni escono sbagliate in silenzio, senza nessun errore. Tornando alla lasagna: hai surgelato il ragù ma non la besciamella. Salva sempre tutta la catena.
Un modello salvato senza il suo preprocessing è una bomba a orologeria: non esplode subito, esplode alla prima predizione in produzione.
L’etichetta sul contenitore: salvare i metadati
Un file modello.joblib da solo, tra tre mesi, è un mistero: su quali dati era addestrato? Con che punteggio? Con quale versione di scikit-learn? Quest’ultima domanda non è pignoleria: caricare un modello con una versione di scikit-learn diversa da quella usata per salvarlo può fallire o, peggio, comportarsi in modo sottilmente diverso. La soluzione è banale e potentissima: accanto a ogni modello, un file JSON con i suoi metadati.
import json
import sklearn
from datetime import datetime, timezone
from sklearn.metrics import f1_score
y_pred = modello.predict(X_test)
metadati = {
"nome": "logreg_scalata",
"data": datetime.now(timezone.utc).isoformat(),
"sklearn_version": sklearn.__version__, # fondamentale!
"accuratezza_test": round(modello.score(X_test, y_test), 4),
"f1_test": round(f1_score(y_test, y_pred), 4),
"note": "baseline con StandardScaler, max_iter=5000",
}
with open("modello.json", "w") as f:
json.dump(metadati, f, indent=2)
Se non ricordi cos’è l’F1 score, ti basta sapere che è una media tra precisione e richiamo, utile quando le classi sono sbilanciate e la sola accuratezza racconta mezza storia.
Confrontare i modelli: la gara con la classifica scritta
Adesso il passo che trasforma due trucchi in un metodo di lavoro. Nei progetti reali non addestri un modello: ne provi tre, cinque, dieci. Se salvi ciascuno con la sua etichetta, il confronto diventa la lettura di una classifica, non un esercizio di memoria:
from pathlib import Path
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
Path("modelli").mkdir(exist_ok=True)
candidati = {
"logreg": make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000)),
"random_forest": make_pipeline(RandomForestClassifier(random_state=42)),
"svm_rbf": make_pipeline(StandardScaler(), SVC(random_state=42)),
}
# Addestra, valuta e salva ogni candidato con la sua etichetta
for nome, pipe in candidati.items():
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
joblib.dump(pipe, f"modelli/{nome}.joblib", compress=3)
with open(f"modelli/{nome}.json", "w") as f:
json.dump({
"nome": nome,
"sklearn_version": sklearn.__version__,
"accuratezza_test": round(pipe.score(X_test, y_test), 4),
"f1_test": round(f1_score(y_test, y_pred), 4),
}, f, indent=2)
# Il giorno dopo: leggi le etichette e stampa la classifica
schede = [json.loads(p.read_text()) for p in Path("modelli").glob("*.json")]
schede.sort(key=lambda s: s["f1_test"], reverse=True)
for s in schede:
print(f"{s['nome']:<15} F1={s['f1_test']:.4f} acc={s['accuratezza_test']:.4f}")
# Ricarichi SOLO il vincitore, senza riaddestrare nulla
vincitore = joblib.load(f"modelli/{schede[0]['nome']}.joblib")
È come una gara di cucina in cui ogni piatto arriva al tavolo dei giudici con il cartellino dei voti già compilato: per proclamare il vincitore non serve riassaggiare nulla, basta leggere i cartellini. E il punto chiave è l’ultimo: ricarichi in un secondo il modello migliore, addestrato ieri, e lo usi subito.
Il ciclo di serializzazione: la pipeline addestrata diventa un file su disco (con la sua «etichetta» di metadati) e torna in memoria identica quando serve. Il costo dell’addestramento si paga una volta sola.
Onestà: quando joblib non basta (e quando basta eccome)
Due avvertenze da amico. La prima riguarda la sicurezza: joblib usa sotto il cofano il formato pickle di Python, che durante il caricamento può eseguire codice arbitrario. Tradotto: carica solo file che hai creato tu o di cui ti fidi ciecamente, mai un .joblib scaricato da internet. Se devi condividere modelli scikit-learn pubblicamente, guarda la libreria skops, nata proprio per un formato di scambio più sicuro.
La seconda riguarda le dimensioni del progetto. Per i tuoi esperimenti, per una tesi, per un piccolo servizio in produzione, joblib più un JSON di metadati è tutto ciò che serve — davvero. Non installare MLflow o Weights & Biases per confrontare tre modelli su un dataset: è come comprare un carrello elevatore per spostare una cassetta d’acqua. Questi strumenti diventano preziosi quando gli esperimenti sono centinaia, il team è numeroso e serve tracciabilità automatica. Fino ad allora, il pattern che hai imparato oggi copre il 90% dei casi reali. E se un giorno devi portare un modello fuori dal mondo Python (un’app mobile, un servizio in C#), il formato di scambio si chiama ONNX — ma è un ponte da attraversare quando ci arrivi.
Prova tu
Prendi il dataset load_wine di scikit-learn e organizza la tua gara: addestra almeno quattro pipeline diverse (varia modelli e preprocessing), salva ciascuna in modelli/ con il suo JSON di metadati, e scrivi uno script separato classifica.py che legge solo i JSON e stampa il podio ordinato per F1 (usa average="macro", visto che le classi sono tre). Verifica finale: chiudi tutto, riapri, ricarica il vincitore e fai una predizione senza riaddestrare nulla. Se funziona al primo colpo, hai capito la serializzazione.
Per approfondire, la pagina ufficiale Model persistence della documentazione di scikit-learn è il riferimento definitivo: confronta joblib, pickle, skops e ONNX, con tutti i dettagli sulla compatibilità tra versioni e le implicazioni di sicurezza che qui abbiamo solo sfiorato.
I commenti sono riservati agli iscritti.
Accedi per commentare