Desplegament de models
- Què és MLOps?
- Per què el desplegament és difícil?
- Preparació de dades per producció
- Serialització de models
- Predicció online vs. predicció batch
- Servir models amb FastAPI
- Contenidors amb Docker
- Resum
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:
- Desplegament de models (aquest capítol): Docker, APIs, versionat
- Qualitat i testing: Validació, CI/CD, estratègies de desplegament
- Monitorització de drift: Detecció de degradació, alertes
- 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
42i 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):
| Format | Mida | Temps lectura |
|---|---|---|
| CSV | 15 MB | 1.2s |
| Parquet | 3 MB | 0.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:
-
Avaluació final: Abans de desplegar, avaluem el model en dades que mai ha vist. Això ens diu com funcionarà amb usuaris reals.
-
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:
- Entrenar el model una vegada (pot trigar hores)
- Serialitzar el model a un fitxer
- Desplegar el fitxer al servidor de producció
- Deserialitzar el model a l’inici de l’aplicació
- 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
joblibde 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ètode | Avantatges | Desavantatges |
|---|---|---|
| Model complet | Simple, ideal per aprendre | Menys portable, risc de seguretat amb pickle |
state_dict | Flexible, pràctica recomanada per PyTorch | Cal tenir la classe definida |
| TorchScript | Optimitzat, portable, pot executar-se en C++ | Algunes operacions no suportades |
| SafeTensors | Segur, ràpid, usat per Hugging Face | Cal 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
| Llibreria | Format recomanat | Extensió | Notes |
|---|---|---|---|
| scikit-learn | joblib | .pkl | Inclou preprocessadors |
| PyTorch | torch.save (model complet) | .pt | SafeTensors (.safetensors) per seguretat |
| XGBoost | Natiu UBJSON | .ubj | .json per inspecció manual |
| Cross-platform | ONNX | .onnx | Per producció optimitzada |
Bones pràctiques de serialització
- Serialitza sempre els preprocessadors: Scalers, encoders, etc. han d’anar amb el model
- Documenta les versions: La compatibilitat pot trencar-se entre versions de llibreries
- Usa formats natius quan puguis: Són més segurs que pickle genèric
- Considera ONNX per producció: Especialment si necessites rendiment o portabilitat
- 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
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:
- Dades acumulades: Es recullen i acumulen les dades que s’han de processar.
- Script batch: S’executa un script o procés per iniciar el lot de prediccions.
- Carregar model: El model de machine learning es carrega a la memòria.
- Processar tot: Es processen totes les dades d’una sola vegada, aplicant el model a cada cas.
- 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:
Comparativa
| Aspecte | Online | Batch |
|---|---|---|
| Latència | Mil·lisegons | Minuts/hores |
| Volum per execució | Baix | Alt |
| Recursos | Sempre actius | Sota demanda |
| Complexitat | Més alta | Més baixa |
| Freshness de prediccions | Temps real | Periòdic |
| On es guarden resultats | Retorn immediat | Base 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 modelv1.0.1 → v1.1.0: Afegida feature “region”, mateix RandomForestv1.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"
}
Symlink per “model actual”
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.pklper 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-learnpodria 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.txtabans 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-toolsper combinar mantenibilitat amb control total. Crea un fitxerrequirements.inamb les dependències directes, i generarequirements.txtambpip-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
El procés típic és:
- Escrivim el Dockerfile que descriu com construir la imatge
- Construïm la imatge localment amb
docker build - Provem el contenidor localment amb
docker run - Pugem la imatge a un registre (pot ser un registre privat al nostre servidor)
- 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:
- Retorni HTTP 200 quan el servei està operatiu
- Sigui ràpid (< 100ms idealment)
- No requereixi autenticació (ha de ser accessible per l’orquestrador)
- 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
- Noms de serveis clars:
model-api,postgres, noservice1,app - Health checks sempre: Per detectar problemes ràpidament
- Volums per dades persistents: Models, logs, bases de dades
- Variables d’entorn: Mai hardcoded secrets o configuracions
- restart: unless-stopped: Per serveis que han d’estar sempre actius
- 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.