Al projecte 1 vas treballar amb dades tabulars, el terreny natural de scikit-learn. Ara canviem de món: imatges. L'objectiu d'aquest projecte és doble. Primer, comprovar fins on arriben els mètodes clàssics del mòdul 4 amb imatges petites (els dígits manuscrits de load_digits); després, fer el salt a Fashion-MNIST —70.000 fotos de peces de roba en 10 categories— i resoldre'l amb Keras, comparant una xarxa densa (MLP) amb una xarxa convolucional (CNN), reprenent a la pràctica tot el que vas veure a 07-04 "Xarxes neuronals profundes (Deep Learning)". Al final tindràs criteri propi per respondre la pregunta important: quan n'hi ha prou amb el clàssic i quan cal deep learning?

Contingut

  1. Definició del problema i mètrica objectiu
  2. Etapa A: dígits amb mètodes clàssics
  3. Els límits de l'enfocament clàssic
  4. Etapa B: Fashion-MNIST amb Keras
  5. MLP densa: el baseline neuronal
  6. CNN petita: convolucions en acció
  7. Overfitting a la pràctica: dropout i early stopping
  8. Avaluació final: matriu de confusió de les 10 classes
  9. Conclusions: clàssic o deep?

Definició del problema i mètrica objectiu

  • Problema: classificació multiclasse d'imatges — 10 dígits primer, 10 peces de roba després.
  • Mètrica principal: accuracy. A diferència del projecte de frau que t'espera a 09-04, aquí les classes estan perfectament equilibrades (un 10 % cadascuna), així que l'accuracy no enganya — és el cas benigne que vam descriure a 06-02 "Mètriques d'avaluació". L'acompanyarem amb la matriu de confusió per veure què es confon amb què.
  • Criteri d'èxit: en dígits, superar el 95 % (és un dataset fàcil); en Fashion-MNIST, superar el 90 %, cosa que exigeix una CNN.

Una imatge en escala de grisos és una matriu de píxels (0 = negre, 255 = blanc). Per a un model clàssic, cada píxel és simplement una columna més: una imatge de 8×8 són 64 features. Aquesta "traducció" — aplanar la imatge a un vector — és la clau de l'etapa A i també el seu taló d'Aquil·les.

Etapa A: dígits amb mètodes clàssics

load_digits porta 1.797 dígits manuscrits de 8×8 píxels, en el mateix format que qualsevol dataset tabular de sklearn:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split, cross_val_score

digits = load_digits()
X, y = digits.data, digits.target   # X: (1797, 64), valors 0-16
print(X.shape, np.unique(y))

# Vegem alguns exemples
fig, eixos = plt.subplots(2, 8, figsize=(10, 3))
for eix, img, etiqueta in zip(eixos.ravel(), digits.images, y):
    eix.imshow(img, cmap="gray_r")
    eix.set_title(etiqueta); eix.axis("off")
plt.tight_layout(); plt.show()

Dividim (estratificant, com sempre des de 06-01 "Divisió de dades: entrenament, validació i prova") i provem dos clàssics amb escalat en Pipeline — la logística de 04-02 "Regressió logística" i la SVM de 04-04 "Màquines de suport vectorial (SVM)":

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

classics = {
    "Logistica": Pipeline([("esc", StandardScaler()),
                           ("m", LogisticRegression(max_iter=2000))]),
    "SVM (RBF)": Pipeline([("esc", StandardScaler()),
                           ("m", SVC(kernel="rbf", gamma="scale"))]),
}

for nom, model in classics.items():
    puntuacions = cross_val_score(model, X_train, y_train, cv=5)
    print(f"{nom}: {puntuacions.mean():.3f} +- {puntuacions.std():.3f}")

Resultats típics: logística ≈ 0,96 i SVM ≈ 0,98 d'accuracy en validació creuada. Amb imatges diminutes i ben centrades, el clàssic funciona d'allò més bé.

Per entendre per què, visualitza el dataset amb el PCA de 05-03 "Anàlisi de components principals (PCA)":

from sklearn.decomposition import PCA

pca = PCA(n_components=2)
X_2d = pca.fit_transform(StandardScaler().fit_transform(X_train))

plt.figure(figsize=(8, 6))
sc = plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y_train, cmap="tab10", s=10, alpha=0.6)
plt.colorbar(sc, label="Digit")
plt.title("Digits projectats a 2D amb PCA")
plt.show()
print("Variancia explicada:", pca.explained_variance_ratio_.sum().round(3))

Fins i tot amb només 2 components (≈ 22 % de la variància), s'hi intueixen núvols per dígit: les classes són força separables fins i tot després d'aplanar la imatge. Aquest és el secret de l'èxit clàssic aquí.

Els límits de l'enfocament clàssic

Per què no continuar així amb qualsevol imatge? Perquè aplanar destrueix l'estructura espacial:

  • Un píxel deixa de "saber" qui eren els seus veïns: la columna 13 i la 14 són adjacents a la imatge, però per a la logística són dues features tan independents com MedInc i Latitude al projecte 1.
  • El model no és invariant a translacions: el mateix dígit desplaçat dos píxels activa columnes completament diferents i sembla "un altre" patró.
  • La dimensionalitat esclata: 8×8 són 64 features; una foto modesta de 224×224 en color són 150.528. Amb features tan crues, els mètodes del mòdul 4 s'ofeguen.

Amb dígits de 8×8, centrats i nets, aquests problemes gairebé no es noten. Amb fotos de roba de 28×28 començaran a pesar; amb fotos reals, serien fatals. Aquesta és exactament la motivació de les convolucions que vas veure a 07-04: processar la imatge respectant-ne la geometria.

Etapa B: Fashion-MNIST amb Keras

Fashion-MNIST és el substitut modern del MNIST original: 60.000 imatges d'entrenament i 10.000 de test, de 28×28 en escala de grisos, amb 10 tipus de peça de roba. Ve inclòs a Keras:

import tensorflow as tf
from tensorflow import keras

(X_train, y_train), (X_test, y_test) = keras.datasets.fashion_mnist.load_data()

classes = ["Samarreta", "Pantalo", "Jersei", "Vestit", "Abric",
           "Sandalia", "Camisa", "Sabatilla", "Bossa", "Boti"]
print(X_train.shape, X_test.shape)  # (60000, 28, 28) (10000, 28, 28)

Normalització de píxels: els valors van de 0 a 255; les xarxes entrenen molt millor amb entrades petites i homogènies (és el mateix principi de 03-05 "Normalització i estandardització", aplicat a imatges). Dividim entre 255 per portar-los a [0, 1], i apartem un conjunt de validació per vigilar l'entrenament:

X_train = X_train / 255.0
X_test = X_test / 255.0

# Validacio: ultimes 10.000 imatges del train
X_val, y_val = X_train[50000:], y_train[50000:]
X_tr,  y_tr  = X_train[:50000], y_train[:50000]

MLP densa: el baseline neuronal

Comencem per l'arquitectura que ja coneixes de 04-07 "Xarxes neuronals" i 07-04: capes denses sobre la imatge aplanada. És el nostre baseline "sense estructura espacial":

mlp = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),   # 784 features
    keras.layers.Dense(256, activation="relu"),
    keras.layers.Dense(128, activation="relu"),
    keras.layers.Dense(10, activation="softmax"), # 10 probabilitats
])

mlp.compile(optimizer="adam",
            loss="sparse_categorical_crossentropy",
            metrics=["accuracy"])

hist_mlp = mlp.fit(X_tr, y_tr, epochs=20, batch_size=128,
                   validation_data=(X_val, y_val), verbose=2)

Recordatori de 07-04: softmax converteix les 10 sortides en probabilitats que sumen 1, i sparse_categorical_crossentropy és la pèrdua adequada quan les etiquetes són enters (0–9) en lloc de one-hot. Resultat típic: ≈ 0,89 d'accuracy en validació, amb la corba d'entrenament desenganxant-se de la de validació en les últimes èpoques — l'overfitting traient el cap, com a 06-05 "Overfitting i underfitting".

CNN petita: convolucions en acció

Ara l'alternativa que respecta la geometria. Reprenent 07-04: una capa Conv2D fa lliscar filtres petits (3×3) per tota la imatge, aprenent detectors locals (vores, textures) que s'apliquen igual a qualsevol posició — això dona invariància a translacions i moltíssims menys paràmetres. MaxPooling2D redueix la resolució a la meitat quedant-se amb l'activació més forta de cada finestra 2×2, guanyant robustesa davant petits desplaçaments:

cnn = keras.Sequential([
    keras.layers.Reshape((28, 28, 1), input_shape=(28, 28)),
    keras.layers.Conv2D(32, (3, 3), activation="relu"),  # 32 detectors 3x3
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Conv2D(64, (3, 3), activation="relu"),  # combina patrons
    keras.layers.MaxPooling2D((2, 2)),
    keras.layers.Flatten(),
    keras.layers.Dense(128, activation="relu"),
    keras.layers.Dropout(0.3),
    keras.layers.Dense(10, activation="softmax"),
])
cnn.summary()

Llegeix el summary(): les capes convolucionals tenen pocs milers de paràmetres (els filtres es comparteixen per tota la imatge), mentre que a la MLP la primera capa densa tota sola en tenia 784 × 256 ≈ 200.000. Menys paràmetres fent més feina útil: aquesta és la idea convolucional.

Overfitting a la pràctica: dropout i early stopping

Dues defenses de 07-04 aplicades de debò:

  • Dropout(0.3): a cada pas d'entrenament apaga a l'atzar el 30 % de les neurones d'aquella capa, impedint que la xarxa memoritzi coadaptacions fràgils.
  • Early stopping: vigila la pèrdua de validació i atura l'entrenament quan deixa de millorar, restaurant els millors pesos — l'equivalent pràctic de triar el punt òptim de la corba de 06-05.
cnn.compile(optimizer="adam",
            loss="sparse_categorical_crossentropy",
            metrics=["accuracy"])

aturada = keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=3, restore_best_weights=True
)

hist_cnn = cnn.fit(X_tr, y_tr, epochs=30, batch_size=128,
                   validation_data=(X_val, y_val),
                   callbacks=[aturada], verbose=2)

# Corbes d'aprenentatge
plt.plot(hist_cnn.history["accuracy"], label="train")
plt.plot(hist_cnn.history["val_accuracy"], label="validacio")
plt.xlabel("Epoca"); plt.ylabel("Accuracy"); plt.legend()
plt.title("CNN: corbes d'aprenentatge"); plt.show()

Resultat típic: ≈ 0,91–0,92 en validació, amb les corbes molt més enganxades que a la MLP. L'early stopping sol aturar-se entre les èpoques 10 i 15.

Avaluació final: matriu de confusió de les 10 classes

Com sempre, el test s'obre una sola vegada, amb el model ja decidit:

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, accuracy_score

y_pred = cnn.predict(X_test).argmax(axis=1)
print("Accuracy test CNN:", round(accuracy_score(y_test, y_pred), 4))

cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(cm, display_labels=classes)
fig, eix = plt.subplots(figsize=(9, 9))
disp.plot(ax=eix, cmap="Blues", xticks_rotation=45, colorbar=False)
plt.title("Fashion-MNIST: matriu de confusio (test)")
plt.tight_layout(); plt.show()
Model Accuracy (validació) Accuracy (test) Paràmetres aprox.
MLP densa ≈ 0,89 ≈ 0,88 ≈ 235.000
CNN petita ≈ 0,92 ≈ 0,91 ≈ 225.000

La matriu de confusió explica la història interessant: els errors no es reparteixen a l'atzar. Les confusions dominants són Camisa ↔ Samarreta ↔ Jersei ↔ Abric (totes són peces de tors amb una silueta semblant a 28×28 píxels) i, en menor mesura, Sabatilla ↔ Botí ↔ Sandàlia. En canvi, Pantaló i Bossa gairebé no es confonen amb res: les seves siluetes són inconfusibles. La xarxa s'equivoca allà on un humà, mirant una miniatura borrosa de 28×28, també dubtaria — un error raonable és senyal d'un model que ha après patrons reals i no soroll.

Errors Comuns i Consells

  • Oblidar normalitzar els píxels. Amb valors 0–255 la xarxa entrena malament o molt a poc a poc (gradients desproporcionats). Dividir entre 255 és una línia que ho canvia tot.
  • Normalitzar train i test amb criteris diferents. Aquí és una constant (255), però si fessis servir mitjana i desviació, es calcularien només amb el train — la mateixa disciplina de 03-05 i 06-01.
  • Avaluar en test cada època. El test no és la validació: si el fas servir per decidir quan aturar-te o quina arquitectura triar, deixa de mesurar generalització. Per a això hi ha validation_data.
  • Comparar xarxes amb una sola execució. La inicialització aleatòria fa variar el resultat unes dècimes; per afirmar que la CNN guanya la MLP convé repetir amb diverses llavors (amb dades tabulars faries servir la CV de 06-03; amb xarxes grans es repeteixen entrenaments, perquè la CV completa és cara).
  • Xarxes enormes per a problemes petits. La temptació d'apilar capes arriba abans que la necessitat. Comença petit, mesura, i creix només si la validació ho demana.

Reptes per ampliar

  1. Data augmentation. Genera variants de les imatges d'entrenament (desplaçaments, girs lleus, mirall horitzontal) per ensenyar a la xarxa invariàncies que les dades no mostren. Pista: investiga les capes keras.layers.RandomTranslation i RandomFlip col·locades a l'inici del model; amb roba, compte amb el mirall vertical (un botí cap per avall continua sent un botí?).
  2. Transfer learning. En problemes reals gairebé mai no s'entrena una CNN des de zero: es parteix d'una xarxa preentrenada amb milions de fotos (ResNet, MobileNet, disponibles a keras.applications) i es reentrena només l'última capa. Pista: requereix redimensionar les imatges a la mida d'entrada de la xarxa triada i convertir-les a 3 canals; amb Fashion-MNIST el guany és modest, però el patró és el que faràs servir a la feina.
  3. Les teves pròpies fotos. Fotografia una sabatilla o una samarreta sobre un fons clar, converteix-la a escala de grisos de 28×28 amb PIL, inverteix el contrast si cal (Fashion-MNIST té el fons negre) i passa-la a la CNN. Pista: probablement fallarà més del que esperes — reflexiona sobre el drift entre les teves fotos i les d'entrenament, el mateix fenomen de 08-03 "Manteniment i monitoratge de models".

Conclusió

Aquest projecte t'ha donat la resposta pràctica a "clàssic o deep?": amb imatges diminutes i centrades com els dígits, una SVM amb escalat frega el 98 % i no necessites res més; amb imatges tot just una mica més riques com Fashion-MNIST, aplanar píxels comença a costar punts i les convolucions — filtres locals compartits, pooling, invariància a translacions — els recuperen amb menys paràmetres. També has practicat l'ofici d'entrenar xarxes: normalitzar píxels, vigilar les corbes train/validació, frenar l'overfitting amb dropout i early stopping, i llegir la matriu de confusió com un mapa d'errors raonables. La regla general que t'endus: comença pel mètode més simple que pugui funcionar, i que siguin les dades les que justifiquin cada capa addicional. En el pròxim projecte aplicaràs aquesta mateixa regla a un altre tipus de dada no tabular — el text — on els mètodes clàssics, amb la representació adequada, encara planten cara.

Curs de Machine Learning

Mòdul 1: Introducció al Machine Learning

Mòdul 2: Fonaments d'Estadística i Probabilitat

Mòdul 3: Preprocessament de Dades

Mòdul 4: Algorismes de Machine Learning Supervisat

Mòdul 5: Algorismes de Machine Learning No Supervisat

Mòdul 6: Avaluació i Validació de Models

Mòdul 7: Tècniques Avançades i Optimització

Mòdul 8: Implementació i Desplegament de Models

Mòdul 9: Projectes Pràctics

Mòdul 10: Recursos Addicionals

© Copyright 2026. Tots els drets reservats