🧠 Reti Neurali con TensorFlow: Riconoscimento Cifre MNIST

Impara Deep Learning costruendo reti neurali che riconoscono cifre scritte a mano

Introduzione

Benvenuto nel mondo affascinante delle reti neurali! In questa esercitazione costruirai la tua prima rete neurale da zero usando TensorFlow, la libreria più potente per il Deep Learning. Imparerai a costruire, addestrare e ottimizzare reti neurali per il riconoscimento di cifre scritte a mano usando il famoso dataset MNIST.

Scenario Reale:
Lavorerai come Deep Learning Engineer per una startup che sviluppa sistemi di riconoscimento ottico per la digitalizzazione di documenti. Costruirai una rete neurale che riconosce automaticamente cifre scritte a mano con accuratezza superiore al 99%.

⚠️ ATTENZIONE: COPIA BLOCCATA

Il codice è protetto da sistema anti-copia. Dovrai SCRIVERE manualmente tutto il codice per imparare veramente! 🚫📋

Parte 1: Setup e Caricamento Dataset MNIST

▶ Importazione TensorFlow e Caricamento Dati

1
Crea una nuova cella su Google Colab e importa TensorFlow:
PYTHON
# IMPORT TENSORFLOW E LIBRERIE PER DEEP LEARNING
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models, callbacks
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import random
import time
import warnings
warnings.filterwarnings('ignore')

# Configurazione TensorFlow per performance
tf.config.threading.set_inter_op_parallelism_threads(2)
tf.config.threading.set_intra_op_parallelism_threads(2)

# Configurazione visualizzazione
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (14, 8)
plt.rcParams['font.size'] = 12

print("✅ TensorFlow e librerie importate correttamente!")
print(f"📊 Versione TensorFlow: {tf.__version__}")
print(f"📊 Versione Keras: {keras.__version__}")

# Verifica GPU disponibile (su Google Colab)
gpu_devices = tf.config.list_physical_devices('GPU')
if gpu_devices:
    print(f"🎮 GPU disponibile: {len(gpu_devices)} dispositivo(i)")
    for device in gpu_devices:
        print(f"  • {device}")
else:
    print("⚠️  Nessuna GPU disponibile, utilizzeremo la CPU")

print("\n🧠 Componenti TensorFlow disponibili:")
print("  • layers: Costruzione layer neurali")
print("  • models: Creazione modelli sequenziali/funzionali")
print("  • callbacks: Callback per monitoraggio training")
print("  • optimizers: Ottimizzatori (Adam, SGD, etc.)")
print("  • losses: Funzioni di loss")
print("  • metrics: Metriche di valutazione")

▶ Caricamento e Esplorazione Dataset MNIST

2
Carica il famoso dataset MNIST di cifre scritte a mano:
PYTHON
# 2. CARICAMENTO DATASET MNIST
print("🔢 CARICAMENTO DATASET MNIST - CIFRE SCRITTE A MANO")
print("="*60)

# Carica dataset MNIST da Keras
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()

print(f"✅ Dataset MNIST caricato con successo!")
print(f"\n📊 INFORMAZIONI DATASET:")

# Informazioni dataset
print(f"  • X_train shape: {X_train.shape} → {X_train.shape[0]} immagini di {X_train.shape[1]}x{X_train.shape[2]} pixel")
print(f"  • y_train shape: {y_train.shape} → {y_train.shape[0]} etichette")
print(f"  • X_test shape: {X_test.shape} → {X_test.shape[0]} immagini di test")
print(f"  • y_test shape: {y_test.shape} → {y_test.shape[0]} etichette di test")

# Informazioni pixel
print(f"\n🎨 INFORMAZIONI PIXEL:")
print(f"  • Tipo dati: {X_train.dtype}")
print(f"  • Valore minimo pixel: {X_train.min()}")
print(f"  • Valore massimo pixel: {X_train.max()}")
print(f"  • Valore medio pixel: {X_train.mean():.2f}")

# Distribuzione classi
print(f"\n🎯 DISTRIBUZIONE CLASSI (CIFRE 0-9):")
unique, counts = np.unique(y_train, return_counts=True)
distribuzione_train = dict(zip(unique, counts))

unique_test, counts_test = np.unique(y_test, return_counts=True)
distribuzione_test = dict(zip(unique_test, counts_test))

print(f"\nSet di Training:")
for cifra in range(10):
    count = distribuzione_train[cifra]
    percentuale = (count / len(y_train)) * 100
    print(f"  • Cifra {cifra}: {count:5d} immagini ({percentuale:5.1f}%)")

print(f"\nSet di Test:")
for cifra in range(10):
    count = distribuzione_test[cifra]
    percentuale = (count / len(y_test)) * 100
    print(f"  • Cifra {cifra}: {count:5d} immagini ({percentuale:5.1f}%)")

# Visualizzazione esempi dataset
print(f"\n🎨 VISUALIZZAZIONE ESEMPI DATASET...")

fig, axes = plt.subplots(3, 10, figsize=(20, 6))
fig.suptitle('Esempi Dataset MNIST - Cifre da 0 a 9', fontsize=16, fontweight='bold')

for cifra in range(10):
    # Trova indici per questa cifra
    indices = np.where(y_train == cifra)[0]
    
    # Seleziona 3 immagini casuali per ogni cifra
    sample_indices = np.random.choice(indices, 3, replace=False)
    
    for row, idx in enumerate(sample_indices):
        ax = axes[row, cifra]
        ax.imshow(X_train[idx], cmap='gray', interpolation='nearest')
        ax.set_title(f'Cifra: {cifra}', fontsize=10)
        ax.axis('off')
        
        # Mostra valore pixel in alto a sinistra
        ax.text(0.05, 0.95, f'Pixel max: {X_train[idx].max()}', 
                transform=ax.transAxes, fontsize=8, verticalalignment='top',
                bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))

plt.tight_layout()
plt.show()

print("✅ Dataset esplorato e visualizzato correttamente!")

Parte 2: Preprocessing Dati per Deep Learning

▶ Normalizzazione e Preparazione Dati

3
Prepara i dati per le reti neurali:
PYTHON
# 3. PREPROCESSING DATI PER RETI NEURALI
print("🔧 PREPROCESSING DATI PER DEEP LEARNING")
print("="*60)

print("Applicando preprocessing in 4 passaggi...")

# 3.1 Normalizzazione pixel (da 0-255 a 0-1)
print("\n1. 🔢 NORMALIZZAZIONE PIXEL (0-255 → 0-1):")
print(f"   Prima: pixel range = [{X_train.min()}, {X_train.max()}]")

X_train_normalized = X_train.astype('float32') / 255.0
X_test_normalized = X_test.astype('float32') / 255.0

print(f"   Dopo: pixel range = [{X_train_normalized.min():.3f}, {X_train_normalized.max():.3f}]")

# 3.2 Reshape per reti neurali (aggiungi canale colore)
print("\n2. 📐 RESHAPE PER RETI NEURALI:")
print(f"   Prima shape: {X_train_normalized.shape}")

# Aggiungi dimensione canale (28, 28) → (28, 28, 1) per CNN
X_train_reshaped = X_train_normalized.reshape(-1, 28, 28, 1)
X_test_reshaped = X_test_normalized.reshape(-1, 28, 28, 1)

print(f"   Dopo shape: {X_train_reshaped.shape}")
print(f"   • -1: dimensioni automatiche (numero immagini)")
print(f"   • 28, 28: altezza, larghezza immagine")
print(f"   • 1: canale (grayscale)")

# 3.3 One-hot encoding delle etichette
print("\n3. 🔤 ONE-HOT ENCODING ETICHETTE:")
print(f"   Prima: y_train[0] = {y_train[0]} (tipo: {type(y_train[0])})")

# Converti etichette in one-hot encoding
y_train_onehot = keras.utils.to_categorical(y_train, 10)
y_test_onehot = keras.utils.to_categorical(y_test, 10)

print(f"   Dopo: y_train_onehot[0] = {y_train_onehot[0]}")
print(f"   • Lunghezza vettore: {len(y_train_onehot[0])}")
print(f"   • Posizione 1: index {np.argmax(y_train_onehot[0])}")

# 3.4 Creazione validation set
print("\n4. 🎯 CREAZIONE VALIDATION SET:")
print(f"   Training set originale: {X_train_reshaped.shape[0]} immagini")

# Usiamo 10% del training come validation
validation_split = 0.1
val_size = int(X_train_reshaped.shape[0] * validation_split)

# Mescola indici
indices = np.arange(X_train_reshaped.shape[0])
np.random.shuffle(indices)

# Split
X_val = X_train_reshaped[indices[:val_size]]
y_val = y_train_onehot[indices[:val_size]]
X_train_final = X_train_reshaped[indices[val_size:]]
y_train_final = y_train_onehot[indices[val_size:]]

print(f"   • Training set finale: {X_train_final.shape[0]} immagini")
print(f"   • Validation set: {X_val.shape[0]} immagini")
print(f"   • Test set: {X_test_reshaped.shape[0]} immagini")

# 3.5 Verifica preprocessing
print(f"\n✅ PREPROCESSING COMPLETATO!")
print(f"\n📊 RIEPILOGO DATASET FINALE:")
datasets = {
    'Training': (X_train_final, y_train_final),
    'Validation': (X_val, y_val),
    'Test': (X_test_reshaped, y_test_onehot)
}

for name, (X, y) in datasets.items():
    print(f"\n{name} Set:")
    print(f"  • Immagini: {X.shape[0]}")
    print(f"  • Dimensione immagine: {X.shape[1:]} (altezza, larghezza, canali)")
    print(f"  • Etichette: {y.shape}")
    print(f"  • Range pixel: [{X.min():.3f}, {X.max():.3f}]")
    
    # Distribuzione classi
    if len(y.shape) > 1:  # One-hot encoded
        y_labels = np.argmax(y, axis=1)
    else:
        y_labels = y
    
    unique, counts = np.unique(y_labels, return_counts=True)
    print(f"  • Distribuzione classi: {dict(zip(unique, counts))}")

# 3.6 Visualizzazione dati preprocessati
print(f"\n🎨 VISUALIZZAZIONE DATI PREPROCESSATI...")

fig, axes = plt.subplots(2, 5, figsize=(15, 6))

for i in range(5):
    # Immagine originale
    axes[0, i].imshow(X_train[i], cmap='gray')
    axes[0, i].set_title(f'Originale\nCifra: {y_train[i]}', fontsize=11)
    axes[0, i].axis('off')
    
    # Immagine preprocessata
    img_preprocessed = X_train_final[i].reshape(28, 28)
    axes[1, i].imshow(img_preprocessed, cmap='gray')
    axes[1, i].set_title(f'Preprocessata\nPixel: [{img_preprocessed.min():.2f}, {img_preprocessed.max():.2f}]', fontsize=11)
    axes[1, i].axis('off')

plt.suptitle('Confronto Immagini Originali vs Preprocessate', fontsize=14, y=1.02)
plt.tight_layout()
plt.show()

print("✅ Dati pronti per le reti neurali!")

Parte 3: Costruzione Rete Neurale Fully Connected

▶ Creazione Rete Neurale Dense (MLP)

4
Crea la tua prima rete neurale fully connected:
PYTHON
# 4. COSTRUZIONE RETE NEURALE FULLY CONNECTED
print("🔌 COSTRUZIONE RETE NEURALE FULLY CONNECTED (MLP)")
print("="*60)

print("🧱 Costruzione architettura rete neurale...")

# 4.1 Definisci architettura rete neurale
model_mlp = keras.Sequential([
    # Flatten layer: trasforma immagine 28x28 in vettore di 784 pixel
    layers.Flatten(input_shape=(28, 28, 1), name='flatten_input'),
    
    # Primo hidden layer: 128 neuroni con attivazione ReLU
    layers.Dense(128, activation='relu', name='hidden_layer_1'),
    
    # Dropout per prevenire overfitting (20% neuroni disattivati casualmente)
    layers.Dropout(0.2, name='dropout_1'),
    
    # Secondo hidden layer: 64 neuroni
    layers.Dense(64, activation='relu', name='hidden_layer_2'),
    
    # Dropout
    layers.Dropout(0.2, name='dropout_2'),
    
    # Terzo hidden layer: 32 neuroni
    layers.Dense(32, activation='relu', name='hidden_layer_3'),
    
    # Output layer: 10 neuroni (uno per ogni cifra) con attivazione softmax
    layers.Dense(10, activation='softmax', name='output_layer')
])

print(f"✅ Architettura rete neurale creata!")

# 4.2 Mostra architettura modello
print(f"\n📐 ARCHITETTURA MODELLO MLP:")
model_mlp.summary()

# 4.3 Calcolo parametri
print(f"\n🧮 CALCOLO PARAMETRI MODELLO:")
total_params = model_mlp.count_params()
print(f"  • Parametri totali: {total_params:,}")
print(f"  • Parametri trainable: {total_params:,}")  # Tutti i parametri sono trainable
print(f"  • Parametri non-trainable: 0")

# Spiegazione calcolo parametri
print(f"\n🔍 SPIEGAZIONE CALCOLO PARAMETRI:")
print("Layer Flatten → Dense(128):")
print(f"  • Input: 28x28x1 = 784 pixel")
print(f"  • Output: 128 neuroni")
print(f"  • Parametri: (784 * 128) + 128 bias = {784*128 + 128:,}")

print("\nLayer Dense(128) → Dense(64):")
print(f"  • Parametri: (128 * 64) + 64 bias = {128*64 + 64:,}")

print("\nLayer Dense(64) → Dense(32):")
print(f"  • Parametri: (64 * 32) + 32 bias = {64*32 + 32:,}")

print("\nLayer Dense(32) → Dense(10):")
print(f"  • Parametri: (32 * 10) + 10 bias = {32*10 + 10:,}")

# 4.4 Compilazione modello
print(f"\n⚙️  COMPILAZIONE MODELLO...")

model_mlp.compile(
    optimizer='adam',               # Ottimizzatore Adam (adaptive learning rate)
    loss='categorical_crossentropy', # Loss per classificazione multi-classe
    metrics=['accuracy']            # Metriche da monitorare
)

print(f"✅ Modello compilato con successo!")
print(f"  • Optimizer: Adam (learning rate adattivo)")
print(f"  • Loss function: Categorical Crossentropy")
print(f"  • Metrics: Accuracy")

# 4.5 Visualizzazione architettura
print(f"\n🎨 VISUALIZZAZIONE ARCHITETTURA RETE...")

try:
    # Prova a visualizzare con keras.utils.plot_model
    keras.utils.plot_model(
        model_mlp,
        to_file='model_mlp.png',
        show_shapes=True,
        show_layer_names=True,
        rankdir='TB',  # Top to Bottom
        expand_nested=False,
        dpi=96
    )
    
    # Mostra immagine
    from IPython.display import Image, display
    display(Image(filename='model_mlp.png', width=600))
    
    print("✅ Visualizzazione architettura generata!")
except Exception as e:
    print(f"⚠️  Impossibile generare visualizzazione: {e}")
    print("⚠️  Su Google Colab potrebbe servire graphviz installato")

# 4.6 Test forward pass (propagazione in avanti)
print(f"\n🔬 TEST FORWARD PASS (propagazione in avanti):")

# Seleziona una singola immagine
sample_image = X_train_final[0:1]  # Batch di 1 immagine
print(f"  • Input shape: {sample_image.shape}")
print(f"  • Input valori: [{sample_image.min():.3f}, {sample_image.max():.3f}]")

# Esegui forward pass
with tf.device('/CPU:0'):  # Forza CPU per test
    output = model_mlp.predict(sample_image, verbose=0)

print(f"  • Output shape: {output.shape}")
print(f"  • Output valori (probabilità):")
for i, prob in enumerate(output[0]):
    print(f"     Cifra {i}: {prob:.4f}")

predicted_class = np.argmax(output[0])
true_class = np.argmax(y_train_final[0])

print(f"  • Classe predetta: {predicted_class}")
print(f"  • Classe vera: {true_class}")
print(f"  • Corretta: {'✅' if predicted_class == true_class else '❌'}")

print("\n✅ Rete neurale MLP costruita con successo!")

Parte 4: Addestramento e Monitoraggio

▶ Training della Rete con Callbacks

5
Addestra la rete neurale con tecniche avanzate:
PYTHON
# 5. ADDESTRAMENTO RETE NEURALE
print("⚡ ADDESTRAMENTO RETE NEURALE MLP")
print("="*60)

# 5.1 Definizione callbacks per training avanzato
print("\n🎯 SETUP CALLBACKS PER TRAINING AVANZATO:")

callbacks_list = [
    # Early Stopping: ferma training se validation loss non migliora
    callbacks.EarlyStopping(
        monitor='val_loss',
        patience=5,           # Aspetta 5 epoch senza miglioramenti
        restore_best_weights=True,  # Ripristina pesi migliori
        verbose=1
    ),
    
    # ReduceLROnPlateau: riduce learning rate se stagnazione
    callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.5,           # Riduci LR del 50%
        patience=3,           # Aspetta 3 epoch
        min_lr=1e-6,          # LR minimo
        verbose=1
    ),
    
    # Model Checkpoint: salva miglior modello
    callbacks.ModelCheckpoint(
        filepath='best_mlp_model.keras',
        monitor='val_accuracy',
        save_best_only=True,
        save_weights_only=False,
        mode='max',
        verbose=1
    ),
    
    # TensorBoard: logging per visualizzazione (opzionale)
    # callbacks.TensorBoard(log_dir='./logs_mlp', histogram_freq=1)
]

print("✅ Callbacks configurati:")
print("  1. EarlyStopping: previene overfitting")
print("  2. ReduceLROnPlateau: adatta learning rate")
print("  3. ModelCheckpoint: salva miglior modello")
print("  4. TensorBoard: logging avanzato (opzionale)")

# 5.2 Training della rete
print(f"\n🚀 INIZIO ADDESTRAMENTO...")
print(f"  • Batch size: 64")
print(f"  • Epochs: 30")
print(f"  • Training samples: {X_train_final.shape[0]}")
print(f"  • Validation samples: {X_val.shape[0]}")

start_time = time.time()

history_mlp = model_mlp.fit(
    X_train_final,
    y_train_final,
    batch_size=64,
    epochs=30,
    validation_data=(X_val, y_val),
    callbacks=callbacks_list,
    verbose=1
)

training_time = time.time() - start_time
print(f"\n✅ Addestramento completato in {training_time:.1f} secondi!")
print(f"  • Epochs eseguite: {len(history_mlp.history['loss'])}")
print(f"  • Tempo per epoch: {training_time/len(history_mlp.history['loss']):.2f} secondi")

# 5.3 Carica miglior modello salvato
print(f"\n📂 CARICAMENTO MIGLIOR MODELLO SALVATO...")
try:
    best_model_mlp = keras.models.load_model('best_mlp_model.keras')
    print(f"✅ Miglior modello caricato da 'best_mlp_model.keras'")
    print(f"  • Validation accuracy miglior modello: {max(history_mlp.history['val_accuracy']):.4f}")
except:
    best_model_mlp = model_mlp
    print(f"⚠️  Impossibile caricare modello salvato, uso modello corrente")

# 5.4 Visualizzazione andamento training
print(f"\n📈 VISUALIZZAZIONE ANDAMENTO TRAINING...")

fig, axes = plt.subplots(1, 2, figsize=(15, 5))

# Plot accuracy
axes[0].plot(history_mlp.history['accuracy'], label='Training Accuracy', color='blue', linewidth=2)
axes[0].plot(history_mlp.history['val_accuracy'], label='Validation Accuracy', color='orange', linewidth=2)
axes[0].set_title('Accuracy durante Training', fontsize=14)
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Accuracy')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Aggiungi miglior accuracy
best_epoch = np.argmax(history_mlp.history['val_accuracy'])
best_acc = history_mlp.history['val_accuracy'][best_epoch]
axes[0].axvline(x=best_epoch, color='red', linestyle='--', alpha=0.7, 
                label=f'Best: {best_acc:.3f} (epoch {best_epoch})')
axes[0].legend()

# Plot loss
axes[1].plot(history_mlp.history['loss'], label='Training Loss', color='blue', linewidth=2)
axes[1].plot(history_mlp.history['val_loss'], label='Validation Loss', color='orange', linewidth=2)
axes[1].set_title('Loss durante Training', fontsize=14)
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# Aggiungi miglior loss
best_epoch_loss = np.argmin(history_mlp.history['val_loss'])
best_loss = history_mlp.history['val_loss'][best_epoch_loss]
axes[1].axvline(x=best_epoch_loss, color='red', linestyle='--', alpha=0.7,
                label=f'Best: {best_loss:.3f} (epoch {best_epoch_loss})')
axes[1].legend()

plt.tight_layout()
plt.show()

# 5.5 Valutazione finale su test set
print(f"\n🧪 VALUTAZIONE FINALE SU TEST SET...")

test_loss, test_accuracy = best_model_mlp.evaluate(
    X_test_reshaped, 
    y_test_onehot,
    verbose=0
)

print(f"✅ Valutazione completata!")
print(f"\n📊 PERFORMANCE FINALE MLP:")
print(f"  • Test Loss: {test_loss:.4f}")
print(f"  • Test Accuracy: {test_accuracy:.4f} ({test_accuracy*100:.2f}%)")

# Confronto con accuracy umana
print(f"\n👤 CONFRONTO CON PERFORMANCE UMANA:")
print(f"  • Accuracy rete neurale: {test_accuracy*100:.1f}%")
print(f"  • Accuracy umana tipica su MNIST: ~98-99%")
print(f"  • Differenza: {abs(test_accuracy*100 - 98.5):.1f}% punti")

if test_accuracy > 0.98:
    print(f"  🎉 La rete neurale supera/si avvicina alla performance umana!")
else:
    print(f"  🔧 Possibile spazio per miglioramenti...")

print("\n✅ Addestramento rete neurale completato!")

Parte 5: Rete Neurale Convoluzionale Avanzata

▶ Costruzione e Addestramento CNN

6
Crea una rete neurale convoluzionale (CNN) più potente:
PYTHON
# 6. RETE NEURALE CONVOLUZIONALE (CNN) AVANZATA
print("🎨 COSTRUZIONE RETE NEURALE CONVOLUZIONALE (CNN)")
print("="*60)

print("Le CNN sono specializzate per il riconoscimento immagini!")
print("Utilizzano operazioni di convoluzione per estrarre features spaziali.")

# 6.1 Costruzione architettura CNN
print("\n🧱 COSTRUZIONE ARCHITETTURA CNN...")

model_cnn = keras.Sequential([
    # Primo blocco convoluzionale
    layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1), padding='same', name='conv1'),
    layers.BatchNormalization(name='bn1'),
    layers.Conv2D(32, (3, 3), activation='relu', padding='same', name='conv2'),
    layers.MaxPooling2D((2, 2), name='pool1'),
    layers.Dropout(0.25, name='dropout1'),
    
    # Secondo blocco convoluzionale
    layers.Conv2D(64, (3, 3), activation='relu', padding='same', name='conv3'),
    layers.BatchNormalization(name='bn2'),
    layers.Conv2D(64, (3, 3), activation='relu', padding='same', name='conv4'),
    layers.MaxPooling2D((2, 2), name='pool2'),
    layers.Dropout(0.25, name='dropout2'),
    
    # Terzo blocco convoluzionale
    layers.Conv2D(128, (3, 3), activation='relu', padding='same', name='conv5'),
    layers.BatchNormalization(name='bn3'),
    layers.Dropout(0.25, name='dropout3'),
    
    # Flatten per passare a fully connected
    layers.Flatten(name='flatten_cnn'),
    
    # Fully connected layers
    layers.Dense(128, activation='relu', name='fc1'),
    layers.BatchNormalization(name='bn4'),
    layers.Dropout(0.5, name='dropout4'),
    
    layers.Dense(64, activation='relu', name='fc2'),
    layers.Dropout(0.5, name='dropout5'),
    
    # Output layer
    layers.Dense(10, activation='softmax', name='output_cnn')
])

print(f"✅ Architettura CNN creata!")

# 6.2 Mostra architettura CNN
print(f"\n📐 ARCHITETTURA MODELLO CNN:")
model_cnn.summary()

# 6.3 Compilazione CNN
print(f"\n⚙️  COMPILAZIONE CNN...")

# Ottimizzatore personalizzato
optimizer = keras.optimizers.Adam(learning_rate=0.001)

model_cnn.compile(
    optimizer=optimizer,
    loss='categorical_crossentropy',
    metrics=['accuracy']
)

print(f"✅ CNN compilata con ottimizzatore Adam (lr=0.001)")

# 6.4 Callbacks per CNN
print(f"\n🎯 SETUP CALLBACKS AVANZATI PER CNN...")

cnn_callbacks = [
    callbacks.EarlyStopping(
        monitor='val_accuracy',
        patience=10,
        restore_best_weights=True,
        verbose=1
    ),
    
    callbacks.ReduceLROnPlateau(
        monitor='val_loss',
        factor=0.5,
        patience=5,
        min_lr=1e-7,
        verbose=1
    ),
    
    callbacks.ModelCheckpoint(
        filepath='best_cnn_model.keras',
        monitor='val_accuracy',
        save_best_only=True,
        mode='max',
        verbose=1
    )
]

# 6.5 Addestramento CNN
print(f"\n🚀 ADDESTRAMENTO CNN (potrebbe richiedere qualche minuto)...")

start_time_cnn = time.time()

history_cnn = model_cnn.fit(
    X_train_final,
    y_train_final,
    batch_size=128,  # Batch più grande per CNN
    epochs=50,
    validation_data=(X_val, y_val),
    callbacks=cnn_callbacks,
    verbose=1
)

cnn_training_time = time.time() - start_time_cnn
print(f"\n✅ Addestramento CNN completato in {cnn_training_time:.1f} secondi!")

# 6.6 Carica miglior CNN
print(f"\n📂 CARICAMENTO MIGLIOR CNN...")
try:
    best_model_cnn = keras.models.load_model('best_cnn_model.keras')
    print(f"✅ Miglior CNN caricata!")
except:
    best_model_cnn = model_cnn
    print(f"⚠️  Uso CNN corrente")

# 6.7 Valutazione CNN su test set
print(f"\n🧪 VALUTAZIONE CNN SU TEST SET...")

cnn_test_loss, cnn_test_accuracy = best_model_cnn.evaluate(
    X_test_reshaped,
    y_test_onehot,
    verbose=0
)

print(f"✅ Valutazione CNN completata!")
print(f"\n📊 PERFORMANCE FINALE CNN:")
print(f"  • Test Loss: {cnn_test_loss:.4f}")
print(f"  • Test Accuracy: {cnn_test_accuracy:.4f} ({cnn_test_accuracy*100:.2f}%)")

# 6.8 Confronto MLP vs CNN
print(f"\n⚖️  CONFRONTO MLP vs CNN:")

comparison_data = {
    'Modello': ['MLP (Fully Connected)', 'CNN (Convolutional)'],
    'Test Accuracy': [test_accuracy, cnn_test_accuracy],
    'Test Accuracy %': [f"{test_accuracy*100:.2f}%", f"{cnn_test_accuracy*100:.2f}%"],
    'Training Time (s)': [f"{training_time:.1f}", f"{cnn_training_time:.1f}"],
    'Parametri': [f"{model_mlp.count_params():,}", f"{model_cnn.count_params():,}"],
    'Architettura': ['3 Dense Layers', '3 Conv Blocks + 2 Dense Layers']
}

comparison_df = pd.DataFrame(comparison_data)
print("\n" + comparison_df.to_string(index=False))

# 6.9 Visualizzazione risultati CNN
print(f"\n🎨 VISUALIZZAZIONE RISULTATI CNN...")

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# 1. Confronto accuracy MLP vs CNN
epochs_mlp = range(1, len(history_mlp.history['val_accuracy']) + 1)
epochs_cnn = range(1, len(history_cnn.history['val_accuracy']) + 1)

axes[0, 0].plot(epochs_mlp, history_mlp.history['val_accuracy'], 'b-', label='MLP', linewidth=2)
axes[0, 0].plot(epochs_cnn, history_cnn.history['val_accuracy'], 'r-', label='CNN', linewidth=2)
axes[0, 0].set_title('Confronto Validation Accuracy: MLP vs CNN')
axes[0, 0].set_xlabel('Epoch')
axes[0, 0].set_ylabel('Accuracy')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

# 2. Matrice confusione CNN
y_pred_cnn = best_model_cnn.predict(X_test_reshaped, verbose=0)
y_pred_classes = np.argmax(y_pred_cnn, axis=1)
y_true_classes = np.argmax(y_test_onehot, axis=1)

conf_matrix = tf.math.confusion_matrix(y_true_classes, y_pred_classes)
sns.heatmap(conf_matrix.numpy(), annot=True, fmt='d', cmap='Blues', ax=axes[0, 1])
axes[0, 1].set_title('Matrice Confusione CNN')
axes[0, 1].set_xlabel('Predetto')
axes[0, 1].set_ylabel('Vero')

# 3. Esempi predizioni corrette/errate
correct_indices = np.where(y_pred_classes == y_true_classes)[0]
wrong_indices = np.where(y_pred_classes != y_true_classes)[0]

# Seleziona esempi
correct_samples = np.random.choice(correct_indices, 3, replace=False)
wrong_samples = np.random.choice(wrong_indices, min(3, len(wrong_indices)), replace=False)

# Plot esempi corretti
for i, idx in enumerate(correct_samples[:3]):
    ax = axes[1, i]
    ax.imshow(X_test[idx], cmap='gray')
    ax.set_title(f'Corretto\nVero: {y_true_classes[idx]}, Pred: {y_pred_classes[idx]}', fontsize=11)
    ax.axis('off')

# Plot esempi errati
for i, idx in enumerate(wrong_samples[:3]):
    ax = axes[1, i+3] if i+3 < 3 else axes[1, 2]
    ax.imshow(X_test[idx], cmap='gray')
    ax.set_title(f'Errato\nVero: {y_true_classes[idx]}, Pred: {y_pred_classes[idx]}', fontsize=11)
    ax.axis('off')

plt.tight_layout()
plt.show()

# 6.10 Test su immagini personalizzate (simulate)
print(f"\n🔮 TEST SU IMMAGINI PERSONALIZZATE (SIMULATE)...")

# Crea alcune "immagini personalizzate" (in realtà dal test set ma presentate come nuove)
custom_test_indices = np.random.choice(len(X_test), 5, replace=False)

print("\n{:^5} | {:^25} | {:^15} | {:^10}".format(
    "Img", "Descrizione", "Predizione CNN", "Corretto?"))
print("-" * 60)

for i, idx in enumerate(custom_test_indices):
    # Prepara immagine
    img = X_test_reshaped[idx:idx+1]
    true_label = y_true_classes[idx]
    
    # Predizione CNN
    pred_proba = best_model_cnn.predict(img, verbose=0)[0]
    pred_label = np.argmax(pred_proba)
    confidence = pred_proba[pred_label] * 100
    
    # Descrizione
    if true_label in [0, 6, 8, 9]:
        descrizione = "Cifra con loop/chiusure"
    elif true_label in [1, 7]:
        descrizione = "Cifra con tratti retti"
    else:
        descrizione = "Cifra con curve"
    
    correct = "✅" if pred_label == true_label else "❌"
    
    print("{:^5} | {:^25} | {:^15} | {:^10}".format(
        f"#{i+1}", descrizione, f"{pred_label} ({confidence:.0f}%)", correct))

# 6.11 Salvataggio modelli
print(f"\n💾 SALVATAGGIO MODELLI PER USO FUTURO...")

# Salva CNN (il modello migliore)
best_model_cnn.save('mnist_cnn_final.keras')
print(f"✅ CNN salvata come 'mnist_cnn_final.keras'")

# Salva anche MLP per confronto
best_model_mlp.save('mnist_mlp_final.keras')
print(f"✅ MLP salvata come 'mnist_mlp_final.keras'")

print(f"\n🎉 ESERCITAZIONE COMPLETATA CON SUCCESSO!")
print("="*60)
print(f"🎯 RISULTATI FINALI:")
print(f"  • MLP Accuracy: {test_accuracy*100:.2f}%")
print(f"  • CNN Accuracy: {cnn_test_accuracy*100:.2f}%")
print(f"  • Miglioramento CNN vs MLP: {(cnn_test_accuracy - test_accuracy)*100:.2f}% punti")
print(f"\n💡 COSA HAI IMPARATO:")
print(f"  1. Le CNN sono superiori per riconoscimento immagini")
print(f"  2. La convoluzione estrae features spaziali")
print(f"  3. BatchNormalization stabilizza il training")
print(f"  4. Dropout previene l'overfitting")
print(f"  5. EarlyStopping ottimizza le epoche di training")
print(f"\n🚀 Prossimi passi: Prova a superare il 99.5% di accuracy con:")
print(f"  • Data augmentation")
print(f"  • Architetture più complesse (ResNet)")
print(f"  • Ensemble di più modelli")
print(f"  • Transfer learning")

Concetti di Deep Learning Appresi

Congratulazioni! Ora conosci:

  • TensorFlow & Keras: Framework più usato per Deep Learning
  • Dataset MNIST: Benchmark per riconoscimento cifre
  • Preprocessing Immagini: Normalizzazione, reshape, one-hot encoding
  • Reti Fully Connected (MLP): Layer Dense, attivazioni ReLU/Softmax
  • Reti Convoluzionali (CNN): Conv2D, Pooling, BatchNorm
  • Ottimizzatori: Adam con learning rate adattivo
  • Funzioni di Loss: Categorical Crossentropy per classificazione
  • Callbacks Avanzati: EarlyStopping, ReduceLROnPlateau, ModelCheckpoint
  • Regularizzazione: Dropout per prevenire overfitting
  • Batch Normalization: Stabilizzazione e accelerazione training
  • Valutazione Modelli: Accuracy, Confusion Matrix, confronto MLP vs CNN

Prossimi Passi nel Deep Learning

Nella prossima esercitazione imparerai:

  • 📸 CNN avanzate per riconoscimento oggetti
  • 🗣️ Reti neurali ricorrenti (RNN/LSTM) per testo
  • 🎨 Generative Adversarial Networks (GAN)
  • 🚀 Transfer learning con modelli pre-addestrati
  • 🧠 Reti neurali con attention e transformer

Hai costruito reti neurali che riconoscono cifre con accuratezza superiore al 99%!

Torna in alto