Validació i qualitat
- Introducció
- Quan està un model llest per desplegar?
- Testing de sistemes ML
- Qualitat del codi
- Optimització d’inferència
- Resum
Introducció
Al capítol anterior vam aprendre com preparar i desplegar models de machine learning utilitzant Docker, FastAPI, i estratègies de predicció online i batch. Ara que sabem com desplegar, és moment d’aprendre com fer-ho bé.
Desplegar un model sense validació adequada és com publicar un llibre sense revisar-lo: pot funcionar, però els errors poden ser costosos. En aquest capítol explorarem:
- Criteris objectius per decidir si un model està llest per producció
- Testing específic per a sistemes de machine learning
- Automatització de validació amb git hooks i CI/CD
- Optimització de models per a producció
Aquestes pràctiques garanteixen que només models validats i de qualitat arribin als teus usuaris.
Quan està un model llest per desplegar?
Abans de posar un model en producció, necessitem establir criteris clars que ens diguin si el model és “prou bo”. Sense aquests criteris, podem acabar desplegant models que no funcionen adequadament o, al contrari, rebutjant models perfectament vàlids.
El problema de la validació subjectiva
Sense criteris clars:
- “Aquest F1 de 0.78 és bo?” → Depèn del context
- “Podem desplegar?” → No ho sabem
- “El nou model és millor?” → Difícil de comparar
Necessitem llindars objectius definits abans d’entrenar.
Tipus de criteris de desplegament
1. Mètriques tècniques
Rendiment mesurable del model en el validation set:
| Mètrica | Descripció | Exemple de llindar |
|---|---|---|
| Accuracy | Percentatge de prediccions correctes | > 0.85 |
| F1 Score | Mitjana harmònica de precision i recall | > 0.75 |
| Precision | % de positius predits que són correctes | > 0.80 |
| Recall | % de positius reals que detectem | > 0.70 |
| ROC-AUC | Capacitat de discriminar entre classes | > 0.85 |
| MAE/RMSE | Error en regressió | < 5000 (depèn de l’escala) |
2. Mètriques de negoci
Impacte real en el negoci o usuaris:
# Exemple: cost de classificació mèdica
cost_false_negative = 10_000 # € - malaltia no detectada
cost_false_positive = 500 # € - test innecessari
# Criteri: cost total < baseline
def business_cost(y_true, y_pred):
fn = ((y_true == 1) & (y_pred == 0)).sum()
fp = ((y_true == 0) & (y_pred == 1)).sum()
return fn * cost_false_negative + fp * cost_false_positive
Criteris de negoci típics:
- Cost de falsos positius vs falsos negatius
- Impacte en conversió / vendes
- Experiència d’usuari (latència, qualitat)
- ROI (retorn d’inversió)
3. Mètriques operacionals
Requisits d’infraestructura i rendiment:
| Mètrica | Descripció | Exemple de llindar |
|---|---|---|
| Latència p50 | Temps de resposta mitjà | < 50ms |
| Latència p95 | Temps de resposta del 95% de peticions | < 100ms |
| Latència p99 | Temps de resposta del 99% de peticions | < 200ms |
| Mida del model | Espai en disc / memòria | < 500 MB |
| Throughput | Prediccions per segon | > 100 req/s |
Definir llindars: el procés
Pas 1: Establir la baseline
Abans de desplegar el primer model, definir què és “acceptable”. Opcions per la baseline:
- Predicció constant: Sempre predir la classe més freqüent
- Regla heurística: Lògica simple basada en dominis (if-then)
- Model anterior: Si n’hi ha un en producció
- Requisit de negoci: Cost màxim acceptable
Exemple:
# Baseline: predicció constant (classe majoritària)
from sklearn.dummy import DummyClassifier
baseline = DummyClassifier(strategy='most_frequent')
baseline.fit(X_train, y_train)
baseline_f1 = f1_score(y_val, baseline.predict(X_val))
# baseline_f1 = 0.45
# Criteris: millor que baseline + marge
MIN_F1 = baseline_f1 + 0.10 # = 0.55
MIN_RECALL = 0.70 # Requisit de negoci
Pas 2: Consensuar amb stakeholders
Els criteris no són només tècnics. Cal involucrar:
- Product owners (impacte de negoci)
- Usuaris finals (experiència)
- Operacions (infraestructura)
- Legal/ètica (biaixos, fairness)
Pas 3: Documentar els criteris
# config/deployment_criteria.yaml
model_criteria:
technical:
f1_score_min: 0.75
recall_min: 0.70
precision_min: 0.65
roc_auc_min: 0.80
business:
cost_per_prediction_max: 0.05 # euros
false_negative_rate_max: 0.30
operational:
latency_p95_max_ms: 100
latency_p99_max_ms: 200
model_size_max_mb: 500
baseline:
baseline_f1: 0.45
required_improvement: 0.10
Implementar la validació
# src/validate_model.py
import yaml
import joblib
from sklearn.metrics import f1_score, recall_score, precision_score
def load_criteria(criteria_path: str) -> dict:
"""Carrega criteris de desplegament."""
with open(criteria_path, 'r') as f:
return yaml.safe_load(f)
def is_deployment_ready(model, X_val, y_val, criteria: dict) -> tuple[bool, list]:
"""
Comprova si el model compleix tots els criteris.
Returns:
(approved, reasons): Bool indicant si aprovat, llista de raons
"""
y_pred = model.predict(X_val)
# Calcular mètriques
metrics = {
'f1': f1_score(y_val, y_pred, average='weighted'),
'recall': recall_score(y_val, y_pred, average='weighted'),
'precision': precision_score(y_val, y_pred, average='weighted')
}
# Validar contra criteris
issues = []
tech_criteria = criteria['model_criteria']['technical']
if metrics['f1'] < tech_criteria['f1_score_min']:
issues.append(f"F1={metrics['f1']:.3f} < {tech_criteria['f1_score_min']}")
if metrics['recall'] < tech_criteria['recall_min']:
issues.append(f"Recall={metrics['recall']:.3f} < {tech_criteria['recall_min']}")
if metrics['precision'] < tech_criteria['precision_min']:
issues.append(f"Precision={metrics['precision']:.3f} < {tech_criteria['precision_min']}")
approved = len(issues) == 0
return approved, issues
# Ús
criteria = load_criteria('config/deployment_criteria.yaml')
model = joblib.load('models/candidate_model.pkl')
approved, issues = is_deployment_ready(model, X_val, y_val, criteria)
if approved:
print("✓ Model aprovat per desplegament")
# Desplegar
else:
print("✗ Model NO aprovat:")
for issue in issues:
print(f" - {issue}")
# No desplegar, iterar
Comparar amb el model actual
Si ja tenim un model en producció, el nou model ha de ser al menys tan bo o millor:
def compare_with_current(new_metrics: dict, current_metrics: dict, tolerance: float = 0.02) -> bool:
"""
Comprova que el nou model no és pitjor que l'actual.
tolerance: Degradació acceptable (e.g., 0.02 = 2%)
"""
for metric_name in ['f1', 'recall']:
new_val = new_metrics[metric_name]
current_val = current_metrics[metric_name]
# Nou model no pot ser pitjor (amb tolerància)
if new_val < current_val - tolerance:
print(f"⚠️ {metric_name}: {new_val:.3f} < {current_val:.3f} (degradació > {tolerance})")
return False
return True
Exemple complet: Pipeline de validació
# scripts/validate_and_deploy.py
def main():
print("=== Validació del model candidat ===\n")
# 1. Carregar model i dades
candidate = joblib.load('models/candidate_v2.0.0.pkl')
X_val = pd.read_parquet('data/validation_set.parquet').drop('target', axis=1)
y_val = pd.read_parquet('data/validation_set.parquet')['target']
# 2. Validar criteris absoluts
criteria = load_criteria('config/deployment_criteria.yaml')
approved, issues = is_deployment_ready(candidate, X_val, y_val, criteria)
if not approved:
print("✗ Model rebutjat (no compleix criteris):")
for issue in issues:
print(f" {issue}")
return False
print("✓ Model compleix criteris mínims")
# 3. Comparar amb model actual (si existeix)
if os.path.exists('models/current_model_metrics.json'):
with open('models/current_model_metrics.json') as f:
current_metrics = json.load(f)
new_metrics = {
'f1': f1_score(y_val, candidate.predict(X_val)),
'recall': recall_score(y_val, candidate.predict(X_val))
}
if not compare_with_current(new_metrics, current_metrics):
print("✗ Model pitjor que l'actual")
return False
print("✓ Model millor o igual que l'actual")
# 4. Aprovar per desplegament
print("\n✓✓✓ Model APROVAT per desplegament ✓✓✓")
return True
if __name__ == '__main__':
success = main()
exit(0 if success else 1)
Resum: checklist de desplegament
Abans de desplegar un model, verifica:
- ✅ Criteris tècnics: F1, recall, precision compleixen llindars
- ✅ Criteris de negoci: Cost, ROI, impacte acceptable
- ✅ Criteris operacionals: Latència, mida, throughput acceptables
- ✅ Comparació: Nou model ≥ model actual (si n’hi ha)
- ✅ Baseline: Millor que predicció naive
- ✅ Documentat: Criteris escrits i versionats
- ✅ Consensuat: Stakeholders d’acord amb els criteris
Amb criteris clars, podem desplegar models amb confiança i justificar les decisions tècniques!
Testing de sistemes ML
Un cop hem desenvolupat el nostre servei de prediccions, necessitem assegurar-nos que funciona correctament abans de desplegar-lo. Però testejar sistemes de machine learning és diferent de testejar software tradicional.
Per què el testing ML és diferent?
En software tradicional, testem que el codi fa exactament el que esperem: suma(2, 3) ha de retornar 5, sempre. Però en ML, no podem testejar prediccions concretes perquè:
- Els models són estocàstics: L’entrenament té randomització, el model pot canviar
- L’accuracy fluctua: Un model millor podria donar diferents prediccions
- El que importa és el rendiment global, no prediccions individuals
Principi clau: Testem la infraestructura (el codi al voltant del model), no el model en si.
Què SÍ hem de testejar
1. Validació de dades
Les funcions que validen les dades d’entrada són deterministes i crítiques:
# src/validation.py
def validate_age(age: float) -> bool:
"""Comprova que l'edat és vàlida."""
return 0 <= age <= 120
def validate_income(income: float) -> bool:
"""Comprova que l'ingrés és vàlid."""
return income >= 0
# tests/test_validation.py
def test_validate_age_valid():
assert validate_age(30) == True
assert validate_age(0) == True
assert validate_age(120) == True
def test_validate_age_invalid():
assert validate_age(-5) == False
assert validate_age(150) == False
assert validate_age(float('nan')) == False
Per què: Bugs en la validació poden permetre dades invàlides que trenquen el model silenciosament.
2. Funcions de preprocessament
Les transformacions de dades han de produir resultats correctes:
# src/preprocessing.py
def normalize_value(value: float, min_val: float, max_val: float) -> float:
"""Normalitza un valor entre 0 i 1."""
return (value - min_val) / (max_val - min_val)
# tests/test_preprocessing.py
def test_normalize_value():
assert normalize_value(0, 0, 100) == 0.0
assert normalize_value(100, 0, 100) == 1.0
assert normalize_value(50, 0, 100) == 0.5
3. Càrrega i funcionament bàsic del model
No testem accuracy, però sí que el model es pot carregar i usar:
# tests/test_model.py
import joblib
def test_model_loads():
"""Verifica que el model es pot carregar."""
model = joblib.load('models/model.pkl')
assert model is not None
assert hasattr(model, 'predict')
def test_model_predicts_correct_shape():
"""Verifica que les prediccions tenen la forma correcta."""
model = joblib.load('models/model.pkl')
X = [[30, 50000, 5]] # Exemple amb 3 features
predictions = model.predict(X)
assert len(predictions) == 1
assert isinstance(predictions[0], (int, float))
4. Endpoints de l’API
Els tests d’integració verifiquen que l’API funciona correctament:
# tests/test_api.py
from fastapi.testclient import TestClient
from api import app
client = TestClient(app)
def test_health_endpoint():
"""Verifica que el health check funciona."""
response = client.get("/health")
assert response.status_code == 200
def test_predict_endpoint_valid():
"""Verifica predicció amb dades vàlides."""
response = client.post("/predict", json={
"age": 30,
"income": 50000,
"score": 5
})
assert response.status_code == 200
assert "prediction" in response.json()
def test_predict_endpoint_invalid():
"""Verifica que dades invàlides són rebutjades."""
response = client.post("/predict", json={
"age": -5, # Invàlid!
"income": 50000
})
assert response.status_code == 422 # Validation error
Què NO hem de testejar
❌ No testejar accuracy o mètriques del model:
# ❌ MAL - Tests inestables!
def test_model_accuracy():
assert model.score(X_test, y_test) > 0.95 # Això fallarà impredictiblement
Per què no: L’accuracy varia amb l’entrenament. Això és validació, no testing. Usa el validation set durant l’entrenament, no tests.
❌ No testejar prediccions específiques:
# ❌ MAL - Model pot canviar legítimament
def test_specific_prediction():
X = [[30, 50000, 5]]
assert model.predict(X)[0] == 1 # Massa específic!
Per què no: Si millorem el model, aquest test podria fallar tot i que el nou model és millor.
❌ No testejar el pipeline d’entrenament complet:
# ❌ MAL - Massa lent i complex
def test_entire_training():
data = load_data() # 5 minuts
model = train(data) # 20 minuts
assert model.score(X_test, y_test) > 0.8 # Massa temps!
Per què no: Els tests han de córrer ràpid (segons, no minuts). Testa components individuals.
Pytest: els fonaments
pytest és l’eina estàndard per testejar en Python. Funciona així:
- Crea fitxers que comencin per
test_dins d’un directoritests/ - Escriu funcions que comencin per
test_ - Usa
assertper comprovar condicions
# Estructura recomanada
project/
├── src/
│ ├── api.py
│ ├── validation.py
│ └── preprocessing.py
├── tests/
│ ├── test_api.py
│ ├── test_validation.py
│ ├── test_preprocessing.py
│ └── test_model.py
└── models/
└── model.pkl
Executar tests:
# Instal·lar pytest
pip install pytest
# Executar tots els tests
pytest tests/
# Executar amb més detall
pytest tests/ -v
# Executar només un fitxer
pytest tests/test_api.py
Fixtures: reutilitzar configuració
Si necessitem configurar coses abans dels tests (carregar un model, crear dades), usem fixtures:
# tests/conftest.py
import pytest
import joblib
@pytest.fixture
def model():
"""Carrega el model una vegada per tots els tests."""
return joblib.load('models/model.pkl')
@pytest.fixture
def sample_data():
"""Dades d'exemple per tests."""
return {
"age": 30,
"income": 50000,
"score": 5
}
# tests/test_model.py
def test_model_predicts(model, sample_data):
"""Usa les fixtures model i sample_data."""
X = [[sample_data['age'], sample_data['income'], sample_data['score']]]
prediction = model.predict(X)
assert prediction is not None
Quan executar els tests
- Abans de cada commit: Assegurar que no trenquem res
- Abans de desplegar: Verificar que tot funciona
- Automàticament (recomanat):
- Git hooks locals (pre-commit): Executa tests abans de permetre el commit
- CI/CD al servidor: Executa tests automàticament en cada push al repositori
# Workflow recomanat abans de desplegar
pytest tests/ # Tests passen?
docker-compose up --build # Build funciona?
curl http://localhost:8000/health # API respon?
docker-compose down
# Si tot OK → Desplegar
Git hooks: validació local automàtica
Fins ara hem vist com executar tests manualment abans de fer commit. Però quan treballem en projectes reals, és fàcil oblidar-se d’executar els tests, especialment sota pressió o amb canvis petits. Aquí és on entren els git hooks.
Què són els git hooks?
Els git hooks són scripts que s’executen automàticament en resposta a esdeveniments de git (com commit, push, merge, etc.). El més útil per a nosaltres és el pre-commit hook, que s’executa abans que git accepti el commit.
Flux normal (sense hook):
git commit -m "fix bug" → Commit creat ✓
Flux amb pre-commit hook:
git commit -m "fix bug"
↓
[Executa tests automàticament]
↓
Tests OK? ──┬─ SÍ → Commit creat ✓
└─ NO → Commit REFUSAT ✗
Per què són útils?
- Prevenen errors al repositori: El codi erroni no arriba mai al repo
- Feedback immediat: Saps immediatament si has trencat alguna cosa
- Consistència en l’equip: Tots els desenvolupadors apliquen les mateixes validacions
- Estalvien temps: Detectar errors localment és més ràpid que descobrir-los després del push
Quan usar git hooks?
Recomanat per:
- ✅ Projectes en equip (garantir qualitat consistent)
- ✅ Codi en producció (prevenir bugs crítics)
- ✅ Desenvolupament continu (CI/CD, que veurem a continuació)
Opcional per:
- ⚠️ Experimentació ràpida (poden alentir el workflow)
- ⚠️ Notebooks exploratòries (massa overhead)
Limitació important
Els git hooks són locals: només funcionen a la teva màquina. Si un company oblida instal·lar-los o els desactiva (amb --no-verify), el codi erroni pot arribar al repositori.
Solució: Combinar hooks locals amb validació al servidor (CI/CD). Aquesta validació al servidor, explicada a la secció “Desplegament a producció”, garanteix que el codi validat arribi a producció.
CI/CD: validació automàtica al servidor
Els git hooks locals poden prevenir que codi erroni arribi al repositori executant tests abans de cada commit. Però aquesta validació té una limitació important: depèn de cada desenvolupador. Què passa si algú oblida instal·lar els hooks? O si els desactiva amb git commit --no-verify? El codi erroni arribaria al repositori.
Per garantir que tot el codi que es desplega ha passat els tests, necessitem validació al servidor. Aquí és on entra CI/CD.
Què és CI/CD?
CI/CD són les sigles de:
- CI (Continuous Integration): Integració contínua - validar automàticament cada canvi que es puja al repositori
- CD (Continuous Deployment): Desplegament continu - desplegar automàticament els canvis que passen la validació
En el nostre context ML, CI/CD és un sistema al servidor que:
- Detecta quan fas push al repositori
- Executa automàticament tots els tests
- Construeix la imatge Docker
- Verifica que tot funciona correctament
- Només si tot és correcte, permet el desplegament
Diferència entre git hooks i CI/CD
| Aspecte | Git hooks (local) | CI/CD (servidor) |
|---|---|---|
| On s’executa | A la teva màquina | Al servidor (GitHub, GitLab, etc.) |
| Qui ho controla | Desenvolupador individual | L’organització / equip |
| Es pot saltar | Sí (--no-verify) | No - sempre s’executa |
| Objectiu | Feedback ràpid | Garantia de qualitat |
| Quan actua | Abans del commit | Després del push |
Analogia:
- Git hooks = Revisar el teu examen abans de lliurar-lo
- CI/CD = El professor revisa l’examen i només l’accepta si està correcte
Per què necessitem ambdós?
Idealment, volem validació en dues capes:
┌─────────────────────────────────────────────────────────────┐
│ DESENVOLUPADOR (Validació local) │
│ │
│ [Escriure codi] → [Executar tests locals] │
│ → [Git hook: tests] │
│ → [Commit acceptat] → [Push al repo] │
│ │
│ ✅ Avantatge: Feedback immediat (segons) │
│ ⚠️ Limitació: Es pot saltar │
└────────────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ SERVIDOR CI/CD (Validació remota) │
│ │
│ [Repo actualitzat] │
│ ↓ │
│ [Executar tots els tests] │
│ ↓ │
│ [Construir Docker image] │
│ ↓ │
│ [Verificar health checks] │
│ ↓ │
│ Tests OK? ──┬─── SÍ → [Desplegament permès] ✅ │
│ └─── NO → [Desplegament BLOQUEJAT] ❌ │
│ [Notificar l'equip] │
│ │
│ ✅ Avantatge: Impossible saltar-se, garantia total │
│ ⚠️ Limitació: Feedback més lent (minuts) │
└─────────────────────────────────────────────────────────────┘
Flux ideal:
- Local: Els hooks et donen feedback ràpid si has trencat alguna cosa
- Servidor: CI/CD garanteix que res erroni arribi a producció, encara que algú salti els hooks
Flux CI/CD conceptual per a ML
Quan fas push al repositori, el servidor CI/CD executa automàticament aquests passos:
Pas 1: Executar tests
pytest tests/ -v
mypy src/ # Type checking (opcional)
Si fallen → ❌ Procés aturat, notificació a l’equip
Pas 2: Construir imatge Docker
docker build -t ml-model:v1.2.0 .
Si falla el build → ❌ Procés aturat
Pas 3: Verificar funcionalitat bàsica
docker run -d ml-model:v1.2.0
curl http://localhost:8000/health # Health check
curl http://localhost:8000/predict -X POST -d '{"data": [...]}'
Si no respon correctament → ❌ Procés aturat
Pas 4: Desplegar (només si tots els passos anteriors passen)
docker push registry.company.com/ml-model:v1.2.0
# Actualitzar producció amb la nova imatge
El problema de la desincronització
Pregunta important: Què passa si el codi arriba al repositori però els tests fallen en CI/CD?
Resposta: Amb un sistema CI/CD ben configurat:
- El codi sí està al repositori (perquè ja has fet push)
- Però el codi NO es desplegarà a producció (perquè CI/CD ho bloqueja)
És això un problema? No, és l’objectiu! Volem que:
- Tot el codi estigui al repo (per col·laboració, historial, etc.)
- Però només el codi validat arribi a producció
Millor pràctica: Usar branch protection rules (protecció de branques):
- Només es pot fer merge a
mainsi CI/CD passa - Això garanteix que
mainsempre reflecteix el que està (o pot estar) en producció
Eines comunes de CI/CD
Hi ha moltes plataformes que ofereixen CI/CD:
- GitHub Actions: Integrat amb GitHub, fàcil de configurar
- GitLab CI/CD: Integrat amb GitLab, molt potent
- Jenkins: Plataforma open-source autohospedada, més complexa però molt flexible
- CircleCI, Travis CI: Alternatives cloud populars
Nota: Totes funcionen amb el mateix principi: detectar push → executar tasques automàtiques → bloquejar desplegament si falla.
Avantatges del CI/CD per a ML
Per a sistemes de machine learning, CI/CD ofereix beneficis únics:
- ✅ Garantia de reproducibilitat: Cada build es fa en un entorn net i controlat
- ✅ Detecció primerenca d’errors: Tests fallen abans d’arribar a producció
- ✅ Historial complet: Registre de quins canvis van causar quins problemes
- ✅ Validació de Docker: Assegurem que la imatge es pot construir i executar
- ✅ Desplegament segur: Només models validats arriben a producció
- ✅ Col·laboració: Tot l’equip veu l’estat dels tests i builds
Consell pràctic
Si estàs començant amb desplegament de models:
- Fase 1: Començar amb validació manual (executar tests abans de desplegar)
- Fase 2: Afegir git hooks locals per feedback ràpid
- Fase 3: Configurar CI/CD quan el projecte creix o treballa en equip
No cal implementar-ho tot d’un cop! La validació manual ja és millor que res, i pots anar afegint capes de protecció gradualment.
Resum: Doble capa de protecció
| Validació local (Git hooks) | Validació remota (CI/CD) |
|---|---|
| Feedback ràpid (segons) | Feedback més lent (minuts) |
| Es pot saltar | Impossible saltar |
| Detecta errors abans del push | Detecta errors abans del desplegament |
| Configurat per cada desenvolupador | Configurat centralment |
| Primera línia de defensa | Última línia de defensa |
Junts, garanteixen que el codi erroni no arribi mai a producció!
Resum del testing ML
Testa:
- ✅ Validació de dades
- ✅ Preprocessament
- ✅ Càrrega del model
- ✅ API endpoints
- ✅ Format de les prediccions
NO testis:
- ❌ Accuracy del model
- ❌ Prediccions específiques
- ❌ Pipeline d’entrenament complet
Recorda: Els tests asseguren que la infraestructura funciona. La qualitat del model es valida durant l’entrenament amb el validation set.
Qualitat del codi
A més de testing, hi ha altres pràctiques que milloren la qualitat del codi en sistemes ML. Una de les més importants és l’ús de type hints.
Type hints en producció
Els type hints (anotacions de tipus) són especialment valuosos en sistemes ML de producció:
Per què són importants:
- Detecció d’errors: Eines com
mypydetecten errors abans d’executar el codi - Documentació viva: Els tipus expliquen què espera cada funció sense haver de llegir el codi
- IDE autocomplete: Millora la productivitat amb autocompletació intel·ligent
- Refactoring segur: Canvis amb menys risc d’introduir errors
Quan usar-los:
✅ Sempre en funcions públiques (APIs, endpoints, interfícies) ✅ Sempre en funcions de validació i preprocessament de dades ✅ Recomanat en funcions complexes o amb molts paràmetres ⚠️ Opcional en scripts d’experimentació o notebooks
Sintaxi moderna (Python 3.10+):
# ✅ Modern (Python 3.10+) - recomanat
def predict(features: list[float]) -> dict[str, float]:
...
def load_data(path: str) -> pd.DataFrame:
...
# ❌ Antic (Python < 3.10) - evitar
from typing import List, Dict
def predict(features: List[float]) -> Dict[str, float]:
...
Nivell de detall recomanat:
# Mínim acceptable (funcions públiques)
def preprocess(data: pd.DataFrame) -> pd.DataFrame:
...
# Recomanat (més específic)
def preprocess(data: pd.DataFrame, columns: list[str]) -> pd.DataFrame:
...
# Exhaustiu (només si és necessari per evitar errors)
from typing import TypedDict
class ModelMetrics(TypedDict):
f1: float
recall: float
precision: float
def evaluate_model(model, X: np.ndarray, y: np.ndarray) -> ModelMetrics:
...
Validació amb mypy:
# Instal·lar mypy
pip install mypy
# Validar el teu codi
mypy src/
# Exemple d'error que detecta:
# error: Argument 1 to "predict" has incompatible type "str"; expected "list[float]"
Configuració recomanada per projectes ML:
Afegeix aquesta configuració a pyproject.toml (mypy la llegeix automàticament):
# pyproject.toml
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true # Necessari per sklearn, pandas, etc.
L’opció ignore_missing_imports = true és important perquè moltes llibreries de ML (scikit-learn, pandas, numpy) no tenen stubs de tipus complets, i sense aquesta opció mypy generaria molts errors falsos.
Consell pràctic: Comença afegint type hints a les funcions que causen més bugs o confusió en el teu projecte. No cal fer-ho tot d’un cop!
Optimització d’inferència
Quan passem d’un notebook a un sistema en producció, un dels reptes principals és la velocitat d’inferència: quant triga el model a respondre. Les tècniques més importants per reduir latència i consum de recursos són:
Reduir la mida i el cost del model
Aquestes tècniques fan el model més lleuger sense alterar gaire la seva qualitat:
-
Quantització: Fer els números més petits.
- Consisteix a representar els pesos del model amb menys bits.
- Això redueix memòria, temps de càlcul i sovint té un impacte molt petit en la precisió.
- Exemples:
float32 → float16: la meitat d’espai, gairebé mateixa qualitatfloat32 → int8: un 75% menys d’espai, rendiment molt millor
- Útil quan la latència és crítica (mòbils, edge, real-time).
-
Poda (Pruning): Treure neurones que fan poca feina.
- Elimina parts del model que aporten molt poc.
- Imaginem una xarxa com un arbre: la poda treu branques inútils.
- Menys pesos → càlcul més ràpid.
- L’impacte en precisió sol ser petit si es fa progressivament.
-
Destil·lació (Knowledge Distillation): Un model gran ensenya a un de petit com comportar-se.
- El model “professor” genera sortides suaus.
- Un model “estudiant” més petit aprèn a imitar-lo.
- Permet mantenir gairebé el mateix rendiment amb un model molt més lleuger.
-
Factorització de baix rang (Low-rank): Descompondre matrius grans en peces més petites.
- Divideix matrius de pesos en dues o tres matrius petites.
- Mantenim quasi el mateix comportament amb menys càlcul.
Optimitzar el flux intern del model
Millorem com el model “fa les coses per dins”:
-
Fusió d’operacions (Operator Fusion): Fer diverses operacions en una sola passa.
- Evita moviments de memòria i passos innecessaris.
- Per exemple, combinar Convolució + BatchNorm + ReLU.
- Runtimes com ONNX Runtime, TensorRT o OpenVINO ho fan automàticament.
-
Optimització del graf (Graph Optimization): Netejar i reordenar les operacions.
- Inclou:
- treure nodes inútils
- simplificar expressions
- refactoritzar el flux d’operacions
- S’activa sovint en exportar a ONNX.
- Inclou:
-
Compilació específica per hardware: Generar codi optimitzat per la màquina on s’executarà.
- Exemples:
- TorchScript (PyTorch)
- XLA (TensorFlow)
- TVM (compilador ML open source)
- TensorRT (NVIDIA, molt ràpid en GPU)
- Exemples:
Escollir millors models i millor arquitectura
A vegades la millor optimització és utilitzar un model més eficient:
-
Models dissenyats per ser lleugers: Arquitectures pensades per ser ràpides.
- Exemples coneguts:
- MobileNet (visió)
- EfficientNet-Lite (visió)
- DistilBERT / TinyBERT (text)
- Whisper-Tiny (àudio)
- Exemples coneguts:
-
Selecció de features: Menys característiques = menys feina.
- El pre-processament pot ser la part més lenta del pipeline.
- Reduir features també redueix latència.
Estratègies de serving i producció
Maneres de millorar el rendiment al desplegar el model:
-
Batching dinàmic: Agrupar peticions en mini-lots per aprofitar millor el hardware.
- Permet fer 10 inferències en una sola passa en comptes de 10 passes separades.
- Eines que ho ofereixen:
- NVIDIA Triton
- Ray Serve
- KServe
- BentoML
-
Paral·lelització i multithreading: Aprofitar diversos nuclis de CPU.
- Podeu servir múltiples peticions en paral·lel amb diversos workers o processos.
-
Caching d’inferències: Si una petició es repeteix, no cal recalcular.
- Desa resultats en memòria (Redis/Memcached).
- Per a moltes aplicacions, és una de les optimitzacions més potents.
Millorar l’aprofitament del hardware
Per millorar l’aprofitament del hardware:
-
GPU acceleration: Les GPUs estan fetes per multiplicar matrius molt ràpid.
- Ideal per models grans i procesaments paral·lels.
-
Instruccions vectorials de CPU: Fer més operacions per instrucció.
- Si el servidor suporta AVX/AVX2/AVX-512, el model anirà força més ràpid.
-
Hardware especialitzat: Dispositius creats només per fer inferència.
- Google TPU
- AWS Inferentia
- Intel Movidius (edge)
Bones pràctiques quan uses Docker en producció
Bones pràctiques quan uses Docker en producció:
- Utilitzar imatges minimalistes: Menys capa → arrencada més ràpida → menys vulnerabilitats.
- Precarregar el model en arrencar: Evita la latència de carregar-lo quan arriba la primera petició.
- Evitar dependències innecessàries: Menys mida, menys riscos, menys temps de build.
- Monitoritzar sempre: Prometheus + Grafana per controlar:
- latència
- throughput
- càrrega CPU/GPU
- memòria i ús de model
Resum final
| Acció | Tècniques principals |
|---|---|
| Reduir el model | quantització, poda, destil·lació, low-rank |
| Optimitzar el model | fusió d’operacions, optimització del gràfic, compilació |
| Escollir arquitectura | models lleugers, menys features |
| Millorar el serving | batching, paral·lelització, caching |
| Aprofitar hardware | GPU, AVX, dispositius especialitzats |
| Desplegament | imatges petites, precarrega, monitorització |
Resum
En aquest capítol hem après a garantir la qualitat dels nostres models:
- Definir criteris objectius (tècnics, de negoci, operacionals) per aprovar desplegaments
- Testejar la infraestructura ML sense testejar el model mateix
- Usar git hooks per validació local automàtica abans de commits
- Implementar CI/CD per validació al servidor abans de desplegaments
- Aplicar tècniques d’optimització quan sigui necessari
Amb aquestes pràctiques, tenim control complet sobre la qualitat dels models desplegats. Al proper capítol veurem com monitoritzar models en producció i detectar quan les dades canvien (drift).