Salta al contenuto principale

01 - MLOps: Da Esperimento a Produzione

Guida completa a MLOps: Da Esperimento a Produzione: architettura, implementazione pratica e best practices per developer e team tecnici.

Cosa ne pensi di questo articolo?

In Breve

Guida completa a MLOps: Da Esperimento a Produzione: architettura, implementazione pratica e best practices per developer e team tecnici.

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.

Ciclo di Vita MLOps

    +----------+     +---------+     +----------+
    |   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.

notebook_v1.ipynb - Il classico notebook monolitico

# 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.

src/data/preprocessing.py - Modulo preparazione dati

"""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)
src/models/trainer.py - Modulo training

"""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),
    }
src/pipeline.py - Orchestrazione della pipeline

"""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 - Configurazione esternalizzata

# 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 MLflow

# Installazione
pip install mlflow

# Avvio del tracking server locale
mlflow server --host 127.0.0.1 --port 5000
src/pipeline_tracked.py - Pipeline con MLflow tracking

"""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
Promozione modello con MLflow Model Registry

"""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.

src/serving/app.py - API FastAPI per serving del modello

"""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 - Container per il serving

# 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 e avvio del container

# 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.
src/monitoring/drift_detector.py - Rilevamento drift semplificato

"""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.

Struttura progetto MLOps consigliata

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

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