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

Avaluació de models

Aquest document és de consulta. Cada idea s’introdueix al document del model on es necessita per primer cop, i aquí es recull tota junta, amb més detall i amb els gràfics, per tornar-hi quan calgui.

IdeaS’introdueix a
Separar test i validació, RMSE, MAE, R²Aprenentatge supervisat
Matriu de confusió, precisió, recall, llindarClassificació
Biaix i variànciaAprenentatge supervisat
Corbes d’aprenentatge, early stoppingXarxes neuronals
Validació creuada, cerca d’hiperparàmetresArbres de decisió

Per a les decisions d’un projecte complet (quin model provar primer, quan calen més dades), consulta la Guia pràctica.

Separar les dades

Un model sempre sembla bo sobre les dades amb què l’hem entrenat. La pregunta que importa és com funcionarà amb dades noves, i per respondre-la cal guardar dades que el model no hagi vist mai.

Entrenament, validació i test

Dividim les dades en tres parts:

ConjuntProporció típicaPer a què serveix
Entrenament (train)60%Ajustar els paràmetres del model (pesos, arbres…)
Validació (validation, dev)20%Prendre decisions: grau del polinomi, profunditat de l’arbre, valor de λ, llindar
Test20%Mesurar el resultat final, una sola vegada

Per què no n’hi ha prou amb entrenament i test? Imagina que proves deu graus de polinomi i et quedes amb el que dona menys error al test. Has fet servir el test per triar, i per tant l’error del test ja no és una estimació honesta: és optimista. El conjunt de validació existeix per prendre aquestes decisions sense tocar el test.

Regla d’or: el test no s’utilitza per prendre cap decisió sobre el model. Només es mira al final, amb el model ja triat.

Fuga de dades

Hi ha fuga de dades (data leakage) quan informació del conjunt de validació o test s’escola a l’entrenament. El resultat és un model que sembla molt bo i que falla en producció.

L’exemple més habitual és l’escalat. Si calculem la mitjana i la desviació estàndard amb totes les dades i després les dividim, l’entrenament ja “ha vist” el test. L’escalador s’ha d’ajustar només amb les dades d’entrenament. Amb scikit-learn, la manera segura és posar l’escalador i el model dins d’un Pipeline, que fa aquest ordre per tu:

from sklearn.datasets import load_diabetes
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

X, y = load_diabetes(return_X_y=True)  # 442 pacients, 10 mesures cadascun
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = make_pipeline(StandardScaler(), LinearRegression())
model.fit(X_train, y_train)          # l'escalador només veu X_train
print(model.score(X_test, y_test))   # R² sobre el test: 0.45

Validació creuada (K-fold)

Amb una sola divisió, el resultat depèn de la sort: si el conjunt de validació ha quedat amb exemples més fàcils que la mitjana, l’error de validació ens enganya.

La validació creuada de K particions (K-fold cross-validation) fa diverses divisions i en fa la mitjana:

  1. Dividim les dades en K parts iguals (per exemple, K = 5).
  2. Entrenem K vegades. Cada vegada una part diferent fa de validació i les altres K-1 d’entrenament.
  3. L’error de validació final és la mitjana dels K errors.

Cada exemple serveix tant per entrenar com per validar, i l’estimació és més estable. El preu és entrenar K vegades. Per això s’utilitza sobretot amb datasets petits o mitjans i models ràpids. Amb xarxes neuronals grans és habitual quedar-se amb una sola divisió.

Valors típics: K = 5 o K = 10. En classificació amb classes desbalancejades, StratifiedKFold manté la proporció de classes a cada partició.

Amb les dades de diabetis de l’apartat anterior:

from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import LinearRegression

cv = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(LinearRegression(), X_train, y_train,
                         cv=cv, scoring='neg_root_mean_squared_error')
rmse = -scores  # scikit-learn retorna els errors amb signe negatiu ("més alt és millor")
print(rmse.round(1))                                # [53.4 56.8 53.  54.5 59.3]
print(f"RMSE mitjà: {rmse.mean():.1f} ± {rmse.std():.1f}")  # RMSE mitjà: 55.4 ± 2.4

Cada partició dona un error una mica diferent, entre 53 i 59. La mitjana és una estimació més fiable que qualsevol d’elles per separat, i la desviació (± 2.4) diu quant pot variar segons quines dades caiguin a la validació.

K-fold amb una xarxa neuronal (PyTorch)
# Suposa definits: dataset (un TensorDataset amb X i y), la classe MLP,
# in_features, num_classes, batch_size, lr i epochs
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Subset
from sklearn.model_selection import KFold
import numpy as np

k = 5
seed = 42
kf = KFold(n_splits=k, shuffle=True, random_state=seed)
fold_accs = []

for fold, (train_idx, val_idx) in enumerate(kf.split(X), 1):
    torch.manual_seed(seed + fold)

    train_loader = DataLoader(Subset(dataset, train_idx), batch_size=batch_size, shuffle=True)
    val_loader = DataLoader(Subset(dataset, val_idx), batch_size=batch_size * 2, shuffle=False)

    # Un model nou a cada partició
    model = MLP(in_features, num_classes=num_classes)
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()

    for epoch in range(epochs):
        model.train()
        for xb, yb in train_loader:
            optimizer.zero_grad()
            loss = criterion(model(xb), yb)
            loss.backward()
            optimizer.step()

    model.eval()
    correct = total = 0
    with torch.no_grad():
        for xb, yb in val_loader:
            preds = model(xb).argmax(dim=1)
            correct += (preds == yb).sum().item()
            total += yb.size(0)

    fold_accs.append(correct / total)
    print(f"Partició {fold}/{k}: val_acc={fold_accs[-1]:.4f}")

print(f"Mitjana: {np.mean(fold_accs):.4f} ± {np.std(fold_accs):.4f}")

Flux recomanat:

  1. Separa un 20% per a test i no el toquis.
  2. Amb la resta, fes servir validació creuada per triar el model i els hiperparàmetres.
  3. Entrena el model final amb tota la resta i els millors hiperparàmetres.
  4. Avalua’l una sola vegada amb el test.

Mètriques de regressió

La funció de cost serveix per entrenar. Per explicar com de bo és un model a una persona necessitem mètriques que es puguin interpretar.

RMSE i MAE

Totes dues responen “de mitjana, quant m’equivoco?”, en les mateixes unitats que la variable que predim.

\[ \text{MAE} = \frac{1}{m} \sum_{i=1}^m |y_i - \hat{y}i| \qquad \text{RMSE} = \sqrt{\frac{1}{m} \sum{i=1}^m (y_i - \hat{y}_i)^2} \]

  • m: nombre d’exemples
  • yᵢ: valor real de l’exemple i
  • ŷᵢ: predicció del model per a l’exemple i

En paraules senzilles: el MAE fa la mitjana dels errors sense signe. El RMSE eleva cada error al quadrat abans de fer la mitjana, i després en fa l’arrel per tornar a les unitats originals. Per això el RMSE castiga més els errors grans.

L’MSE (error quadràtic mitjà) és el RMSE sense l’arrel. És pràcticament la funció de cost de la regressió lineal, però les seves unitats són al quadrat (euros²), i per això costa d’interpretar.

Exemple: si predim preus de pisos i el MAE és de 15.000 €, de mitjana ens equivoquem en 15.000 €.

Regla pràctica: compara les dues. Si el RMSE és molt més gran que el MAE, hi ha uns quants errors molt grans (outliers) que val la pena mirar. Si són semblants, els errors són uniformes.

R² (coeficient de determinació)

El R² diu quina part de la variació de les dades explica el model, comparat amb la predicció més tonta possible: dir sempre la mitjana.

\[ R^2 = 1 - \frac{\sum_{i=1}^m (y_i - \hat{y}i)^2}{\sum{i=1}^m (y_i - \bar{y})^2} \]

  • ȳ: mitjana de tots els valors reals
  • Numerador: error del model
  • Denominador: error de predir sempre la mitjana

En paraules senzilles: R² = 1 és perfecte, R² = 0 vol dir que el model no és millor que dir sempre la mitjana, i un R² negatiu vol dir que encara és pitjor. Un R² de 0.8 vol dir que el model explica el 80% de la variació.

Com que no té unitats, és útil per comparar models i per explicar resultats. Té una trampa: afegir variables mai el fa baixar, encara que no aportin res.

Resum

MètricaUnitatsSensibilitat a outliersQuan usar-la
MAEOriginalsBaixaSaber l’error típic; dades amb valors extrems
RMSEOriginalsAltaQuan els errors grans són especialment greus
R²Sense unitatsMitjanaComparar models, explicar el resultat

Amb el model de diabetis de l’apartat Fuga de dades:

from sklearn.metrics import r2_score, mean_absolute_error, root_mean_squared_error

y_pred = model.predict(X_test)
print(f"MAE  = {mean_absolute_error(y_test, y_pred):.2f}")
print(f"RMSE = {root_mean_squared_error(y_test, y_pred):.2f}")
print(f"R²   = {r2_score(y_test, y_pred):.4f}")
MAE  = 42.79
RMSE = 53.85
R²   = 0.4526

L’índex de progressió va de 25 a 346, i el model s’equivoca en uns 43 punts de mitjana. El RMSE és una mica més alt que el MAE, senyal que hi ha alguns errors grans però no extrems. Un R² de 0.45 vol dir que les 10 mesures expliquen menys de la meitat de la variació: per a aquest problema, és un resultat raonable però limitat.

Mètriques de classificació

Taxa d’encerts i la seva trampa

La taxa d’encerts (accuracy) és el percentatge de prediccions correctes. Si el model n’encerta 90 de 100, la taxa és del 90%.

El problema apareix quan una classe és molt més freqüent que l’altra. Imagina una malaltia que té 1 de cada 100 persones. Un “model” que sempre diu “no està malalt” encerta 99 de cada 100: un 99% de taxa d’encerts, i no detecta cap malalt. Per això necessitem mètriques que separin els diferents tipus d’error.

Matriu de confusió

La matriu de confusió compta els quatre resultats possibles. Exemple amb 100 pacients, 10 dels quals estan malalts:

Predicció: malaltPredicció: sa
Real: malalt8 (detectats)2 (escapats)
Real: sa5 (falses alarmes)85 (correctes)

Els noms habituals, en anglès, són TP (true positive, els 8 detectats), FN (false negative, els 2 escapats), FP (false positive, les 5 falses alarmes) i TN (true negative, els 85 sans ben classificats).

Cada error té un cost diferent. Una falsa alarma vol dir una prova addicional. Un malalt escapat pot voler dir un tractament que arriba tard.

Precisió, recall i F1

\[ \text{Precisió} = \frac{\text{detectats}}{\text{detectats} + \text{falses alarmes}} \qquad \text{Recall} = \frac{\text{detectats}}{\text{detectats} + \text{escapats}} \]

En paraules senzilles:

  • Precisió (precision): quan el model diu “positiu”, quantes vegades té raó? Aquí, 8 / 13 = 0.62.
  • Recall (sensibilitat, exhaustivitat): de tots els positius reals, quants n’ha trobat? Aquí, 8 / 10 = 0.80.

Hi ha un compromís entre les dues. Un model que marca positiu amb poca evidència troba gairebé tots els casos (recall alt) però fa moltes falses alarmes (precisió baixa). Un model molt exigent fa al revés. És com un detector de metalls: si és molt sensible, sona per qualsevol moneda; si ho és poc, deixa passar coses perilloses.

El F1 resumeix les dues en un sol número:

\[ \text{F1} = 2 \times \frac{\text{Precisió} \times \text{Recall}}{\text{Precisió} + \text{Recall}} \]

En paraules senzilles: és una mitjana que castiga els desequilibris. Amb precisió 1.0 i recall 0.1, la mitjana normal seria 0.55, però el F1 és només 0.18.

Error més costósMètrica a vigilarExemple
Falses alarmesPrecisióFiltre de spam: no vols perdre correus importants
Casos escapatsRecallDiagnòstic mèdic, detecció de frau
Tots dosF1Quan no està clar quin error pesa més

El llindar de decisió

Un classificador com la regressió logística retorna una probabilitat. Per convertir-la en una classe fem servir un llindar: si la probabilitat supera el llindar, diem “positiu”.

El llindar de 0.5 és una elecció, no una regla. Baixar-lo (per exemple, a 0.3) detecta més positius: puja el recall i baixa la precisió. Pujar-lo fa el contrari.

probabilitats = [0.9, 0.7, 0.45, 0.3, 0.1]

# llindar 0.5 → [1, 1, 0, 0, 0]
# llindar 0.4 → [1, 1, 1, 0, 0]  (més permissiu)
# llindar 0.8 → [1, 0, 0, 0, 0]  (més estricte)

El llindar es tria amb el conjunt de validació, com qualsevol altre hiperparàmetre.

Corbes ROC i Precision-Recall

Les dues corbes mostren com es comporta el model amb tots els llindars possibles alhora. Serveixen per comparar models sense haver triat encara el llindar.

La corba ROC dibuixa, per a cada llindar, la proporció de positius detectats (el recall) contra la proporció de negatius que han rebut una falsa alarma.

L’AUC és l’àrea sota la corba. Val 0.5 per a un model que tria a l’atzar (la diagonal) i 1 per a un model perfecte. Té una lectura intuïtiva: un AUC de 0.85 vol dir que, si agafem un positiu i un negatiu a l’atzar, el model dona més probabilitat al positiu en el 85% dels casos.

La corba Precision-Recall dibuixa la precisió contra el recall per a cada llindar. La línia de referència és la proporció de positius del dataset.

L’àrea sota aquesta corba s’anomena AP (average precision).

SituacióCorba recomanada
Classes més o menys equilibradesROC
Pocs positius (menys d’un 10% aproximadament)Precision-Recall
En cas de dubteTotes dues

Per què la diferència? Quan hi ha molts negatius, fins i tot centenars de falses alarmes són una proporció petita d’aquests negatius, i la corba ROC es veu bé. La precisió, en canvi, compara les falses alarmes amb els positius detectats, i ho reflecteix tot.

Les dues àrees es calculen a partir de les probabilitats, no de les classes predites. Amb les dades de tumors de Classificació:

from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, average_precision_score, classification_report
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)
y = 1 - y  # 1 = tumor maligne, la classe que volem detectar
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

clf = make_pipeline(StandardScaler(), LogisticRegression()).fit(X_train, y_train)
y_pred = clf.predict(X_test)
y_prob = clf.predict_proba(X_test)[:, 1]  # probabilitat de la classe 1

print(classification_report(y_test, y_pred))  # precisió, recall i F1 per classe
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob):.3f}")  # ROC-AUC: 0.996
print(f"AP:      {average_precision_score(y_test, y_prob):.3f}")  # AP:      0.994

Totes dues són gairebé 1: el model separa molt bé els dos tipus de tumor. Aquí el 37% del test són malignes, així que les dues corbes diuen el mateix. Les diferències apareixen quan els positius són pocs, com a l’exemple següent.

Classes desbalancejades

Exemple: 10.000 transaccions, de les quals 100 són frau (1%).

Model A, que sempre diu “no és frau”:

Predicció: frauPredicció: normal
Real: frau0100
Real: normal09.900

Taxa d’encerts del 99%, recall del 0%. És inútil.

Model B, entrenat amb cura:

Predicció: frauPredicció: normal
Real: frau6040
Real: normal2009.700
  • Taxa d’encerts: 9.760 / 10.000 = 97.6%. Sembla pitjor que A.
  • Recall: 60 / 100 = 60%. Troba més de la meitat dels fraus.
  • Precisió: 60 / 260 = 23%. Una de cada quatre alertes és frau real.
  • ROC-AUC: 0.92, que sembla excel·lent perquè les 200 falses alarmes són només el 2% de les transaccions normals.

La precisió del 23% és la xifra honesta: el model és útil, però genera moltes alertes per revisar.

Què es pot fer amb classes desbalancejades:

  • Mesurar amb precisió, recall, F1 i la corba Precision-Recall, no amb la taxa d’encerts.
  • Donar més pes a la classe minoritària durant l’entrenament (class_weight='balanced' a scikit-learn).
  • Ajustar el llindar amb el conjunt de validació.
  • Tornar a mostrejar les dades: duplicar exemples de la classe minoritària o reduir la majoritària.

Més de dues classes

Amb tres o més classes, la matriu de confusió és N×N i la precisió, el recall i el F1 es calculen per a cada classe. Després cal combinar-los en un sol número:

  • Macro: mitjana simple de les classes. Totes compten igual, encara que en tinguin pocs exemples.
  • Ponderada (weighted): mitjana ponderada pel nombre d’exemples de cada classe.
  • Micro: es compten tots els encerts junts, com si fos un sol problema. En multiclasse coincideix amb la taxa d’encerts.

Exemple amb tres flors:

                Predicció
              Rosa  Tulipa  Marguerida
Real   Rosa     2      1        0
      Tulipa    1      3        0
  Marguerida    1      0        2
ClassePrediccionsCorrectesPrecisió
Rosa420.50
Tulipa430.75
Marguerida221.00
Total107
  • Macro: (0.50 + 0.75 + 1.00) / 3 = 0.75
  • Micro: 7 / 10 = 0.70
from sklearn.metrics import precision_score, classification_report

# Les 10 flors de la matriu anterior (0 = Rosa, 1 = Tulipa, 2 = Marguerida)
y_true = [0, 0, 0, 1, 1, 1, 1, 2, 2, 2]
y_pred = [0, 0, 1, 1, 1, 0, 1, 2, 2, 0]

print(precision_score(y_true, y_pred, average='macro'))
print(classification_report(y_true, y_pred, target_names=['Rosa', 'Tulipa', 'Marguerida']))

Si totes les classes són igual d’importants, fes servir macro.

Log loss: avaluar les probabilitats

Dos models poden encertar les mateixes classes i donar probabilitats molt diferents: un diu 0.99 i l’altre 0.51 per al mateix encert. El log loss (entropia creuada) mesura la qualitat de les probabilitats, i és la mateixa funció de cost que fa servir la regressió logística per entrenar:

\[ \text{Log Loss} = -\frac{1}{m} \sum_{i=1}^m \left[ y_i \log(\hat{y}_i) + (1-y_i) \log(1-\hat{y}_i) \right] \]

  • ŷᵢ: probabilitat que el model dona a la classe 1
  • yᵢ: classe real (0 o 1)

En paraules senzilles: castiga molt les prediccions molt segures i equivocades. Importa quan les probabilitats s’utilitzen directament, per exemple per ordenar recomanacions o per decidir quan cal revisió humana.

Diagnosticar un model

Rendiment de referència

Abans de dir si un error és alt o baix, cal saber quin error és raonable esperar. Aquesta referència (baseline) pot venir de:

  • Una estratègia trivial: predir sempre la mitjana o la classe majoritària (DummyRegressor, DummyClassifier a scikit-learn). Un model que no la supera no aporta res.
  • El rendiment humà, sobretot amb imatges, àudio o text.
  • Models existents per al mateix problema.

En problemes sorollosos (per exemple, reconeixement de veu amb soroll de fons) ni tan sols una persona arriba a error zero, i la referència ho ha de reflectir.

Biaix i variància

Mirant només dos números, l’error d’entrenament i l’error de validació, podem saber què li passa a un model:

Error d’entrenamentError de validacióDiagnòstic
Alt (lluny de la referència)AltBiaix alt (subajustament): el model és massa simple
BaixMolt més altVariància alta (sobreajustament): el model memoritza
BaixBaixCorrecte

Exemple amb polinomis de grau creixent:

  • L’error d’entrenament sempre baixa quan augmenta el grau: un polinomi més flexible s’ajusta millor a les dades que ja ha vist.
  • L’error de validació baixa i després torna a pujar. El mínim marca el grau adequat (aquí, 3).

En models molt complexos, com les xarxes neuronals, es poden donar les dues coses alhora: el model s’ajusta massa en algunes zones de les dades i massa poc en d’altres.

Regularització

La regularització penalitza els pesos grans durant l’entrenament. Un model amb pesos petits és més suau i generalitza millor. La força de la penalització la controla un paràmetre, λ (lambda):

  • λ massa petit: gairebé no hi ha penalització. Risc de sobreajustament.
  • λ massa gran: tots els pesos tendeixen a zero i el model gairebé prediu una constant. Subajustament.
  • λ intermedi: l’equilibri. Es tria amb el conjunt de validació.

Aquí l’error d’entrenament sempre puja amb λ, i l’error de validació té un mínim (aquí, λ = 1).

La fórmula

La regularització L2 afegeix un terme a la funció de cost:

\[ J(w, b) = J_\text{original}(w, b) + \frac{\lambda}{2m} \sum_{j=1}^{n} w_j^2 \]

  • J original: l’error de sempre (per exemple, l’error quadràtic)
  • λ: força de la penalització
  • n: nombre de pesos
  • wⱼ: cada pes del model. El biaix b normalment no es penalitza.

En el descens del gradient, això afegeix a cada pas un terme que empeny cada pes una mica cap a zero:

\[ w_j := w_j - \alpha \left( \text{gradient habitual} + \frac{\lambda}{m} w_j \right) \]

En paraules senzilles: un pes només es manté gran si ajuda de veritat a reduir l’error. A scikit-learn, Ridge és la regressió lineal amb aquesta penalització. A les xarxes neuronals de PyTorch, fa la mateixa funció el paràmetre weight_decay de l’optimitzador (consulta Xarxes neuronals).

Corbes d’aprenentatge

Una corba d’aprenentatge mostra l’error d’entrenament i el de validació a mesura que augmentem el nombre d’exemples d’entrenament:

  • Amb pocs exemples, el model s’hi ajusta gairebé perfectament (error d’entrenament baix) però generalitza malament (error de validació alt).
  • Amb més exemples, l’error d’entrenament puja una mica i el de validació baixa.

La forma de les corbes diu si val la pena aconseguir més dades:

  • Biaix alt: les dues corbes queden juntes i altes, per sobre de la referència, i s’aplanen. Més dades no ajudaran: cal un model més potent.
  • Variància alta: hi ha una gran separació entre les dues corbes. Més dades sí que ajuden, perquè l’error de validació continua baixant.

Què fer si el model falla

RemeiAjuda amb
Aconseguir més exemples d’entrenamentVariància alta
Fer servir menys característiquesVariància alta
Augmentar λ (més regularització)Variància alta
Afegir característiques novesBiaix alt
Afegir característiques polinòmiques (\( x^2 \), \( x_1 x_2 \)…)Biaix alt
Reduir λ (menys regularització)Biaix alt
Un model més gran (més capes, arbres més profunds)Biaix alt

Reduir el nombre d’exemples d’entrenament no apareix a la taula: no resol cap dels dos problemes.

L’ordre de treball és sempre el mateix: compara l’error d’entrenament amb la referència (biaix), compara l’error de validació amb el d’entrenament (variància), i tria el remei que ataca el problema que has trobat.

Last change: , commit: cc6333d