MLOps: Da Esperimento a Produzione
Ogni data scientist ha vissuto questo momento: il modello funziona perfettamente nel Jupyter notebook, le metriche sono eccellenti, il team applaude durante la demo. Poi arriva la domanda fatale: "Quando lo mettiamo in produzione?". E inizia il silenzio. Secondo le stime di settore, fino all'85% dei progetti di machine learning non raggiunge mai l'ambiente di produzione. Non perchè i modelli non funzionino, ma perchè manca l'infrastruttura, il processo e la disciplina per farli funzionare in modo affidabile e continuo.
MLOps (Machine Learning Operations) nasce esattamente per colmare questo divario. Non si tratta di una singola tecnologia, ma di un insieme di pratiche, strumenti e cultura che trasformano esperimenti isolati in sistemi ML di produzione robusti. In questo articolo vedremo cosa significa MLOps, perchè e diventato indispensabile e come iniziare ad applicarlo concretamente, anche con un budget limitato.
Cosa Imparerai
- perchè la maggior parte dei progetti ML non arriva in produzione e come MLOps risolve il problema
- Le differenze chiave tra DevOps e MLOps
- I 3 livelli di maturita MLOps secondo il modello Google
- Il ciclo di vita completo di un modello ML in produzione
- Come fare experiment tracking con MLflow
- Come servire un modello con FastAPI e Docker
- Lo stack open-source per iniziare con meno di 5.000 EUR/anno
Cos'è MLOps e perchè Serve
MLOps e l'applicazione dei principi di DevOps al ciclo di vita del machine learning. Cosi come DevOps ha unito sviluppo e operations per il software tradizionale, MLOps unisce data science, engineering e operations per i sistemi ML. L'obiettivo e automatizzare e rendere riproducibili tutte le fasi: dalla preparazione dei dati al training, dalla validazione al deployment, dal monitoring al retraining.
DevOps vs MLOps: Le Differenze Chiave
Chi viene dal mondo software potrebbe pensare che basti applicare le stesse pratiche DevOps ai modelli ML. In realta ci sono differenze fondamentali che rendono MLOps una disciplina a se.
| Aspetto | DevOps | MLOps |
|---|---|---|
| Artefatto | Codice sorgente | Codice + Dati + Modello |
| Versionamento | Git per il codice | Git + DVC per dati e modelli |
| Testing | Unit test, integration test | Data validation, model validation, A/B test |
| CI/CD | Build, test, deploy codice | Train, validate, deploy modello |
| Monitoring | Latenza, errori, uptime | Data drift, concept drift, model performance |
| Degradazione | Bug espliciti | Degradazione silenziosa nel tempo |
| Riproducibilità | Stesso codice = stesso output | Stesso codice + stessi dati + stesso seed = stesso output |
La differenza più critica e la degradazione silenziosa. Un servizio software tradizionale funziona o non funziona: se c'è un bug, genera un errore. Un modello ML può continuare a restituire predizioni senza errori tecnici, ma con accuratezza progressivamente peggiore perchè i dati in ingresso sono cambiati rispetto al training. Senza monitoring specifico, nessuno se ne accorge fino a quando gli utenti iniziano a lamentarsi.
Il Problema della "Valle della Morte" ML
Gartner stima che il 30% dei progetti di AI generativa verrà abbandonato dopo la fase di proof-of-concept entro fine 2025, a causa di scarsa qualità dei dati, controlli di rischio inadeguati, costi crescenti o valore di business poco chiaro. MLOps affronta sistematicamente ciascuno di questi problemi.
Il Mercato MLOps: Numeri e Tendenze
Il mercato MLOps sta crescendo a un ritmo impressionante. Secondo le analisi di settore, il valore globale del mercato MLOps era stimato tra i 2 e i 3 miliardi di dollari nel 2025, con proiezioni che lo vedono raggiungere tra i 25 e i 56 miliardi di dollari entro il 2035, con un CAGR (tasso di crescita annuale composto) tra il 29% e il 42% a seconda delle fonti.
Questi numeri riflettono una realta concreta: le aziende stanno investendo massicciamente per portare i modelli ML in produzione. Secondo le stime di mercato, oltre il 70% delle grandi aziende in Nord America gestisce workload AI in produzione, e più del 55% ha integrato sistemi di monitoring automatico dei modelli. Tuttavia, quasi due terzi delle organizzazioni restano ancora bloccate nella fase pilota, senza riuscire a scalare l'AI a livello enterprise.
I 3 Livelli di Maturita MLOps
Google ha definito un modello di maturita MLOps in 3 livelli che e diventato lo standard de facto del settore. Ogni livello rappresenta un grado crescente di automazione e affidabilità nel ciclo di vita ML.
Level 0: Processo Manuale
Al livello 0, ogni fase e manuale. Il data scientist lavora nel suo notebook, addestra il modello localmente, lo esporta come file e lo consegna al team di engineering che lo wrappa in un'API. Non c'è automazione, non c'è monitoring, non c'è retraining automatico.
| Caratteristica | Level 0 |
|---|---|
| Training | Manuale, nel notebook |
| Deploy | Manuale, consegna del file .pkl o .h5 |
| Monitoring | Nessuno o manuale |
| Retraining | Solo su richiesta esplicita |
| Riproducibilità | Scarsa o assente |
Questo livello e comune nelle organizzazioni che iniziano ad applicare ML ai propri use case. Può essere sufficiente quando i modelli vengono aggiornati raramente e i dati cambiano poco, ma non scala.
Level 1: Automazione della Pipeline ML
Al livello 1, il training viene automatizzato tramite una pipeline ML. Non si deploya più un singolo modello, ma l'intera pipeline che lo produce. Questo permette il continuous training: quando arrivano nuovi dati, la pipeline riadestra automaticamente il modello.
| Caratteristica | Level 1 |
|---|---|
| Training | Automatico via pipeline |
| Deploy | Pipeline automatizzata |
| Monitoring | Performance del modello + trigger retraining |
| Retraining | Automatico su nuovi dati o degradazione |
| Riproducibilità | Buona (pipeline versionate) |
Il livello 1 e sufficiente quando i dati cambiano frequentemente ma l'approccio ML resta stabile. La pipeline e la stessa, ma viene rieseguita periodicamente con dati freschi.
Level 2: CI/CD per Machine Learning
Al livello 2, si aggiunge un sistema completo di CI/CD specifico per ML. Non cambia solo il dato, ma anche il codice della pipeline, le feature, gli iperparametri, l'architettura del modello. Ogni modifica passa attraverso test automatici, validazione e deployment controllato.
| Caratteristica | Level 2 |
|---|---|
| Training | Automatico + CI/CD sulla pipeline stessa |
| Deploy | Blue/green, canary, A/B testing |
| Monitoring | Completo: data drift, concept drift, performance, latenza |
| Retraining | Automatico con validazione e rollback |
| Riproducibilità | Completa (codice + dati + ambiente versionati) |
Raggiungere il livello 2 e l'obiettivo per le organizzazioni mature. Richiede investimenti significativi in infrastruttura e cultura, ma e l'unico modo per gestire decine o centinaia di modelli in produzione in modo sostenibile.
Il Ciclo di Vita MLOps
Il ciclo di vita di un modello ML in produzione e un processo iterativo che attraversa sei fasi principali. A differenza dello sviluppo software tradizionale, questo ciclo non termina mai: un modello in produzione richiede manutenzione continua.
+----------+ +---------+ +----------+
| DATA |---->| TRAIN |---->| EVALUATE |
| Collect | | Feature | | Validate |
| Clean | | Train | | Compare |
| Version | | Tune | | Approve |
+----------+ +---------+ +----------+
^ |
| v
+----------+ +---------+ +----------+
| RETRAIN |<----| MONITOR |<----| DEPLOY |
| Trigger | | Drift | | Stage |
| Schedule | | Metrics | | Canary |
| Auto | | Alert | | Release |
+----------+ +---------+ +----------+
1. Data: Raccolta, Pulizia e Versionamento
Tutto parte dai dati. In questa fase si raccolgono i dati grezzi, si puliscono (gestendo valori mancanti, outlier, duplicati), si trasformano in feature utili e si versionano. Il versionamento dei dati e fondamentale: per riprodurre un modello serve sapere esattamente quali dati sono stati usati per il training. Strumenti come DVC (Data Version Control) permettono di versionare dataset di grandi dimensioni in modo simile a Git.
2. Train: Feature Engineering e Addestramento
Con i dati pronti, si costruiscono le feature, si sceglie l'algoritmo e si addestra il modello. Ogni esperimento (combinazione di iperparametri, feature, architettura) viene tracciato con i relativi parametri e metriche. Strumenti come MLflow rendono questo processo sistematico e riproducibile.
3. Evaluate: Validazione e Confronto
Il modello addestrato viene validato contro metriche predefinite (accuracy, F1-score, RMSE, AUC) e confrontato con la versione attualmente in produzione. Se il nuovo modello non supera le soglie minime o non migliora il precedente, non viene promosso.
4. Deploy: Staging, Canary e Release
Il modello approvato passa attraverso ambienti progressivi: staging per test di integrazione, canary per validazione con traffico reale limitato, e infine produzione completa. Strategie come blue/green deployment e canary release minimizzano il rischio.
5. Monitor: Drift, Metriche e Alert
In produzione, il modello viene monitorato continuamente. Si tracciano metriche tecniche (latenza, throughput, errori) e metriche ML (accuracy su dati reali, distribuzione delle predizioni, data drift). Gli alert scattano quando le metriche scendono sotto le soglie.
6. Retrain: Trigger e Automazione
Quando il monitoring rileva degradazione, si attiva il retraining. Questo può essere schedulato (es. settimanale), basato su trigger (es. accuracy sotto il 90%) o manuale. Il nuovo modello attraversa nuovamente le fasi di evaluate e deploy.
Lo Stack MLOps Open-Source
Uno dei vantaggi di MLOps e che esiste un ecosistema open-source maturo che copre ogni fase del ciclo di vita. Non serve acquistare piattaforme enterprise costose per iniziare: con gli strumenti giusti si può costruire una pipeline MLOps completa.
| Fase | Strumento | Funzione |
|---|---|---|
| Data Versioning | DVC | Versioning di dataset e modelli, integrato con Git |
| Experiment Tracking | MLflow | Logging parametri, metriche, artefatti per ogni esperimento |
| Model Registry | MLflow Model Registry | Versionamento e promozione modelli (staging/production) |
| Pipeline Orchestration | Prefect / Airflow | Orchestrazione workflow, scheduling, retry |
| Model Serving | FastAPI + Docker | API REST per servire predizioni, containerizzata |
| Containerizzazione | Docker + K8s | Ambienti riproducibili, scalabilità orizzontale |
| Monitoring | Prometheus + Grafana | Metriche, dashboard, alerting |
| Data Validation | Great Expectations | Test automatici sulla qualità dei dati |
Da Notebook a Pipeline: Esempio Pratico
Vediamo il passaggio più comune che ogni team ML deve affrontare: trasformare codice scritto in un Jupyter notebook in una pipeline modulare e riproducibile. Prendiamo un esempio reale di classificazione e vediamo come ristrutturarlo.
Prima: Il Notebook Monolitico
Ecco il tipico notebook dove tutto vive in un unico file, senza separazione delle responsabilità, senza logging, senza versionamento.
# Cella 1: Tutto nello stesso notebook
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score
import pickle
# Caricamento dati
df = pd.read_csv("data/customers.csv")
# Feature engineering inline
df["age_group"] = pd.cut(df["age"], bins=[0, 25, 45, 65, 100],
labels=["young", "adult", "senior", "elderly"])
df["total_spend"] = df["orders"] * df["avg_order_value"]
# Split
X = df[["age", "total_spend", "visits", "days_since_last"]]
y = df["churned"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# Training - parametri hardcoded
model = RandomForestClassifier(n_estimators=100, max_depth=10)
model.fit(X_train, y_train)
# Valutazione - print a schermo
y_pred = model.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")
print(f"F1: {f1_score(y_test, y_pred)}")
# Salvataggio - pickle senza versioning
with open("model.pkl", "wb") as f:
pickle.dump(model, f)
print("Modello salvato!")
Problemi del Notebook Monolitico
- Non riproducibile: nessun seed, nessun versionamento dati
- Non tracciato: i parametri e le metriche vivono solo nell'output del notebook
- Non testabile: nessuna funzione isolata da testare
- Non deployabile: il pickle non e una API
- Non manutenibile: modificare una feature richiede di rieseguire tutto
Dopo: Pipeline Modulare
Ristrutturiamo il codice in moduli separati, ciascuno con una responsabilità precisa. Ogni funzione e testabile, ogni parametro e configurabile, ogni metrica e tracciata.
"""Modulo per la preparazione e trasformazione dei dati."""
import pandas as pd
from pathlib import Path
from typing import Tuple
def load_data(path: str) -> pd.DataFrame:
"""Carica il dataset dal path specificato."""
filepath = Path(path)
if not filepath.exists():
raise FileNotFoundError(f"Dataset non trovato: {path}")
return pd.read_csv(filepath)
def create_features(df: pd.DataFrame) -> pd.DataFrame:
"""Crea le feature derivate per il modello."""
result = df.copy()
result["age_group"] = pd.cut(
result["age"],
bins=[0, 25, 45, 65, 100],
labels=["young", "adult", "senior", "elderly"]
)
result["total_spend"] = result["orders"] * result["avg_order_value"]
return result
def split_data(
df: pd.DataFrame,
target_col: str = "churned",
feature_cols: list = None,
test_size: float = 0.2,
random_state: int = 42
) -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]:
"""Split dei dati in train/test con seed fisso per riproducibilità."""
from sklearn.model_selection import train_test_split
if feature_cols is None:
feature_cols = ["age", "total_spend", "visits", "days_since_last"]
X = df[feature_cols]
y = df[target_col]
return train_test_split(X, y, test_size=test_size, random_state=random_state)
"""Modulo per il training e la valutazione del modello."""
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
from typing import Dict, Any
import pandas as pd
def train_model(
X_train: pd.DataFrame,
y_train: pd.Series,
n_estimators: int = 100,
max_depth: int = 10,
random_state: int = 42
) -> RandomForestClassifier:
"""Addestra un RandomForestClassifier con parametri configurabili."""
model = RandomForestClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
random_state=random_state
)
model.fit(X_train, y_train)
return model
def evaluate_model(
model: RandomForestClassifier,
X_test: pd.DataFrame,
y_test: pd.Series
) -> Dict[str, float]:
"""Valuta il modello e restituisce un dizionario di metriche."""
y_pred = model.predict(X_test)
return {
"accuracy": accuracy_score(y_test, y_pred),
"f1_score": f1_score(y_test, y_pred),
"precision": precision_score(y_test, y_pred),
"recall": recall_score(y_test, y_pred),
}
"""Pipeline principale che orchestra tutte le fasi."""
from src.data.preprocessing import load_data, create_features, split_data
from src.models.trainer import train_model, evaluate_model
import yaml
from pathlib import Path
def run_pipeline(config_path: str = "config.yaml") -> None:
"""Esegue l'intera pipeline ML con configurazione esterna."""
# 1. Carica configurazione
with open(config_path) as f:
config = yaml.safe_load(f)
# 2. Data preparation
print("[1/4] Caricamento dati...")
df = load_data(config["data"]["path"])
df = create_features(df)
# 3. Split
print("[2/4] Split train/test...")
X_train, X_test, y_train, y_test = split_data(
df,
test_size=config["data"]["test_size"],
random_state=config["data"]["random_state"]
)
# 4. Training
print("[3/4] Training modello...")
model = train_model(
X_train, y_train,
n_estimators=config["model"]["n_estimators"],
max_depth=config["model"]["max_depth"],
random_state=config["model"]["random_state"]
)
# 5. Evaluation
print("[4/4] Valutazione...")
metrics = evaluate_model(model, X_test, y_test)
for name, value in metrics.items():
print(f" {name}: {value:.4f}")
if __name__ == "__main__":
run_pipeline()
# config.yaml - Tutti i parametri in un unico file
data:
path: "data/customers.csv"
test_size: 0.2
random_state: 42
feature_cols:
- age
- total_spend
- visits
- days_since_last
model:
algorithm: "random_forest"
n_estimators: 100
max_depth: 10
random_state: 42
evaluation:
metrics:
- accuracy
- f1_score
- precision
- recall
min_accuracy: 0.85
Vantaggi della Pipeline Modulare
- Riproducibile: seed fisso, configurazione esternalizzata, dati versionabili
- Testabile: ogni funzione e isolata e può avere unit test dedicati
- Manutenibile: modificare le feature non tocca il training e viceversa
- Configurabile: cambiare iperparametri senza toccare il codice
- Automatizzabile: la pipeline può essere eseguita da CI/CD
Experiment Tracking con MLflow
Quante volte hai cambiato un iperparametro e poi non ricordavi quale combinazione aveva dato il risultato migliore? L'experiment tracking risolve questo problema registrando automaticamente parametri, metriche e artefatti di ogni esperimento.
MLflow e lo strumento open-source più diffuso per experiment tracking. Offre un server con UI web per visualizzare e confrontare esperimenti, un'API Python per il logging e un Model Registry per gestire il ciclo di vita dei modelli.
Setup e Primo Esperimento
# Installazione
pip install mlflow
# Avvio del tracking server locale
mlflow server --host 127.0.0.1 --port 5000
"""Pipeline ML con experiment tracking via MLflow."""
import mlflow
import mlflow.sklearn
from src.data.preprocessing import load_data, create_features, split_data
from src.models.trainer import train_model, evaluate_model
def run_tracked_pipeline(config: dict) -> None:
"""Esegue la pipeline tracciando tutto con MLflow."""
# Imposta il tracking URI (server locale o remoto)
mlflow.set_tracking_uri("http://127.0.0.1:5000")
mlflow.set_experiment("churn-prediction")
with mlflow.start_run(run_name="rf-baseline") as run:
# Log dei parametri
mlflow.log_param("algorithm", "RandomForest")
mlflow.log_param("n_estimators", config["model"]["n_estimators"])
mlflow.log_param("max_depth", config["model"]["max_depth"])
mlflow.log_param("test_size", config["data"]["test_size"])
mlflow.log_param("random_state", config["data"]["random_state"])
# Data preparation
df = load_data(config["data"]["path"])
df = create_features(df)
X_train, X_test, y_train, y_test = split_data(
df,
test_size=config["data"]["test_size"],
random_state=config["data"]["random_state"]
)
# Log dimensioni dataset
mlflow.log_param("train_samples", len(X_train))
mlflow.log_param("test_samples", len(X_test))
mlflow.log_param("n_features", X_train.shape[1])
# Training
model = train_model(
X_train, y_train,
n_estimators=config["model"]["n_estimators"],
max_depth=config["model"]["max_depth"]
)
# Evaluation
metrics = evaluate_model(model, X_test, y_test)
# Log delle metriche
for name, value in metrics.items():
mlflow.log_metric(name, value)
# Log del modello come artefatto
mlflow.sklearn.log_model(
model,
artifact_path="model",
registered_model_name="churn-classifier"
)
# Log della configurazione come artefatto
mlflow.log_artifact("config.yaml")
print(f"Run ID: {run.info.run_id}")
print(f"Metriche: {metrics}")
Dopo aver eseguito diversi esperimenti, apri http://127.0.0.1:5000 nel browser.
La UI di MLflow mostra una tabella con tutti gli esperimenti, permettendoti di confrontare
metriche, ordinare per performance e visualizzare grafici dei parametri vs metriche.
Model Registry: Versionare i Modelli
Cosi come il codice viene versionato con Git, i modelli ML devono essere versionati con un Model Registry. MLflow Model Registry offre un sistema centralizzato per gestire il ciclo di vita dei modelli attraverso tre stadi.
| Stadio | Descrizione | Chi lo usa |
|---|---|---|
| None / Staging | Modello in fase di test e validazione | Data scientist, QA |
| Production | Modello approvato, serve traffico reale | API di serving, utenti finali |
| Archived | Modello dismesso, mantenuto per audit | Compliance, rollback |
"""Gestione del ciclo di vita del modello con MLflow Model Registry."""
from mlflow.tracking import MlflowClient
client = MlflowClient("http://127.0.0.1:5000")
# Recupera l'ultima versione del modello in staging
latest_versions = client.get_latest_versions(
name="churn-classifier",
stages=["Staging"]
)
if latest_versions:
version = latest_versions[0].version
print(f"Modello in staging: v{version}")
# Promuovi a Production dopo validazione
client.transition_model_version_stage(
name="churn-classifier",
version=version,
stage="Production",
archive_existing_versions=True # Archivia versione precedente
)
print(f"Modello v{version} promosso a Production")
# Carica il modello in produzione per inference
import mlflow.pyfunc
model = mlflow.pyfunc.load_model("models:/churn-classifier/Production")
prediction = model.predict(new_data)
Deployment: FastAPI + Docker
Un modello ML in produzione viene tipicamente esposto come API REST. FastAPI e la scelta ideale per Python: e veloce (basato su ASGI), genera documentazione automatica (OpenAPI/Swagger) e ha un'eccellente validazione dei dati tramite Pydantic. Containerizzando con Docker, otteniamo un artefatto deployabile ovunque.
"""API REST per servire predizioni del modello ML."""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import mlflow.pyfunc
import pandas as pd
import logging
from typing import List
# Configurazione logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="Churn Prediction API",
description="API per predizioni di churn basata su ML",
version="1.0.0"
)
class PredictionRequest(BaseModel):
"""Schema della richiesta di predizione."""
age: int = Field(..., ge=0, le=120, description="Eta del cliente")
total_spend: float = Field(..., ge=0, description="Spesa totale")
visits: int = Field(..., ge=0, description="Numero visite")
days_since_last: int = Field(..., ge=0, description="Giorni dall'ultima visita")
class PredictionResponse(BaseModel):
"""Schema della risposta di predizione."""
prediction: int
probability: float
model_version: str
# Caricamento modello all'avvio
MODEL_NAME = "churn-classifier"
MODEL_STAGE = "Production"
model = None
model_version = "unknown"
@app.on_event("startup")
async def load_model():
"""Carica il modello MLflow all'avvio del server."""
global model, model_version
try:
model_uri = f"models:/{MODEL_NAME}/{MODEL_STAGE}"
model = mlflow.pyfunc.load_model(model_uri)
model_version = model.metadata.run_id[:8]
logger.info(f"Modello caricato: {MODEL_NAME} ({model_version})")
except Exception as e:
logger.error(f"Errore caricamento modello: {e}")
raise
@app.get("/health")
async def health_check():
"""Endpoint di health check."""
return {"status": "healthy", "model_loaded": model is not None}
@app.post("/predict", response_model=PredictionResponse)
async def predict(request: PredictionRequest):
"""Genera una predizione di churn per un cliente."""
if model is None:
raise HTTPException(status_code=503, detail="Modello non caricato")
try:
input_data = pd.DataFrame([request.model_dump()])
prediction = model.predict(input_data)
probability = float(prediction[0]) if hasattr(prediction[0], '__float__') else 0.0
return PredictionResponse(
prediction=int(prediction[0]),
probability=probability,
model_version=model_version
)
except Exception as e:
logger.error(f"Errore predizione: {e}")
raise HTTPException(status_code=500, detail="Errore nella predizione")
@app.post("/predict/batch", response_model=List[PredictionResponse])
async def predict_batch(requests: List[PredictionRequest]):
"""Genera predizioni batch per più clienti."""
if model is None:
raise HTTPException(status_code=503, detail="Modello non caricato")
input_data = pd.DataFrame([r.model_dump() for r in requests])
predictions = model.predict(input_data)
return [
PredictionResponse(
prediction=int(p),
probability=float(p),
model_version=model_version
)
for p in predictions
]
# Dockerfile per il serving del modello ML
FROM python:3.11-slim
WORKDIR /app
# Dipendenze di sistema
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Dipendenze Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Codice applicazione
COPY src/serving/ ./serving/
COPY config.yaml .
# Porta del servizio
EXPOSE 8000
# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Avvio con uvicorn
CMD ["uvicorn", "serving.app:app", "--host", "0.0.0.0", "--port", "8000"]
# Build dell'immagine
docker build -t churn-api:v1.0.0 .
# Avvio del container
docker run -d \
--name churn-api \
-p 8000:8000 \
-e MLFLOW_TRACKING_URI=http://mlflow-server:5000 \
churn-api:v1.0.0
# Test dell'API
curl -X POST http://localhost:8000/predict \
-H "Content-Type: application/json" \
-d '{"age": 35, "total_spend": 1250.50, "visits": 12, "days_since_last": 45}'
Monitoring in Produzione
Il deploy non e la fine del lavoro, ma l'inizio di una nuova fase critica: il monitoring. Un modello in produzione degrada nel tempo perchè il mondo cambia e i dati con esso. Il monitoring deve coprire tre aree principali.
Metriche da Tracciare
| Categoria | Metriche | Strumento |
|---|---|---|
| Infrastruttura | Latenza (p50, p95, p99), throughput, errori HTTP, CPU/RAM | Prometheus + Grafana |
| Modello | Accuracy, F1-score, distribuzione predizioni, confidence | MLflow + custom metrics |
| Dati | Data drift, feature drift, missing values, distribuzione input | Evidently AI / Great Expectations |
Data Drift vs Concept Drift
E fondamentale distinguere tra due tipi di degradazione del modello:
- Data Drift: la distribuzione dei dati in ingresso cambia rispetto al training set. Esempio: un modello addestrato su clienti 25-45 anni inizia a ricevere richieste per clienti 60+.
- Concept Drift: la relazione tra input e output cambia. Esempio: dopo una pandemia, i pattern di churn dei clienti sono completamente diversi, ma le feature in ingresso hanno la stessa distribuzione.
"""Rilevamento data drift con test statistici."""
import numpy as np
from scipy import stats
from typing import Dict, Tuple
def detect_drift(
reference_data: np.ndarray,
production_data: np.ndarray,
feature_names: list,
threshold: float = 0.05
) -> Dict[str, Dict]:
"""
Rileva data drift confrontando distribuzioni con il test KS.
Args:
reference_data: dati di training (riferimento)
production_data: dati di produzione (attuali)
feature_names: nomi delle feature
threshold: soglia p-value per il drift (default 0.05)
Returns:
Report di drift per ogni feature
"""
drift_report = {}
for i, feature in enumerate(feature_names):
ref_values = reference_data[:, i]
prod_values = production_data[:, i]
# Test Kolmogorov-Smirnov
ks_stat, p_value = stats.ks_2samp(ref_values, prod_values)
drift_detected = p_value < threshold
drift_report[feature] = {
"ks_statistic": round(ks_stat, 4),
"p_value": round(p_value, 4),
"drift_detected": drift_detected,
"ref_mean": round(float(np.mean(ref_values)), 4),
"prod_mean": round(float(np.mean(prod_values)), 4),
}
if drift_detected:
print(f"DRIFT RILEVATO su '{feature}': "
f"KS={ks_stat:.4f}, p={p_value:.4f}")
return drift_report
Quando Riattivare il Retraining
Non ogni drift richiede un retraining immediato. Definisci soglie chiare: data drift su feature critiche, calo di accuracy superiore al 5%, o distribuzione delle predizioni significativamente sbilanciata. Evita il retraining eccessivo che può introdurre instabilita.
Come Iniziare con Budget Inferiore a 5.000 EUR/anno
MLOps non significa necessariamente piattaforme enterprise da centinaia di migliaia di euro. Per una PMI italiana o un team piccolo, e possibile costruire un'infrastruttura MLOps efficace con strumenti open-source e costi minimi.
Stack Proposto per PMI
| Componente | Soluzione | Costo Annuale |
|---|---|---|
| Codice | GitHub Free / GitLab CE | 0 EUR |
| Data Versioning | DVC + Google Cloud Storage (5 GB free) | 0 - 50 EUR |
| Experiment Tracking | MLflow su VM economica | 200 - 500 EUR |
| Training | Google Colab Pro / spot VM | 120 - 600 EUR |
| Serving | FastAPI su VM (2 vCPU, 4 GB RAM) | 300 - 800 EUR |
| Monitoring | Prometheus + Grafana (self-hosted) | 0 EUR (sulla stessa VM) |
| CI/CD | GitHub Actions (2000 min/mese free) | 0 EUR |
| Container Registry | GitHub Container Registry | 0 EUR |
Totale stimato: 620 - 1.950 EUR/anno, ben sotto la soglia dei 5.000 EUR. Questo stack supporta fino a 5-10 modelli in produzione con volumi di traffico moderati (migliaia di predizioni al giorno).
Consigli per Ridurre i Costi
- Spot/preemptible VM: fino al 70% di risparmio per training non urgente
- Autoscaling: scala a zero quando non ci sono richieste
- Model compression: modelli più piccoli = meno risorse di serving
- Batch inference: se non servono predizioni real-time, usa batch notturni
- Multi-tenant: una singola infrastruttura MLflow/Grafana per tutti i progetti
Struttura di un Progetto MLOps
Per concludere con qualcosa di immediatamente utilizzabile, ecco la struttura di cartelle consigliata per un progetto MLOps. Questa organizzazione segue la separazione delle responsabilità e facilità automazione, testing e collaborazione.
churn-prediction/
data/
raw/ # Dati grezzi (versionati con DVC)
processed/ # Dati trasformati
data.dvc # File di tracking DVC
src/
data/
preprocessing.py # Pulizia e feature engineering
validation.py # Validazione qualità dati
models/
trainer.py # Logica di training
evaluator.py # Valutazione e metriche
serving/
app.py # FastAPI application
schemas.py # Pydantic schemas
monitoring/
drift_detector.py # Rilevamento drift
metrics.py # Metriche custom
pipeline.py # Orchestrazione pipeline
tests/
test_preprocessing.py
test_trainer.py
test_api.py
config.yaml # Configurazione pipeline
Dockerfile # Container per serving
docker-compose.yaml # Stack locale completo
requirements.txt # Dipendenze Python
.dvc/ # Configurazione DVC
.github/
workflows/
train.yaml # CI/CD per training
deploy.yaml # CI/CD per deployment
mlflow/ # Artefatti MLflow (locale)
README.md
Conclusioni e Prossimi Passi
MLOps non e un lusso riservato alle grandi aziende tecnologiche. E una necessità per chiunque voglia portare modelli ML in produzione in modo affidabile e sostenibile. In questo articolo abbiamo coperto i fondamentali: dalla comprensione del problema (perchè i progetti ML falliscono) alla soluzione concreta (pipeline modulari, experiment tracking, model registry, serving containerizzato e monitoring).
La chiave e iniziare in modo incrementale. Non serve raggiungere il Level 2 del modello di maturita Google dal giorno uno. Inizia dal Level 0 con buone pratiche:
- Subito: Separa il codice del notebook in moduli. Usa un config.yaml.
- Settimana 1: Aggiungi MLflow per tracciare esperimenti.
- Settimana 2: Containerizza il modello con FastAPI + Docker.
- Mese 1: Implementa una pipeline CI/CD con GitHub Actions.
- Mese 2: Aggiungi monitoring con Prometheus e alerting basico.
- Mese 3: Implementa DVC per il versionamento dei dati.
Nei prossimi articoli della serie approfondiremo ogni componente: la gestione dei dati con DVC, la creazione di pipeline CI/CD specifiche per ML, il monitoring avanzato con Evidently AI e il deployment scalabile su Kubernetes. Ogni articolo sarà pratico, con codice funzionante e istruzioni passo-passo.
Roadmap della Serie
- Articolo 2: DVC - Versionamento dei Dati per ML
- Articolo 3: MLflow Deep Dive - Experiment Tracking Avanzato
- Articolo 4: CI/CD per Machine Learning con GitHub Actions
- Articolo 5: Feature Store e Feature Engineering in Produzione
- Articolo 6: Model Serving Scalabile con Kubernetes
- Articolo 7: Monitoring Avanzato: Data Drift ed Evidently AI
- Articolo 8: Governance, Compliance e ML Responsabile







