El mòdul 5 va deixar CineClick amb un sistema que es mou sol però que encara no mira: l'ombra s'activa a mà, i l'única manera de saber si el champion envelleix és que algú es recordi de comprovar-ho. Aquesta lliçó construeix els ulls del sistema. Veurem que un sistema de ML es vigila en dos plans diferents — el del servei (respon?) i el del model (encerta?) — i que el segon pot estar completament trencat mentre el primer llueix verd a tots els panells. Aprendràs a exposar mètriques del servei amb prometheus-client, a definir proxies operatius per vigilar el model quan encara no hi ha etiquetes, a persistir una taula de prediccions auditable i a tancar el cercle quan el ground truth per fi arriba.
Contingut
- Els dos plans del monitoratge
- Pla de servei: mètriques RED i
/metrics - Prometheus i Grafana en quatre pinzellades
- Pla de model sense etiquetes: proxies operatius
- La taula de prediccions: el log que es converteix en dada
- Quan arriben les etiquetes: mètriques reals
- Un dashboard, dos plans
Els dos plans del monitoratge
A la lliçó 01-02 vam fixar una idea que ara toca materialitzar: un sistema de ML té dues maneres independents de fallar.
- Pla de servei: el sistema respon? Latència, taxa d'errors, saturació de recursos. És exactament el mateix que monitoraries a qualsevol microservei; res d'això no és específic de ML.
- Pla de model: el sistema encerta? Li arriben dades semblants a les que va veure a l'entrenament? Això és exclusiu de ML, i és el pla que les eines d'infraestructura tradicionals no veuen.
La trampa és que són independents de debò:
| Situació | Pla servei | Pla model | Ho detecta un dashboard d'infraestructura? |
|---|---|---|---|
| Pod caigut, timeouts | Vermell | — | Sí |
| p95 de 800 ms per un desplegament mal dimensionat | Vermell | Verd | Sí |
L'equip de dades canvia la unitat de hores_setmana a minuts |
Verd | Vermell | No |
| Una promoció de màrqueting atreu una cohort de clients nova | Verd | Vermell (degradant-se) | No |
A les dues últimes files, l'API de CineClick retorna 200 OK en 38 ms, l'HPA està tranquil, Kubernetes reporta 3/3 rèpliques sanes… i el model està predient brossa. El servei "verd" amb el model trencat és el mode de fallada característic dels sistemes de ML, i la raó que aquest mòdul existeixi.
flowchart LR
subgraph Pla de servei
A["Respon?<br/>latència, errors, saturació"]
end
subgraph Pla de model
B["Encerta?<br/>dades normals? prediccions normals?"]
end
A -->|"eines estàndard<br/>(Prometheus, Grafana)"| C[Dashboard]
B -->|"taula de prediccions,<br/>proxies, ground truth"| CPla de servei: mètriques RED i /metrics
Per al pla de servei farem servir el patró RED, un estàndard de facto per a serveis de petició/resposta:
- Rate: peticions per segon.
- Errors: taxa de respostes amb error (4xx del client, 5xx nostres).
- Duration: distribució de latències (p50, p95, p99 — no la mitjana, que amaga les cues).
La manera canònica d'exposar-les és un endpoint GET /metrics en format Prometheus. Afegim prometheus-client al servei FastAPI que vam construir a 04-02:
# src/cineclick_churn/api/metriques.py
from prometheus_client import Counter, Histogram
# Counter: valor que només creix. Prometheus en deriva taxes.
PREDICCIONS_TOTAL = Counter(
"cineclick_prediccions_total",
"Nombre de prediccions servides",
["resultat"], # etiqueta: "risc" (p >= llindar) o "sense_risc"
)
# Histogram: reparteix observacions en cubetes (buckets) per calcular percentils.
# Les cubetes es trien al voltant del que ens importa: el pressupost de 300 ms.
LATENCIA_PREDICCIO = Histogram(
"cineclick_latencia_prediccio_segons",
"Latència de la predicció end-to-end",
buckets=[0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.5, 1.0],
)Explicació peça a peça:
- Un
Counternomés suma. No guardem "peticions per segon": guardem el total acumulat, i Prometheus en deriva la taxa ambrate(). Això el fa robust davant de reinicis i scrapes perduts. - L'etiqueta
resultatens dona de franc una mètrica de model dins del pla de servei: la taxa de positius (proporció de prediccions "risc"), que farem servir com a proxy més endavant. - Un
Histogramno guarda cada latència individual, sinó quantes observacions han caigut a cada cubeta. Per això les cubetes han d'envoltar els valors crítics: amb el p95 real a 92 ms i el pressupost a 300 ms, posem cubetes fines per sota de 300 ms i gruixudes per sobre.
Instrumentem l'endpoint de 04-02 sense tocar-ne la lògica:
# src/cineclick_churn/api/enrutament.py (fragment)
import time
from .metriques import PREDICCIONS_TOTAL, LATENCIA_PREDICCIO
@router.post("/predir")
def predir(client: ClientEntrada):
inici = time.perf_counter()
probabilitat = model.predict_proba(construir_features(client))[0, 1]
resultat = "risc" if probabilitat >= LLINDAR_ABANDONAMENT else "sense_risc"
PREDICCIONS_TOTAL.labels(resultat=resultat).inc()
LATENCIA_PREDICCIO.observe(time.perf_counter() - inici)
# ... el logging JSON de 04-02 continua igual ...
return PrediccioSortida(probabilitat=probabilitat, resultat=resultat)I exposem l'endpoint a main.py:
# src/cineclick_churn/api/main.py (fragment)
from prometheus_client import make_asgi_app
app.mount("/metrics", make_asgi_app())Amb això, curl http://localhost:8000/metrics retorna text pla amb els comptadors i les cubetes de l'histograma. No cal enviar res enlloc: és Prometheus qui ve a llegir.
Prometheus i Grafana en quatre pinzellades
No convertirem això en un tutorial de Prometheus (per a això hi ha l'operació de plataforma); només el mínim perquè el sistema quedi connectat. El model mental és pull: Prometheus visita el /metrics de cada pod cada pocs segons (scrape), emmagatzema les sèries temporals, i Grafana les dibuixa.
A Kubernetes, amb el Prometheus Operator que ja fa servir la plataforma de CineClick, n'hi ha prou de declarar què ha de scrapejar:
# deploy/k8s/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: churn-api
namespace: cineclick
spec:
selector:
matchLabels:
app: churn-api # el Service del mòdul 4
endpoints:
- port: http
path: /metrics
interval: 15s # freqüència de scrapeLes dues alertes del pla de servei surten directament de les mètriques de guàrdia que vam definir a 05-04 per als canaries — aquí les fem permanents per al 100% del trànsit:
# deploy/k8s/alertes-servei.yaml (fragment de PrometheusRule)
groups:
- name: churn-api-servei
rules:
- alert: LatenciaP95Excedida
# histogram_quantile calcula el percentil a partir de les cubetes
expr: histogram_quantile(0.95,
rate(cineclick_latencia_prediccio_segons_bucket[5m])) > 0.3
for: 10m
labels: {severity: warning, canal: alertes-ml}
- alert: TaxaErrors5xx
expr: (sum(rate(http_requests_total{status=~"5.."}[10m]))
/ sum(rate(http_requests_total[10m]))) > 0.01
for: 10m
labels: {severity: critical, canal: alertes-ml}Lectura: si el p95 supera els 300 ms de pressupost durant 10 minuts, o els 5xx superen l'1% del trànsit, arriba un avís a #alertes-ml. Són exactament els mateixos llindars que disparaven el rollback automàtic del canary: la coherència entre "el que vigila un release" i "el que vigila el dia a dia" evita tenir dues veritats diferents.
Pla de model sense etiquetes: proxies operatius
Aquí comença l'específic de ML, i amb això el concepte clau d'aquesta lliçó: el retard del ground truth (ground truth delay). Per saber si una predicció de churn va encertar, cal esperar que el client es doni de baixa… o no. A CineClick això són setmanes. Avui no podem calcular el recall de les prediccions d'avui; només ho podrem fer quan el futur arribi.
Llavors el pla de model queda cec mentrestant? No: vigilem proxies operatius — senyals que no mesuren l'encert però que, si es mouen, indiquen que passa alguna cosa estranya abans que el ground truth ho confirmi:
| Proxy | Què vigila | Senyal d'alarma a CineClick |
|---|---|---|
| Distribució de probabilitats emeses | La "forma" del que opina el model | L'histograma de scores canvia de forma d'una setmana a l'altra |
| Taxa de positius (p ≥ 0.5) | El volum de clients marcats en risc | Fora de la banda esperada 10%–25% (la mateixa banda de guàrdia de 05-04) |
Volum per pla i metode_pagament |
La mescla d'entrada | El pla basic passa del 40% al 70% de les peticions |
| % de valors atípics a les features | La qualitat de la dada d'entrada | hores_setmana > 100, antiguitat_mesos = 0 amb tiquets_suport = 50… |
Fixa't en la lògica: cap d'aquests proxies no requereix saber qui es va donar de baixa. La taxa de positius ja la tenim a Prometheus gràcies a l'etiqueta resultat del counter. Els altres requereixen mirar les prediccions amb més detall — i per a això cal persistir-les.
Un aclariment d'abast: si un proxy es mou, quant s'ha mogut i és estadísticament rellevant? Aquesta pregunta — la deriva formal, amb PSI i tests — és la lliçó 06-02. Aquí només deixem els senyals encesos.
La taula de prediccions: el log que es converteix en dada
A 04-02 el servei ja escriu una línia JSON per predicció (hash_entrada, probabilitat, versio_model, latencia_ms, i des de 05-04 els camps d'ombra/canary). Fins ara aquell log només servia per depurar. El salt d'aquesta lliçó és tractar-lo com a dada de primera classe: la taula de prediccions és la matèria primera de tot el monitoratge de model, de l'anàlisi de deriva de 06-02 i de l'avaluació real que veurem a l'apartat següent.
On va a parar? Un col·lector (a CineClick, el mateix stack de logs de la plataforma) recull les línies JSON dels pods i les materialitza com a Parquet particionat per data — el mateix format que ja fa servir el batch setmanal:
Què ha de contenir cada fila perquè d'aquí a tres mesos puguem auditar i avaluar?
| Camp | Per a què serveix després |
|---|---|
id_prediccio, timestamp |
Identificar i ordenar; la finestra temporal del join amb etiquetes |
hash_entrada |
Detectar duplicats i auditar sense guardar el payload cru |
features (les que van entrar al model, inclosa ratio_tickets) |
Reproduir la predicció; anàlisi de deriva per feature |
probabilitat |
Distribució de scores, taxa de positius, avaluació real |
versio_model, alias |
Atribuir cada predicció a un model concret (v2 o el challenger?) |
probabilitat_ombra, versio_model_ombra |
Comparar champion vs. challenger offline |
grup (canary/control/estable) |
No barrejar poblacions en avaluar |
latencia_ms |
Correlacionar degradacions de servei amb canvis de model |
Regla d'or: guarda les features tal com van entrar al model, no només l'input cru. Si demà features.py canvia, l'input cru ja no reprodueix el que el model va veure.
Quan arriben les etiquetes: mètriques reals
Setmanes després, el ground truth arriba: el sistema de facturació de CineClick sap quins clients es van donar de baixa. Ara podem respondre la pregunta que cap proxy no respon: quin recall i quina precision REALS està donant el champion en producció?
El mecanisme és un job (a CineClick, una task més dins de l'ecosistema Prefect que ja orquestra el scoring dels dilluns) que casa prediccions amb baixes:
# flows/avaluacio_real.py (fragment de la task principal)
import pandas as pd
def avaluar_setmana(prediccions: pd.DataFrame, baixes: pd.DataFrame,
finestra_dies: int = 30) -> dict:
"""Casa prediccions amb baixes reals i calcula mètriques de producció.
prediccions: taula de prediccions d'UNA setmana (id_client, timestamp,
probabilitat, versio_model, grup).
baixes: baixes confirmades (id_client, data_baixa).
"""
df = prediccions.merge(baixes, on="id_client", how="left")
# Etiqueta real: es va donar de baixa DINS de la finestra posterior a la
# predicció. La finestra importa: una baixa 6 mesos després no valida la
# predicció d'avui.
df["abandonament_real"] = (
(df["data_baixa"].notna())
& (df["data_baixa"] > df["timestamp"])
& (df["data_baixa"] <= df["timestamp"] + pd.Timedelta(days=finestra_dies))
).astype(int)
df["prediccio"] = (df["probabilitat"] >= 0.5).astype(int)
vp = int(((df.prediccio == 1) & (df.abandonament_real == 1)).sum())
fp = int(((df.prediccio == 1) & (df.abandonament_real == 0)).sum())
fn = int(((df.prediccio == 0) & (df.abandonament_real == 1)).sum())
return {
"recall_real": vp / (vp + fn) if (vp + fn) else None,
"precision_real": vp / (vp + fp) if (vp + fp) else None,
"n_prediccions": len(df),
"n_baixes": int(df.abandonament_real.sum()),
}Tres detalls que fan o desfan aquest job:
- El join és per id I per finestra temporal. Sense la finestra, qualsevol baixa històrica "valida" qualsevol predicció i les mètriques surten inflades.
- Filtra per
grup. Els clients del grup de control permanent (05-04) no reben el descompte de retenció: són l'única població on l'etiqueta no està contaminada per la nostra pròpia intervenció. Les mètriques "netes" es calculen sobre ells. - Compara sempre amb la referència offline. El champion v2 prometia recall 0.68 i precision 0.55 al test set. Si producció dona 0.62/0.51 de manera estable, és la degradació normal del salt offline→online. Si el recall real cau setmana rere setmana, el model està envellint — i aquesta tendència és l'entrada de la política de reentrenament de 06-03.
El resultat de cada execució s'escriu com a sèrie temporal (recall_real, precision_real per setmana i per versio_model), a punt per pintar-se al costat de la resta.
Un dashboard, dos plans
L'error clàssic és tenir el dashboard de servei a Grafana i "allò del model" en un notebook que algú executa quan se'n recorda. CineClick munta un únic dashboard amb els dos plans, perquè els incidents reals creuen la frontera (un pic de latència coincideix amb un canvi de mescla d'entrada; una caiguda de taxa de positius coincideix amb un desplegament). Esbós de panells:
| Fila | Panells | Font |
|---|---|---|
| Servei | req/s, taxa 5xx, p50/p95/p99 vs. línia de 300 ms, rèpliques/HPA | Prometheus |
| Model (proxies) | histograma de probabilitats per dia, taxa de positius amb banda 10–25%, mescla per pla/mètode de pagament, % atípics | Taula de prediccions |
| Model (ground truth) | recall i precision reals per setmana vs. línies offline (0.68 / 0.55), amb anotacions de desplegaments | Job d'avaluació real |
| Context | versió de model activa (/versio), esdeveniments de release (ombra/canary) |
Registry + CD |
La fila de context és la que converteix el dashboard en eina de diagnòstic: quan alguna cosa es torça, la primera pregunta sempre és "què va canviar i quan?".
Errors Comuns i Consells
- Monitorar només el pla de servei. És l'error número u: l'equip de plataforma "ja monitora" l'API… i ningú no mira si el model encerta. Recorda la taula del principi: el mode de fallada típic de ML és servei verd, model vermell.
- Fer servir la latència mitjana en lloc de percentils. Amb p50 38 ms i p95 92 ms, la mitjana dirà ~45 ms i amagarà que el 5% dels teus usuaris espera el doble. Alerta sobre p95/p99, sempre.
- Loguejar el payload cru "per si de cas". A més del problema de privadesa (el reprendrem a 06-04), infla l'emmagatzematge. El
hash_entrada+ les features processades cobreixen auditoria i anàlisi. - No guardar
versio_modela cada predicció. Quan conviuen champion, ombra i canary, una taula sense versió és inservible: no sabràs a qui atribuir cada encert o errada. - Casar prediccions amb etiquetes sense finestra temporal. Infla recall i precision de manera silenciosa. Defineix la finestra segons el negoci (en churn, l'horitzó de la predicció) i documenta-la.
- Alertar de tot. Si
#alertes-mlrep deu avisos al dia, deixarà de llegir-se. Comença amb poques alertes accionables (les dues de servei + taxa de positius fora de banda) i afegeix-ne només quan una absència t'hagi fet mal.
Exercicis
-
Instrumentació del batch. El flow
scoring-setmanal-churn(05-03) també fa prediccions, però no és un servei HTTP: Prometheus no pot scrapejar-lo de manera contínua. Quines mètriques del pla de model registraries a cada execució setmanal i on les persistiries perquè apareguin al mateix dashboard? -
Diagnòstic amb la taula de prediccions. Un dilluns observes: p95 = 85 ms, 5xx = 0%, taxa de positius = 4% (portava mesos entre el 12% i el 16%). L'equip de plataforma diu que "tot està verd". Enumera, en ordre, les tres primeres comprovacions que faries fent servir la taula de prediccions i l'endpoint
/versio. -
Finestra d'avaluació. El job de ground truth fa servir
finestra_dies=30. Marc proposa abaixar-la a 7 per "tenir mètriques reals abans". Què els passaria arecall_realiprecision_realamb aquesta finestra i per què? Quin compromís proposaries?
Solucions
-
A cada execució del flow: distribució de probabilitats del lot (percentils del score), taxa de positius sobre el total puntuat, nombre de files puntuades (una davallada indica problema aigües amunt), i % d'atípics per feature. Es persisteixen com a files a la mateixa taula de prediccions (el batch ja escriu
data/prediccions_churn.parquet; n'hi ha prou d'afegir els mateixos camps que l'online: versió, timestamp, features) i/o s'empenyen a Prometheus via Pushgateway, que existeix precisament per a jobs efímers. Així el dashboard únic mostra online i batch junts, distingits per una etiquetaorigen. -
Primer,
GET /versio: ha canviatversio_modeloaliasrecentment? Una promoció o un rollback accidental és la causa més ràpida de descartar. Segon, a la taula de prediccions, comparar l'histograma de probabilitats d'aquesta setmana amb el de les anteriors: si tota la distribució s'ha desplaçat cap avall, el model "opina diferent" — probable canvi a les dades d'entrada. Tercer, comparar la mescla d'entrada (distribució depla,metode_pagament, i estadístics dehores_setmana,antiguitat_mesos,ratio_tickets) contra setmanes prèvies per localitzar quina feature s'ha mogut; un valor constant o nul delata una fallada del productor de dades. Nota: el servei està sa — el problema és de dades o de model, exactament l'escenari "verd per fora". -
Amb 7 dies, moltes baixes que la predicció va anticipar correctament caurien fora de la finestra i comptarien com a falsos positius:
precision_realbaixaria artificialment, irecall_reales calcularia sobre moltes menys baixes (només les molt immediates), i es tornaria sorollós i inestable. El compromís habitual: mantenir la finestra alineada amb l'horitzó de negoci (30 dies) per a les mètriques oficials, i publicar a més una mètrica "parcial a 7 dies" clarament etiquetada com a avanç provisional, útil com a senyal primerenc però mai com a criteri de decisió.
Conclusió
CineClick ja té ulls. El pla de servei queda cobert amb mètriques RED exposades a /metrics, scrapejades per Prometheus i alertades amb els mateixos llindars de guàrdia de 05-04 (p95 > 300 ms, 5xx > 1%). El pla de model — el que cap eina d'infraestructura no veu — queda cobert en dos temps: proxies operatius mentre el ground truth no arriba (distribució de scores, taxa de positius en banda 10–25%, mescla d'entrada, atípics) i mètriques reals quan arriba, gràcies al job que casa la taula de prediccions amb les baixes confirmades i compara el recall/precision de producció amb els 0.68/0.55 promesos offline. La peça central és aquesta taula de prediccions: el log JSON de 04-02 elevat a dada de primera classe, amb features, versió i grup a cada fila. Però fins ara només hem dit que els proxies "es mouen" — no quant ni amb quina significació. Posar números rigorosos a aquest moviment, distingir un dilluns estrany d'un canvi real a la població, i automatitzar aquesta vigilància amb Evidently és exactament el tema de la lliçó següent: deriva de dades i deriva de concepte.
Curs de MLOps
Mòdul 1: Fonaments de MLOps
- Què és MLOps i per què els models moren al notebook
- El cicle de vida d'un model de ML en producció
- Nivells de maduresa MLOps i rols de l'equip
- El projecte del curs: del notebook a producció
Mòdul 2: Del notebook al codi reproduïble
- Estructura d'un projecte de ML: del notebook al paquet
- Entorns reproduïbles i gestió de dependències
- Versionat de dades amb DVC
- Pipelines d'entrenament reproduïbles
Mòdul 3: Experiments i registre de models
- Tracking d'experiments amb MLflow
- Model registry: versionar i promocionar models
- Feature stores: quan i per a què
Mòdul 4: Servir models en producció
- Patrons de desplegament: batch, online i streaming
- Un servei de predicció amb FastAPI
- Empaquetatge amb Docker
- Escalat i desplegament: Kubernetes i serverless
- Optimització de la inferència: latència i cost
Mòdul 5: Automatització: CI/CD i orquestració
- CI per a ML: tests de codi, dades i models
- CD: automatitzar el desplegament del model
- Orquestració de pipelines de ML
- Estratègies de release: shadow, canary i A/B
