Salta el contingut

Pràctica PR5072/02: Deep Learning amb Keras/TensorFlow i Docker

Objectius

  • Configurar entorn TensorFlow/Keras amb Docker
  • Explorar i preparar el dataset Fashion MNIST
  • Construir una CNN des de zero amb la Functional API
  • Entrenar amb callbacks professionals (EarlyStopping, ModelCheckpoint, ReduceLROnPlateau)
  • Visualitzar corbes d'aprenentatge i matriu de confusió
  • Aplicar transfer learning amb MobileNetV3
  • Comparar rendiments: CNN pròpia vs transfer learning
  • Dessar i recarregar el model final

Prerequisits

  • Temps estimat: 8 hores
  • RAM mínima: 8 GB (16 GB recomanats per a transfer learning)
  • GPU: Opcional (la pràctica funciona en CPU, però sera mes lenta)
  • Docker Desktop instal·lat i en funcionament
  • Coneixements bàsics de Python
  • Haver completat la PR5072/01 o tenir coneixements de ML supervisat

Introducció

La visió per computador és una de les àrees de la IA on el deep learning ha aconseguit els resultats més espectaculars. La capacitat de les xarxes neuronals convolucionals (CNN) per aprendre representacions jeràrquiques de les imatges —des de vores i textures fins a objectes complexos— ha revolucionat camps com el diagnòstic mèdic, la inspecció industrial, els vehicles autònoms i el comerç electrònic.

En aquesta pràctica construirem dos sistemes de classificació d'imatges sobre el dataset Fashion MNIST, que conté 70.000 imatges en escala de grisos de 10 categories de roba i accessoris. Primer entrenarem una CNN des de zero per entendre l'arquitectura convolucional. Després aplicarem transfer learning amb MobileNetV3, un model preentrenat en ImageNet que adaptem a la nostra tasca amb molt menys temps i dades.

flowchart TD
    A[Fashion MNIST 70K imatges] --> B[Preprocessament i normalitzacio]
    B --> C1[CNN des de zero]
    B --> C2[MobileNetV3 preentrenat]
    C1 --> D1[Entrenament amb callbacks]
    C2 --> D2[Fine-tuning]
    D1 --> E[Comparativa de resultats]
    D2 --> E
    E --> F[Millor model]
    F --> G[Desament model .keras]

Fashion MNIST vs MNIST

Fashion MNIST (creat per Zalando el 2017) substitueix el classic dataset MNIST de digits manuscrits. Es significativament mes difícil (accuracy baseline ~72% vs ~97% per a MNIST) i es considera un benchmark mes representatiu per a la visio per computador moderna.


Part 1: Configuració de l'entorn Docker

1.1 Arrencada del contenidor TensorFlow

# Crea el directori del projecte
mkdir keras-joan-garcia
cd keras-joan-garcia
mkdir notebooks logs models

# Arrenca el contenidor oficial de TensorFlow amb Jupyter
docker run --name keras-joan-garcia \
  -p 8888:8888 \
  -p 6006:6006 \
  -v $(pwd)/notebooks:/tf/notebooks \
  -v $(pwd)/logs:/tf/logs \
  -v $(pwd)/models:/tf/models \
  --shm-size=2g \
  tensorflow/tensorflow:2.16.1-jupyter

# Per veure el token d'acces
docker logs keras-joan-garcia 2>&1 | grep token

Port 6006 per a TensorBoard

El port 6006 esta reservat per a TensorBoard. Un cop el model estigui entrenant, podras obrir http://localhost:6006 al navegador per veure les corbes d'entrenament en temps real.

Usuaris de Windows

Substitueix $(pwd) per la ruta absoluta del directori: -v C:/Users/joan/keras-joan-garcia/notebooks:/tf/notebooks

1.2 Verificació de TensorFlow

# Primera cel·la del notebook: verificacio de l'entorn
import tensorflow as tf
print(f"TensorFlow version: {tf.__version__}")
print(f"Keras version: {tf.keras.__version__}")
print(f"GPU disponible: {tf.config.list_physical_devices('GPU')}")
print(f"Dispositius: {tf.config.list_physical_devices()}")

Part 2: Dataset Fashion MNIST

2.1 Configuració inicial

# =============================================================
# Practica PR5072/02 - Deep Learning amb Keras/TensorFlow
# Alumne: Joan Garcia
# Institut Sa Palomera - Curs IABD 2026-2027
# Module: 5072 Sistemes d'Aprenentatge Automatic
# =============================================================

import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
import json, os
import warnings
warnings.filterwarnings('ignore')

# Metadades de l'alumne (modifica aquests valors)
ALUMNE = "Joan Garcia"
ALUMNE_ID = "joan_garcia"
DATA = datetime.now().strftime("%Y%m%d")

# Directori de sortida
os.makedirs('/tf/models', exist_ok=True)
os.makedirs(f'/tf/logs/{ALUMNE_ID}', exist_ok=True)

print(f"{'='*60}")
print(f"Practica PR5072/02 - Deep Learning amb Keras")
print(f"Alumne: {ALUMNE}")
print(f"TensorFlow: {tf.__version__}")
print(f"{'='*60}")

# Configuracio estetica
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 11

2.2 Càrrega i preprocés del dataset

# Carrrega del dataset
(x_train, y_train), (x_test, y_test) = keras.datasets.fashion_mnist.load_data()

# Categories en catala
CLASSES = [
    'Samarreta',   # 0 - T-shirt/top
    'Pantalons',   # 1 - Trouser
    'Jersey',      # 2 - Pullover
    'Vestit',      # 3 - Dress
    'Abric',       # 4 - Coat
    'Sandalies',   # 5 - Sandal
    'Camisa',      # 6 - Shirt
    'Sabatilla',   # 7 - Sneaker
    'Bossa',       # 8 - Bag
    'Bota'         # 9 - Ankle boot
]

print(f"Shape train: {x_train.shape} | {y_train.shape}")
print(f"Shape test:  {x_test.shape} | {y_test.shape}")
print(f"Rang valors pixels: [{x_train.min()}, {x_train.max()}]")
print(f"Nombre de classes: {len(CLASSES)}")
print(f"\nDistribucio de classes (train):")
unique, counts = np.unique(y_train, return_counts=True)
for cls, cnt in zip(unique, counts):
    print(f"  {CLASSES[cls]:<12}: {cnt} ({cnt/len(y_train)*100:.1f}%)")

2.3 EDA i visualitzacio

# Mostra exemples de cada categoria
fig, axes = plt.subplots(2, 5, figsize=(15, 6))
fig.suptitle(f'Fashion MNIST - Exemples per categoria | Alumne: {ALUMNE}', fontsize=13)

for i, (ax, classe) in enumerate(zip(axes.flat, CLASSES)):
    idx = np.where(y_train == i)[0][0]
    ax.imshow(x_train[idx], cmap='gray')
    ax.set_title(f'{i}: {classe}', fontsize=10)
    ax.axis('off')

plt.tight_layout()
plt.savefig(f'/tf/notebooks/exemples_classes_{ALUMNE_ID}.png', dpi=150)
plt.show()

2.4 Normalitzacio i preparacio dels tensors

# Normalitzacio: pixels 0-255 -> 0.0-1.0
x_train_norm = x_train.astype('float32') / 255.0
x_test_norm  = x_test.astype('float32') / 255.0

# Afegim dimensio del canal (grayscale = 1 canal)
x_train_cnn = x_train_norm[..., np.newaxis]   # Shape: (60000, 28, 28, 1)
x_test_cnn  = x_test_norm[..., np.newaxis]    # Shape: (10000, 28, 28, 1)

print(f"Shape per a CNN: train={x_train_cnn.shape}, test={x_test_cnn.shape}")

# Data Augmentation per prevenir overfitting
data_augmentation = keras.Sequential([
    keras.layers.RandomFlip('horizontal'),
    keras.layers.RandomRotation(0.1),
    keras.layers.RandomZoom(0.1),
    keras.layers.RandomTranslation(height_factor=0.1, width_factor=0.1),
], name='data_augmentation')

# Visualitzacio de l'augmentacio
fig, axes = plt.subplots(1, 6, figsize=(15, 3))
imatge_base = x_train_cnn[0:1]  # Agafem la primera imatge
axes[0].imshow(imatge_base[0, :, :, 0], cmap='gray')
axes[0].set_title('Original')
axes[0].axis('off')

for i in range(1, 6):
    augmented = data_augmentation(imatge_base, training=True)
    axes[i].imshow(augmented[0, :, :, 0], cmap='gray')
    axes[i].set_title(f'Augmentada {i}')
    axes[i].axis('off')

plt.suptitle(f'Data Augmentation | Alumne: {ALUMNE}')
plt.tight_layout()
plt.savefig(f'/tf/notebooks/augmentacio_{ALUMNE_ID}.png', dpi=150)
plt.show()

Part 3: CNN des de zero (Functional API)

3.1 Arquitectura de la xarxa

def build_cnn(alumne_id=ALUMNE_ID):
    """
    Construeix una CNN per a Fashion MNIST amb la Functional API de Keras.
    Arquitectura: Convolucional -> BatchNorm -> Pooling x3 -> Dense -> Dropout -> Softmax
    """
    inputs = keras.Input(shape=(28, 28, 1), name='input_imatge')

    # Augmentacio de dades (nomes activa durant l'entrenament)
    x = data_augmentation(inputs)

    # Bloc 1: 32 filtres 3x3
    x = keras.layers.Conv2D(32, (3, 3), padding='same', name='conv1')(x)
    x = keras.layers.BatchNormalization(name='bn1')(x)
    x = keras.layers.Activation('relu', name='relu1')(x)
    x = keras.layers.MaxPooling2D((2, 2), name='pool1')(x)
    x = keras.layers.Dropout(0.25, name='drop1')(x)

    # Bloc 2: 64 filtres 3x3
    x = keras.layers.Conv2D(64, (3, 3), padding='same', name='conv2')(x)
    x = keras.layers.BatchNormalization(name='bn2')(x)
    x = keras.layers.Activation('relu', name='relu2')(x)
    x = keras.layers.MaxPooling2D((2, 2), name='pool2')(x)
    x = keras.layers.Dropout(0.25, name='drop2')(x)

    # Bloc 3: 128 filtres 3x3
    x = keras.layers.Conv2D(128, (3, 3), padding='same', name='conv3')(x)
    x = keras.layers.BatchNormalization(name='bn3')(x)
    x = keras.layers.Activation('relu', name='relu3')(x)

    # Cap de classificacio
    x = keras.layers.GlobalAveragePooling2D(name='gap')(x)
    x = keras.layers.Dense(256, activation='relu', name='dense1')(x)
    x = keras.layers.Dropout(0.4, name='drop3')(x)
    outputs = keras.layers.Dense(10, activation='softmax', name='output')(x)

    model = keras.Model(inputs, outputs, name=f'CNN_{alumne_id}')
    return model

cnn_model = build_cnn()
cnn_model.summary()

# Recompte de parametres
total_params = cnn_model.count_params()
print(f"\nTotal parametres: {total_params:,}")
print(f"Parametres entrenables: {sum(w.numpy().size for w in cnn_model.trainable_weights):,}")

3.2 Compilació del model

cnn_model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy', keras.metrics.SparseTopKCategoricalAccuracy(k=3, name='top3_acc')]
)

print("Model compilat amb:")
print(f"  Optimitzador: Adam (lr=0.001)")
print(f"  Funcio de perdua: Sparse Categorical Crossentropy")
print(f"  Metriques: accuracy, top-3 accuracy")

Part 4: Entrenament amb callbacks professionals

4.1 Definició dels callbacks

NOM_MODEL = f'model_{ALUMNE_ID}'

callbacks = [
    # Para l'entrenament si la val_loss no millora en 10 epochs
    keras.callbacks.EarlyStopping(
        monitor='val_accuracy',
        patience=10,
        restore_best_weights=True,
        verbose=1
    ),
    # Desa el millor model a disc
    keras.callbacks.ModelCheckpoint(
        filepath=f'/tf/models/{NOM_MODEL}_best.keras',
        monitor='val_accuracy',
        save_best_only=True,
        verbose=1
    ),
    # Redueix el learning rate quan l'entrenament s'estabilitza
    keras.callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.5,
        patience=5,
        min_lr=1e-6,
        verbose=1
    ),
    # Logs per a TensorBoard
    keras.callbacks.TensorBoard(
        log_dir=f'/tf/logs/{ALUMNE_ID}/{DATA}',
        histogram_freq=1,
        write_graph=True
    ),
    # Guardem el millor resultat en JSON
    keras.callbacks.CSVLogger(
        f'/tf/notebooks/training_log_{ALUMNE_ID}.csv'
    )
]

print(f"Callbacks configurats: {[c.__class__.__name__ for c in callbacks]}")

4.2 Entrenament

print(f"\nIniciant entrenament - Alumne: {ALUMNE}")
print(f"Dataset: Fashion MNIST | Model: CNN des de zero")
print(f"Epochs maxims: 50 | Batch size: 64\n")

history = cnn_model.fit(
    x_train_cnn, y_train,
    validation_split=0.2,
    epochs=50,
    batch_size=64,
    callbacks=callbacks,
    verbose=1
)

# Resultats finals
val_acc_final = max(history.history['val_accuracy'])
val_loss_final = min(history.history['val_loss'])
epochs_efectius = len(history.history['accuracy'])

print(f"\n--- RESULTATS D'ENTRENAMENT ---")
print(f"Alumne: {ALUMNE}")
print(f"Epochs efectius: {epochs_efectius}/50")
print(f"Millor val_accuracy: {val_acc_final:.4f} ({val_acc_final*100:.2f}%)")
print(f"Millor val_loss: {val_loss_final:.4f}")

Part 5: Visualització de resultats

5.1 Corbes d'aprenentatge

fig, axes = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle(f'Corbes d'entrenament CNN | Alumne: {ALUMNE}', fontsize=13)

hist = history.history
epochs = range(1, len(hist['accuracy']) + 1)

# Accuracy
axes[0].plot(epochs, hist['accuracy'], 'b-', lw=2, label='Train')
axes[0].plot(epochs, hist['val_accuracy'], 'r-', lw=2, label='Validacio')
axes[0].set_title('Accuracy')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Accuracy')
axes[0].legend()
axes[0].axhline(y=max(hist['val_accuracy']), color='r', linestyle='--', alpha=0.5)
axes[0].text(1, max(hist['val_accuracy'])+0.005,
             f"Max: {max(hist['val_accuracy']):.4f}", color='r', fontsize=9)

# Loss
axes[1].plot(epochs, hist['loss'], 'b-', lw=2, label='Train')
axes[1].plot(epochs, hist['val_loss'], 'r-', lw=2, label='Validacio')
axes[1].set_title('Loss')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].legend()

# Learning Rate
if 'lr' in hist:
    axes[2].plot(epochs, hist['lr'], 'g-', lw=2)
    axes[2].set_title('Learning Rate')
    axes[2].set_xlabel('Epoch')
    axes[2].set_ylabel('LR')
    axes[2].set_yscale('log')
else:
    axes[2].text(0.5, 0.5, 'LR no registrat', ha='center', va='center',
                 transform=axes[2].transAxes)

plt.tight_layout()
plt.savefig(f'/tf/notebooks/corbes_entrenament_{ALUMNE_ID}.png', dpi=150, bbox_inches='tight')
plt.show()

5.2 Matriu de confusió

from sklearn.metrics import classification_report, confusion_matrix

# Prediccions sobre el conjunt de test
y_pred_prob = cnn_model.predict(x_test_cnn, verbose=0)
y_pred = np.argmax(y_pred_prob, axis=1)

# Accuracy final sobre test
test_loss, test_acc, test_top3 = cnn_model.evaluate(x_test_cnn, y_test, verbose=0)
print(f"Test accuracy: {test_acc:.4f} ({test_acc*100:.2f}%)")
print(f"Test top-3 accuracy: {test_top3:.4f}")
print(f"Test loss: {test_loss:.4f}")

# Matriu de confusio
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(12, 10))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=CLASSES, yticklabels=CLASSES, ax=ax)
ax.set_title(f'Matriu de Confusio - CNN | Alumne: {ALUMNE}', fontsize=13)
ax.set_xlabel('Prediccio')
ax.set_ylabel('Real')
plt.tight_layout()
plt.savefig(f'/tf/notebooks/matriu_confusio_cnn_{ALUMNE_ID}.png', dpi=150)
plt.show()

# Informe de classificacio
print("\nInforme de classificacio:")
print(classification_report(y_test, y_pred, target_names=CLASSES))

5.3 Anàlisi d'errors

# Identifica els errors del model
errors_idx = np.where(y_pred != y_test)[0]
print(f"Total errors: {len(errors_idx)} de {len(y_test)} ({len(errors_idx)/len(y_test)*100:.1f}%)")

# Visualitza els 12 errors amb mes confianca (errors "durs")
confiances_errors = y_pred_prob[errors_idx].max(axis=1)
errors_ordenats = errors_idx[np.argsort(confiances_errors)[::-1]][:12]

fig, axes = plt.subplots(3, 4, figsize=(16, 12))
fig.suptitle(f'Errors amb Alta Confianca - CNN | Alumne: {ALUMNE}', fontsize=13)

for ax, idx in zip(axes.flat, errors_ordenats):
    ax.imshow(x_test[idx], cmap='gray')
    real = CLASSES[y_test[idx]]
    pred = CLASSES[y_pred[idx]]
    conf = y_pred_prob[idx].max()
    ax.set_title(f'Real: {real}\nPred: {pred} ({conf:.1%})',
                 fontsize=9, color='red')
    ax.axis('off')

plt.tight_layout()
plt.savefig(f'/tf/notebooks/errors_confianca_{ALUMNE_ID}.png', dpi=150)
plt.show()

Part 6: Transfer Learning amb MobileNetV3

6.1 Preparació del dataset per a MobileNetV3

# MobileNetV3 requereix imatges RGB (3 canals) i mida minima 32x32
# Adaptem Fashion MNIST: 28x28 gray -> 32x32 RGB

def preprocess_for_mobilenet(images):
    """
    Converteix imatges Fashion MNIST per a MobileNetV3:
    - Escala a 32x32
    - Replica el canal gris en 3 canals RGB
    - Aplica preprocessament de MobileNetV3
    """
    # Afegir dimensio de canal si no hi es
    if len(images.shape) == 3:
        images = images[..., np.newaxis]

    # Convertir a tensor i escalar
    images_resized = tf.image.resize(images, [32, 32])

    # Gray -> RGB: repetir el canal 3 vegades
    images_rgb = tf.repeat(images_resized, 3, axis=-1)

    # Preprocessament especific de MobileNetV3 (escala -1 a 1)
    images_prep = keras.applications.mobilenet_v3.preprocess_input(
        images_rgb * 255.0
    )
    return images_prep

# Preprocessem els datasets
print("Preprocessant imatges per a MobileNetV3...")
x_train_mob = preprocess_for_mobilenet(x_train_norm).numpy()
x_test_mob  = preprocess_for_mobilenet(x_test_norm).numpy()

print(f"Shape per a MobileNetV3: {x_train_mob.shape}")
print(f"Rang de valors: [{x_train_mob.min():.2f}, {x_train_mob.max():.2f}]")

6.2 Model de transfer learning

def build_transfer_model(alumne_id=ALUMNE_ID):
    """
    Construeix un model de transfer learning basat en MobileNetV3Small.
    Fase 1: Entrena nomes el cap de classificacio (base congelada).
    """
    # Model base preentrenat en ImageNet
    base_model = keras.applications.MobileNetV3Small(
        input_shape=(32, 32, 3),
        include_top=False,          # Sense el cap de classificacio original
        weights='imagenet',
        include_preprocessing=False
    )

    # Fase 1: Congelar totes les capes de la base
    base_model.trainable = False

    print(f"Parametres base model: {base_model.count_params():,}")
    print(f"Capes totals: {len(base_model.layers)}")

    # Cap de classificacio personalitzat
    inputs = keras.Input(shape=(32, 32, 3), name='input_rgb')
    x = base_model(inputs, training=False)
    x = keras.layers.GlobalAveragePooling2D(name='gap')(x)
    x = keras.layers.Dense(256, activation='relu', name='dense1')(x)
    x = keras.layers.BatchNormalization(name='bn_cap')(x)
    x = keras.layers.Dropout(0.3, name='dropout_cap')(x)
    outputs = keras.layers.Dense(10, activation='softmax', name='output')(x)

    model = keras.Model(inputs, outputs, name=f'TL_{alumne_id}')
    return model, base_model

tl_model, base_model = build_transfer_model()
tl_model.summary()

6.3 Fase 1: Entrenament del cap

tl_model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

callbacks_tl = [
    keras.callbacks.EarlyStopping(monitor='val_accuracy', patience=8,
                                   restore_best_weights=True),
    keras.callbacks.ModelCheckpoint(
        f'/tf/models/model_tl_{ALUMNE_ID}_fase1.keras',
        monitor='val_accuracy', save_best_only=True
    ),
    keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=3, min_lr=1e-6)
]

print("Fase 1: Entrenant cap de classificacio (base congelada)...")
print(f"Parametres entrenables: {sum(w.numpy().size for w in tl_model.trainable_weights):,}")

history_tl_fase1 = tl_model.fit(
    x_train_mob, y_train,
    validation_split=0.2,
    epochs=25,
    batch_size=64,
    callbacks=callbacks_tl,
    verbose=1
)

val_acc_fase1 = max(history_tl_fase1.history['val_accuracy'])
print(f"\nFase 1 completada. Val accuracy: {val_acc_fase1:.4f}")

6.4 Fase 2: Fine-tuning

# Fase 2: Descongelar les ultimes 20 capes per a fine-tuning
print(f"\nFase 2: Fine-tuning de les ultimes 20 capes...")

base_model.trainable = True
capes_totals = len(base_model.layers)
capes_a_congelar = capes_totals - 20

for layer in base_model.layers[:capes_a_congelar]:
    layer.trainable = False

print(f"Capes totals: {capes_totals}")
print(f"Capes congelades: {capes_a_congelar}")
print(f"Capes entrenables: {20}")
print(f"Parametres entrenables: {sum(w.numpy().size for w in tl_model.trainable_weights):,}")

# Recompilacio amb learning rate molt menor
tl_model.compile(
    optimizer=keras.optimizers.Adam(learning_rate=1e-5),  # 100x mes petit
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)

callbacks_tl_fase2 = [
    keras.callbacks.EarlyStopping(monitor='val_accuracy', patience=8,
                                   restore_best_weights=True),
    keras.callbacks.ModelCheckpoint(
        f'/tf/models/model_tl_{ALUMNE_ID}_final.keras',
        monitor='val_accuracy', save_best_only=True
    ),
]

history_tl_fase2 = tl_model.fit(
    x_train_mob, y_train,
    validation_split=0.2,
    epochs=20,
    batch_size=32,
    callbacks=callbacks_tl_fase2,
    verbose=1
)

val_acc_fase2 = max(history_tl_fase2.history['val_accuracy'])
print(f"\nFase 2 completada. Val accuracy: {val_acc_fase2:.4f}")

Part 7: Comparativa CNN vs Transfer Learning

7.1 Avaluació final dels dos models

# Avaluacio CNN pròpia
test_loss_cnn, test_acc_cnn, _ = cnn_model.evaluate(x_test_cnn, y_test, verbose=0)

# Avaluacio Transfer Learning
test_loss_tl, test_acc_tl = tl_model.evaluate(x_test_mob, y_test, verbose=0)

print(f"\n{'='*55}")
print(f"COMPARATIVA FINAL | Alumne: {ALUMNE}")
print(f"{'='*55}")
print(f"{'Model':<25} {'Accuracy Test':>15} {'Loss Test':>12}")
print(f"{'-'*52}")
print(f"{'CNN des de zero':<25} {test_acc_cnn:>14.4f} {test_loss_cnn:>11.4f}")
print(f"{'Transfer Learning':<25} {test_acc_tl:>14.4f} {test_loss_tl:>11.4f}")
print(f"{'='*55}")

millora = (test_acc_tl - test_acc_cnn) * 100
print(f"\nMillora de Transfer Learning: {millora:+.2f} punts percentuals")

# Grafic comparatiu
fig, ax = plt.subplots(figsize=(8, 5))
models_names = ['CNN des de zero', 'Transfer Learning\n(MobileNetV3)']
accuracies = [test_acc_cnn, test_acc_tl]
colors = ['#3498db', '#e74c3c']

bars = ax.bar(models_names, accuracies, color=colors, alpha=0.85, edgecolor='white', width=0.5)
ax.set_ylim(0.8, 1.0)
ax.set_ylabel('Accuracy (Test)')
ax.set_title(f'CNN vs Transfer Learning | Alumne: {ALUMNE}')

for bar, acc in zip(bars, accuracies):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.003,
            f'{acc:.4f}\n({acc*100:.2f}%)', ha='center', fontsize=12)

plt.tight_layout()
plt.savefig(f'/tf/notebooks/comparativa_final_{ALUMNE_ID}.png', dpi=150)
plt.show()

7.2 Corbes d'aprenentatge combinades

fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle(f'Corbes d'entrenament comparades | Alumne: {ALUMNE}', fontsize=13)

# CNN
axes[0].plot(history.history['val_accuracy'], 'b-', lw=2, label='CNN val')
axes[0].plot(history.history['accuracy'], 'b--', alpha=0.5, label='CNN train')

# Transfer Learning (combina les dues fases)
tl_val = (history_tl_fase1.history['val_accuracy'] +
          history_tl_fase2.history['val_accuracy'])
tl_train = (history_tl_fase1.history['accuracy'] +
            history_tl_fase2.history['accuracy'])
fase2_inici = len(history_tl_fase1.history['val_accuracy'])

axes[0].plot(range(fase2_inici, fase2_inici + len(history_tl_fase2.history['val_accuracy'])),
             history_tl_fase2.history['val_accuracy'], 'r-', lw=2, label='TL val (fine-tune)')
axes[0].plot(range(len(history_tl_fase1.history['val_accuracy'])),
             history_tl_fase1.history['val_accuracy'], 'r:', lw=2, label='TL val (cap)')

axes[0].axvline(x=fase2_inici, color='gray', linestyle=':', label='Inici fine-tuning')
axes[0].set_title('Val Accuracy')
axes[0].set_xlabel('Epoch')
axes[0].legend(fontsize=9)

# Loss
axes[1].plot(history.history['val_loss'], 'b-', lw=2, label='CNN val loss')
axes[1].plot(history_tl_fase1.history['val_loss'] + history_tl_fase2.history['val_loss'],
             'r-', lw=2, label='TL val loss')
axes[1].set_title('Val Loss')
axes[1].set_xlabel('Epoch')
axes[1].legend()

plt.tight_layout()
plt.savefig(f'/tf/notebooks/corbes_comparades_{ALUMNE_ID}.png', dpi=150)
plt.show()

Part 8: Desament i càrrega del model final

8.1 Desament del model escollit

import json

# Determina el millor model
if test_acc_tl >= test_acc_cnn:
    model_final = tl_model
    nom_model_final = 'TransferLearning-MobileNetV3'
    x_final_test = x_test_mob
else:
    model_final = cnn_model
    nom_model_final = 'CNN-des-de-zero'
    x_final_test = x_test_cnn

# Desament en format Keras natiu
path_model = f'/tf/models/model_final_{ALUMNE_ID}.keras'
model_final.save(path_model)

mida_bytes = os.path.getsize(path_model)
print(f"Model desat: {path_model}")
print(f"Mida: {mida_bytes / 1024 / 1024:.1f} MB")

# Metadata
metadata = {
    'alumne': ALUMNE,
    'alumne_id': ALUMNE_ID,
    'data_entrenament': datetime.now().isoformat(),
    'model_escollit': nom_model_final,
    'dataset': 'Fashion MNIST',
    'registres_train': len(x_train),
    'registres_test': len(x_test),
    'classes': CLASSES,
    'accuracy_cnn': round(float(test_acc_cnn), 4),
    'accuracy_transfer_learning': round(float(test_acc_tl), 4),
    'accuracy_final': round(float(test_acc_tl if test_acc_tl >= test_acc_cnn else test_acc_cnn), 4),
    'modul': '5072',
    'practica': 'PR5072/02',
    'framework': f'TensorFlow {tf.__version__}'
}

with open(f'/tf/notebooks/metadata_model_{ALUMNE_ID}.json', 'w', encoding='utf-8') as f:
    json.dump(metadata, f, indent=2, ensure_ascii=False)

print(f"\nMetadata guardada.")
print(json.dumps(metadata, indent=2, ensure_ascii=False))

8.2 Verificació de la recàrrega

# Carrega el model des de disc i verifica
model_carregat = keras.models.load_model(path_model)

# Prediccio de 5 exemples
idxs = np.random.choice(len(x_test), 5, replace=False)
preds = model_carregat.predict(x_final_test[idxs], verbose=0)
pred_classes = np.argmax(preds, axis=1)

print("Verificacio del model carregat (5 exemples aleatoris):")
print(f"{'#':<5} {'Real':<14} {'Prediccio':<14} {'Confianca':>10}")
print("-" * 45)
for i, (idx, pred_cls) in enumerate(zip(idxs, pred_classes)):
    real = CLASSES[y_test[idx]]
    pred = CLASSES[pred_cls]
    conf = preds[i].max()
    ok = "OK" if pred_cls == y_test[idx] else "ERROR"
    print(f"{i+1:<5} {real:<14} {pred:<14} {conf:>9.1%}  {ok}")

Preguntes de reflexió

Reflexio i aprofundiment

Respon per escrit (cel·les Markdown al notebook) les seguents preguntes:

Arquitectura i entrenament:

  1. Per quin motiu el transfer learning aconsegueix millors resultats amb menys epochs d'entrenament? Explica el concepte de features preaprenudes.

  2. Que fa exactament BatchNormalization? Per quin motiu millora i accelera l'entrenament?

  3. Quina es la diferencia entre MaxPooling2D i GlobalAveragePooling2D? Quan convindria usar cadascun?

  4. Per quin motiu fem servir un learning rate 100 vegades menor (1e-5) durant el fine-tuning?

Resultats i interpretacio:

  1. Quines categories del Fashion MNIST son mes difícils de classificar? Per quin motiu (semblanca visual, ambiguitat, etc.)?

  2. Quantes dades minimes necessitaries per entrenar una CNN des de zero amb resultats acceptables per a una tasca de visio real?

  3. Com desplograries aquest model en una aplicacio web? Descriu els passos tecnics principals.

Etica i responsabilitat:

  1. Si aquest model s'usas en un sistema de moderacio de contingut (detectar roba inadequada), quins riscos de biaix podrien aparèixer?

Lliurament

Puja els seguents fitxers al Campus Virtual abans de la data llindar:

Fitxer Descripció
deep_learning_joan_garcia.ipynb Notebook Jupyter complet
model_final_joan_garcia.keras Model final en format Keras
metadata_model_joan_garcia.json Metadata del model
informe_dl_joan_garcia.pdf Informe de conclusions en PDF

Requisits minims per ser avaluat

  • El notebook ha d'executar-se de dalt a baix sense errors.
  • El nom de l'alumne ha d'apareixer a la variable ALUMNE i als noms dels fitxers.
  • L'accuracy final al conjunt de test ha de ser superior al 88%.
  • S'han d'haver implementat les dues parts: CNN pròpia i transfer learning.
  • Les 8 preguntes de reflexio han d'estar resposes.

TensorBoard durant l'entrenament

Amb el contenidor en marxa, obre una terminal dins Docker i executa: tensorboard --logdir=/tf/logs --bind_all. Despres accedeix a http://localhost:6006 per monitoritzar l'entrenament en temps real.


Pràctica PR5072/02 | Mòdul 5072 Sistemes d'Aprenentatge Automàtic | Institut Sa Palomera (Blanes) | Curs IABD 2026-2027