Aquest és el moment que fa tot el mòdul que preparem. Ja saps què és un perceptró i per què s'apilen capes (02-01), quina activació posar a cada lloc (02-02), com aprèn la xarxa amb forward i backpropagation (02-03) i quina pèrdua i optimitzador triar (02-04). Avui ho ensamblem tot en un projecte de principi a fi: construir, entrenar i avaluar a Keras una xarxa densa que reconeix dígits escrits a mà fent servir MNIST, el dataset clàssic de la disciplina. Seguim la metodologia de TecnoMarket presentada al mòdul 1 —prototip amb datasets públics → aplicació a dades fictícies de TecnoMarket—: abans d'intentar classificar fotos de productes, l'equip valida el flux de treball complet amb un banc de proves estàndard. Aquest flux (carregar → explorar → preprocessar → definir → entrenar → avaluar → predir → guardar) és idèntic en qualsevol projecte real, i a partir d'avui és teu.

Contingut

  1. El projecte i el dataset: per què MNIST
  2. Carregar i explorar les dades
  3. Preprocessar: normalització i splits
  4. Definir el model (justificant cada elecció)
  5. Entrenar i llegir les corbes de loss/accuracy
  6. Avaluar en test i fer prediccions individuals
  7. Guardar el model a tecnomarket-dl/
  8. Els límits del prototip: per què les fotos reals demanaran una altra arquitectura

El projecte i el dataset: per què MNIST

MNIST conté 70 000 imatges en escala de grisos de 28×28 píxels amb dígits manuscrits (0–9), ja dividides en 60 000 d'entrenament i 10 000 de test. És el "hola món" del deep learning per tres raons pràctiques: es descarrega amb una línia, entrena en minuts sense GPU, i és prou difícil perquè els conceptes importin i prou fàcil perquè tot funcioni a la primera.

Encaix amb TecnoMarket: l'objectiu final de l'equip de visió és classificar fotos de productes en categories (portàtil, cafetera, aspiradora…). MNIST és el seu banc de proves: mateix tipus de problema (classificació multiclasse d'imatges), mida manejable, resultat conegut. Si el flux funciona aquí, migrar a les fotos reals serà qüestió d'arquitectura (mòdul 3) i de dades, no de metodologia.

Treballa on vas preparar l'entorn al mòdul 1: un notebook a tecnomarket-dl/notebooks/ (o Colab, si vas triar aquesta via).

Carregar i explorar les dades

Mai no entrenis sobre dades que no has mirat. És la primera regla de qualsevol projecte.

import numpy as np
import matplotlib.pyplot as plt
from tensorflow import keras

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

print("Train:", X_train.shape, y_train.shape)   # (60000, 28, 28) (60000,)
print("Test: ", X_test.shape, y_test.shape)     # (10000, 28, 28) (10000,)
print("Tipus:", X_train.dtype, "| Rang:", X_train.min(), "-", X_train.max())
print("Classes:", np.unique(y_train))            # [0 1 2 ... 9]
print("Exemples per classe:", np.bincount(y_train))
Train: (60000, 28, 28) (60000,)
Test:  (10000, 28, 28) (10000,)
Tipus: uint8 | Rang: 0 - 255
Classes: [0 1 2 3 4 5 6 7 8 9]
Exemples per classe: [5923 6742 5958 6131 5842 5421 5918 6265 5851 5949]

Lectura de l'exploració: cada imatge és una matriu 28×28 d'enters 0–255 (0 = negre, 255 = blanc), les etiquetes són enters 0–9, i les deu classes estan raonablement equilibrades (entre 5 400 i 6 700 exemples cadascuna) — important, perquè una classe amb molt pocs exemples s'aprendria malament. Vegem algunes imatges amb les seves etiquetes:

fig, eixos = plt.subplots(2, 5, figsize=(10, 4))
for i, eix in enumerate(eixos.flat):
    eix.imshow(X_train[i], cmap="gray")
    eix.set_title(f"etiqueta: {y_train[i]}")
    eix.axis("off")
plt.tight_layout(); plt.show()

Dedica un minut a mirar-les: veuràs sets amb barra i sense, uns inclinats, nous que gairebé semblen quatres. Aquesta variabilitat és exactament el que la xarxa haurà d'absorbir.

Preprocessar: normalització i splits

Tres transformacions separen les dades crues de les dades a punt:

1. Aplanar. La nostra xarxa densa espera un vector per exemple, no una matriu: convertim cada imatge 28×28 en un vector de 784 valors. (Sí, això destrueix l'estructura espacial; tornarem sobre aquesta ferida a l'apartat 8.)

2. Normalitzar. Escalem els píxels de 0–255 a 0–1. Per què? Pel que vam aprendre a 02-03: els gradients depenen de la magnitud de les entrades, i amb entrades de fins a 255 les sumes ponderades serien enormes, saturarien activacions i obligarien a learning rates minúsculs. Entrades al voltant de [0, 1] mantenen l'entrenament a la zona còmoda.

3. Separar validació. Del vocabulari de 01-04: train per ajustar pesos, validation per vigilar el progrés amb dades que la xarxa no fa servir per aprendre, test per a l'examen final, un sol cop. MNIST ja porta el test separat; ara reservem el 10 % del train com a validació.

# 1. Aplanar: (60000, 28, 28) -> (60000, 784)
X_train_prep = X_train.reshape(-1, 784).astype("float32")
X_test_prep  = X_test.reshape(-1, 784).astype("float32")

# 2. Normalitzar a [0, 1]
X_train_prep /= 255.0
X_test_prep  /= 255.0

# 3. Split de validacio: ultims 6000 exemples del train
X_val, y_val     = X_train_prep[54000:], y_train[54000:]
X_train_prep, y_train_prep = X_train_prep[:54000], y_train[:54000]

print(X_train_prep.shape, X_val.shape, X_test_prep.shape)
# (54000, 784) (6000, 784) (10000, 784)

Les etiquetes les deixem com a enters 0–9: a 02-04 vam veure que sparse_categorical_crossentropy les accepta directament, sense conversió one-hot.

Definir el model (justificant cada elecció)

Construïm una xarxa densa 784 → 128 → 64 → 10. Cada decisió surt d'una lliçó d'aquest mòdul:

Decisió Valor Justificació (lliçó)
Tipus de capes Dense (totes amb totes) És l'MLP de 02-01, versió Keras
Activació oculta ReLU Per defecte en ocultes; evita el vanishing gradient de les sigmoides (02-02, 02-03)
Neurones de sortida 10, softmax Multiclasse excloent amb 10 classes (02-02)
Pèrdua sparse_categorical_crossentropy Parella de softmax; etiquetes enteres (02-04)
Optimitzador Adam Punt de partida robust (02-04)
Mides ocultes (128, 64) Hiperparàmetre Elecció raonable de partida; embut progressiu cap a les 10 classes
model = keras.Sequential([
    keras.layers.Input(shape=(784,)),                 # entrada: el vector aplanat
    keras.layers.Dense(128, activation="relu"),       # oculta 1
    keras.layers.Dense(64, activation="relu"),        # oculta 2
    keras.layers.Dense(10, activation="softmax"),     # sortida: 10 probabilitats que sumen 1
])

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

model.summary()
Layer (type)          Output Shape    Param #
dense (Dense)         (None, 128)     100480
dense_1 (Dense)       (None, 64)      8256
dense_2 (Dense)       (None, 10)      650
Total params: 109,386

Fes la comprovació que ja vas fer amb la xarxa 3-4-2-1 de 29 paràmetres a 01-04, ara a l'engròs: $784 \times 128 + 128 = 100480$; $128 \times 64 + 64 = 8256$; $64 \times 10 + 10 = 650$. Total 109 386 paràmetres — pesos i biaixos que backpropagation ajustarà. El mateix recompte de sempre, tres ordres de magnitud més amunt.

Entrenar i llegir les corbes de loss/accuracy

historial = model.fit(
    X_train_prep, y_train_prep,
    epochs=15,                          # 15 passades completes pel train
    batch_size=64,                      # mini-batch (02-04)
    validation_data=(X_val, y_val),     # vigila dades que no fa servir per aprendre
    verbose=2,
)
Epoch 1/15  - loss: 0.2949 - accuracy: 0.9152 - val_loss: 0.1493 - val_accuracy: 0.9558
Epoch 5/15  - loss: 0.0522 - accuracy: 0.9836 - val_loss: 0.0857 - val_accuracy: 0.9743
Epoch 10/15 - loss: 0.0219 - accuracy: 0.9929 - val_loss: 0.0870 - val_accuracy: 0.9770
Epoch 15/15 - loss: 0.0117 - accuracy: 0.9962 - val_loss: 0.1010 - val_accuracy: 0.9772

Cada època són $54000 / 64 \approx 844$ actualitzacions mini-batch dels 109 386 paràmetres, cadascuna amb el seu forward, el seu backward i el seu pas d'Adam: tot 02-03 i 02-04 girant per dins. L'objecte historial guarda les mètriques per època; dibuixar-les és obligatori:

h = historial.history
fig, (eix1, eix2) = plt.subplots(1, 2, figsize=(12, 4))
eix1.plot(h["loss"], label="train"); eix1.plot(h["val_loss"], label="validacio")
eix1.set_title("Perdua"); eix1.set_xlabel("epoca"); eix1.legend(); eix1.grid(alpha=0.3)
eix2.plot(h["accuracy"], label="train"); eix2.plot(h["val_accuracy"], label="validacio")
eix2.set_title("Accuracy"); eix2.set_xlabel("epoca"); eix2.legend(); eix2.grid(alpha=0.3)
plt.show()

Com llegir les corbes:

  • Les dues pèrdues baixen juntes al principi: la xarxa aprèn patrons generals. Bé.
  • Cap a l'època 5–7 se separen: la de train continua caient (fins a 0.01) però la de validació s'estanca i fins i tot puja lleugerament (0.086 → 0.101). La xarxa comença a memoritzar peculiaritats del train que no generalitzen: és el sobreajust (overfitting). Amb aquesta magnitud és lleu (el val_accuracy es manté ≈ 97.7 %), però el senyal és inequívoc: entrenar més èpoques ja no millora res d'útil.
  • Regla pràctica: la corba que importa és la de validació. Les tècniques per combatre el sobreajust (regularització, dropout, early stopping) tenen la seva pròpia lliçó al mòdul 5; de moment, n'hi ha prou a saber detectar-lo i no entrenar de més.

Avaluar en test i fer prediccions individuals

El test es toca un sol cop, al final, per obtenir la xifra honesta que reportaries a l'equip de TecnoMarket:

perdua_test, acc_test = model.evaluate(X_test_prep, y_test, verbose=0)
print(f"Accuracy en test: {acc_test:.4f}")   # -> ~0.977

Un 97.7 % amb una xarxa densa senzilla: unes 230 imatges fallades de 10 000. Ara, prediccions individuals, que és com el model es faria servir en producció:

probs = model.predict(X_test_prep[:5], verbose=0)   # forma (5, 10)
print(np.round(probs[0], 3))
# [0. 0. 0. 0. 0. 0. 0. 0.999 0. 0.001]  <- softmax: gairebe tot a la classe 7
print("Prediccio:", probs.argmax(axis=1))   # [7 2 1 0 4]
print("Realitat:  ", y_test[:5])            # [7 2 1 0 4]

predict retorna les 10 probabilitats del softmax per imatge; argmax tria la classe més probable. Encara més instructiu és mirar els errors:

probs_all = model.predict(X_test_prep, verbose=0)
pred = probs_all.argmax(axis=1)
errors = np.where(pred != y_test)[0]
print(f"{len(errors)} errors de {len(y_test)}")

fig, eixos = plt.subplots(1, 5, figsize=(12, 3))
for eix, idx in zip(eixos, errors[:5]):
    eix.imshow(X_test[idx], cmap="gray")
    eix.set_title(f"real {y_test[idx]} / pred {pred[idx]}")
    eix.axis("off")
plt.show()

Veuràs que moltes fallades són dígits genuïnament ambigus (quatres que semblen nous, sets recargolats) on fins i tot un humà dubtaria. Analitzar errors concrets —no només la mètrica global— és un hàbit professional que reapareixerà en tots els projectes del mòdul 7.

Guardar el model a tecnomarket-dl/

Un model que només viu a la memòria del notebook es perd en tancar la sessió. Guardem els 109 386 pesos, l'arquitectura i la configuració de compile en un sol fitxer, dins de l'estructura de carpetes que vam crear a 01-05:

model.save("tecnomarket-dl/models/mnist_dens_v1.keras")

# Comprovacio: recarregar i verificar que prediu igual
model_carregat = keras.models.load_model("tecnomarket-dl/models/mnist_dens_v1.keras")
print(model_carregat.predict(X_test_prep[:1], verbose=0).argmax())   # -> 7, com abans

El format .keras és el natiu modern de Keras i n'hi ha prou amb un load_model per recuperar el model sencer, a punt per predir o continuar entrenant. Això és tot el que necessites de moment: el versionat seriós, els formats alternatius i el desplegament real de models (servir-los en producció per al web de TecnoMarket) tenen la seva pròpia lliçó al mòdul 6.

Els límits del prototip: per què les fotos reals demanaran una altra arquitectura

El prototip està validat; pot l'equip llançar-se ja a classificar les fotos de productes de TecnoMarket amb aquesta mateixa xarxa? No, i entendre per què és la millor transició al proper mòdul:

  • Aplanar destrueix l'estructura espacial. En convertir 28×28 en 784 valors, la xarxa no sap que el píxel 30 és just a sota del píxel 2. Aprèn a base de força bruta estadística, cosa viable amb dígits centrats de 28×28, però no amb fotos de 224×224 on la cafetera pot aparèixer a qualsevol racó.
  • Sense invariància a la posició. Si desplaces un dígit tres píxels a la dreta, per a la xarxa densa és un vector completament diferent — cada píxel cau en una altra entrada amb altres pesos. Les fotos reals de productes varien en posició, escala, angle i il·luminació.
  • Explosió de paràmetres. Amb fotos en color de 224×224 (150 528 entrades), només la primera capa densa de 128 neurones tindria 19 milions de paràmetres. Inviable i ineficient.

La solució són les xarxes convolucionals (CNN): capes que miren la imatge per retalls, comparteixen pesos i respecten l'estructura espacial. Són exactament el tema del mòdul 3.

Errors Comuns i Consells

  • Oblidar la normalització. Amb píxels 0–255 la xarxa pot continuar entrenant, però pitjor i més a poc a poc (accuracy típic diversos punts per sota). Si els teus resultats són sospitosament dolents, revisa abans de res el rang de les entrades: X.min(), X.max().
  • Normalitzar train i test amb criteris diferents. Tota transformació es defineix amb el train i s'aplica idèntica al test. Aquí és trivial (dividir per 255), però el principi evitarà errors greus amb dades tabulars.
  • Avaluar en test moltes vegades mentre ajustes. Si proves deu arquitectures mirant el test i et quedes amb la millor, el test ha deixat de ser un examen honest: t'hi has ajustat. Per iterar hi ha la validació; el test és la nota final.
  • Fixar-se només en l'accuracy de train. Un 99.6 % en train amb 97.7 % en validació és sobreajust, no èxit. La xifra que compta és sempre la de validació/test.
  • Oblidar aplanar/preprocessar en el moment de predir. El model espera exactament el format amb què es va entrenar: vector de 784 floats a [0, 1], amb forma (n, 784) (encara que sigui una sola imatge: (1, 784), no (784,)!).
  • Èpoques de més "per si de cas". Quan la corba de validació fa diverses èpoques que és plana o puja, continuar entrenant només augmenta el sobreajust. Atura't i queda't amb la zona bona.

Exercicis

  1. Fashion-MNIST, mateix flux. Repeteix el projecte complet amb keras.datasets.fashion_mnist (mateixes formes: 28×28, 10 classes, però peces de roba: samarretes, sabatilles, bosses — un banc de proves encara més proper als productes de TecnoMarket). Quin accuracy de test aconsegueixes? Per què creus que és més baix que a MNIST?
  2. La mida importa (o no?). Sobre MNIST, entrena tres variants: (a) sense capes ocultes (784 → 10 softmax, una regressió logística multiclasse), (b) una oculta de 32, (c) la xarxa del text (128 + 64). Compara l'accuracy de validació i el nombre de paràmetres de cadascuna en una petita taula. Compensa cada salt de mida?
  3. Informe de confiança. Escriu una funció predir_amb_confianca(model, imatge) que retorni la classe predita, la seva probabilitat, i la segona classe més probable amb la seva; prova-la sobre 3 imatges encertades i 3 fallades del test. Què observes en la confiança de les fallades?

Solucions

Exercici 1. El codi és idèntic canviant la línia de càrrega per keras.datasets.fashion_mnist.load_data(). L'accuracy de test típic ronda el 88–90 %, davant del ~98 % de MNIST. La raó: les classes de roba s'assemblen molt més entre si (samarreta vs. camisa vs. abric comparteixen silueta) que els dígits, i la textura/forma fina importa més — dificultat que anticipa la de les fotos reals de productes i que motiva encara més les CNN del mòdul 3.

Exercici 2. Resultats típics (els teus variaran una mica per l'aleatorietat de la inicialització):

Variant Paràmetres Val accuracy
(a) 784 → 10 7 850 ~92.5 %
(b) 784 → 32 → 10 25 450 ~96.5 %
(c) 784 → 128 → 64 → 10 109 386 ~97.7 %

El primer salt (afegir una capa oculta) és el que més aporta: passa d'un model lineal a un de no lineal, +4 punts. El segon salt multiplica per 4 els paràmetres per guanyar ~1 punt: rendiments decreixents. Lliçó: més gran ajuda, però cada vegada menys, i el salt qualitatiu el dona la no linealitat (com sabies des de 02-02).

Exercici 3.

def predir_amb_confianca(model, imatge):
    probs = model.predict(imatge.reshape(1, 784), verbose=0)[0]
    ordre = probs.argsort()[::-1]          # classes de mes a menys probabilitat
    return {"prediccio": int(ordre[0]), "confianca": float(probs[ordre[0]]),
            "segona": int(ordre[1]), "conf_segona": float(probs[ordre[1]])}

A les imatges encertades la confiança sol superar 0.99 i la segona opció queda per sota de 0.01. A les fallades és habitual veure confiances menors (0.5–0.9) amb una segona opció forta que sovint és la classe correcta (real 4, pred 9 amb 0.55, segona 4 amb 0.42). Moralitat per a TecnoMarket: les probabilitats del softmax permeten polítiques de "revisió humana si la confiança és baixa", igual que el llindar graduable del detector de frau.

Conclusió

Acabes de completar el teu primer projecte de deep learning de principi a fi: vas explorar MNIST, el vas normalitzar i particionar, vas definir una xarxa densa 784-128-64-10 justificant cada peça amb les lliçons del mòdul (ReLU en ocultes, softmax + entropia creuada categòrica a la sortida, Adam com a optimitzador, mini-batches de 64), vas entrenar vigilant les corbes i detectant l'inici del sobreajust, vas obtenir un ~97.7 % honest en test, vas examinar errors individuals i vas guardar el model versionat a tecnomarket-dl/models/. El flux que has seguit és el mateix que faràs servir en tots els projectes que queden del curs; només canviaran les dades i les arquitectures.

I precisament d'arquitectures va el pas següent. Hem vist que la xarxa densa tracta la imatge com una llista plana de píxels: li va funcionar amb prou feines amb dígits de 28×28 i li costaria molt més amb les peces de roba de Fashion-MNIST — i seria inviable amb les fotos reals de productes de TecnoMarket. Al mòdul 3 coneixeràs les xarxes neuronals convolucionals (CNN), l'arquitectura que entén les imatges com el que són: estructures espacials on la posició, les vores i les formes importen.

Curs de Deep Learning

Mòdul 1: Introducció al Deep Learning

Mòdul 2: Fonaments de Xarxes Neuronals

Mòdul 3: Xarxes Neuronals Convolucionals (CNN)

Mòdul 4: Xarxes Neuronals Recurrents (RNN)

Mòdul 5: Tècniques Avançades en Deep Learning

Mòdul 6: Eines i Frameworks

Mòdul 7: Projectes Pràctics

Mòdul 8: Consideracions Ètiques i Futur del Deep Learning

© Copyright 2026. Tots els drets reservats