Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Patrons d’implementació per MLOps

Com usar aquest document

Aquest no és un manual per copiar i enganxar. Cada patró resol un problema concret que trobareu quan porteu un model de ML a producció. Abans de mirar el codi, enteneu:

  1. Quin problema resol - Per què necessiteu aquest patró?
  2. Quan aplicar-lo - En quines situacions és útil?
  3. Quines decisions heu de prendre - Què heu de pensar abans d’implementar?

Quan trobeu un problema al vostre projecte, busqueu el patró corresponent aquí.


Part 1: Estructura i tooling

Patrons per organitzar el projecte. Aquestes són les bases que cal establir abans de construir funcionalitats.

Patró: Module-as-Script (python -m)

El problema

python scripts/train.py # Funciona? ./scripts/train.py # I això? cd scripts && python train.py # I des d'aquí?

Depèn del directori, del PYTHONPATH, de moltes coses.

La solució

python -m app.train # Sempre funciona python -m app.pipeline # Des de qualsevol lloc

Com implementar-ho

# app/train.py def main(): # Lògica principal if __name__ == "__main__": main()

Ara podeu:

  • Executar: python -m app.train
  • Importar: from app.train import main
  • Testejar: main() directament

Patró: Path Resolution

El problema

model = joblib.load("models/model.pkl") # Depèn del working directory model = joblib.load("/home/joan/model.pkl") # No portable

La solució

# config.py from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent class Settings(BaseSettings): model_path: str = "models/model.pkl" def get_model_path(self) -> Path: return PROJECT_ROOT / self.model_path

Ara funciona des de qualsevol directori i a Docker.


Patró: Makefile com a Interfície

El problema

  • Cada desenvolupador executa comandes diferents
  • Difícil recordar tots els flags
  • CI/CD duplica scripts

La solució

El Makefile és el vostre contracte d’interfície:

setup: python -m venv venv && pip install -r requirements.txt test: pytest tests/ -v docker-up: docker compose up -d health: curl http://localhost:8000/health

Tothom interactua amb el projecte igual: make test, make docker-up, make health.

Avantatge clau

Si make health funciona localment, funcionarà al servidor CI (mateixes comandes).


Patró: Structured Logging

El problema

Quan alguna cosa falla en producció, com sabeu què ha passat? La primera instíncta és usar print(), però té limitacions clares:

print()import logging
FormatCada llamada té el seu propi formatUn seol format definit una vegada, aplicat a tota la sortida
SortidesSempre sols stdoutPodeu enviar a consola, fitxer, servei extern… sense canviar el codi
FiltratNo podeu desactivar-lo sense esborrar líniesControleu el nivell (DEBUG, INFO, ERROR…) per entorn
UbicacióCal cercar on va fer el printCada missatge porta automàticament el nom del module i la línia

La clau és que amb logging configureu una vegada i tot el codi del projecte n’aprofita automàticament.

Nivells de log

NivellQuan usar-lo
DEBUGDetalls per desenvolupament
INFOConfirmació que les coses funcionen
WARNINGAlguna cosa inesperada però no crítica
ERRORError que impedeix una operació
CRITICALError greu, el sistema pot fallar

Estructura bàsica

Primer, configureu el root logger amb els dos handlers — un per consola i un per fitxer. Feu-ho una vegada, normalment al punt d’entrada del projecte (ex: main() o app/config.py):

import logging def setup_logging(log_level: str = "INFO", log_file: str = "app.log"): handlers = [ logging.StreamHandler(), # Consola (stdout) logging.FileHandler(log_file), # Fitxer ] logging.basicConfig( level=getattr(logging, log_level.upper()), format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=handlers, )

Després, a cada module on necessiteu logar, creeu un logger amb el nom del module:

import logging logger = logging.getLogger(__name__) logger.info("Model carregat: version=%s", version) logger.error("Predicció fallida", exc_info=True) # Inclou traceback

Penseu-hi

  1. Què logar: Startup, cada predicció (sense dades sensibles), errors
  2. On enviar: Console (per Docker), fitxer (per persistència)
  3. Quin nivell per entorn: DEBUG en dev, INFO/WARNING en prod

Error típic

“Uso print() per debugging”

Useu logger.debug(). En producció podeu desactivar debug sense tocar el codi.


Part 2: Gestió de configuració i dades

Un projecte real té configuració (paths, ports, credencials) i dades que cal processar. Com organitzar-ho?

Patró: Configuration Management

El problema

Mireu aquest codi:

model = joblib.load("/home/joan/projecte/models/model.pkl") DB_PASSWORD = "secret123"

Què passa quan:

  • Un altre membre de l’equip clona el projecte?
  • Voleu executar en un servidor amb paths diferents?
  • Algú veu la contrasenya al repositori git?

La solució: Variables d’entorn

Separeu què (el codi) de on/com (la configuració):

.env (fitxer) → pydantic-settings (validació) → settings (objecte Python)

Penseu-hi

Abans d’implementar, classifiqueu la vostra configuració:

ConfiguracióObligatòria?Té default?És secreta?
MODEL_PATHNoNo
API_PORTNoSí (8000)No
DB_PASSWORDNo

Les obligatòries sense default fallaran si no existeixen - això és bo, detecteu errors ràpid.

Estructura bàsica

# config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): model_path: str # Obligatòria api_port: int = 8000 # Amb default log_level: str = "INFO" # Amb default class Config: env_file = ".env" settings = Settings()

Ara a qualsevol lloc:

from config import settings model = joblib.load(settings.model_path)

Error típic

“Tinc config.py però també faig os.environ.get() en altres llocs”

Centralitzeu tota la configuració. Un sol punt d’entrada.


Patró: ETL per ML

El problema

Les dades rarament arriben netes i llestes. Necessiteu:

  1. Obtenir-les (d’un fitxer, base de dades, API…)
  2. Transformar-les (validar, netejar, crear features…)
  3. Guardar-les (en format adequat per entrenar)

Això és ETL: Extract, Transform, Load.

Penseu-hi

Per cada fase:

Extract:

  • D’on venen les dades?
  • Són massa grans per carregar a memòria d’un cop?

Transform:

  • Quines validacions necessiteu?
  • Què feu amb registres invàlids?
  • Quines transformacions apliqueu?

Load:

  • Quin format de sortida? (CSV llegible, Parquet eficient)
  • Guardeu metadades (quants registres, quan es va processar)?

Estructura bàsica

def run_pipeline(input_path: Path, output_dir: Path): # EXTRACT df = pd.read_csv(input_path) # TRANSFORM df = validar(df) df = netejar(df) df = crear_features(df) # SPLIT (específic per ML) train, val, test = dividir(df) # LOAD guardar(train, val, test, output_dir)

Principis importants

  1. Idempotència: Executar dues vegades → mateix resultat
  2. Logging: Registrar cada pas (quants registres entren/surten)
  3. Error handling: Un registre erroni no ha de fer fallar tot

Error típic

“Faig les transformacions al notebook i copio les dades manualment”

El pipeline ha de ser un script executable. Si canvia el dataset, només cal tornar a executar.


Part 3: Servir prediccions

Quan el model està entrenat, heu de decidir com el fareu accessible. Aquesta decisió té impacte en tota l’arquitectura.

Patró: Online vs Batch Prediction

El problema

Teniu un model entrenat. Ara què? Hi ha dues estratègies fonamentalment diferents:

OnlineBatch
MetàforaUn cambrer que serveix plats a demandaUna cuina industrial que prepara 1000 menús
Quan s’executaQuan arriba cada peticióProgramat (cada hora, nit, setmana…)
Qui esperaL’usuari, davant la pantallaNingú - és un procés en background

Penseu-hi

Abans d’implementar, responeu aquestes preguntes:

  1. L’usuari final espera una resposta immediata?
  2. Necessiteu processar moltes dades d’un cop (milers de registres)?
  3. La predicció és per a una acció immediata o per a un report?

Si l’usuari espera → Online. Si processeu en background → Batch.

Quan usar Online

  • Recomanacions mentre l’usuari navega
  • Detecció de frau en una transacció
  • Predicció de preu en una app

L’estructura bàsica és:

Petició HTTP → Validar input → Model.predict() → Retornar JSON

Quan usar Batch

  • Generar un report diari amb prediccions per tots els clients
  • Preprocessar un dataset gran per anàlisi
  • Calcular scores que es consultaran després (cache)

L’estructura bàsica és:

Llegir fitxer → Iterar per chunks → Guardar resultats

Error típic

“Faig un bucle que crida l’API per cada registre del CSV”

Això és el pitjor dels dos mons: la latència d’online amb el volum de batch. Si teniu molts registres, processeu-los en batch o envieu-los tots de cop a l’API.


Patró: Error Handling

El problema

En producció, les coses fallen. La diferència entre un sistema amateur i un professional és com comunica els errors.

Compareu:

  • Amateur: “500 Internal Server Error”
  • Professional: “El camp ‘age’ ha de ser un número positiu (rebut: -5)”

Els codis HTTP que heu de conèixer

CodiSignificatExemple
200Tot béPredicció exitosa
400Error de l’usuariDades invàlides
422Dades mal formadesJSON incorrecte
500Error del servidorBug al codi
503Servei no disponibleModel no carregat

Penseu-hi

  1. Quins errors poden passar? (Feu una llista)
  2. Quins són culpa de l’usuari? (→ 4xx)
  3. Quins són culpa del sistema? (→ 5xx)
  4. Quin missatge és útil per l’usuari sense revelar detalls interns?

Estructura recomanada

@app.post("/predict") def predict(features: Features): # 1. Validació de negoci if not es_valid(features): raise HTTPException(400, detail="Explicació clara del problema") try: # 2. Lògica principal prediction = model.predict(...) return {"prediction": prediction} except HTTPException: raise # Re-llençar errors HTTP except Exception as e: # 3. Log intern (amb detalls), resposta genèrica (sense detalls) logger.error("Error", exc_info=True) raise HTTPException(500, detail="Error intern del servidor")

La clau és: logs detallats per vosaltres, missatges clars per l’usuari.


Part 4: Persistència i base de dades

Ara que l’API serveix prediccions i gestiona errors, necessiteu guardar-les per auditoria, debugging i monitoring.

Patró: SQLAlchemy amb FastAPI

El problema

Voleu guardar cada predicció per:

  • Auditoria: Qui va demanar què i quan
  • Debugging: Investigar prediccions estranyes
  • Monitoring: Estadístiques d’ús i latència

Penseu-hi

  1. Quina base de dades?

    • SQLite: Zero configuració, ideal per prototips
    • PostgreSQL: Robust, per producció real
  2. Què guardeu de cada predicció?

    • Timestamp
    • Input features (totes? les importants?)
    • Output
    • Versió del model
    • Latència

Estructura bàsica

Primer, definiu què voleu guardar:

class PredictionLog(Base): __tablename__ = "predictions" id = Column(Integer, primary_key=True) timestamp = Column(DateTime, default=datetime.utcnow) # TODO: Decidiu quins camps d'input guardeu # ... prediction = Column(Float) model_version = Column(String) latency_ms = Column(Float)

Després, integreu amb l’API usant dependency injection.


Patró: Dependency Injection

El problema

L’endpoint de predicció necessita accés a:

  • El model
  • La base de dades
  • La configuració

Com ho organitzeu sense crear un embolic?

Sense DI (problemàtic)

@app.post("/predict") def predict(features: Features): db = SessionLocal() # Crear connexió model = load_model() # Carregar model try: # ... lògica ... finally: db.close() # Recordar tancar!

Problemes:

  • Difícil de testejar (sempre usa recursos reals)
  • Fàcil oblidar netejar recursos
  • Lògica barrejada amb infraestructura

Amb DI (recomanat)

def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.post("/predict") def predict(features: Features, db: Session = Depends(get_db)): # FastAPI gestiona la connexió automàticament # ...

Avantatge clau: Per testejar, podeu substituir get_db per una funció que retorna una BD de test.


Part 5: Testing

Com testegeu un sistema de ML?

Patró: Testing per ML Systems

El problema

Els sistemes ML tenen parts deterministes (codi) i parts estocàstiques (el model). No podeu testejar tot igual.

Penseu-hi

Què SÍ testejar:

  • Validació d’entrada: Input vàlid passa, invàlid falla
  • Format de sortida: La predicció té l’estructura esperada
  • Endpoints: Retornen els codis HTTP correctes
  • Preprocessament: Les transformacions funcionen

Què NO testejar:

  • Accuracy exacta del model (varia entre entrenaments)
  • Prediccions específiques (el model canvia)

Estructura bàsica amb pytest

# test_api.py def test_predict_valid_input(client, sample_input): response = client.post("/predict", json=sample_input) assert response.status_code == 200 assert "prediction" in response.json() def test_predict_invalid_input(client): invalid = {"age": -5} # Edat negativa response = client.post("/predict", json=invalid) assert response.status_code == 400 # Error d'usuari

Fixtures per reutilitzar setup

# conftest.py @pytest.fixture def sample_valid_input(): return {"age": 30, "income": 50000} @pytest.fixture def test_client(): return TestClient(app)

Error típic

“El meu test verifica que predict(X) == 0.847

Els models no són deterministes. Testegeu que la predicció és un float entre 0 i 1, no un valor concret.


Patró: Test Database Fixtures

El problema

Tests que usen la BD real són:

  • Lents
  • Fràgils (depenen d’estat extern)
  • Problemàtics en CI/CD

La solució: BD temporal en memòria

@pytest.fixture def test_db(): # Crear BD en memòria engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(bind=engine) session = sessionmaker(bind=engine)() try: yield session finally: session.close()

Cada test rep una BD buida i aïllada.


Part 6: Qualitat i operacions

El testing valida que el codi és correcte. Ara, abans de desplegar, necessiteu també validar que el sistema complet està llest per producció.

Patró: Quality Gates

El problema

Un model serialitzat existeix (.pkl), però pot tenir accuracy de 0.3, features incompatibles, o un training que va fallar silenciosament. Com eviteu que arribi a producció?

Penseu-hi

Definiu els vostres criteris de desplegament abans d’implementar:

  1. Quina mètrica és crítica pel vostre problema? (accuracy, F1, MAE…)
  2. Quin és el llindar mínim acceptable?
  3. Hi ha altres condicions? (features presents, latència màxima…)

Documenteu aquests criteris en un fitxer de configuració (config/deployment_criteria.yaml o similar).

Com implementar-ho

Principi clau: El model no es considera desplegable per defecte. Ha de passar una validació explícita. Només llavors es marca com deployment_ready: true a la metadata.

Al final del vostre script d’entrenament:

# Després d'entrenar i avaluar metrics = {"accuracy": accuracy, "f1_score": f1} # Carregar criteris criteria = load_criteria("config/deployment_criteria.yaml") # Decisió: passa o no passa? deployment_ready = ( metrics["accuracy"] >= criteria["min_accuracy"] and metrics["f1_score"] >= criteria["min_f1"] ) # Guardar metadata amb el flag metadata = { "version": version, "metrics": metrics, "deployment_ready": deployment_ready # Resultat de la validació } save_metadata(metadata)

El hook pre-deploy.sh del CI/CD llegeix deployment_ready de la metadata. Si és False → el pipeline falla i el model no es desplega.

make test # Tests passen? python validate.py # Model deployment_ready? make docker-build # Es pot construir? make health # Servei respon?

Si qualsevol passa falla → NO es desplega.

Error típic

“El meu training script sempre posa deployment_ready: true

Si el flag és sempre True independentment de les mètriques, no esteu protegint res. El valor ha de ser el resultat d’una validació real contra criteris definits prèviament.


Patró: Health Check

El problema

Com sabeu si el servei està funcionant correctament?

  • Els load balancers necessiten saber si poden enviar tràfic
  • El monitoring necessita alertar si hi ha problemes
  • Després d’un deploy, voleu verificar que tot funciona

Penseu-hi

Què ha de verificar el health check?

  1. Model carregat? (model is not None)
  2. Base de dades accessible? (db.execute("SELECT 1"))
  3. Espai en disc suficient?

Estructura bàsica

@app.get("/health") def health_check(): model_ok = model is not None db_ok = check_database() if model_ok and db_ok: return {"status": "healthy", "model_version": VERSION} else: raise HTTPException(503, detail="Service unhealthy")

Error típic

“El health check fa una predicció real per verificar el model”

Els health checks han de ser ràpids (< 1 segon). Verificar que el model està carregat és suficient.


Patró: Model Versioning

El problema

Teniu un model en producció. Entreno un de nou. Com gestioneu les versions?

Penseu-hi

El versionat de models (semantic versioning, metadata, estructura de directoris, symlinks, model registry) es tracta en detall al capítol de desplegament. El punt clau per a implementació és afegir el flag deployment_ready a la metadata:

models/ ├── model_v1.0.0.pkl # Model serialitzat └── metadata_v1.0.0.json # Informació sobre el model

El metadata ha d’incloure: versió, data d’entrenament, mètriques, noms de features, i el flag deployment_ready (generat pel Quality Gate).


Patró: Rollback

El problema

Heu desplegat un model nou (v1.2.0). Les mètriques de producció mostren problemes: el temps de resposta és massa alt o les prediccions són pitjors del que esperàveu. Necessiteu tornar a la versió anterior (v1.1.0) ràpidament.

Penseu-hi

Què necessiteu tenir preparat abans que passi el problema?

  • Les versions anteriors del model guardades
  • Una manera de canviar quin model es carrega sense modificar codi
  • Un procés de reinici ràpid

La solució

Si el model es carrega des d’una variable d’entorn:

# config.py class Settings(BaseSettings): model_path: str = "models/model_v1.2.0.pkl"

Fer rollback és:

# 1. Canviar .env MODEL_PATH=models/model_v1.1.0.pkl # 2. Reiniciar el servei docker-compose restart api

Sense canviar codi, sense fer commit, sense CI/CD.

Què fa possible un rollback ràpid

  1. Versions guardades: Mai sobreescriu el model anterior
  2. Configuració externa: El path del model és una variable, no hardcoded
  3. Metadata: Saps quina versió estàs servint i pots comparar

Error típic

# ❌ Sempre carrega "model.pkl" model = joblib.load("models/model.pkl")

Quan fas un nou entrenament, sobreescrius el model anterior. Si alguna cosa falla, no pots tornar enrere.

# ✓ Versions explícites model = joblib.load(settings.model_path) # Controlat per .env

Resum: Preguntes per cada patró

Quan implementeu un patró, responeu primer:

PatróPregunta clau
Module-as-ScriptCom executar scripts de manera consistent?
Path ResolutionCom referenciar fitxers independentment del working directory?
MakefileQuines operacions ha de poder fer qualsevol membre de l’equip?
LoggingQuè necessiteu saber quan alguna cosa falla?
ConfigurationQuines configuracions són obligatòries vs opcionals?
ETLD’on venen les dades i com les valideu?
Online vs BatchL’usuari espera resposta immediata?
Error HandlingQuins errors poden passar i qui és responsable?
SQLAlchemyQuè guardeu de cada predicció?
Dependency InjectionQuins recursos necessita cada endpoint?
TestingQuè és determinista (testejar) vs estocàstic (no testejar)?
Quality GatesQuins criteris ha de passar el codi abans de desplegar?
Health CheckQuins components ha de verificar?
Model VersioningQuan incrementeu major/minor/patch?
RollbackQuè necessiteu tenir preparat abans que el model falli?