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

Què és MLOps?

MLOps (Machine Learning Operations) és la disciplina que combina Machine Learning, DevOps i enginyeria de dades per automatitzar i millorar tot el cicle de vida dels models de ML en producció.

Mentre que el desenvolupament de models se centra en entrenar algoritmes precisos, MLOps se centra en:

  • Desplegar models de manera fiable i reproducible
  • Monitoritzar el comportament dels models en producció
  • Automatitzar el procés d’actualització i reentrenament
  • Garantir la qualitat amb testing i validació contínua

El cicle de vida MLOps

┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Develop │ → │ Deploy │ → │ Monitor │ → │ Update │ │ & Train │ │ & Serve │ │ & Alert │ │ & Retrain │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ Cap. 1 Cap. 1-2 Cap. 3 Cap. 4

En aquest mòdul explorarem els quatre pilars fonamentals de MLOps:

  1. Desplegament de models (aquest capítol): Docker, APIs, versionat
  2. Qualitat i testing: Validació, CI/CD, estratègies de desplegament
  3. Monitorització de drift: Detecció de degradació, alertes
  4. Aprenentatge continu: Reentrenament, A/B testing, rollback

Comencem pel primer pilar: el desplegament. 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, les dades han de passar per un pipeline que les prepari per a l’entorn de producció. En cursos anteriors heu après a dividir dades en splits (60/20/20) i preprocessar-les. Aquí ens centrem en dos aspectes específics del desplegament: el format de dades i la connexió conceptual entre test set i producció.

Per què Parquet i no CSV?

Quan treballem en desenvolupament, sovint usem CSV perquè és llegible i fàcil de compartir. Però per a producció, Parquet és el format recomanat.

Problemes del CSV:

  • Pèrdua de tipus: Tot es guarda com a text. Un enter 42 i un string "42" són idèntics en CSV. Quan el carregues, pandas ha d’endevinar els tipus.
  • Ineficient: Sense compressió, ocupa molt espai.
  • Lent: Cal llegir tot el fitxer per accedir a qualsevol dada.

Avantatges de Parquet:

  • Preserva tipus: Enters, floats, strings, dates… es guarden amb el seu tipus original. No hi ha ambigüitat.
  • Comprimit: Típicament 2-10x més petit que CSV.
  • Ràpid: Format columnar, permet llegir només les columnes necessàries.
  • Estàndard de la indústria: Compatible amb Spark, BigQuery, Snowflake, etc.

Conversió de CSV a Parquet:

import pandas as pd # Llegir CSV df = pd.read_csv('data/raw_data.csv') # Guardar com Parquet df.to_parquet('data/processed_data.parquet', index=False) # Llegir Parquet (més ràpid, preserva tipus) df = pd.read_parquet('data/processed_data.parquet')

Comparativa de mida i velocitat (exemple amb 100,000 registres):

FormatMidaTemps lectura
CSV15 MB1.2s
Parquet3 MB0.1s

Per a projectes de ML en producció, sempre guardeu els splits processats en Parquet.

El test set com a simulació de producció

Quan dividim les dades en 60/20/20, el tercer split té un nom que pot confondre: test set. Però des de la perspectiva de desplegament, és més precís pensar-hi com a production set — dades que simulen el que el model veurà en producció.

Dataset complet │ ├── Training (60%) → Entrenar el model │ ├── Validation (20%) → Ajustar hiperparàmetres │ └── Production (20%) → Simula dades reals de producció

Per què aquesta perspectiva és important?

El test/production set té dues funcions crítiques per al desplegament:

  1. Avaluació final: Abans de desplegar, avaluem el model en dades que mai ha vist. Això ens diu com funcionarà amb usuaris reals.

  2. Baseline per detectar drift: Després del desplegament, necessitem una referència per saber si les dades de producció han canviat. El production set és aquesta referència.

Abans del desplegament: model.evaluate(production_set) → accuracy 0.87 ✓ Llest per desplegar Després del desplegament (setmanes més tard): dades_reals = get_production_data() compare_distribution(production_set, dades_reals) → drift detectat! ⚠️

Per això mai s’ha de “contaminar” el production set durant el desenvolupament. Si l’uses per ajustar hiperparàmetres o prendre decisions, deixa de ser una simulació fiable de producció.

Pipeline de dades per producció

Un pipeline típic segueix aquest flux:

CSV original → Validar → Dividir (60/20/20) → Preprocessar → Guardar Parquet

Exemple d’estructura de fitxers després del pipeline:

data/ ├── heartdisease.csv # Dades originals (CSV) ├── training_set.parquet # 60% - per entrenar ├── validation_set.parquet # 20% - per validar └── production_set.parquet # 20% - simula producció

⚠️ Recordatori: El preprocessador (scaler, encoder) s’entrena només amb training data i s’aplica als altres splits. Això ja ho heu vist al curs de ML, però és crític per a producció: el preprocessador s’ha de guardar i usar exactament igual quan arribin dades reals.

Amb les dades preparades en format Parquet i el production set reservat, estem llestos per entrenar el model i desplegar-lo.

Serialització de models

Quan entrenem un model de machine learning, aquest existeix només a la memòria RAM del nostre programa. Si tanquem Python, el model desapareix. Per poder desplegar un model, necessitem serialitzar-lo: convertir l’objecte Python a un format que es pugui guardar a disc i recuperar més tard.

Què és la serialització?

La serialització (serialization) és el procés de convertir un objecte en memòria a una seqüència de bytes que es pot:

  • Guardar a disc com a fitxer
  • Transmetre per xarxa
  • Carregar més tard en un altre procés o màquina

El procés invers s’anomena deserialització: llegir els bytes i reconstruir l’objecte original en memòria.

┌─────────────────┐ serialitzar ┌─────────────────┐ │ Model entrenat │ ─────────────────► │ Fitxer a disc │ │ (memòria RAM) │ │ (.pkl, .pt, │ │ │ ◄───────────────── │ .json...) │ └─────────────────┘ deserialitzar └─────────────────┘

Per què és essencial per al desplegament?

Sense serialització, hauríem de reentrenar el model cada cop que volem fer prediccions. Això és:

  • Lent: L’entrenament pot trigar hores o dies
  • Costós: Requereix dades i recursos computacionals
  • Impràctic: En producció necessitem respostes en mil·lisegons

Amb serialització, el flux és:

  1. Entrenar el model una vegada (pot trigar hores)
  2. Serialitzar el model a un fitxer
  3. Desplegar el fitxer al servidor de producció
  4. Deserialitzar el model a l’inici de l’aplicació
  5. Predir instantàniament (el model ja està entrenat)

Eines de serialització per llibreria

Cada llibreria de ML té les seves eines de serialització recomanades. Vegem les més comunes:

Scikit-learn: joblib i pickle

Per a models de scikit-learn, joblib és l’opció recomanada. Tot i estar construït sobre pickle, afegeix optimitzacions per a arrays de NumPy (compressió, memory-mapping) que el fan més eficient per a models sklearn.

⚠️ Seguretat: Com que joblib usa pickle internament, mai carreguis fitxers .pkl de fonts no fiables. Pickle pot executar codi arbitrari durant la deserialització.

import joblib from sklearn.ensemble import RandomForestClassifier # Entrenar el model model = RandomForestClassifier(n_estimators=100) model.fit(X_train, y_train) # Serialitzar (guardar) joblib.dump(model, 'models/random_forest.pkl') # Deserialitzar (carregar) model = joblib.load('models/random_forest.pkl') prediction = model.predict(X_new)

pickle (de la llibreria estàndard de Python) també funciona, però és menys eficient per objectes grans:

import pickle # Guardar with open('models/model.pkl', 'wb') as f: pickle.dump(model, f) # Carregar with open('models/model.pkl', 'rb') as f: model = pickle.load(f)

Recomanació: Usa joblib per a sklearn. És més ràpid i genera fitxers més petits.

PyTorch: torch.save i TorchScript

PyTorch ofereix diverses opcions de serialització amb diferents avantatges. Les presentem de més simple a més avançada:

Opció 1: Guardar el model complet (més simple)
import torch import torch.nn as nn # Definir i entrenar el model class NeuralNetwork(nn.Module): def __init__(self): super().__init__() self.layers = nn.Sequential( nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1) ) def forward(self, x): return self.layers(x) model = NeuralNetwork() # ... entrenar ... # Guardar model complet (arquitectura + pesos) torch.save(model, 'models/model_complete.pt') # Carregar (no cal definir la classe) model = torch.load('models/model_complete.pt') model.eval() # Mode avaluació (desactiva dropout, etc.)

Per a principiants: Aquesta opció és la més similar a joblib de sklearn. Guarda tot el model en un sol fitxer i el carrega directament. És ideal per començar.

Opció 2: Guardar només els pesos amb state_dict (recomanat per PyTorch)

Aquesta és la pràctica recomanada oficialment per PyTorch perquè separa l’arquitectura (codi) dels pesos apresos (dades):

# Guardar només els pesos (state_dict) torch.save(model.state_dict(), 'models/model_weights.pt') # Carregar: cal recrear l'arquitectura primer! model = NeuralNetwork() # Crear instància buida model.load_state_dict(torch.load('models/model_weights.pt')) model.eval()
Opció 3: TorchScript (recomanat per producció)

TorchScript compila el model a un format optimitzat i independent de Python:

# Convertir a TorchScript scripted_model = torch.jit.script(model) # o amb tracing (per models sense control flow dinàmic): # scripted_model = torch.jit.trace(model, example_input) # Guardar scripted_model.save('models/model_scripted.pt') # Carregar (pot executar-se sense Python!) loaded_model = torch.jit.load('models/model_scripted.pt')
Opció 4: SafeTensors (format modern i segur)

SafeTensors és un format modern dissenyat per ser segur i eficient. A diferència de pickle, no permet execució de codi arbitrari durant la càrrega:

from safetensors.torch import save_file, load_file # Guardar save_file(model.state_dict(), 'models/model.safetensors') # Carregar state_dict = load_file('models/model.safetensors') model = NeuralNetwork() model.load_state_dict(state_dict) model.eval()
MètodeAvantatgesDesavantatges
Model completSimple, ideal per aprendreMenys portable, risc de seguretat amb pickle
state_dictFlexible, pràctica recomanada per PyTorchCal tenir la classe definida
TorchScriptOptimitzat, portable, pot executar-se en C++Algunes operacions no suportades
SafeTensorsSegur, ràpid, usat per Hugging FaceCal instal·lar safetensors

XGBoost: format natiu

XGBoost té el seu propi format de serialització, que és més eficient i segur que pickle:

import xgboost as xgb # Entrenar el model model = xgb.XGBClassifier(n_estimators=100) model.fit(X_train, y_train) # Guardar en format UBJSON (per defecte des de XGBoost 2.1) model.save_model('models/xgboost_model.ubj') # O en format JSON (llegible per humans) model.save_model('models/xgboost_model.json') # Carregar model = xgb.XGBClassifier() model.load_model('models/xgboost_model.ubj')

Nota: El format .ubj (Universal Binary JSON) és el format per defecte des de XGBoost 2.1, més compacte i sense pèrdua de precisió en nombres decimals. El format .json és llegible per humans, útil per inspeccionar l’estructura del model.

Format universal: ONNX

ONNX (Open Neural Network Exchange) és un format obert que permet interoperabilitat entre diferents frameworks. Un model exportat a ONNX es pot executar amb qualsevol runtime compatible, independentment de si es va entrenar amb PyTorch, TensorFlow, o sklearn.

Avantatges d’ONNX:

  • Portable entre llenguatges (Python, C++, Java, JavaScript…)
  • Optimitzacions de runtime (ONNX Runtime és molt ràpid)
  • Desplegament en dispositius edge (mòbils, IoT)

Resum de formats recomanats

LlibreriaFormat recomanatExtensióNotes
scikit-learnjoblib.pklInclou preprocessadors
PyTorchtorch.save (model complet).ptSafeTensors (.safetensors) per seguretat
XGBoostNatiu UBJSON.ubj.json per inspecció manual
Cross-platformONNX.onnxPer producció optimitzada

Bones pràctiques de serialització

  1. Serialitza sempre els preprocessadors: Scalers, encoders, etc. han d’anar amb el model
  2. Documenta les versions: La compatibilitat pot trencar-se entre versions de llibreries
  3. Usa formats natius quan puguis: Són més segurs que pickle genèric
  4. Considera ONNX per producció: Especialment si necessites rendiment o portabilitat
  5. Testa la càrrega: Sempre verifica que el model carregat fa les mateixes prediccions

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 from pydantic import BaseModel import joblib import numpy as np model = joblib.load("model/model.pkl") app = FastAPI(title="API de Predicció") class InputData(BaseModel): feature_1: float feature_2: float feature_3: float @app.post("/predict") def predict(data: InputData): features = np.array([[data.feature_1, data.feature_2, data.feature_3]]) result = model.predict(features)[0] return {"prediction": float(result), "model_version": "1.0.0"} @app.get("/health") def health(): return {"status": "healthy"}

L’estructura bàsica és: carregar model a l’inici, definir schema d’entrada amb Pydantic, exposar endpoint de predicció i health check.

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

Cada model s’ha de guardar amb un fitxer de metadata que inclogui la versió, mètriques i features:

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

Per a entorns més complexos, la metadata pot incloure també informació del dataset (hash, nombre de registres), hiperparàmetres, versions de llibreries, i informació de desplegament.

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

Incloure versió a l’API

Incloure la versió del model a les respostes de l’API permet traçar quina versió va fer cada predicció:

{ "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!

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"]

Per què requirements.txt?

El fitxer requirements.txt és fonamental per al desplegament de models. Sense ell, les dependències del projecte quedarien indefinides i el model podria fallar en producció per incompatibilitats de versions.

Avantatges clau:

  • Reproducibilitat: Garanteix que tothom (i cada servidor) instal·la exactament les mateixes versions de les llibreries. Sense versions fixades, pip install scikit-learn podria instal·lar versions diferents avui i demà.

  • Aïllament d’errors: Si una nova versió d’una llibreria trenca el codi, pots identificar-ho ràpidament comparant versions.

  • Eficiència amb Docker: Docker guarda cada instrucció del Dockerfile en una capa (layer) que es reutilitza si no ha canviat. Copiar requirements.txt abans del codi permet que Docker reutilitzi la capa de dependències mentre només el codi canvia.

Com crear i usar requirements.txt:

# Guardar les dependències actuals del projecte pip freeze > requirements.txt # Instal·lar totes les dependències des del fitxer pip install -r requirements.txt

pip freeze genera una llista de totes les llibreries instal·lades amb les seves versions exactes. Això és útil per capturar l’estat actual de l’entorn, però pot incloure dependències transitives que no necessites explícitament.

Bones pràctiques:

# Especificar rangs de versions (recomanat) fastapi>=0.109.0,<1.0.0 scikit-learn>=1.4.0,<2.0.0 # O versions exactes (màxima reproducibilitat) fastapi==0.109.2 scikit-learn==1.4.0

Usar rangs (>=1.4.0,<2.0.0) permet actualitzacions de seguretat dins d’una versió major, mentre que versions exactes (==1.4.0) garanteixen reproducibilitat total però requereixen més manteniment.

Com aplicar actualitzacions de seguretat:

Els rangs de versions no s’actualitzen automàticament. Per obtenir les versions més noves dins del rang permès:

# Actualitzar totes les dependències al màxim permès pels rangs pip install --upgrade -r requirements.txt

Amb pip-tools, pots regenerar el fitxer amb les versions més recents:

# Regenerar requirements.txt amb les últimes versions permeses pip-compile --upgrade requirements.in pip install -r requirements.txt

Sense --upgrade, pip-compile manté les versions existents si encara compleixen els rangs. Amb --upgrade, ignora el requirements.txt actual i resol totes les dependències de nou per obtenir les últimes versions permeses.

Consells segons el context:

  • Per a projectes simples o en desenvolupament: Crea el fitxer manualment amb només les dependències directes del projecte. Pip resoldrà automàticament les dependències transitives. Això fa el fitxer més net i fàcil de mantenir.

  • Per a producció o màxima reproducibilitat: Usa pip-tools per combinar mantenibilitat amb control total. Crea un fitxer requirements.in amb les dependències directes, i genera requirements.txt amb pip-compile:

# requirements.in (mantingut manualment) fastapi>=0.109.0,<1.0.0 scikit-learn>=1.4.0,<2.0.0 # Generar requirements.txt amb totes les versions fixades pip-compile requirements.in

Això genera un requirements.txt amb totes les dependències (directes i transitives) amb versions exactes, garantint que l’entorn sigui idèntic en cada instal·lació.

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: el contracte de desplegament

Els health checks són un estàndard universal en sistemes de producció. No són només una bona pràctica, sinó un contracte de desplegament que permet a la infraestructura saber si el servei està operatiu.

Per què són fonamentals?

  • Docker / Docker Swarm: Marca contenidors com “unhealthy” i pot reiniciar-los automàticament
  • Kubernetes: Usa readiness probes (servei llest?) i liveness probes (servei encara viu?)
  • Load balancers (AWS ELB, NGINX): Només envia tràfic a instàncies que responen health checks
  • CI/CD pipelines: Verifica que el desplegament ha tingut èxit
  • Monitoring systems (Prometheus, Datadog): Alerten quan el servei no respon

Configuració a Docker Compose:

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() }

Noms d’endpoint comuns:

Diferents organitzacions usen diferents convencions:

  • /health - El més comú
  • /healthz - Estil Kubernetes
  • /status - Alternatiu
  • /api/health - Si l’API està sota /api

L’important és que:

  1. Retorni HTTP 200 quan el servei està operatiu
  2. Sigui ràpid (< 100ms idealment)
  3. No requereixi autenticació (ha de ser accessible per l’orquestrador)
  4. Verifiqui dependències crítiques (model carregat, base de dades accessible si és crítica)

Readiness vs Liveness (concepte Kubernetes):

  • Liveness: “Estic viu?” - Si falla, reinicia el contenidor
  • Readiness: “Estic llest per rebre tràfic?” - Si falla, para d’enviar tràfic però no reinicia

En sistemes simples, un sol endpoint /health fa ambdues funcions. En sistemes complexos, poden ser endpoints diferents (/health/live i /health/ready).

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

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 a git!)

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

Múltiples configuracions per a diferents entorns

En projectes reals, sovint necessitem configuracions diferents per a desenvolupament, testing i producció. En lloc de tenir un sol docker-compose.yml amb moltes variables d’entorn, podem usar múltiples fitxers de composició.

Per què múltiples fitxers?

Diferents entorns tenen diferents necessitats:

  • Desenvolupament: Hot reload, logs verbosos, eines de debug
  • Testing/Validació: Configuració mínima, execució de tests, fast startup
  • Producció: Optimitzat per rendiment, logs estructurats, restart policies

Estratègia de tres fitxers

project/ ├── docker-compose.yml # Desenvolupament (hot reload, debug) ├── docker-compose.validate.yml # Tests (pytest) └── docker-compose.deploy.yml # Producció (workers, healthcheck, restart)

Cada fitxer configura el mateix servei amb opcions diferents per a cada context. S’executen amb:

docker-compose up # Desenvolupament docker-compose -f docker-compose.validate.yml up # Tests docker-compose -f docker-compose.deploy.yml up -d # Producció

Quan usar aquest patró?

Usar múltiples fitxers quan:

  • Els entorns tenen necessitats molt diferents
  • Vols configuracions clares i explícites
  • Treballes en equip i cal separar responsabilitats
  • Tens un pipeline de CI/CD amb múltiples etapes

Usar un sol fitxer quan:

  • El projecte és simple
  • Les diferències es poden gestionar amb variables d’entorn
  • Només tens un o dos entorns

Aquest patró és especialment útil en pipelines de CI/CD, on cada etapa (validació, staging, producció) necessita una configuració diferent.

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
  • Triar entre predicció online (temps real) i batch (lots programats)
  • Servir prediccions amb FastAPI, incloent versionat de models
  • Empaquetar models amb Docker per garantir reproducibilitat entre entorns

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