Il ML-Ops è l'insieme di pratiche che permette a un modello di funzionare per qualcun altro, in un altro momento e senza di te accanto. Questo modulo esiste per una ragione sola: è l'ultimo del corso e trasforma un notebook che funziona sul tuo portatile in qualcosa che gira per altre persone, in modo ripetibile, con una versione che puoi indicare e un modo per tornare indietro quando qualcosa va storto. Fino a qui hai imparato a costruire un modello; qui impari a consegnarlo.
Le parole del modulo
Il modulo usa poche parole che tornano in ogni lezione, e conviene fissarle subito. Un notebook (di solito Jupyter) è un documento che mescola codice, risultati e appunti, eseguito a celle in un ordine che scegli tu: è il quaderno di brutta, comodissimo per esplorare e pericoloso come fonte di verità. Il modello è il risultato dell'addestramento, un oggetto che dati nuovi in ingresso restituisce una previsione; la produzione è l'ambiente reale in cui serve utenti o altri programmi, il contrario di "sul mio portatile", come la differenza tra provare un discorso allo specchio e tenerlo davanti al pubblico.
Una dipendenza è qualcosa di cui il tuo codice ha bisogno e che non hai scritto tu (una libreria, un file, un servizio); l'ambiente è l'insieme di Python, librerie e versioni in cui il codice gira, e la stessa ricetta in una cucina diversa può venire diversa. La riproducibilità è la capacità di ottenere lo stesso risultato rifacendo lo stesso lavoro, tu tra un mese o un'altra persona oggi. Un artefatto (artifact) è un file prodotto da un processo e che vale la pena conservare: il modello salvato, un grafico, una configurazione. Un esperimento è un gruppo di prove sullo stesso problema e una run è una singola prova, cioè un addestramento con certi parametri; le riprendiamo in MLflow: tracciare gli esperimenti. Infine un servizio è un programma che resta acceso ad aspettare richieste, in contrasto con lo script, che parte, lavora e finisce.
Il filo del modulo, in un colpo d'occhio: il notebook diventa codice sotto controllo di versione, con un ambiente dichiarato, protetto da test, impacchettato in un container, pubblicato in un registry, tracciato con MLflow, rilasciato e osservato.
L'ultimo riquadro, il monitoraggio, è in giallo perché è l'unico che questo modulo nomina senza costruirlo: lo approfondisce la serie del sito su drift e retraining. Quello che segue nel modulo copre gli altri sette riquadri.
L'analogia: la cucina di casa e il ristorante
Una ricetta riuscita in cucina di casa non è ancora un piatto da ristorante. A casa funziona perché ci sei tu: conosci il forno che scalda male, sai che il sale è quello grosso, assaggi mentre cucini. Nel ristorante la stessa ricetta deve uscire uguale sabato sera, con un altro cuoco, con ingredienti arrivati da un altro fornitore. Servono ricetta scritta con le quantità, fornitori noti, procedure, controlli prima di servire e qualcuno che se ne accorge se il piatto torna indietro.
Il notebook è la cucina di casa. Funziona perché dentro c'è la tua memoria: l'ordine in cui hai lanciato le celle, il file che hai scaricato la settimana scorsa, la versione di scikit-learn che per caso era installata. Il ML-Ops è il passaggio alla cucina professionale.
Cosa c'è di implicito in un notebook
Quando un modello "funziona sul mio portatile" funzionano in realtà cinque cose che nessuno ha scritto da nessuna parte. Ognuna è una dipendenza nascosta, e ognuna ha una lezione di questo modulo che la rende esplicita.
| Dipendenza implicita | Come si rompe | Lezione e strumento |
|---|---|---|
| Il codice: l'ordine in cui hai eseguito le celle | Una cella cancellata definiva una variabile che usi ancora; il risultato non si rigenera | Git, GitHub, pytest |
| Le librerie: le versioni installate per caso | Un collega installa una versione più recente e i numeri cambiano, o il codice non parte | UV e lockfile |
| La configurazione: percorsi, indirizzi, chiavi | Un percorso /home/tuonome/dati.csv scritto nel codice non esiste altrove; una chiave finisce in un commit | variabili d'ambiente |
| Il sistema operativo e i pacchetti di sistema | Funziona su Ubuntu e non sul server, o viceversa | container |
| La memoria dei risultati: quale esperimento ha dato quel numero | Tre settimane dopo non sai più con quali parametri hai ottenuto il modello migliore | MLflow |
L'ultima riga manca spesso nelle liste "di buone pratiche", ma è la più specifica del machine learning: nel software tradizionale, se hai il codice hai il programma. In un progetto ML, il comportamento dipende da codice, dati, parametri e seed insieme. Se non li registri, il risultato non è riproducibile nemmeno da te. La serie MLOps: da esperimento a produzione sviluppa il quadro teorico (livelli di maturità, differenze con DevOps); qui lavoriamo sul lato pratico, comando per comando.
La mappa del modulo
Le lezioni seguono l'ordine in cui il problema si presenta nella vita di un progetto:
- Git e GitHub: per non perdere il lavoro e poterlo rivedere.
- Open source e licenze: perché quasi tutto ciò che userai non è tuo.
- UV: per avere lo stesso ambiente su ogni macchina.
- pytest: per sapere se una modifica ha rotto qualcosa, modello incluso.
- Variabili d'ambiente: per tenere la configurazione fuori dal codice.
- Container, Dockerfile e registry: per impacchettare il servizio.
- MLflow: per tracciare gli esperimenti e gestire le versioni del modello.
- Deploy, e quando una pipeline automatica conviene o non conviene.
L'ordine qui conta: non ha senso scrivere test se il codice non è sotto controllo di versione, e non ha senso impacchettare in un container un ambiente che non sai ricostruire. Ciascun passo presuppone quelli sopra, e per questo conviene seguire le lezioni nella sequenza data, partendo da Git, invece di saltare direttamente a Docker o a MLflow.
Un caso reale: il servizio ML di questo sito
Questo sito ha un microservizio di machine learning, ml-service/: un'applicazione FastAPI che fa ricerca semantica sugli articoli con RAG (embedding salvati in Redis con indice HNSW) e usa un modello linguistico locale servito da Ollama sullo stesso server. Non è un notebook: è la forma finale a cui puntiamo. Tre dettagli, tutti verificabili nel repository, mostrano che cosa significa "in produzione". Gira come unità systemd (infra/systemd/portfolio-ml.service) con Restart=always, quindi se si ferma riparte da solo. La sua configurazione arriva da un file di ambiente caricato con EnvironmentFile=, non dal codice, come vedremo nella lezione sulle variabili d'ambiente. E l'unità dichiara TimeoutStartSec=300 con un commento che spiega il motivo: all'avvio a freddo il servizio scarica un modello di embedding di circa 90 MB, lo carica e indicizza gli articoli, e può superare i 90 secondi di default di systemd. È un limite che nessun notebook ti avrebbe mai mostrato.
Il deploy è manuale per scelta: una riga di comando che aggiorna il codice, installa il pacchetto, sincronizza i file e riavvia il servizio. Il progetto vieta esplicitamente il deploy automatico. Nell'ultima lezione, deploy manuale contro CI/CD, vedremo perché è una decisione sensata e che cosa insegna sul modo di ragionare in ML-Ops.
Quanto ML-Ops serve davvero
La tentazione, studiando questa materia, è costruire subito la piattaforma completa: orchestratore, registry, pipeline, monitoraggio. È un errore costoso. Ogni pratica ha un costo di manutenzione, e il criterio giusto è: introduci la pratica quando il dolore che risolve è reale. Con due eccezioni che valgono sempre, perché costano poco e proteggono molto: Git dal primo giorno, anche da solo e anche per un esperimento di una sera, e un ambiente riproducibile (UV), perché ricostruirlo a posteriori è molto più faticoso che dichiararlo subito.
Tutto il resto segue i bisogni. Un modello che usi solo tu per un'analisi non ha bisogno di container. Un servizio che altri chiamano ogni giorno sì.
Il test della cucina pulita
Prima di dichiarare "pronto" un progetto, fai questo esercizio mentale (e, appena puoi, fallo davvero su un'altra macchina o in una cartella vuota). Rispondi a cinque domande:
- Qualcuno che clona il repository può rigenerare l'ambiente con un solo comando?
- Esiste un comando che esegue i test e dice sì o no?
- Nel repository c'è un solo percorso assoluto, una sola chiave, una sola password? (Non devono esserci.)
- Se il servizio parte con una configurazione sbagliata, si ferma subito con un messaggio chiaro, o parte e sbaglia in silenzio?
- Sai dire quale versione del modello sta rispondendo adesso, e come tornare alla precedente?
Ogni "no" corrisponde a una lezione di questo modulo: il primo a UV, il secondo a pytest, il terzo e il quarto alle variabili d'ambiente, il quinto al registry dei modelli. Non servono tutte subito, ma devi sapere quale stai rimandando e perché: un "no" consapevole è una scelta, un "no" scoperto in produzione è un incidente.
Una trappola da conoscere fin d'ora
L'errore più subdolo in ML-Ops non è quello che fa rumore, è quello che sembra un successo. Nel repository di questo sito, il 9 ottobre 2026, un build fallito è passato per riuscito: il comando era in pipe verso tail, lo script ha visto l'esito di tail (sempre zero) e non quello del build, quindi ha pubblicato la versione precedente. Il sito rispondeva 200 e sembrava tutto a posto, ma il codice nuovo non c'era. Lo smonteremo nella lezione sul deploy manuale. Per ora tienine la morale: "il servizio risponde" non dimostra che il rilascio sia avvenuto.
Cosa serve per seguire il modulo: strumenti e versioni
Gli esempi sono stati eseguiti su Linux con questi strumenti. Le versioni contano: i numeri e i messaggi che vedrai nelle lezioni vengono da queste, e strumenti più nuovi o più vecchi possono comportarsi in modo leggermente diverso. Per sapere cosa hai installato:
git --version # usato: git version 2.53.0
uv --version # usato: uv 0.12.21
docker --version # usato: Docker version 29.3.0
Ogni comando stampa la versione e basta: è il modo più rapido per accorgersi di una differenza. Sulla macchina di prova (misurato il 2026-11-12) le tre righe coincidono con quelle indicate nei commenti. Il resto (Python 3.12, pytest, scikit-learn, MLflow, FastAPI) lo installa UV dentro un ambiente isolato per ciascun progetto, quindi non occorre installarlo a mano sul sistema.
| Strumento | Versione usata | A cosa serve nel modulo |
|---|---|---|
| Git | 2.53 | Storia del codice |
| UV | 0.12 | Python, ambienti, lockfile |
| Python | 3.12 (nei container); 3.13 dove UV non era vincolato | Il linguaggio |
| pytest | 9.1 | Test |
| scikit-learn / pandas | 1.9.1 / 3.0.6 | Il modello d'esempio |
| Docker | 29.3 | Container e registry |
| MLflow | 3.17.0 | Tracking e registry dei modelli |
| FastAPI / uvicorn | 0.143 / 0.54 | Il servizio che serve il modello |
Di questa tabella ho verificato su questa macchina solo le prime tre righe di versione (Git, UV, Docker); le altre vengono dalle lezioni in cui ciascuno strumento viene installato, e lì è dichiarato come sono state ottenute.
Il progetto filo rosso: un classificatore di abbandono
Per non cambiare esempio a ogni lezione, il modulo costruisce un unico piccolo progetto: churn. "Churn" (letteralmente "rimescolamento") è il termine con cui le aziende indicano i clienti che smettono di usare un servizio; un classificatore è un modello che assegna un caso a una categoria, qui "abbandona" oppure "resta". I dati sono sintetici, generati da codice (così sono sempre riproducibili e non contengono dati di nessuno): ogni cliente ha i mesi di anzianità (tenure), il numero di reclami (complaints) e l'etichetta churn (1 = ha abbandonato). Il modello è una regressione logistica, la più semplice tra i classificatori, scelta apposta: il modulo non riguarda il modello, riguarda tutto ciò che lo circonda. Alla fine il progetto ha questa forma:
churn/
pyproject.toml # dipendenze dichiarate (UV)
uv.lock # dipendenze risolte (UV)
.python-version # versione di Python (UV)
.gitignore # cosa non versionare (Git)
src/churn/
data.py # genera i dati sintetici
model.py # addestra e valuta
tests/
test_data.py # test sui dati (pytest)
test_model.py # invarianti e soglie (pytest)
track.py # tracciamento MLflow
register.py # registry e alias
app.py + Dockerfile # servizio e container
Ogni file corrisponde a una lezione: puoi usare l'albero come indice. Se segui il modulo scrivendo il codice, alla fine avrai un progetto che non è un notebook. L'albero è la struttura finale che le lezioni costruiscono, non un progetto già pronto da scaricare: ogni lezione scrive i suoi file, e lo dichiara.
Il notebook che si rompe: lo stato nascosto in un esempio
Prima delle cinque dipendenze implicite, un esempio concreto di quella più subdola, il codice. In un notebook le celle condividono lo stato in memoria: una variabile definita in una cella resta disponibile in tutte le altre, anche se la cella che la definiva è stata modificata o cancellata. Simuliamo due celle con due blocchi di codice:
# Cella A (poi cancellata)
soglia = 0.7
# Cella B (ancora presente)
def e_a_rischio(probabilita):
return probabilita >= soglia
print(e_a_rischio(0.75))
# Output: True
Ho eseguito il blocco due volte con Python 3.14.4: l'uscita è True in entrambe. La funzione della cella B dipende da soglia, definita nella cella A. Finché il kernel (il processo Python che tiene in memoria lo stato del notebook) resta acceso, tutto funziona. Se cancelli la cella A e riavvii il kernel, la cella B fallisce con NameError: name 'soglia' is not defined: ho riprodotto proprio quel messaggio eseguendo la sola funzione senza la riga soglia = 0.7. Peggio: se cambi in A il valore a 0.9 e rilanci solo B, il risultato dipende da quali celle hai eseguito per ultime, non da ciò che c'è scritto. Il notebook ha uno stato nascosto: ciò che il programma fa dipende dalla storia delle tue azioni, non solo dal codice. Un file .py eseguito dall'alto in basso non ha questo problema, ed è per questo che il modulo porta il codice in file normali, sotto Git, con test.
Il rimedio immediato, anche prima di tutto il resto, è il gesto "riavvia il kernel ed esegui tutto dall'inizio": se il notebook dà gli stessi risultati, è almeno coerente con se stesso. Se no, hai scoperto una dipendenza nascosta prima che la scoprisse qualcun altro. Il costo è che un notebook lento da rieseguire scoraggia il gesto, ed è un buon motivo per spostare le parti costose in funzioni dentro file .py.
Il perché prima del come: cinque problemi, cinque strumenti
Ogni strumento di questo modulo esiste perché qualcuno, prima di te, si è fatto male in un modo preciso. Se impari prima il comando e poi il motivo, il comando sembra burocrazia; se impari prima il dolore, il comando diventa ovvio. Vediamo i cinque dolori uno per uno, con la loro "causa radice" e ciò che costa risolverli.
1. "Non so più cosa ho cambiato" (il codice)
Immagina di lavorare a un modello per tre settimane. Un giorno la metrica cala e vuoi capire quando. Se il lavoro è in un notebook, hai due strade: ricordare (non ci riesci) oppure confrontare file con nomi come modello_finale.ipynb, modello_finale2.ipynb, modello_finale_DAVVERO.ipynb. È il sintomo della mancanza di una storia: ogni versione è un oggetto separato, senza relazione dichiarata con le altre. Git risolve il problema alla radice, non con una convenzione sui nomi: una storia unica, in cui ogni cambiamento ha un autore, una data, un messaggio e un genitore. Il costo è imparare una decina di comandi e la disciplina di fare commit piccoli. Il guadagno è che la domanda "quando è cambiato?" ha una risposta meccanica (git bisect, nella lezione su Git) invece che una indagine.
2. "Da me funziona" (le librerie)
Il tuo codice usa librerie, e le librerie cambiano. Una funzione viene rinominata, un valore predefinito cambia, un algoritmo ottiene un'implementazione diversa e i numeri slittano di una cifra decimale. Quando chiedi a un collega di eseguire il tuo progetto e lui ha una versione diversa, il risultato può essere un errore (il caso fortunato, perché visibile) o un numero leggermente diverso (il caso sfortunato, perché silenzioso). La causa radice è che il codice dichiara cosa usa solo a metà: dice "scikit-learn", non "quella versione precisa di scikit-learn, con quelle versioni precise di NumPy e SciPy sotto". Il lockfile di UV (lezione su UV) scrive la seconda cosa. Il costo è un file in più da committare e una routine di aggiornamento; il guadagno è che "da me funziona" diventa "funziona uguale ovunque, finché non cambio io il lock".
3. "Si è rotto e non me ne sono accorto" (i test)
Modificare codice funzionante è rischioso perché le conseguenze sono lontane dalla causa: cambi una funzione di pulizia dei dati e, tre moduli più in là, il modello peggiora di due punti senza alcun errore. La causa radice è l'assenza di un controllo automatico che dica, a ogni modifica, "il comportamento che mi aspettavo c'è ancora". I test sono quel controllo. Nel machine learning contano di più che nel software ordinario, perché il guasto tipico non è un errore ma un degrado silenzioso: il programma gira, produce numeri plausibili, e sono peggiori di prima. Un test con soglia (testare un modello) trasforma il degrado silenzioso in un rosso visibile. Il costo è tempo per scriverli e per tenerli veloci; il guadagno è la possibilità di cambiare codice senza paura.
4. "Funziona sul mio portatile e non sul server" (il sistema)
Anche con le stesse librerie, il programma gira dentro un sistema operativo, con le sue librerie di sistema, le sue variabili d'ambiente, i suoi permessi. Una libreria nativa mancante, una versione diversa di un compilatore, un percorso che esiste solo da te: sono tutte cause di fallimento che il codice non dichiara. La causa radice è che l'ambiente di esecuzione non è descritto. Due risposte, di peso diverso: la configurazione esterna (variabili d'ambiente) che separa ciò che cambia da ciò che no, e il container (concetti di container) che impacchetta anche il sistema di base. Il container ha un costo reale (immagini da costruire e conservare, un livello in più nel debug) e questo progetto, che ha un'unica macchina, ha scelto di non pagarlo per i suoi servizi: l'unità systemd vista sopra gira direttamente sul server. Anche questa è una decisione di ML-Ops.
5. "Quale modello è quello giusto?" (la memoria dei risultati)
Addestri venti varianti di un modello cambiando parametri e dati. Tre giorni dopo ti chiedono "quello che hai consegnato martedì, con quali parametri era?". Se non l'hai annotato, rifai l'esperimento sperando di ottenere lo stesso risultato; se non hai fissato il seed, non lo ottieni. La causa radice è che un esperimento non ha identità: è un evento che accade e svanisce. MLflow (tracking e registry) gli dà un'identità: una run con un identificatore, i suoi parametri, le sue metriche, il suo modello. E il registry dà un'identità anche al modello in servizio: una versione numerata, non "il file che c'è in quella cartella". Il costo è abituarsi a passare dal tracciamento invece di lanciare script a mano; il guadagno è la riproducibilità e la capacità di tornare indietro.
Perché ML-Ops non è solo DevOps applicato ai modelli
Chi viene dal software conosce già il DevOps: l'insieme di pratiche che unisce sviluppo e gestione operativa (versionamento, test automatici, rilascio ripetibile, monitoraggio). Tutto quello che abbiamo visto finora ne deriva. Ma tre cose rendono il machine learning diverso, e spiegano perché esiste un nome a parte.
Primo, il comportamento dipende dai dati, non solo dal codice. In un programma ordinario, se il codice è identico, il comportamento è identico. In un modello, lo stesso codice addestrato su dati diversi produce un programma diverso. Quindi versionare solo il codice non basta: servono anche l'identità dei dati e i parametri. Per questo il tracciamento registra dataset e seed insieme ai parametri, e per questo il registry lega ogni versione alla run che l'ha prodotta.
Secondo, lo sviluppo è sperimentale. Un software si scrive per raggiungere un comportamento noto; un modello si cerca provando molte varianti e misurando. Il lavoro quotidiano genera tantissimi risultati intermedi, quasi tutti da scartare, e la parte difficile è non perdere quello buono. Un sistema di tracciamento è il "quaderno" che questo modo di lavorare richiede e che il software ordinario non richiede con la stessa urgenza.
Terzo, il guasto è spesso silenzioso, e arriva con il tempo. Un servizio ordinario si rompe con un errore. Un modello può continuare a rispondere perfettamente mentre il mondo cambia intorno a lui: i clienti di oggi non assomigliano a quelli su cui è stato addestrato, e la qualità cala senza una riga di errore. Questo fenomeno (in inglese drift, "deriva") richiede un monitoraggio dei risultati e dei dati, non solo delle risorse del server. Non lo trattiamo in questo modulo, ma la serie drift detection e retraining automatico lo affronta, e la serie MLOps: da esperimento a produzione descrive i livelli di maturità con cui un team passa dal lavoro manuale al ciclo automatizzato. Il documento di Sculley e colleghi, «Hidden Technical Debt in Machine Learning Systems», è la lettura classica su perché i sistemi ML accumulano debito tecnico in modo diverso dal software ordinario.
Il ciclo di vita completo di un modello
Mettendo insieme i pezzi, la vita di un modello in produzione non è una linea (addestro, rilascio, fine) ma un ciclo:
- Addestrare: da codice e dati versionati, in un ambiente riproducibile, producendo un modello e le sue metriche.
- Verificare: test sui dati, sul comportamento e sulla qualità minima. Se un test fallisce, il modello non va avanti.
- Registrare: il modello entra nel catalogo con una versione e le sue metriche.
- Impacchettare e rilasciare: il servizio, con la sua configurazione e il suo ambiente, arriva dove deve girare.
- Osservare: il servizio e il modello vengono monitorati. Quando qualcosa degrada, si torna al punto 1 con nuovi dati.
Il modulo copre i punti dall'1 al 4. Il punto 5 chiude il cerchio ed è ciò che distingue un esperimento da un prodotto: un prodotto ha qualcuno che si accorge quando non funziona più. Il punto 2 fa da cancello: nessun modello va registrato se prima non ha superato i test, ed è l'unico passo in cui una decisione umana può essere sostituita da una regola scritta.
Il progetto reale: cosa c'è dietro "un servizio ML"
Per vedere quanto sia concreta questa lista, basta contare cosa tiene in piedi questo sito. Nella cartella infra/systemd/ del repository ci sono 50 unità di servizio e 64 timer (contati il 2026-11-12 con ls infra/systemd/*.service | wc -l e lo stesso per *.timer). Non sono tutti servizi di machine learning: molti sono aggiornamenti periodici di dati, il riaddestramento settimanale del modello sportivo, i controlli SEO. Il punto non è il numero, è l'idea: un sistema che funziona da solo, di notte e senza di te, è fatto di tanti piccoli programmi con un orario, una configurazione, un modo di riprovare se falliscono e un registro di ciò che hanno fatto. Quella è la "produzione". Un notebook non ha nessuna di queste proprietà, ed è la distanza che il modulo ti insegna a colmare.
Tre trappole di deploy documentate nel repository
Il runbook di deploy del progetto (docs/01_foundation/04_deploy/11_deploy_manuale.md, sezione 0) raccoglie gli errori che hanno già fatto danni, scritti perché non si ripetano. Tre li ritroverai nella lezione sul deploy manuale. Il primo è il comando in pipe che nasconde un fallimento (build | tail): un build fallito è passato per riuscito e il deploy ha pubblicato la versione precedente, con il sito che rispondeva regolarmente. Il secondo è l'interruzione di un processo che colpisce anche il suo "involucro": un pkill -f con una stringa contenuta anche nel comando che avvolge il processo ha interrotto una catena di passi che doveva proseguire anche dopo un fallimento previsto. Il terzo è un controllo che interroga la produzione dalla macchina di produzione: durante la compilazione, la fase preparatoria invia una richiesta HEAD a ciascuna delle circa 5.139 URL delle mappe del sito, contro il sito stesso, per 9 minuti nel caso migliore (cifre del runbook, §0.3, non rimisurate qui). Il runbook prescrive di saltarlo nel deploy con SKIP_SITEMAP_AUDIT=1.
C'è un quarto dettaglio, nello stesso runbook: lo stesso errore dei comandi in pipe aveva già mascherato, poche ore prima, un git pull fallito perché alcuni file generati dalla fase preparatoria avevano sporcato la cartella di lavoro. Il prebuild del frontend esegue diversi script generate:* (serie, manifest del blog, metadati) che scrivono file dentro il repository: se quei file risultano modificati, un successivo aggiornamento del codice può rifiutarsi di procedere. È un esempio perfetto di dipendenza implicita: nessun file dice "il build lascia la cartella sporca", ma se non lo sai il rilascio successivo fallisce in un modo che sembra non c'entrare nulla.
Errori di mentalità che conviene togliersi subito
Il primo è credere che il modello sia il prodotto: il modello è una parte, il prodotto è il modello più il servizio che lo espone, i dati che lo alimentano, i controlli che lo proteggono e le persone che lo mantengono. Il secondo è il "prima faccio funzionare, poi lo sistemo": con il codice sperimentale è un buon approccio, a patto che "dopo" arrivi davvero, perché il rischio è che il notebook di esplorazione diventi, per inerzia, il sistema in produzione. Il terzo è pensare che l'automazione sia sempre meglio: automatizzare un processo che non si capisce moltiplica i suoi difetti, e l'ordine giusto è capire, documentare, poi eventualmente automatizzare (deploy manuale contro CI/CD).
Il quarto è "i test sono per i programmatori, io faccio data science": i test di un modello sono proprio il luogo in cui l'esperienza di data science (che soglia ha senso? quali comportamenti sono assurdi?) diventa un controllo che nessuno può dimenticare. Il quinto è "serve uno strumento per ogni cosa": serve il problema che lo strumento risolve, e se il problema non c'è ancora, lo strumento è un costo senza beneficio.
Cosa sai fare adesso
Sai dire perché un notebook che funziona non è ancora un servizio, e sai nominare le cinque cose implicite che lo tengono in piedi: codice, librerie, configurazione, sistema, memoria dei risultati. Sai usare il test della cucina pulita su un progetto tuo e scoprire quale lezione ti serve per prima. Sai anche resistere alla tentazione della piattaforma completa, tenendo solo Git e un ambiente riproducibile come punti fermi dal primo giorno. Si parte dalla base di tutto: Git, la macchina del tempo del tuo progetto.
Nella stessa serie trovi anche Far girare un modello in locale: quantizzazione, memoria e latenza misurate (articolo precedente).
Fonti
- systemd.service, manuale ufficiale di systemd - il significato di
Restart=,EnvironmentFile=eTimeoutStartSec=usati dall'unità del servizio ML, consultata il 2026-11-12 - Documentazione di FastAPI - il framework del microservizio descritto nel caso reale, consultata il 2026-11-12
- Sculley et al., «Hidden Technical Debt in Machine Learning Systems», NeurIPS 2015 - la tesi che i sistemi ML accumulano debito tecnico in modo specifico; ho letto la scheda con l'abstract, non il testo integrale, consultata il 2026-11-12
- Il file
infra/systemd/portfolio-ml.servicee il runbookdocs/01_foundation/04_deploy/11_deploy_manuale.mddel repository del sito - unità, timeout, trappole di deploy e la cifra delle URL delle sitemap; i conteggi di unità e timer vengono dals infra/systemd/*.service | wc -l(50) els infra/systemd/*.timer | wc -l(64), 2026-11-12
Il blocco Python dello stato nascosto è stato eseguito due volte con Python 3.14.4 con uscita identica (True); le versioni di Git, UV e Docker sono state lette con i comandi mostrati. Le versioni di pytest, scikit-learn, pandas, MLflow e FastAPI nella tabella non sono state rimisurate qui. I nomi dei clienti, le colonne e i dati del progetto "churn" sono inventati.
Commenti
Caricamento commenti...
Accedi per commentare