El mòdul 2 va deixar CineClick amb un projecte reproduïble i una pregunta oberta: comparar una execució contra una altra amb dvc metrics diff funciona, però explorar desenes de combinacions d'hiperparàmetres i recordar què es va provar fa tres setmanes demana una altra eina. En aquesta lliçó instrumentem el pipeline de churn amb MLflow Tracking: cada entrenament quedarà registrat amb els seus paràmetres, mètriques, codi i dades, consultable en una interfície web amb taula, filtres i comparacions. Amb aquesta memòria d'equip muntada, Laura llançarà una petita cerca d'hiperparàmetres que trobarà un candidat clarament millor per al negoci — i podrem resoldre, per fi, el misteri de "la v2" que arrossega des del mòdul 1.
Contingut
- El problema: comparar desenes d'execucions (i per què
dvc metrics diffes queda curt) - Conceptes de MLflow: tracking server, experiment, run, params, metrics, tags i artifacts
- Instrumentar
train.pyievaluate.pyde CineClick - La interfície de MLflow: taula de runs, filtres i comparació
- Una cerca d'hiperparàmetres: 12 runs i un candidat nou
- On viu el tracking:
mlrunslocal vs. servidor compartit - DVC i MLflow junts, i el misteri de "la v2" resolt
El problema: comparar desenes d'execucions
Al mòdul 2, quan Laura va provar n_estimators: 300, el flux va ser: editar params.yaml, executar dvc repro i comparar amb dvc metrics diff. Perfecte per respondre "aquest canvi millora respecte al que hi ha a Git?". Però el diff de DVC compara l'estat actual contra una revisió: és una foto de dos punts, no una memòria històrica.
Les preguntes que Laura no pot respondre amb aquest flux:
- Quines combinacions hem provat ja? Si en prova 12, hauria de fer 12 commits (o apuntar-les a mà en un full de càlcul, que és el que feia amb el notebook).
- Quin va ser el millor recall de totes les proves del mes passat, i amb quins paràmetres?
git logguarda commits, no una taula ordenable de mètriques. - Aquell experiment el va llançar Laura o Marc, amb quina versió de les dades? No hi ha on mirar-ho.
El tracking d'experiments resol exactament això: un registre centralitzat on cada execució d'entrenament s'anota automàticament amb tot el seu context. No substitueix DVC (de seguida veurem com conviuen): hi afegeix la dimensió que a DVC li falta, l'exploració massiva i la seva història.
| Necessitat | dvc metrics diff |
Tracking (MLflow) |
|---|---|---|
| Comparar estat actual vs. un commit | Excel·lent | Possible però indirecte |
| Taula amb 50 execucions ordenada per recall | No | Sí, natiu |
| Filtrar "totes les runs amb recall > 0.5" | No | Sí (sintaxi de cerca) |
| Saber qui va llançar què i quan | Via commits (si es commiteja tot) | Automàtic a cada run |
| Reproduir exactament una execució | Excel·lent (és la seva raó de ser) | Només si registres commit i dades |
Fixa't en l'última fila: el tracking observa, no garanteix reproduïbilitat per si mateix. Per això les dues eines es complementen en lloc de competir.
Conceptes de MLflow
MLflow és una plataforma open source de gestió del cicle de vida de ML. Té diversos components (Tracking, Model Registry, Models, Projects); en aquesta lliçó fem servir Tracking, i a la següent el registry. El seu vocabulari:
- Tracking server: el servei que rep i emmagatzema els registres. Pot ser simplement una carpeta local (
mlruns/) o un servidor compartit amb base de dades — ho veiem a l'apartat 6. - Experiment: un contenidor amb nom que agrupa execucions relacionades. Per a nosaltres:
churn-cineclick. Un projecte sol tenir un o pocs experiments. - Run: una execució concreta d'entrenament. És la unitat central: cada
dvc reproque entreni un model, o cada iteració d'un bucle de cerca, serà una run amb el seu identificador únic (run_id). - Params: els hiperparàmetres i la configuració d'aquella run (
n_estimators=100,test_size=0.2). Són parells clau-valor immutables: s'escriuen una vegada. - Metrics: valors numèrics de resultat (
recall=0.43). Es poden registrar diverses vegades per run (per exemple, una mètrica per època en deep learning), tot i que en el nostre cas serà un valor final. - Tags: metadades lliures per organitzar i cercar (
autor=laura,dades_dvc=af12bc...). MLflow n'afegeix alguns automàticament, com el commit de Git. - Artifacts: fitxers associats a la run — el model serialitzat, gràfiques, el
params.yamlusat. El que no cap en un parell clau-valor, va aquí.
graph TD
E["Experiment: churn-cineclick"] --> R1["Run a3f9... (baseline)"]
E --> R2["Run 7c21... (300 arbres)"]
E --> R3["Run f0d4... (balanced)"]
R3 --> P["Params: n_estimators, max_depth, class_weight"]
R3 --> M["Metrics: accuracy, precision, recall, f1"]
R3 --> T["Tags: commit de Git, dades DVC, autor"]
R3 --> A["Artifacts: model, signatura, exemple d'entrada"]Instrumentar train.py i evaluate.py de CineClick
Primer, MLflow entra a les dependències del projecte com qualsevol altra llibreria (mòdul 2: res no s'instal·la "a mà"):
# Afegim mlflow a pyproject.toml (secció dependencies) i recompilem el lock pip-compile pyproject.toml -o requirements.lock pip-sync requirements.lock
El nostre pipeline té una particularitat: entrenar i avaluar són stages separats de dvc.yaml, però conceptualment són una mateixa run (un model i les seves mètriques). La solució estàndard: train.py obre la run, en desa el run_id en un fitxer, i evaluate.py reobre aquesta mateixa run per afegir-hi les mètriques.
src/cineclick_churn/train.py instrumentat (fragment rellevant):
import json
from pathlib import Path
import mlflow
import pandas as pd
import yaml
from mlflow.models import infer_signature
from sklearn.ensemble import RandomForestClassifier
def entrenar(ruta_features: str, ruta_model: str, ruta_params: str = "params.yaml"):
# 1. Carreguem els paràmetres des de params.yaml, l'única font de veritat
params = yaml.safe_load(Path(ruta_params).read_text())["entrenar"]
df = pd.read_csv(ruta_features)
X, y = df.drop(columns=["abandonament"]), df["abandonament"]
# 2. Seleccionem (o creem) l'experiment del projecte
mlflow.set_experiment("churn-cineclick")
# 3. Obrim una run: tot el que es registri dins del "with" queda associat a ella
with mlflow.start_run() as run:
# 4. Registrem TOTS els params de la secció 'entrenar' de cop
mlflow.log_params(params)
model = RandomForestClassifier(
n_estimators=params["n_estimators"],
random_state=params["random_state"],
)
model.fit(X, y)
# 5. La signatura (signature) descriu l'esquema d'entrada/sortida del model;
# l'input_example desa 5 files reals com a exemple documentat.
signatura = infer_signature(X, model.predict(X))
mlflow.sklearn.log_model(
model,
artifact_path="model",
signature=signatura,
input_example=X.head(5),
)
# 6. Desem el run_id perquè l'stage 'avaluar' reobri aquesta mateixa run
Path("models/mlflow_run_id.txt").write_text(run.info.run_id)
# El .pkl es continua generant: és l'output que DVC rastreja (de moment)
import joblib
joblib.dump(model, ruta_model)Punts que convé entendre bé:
mlflow.set_experiment("churn-cineclick")crea l'experiment si no existeix i l'activa. Sense aquesta línia, les runs caurien a l'experiment per defecte (Default), un calaix de sastre que convé evitar.mlflow.log_params(params)rep el diccionari sencer: no copiem valors a mà, així el que es registra coincideix sempre amb el que s'executa. Aquest detall mata una classe sencera d'errors ("l'Excel deia 200 arbres però eren 100").- La signatura i l'
input_examplesemblen burocràcia, però són or: documenten quines columnes, amb quins tipus, espera el model. Quan al mòdul 4 calgui servir-lo, aquesta serà l'especificació del contracte d'entrada, i MLflow validarà contra ella.
I evaluate.py, que reobre la run i hi afegeix les mètriques (les mateixes que continua escrivint a metrics.json per a DVC):
import json
from pathlib import Path
import mlflow
def avaluar(ruta_model: str, ruta_test: str, ruta_metriques: str = "metrics.json"):
metriques = _calcular_metriques(ruta_model, ruta_test) # accuracy, precision, recall, f1
# metrics.json continua existint: és el fitxer que dvc.yaml declara com a mètrica
Path(ruta_metriques).write_text(json.dumps(metriques, indent=2))
# Reobrim la run que va deixar oberta (i tancada) train.py, indicant-ne l'id
run_id = Path("models/mlflow_run_id.txt").read_text().strip()
with mlflow.start_run(run_id=run_id):
mlflow.log_metrics(metriques)Ara un dvc repro normal deixa, a més dels outputs de sempre, una run completa a MLflow. Res del flux del mòdul 2 no s'ha trencat: només hi hem afegit observació.
La interfície de MLflow: taula, filtres i comparació
Per explorar el que s'ha registrat, engeguem la interfície web local:
# Des de l'arrel del repo (on hi ha la carpeta mlruns/ que MLflow ha creat) mlflow ui --port 5000 # Obre http://127.0.0.1:5000 al navegador
L'essencial de la UI:
- Taula de runs: cada fila una run; columnes configurables amb params i metrics. Es pot ordenar per qualsevol mètrica — "dona'm la taula ordenada per recall descendent" és un clic.
- Cerca amb filtres: la caixa de cerca accepta una sintaxi de tipus SQL. Exemples útils:
metrics.recall > 0.5 metrics.recall > 0.5 and params.class_weight = 'balanced' tags.mlflow.source.git.commit = 'a1b2c3d'
- Comparació de runs: selecciones diverses files, prems Compare, i obtens els params costat a costat (amb les diferències ressaltades) i gràfics de dispersió mètrica-vs-paràmetre. És la versió visual i multi-run del
dvc metrics diff. - Vista de detall: dins d'una run hi ha els seus artifacts navegables — el model, la seva signatura, l'
input_example.
Una cerca d'hiperparàmetres: 12 runs i un candidat nou
Recordem el problema de negoci: el model base té recall 0.43 — de cada 100 clients que se'n van, només en detecta 43. Per a una campanya de retenció, això deixa escapar més de la meitat dels que es podrien salvar. Laura sospita que el desbalanceig (~15% d'abandonament) n'és el culpable: el bosc aprèn que "gairebé ningú no se'n va" i es queda tan ample.
L'exploració: un bucle sobre tres hiperparàmetres. Això és exploració, no el pipeline beneït, així que va en un script a part (scripts/cercar_hiperparametres.py) i no toca dvc.yaml:
import itertools
import mlflow
import pandas as pd
from mlflow.models import infer_signature
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
from sklearn.model_selection import train_test_split
df = pd.read_csv("data/processed/features.csv")
X, y = df.drop(columns=["abandonament"]), df["abandonament"]
# Mateix split que el pipeline: test_size=0.2, random_state=42, estratificat
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
mlflow.set_experiment("churn-cineclick")
# 2 x 3 x 2 = 12 combinacions
malla = itertools.product(
[100, 300], # n_estimators
[10, 20, None], # max_depth
[None, "balanced"], # class_weight
)
for n_est, profunditat, pes in malla:
with mlflow.start_run(run_name=f"rf-{n_est}-{profunditat}-{pes}"):
mlflow.log_params({
"n_estimators": n_est,
"max_depth": profunditat,
"class_weight": pes,
})
mlflow.set_tag("tipus", "cerca-hiperparametres")
model = RandomForestClassifier(
n_estimators=n_est, max_depth=profunditat,
class_weight=pes, random_state=42,
)
model.fit(X_tr, y_tr)
pred = model.predict(X_te)
mlflow.log_metrics({
"accuracy": accuracy_score(y_te, pred),
"precision": precision_score(y_te, pred),
"recall": recall_score(y_te, pred),
"f1": f1_score(y_te, pred),
})
signatura = infer_signature(X_tr, model.predict(X_tr))
mlflow.sklearn.log_model(model, "model", signature=signatura,
input_example=X_tr.head(5))Dotze runs en uns minuts. A la UI, Laura ordena per recall i el patró salta a la vista — resumit:
| run | n_estimators | max_depth | class_weight | accuracy | precision | recall | F1 |
|---|---|---|---|---|---|---|---|
| rf-300-20-balanced | 300 | 20 | balanced | 0.842 | 0.55 | 0.68 | 0.61 |
| rf-100-20-balanced | 100 | 20 | balanced | 0.839 | 0.54 | 0.67 | 0.60 |
| rf-300-None-balanced | 300 | None | balanced | 0.848 | 0.57 | 0.62 | 0.59 |
| rf-300-None-None | 300 | None | None | 0.861 | 0.62 | 0.44 | 0.51 |
| rf-100-None-None (≈ base) | 100 | None | None | 0.860 | 0.61 | 0.43 | 0.50 |
La lectura, que és el que importa:
class_weight="balanced"és el factor decisiu: li diu al bosc que cada client de la classe minoritària (abandonament=1) pesa més en l'entrenament, compensant el desbalanceig del 15%. El recall salta de 0.43 a 0.68.- El preu: la precision baixa de 0.61 a 0.55 (més falses alarmes) i l'accuracy cau de 0.86 a 0.842. És un bon tracte? Per a CineClick, sí: una trucada de retenció a un client que no pensava marxar costa poc; un churner no detectat és una subscripció perduda. Caçar-ne 68 de cada 100 en lloc de 43 és una millora de negoci real. L'F1 puja de 0.50 a 0.61, confirmant que l'intercanvi és globalment favorable.
- Compareu amb el mòdul 2: pujar a 300 arbres sense tocar el pes de classes donava una millora marginal (recall 0.44). La cerca sistemàtica va trobar en una tarda el que els retocs puntuals no trobaven.
El candidat nou: RandomForest amb n_estimators=300, max_depth=20, class_weight="balanced" — accuracy 0.842, precision 0.55, recall 0.68, F1 0.61. Per consagrar-lo, el flux del mòdul 2 continua vigent: s'afegeixen max_depth i class_weight a la secció entrenar: de params.yaml (i com a arguments a train.py), s'executa dvc repro i es commiteja. Exploració amb MLflow, consagració amb DVC + Git. Què fer amb aquest model "guanyador" — versionar-lo i marcar-lo formalment — és exactament el tema de la propera lliçó.
On viu el tracking: mlruns local vs. servidor compartit
Fins ara tot s'ha desat en una carpeta mlruns/ al costat del repo (convé afegir-la al .gitignore: són dades de registre, no codi). Això funciona per a una persona, però l'objectiu era la memòria de l'equip: si Marc llança runs a la seva màquina, Laura no les veu.
L'arquitectura de MLflow separa dos magatzems:
- Backend store: on van params, metrics i tags — dades estructurades i consultables. En seriós: una base de dades (PostgreSQL, MySQL, o SQLite per començar).
- Artifact store: on van els fitxers grans (models, gràfiques). En seriós: emmagatzematge d'objectes (S3, Azure Blob, GCS...) — el mateix tipus de lloc on viu el remote
almacende DVC.
graph LR
L["Laura<br/>train.py"] -->|"log_params, log_metrics"| S["Tracking server<br/>(mlflow server)"]
M["Marc<br/>cercar_hiperparametres.py"] -->|"log_model"| S
S --> B[("Backend store<br/>PostgreSQL: params, metrics, tags")]
S --> A[("Artifact store<br/>S3/Blob: models, fitxers")]
U["Navegador de l'equip<br/>UI de MLflow"] --> SAixecar un servidor compartit (aquí en versió mínima local, amb SQLite):
mlflow server \ --backend-store-uri sqlite:///mlflow.db \ --artifacts-destination ./mlartifacts \ --host 0.0.0.0 --port 5000
I els scripts hi apunten amb una variable d'entorn — sense canviar ni una línia de codi:
En producció real, aquest servidor és un servei intern més (un contenidor amb la seva base de dades gestionada i el seu bucket), però la mecànica que has après és idèntica: només canvia la URI.
DVC i MLflow junts, i el misteri de "la v2" resolt
La divisió de treball que farà servir CineClick la resta del curs:
| Responsabilitat | Eina | Per què |
|---|---|---|
| Versionar datasets i els seus canvis | DVC | És la seva funció nativa (mòdul 2) |
| Definir i executar el pipeline reproduïble | DVC (dvc.yaml + dvc repro) |
Invalidació selectiva, lligat a Git |
| Registrar i comparar experiments en massa | MLflow Tracking | Taula, filtres, història de l'equip |
| Desar el model amb signatura i context | MLflow | Artifacts + signature + registry (propera lliçó) |
DVC governa (decideix què s'executa i garanteix que sigui reproduïble); MLflow observa i recorda (anota què va passar i amb quin resultat). Perquè l'observació tingui valor de traçabilitat completa, cada run ha de quedar lligada a codi i dades:
- Codi: MLflow registra automàticament el commit de Git al tag
mlflow.source.git.commit(si llances l'script des d'un repo amb la feina commitejada — una altra raó per commitejar abans d'entrenar). - Dades: ho lliguem nosaltres amb un tag, llegint el hash que DVC ja calcula:
import yaml
lock = yaml.safe_load(open("dvc.lock"))
md5_dades = lock["stages"]["preparar_dades"]["deps"][0]["md5"]
mlflow.set_tag("dades_dvc_md5", md5_dades)I amb això, tanquem el compte pendent. Al mòdul 1, l'anècdota: existia un model_churn_v2.pkl que algú va entrenar, ningú no sabia amb quins paràmetres ni amb quines dades, i l'etern dubte de si "la v2 era pitjor". La resposta honesta és que era incontestable: la informació necessària per respondre-la no es va desar enlloc. No era un misteri, era una absència de registre. Amb el sistema d'aquesta lliçó, la pregunta ni tan sols es pot formular: qualsevol "v2" futura serà una run amb els seus params, les seves mètriques, el seu commit de codi i el seu hash de dades — i comparar-la amb la v1 costarà dos clics a la pestanya Compare. La moralitat del tracking és aquesta: no fa millors models, fa impossible no saber d'on va sortir cadascun.
Errors Comuns i Consells
- Registrar mètriques a mà en lloc de des de variables: si escrius
mlflow.log_metric("recall", 0.68)amb el número copiat, el tracking menteix tan bon punt canviï alguna cosa. Registra sempre el resultat calculat. - Oblidar
set_experimenti omplirDefault: acabaràs amb 200 runs de tres projectes barrejades. Fixa l'experiment al principi de l'script, sempre. - Runs òrfenes de context: una run sense commit de Git associat (perquè vas llançar amb canvis sense commitejar) o sense referència a les dades torna al problema de "la v2". Convenció d'equip: commitejar abans de llançar experiments "seriosos".
- Ficar la cerca d'hiperparàmetres a
dvc.yaml: el pipeline de DVC és el camí beneït i determinista; l'exploració és divergent per naturalesa. Separa-les: els scripts d'exploració registren a MLflow; el que guanya, es consagra aparams.yamlidvc repro. - Commitejar
mlruns/: són dades de registre (potencialment gigues de models). Al.gitignore. La memòria compartida s'aconsegueix amb un tracking server, no amb Git. - Confondre params i metrics: els params són entrada (els tries tu), les metrics són sortida (les produeix l'avaluació). Si registres
test_sizecom a mètrica, els filtres i gràfics de la UI deixaran de tenir sentit.
Exercicis
-
Filtres de la UI. Escriu les expressions de cerca de MLflow per a: (a) runs amb F1 superior a 0.55; (b) runs de la cerca d'hiperparàmetres (tag
tipus) que facin servirclass_weightbalanced; (c) runs amb recall més gran que 0.6 i precision més gran que 0.5. -
Ampliar la malla. Modifica
scripts/cercar_hiperparametres.pyper afegir l'hiperparàmetremin_samples_leafamb valors[1, 5]a la malla. Quantes runs generarà ara el bucle? Afegeix a més un taglotamb valor"malla-2"per poder distingir aquesta tanda de la primera a la UI. -
Traçabilitat completa. Marc troba a la UI una run interessant de fa dues setmanes i vol reproduir-la exactament a la seva màquina. Enumera els passos, indicant de quin camp de la run surt cada dada que necessita (suposant que la run té el tag
mlflow.source.git.commiti el tagdades_dvc_md5).
Solucions
- Les tres expressions:
metrics.f1 > 0.55 tags.tipus = 'cerca-hiperparametres' and params.class_weight = 'balanced' metrics.recall > 0.6 and metrics.precision > 0.5
Nota: els valors de params i tags es comparen com a cadenes (cometes simples); les mètriques, com a números.
- La malla passa a ser 2 × 3 × 2 × 2 = 24 runs. Canvis a l'script:
malla = itertools.product(
[100, 300], [10, 20, None], [None, "balanced"],
[1, 5], # min_samples_leaf
)
for n_est, profunditat, pes, fulla in malla:
with mlflow.start_run(run_name=f"rf-{n_est}-{profunditat}-{pes}-{fulla}"):
mlflow.log_params({..., "min_samples_leaf": fulla})
mlflow.set_tag("tipus", "cerca-hiperparametres")
mlflow.set_tag("lot", "malla-2")
model = RandomForestClassifier(..., min_samples_leaf=fulla, random_state=42)Consell: les malles creixen multiplicativament; abans d'afegir un quart hiperparàmetre, pregunta't si els resultats de la malla anterior suggereixen que val la pena.
- Passos de Marc: (1) de la run, copia el commit del tag
mlflow.source.git.commiti executagit checkout <commit>al seu clon — això recupera codi,params.yaml,dvc.yamli els metafitxers.dvc; (2) executadvc checkout(ambdvc pullsi li falten dades a la caché) — com que els metafitxers del commit apunten a les dades d'aleshores, eldades_dvc_md5de la run li serveix de verificació que té la versió correcta; (3) recrea l'entorn amb el lock d'aquell commit (pip-sync requirements.lock); (4) executadvc repro. Si la run era d'exploració (script fora del pipeline), executa l'script amb els params que mostra la run. Tot el context necessari era a la run: això és exactament el que "la v2" mai no va tenir.
Conclusió
CineClick ara té memòria: cada entrenament queda registrat a l'experiment churn-cineclick amb els seus paràmetres, mètriques, signatura del model, commit de codi i hash de dades, consultable i comparable a la UI de MLflow. La divisió de treball és clara — DVC governa dades i pipeline, MLflow observa i recorda — i la primera cerca sistemàtica d'hiperparàmetres ha donat fruit: amb class_weight="balanced", n_estimators=300 i max_depth=20, el recall puja de 0.43 a 0.68 (precision 0.55, F1 0.61), un intercanvi clarament favorable per al negoci de retenció. Però aquest model guanyador és, ara mateix, només "la run f0d4... que va sortir bé" i un model_churn.pkl que algú haurà de copiar allà on toqui. Com es converteix una run guanyadora en el model oficial, amb versió, estat i un nom pel qual producció el pugui demanar? Aquesta és la feina del model registry, la peça que gestiona el model "beneït" — i el tema de la propera lliçó.
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
