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

Desplegament de models

Introducció

Un cop hem entrenat un model de machine learning que funciona bé en el nostre entorn de desenvolupament, ens enfrontem a un repte completament diferent: fer que aquest model estigui disponible per a usuaris reals, de manera fiable i escalable. Aquest procés s’anomena desplegament (deployment) i és on molts projectes de ML fallen. Segons diverses estimacions de la indústria, menys del 50% dels models entrenats arriben mai a producció.

En aquest capítol aprendrem els fonaments del desplegament de models, introduirem les tecnologies que ens permetran empaquetar i servir els nostres models (Docker i FastAPI), i explorarem les decisions arquitectòniques que haurem de prendre.

Per què el desplegament és difícil?

Quan entrenem un model, treballem en un entorn controlat: tenim les nostres llibreries instal·lades, les versions que ens agraden, i executem el codi manualment. En producció, tot canvia:

  • El model ha de funcionar en una màquina diferent, potser amb un sistema operatiu diferent
  • Ha de respondre a peticions de múltiples usuaris simultàniament
  • Ha de ser actualitzable sense interrompre el servei
  • Ha de gestionar errors de manera elegant
  • Ha de ser monitoritzable per saber si funciona correctament

La solució a molts d’aquests problemes passa per contenidors i APIs.

Preparació de dades per producció

Abans de desplegar un model, necessitem preparar adequadament les nostres dades. Aquest pas és crucial perquè determina com podrem entrenar, validar, i simular el comportament del model en producció.

Per què dividir les dades?

Quan treballem en un entorn real, les dades es divideixen en diferents conjunts amb funcions diferents:

  • Training set: Per entrenar el model
  • Validation set: Per ajustar hiperparàmetres i avaluar el model durant el desenvolupament
  • Production set: Per simular les dades que el model veurà en producció (testing final, detecció de drift)

La clau és mantenir aquests conjunts completament separats per evitar que el model “vegi” dades que hauria de predir més endavant.

Splits recomanats: 60/20/20

Una divisió comuna i pràctica és:

Dataset complet (exemple: 10,000 registres) │ ├── Training (60% = 6,000) → Entrenar el model │ ├── Validation (20% = 2,000) → Validar durant desenvolupament │ └── Production (20% = 2,000) → Simular producció, detectar drift

Per què aquests percentatges?

  • 60% training: Suficient per entrenar models robustos
  • 20% validation: Suficient per avaluar de manera fiable
  • 20% production: Simula dades noves que arribaran en producció

Implementació de la divisió

Podem usar scikit-learn per crear aquests splits de manera fàcil:

from sklearn.model_selection import train_test_split import pandas as pd # Carregar dataset df = pd.read_csv('data/raw/dataset.csv') # Primera divisió: separar production set (20%) train_val, production = train_test_split( df, test_size=0.2, random_state=42, stratify=df['target'] # Mantenir proporció de classes ) # Segona divisió: separar training i validation (60% i 20% del total) training, validation = train_test_split( train_val, test_size=0.25, # 0.25 * 0.8 = 0.2 del total random_state=42, stratify=train_val['target'] ) # Verificar les mides print(f"Training: {len(training)} ({len(training)/len(df):.1%})") print(f"Validation: {len(validation)} ({len(validation)/len(df):.1%})") print(f"Production: {len(production)} ({len(production)/len(df):.1%})")

Guardar els splits

És important guardar aquests splits en fitxers separats per poder usar-los consistentment:

# Guardar com a Parquet (més eficient que CSV per a datasets grans) training.to_parquet('data/training_set.parquet', index=False) validation.to_parquet('data/validation_set.parquet', index=False) production.to_parquet('data/production_set.parquet', index=False) # Alternativa: CSV (més universal però menys eficient) training.to_csv('data/training_set.csv', index=False) validation.to_csv('data/validation_set.csv', index=False) production.to_csv('data/production_set.csv', index=False)

Parquet vs CSV:

  • Parquet: Format columnar, més ràpid de llegir, ocupa menys espai
  • CSV: Format més universal, fàcil de visualitzar en editors de text

Per datasets > 10K registres, Parquet és preferible.

Preprocessament de dades

Després de dividir, sovint cal preprocessar les dades. Les transformacions típiques inclouen:

1. Normalització de features numèriques

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() # Entrenar el scaler NOMÉS amb training data scaler.fit(training[['age', 'income']]) # Aplicar a tots els splits training[['age', 'income']] = scaler.transform(training[['age', 'income']]) validation[['age', 'income']] = scaler.transform(validation[['age', 'income']]) production[['age', 'income']] = scaler.transform(production[['age', 'income']])

⚠️ Important: Entrenar el preprocessador (scaler, encoder, etc.) només amb training data. Després aplicar-lo als altres splits. Mai entrenar amb validation o production!

2. Encoding de features categòriques

from sklearn.preprocessing import LabelEncoder encoder = LabelEncoder() # Entrenar amb training data encoder.fit(training['region']) # Aplicar a tots training['region'] = encoder.transform(training['region']) validation['region'] = encoder.transform(validation['region']) production['region'] = encoder.transform(production['region'])

3. Pipeline complet

Per mantenir consistència, podem crear un script de preprocessament:

# src/preprocess.py import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import joblib def preprocess_and_split(input_path: str, output_dir: str): """ Carrega, preprocessa, i divideix el dataset. """ # 1. Carregar dades df = pd.read_csv(input_path) # 2. Netejar dades (eliminar nulls, outliers extrems, etc.) df = df.dropna() # 3. Dividir train_val, production = train_test_split(df, test_size=0.2, random_state=42) training, validation = train_test_split(train_val, test_size=0.25, random_state=42) # 4. Preprocessar features numèriques numeric_features = ['age', 'income'] scaler = StandardScaler() scaler.fit(training[numeric_features]) training[numeric_features] = scaler.transform(training[numeric_features]) validation[numeric_features] = scaler.transform(validation[numeric_features]) production[numeric_features] = scaler.transform(production[numeric_features]) # 5. Guardar splits training.to_parquet(f'{output_dir}/training_set.parquet') validation.to_parquet(f'{output_dir}/validation_set.parquet') production.to_parquet(f'{output_dir}/production_set.parquet') # 6. Guardar preprocessadors (per usar en producció!) joblib.dump(scaler, f'{output_dir}/scaler.pkl') print(f"✓ Splits guardats a {output_dir}") print(f" Training: {len(training)} registres") print(f" Validation: {len(validation)} registres") print(f" Production: {len(production)} registres") if __name__ == '__main__': preprocess_and_split('data/raw/dataset.csv', 'data/processed')

Ús del production set

El production set té dues funcions principals:

  1. Testing final: Avaluar el model entrenat en dades “fresques” que mai ha vist
  2. Simular drift: Modificar aquest set per simular canvis en la distribució (útil per capítols posteriors)
# Exemple: simular drift d'edat (població envelleix) production_drift = production.copy() production_drift['age'] = production_drift['age'] + 5 # +5 anys # Ara podem detectar aquest drift amb tests estadístics!

Bones pràctiques

  1. Random seed fix: Usa sempre el mateix random_state per reproduibilitat
  2. Stratify per classes: En classificació, usa stratify=df['target'] per mantenir proporcions
  3. Guardar preprocessadors: Desa scalers, encoders, etc. per usar-los en producció
  4. Documentar splits: Inclou un fitxer data/README.md explicant com es van crear
# Dataset Splits - **Source**: California Housing dataset - **Total records**: 20,640 - **Split date**: 2024-01-15 - **Split method**: 60/20/20 stratified by price_category - **Random seed**: 42 ## Files - `training_set.parquet`: 12,384 records (60%) - `validation_set.parquet`: 4,128 records (20%) - `production_set.parquet`: 4,128 records (20%) - `scaler.pkl`: StandardScaler fitted on training data

Amb les dades preparades adequadament, estem llestos per entrenar el model i desplegar-lo!

Contenidors amb Docker

Què és un contenidor?

Un contenidor (container) és una unitat de programari que empaqueta el codi i totes les seves dependències perquè l’aplicació s’executi de manera ràpida i fiable en diferents entorns. Podem pensar en un contenidor com una “capsa” que conté tot el necessari per executar la nostra aplicació: el codi, les llibreries, les configuracions, i fins i tot una versió mínima del sistema operatiu.

┌─────────────────────────────────────┐ │ Contenidor │ │ ┌───────────────────────────────┐ │ │ │ La nostra aplicació (model) │ │ │ ├───────────────────────────────┤ │ │ │ Python 3.11, scikit-learn, │ │ │ │ FastAPI, pandas... │ │ │ ├───────────────────────────────┤ │ │ │ Sistema operatiu mínim │ │ │ │ (Ubuntu, Alpine...) │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘

Docker: l’eina estàndard

Docker és l’eina més utilitzada per crear i gestionar contenidors. Els conceptes clau són:

  • Imatge (image): Una plantilla de només lectura amb les instruccions per crear un contenidor. És com un “motlle”.
  • Contenidor (container): Una instància executable d’una imatge. És el “pastís” fet a partir del motlle.
  • Dockerfile: Un fitxer de text amb les instruccions per construir una imatge.
  • Docker Compose: Una eina per definir i executar aplicacions amb múltiples contenidors.

Estructura bàsica d’un Dockerfile

Un Dockerfile per a un model de ML típicament té aquesta estructura:

# Imatge base amb Python FROM python:3.11-slim # Directori de treball dins del contenidor WORKDIR /app # Copiar i instal·lar dependències COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copiar el codi i el model COPY model/ ./model/ COPY app.py . # Port on escoltarà l'aplicació EXPOSE 8000 # Comanda per executar l'aplicació CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

El flux de treball amb Docker

Codi + Model

Dockerfile

docker build

Imatge

docker run

Contenidor en execució

Registre d'imatges

Servidor de producció

docker run

Contenidor en producció

El procés típic és:

  1. Escrivim el Dockerfile que descriu com construir la imatge
  2. Construïm la imatge localment amb docker build
  3. Provem el contenidor localment amb docker run
  4. Pugem la imatge a un registre (pot ser un registre privat al nostre servidor)
  5. Al servidor de producció, descarreguem i executem la imatge

Docker Compose: orquestració simplificada

Fins ara hem vist com crear una imatge Docker, però quan despleguem aplicacions reals, sovint necessitem múltiples contenidors treballant junts (per exemple, API + base de dades) o simplement volem configurar més fàcilment un sol contenidor. Aquí és on entra Docker Compose.

Docker Compose és una eina per definir i executar aplicacions Docker amb múltiples contenidors usant un fitxer YAML.

Quan usar Docker Compose?

  • Configuració complexa: Moltes variables d’entorn, volums, ports
  • Múltiples serveis: API + base de dades + cache
  • Desenvolupament local: Levantar tot l’entorn amb un sol comando
  • Reproduibilitat: Configuració compartible i versionable

Estructura bàsica d’un docker-compose.yml

version: '3.8' services: api: build: . ports: - "8000:8000" environment: - MODEL_PATH=/models/model.pkl - LOG_LEVEL=info volumes: - ./models:/models - ./logs:/logs healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s

Explicació dels camps:

  • services: Defineix els contenidors (aquí només n’hi ha un: api)
  • build: Directori amb el Dockerfile
  • ports: Mapatge port host:contenidor (8000:8000 = port 8000 del host → port 8000 del contenidor)
  • environment: Variables d’entorn
  • volumes: Muntar directoris del host al contenidor
  • healthcheck: Comanda per verificar que el servei està sa

Health checks: verificar que tot funciona

El health check és crucial per saber si el contenidor està funcionant correctament:

healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s # Cada quan executar el check timeout: 10s # Temps màxim per resposta retries: 3 # Quants errors abans de marcar com unhealthy start_period: 40s # Temps d'espera inicial (per permetre arrencada)

Aquest health check crida l’endpoint /health cada 30 segons. Si falla 3 vegades seguides, Docker marca el contenidor com “unhealthy”.

Endpoint de health a FastAPI:

@app.get("/health") def health(): """Health check endpoint.""" return { "status": "healthy", "model_loaded": model is not None, "timestamp": datetime.now().isoformat() }

Comandes de Docker Compose

# Construir i iniciar tots els serveis docker-compose up --build # Iniciar en mode detached (background) docker-compose up -d # Veure logs docker-compose logs -f # Aturar serveis docker-compose down # Veure estat dels serveis docker-compose ps

Exemple complet: API amb configuració

# docker-compose.yml version: '3.8' services: model-api: build: context: . dockerfile: Dockerfile container_name: ml_model_api ports: - "8000:8000" environment: - MODEL_PATH=/app/models/model.pkl - MODEL_VERSION=1.0.0 - LOG_LEVEL=info - MAX_WORKERS=4 volumes: - ./models:/app/models:ro # :ro = read-only - ./logs:/app/logs healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s restart: unless-stopped # Reiniciar automàticament si falla

Avantatges d’aquesta configuració:

  • Models com volum: Podem actualitzar el model sense reconstruir la imatge
  • Logs persistents: Els logs es guarden al host
  • Health check: Docker sap si el servei funciona
  • Restart policy: El contenidor es reinicia automàticament si falla

Variables d’entorn: separar configuració i codi

En lloc de hardcoded values al Dockerfile, usem variables d’entorn:

# api.py import os MODEL_PATH = os.getenv('MODEL_PATH', 'models/model.pkl') MODEL_VERSION = os.getenv('MODEL_VERSION', 'unknown') LOG_LEVEL = os.getenv('LOG_LEVEL', 'info') model = joblib.load(MODEL_PATH)

Això permet canviar configuració sense reconstruir:

environment: - MODEL_PATH=/app/models/model_v2.pkl # Canviar model - LOG_LEVEL=debug # Canviar log level

Docker Compose amb múltiples serveis (exemple avançat)

Si necessitem una base de dades per guardar prediccions:

version: '3.8' services: api: build: . ports: - "8000:8000" environment: - DATABASE_URL=postgresql://user:pass@db:5432/predictions depends_on: - db healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s db: image: postgres:15 environment: - POSTGRES_USER=user - POSTGRES_PASSWORD=pass - POSTGRES_DB=predictions volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U user"] interval: 10s volumes: postgres_data:

Ara amb docker-compose up, aixequem API + PostgreSQL automàticament!

Bones pràctiques amb Docker Compose

  1. Noms de serveis clars: model-api, postgres, no service1, app
  2. Health checks sempre: Per detectar problemes ràpidament
  3. Volums per dades persistents: Models, logs, bases de dades
  4. Variables d’entorn: Mai hardcoded secrets o configuracions
  5. restart: unless-stopped: Per serveis que han d’estar sempre actius
  6. Fitxer .env: Per secrets (no comitear!)
# .env (no comitear a git!) DATABASE_PASSWORD=super_secret_123 MODEL_VERSION=1.2.0
# docker-compose.yml services: api: env_file: .env # Carrega variables des de .env

Amb Docker Compose, el desplegament és tan simple com docker-compose up!

Predicció online vs. predicció batch

Hi ha dues estratègies principals per servir prediccions, i la tria depèn del cas d’ús.

Predicció online (en temps real)

La predicció online (online prediction) és quan el model rep una petició i ha de respondre immediatament. És el que veurem amb FastAPI a la següent secció.

Característiques:

  • Latència baixa (mil·lisegons)
  • Una predicció per petició (o poques)
  • El model està carregat en memòria constantment
  • Requereix infraestructura sempre disponible

Casos d’ús:

  • Recomanacions en temps real
  • Detecció de frau en transaccions
  • Assistents virtuals
  • Cerca personalitzada
ModelAPIUsuariModelAPIUsuariPetició amb dadesPassar featuresPrediccióResposta (ms)

Predicció batch (per lots)

La predicció batch (batch prediction) és quan processem moltes prediccions alhora, normalment de manera programada.

Característiques:

  • La latència no és crítica (minuts o hores acceptable)
  • Milers o milions de prediccions alhora
  • El model es carrega, processa, i s’atura
  • Més eficient en recursos per a grans volums
  • Les prediccions es guarden en una base de dades per ser consultades després

Casos d’ús:

  • Enviament massiu de correus personalitzats
  • Càlcul nocturn de puntuacions de risc
  • Generació de recomanacions pre-calculades
  • Informes periòdics

Flux típic d’una predicció batch:

  1. Dades acumulades: Es recullen i acumulen les dades que s’han de processar.
  2. Script batch: S’executa un script o procés per iniciar el lot de prediccions.
  3. Carregar model: El model de machine learning es carrega a la memòria.
  4. Processar tot: Es processen totes les dades d’una sola vegada, aplicant el model a cada cas.
  5. Guardar a base de dades: Els resultats es desen en una base de dades (o fitxers) per a consulta posterior.

La diferència clau amb predicció online és que els resultats no es retornen immediatament a l’usuari. En lloc d’això, es guarden i l’aplicació els consulta quan els necessita:

AplicacióUsuariBase de dadesModelScript batchAplicacióUsuariBase de dadesModelScript batchProcés batch (nit, cada hora...)Més tard...Processar totes les dadesPrediccionsGuardar prediccionsPeticióConsultar prediccióPredicció pre-calculadaResposta (ms)

Comparativa

AspecteOnlineBatch
LatènciaMil·lisegonsMinuts/hores
Volum per execucióBaixAlt
RecursosSempre actiusSota demanda
ComplexitatMés altaMés baixa
Freshness de prediccionsTemps realPeriòdic
On es guarden resultatsRetorn immediatBase de dades

Arquitectura híbrida

En molts sistemes reals es combinen ambdues estratègies:

  • Batch per pre-calcular prediccions per als casos més comuns
  • Online per als casos nous o que requereixen context en temps real

Per exemple, una botiga online podria pre-calcular recomanacions cada nit per a tots els usuaris (batch), però calcular en temps real les recomanacions basades en el que l’usuari està mirant ara mateix (online).

A continuació, veurem com implementar predicció online amb FastAPI.

Servir models amb FastAPI

Per què una API?

Per fer que el nostre model sigui accessible, necessitem exposar-lo a través d’una API (Application Programming Interface). Una API web permet que altres aplicacions facin peticions al nostre model i rebin prediccions com a resposta.

El protocol més comú per a APIs web és HTTP, i l’arquitectura més utilitzada és REST (Representational State Transfer). En una API REST, fem peticions a URLs específiques (anomenades endpoints) utilitzant mètodes HTTP com GET o POST.

FastAPI: modern i ràpid

FastAPI és un framework de Python per crear APIs. Els seus avantatges principals són:

  • Ràpid: Un dels frameworks més ràpids de Python, comparable a Node.js o Go
  • Fàcil: Sintaxi intuïtiva basada en tipus de Python
  • Documentació automàtica: Genera documentació interactiva automàticament
  • Validació automàtica: Valida les dades d’entrada automàticament

Exemple bàsic d’una API per servir un model

from fastapi import FastAPI, HTTPException from pydantic import BaseModel import joblib import numpy as np import logging logger = logging.getLogger(__name__) # Carregar el model entrenat model = joblib.load("model/model.pkl") # Crear l'aplicació FastAPI app = FastAPI(title="API de Predicció") # Definir l'estructura de les dades d'entrada class InputData(BaseModel): feature_1: float feature_2: float feature_3: float # Definir l'estructura de la resposta class Prediction(BaseModel): prediction: float model_version: str = "1.0.0" # Endpoint per fer prediccions @app.post("/predict", response_model=Prediction) def predict(data: InputData) -> Prediction: try: # Convertir a format que espera el model features = np.array([[data.feature_1, data.feature_2, data.feature_3]]) # Fer la predicció result = model.predict(features)[0] return Prediction(prediction=float(result)) except (ValueError, TypeError) as e: logger.error(f"Validation error: {str(e)}") raise HTTPException(status_code=422, detail=f"Invalid input: {str(e)}") except Exception as e: logger.error(f"Prediction error: {str(e)}") raise HTTPException(status_code=500, detail="Server error") # Endpoint de salut (health check) @app.get("/health") def health() -> dict[str, str]: return {"status": "healthy"}

Integrant FastAPI amb Docker

El Dockerfile complet per a aquesta aplicació seria:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY model/ ./model/ COPY main.py . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

On requirements.txt contindria:

fastapi>=0.109.0,<1.0.0 uvicorn[standard]>=0.27.0,<1.0.0 joblib>=1.4.0,<2.0.0 scikit-learn>=1.4.0,<2.0.0 pydantic>=2.5.0,<3.0.0 numpy>=1.24.0,<2.0.0

Versionat de models

Un aspecte crític del desplegament de models és el versionat: mantenir un registre clar de quins models hem desplegat, quan, i amb quines característiques. Sense versionat, és impossible fer rollback, comparar models, o simplement saber què està en producció.

Per què versionar models?

Problemes sense versionat:

  • “Quin model està en producció ara?” → No ho sabem
  • “Aquest model funciona malament, tornem a l’anterior” → Quin era?
  • “Aquest bug existia en la v1.2?” → No tenim v1.2, només model.pkl

Avantatges del versionat:

  • Traçabilitat: Saber exactament què està desplegat
  • Rollback: Tornar a versions anteriors si cal
  • Comparació: Avaluar si un nou model és millor
  • Reproducibilitat: Reconstruir models antics si cal
  • Auditoria: Complir amb requisits regulatoris

Semantic Versioning per models

Adaptem semantic versioning (MAJOR.MINOR.PATCH) per a models ML:

v1.2.3 │ │ │ │ │ └─ PATCH: Bug fixes en preprocessament, mateix model i features │ └─── MINOR: Mateix algoritme, afegir features o reentrenament amb més dades └───── MAJOR: Nou algoritme o arquitectura significativament diferent

Exemples:

  • v1.0.0 → v1.0.1: Fix en normalització, mateix model
  • v1.0.1 → v1.1.0: Afegida feature “region”, mateix RandomForest
  • v1.1.0 → v2.0.0: Canvi de RandomForest a XGBoost

Guardar models amb versions

Estructurar els models amb informació de versió:

import joblib import json from datetime import datetime def save_versioned_model( model, version: str, metrics: dict[str, float], features: list[str], output_dir: str ) -> tuple[str, str]: """ Guarda un model amb la seva versió i metadata. Args: model: Model entrenat version: Versió (e.g., "1.2.0") metrics: Diccionari de mètriques {'f1': 0.82, 'recall': 0.85} features: Llista de features usades ['age', 'income', ...] output_dir: Directori on guardar Returns: Tuple amb (model_path, metadata_path) """ # Preparar metadata metadata = { 'version': version, 'trained_at': datetime.now().isoformat(), 'metrics': metrics, 'features': features, 'model_type': type(model).__name__ } # Guardar model model_path = f'{output_dir}/model_v{version}.pkl' joblib.dump(model, model_path) print(f"✓ Model guardat: {model_path}") # Guardar metadata metadata_path = f'{output_dir}/metadata_v{version}.json' with open(metadata_path, 'w') as f: json.dump(metadata, f, indent=2) print(f"✓ Metadata guardada: {metadata_path}") return model_path, metadata_path # Ús model = train_model(X_train, y_train) metrics = evaluate_model(model, X_val, y_val) save_versioned_model( model=model, version='1.2.0', metrics={'f1': 0.82, 'recall': 0.85, 'precision': 0.79}, features=['age', 'income', 'score', 'region'], output_dir='models' )

Això genera:

models/ ├── model_v1.2.0.pkl └── metadata_v1.2.0.json

Contingut de metadata_v1.2.0.json:

{ "version": "1.2.0", "trained_at": "2024-01-15T14:30:00", "metrics": { "f1": 0.82, "recall": 0.85, "precision": 0.79 }, "features": ["age", "income", "score", "region"], "model_type": "RandomForestClassifier" }

Per facilitar el desplegament, crear un symlink que apunti al model actual:

# En Linux/Mac ln -sf model_v1.2.0.pkl current_model.pkl # Ara podem carregar sempre current_model.pkl model = joblib.load('models/current_model.pkl')

Quan despleguem una nova versió:

# Actualitzar symlink a nova versió ln -sf model_v1.3.0.pkl current_model.pkl # Rollback si cal ln -sf model_v1.2.0.pkl current_model.pkl

Metadata extesa (recomanat per producció)

Per a entorns més complexos, guardar més informació:

metadata = { 'version': '1.2.0', 'trained_at': '2024-01-15T14:30:00', # Dataset info 'dataset': { 'training_rows': 10000, 'validation_rows': 2000, 'data_hash': 'abc123def456', # Hash del dataset per reproducibilitat }, # Mètriques 'metrics': { 'f1': 0.82, 'precision': 0.79, 'recall': 0.85, 'roc_auc': 0.89, 'latency_p95_ms': 45 }, # Features 'features': ['age', 'income', 'score', 'region'], 'num_features': 4, # Model info 'model_type': 'RandomForestClassifier', 'hyperparameters': { 'n_estimators': 100, 'max_depth': 10, 'random_state': 42 }, # Entorn 'python_version': '3.11.0', 'sklearn_version': '1.4.0', # Deployment 'deployed_at': None, # S'omple quan es desplega 'deployed_by': 'ci/cd', 'environment': 'production' }

Model registry simple

Un model registry és un lloc centralitzat per guardar i gestionar models. Pot ser tan simple com un directori amb convencions:

models/ ├── v1.0.0/ │ ├── model.pkl │ ├── metadata.json │ └── scaler.pkl ├── v1.1.0/ │ ├── model.pkl │ ├── metadata.json │ └── scaler.pkl ├── v1.2.0/ │ ├── model.pkl │ ├── metadata.json │ └── scaler.pkl └── current -> v1.2.0/ # Symlink

Funcions útils per gestionar el registry:

import os import json from pathlib import Path def list_model_versions(registry_dir: str) -> list[str]: """Llista totes les versions disponibles.""" return sorted([ d.name for d in Path(registry_dir).iterdir() if d.is_dir() and d.name.startswith('v') ]) def load_model_by_version(registry_dir: str, version: str): """Carrega un model específic per versió.""" model_path = f'{registry_dir}/{version}/model.pkl' if not os.path.exists(model_path): raise ValueError(f"Model {version} no trobat") model = joblib.load(model_path) # Carregar metadata metadata_path = f'{registry_dir}/{version}/metadata.json' with open(metadata_path, 'r') as f: metadata = json.load(f) return model, metadata # Ús versions = list_model_versions('models') print(f"Versions disponibles: {versions}") # ['v1.0.0', 'v1.1.0', 'v1.2.0'] model, metadata = load_model_by_version('models', 'v1.2.0') print(f"Model: {metadata['model_type']}, F1: {metadata['metrics']['f1']}")

Incloure versió a l’API

Afegir la versió del model a les respostes de l’API:

from fastapi import FastAPI import joblib import json app = FastAPI() # Carregar model i metadata model = joblib.load('models/current/model.pkl') with open('models/current/metadata.json') as f: metadata = json.load(f) @app.get("/model/info") def model_info(): """Retorna informació del model actual.""" return { "version": metadata['version'], "trained_at": metadata['trained_at'], "model_type": metadata['model_type'], "metrics": metadata['metrics'] } @app.post("/predict") def predict(data: InputData): prediction = model.predict([data.features])[0] return { "prediction": float(prediction), "model_version": metadata['version'] # Incloure versió! }

Ara cada predicció indica quina versió del model s’ha usat:

{ "prediction": 0.85, "model_version": "1.2.0" }

Resum del versionat

Essencial:

  • ✅ Versió clara per cada model (v1.2.0)
  • ✅ Metadata amb mètriques i features
  • ✅ Convencions de noms (model_v1.2.0.pkl, metadata_v1.2.0.json)

Recomanat:

  • ✅ Symlink current_model.pkl per facilitar desplegament
  • ✅ Directori per versió amb model + metadata + preprocessadors
  • ✅ Versió inclosa a les respostes de l’API

Avançat:

  • ✅ Metadata extesa (dataset hash, hyperparameters, entorn)
  • ✅ Model registry centralitzat (MLflow, DVC, o directori estructurat)
  • ✅ CI/CD que gestiona versions automàticament

Amb versionat adequat, tenim control complet sobre els models desplegats!

Desplegament a producció: el flux complet

Per tancar, vegem el flux complet des del desenvolupament fins a producció:

Producció

Desplegament

Desenvolupament

Entrenar model

Exportar model

Crear API FastAPI

Escriure Dockerfile

Construir imatge

Provar localment

Pujar imatge al registre

Descarregar al servidor

Executar contenidor

Model en producció

Rebre peticions

Retornar prediccions

Monitoratge

Bones pràctiques

  1. Versionar tot: El codi, el model, i les imatges Docker han de tenir versions clares
  2. Health checks: Incloure endpoints per verificar que el servei funciona (/health)
  3. Logging: Registrar peticions, errors, i temps de resposta
  4. Variables d’entorn: Configuracions (ports, paths) com a variables d’entorn, no hardcoded
  5. Provar localment: Mai desplegar directament a producció sense provar primer en local

Resum

En aquest capítol hem après els fonaments del desplegament de models:

  • Preparar dades amb splits adequats (60/20/20) i preprocessament consistent
  • Empaquetar models amb Docker per garantir reproducibilitat entre entorns
  • Servir prediccions amb FastAPI, incloent versionat de models
  • Triar entre predicció online (temps real) i batch (lots programats)
  • El flux complet de desplegament des de desenvolupament fins a producció

Ara que sabem com desplegar models, al proper capítol aprendrem com garantir la seva qualitat i validació abans i després del desplegament.