La lliçó anterior va acabar amb un model guanyador — recall 0.68 davant del 0.43 del base — que de moment només existeix com "la run que va sortir bé" i un model_churn.pkl en una carpeta. Aquest fitxer és l'última baula feble de la cadena: ningú no pot saber, mirant-lo, de quina run va sortir, si és la versió bona, ni qui va decidir que ho fos. En aquesta lliçó ho resolem amb el model registry de MLflow: un catàleg amb nom, versions numerades i aliases (@champion, @challenger) que converteix "el pickle beneït a mà" en un model versionat, traçable i carregable per nom des de qualsevol codi — exactament el que necessitarà el servei de predicció del mòdul 4.

Contingut

  1. El problema del model "beneït" a mà
  2. Què és un model registry
  3. Registrar el model guanyador com a churn-cineclick
  4. Versions del model
  5. Aliases i tags: @champion, @challenger (i per què les stages clàssiques estan deprecades)
  6. Carregar el model per alias des de codi
  7. El flux de promoció: revisió humana, sempre
  8. Metadades i lineage: documentar el model

El problema del model "beneït" a mà

Pensem en com funciona avui CineClick, fins i tot amb tot el que hem muntat a les lliçons anteriors. El pipeline produeix models/model_churn.pkl. Si demà el servei de predicció del mòdul 4 necessita el model, què fa? Copiar aquest fitxer? De quina màquina, de l'últim dvc repro de qui? I quan la cerca d'hiperparàmetres produeix un candidat millor, com s'"oficialitza"? Trepitjant el pickle? Aleshores perdem l'anterior. Reanomenant-lo model_churn_v2.pkl? Això és literalment com va néixer el misteri de la v2.

Els símptomes del model gestionat a mà:

  • Sense identitat: un .pkl no porta a dins les seves mètriques, la seva run d'origen ni les seves dades d'entrenament.
  • Sense versions: o trepitges (perds història) o reanomenes a mà (caos de sufixos: _v2, _final, _DEFINITIU).
  • Sense estat: res no distingeix "el model en producció" d'"un experiment del dimarts". L'estat viu al cap de Laura.
  • Sense contracte de consum: cada consumidor (un servei, un batch, un notebook) apunta a una ruta de fitxer diferent i s'assabenta dels canvis per sorpresa.

DVC versiona el .pkl com a output del pipeline, i això resol la reproduïbilitat (pots tornar a qualsevol commit i regenerar-lo). Però no resol la gestió del cicle de vida: quina versió és l'oficial, qui la va aprovar i com la demanen els consumidors. Per a això existeix el registry.

Què és un model registry

Un model registry és un catàleg centralitzat de models amb tres idees clau:

  • Registered model: una entrada amb nom estable — per a nosaltres, churn-cineclick. Representa el problema ("el model de churn de CineClick"), no un fitxer concret. És el nom pel qual tothom demanarà el model, per sempre.
  • Versió: cada vegada que es registra un model sota aquest nom, el registry li assigna un número incremental (1, 2, 3...). Cada versió apunta als artifacts de la run de la qual va sortir: el binari, la signatura, l'input_example.
  • Estat (aliases/tags): etiquetes que marquen quina versió juga quin paper — quina és la campiona en producció, quina és l'aspirant en avaluació.
Pickle en carpeta Model registry
Identitat Ruta de fitxer (models/model_churn.pkl) Nom estable + número de versió
Història Es perd en trepitjar (llevat d'arqueologia a Git/DVC) Totes les versions conviuen, llistades
Origen (lineage) Desconegut Enllaç directe a la run: params, mètriques, commit, dades
Estat (quin és el bo?) Convenció verbal / memòria de Laura Alias explícit i consultable (@champion)
Contracte d'entrada Cap Signatura (signature) validable
Com el demana un consumidor Copiant un fitxer d'algun lloc URI estable: models:/churn-cineclick@champion
Auditoria (qui va promocionar i quan) Impossible Registrada al registry

Un detall pràctic abans de començar: el registry necessita un tracking server amb base de dades com a backend store — no funciona contra una simple carpeta mlruns/. El servidor que vam aixecar a la lliçó anterior ja compleix:

mlflow server \
  --backend-store-uri sqlite:///mlflow.db \
  --artifacts-destination ./mlartifacts \
  --host 0.0.0.0 --port 5000
export MLFLOW_TRACKING_URI=http://127.0.0.1:5000

Registrar el model guanyador com a churn-cineclick

Hi ha dues maneres de registrar un model. La primera: a posteriori, des d'una run ja existent. Laura localitza a la UI la run guanyadora de la cerca (rf-300-20-balanced) i en registra el model:

import mlflow

# El run_id es copia de la UI (o es busca amb mlflow.search_runs)
run_id = "f0d4a91c2e8b4f7a9c31d5e6b8a2f4c0"

# "runs:/<run_id>/model" apunta a l'artifact 'model' d'aquesta run
versio = mlflow.register_model(
    model_uri=f"runs:/{run_id}/model",
    name="churn-cineclick",
)
print(versio.version)  # -> 1 (primera versió del registered model)

La segona, més còmoda per al flux habitual: en el moment de loguejar, afegint registered_model_name a la crida de log_model que ja teníem a train.py:

mlflow.sklearn.log_model(
    model,
    artifact_path="model",
    signature=signatura,
    input_example=X.head(5),
    registered_model_name="churn-cineclick",  # registra una versió nova
)

Quina fer servir? Depèn de la política de l'equip. Registrar des de train.py significa que cada dvc repro crea una versió — còmode, però omple el registry de versions intermèdies. Registrar a posteriori significa que només els candidats deliberadament triats entren al catàleg. CineClick adopta aquesta segona política: al registry només hi entren candidats seriosos, triats per un humà després de mirar la taula de runs; l'exploració es queda al tracking.

Seguint aquesta política, Laura registra dues versions, i la numeració explica la història del projecte:

  • Versió 1: el model base del mòdul 2 (run del dvc repro amb 100 arbres) — accuracy 0.86, precision 0.61, recall 0.43. És el que hi ha "en producció" conceptualment avui.
  • Versió 2: el guanyador de la cerca (n_estimators=300, max_depth=20, class_weight="balanced") — accuracy 0.842, precision 0.55, recall 0.68, F1 0.61.

Fixeu-vos en la ironia feliç: CineClick torna a tenir "una v2"... però aquesta vegada la v2 té run d'origen, params, mètriques, commit i dades enllaçats. La diferència entre les dues v2 és tot aquest mòdul.

Versions del model

Cada versió d'un registered model és immutable en l'essencial: apunta als artifacts de la seva run i això no canvia. El que sí que es pot editar és la seva descripció, els seus tags i els seus aliases. Idees que convé fixar:

  • Les versions són numeració automàtica i incremental: no tries "v2.1"; el registry assigna 1, 2, 3... La semàntica ("aquesta és la bona") no va al número, va als aliases.
  • Diverses versions conviuen: la 1 no desapareix quan arriba la 2. Això habilita el rollback instantani: si la 2 decep, l'alias torna a la 1 sense reentrenar res.
  • Una versió enllaça amb la seva run: des de la UI del registry, un clic porta a la run amb tots els seus params i mètriques. El lineage és navegable, no arqueològic.

Aliases i tags: @champion i @challenger

Com es marca quina versió és "l'oficial"? El MLflow modern fa servir aliases: punters amb nom que assenyalen exactament una versió, i que es poden moure.

Una nota històrica que evita confusions en llegir documentació antiga: MLflow tenia un sistema de stages (Staging, Production, Archived) pel qual les versions "transicionaven". Aquest sistema està deprecat des de MLflow 2.9: era rígid (només aquells estats fixos) i ambigu (poden dues versions estar a Production?). Els aliases el substitueixen amb avantatge: defineixes els noms que el teu procés necessiti i cada alias apunta exactament a una versió. Si un tutorial t'ensenya transition_model_version_stage, és material antic.

La convenció que adopta CineClick (i que ja és un estàndard de facto):

  • @champion: la versió aprovada per a producció. Només n'hi ha una.
  • @challenger: la versió candidata a destronar-la, en avaluació.

Assignar-los, amb l'API client:

from mlflow import MlflowClient

client = MlflowClient()

# La versió 1 (model base) és la campiona actual: és el que hi ha funcionant
client.set_registered_model_alias("churn-cineclick", "champion", version=1)

# La versió 2 (balanced) entra com a aspirant
client.set_registered_model_alias("churn-cineclick", "challenger", version=2)

Els tags complementen els aliases: mentre que un alias és un punter únic ("la campiona"), un tag és informació descriptiva que es pot repetir entre versions (validat_qa: "true", dataset: "juliol-2026"). Regla mental: alias = rol, tag = atribut.

client.set_model_version_tag("churn-cineclick", version=2, key="validat_subgrups", value="pendent")

Carregar el model per alias des de codi

Aquí hi ha el pagament de tot l'esforç. Qualsevol consumidor pot demanar el model per la seva URI de registry, sense saber res de rutes, runs ni versions:

import mlflow

mlflow.set_tracking_uri("http://127.0.0.1:5000")

# "Dona'm la versió que tingui l'alias champion, sigui quina sigui"
model = mlflow.sklearn.load_model("models:/churn-cineclick@champion")

# També es pot fixar una versió concreta (útil per depurar o auditar):
model_v2 = mlflow.sklearn.load_model("models:/churn-cineclick/2")

Dues observacions importants:

  • El consumidor que carrega @champion no canvia ni una línia quan es promociona una versió nova: l'alias es mou al registry i la següent càrrega porta la nova campiona. El desplegament del model es desacobla del desplegament del codi.
  • Aquesta URI — models:/churn-cineclick@champion — és exactament la que farà servir el servei FastAPI del mòdul 4 per carregar el model en arrencar. L'estem deixant fixada aquí com el contracte entre entrenament i serving.
graph LR
    R["Run guanyadora<br/>(tracking, 03-01)"] -->|register_model| RM["Registered model<br/>churn-cineclick"]
    RM --> V1["Versió 1<br/>recall 0.43"]
    RM --> V2["Versió 2<br/>recall 0.68"]
    V2 -->|alias| CH["@champion"]
    CH -->|"models:/churn-cineclick@champion"| C1["Servei de predicció<br/>(mòdul 4)"]
    CH --> C2["Scoring batch"]
    V1 -.->|rollback possible| CH

El flux de promoció: revisió humana, sempre

I com passa la versió 2 de @challenger a @champion? Aquesta és la part del registry que no és tècnica, i és deliberat: promocionar un model és una decisió amb conseqüències de negoci, i la pren una persona. El sistema calcula, compara i recomana; la decisió de promocionar és sempre humana. (I en un sector regulat, a més, auditable: qui va aprovar què i quan.)

El flux que fixa CineClick:

  1. Candidat registrat com a versió nova amb alias @challenger, amb la seva descripció i lineage complets.
  2. Revisió de mètriques globals contra la campiona, sobre el mateix conjunt de test (garantit aquí per random_state=42 i l'split estratificat del pipeline). v2 vs. v1: recall 0.68 vs. 0.43, F1 0.61 vs. 0.50, a canvi de precision 0.55 vs. 0.61.
  3. Validació en subgrups: les mètriques globals poden amagar degradacions locals. Laura talla les mètriques per pla, per trams d'antiguitat_mesos i per metode_pagament, i comprova que la millora de recall no es concentra en un segment mentre un altre empitjora. (Això també és una primera línia de defensa d'equitat: que el model no funcioni notablement pitjor per a un col·lectiu concret.)
  4. Criteri de negoci explícit: acordat amb l'equip de retenció — "acceptem baixar la precision fins a 0.50 si el recall supera 0.60, perquè el cost d'una trucada innecessària és molt inferior al d'una baixa no detectada". La v2 el compleix amb marge.
  5. Decisió i moviment de l'alias, amb la revisió documentada:
# La revisora (Laura, amb el vistiplau de retenció) mou l'alias
client.set_registered_model_alias("churn-cineclick", "champion", version=2)
client.delete_registered_model_alias("churn-cineclick", "challenger")
client.set_model_version_tag("churn-cineclick", 2, "aprovat_per", "laura")
client.set_model_version_tag("churn-cineclick", 2, "data_promocio", "2026-07-07")
  1. Rollback preparat: si després del desplegament la v2 decep, set_registered_model_alias("churn-cineclick", "champion", version=1) i el següent consumidor que carregui l'alias torna al model anterior. Sense reentrenar, sense buscar fitxers.

Al mòdul 5 automatitzarem parts d'aquest flux (tests que verifiquen els criteris com a gates de CD), i al 5.4 veurem com exposar el challenger a trànsit real (shadow/canary) abans de decidir. Però l'estructura — candidat, evidència, criteris, decisió humana, rollback — queda fixada aquí.

Metadades i lineage: documentar el model

Una versió sense context és un altre pickle amb número. L'última milla és anotar al registry tot el que un futur lector (Marc d'aquí a sis mesos, un auditor, la mateixa Laura) necessitarà:

client.update_model_version(
    name="churn-cineclick",
    version=2,
    description=(
        "RandomForest amb class_weight='balanced', n_estimators=300, max_depth=20. "
        "Puja el recall de 0.43 a 0.68 (precision 0.61 -> 0.55, F1 0.50 -> 0.61). "
        "Triat a la cerca d'hiperparàmetres del 2026-07-07 (12 runs). "
        "Criteri: maximitzar recall amb precision >= 0.50 (acord amb retenció)."
    ),
)
client.set_model_version_tag("churn-cineclick", 2, "git_commit", "a1b2c3d")
client.set_model_version_tag("churn-cineclick", 2, "dades_dvc_md5", "af12bc90...")

Bona part del lineage ja ve "gratis" perquè la versió enllaça amb la seva run, i la run (lliçó anterior) porta el commit de Git i el hash de dades DVC. La descripció i els tags de la versió ho fan visible sense excavar: la fitxa del model es llegeix a la UI del registry en trenta segons. La signatura (signature) que vam registrar amb el model completa la fitxa: qualsevol pot veure quines columnes i tipus espera la v2 abans d'intentar fer-la servir. Al mòdul 6 formalitzarem aquesta documentació (model cards, governança); la disciplina d'anotar cada versió comença aquí.

Errors Comuns i Consells

  • Fer servir les stages deprecades (Staging/Production) perquè el tutorial que has trobat és del 2022. Funcionaran una temporada amb avisos, però estàs aprenent un sistema en extinció. Aliases i tags són el present.
  • Registrar cada run automàticament "per si de cas": un registry amb 80 versions sense criteri és tan opac com una carpeta amb 80 pickles. Decideix una política d'entrada (a CineClick: només candidats revisats per un humà) i compleix-la.
  • Moure l'alias @champion sense deixar rastre del perquè: el moviment queda registrat, però el motiu no, llevat que l'escriguis (descripció/tags). La pregunta de l'auditor no és "quan va canviar?" sinó "per què i amb quina evidència?".
  • Carregar per versió fixa en producció (models:/churn-cineclick/2) i oblidar-ho: perds el desacoblament que dona l'alias, i les promocions deixen d'arribar al servei. Versió fixa per depurar i auditar; alias per consumir.
  • Comparar challenger i champion sobre tests diferents: si cadascun es va avaluar amb un altre split o una altra versió de les dades, la comparació és soroll. Mateix test set, mateixes dades (el hash DVC de les runs permet verificar-ho).
  • Oblidar que el registry necessita servidor amb base de dades: contra una carpeta mlruns/ sense backend store, register_model falla. Si treballes sol, mlflow server --backend-store-uri sqlite:///mlflow.db és suficient.

Exercicis

  1. Política de rollback. Tres dies després de promocionar la v2, l'equip de retenció reporta que arriben massa trucades a clients de pla premium que no tenien cap intenció de donar-se de baixa. Escriu (a) el codi per retornar @champion a la versió 1, (b) els tags que deixaries a la versió 2 per documentar el que ha passat, i (c) quina anàlisi del flux de promoció hauria d'haver detectat el problema abans.

  2. URIs de model. Indica què carrega cadascuna d'aquestes URIs i en quina situació faries servir cadascuna: models:/churn-cineclick@champion, models:/churn-cineclick/1, runs:/f0d4a91c.../model.

  3. Disseny d'aliases. CineClick vol, a més de champion i challenger, mantenir identificada l'última versió que ha passat la validació de subgrups encara que no sigui encara candidata. Ho modelaries amb un alias o amb un tag? Justifica l'elecció i escriu el codi.

Solucions

  1. (a) Rollback:
from mlflow import MlflowClient
client = MlflowClient()
client.set_registered_model_alias("churn-cineclick", "champion", version=1)

(b) Documentació a la v2:

client.set_model_version_tag("churn-cineclick", 2, "estat", "retirada")
client.set_model_version_tag("churn-cineclick", 2, "motiu_retirada",
                             "excés de falsos positius en pla premium (2026-07-10)")

(c) El pas 3 del flux (validació en subgrups): la precision per segment pla=premium hauria mostrat la concentració de falsos positius abans de promocionar. Lliçó: les mètriques globals (precision 0.55) poden ser acceptables mentre un subgrup concret està molt per sota.

  1. models:/churn-cineclick@champion carrega la versió que tingui l'alias en aquell moment — és la URI dels consumidors de producció, perquè segueix les promocions sense canviar codi. models:/churn-cineclick/1 carrega exactament la versió 1 encara que l'alias s'hagi mogut — útil per auditar, comparar o reproduir un comportament passat. runs:/f0d4a91c.../model carrega l'artifact directament des de la run, sense passar pel registry — útil durant l'exploració, abans que el model tingui entitat de candidat.

  2. Amb un alias (per exemple @validat): el requisit és assenyalar una versió concreta ("l'última que ha passat la validació"), que és exactament la semàntica d'un alias — punter únic que es mou. Un tag validat=true s'acumularia en totes les versions que alguna vegada han passat la validació, i caldria buscar quina és la més recent. Tots dos poden conviure: el tag com a atribut històric, l'alias com a punter a l'actual.

client.set_registered_model_alias("churn-cineclick", "validat", version=2)

Conclusió

El model de churn ja no és un fitxer solt: és el registered model churn-cineclick, amb la versió 1 (el base, recall 0.43) i la versió 2 (el balanced, recall 0.68) convivint al catàleg, la 2 promocionada a @champion després d'un flux de revisió amb criteris explícits i decisió humana, i qualsevol consumidor pot carregar-la amb models:/churn-cineclick@champion — la URI que el mòdul 4 farà servir com a contracte. Versions, estat, lineage i rollback: el problema del "model beneït" del diagnòstic queda tancat. Però en preparar el terreny per servir-lo apareix un risc més subtil: el model espera features (les que construeix features.py), i en producció algú les haurà de calcular de nou, en un altre codi i en un altre moment. Si aquest càlcul no és idèntic al de l'entrenament, el model campió donarà prediccions corruptes sense que res falli visiblement. D'aquest problema — i de l'eina que la indústria ha inventat per a ell, el feature store, amb la seva corresponent mirada crítica — tracta l'última lliçó del mòdul.

© Copyright 2026. Tots els drets reservats