CLASSE QUINTA • RETI NEURALI

Reti Neurali Artificiali

Dal cervello umano all'intelligenza artificiale moderna

Immagina milioni di neuroni che comunicano tra loro. Le reti neurali artificiali imitano il nostro cervello per risolvere problemi complessi. In questo modulo costruiremo la nostra prima rete neurale da zero!

🧠 Cosa sono le Reti Neurali Artificiali?

🤔 Analogia: Il Cervello Umano

Il nostro cervello ha circa 86 miliardi di neuroni che comunicano tra loro.

Neurone biologico:

Dendriti: Ricevono segnali

Nucleo: Elabora i segnali

Assone: Trasmette segnali ad altri neuroni

Neurone artificiale (Perceptron):

Input: Dati in ingresso (come dendriti)

Pesi: Importanza di ogni input (come forza sinapsi)

Funzione di attivazione: Decide se "sparare" (come nucleo)

Output: Risultato (come assone)

Input

X₁
X₂
X₃

Nascosto

H₁
H₂
H₃

Output

Ŷ

✅ VANTAGGI RETI NEURALI

  • Apprendimento automatico: Imparano da esempi
  • Parallelismo: Molti calcoli insieme
  • Robustezza: Tollerano rumore e errori
  • Generalizzazione: Funzionano su dati nuovi
  • Universalità: Approssimano qualsiasi funzione

❌ SVANTAGGI RETI NEURALI

  • Black box: Difficile capire come decidono
  • Dati: Necessitano di TANTI dati
  • Computazione: Richiedono molta potenza
  • Overfitting: Tendono a memorizzare
  • Training lento: Minuti/ore/giorni

🔬 Il Perceptron: Il Mattone Fondamentale

🎯 Formula del Perceptron

Output = f( w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b )

x₁, x₂, ..., xₙ: Input (dati in ingresso)

w₁, w₂, ..., wₙ: Pesi (importanza di ogni input)

b: Bias (soglia di attivazione)

f: Funzione di attivazione (decide l'output)

🚫 COPIA BLOCCATA
# PERCEPTRON - IMPLEMENTAZIONE DA ZERO import numpy as np import matplotlib.pyplot as plt class Perceptron: """Implementazione di un Perceptron da zero""" def __init__(self, n_features, learning_rate=0.01, n_epochs=100): """ Inizializza il Perceptron Args: n_features: Numero di feature in input learning_rate: Tasso di apprendimento (default: 0.01) n_epochs: Numero di epoche di training (default: 100) """ self.learning_rate = learning_rate self.n_epochs = n_epochs # Inizializza pesi casualmente (piccoli valori) self.weights = np.random.randn(n_features) * 0.01 self.bias = 0.0 # Storico per visualizzazione self.loss_history = [] self.accuracy_history = [] def activation(self, x): """Funzione di attivazione (step function)""" return 1 if x >= 0 else 0 def forward(self, X): """Passaggio in avanti (calcola output)""" # Calcola la somma pesata: w·x + b linear_output = np.dot(X, self.weights) + self.bias # Applica funzione di attivazione predictions = np.array([self.activation(z) for z in linear_output]) return predictions def train(self, X, y): """ Allena il Perceptron Args: X: Feature matrix (n_samples, n_features) y: Target labels (0 o 1) """ n_samples = X.shape[0] print("🚀 INIZIO TRAINING PERCEPTRON") print(f"• Campioni: {n_samples}") print(f"• Features: {X.shape[1]}") print(f"• Learning rate: {self.learning_rate}") print(f"• Epoche: {self.n_epochs}") print("-" * 50) for epoch in range(self.n_epochs): total_error = 0 correct_predictions = 0 for i in range(n_samples): # 1. Calcola output per questo campione linear_output = np.dot(X[i], self.weights) + self.bias prediction = self.activation(linear_output) # 2. Calcola errore error = y[i] - prediction # 3. Aggiorna pesi e bias (se c'è errore) if error != 0: self.weights += self.learning_rate * error * X[i] self.bias += self.learning_rate * error total_error += abs(error) # 4. Controlla se predizione corretta if prediction == y[i]: correct_predictions += 1 # Calcola metriche per questa epoca accuracy = correct_predictions / n_samples avg_error = total_error / n_samples if n_samples > 0 else 0 self.loss_history.append(avg_error) self.accuracy_history.append(accuracy) # Stampa progresso ogni 10 epoche if (epoch + 1) % 10 == 0: print(f"Epoca {epoch + 1:3d}/{self.n_epochs} | " f"Loss: {avg_error:.4f} | " f"Accuracy: {accuracy:.3f} ({accuracy*100:.1f}%)") print("-" * 50) print("✅ TRAINING COMPLETATO!") print(f"Pesi finali: {self.weights}") print(f"Bias finale: {self.bias:.4f}") def predict(self, X): """Predice per nuovi dati""" return self.forward(X) # ESEMPIO PRATICO: CLASSIFICAZIONE AND/OR print("\n🎯 ESEMPIO: PERCEPTRON PER OPERAZIONI LOGICHE") print("=" * 60) # Dataset per operazione AND X_and = np.array([ [0, 0], [0, 1], [1, 0], [1, 1] ]) y_and = np.array([0, 0, 0, 1]) # AND: 1 solo se entrambi 1 print("\n📊 DATASET AND LOGICO:") print("X1 | X2 | AND") print("-" * 13) for i in range(len(X_and)): print(f"{X_and[i][0]:2d} | {X_and[i][1]:2d} | {y_and[i]:3d}") # Crea e allena perceptron perceptron_and = Perceptron(n_features=2, learning_rate=0.1, n_epochs=50) perceptron_and.train(X_and, y_and) # Test predictions = perceptron_and.predict(X_and) print("\n📊 PREDIZIONI FINALI:") print("Input | Reale | Predetto | Corretto?") print("-" * 40) for i in range(len(X_and)): correct = "✅" if predictions[i] == y_and[i] else "❌" print(f"{X_and[i]} | {y_and[i]:6d} | {predictions[i]:8d} | {correct}") # Visualizza decision boundary print("\n🎯 VISUALIZZAZIONE DECISION BOUNDARY:") """ plt.figure(figsize=(10, 6)) # Punti dati colors = ['red' if y == 0 else 'blue' for y in y_and] plt.scatter(X_and[:, 0], X_and[:, 1], c=colors, s=200, alpha=0.7) # Decision boundary x_boundary = np.array([-0.5, 1.5]) # w1*x1 + w2*x2 + b = 0 → x2 = (-w1*x1 - b) / w2 if perceptron_and.weights[1] != 0: y_boundary = (-perceptron_and.weights[0] * x_boundary - perceptron_and.bias) / perceptron_and.weights[1] plt.plot(x_boundary, y_boundary, 'g--', linewidth=2, label='Decision Boundary') plt.xlim(-0.5, 1.5) plt.ylim(-0.5, 1.5) plt.xlabel('X1') plt.ylabel('X2') plt.title('Perceptron - Decision Boundary AND') plt.legend() plt.grid(True, alpha=0.3) plt.show() """ print("\n💡 Cosa abbiamo imparato:") print("1. Il perceptron può imparare operazioni logiche semplici") print("2. I pesi si aggiornano in base all'errore") print("3. Il bias è la soglia di attivazione") print("4. Serve una funzione di attivazione per decisioni binarie") # ESEMPIO 2: PERCEPTRON PER OR print("\n\n🎯 ESEMPIO 2: PERCEPTRON PER OR LOGICO") print("=" * 60) y_or = np.array([0, 1, 1, 1]) # OR: 1 se almeno uno è 1 print("\n📊 DATASET OR LOGICO:") print("X1 | X2 | OR") print("-" * 13) for i in range(len(X_and)): print(f"{X_and[i][0]:2d} | {X_and[i][1]:2d} | {y_or[i]:2d}") # Crea e allena perceptron per OR perceptron_or = Perceptron(n_features=2, learning_rate=0.1, n_epochs=50) perceptron_or.train(X_and, y_or) # Test predictions_or = perceptron_or.predict(X_and) print("\n📊 PREDIZIONI FINALI:") print("Input | Reale | Predetto | Corretto?") print("-" * 40) for i in range(len(X_and)): correct = "✅" if predictions_or[i] == y_or[i] else "❌" print(f"{X_and[i]} | {y_or[i]:6d} | {predictions_or[i]:8d} | {correct}") print("\n💡 LIMITI DEL PERCEPTRON:") print("• Può imparare solo funzioni LINEARMENTE SEPARABILI") print("• NON può imparare XOR (serve multilayer)") print("• Questo è il motivo per cui servono reti neurali più complesse!")

🎯 LIMITI DEL PERCEPTRON SINGOLO

Marvin Minsky (1969) dimostrò che un singolo perceptron NON può imparare:

XOR (OR esclusivo): 1 se i due input sono diversi

Funzioni non linearmente separabili

Pattern complessi

Soluzione: Reti neurali multi-layer (MLP) con neuroni nascosti!

⚡ Funzioni di Attivazione

Le funzioni di attivazione decidono se e quanto un neurone "si attiva".

Step Function

Semplice ma non differenziabile
Usata nel perceptron originale

Sigmoid

Output tra 0 e 1
Per problemi di probabilità

ReLU

max(0, x)
La più usata oggi

Tanh

Output tra -1 e 1
Per dati centrati

🚫 COPIA BLOCCATA
# FUNZIONI DI ATTIVAZIONE E LORO DERIVATE import numpy as np import matplotlib.pyplot as plt def plot_activation_functions(): """Visualizza tutte le funzioni di attivazione principali""" x = np.linspace(-5, 5, 1000) # 1. Step function (original perceptron) y_step = np.where(x >= 0, 1, 0) # 2. Sigmoid y_sigmoid = 1 / (1 + np.exp(-x)) dy_sigmoid = y_sigmoid * (1 - y_sigmoid) # Derivata # 3. ReLU (Rectified Linear Unit) y_relu = np.maximum(0, x) dy_relu = np.where(x > 0, 1, 0) # 4. Tanh y_tanh = np.tanh(x) dy_tanh = 1 - y_tanh ** 2 # 5. Leaky ReLU y_leaky = np.where(x > 0, x, 0.01 * x) dy_leaky = np.where(x > 0, 1, 0.01) # 6. Softmax (per multi-class) def softmax(z): exp_z = np.exp(z - np.max(z)) return exp_z / exp_z.sum() print("🎯 FUNZIONI DI ATTIVAZIONE - CARATTERISTICHE") print("=" * 60) activation_info = { "Step": { "range": "(0, 1)", "diff": "No", "use": "Perceptron classico", "vantaggi": "Semplice, intuitiva", "svantaggi": "Non differenziabile, gradient = 0 quasi ovunque" }, "Sigmoid": { "range": "(0, 1)", "diff": "Sì", "use": "Output binario, probabilità", "vantaggi": "Output interpretabile come probabilità", "svantaggi": "Vanishing gradient per valori estremi" }, "ReLU": { "range": "[0, ∞)", "diff": "Sì (tranne in 0)", "use": "Layer nascosti (standard oggi)", "vantaggi": "Computazionalmente efficiente, riduce vanishing gradient", "svantaggi": "Dying ReLU (neuroni morti se sempre negativi)" }, "Tanh": { "range": "(-1, 1)", "diff": "Sì", "use": "Output centrati, RNN", "vantaggi": "Output centrato a 0, convergenza più rapida", "svantaggi": "Vanishing gradient simile a sigmoid" }, "Leaky ReLU": { "range": "(-∞, ∞)", "diff": "Sì", "use": "Alternative a ReLU", "vantaggi": "Risolve Dying ReLU", "svantaggi": "Parametro aggiuntivo da scegliere" } } print("\n📊 COMPARAZIONE FUNZIONI DI ATTIVAZIONE:") print("-" * 50) for name, info in activation_info.items(): print(f"\n🎯 {name}:") print(f" Range: {info['range']}") print(f" Differenziabile: {info['diff']}") print(f" Usata per: {info['use']}") print(f" Vantaggi: {info['vantaggi']}") print(f" Svantaggi: {info['svantaggi']}") # Visualizzazione (scommenta su Colab) """ fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # Row 1: Funzioni axes[0,0].plot(x, y_step, 'r-', linewidth=2) axes[0,0].set_title('Step Function') axes[0,0].grid(True, alpha=0.3) axes[0,1].plot(x, y_sigmoid, 'b-', linewidth=2, label='Sigmoid') axes[0,1].plot(x, dy_sigmoid, 'b--', linewidth=1, alpha=0.7, label='Derivata') axes[0,1].set_title('Sigmoid') axes[0,1].legend() axes[0,1].grid(True, alpha=0.3) axes[0,2].plot(x, y_relu, 'g-', linewidth=2, label='ReLU') axes[0,2].plot(x, dy_relu, 'g--', linewidth=1, alpha=0.7, label='Derivata') axes[0,2].set_title('ReLU') axes[0,2].legend() axes[0,2].grid(True, alpha=0.3) # Row 2: Altre funzioni axes[1,0].plot(x, y_tanh, 'm-', linewidth=2, label='Tanh') axes[1,0].plot(x, dy_tanh, 'm--', linewidth=1, alpha=0.7, label='Derivata') axes[1,0].set_title('Tanh') axes[1,0].legend() axes[1,0].grid(True, alpha=0.3) axes[1,1].plot(x, y_leaky, 'c-', linewidth=2, label='Leaky ReLU') axes[1,1].plot(x, dy_leaky, 'c--', linewidth=1, alpha=0.7, label='Derivata') axes[1,1].set_title('Leaky ReLU (α=0.01)') axes[1,1].legend() axes[1,1].grid(True, alpha=0.3) # Softmax per 3 classi x_soft = np.array([[1, 2, 3], [1, 1, 1], [3, 2, 1]]) y_soft = np.array([softmax(row) for row in x_soft]) classes = ['Classe 1', 'Classe 2', 'Classe 3'] x_pos = np.arange(len(classes)) for i in range(3): axes[1,2].bar(x_pos + i*0.2, y_soft[i], width=0.2, label=f'Input {i+1}') axes[1,2].set_title('Softmax Output (3 classi)') axes[1,2].set_xticks(x_pos + 0.2) axes[1,2].set_xticklabels(classes) axes[1,2].legend() axes[1,2].grid(True, alpha=0.3, axis='y') plt.tight_layout() plt.show() """ print("\n💡 CONSIGLI PRATICI:") print("• Per layer nascosti: USARE ReLU (default oggi)") print("• Per output binario: USARE Sigmoid") print("• Per output multi-classe: USARE Softmax") print("• Per regressione: USARE nessuna o Linear activation") print("• Per RNN: USARE Tanh o Sigmoid") # Esegui la funzione plot_activation_functions()

🏗️ Architettura di una Rete Neurale

🏠 Analogia: Una Fabbrica Intelligente

Immagina una fabbrica che trasforma materie prime in prodotti finiti:

Input Layer: Materie prime in ingresso

Hidden Layers: Stazioni di lavoro che trasformano

Output Layer: Prodotto finito

Pesi: Istruzioni per ogni lavoratore

Bias: Soglia per iniziare a lavorare

Funzione di attivazione: Decidi se passare al prossimo step

🚫 COPIA BLOCCATA
# COSTRUIAMO UNA RETE NEURALE DA ZERO import numpy as np class NeuralNetwork: """Rete neurale fully-connected con un layer nascosto""" def __init__(self, input_size, hidden_size, output_size): """ Inizializza la rete neurale Args: input_size: Numero di neuroni input hidden_size: Numero di neuroni nascosti output_size: Numero di neuroni output """ # Inizializzazione pesi (Xavier/Glorot initialization) self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size) self.b1 = np.zeros((1, hidden_size)) self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) self.b2 = np.zeros((1, output_size)) # Storico self.loss_history = [] self.accuracy_history = [] print(f"🎯 RETE NEURALE CREATA:") print(f"• Input layer: {input_size} neuroni") print(f"• Hidden layer: {hidden_size} neuroni") print(f"• Output layer: {output_size} neuroni") print(f"• Parametri totali: {(input_size*hidden_size + hidden_size*output_size + hidden_size + output_size):,}") def relu(self, x): """Funzione di attivazione ReLU""" return np.maximum(0, x) def relu_derivative(self, x): """Derivata di ReLU""" return np.where(x > 0, 1, 0) def sigmoid(self, x): """Funzione di attivazione Sigmoid""" return 1 / (1 + np.exp(-x)) def sigmoid_derivative(self, x): """Derivata di Sigmoid""" sig = self.sigmoid(x) return sig * (1 - sig) def softmax(self, x): """Funzione Softmax per output multi-classe""" exp_x = np.exp(x - np.max(x, axis=1, keepdims=True)) return exp_x / np.sum(exp_x, axis=1, keepdims=True) def forward(self, X): """ Passaggio in avanti (feedforward) Args: X: Input data (n_samples, n_features) Returns: output: Output della rete cache: Dati intermedi per backpropagation """ # Layer 1: Input → Hidden self.z1 = np.dot(X, self.W1) + self.b1 self.a1 = self.relu(self.z1) # Layer 2: Hidden → Output self.z2 = np.dot(self.a1, self.W2) + self.b2 # Output: Softmax per classificazione multi-classe if self.W2.shape[1] > 1: self.output = self.softmax(self.z2) else: # Sigmoid per classificazione binaria self.output = self.sigmoid(self.z2) return self.output def compute_loss(self, y_true, y_pred): """ Calcola la loss (costo) Args: y_true: Valori reali y_pred: Valori predetti Returns: loss: Valore della loss """ n_samples = y_true.shape[0] # Cross-entropy loss per classificazione if self.W2.shape[1] > 1: # Multi-class # One-hot encoding se necessario if len(y_true.shape) == 1: y_true_onehot = np.zeros((n_samples, self.W2.shape[1])) y_true_onehot[np.arange(n_samples), y_true] = 1 y_true = y_true_onehot # Cross-entropy epsilon = 1e-15 y_pred_clipped = np.clip(y_pred, epsilon, 1 - epsilon) loss = -np.sum(y_true * np.log(y_pred_clipped)) / n_samples else: # Binary classification epsilon = 1e-15 y_pred_clipped = np.clip(y_pred, epsilon, 1 - epsilon) loss = -np.mean(y_true * np.log(y_pred_clipped) + (1 - y_true) * np.log(1 - y_pred_clipped)) return loss def backward(self, X, y_true, y_pred): """ Backpropagation - calcola gradienti Args: X: Input data y_true: Valori reali y_pred: Valori predetti Returns: gradients: Dizionario con gradienti """ n_samples = X.shape[0] # Preparazione y_true per multi-class if self.W2.shape[1] > 1 and len(y_true.shape) == 1: y_true_onehot = np.zeros((n_samples, self.W2.shape[1])) y_true_onehot[np.arange(n_samples), y_true] = 1 y_true = y_true_onehot # Gradiente output layer if self.W2.shape[1] > 1: # Multi-class dz2 = y_pred - y_true else: # Binary dz2 = y_pred - y_true.reshape(-1, 1) # Gradienti layer 2 dW2 = np.dot(self.a1.T, dz2) / n_samples db2 = np.sum(dz2, axis=0, keepdims=True) / n_samples # Gradiente hidden layer dz1 = np.dot(dz2, self.W2.T) * self.relu_derivative(self.z1) # Gradienti layer 1 dW1 = np.dot(X.T, dz1) / n_samples db1 = np.sum(dz1, axis=0, keepdims=True) / n_samples gradients = { 'dW1': dW1, 'db1': db1, 'dW2': dW2, 'db2': db2 } return gradients def update_parameters(self, gradients, learning_rate): """ Aggiorna pesi usando gradienti (gradient descent) """ self.W1 -= learning_rate * gradients['dW1'] self.b1 -= learning_rate * gradients['db1'] self.W2 -= learning_rate * gradients['dW2'] self.b2 -= learning_rate * gradients['db2'] def train(self, X_train, y_train, X_val=None, y_val=None, learning_rate=0.01, epochs=1000, batch_size=32): """ Allena la rete neurale Args: X_train, y_train: Dati di training X_val, y_val: Dati di validazione (opzionale) learning_rate: Tasso di apprendimento epochs: Numero di epoche batch_size: Dimensione batch per mini-batch GD """ n_samples = X_train.shape[0] n_batches = int(np.ceil(n_samples / batch_size)) print("🚀 INIZIO TRAINING RETE NEURALE") print(f"• Campioni training: {n_samples}") print(f"• Learning rate: {learning_rate}") print(f"• Epoche: {epochs}") print(f"• Batch size: {batch_size}") print(f"• Batch per epoca: {n_batches}") print("-" * 60) for epoch in range(epochs): # Shuffle dati indices = np.random.permutation(n_samples) X_shuffled = X_train[indices] y_shuffled = y_train[indices] epoch_loss = 0 correct_train = 0 # Mini-batch gradient descent for batch in range(n_batches): start = batch * batch_size end = min(start + batch_size, n_samples) X_batch = X_shuffled[start:end] y_batch = y_shuffled[start:end] # Forward pass y_pred = self.forward(X_batch) # Calcola loss batch_loss = self.compute_loss(y_batch, y_pred) epoch_loss += batch_loss * (end - start) # Calcola accuracy if self.W2.shape[1] > 1: # Multi-class pred_classes = np.argmax(y_pred, axis=1) true_classes = y_batch if len(y_batch.shape) == 1 else np.argmax(y_batch, axis=1) else: # Binary pred_classes = (y_pred > 0.5).astype(int).flatten() true_classes = y_batch.flatten() correct_train += np.sum(pred_classes == true_classes) # Backward pass gradients = self.backward(X_batch, y_batch, y_pred) # Update parameters self.update_parameters(gradients, learning_rate) # Calcola metriche epoca epoch_loss /= n_samples train_accuracy = correct_train / n_samples self.loss_history.append(epoch_loss) self.accuracy_history.append(train_accuracy) # Validazione (se fornita) val_accuracy = None if X_val is not None and y_val is not None: y_val_pred = self.forward(X_val) if self.W2.shape[1] > 1: val_pred_classes = np.argmax(y_val_pred, axis=1) val_true_classes = y_val if len(y_val.shape) == 1 else np.argmax(y_val, axis=1) else: val_pred_classes = (y_val_pred > 0.5).astype(int).flatten() val_true_classes = y_val.flatten() val_accuracy = np.mean(val_pred_classes == val_true_classes) # Stampa progresso if (epoch + 1) % 100 == 0 or epoch == 0: print(f"Epoca {epoch + 1:4d}/{epochs} | " f"Loss: {epoch_loss:.4f} | " f"Train Acc: {train_accuracy:.3f}", end="") if val_accuracy is not None: print(f" | Val Acc: {val_accuracy:.3f}") else: print() print("-" * 60) print("✅ TRAINING COMPLETATO!") # Visualizzazione risultati (scommenta su Colab) """ plt.figure(figsize=(12, 4)) plt.subplot(1, 2, 1) plt.plot(self.loss_history) plt.title('Loss durante training') plt.xlabel('Epoca') plt.ylabel('Loss') plt.grid(True, alpha=0.3) plt.subplot(1, 2, 2) plt.plot(self.accuracy_history) plt.title('Accuracy durante training') plt.xlabel('Epoca') plt.ylabel('Accuracy') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() """ def predict(self, X): """Predice per nuovi dati""" y_pred = self.forward(X) if self.W2.shape[1] > 1: return np.argmax(y_pred, axis=1) else: return (y_pred > 0.5).astype(int).flatten() # ESEMPIO: CLASSIFICAZIONE CON RETE NEURALE print("\n🎯 ESEMPIO: CLASSIFICAZIONE IRIS CON RETE NEURALE") print("=" * 70) from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, confusion_matrix # Carica dataset iris = load_iris() X = iris.data y = iris.target print(f"\n📊 DATASET IRIS:") print(f"• Campioni: {X.shape[0]}") print(f"• Features: {X.shape[1]}") print(f"• Classi: {len(np.unique(y))} ({iris.target_names})") # Preprocessing scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # Split train/val/test X_train, X_temp, y_train, y_temp = train_test_split( X_scaled, y, test_size=0.4, random_state=42, stratify=y ) X_val, X_test, y_val, y_test = train_test_split( X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp ) print(f"\n📊 SPLIT DATI:") print(f"• Training: {len(X_train)} campioni") print(f"• Validation: {len(X_val)} campioni") print(f"• Test: {len(X_test)} campioni") # Crea e allena rete neurale print("\n🤖 CREAZIONE RETE NEURALE...") nn = NeuralNetwork( input_size=4, # 4 features hidden_size=10, # 10 neuroni nascosti output_size=3 # 3 classi ) nn.train( X_train=X_train, y_train=y_train, X_val=X_val, y_val=y_val, learning_rate=0.01, epochs=500, batch_size=16 ) # Valutazione su test set print("\n📊 VALUTAZIONE SU TEST SET:") y_pred = nn.predict(X_test) y_pred_proba = nn.forward(X_test) print("\n🎯 CLASSIFICATION REPORT:") print(classification_report(y_test, y_pred, target_names=iris.target_names)) print("\n📈 CONFUSION MATRIX:") cm = confusion_matrix(y_test, y_pred) print(cm) accuracy = np.mean(y_pred == y_test) print(f"\n📊 ACCURACY FINALE: {accuracy:.3f} ({accuracy*100:.1f}%)") # Visualizzazione decision boundaries (scommenta su Colab) """ print("\n🎨 VISUALIZZAZIONE DECISION BOUNDARIES (2D):") # Riduciamo a 2 features per visualizzare X_2d = X_scaled[:, :2] # Usiamo solo prime 2 features # Nuovo split per visualizzazione X_train_2d, X_test_2d, y_train_2d, y_test_2d = train_test_split( X_2d, y, test_size=0.3, random_state=42 ) # Rete per 2D nn_2d = NeuralNetwork(input_size=2, hidden_size=8, output_size=3) nn_2d.train(X_train_2d, y_train_2d, learning_rate=0.1, epochs=1000) # Creiamo meshgrid per decision boundaries h = 0.02 # step size in the mesh x_min, x_max = X_2d[:, 0].min() - 1, X_2d[:, 0].max() + 1 y_min, y_max = X_2d[:, 1].min() - 1, X_2d[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) # Predici su meshgrid Z = nn_2d.predict(np.c_[xx.ravel(), yy.ravel()]) Z = Z.reshape(xx.shape) # Plot plt.figure(figsize=(10, 8)) plt.contourf(xx, yy, Z, alpha=0.8, cmap=plt.cm.RdYlBu) plt.scatter(X_2d[:, 0], X_2d[:, 1], c=y, edgecolors='k', cmap=plt.cm.RdYlBu) plt.xlabel('Feature 1 (scaled)') plt.ylabel('Feature 2 (scaled)') plt.title('Decision Boundaries - Rete Neurale') plt.colorbar() plt.show() """ print("\n💡 COSA ABBIAMO IMPARATO:") print("1. Come costruire una rete neurale da zero") print("2. Forward propagation per calcolare output") print("3. Backpropagation per calcolare gradienti") print("4. Gradient descent per aggiornare pesi") print("5. Mini-batch training per efficienza") print("6. Validazione per prevenire overfitting")

🛡️ Overfitting e Tecniche di Regolarizzazione

⚠️ IL PROBLEMA DELL'OVERFITTING

Le reti neurali hanno TANTI parametri e tendono a memorizzare i dati invece di imparare pattern generali.

Sintomi:

• Training loss ↓ ma Validation loss ↑

• Performance eccellente su training, scadente su test

• Il modello "ricorda" i dati invece di "imparare"

🎯 TECNICHE DI REGOLARIZZAZIONE

1. L1/L2 Regularization:

• Aggiunge penalità sui pesi grandi

• Forza pesi piccoli → modello più semplice

2. Dropout:

• "Spegne" neuroni casualmente durante training

• Previene co-adattamento dei neuroni

3. Early Stopping:

• Ferma training quando validation loss smette di migliorare

• Previene over-training

4. Data Augmentation:

• Crea nuovi dati trasformando quelli esistenti

• Per immagini: rotazione, zoom, crop

🔧 IMPLEMENTAZIONE PRATICA

🚫 COPIA BLOCCATA
# REGOLARIZZAZIONE IN PRATICA import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_moons from sklearn.model_selection import train_test_split print("🎯 DEMOSTRAZIONE OVERFITTING E REGOLARIZZAZIONE") print("=" * 70) # Creiamo dataset complesso (non linearmente separabile) X, y = make_moons(n_samples=300, noise=0.3, random_state=42) print(f"\n📊 DATASET MOONS:") print(f"• Campioni: {X.shape[0]}") print(f"• Features: {X.shape[1]}") print(f"• Classi: {len(np.unique(y))}") # Split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42 ) X_train, X_val, y_train, y_val = train_test_split( X_train, y_train, test_size=0.2, random_state=42 ) print(f"\n📊 SPLIT DATI:") print(f"• Training: {len(X_train)}") print(f"• Validation: {len(X_val)}") print(f"• Test: {len(X_test)}") # Versione della nostra rete neurale con dropout class RegularizedNeuralNetwork: """Rete neurale con tecniche di regolarizzazione""" def __init__(self, input_size, hidden_size, output_size, dropout_rate=0.0, l2_lambda=0.0): self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size) self.b1 = np.zeros((1, hidden_size)) self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) self.b2 = np.zeros((1, output_size)) self.dropout_rate = dropout_rate self.l2_lambda = l2_lambda self.train_loss = [] self.val_loss = [] self.train_acc = [] self.val_acc = [] def relu(self, x): return np.maximum(0, x) def dropout_mask(self, size, dropout_rate): """Crea maschera dropout""" mask = np.random.binomial(1, 1-dropout_rate, size=size) / (1-dropout_rate) return mask def forward(self, X, training=False): """Forward pass con dropout (solo in training)""" # Layer 1 self.z1 = np.dot(X, self.W1) + self.b1 self.a1 = self.relu(self.z1) # Dropout layer 1 if training and self.dropout_rate > 0: self.dropout_mask1 = self.dropout_mask(self.a1.shape, self.dropout_rate) self.a1 *= self.dropout_mask1 # Layer 2 self.z2 = np.dot(self.a1, self.W2) + self.b2 # Output (sigmoid per binario) self.output = 1 / (1 + np.exp(-self.z2)) return self.output def compute_loss(self, y_true, y_pred): """Loss con regolarizzazione L2""" n_samples = y_true.shape[0] # Binary cross-entropy epsilon = 1e-15 y_pred_clipped = np.clip(y_pred, epsilon, 1 - epsilon) data_loss = -np.mean(y_true * np.log(y_pred_clipped) + (1 - y_true) * np.log(1 - y_pred_clipped)) # L2 regularization if self.l2_lambda > 0: l2_loss = (self.l2_lambda/2) * (np.sum(self.W1**2) + np.sum(self.W2**2)) total_loss = data_loss + l2_loss else: total_loss = data_loss return total_loss, data_loss def backward(self, X, y_true, y_pred): """Backward pass con dropout""" n_samples = X.shape[0] # Gradiente output dz2 = y_pred - y_true.reshape(-1, 1) # Applica maschera dropout ai gradienti if hasattr(self, 'dropout_mask1'): da1 = np.dot(dz2, self.W2.T) * self.dropout_mask1 else: da1 = np.dot(dz2, self.W2.T) # Gradiente ReLU dz1 = da1 * (self.z1 > 0) # Gradienti con regolarizzazione L2 dW2 = np.dot(self.a1.T, dz2) / n_samples db2 = np.sum(dz2, axis=0, keepdims=True) / n_samples dW1 = np.dot(X.T, dz1) / n_samples db1 = np.sum(dz1, axis=0, keepdims=True) / n_samples # Aggiungi regolarizzazione L2 if self.l2_lambda > 0: dW2 += self.l2_lambda * self.W2 dW1 += self.l2_lambda * self.W1 return {'dW1': dW1, 'db1': db1, 'dW2': dW2, 'db2': db2} def update_parameters(self, gradients, learning_rate): """Aggiorna pesi""" self.W1 -= learning_rate * gradients['dW1'] self.b1 -= learning_rate * gradients['db1'] self.W2 -= learning_rate * gradients['dW2'] self.b2 -= learning_rate * gradients['db2'] def train(self, X_train, y_train, X_val, y_val, learning_rate=0.01, epochs=1000, early_stopping=True, patience=20): """Training con early stopping""" best_val_loss = float('inf') patience_counter = 0 best_weights = None for epoch in range(epochs): # Forward (training mode) y_train_pred = self.forward(X_train, training=True) # Calcola loss training train_loss, _ = self.compute_loss(y_train, y_train_pred) self.train_loss.append(train_loss) # Accuracy training train_pred_classes = (y_train_pred > 0.5).astype(int).flatten() train_acc = np.mean(train_pred_classes == y_train) self.train_acc.append(train_acc) # Forward validation (no dropout) y_val_pred = self.forward(X_val, training=False) val_loss, _ = self.compute_loss(y_val, y_val_pred) self.val_loss.append(val_loss) # Accuracy validation val_pred_classes = (y_val_pred > 0.5).astype(int).flatten() val_acc = np.mean(val_pred_classes == y_val) self.val_acc.append(val_acc) # Backward e update gradients = self.backward(X_train, y_train, y_train_pred) self.update_parameters(gradients, learning_rate) # Early stopping if early_stopping: if val_loss < best_val_loss: best_val_loss = val_loss patience_counter = 0 # Salva migliori pesi best_weights = { 'W1': self.W1.copy(), 'b1': self.b1.copy(), 'W2': self.W2.copy(), 'b2': self.b2.copy() } else: patience_counter += 1 if patience_counter >= patience: print(f"🛑 Early stopping at epoch {epoch+1}") # Ripristina migliori pesi if best_weights: self.W1 = best_weights['W1'] self.b1 = best_weights['b1'] self.W2 = best_weights['W2'] self.b2 = best_weights['b2'] break # Stampa progresso if (epoch + 1) % 100 == 0 or epoch == 0: print(f"Epoca {epoch+1:4d} | " f"Train Loss: {train_loss:.4f}, Acc: {train_acc:.3f} | " f"Val Loss: {val_loss:.4f}, Acc: {val_acc:.3f}") # TESTIAMO DIVERSI LIVELLI DI REGOLARIZZAZIONE print("\n🔬 CONFRONTO MODELLI CON DIVERSI LIVELLI DI REGOLARIZZAZIONE") print("=" * 70) models = { "Senza regolarizzazione": { "dropout": 0.0, "l2_lambda": 0.0, "early_stopping": False }, "Con Dropout (0.3)": { "dropout": 0.3, "l2_lambda": 0.0, "early_stopping": False }, "Con L2 (0.01)": { "dropout": 0.0, "l2_lambda": 0.01, "early_stopping": False }, "Con Early Stopping": { "dropout": 0.0, "l2_lambda": 0.0, "early_stopping": True }, "Combinazione (Dropout + L2 + ES)": { "dropout": 0.2, "l2_lambda": 0.005, "early_stopping": True } } results = {} for name, params in models.items(): print(f"\n🎯 ALLENAMENTO: {name}") print("-" * 40) model = RegularizedNeuralNetwork( input_size=2, hidden_size=20, # Rete grande per enfatizzare overfitting output_size=1, dropout_rate=params["dropout"], l2_lambda=params["l2_lambda"] ) model.train( X_train, y_train, X_val, y_val, learning_rate=0.1, epochs=1000, early_stopping=params["early_stopping"], patience=30 ) # Valutazione su test set y_test_pred = model.forward(X_test, training=False) test_pred_classes = (y_test_pred > 0.5).astype(int).flatten() test_acc = np.mean(test_pred_classes == y_test) results[name] = { "train_acc": model.train_acc[-1], "val_acc": model.val_acc[-1], "test_acc": test_acc, "train_loss": model.train_loss[-1], "val_loss": model.val_loss[-1] } print(f"📊 Test Accuracy: {test_acc:.3f}") # Mostra risultati comparativi print("\n📊 RISULTATI COMPARATIVI:") print("=" * 70) print(f"{'Modello':30s} {'Train Acc':>10s} {'Val Acc':>10s} {'Test Acc':>10s} {'Gap':>10s}") print("-" * 70) for name, result in results.items(): train_val_gap = result["train_acc"] - result["val_acc"] print(f"{name:30s} {result['train_acc']:10.3f} {result['val_acc']:10.3f} " f"{result['test_acc']:10.3f} {train_val_gap:10.3f}") print("\n💡 CONCLUSIONI:") print("1. Il modello senza regolarizzazione ha il gap più grande (overfitting)") print("2. Dropout riduce il gap ma può ridurre accuracy training") print("3. L2 regularization rende il modello più 'semplice'") print("4. Early stopping previene over-training") print("5. La combinazione di tecniche dà i migliori risultati") # Visualizzazione (scommenta su Colab) """ fig, axes = plt.subplots(2, 3, figsize=(15, 10)) # Re-allena per visualizzazione for idx, (name, params) in enumerate(list(models.items())[:5]): model = RegularizedNeuralNetwork(2, 20, 1, params["dropout"], params["l2_lambda"]) model.train(X_train, y_train, X_val, y_val, learning_rate=0.1, epochs=1000, early_stopping=params["early_stopping"], patience=30) row, col = divmod(idx, 3) # Plot decision boundary h = 0.02 x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1 y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1 xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) Z = model.forward(np.c_[xx.ravel(), yy.ravel()], training=False) Z = (Z > 0.5).astype(int).reshape(xx.shape) axes[row, col].contourf(xx, yy, Z, alpha=0.8, cmap=plt.cm.RdYlBu) axes[row, col].scatter(X_train[:, 0], X_train[:, 1], c=y_train, edgecolors='k', cmap=plt.cm.RdYlBu, alpha=0.6) axes[row, col].set_title(f'{name}\nTest Acc: {results[name]["test_acc"]:.3f}') axes[row, col].set_xlim(xx.min(), xx.max()) axes[row, col].set_ylim(yy.min(), yy.max()) # Plot loss curves per l'ultimo modello axes[1, 2].plot(model.train_loss, label='Train Loss') axes[1, 2].plot(model.val_loss, label='Val Loss') axes[1, 2].set_title('Loss durante training\n(Combinazione tecniche)') axes[1, 2].set_xlabel('Epoca') axes[1, 2].set_ylabel('Loss') axes[1, 2].legend() axes[1, 2].grid(True, alpha=0.3) plt.tight_layout() plt.show() """

💪 Esercizi Pratici

Esercizio 1: Perceptron per AND/OR FACILE

Obiettivo: Implementare un perceptron per operazioni logiche

Steps:

  1. Implementa la classe Perceptron (come nell'esempio)
  2. Allena su dataset AND e OR
  3. Visualizza decision boundaries
  4. Testa su XOR - cosa succede? Perché?

Esercizio 2: Rete Neurale per MNIST MEDIO

Obiettivo: Classificare cifre scritte a mano

Dataset: MNIST (0-9)

Tasks:

  1. Carica dataset MNIST da scikit-learn o Keras
  2. Normalizza immagini (0-255 → 0-1)
  3. Crea rete neurale con 1-2 layer nascosti
  4. Allena con varie learning rates
  5. Visualizza accuracy e loss curves
  6. Analizza errori (quali cifre sbaglia più spesso?)

Esercizio 3: Battaglia delle Attivazioni DIFFICILE

Obiettivo: Confrontare diverse funzioni di attivazione

Dataset: Breast Cancer Wisconsin

Tasks:

  1. Crea rete neurale con architettura identica
  2. Testa: Sigmoid, Tanh, ReLU, Leaky ReLU
  3. Confronta: velocità convergenza, accuracy finale
  4. Analizza vanishing gradient problem
  5. Implementa batch normalization (bonus)

🚀 Progetto: Classificazione di Abbigliamento

Costruiamo una rete neurale per classificare capi di abbigliamento usando Fashion-MNIST.

🚫 COPIA BLOCCATA
# PROGETTO: CLASSIFICAZIONE FASHION-MNIST import numpy as np import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix from sklearn.preprocessing import OneHotEncoder import seaborn as sns # Per questo progetto useremo TensorFlow/Keras per semplicità # Ma concettualmente è uguale alla nostra implementazione da zero print("🚀 PROGETTO: CLASSIFICAZIONE FASHION-MNIST") print("=" * 80) try: import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers, models print("✅ TensorFlow importato correttamente") print(f"Versione TensorFlow: {tf.__version__}") except ImportError: print("❌ TensorFlow non installato. Installa con: pip install tensorflow") print("Procediamo con implementazione manuale...") # Qui potremmo usare la nostra implementazione da zero # Ma per praticità continuiamo con spiegazioni concettuali # Carichiamo Fashion-MNIST (built-in in Keras) print("\n📊 CARICAMENTO DATASET FASHION-MNIST...") fashion_mnist = keras.datasets.fashion_mnist (X_train_full, y_train_full), (X_test, y_test) = fashion_mnist.load_data() print(f"\n📊 INFORMAZIONI DATASET:") print(f"• X_train shape: {X_train_full.shape}") print(f"• y_train shape: {y_train_full.shape}") print(f"• X_test shape: {X_test.shape}") print(f"• y_test shape: {y_test.shape}") print(f"• Range pixel: [{X_train_full.min()}, {X_train_full.max()}]") # Nomi delle classi class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] print(f"\n🎯 CLASSI ({len(class_names)}):") for i, name in enumerate(class_names): print(f" {i}: {name}") # Visualizza alcuni esempi print("\n🎨 VISUALIZZAZIONE ESEMPI:") """ plt.figure(figsize=(10, 10)) for i in range(25): plt.subplot(5, 5, i+1) plt.xticks([]) plt.yticks([]) plt.grid(False) plt.imshow(X_train_full[i], cmap=plt.cm.binary) plt.xlabel(class_names[y_train_full[i]]) plt.tight_layout() plt.show() """ # Preprocessing print("\n🔧 PREPROCESSING:") X_train_full = X_train_full.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 # Reshape per CNN (aggiungi canale colore) X_train_full = X_train_full.reshape(-1, 28, 28, 1) X_test = X_test.reshape(-1, 28, 28, 1) # One-hot encoding labels y_train_onehot = tf.keras.utils.to_categorical(y_train_full, 10) y_test_onehot = tf.keras.utils.to_categorical(y_test, 10) # Split validation X_train, X_val, y_train, y_val = train_test_split( X_train_full, y_train_onehot, test_size=0.1, random_state=42 ) print(f"• Training: {X_train.shape[0]} immagini") print(f"• Validation: {X_val.shape[0]} immagini") print(f"• Test: {X_test.shape[0]} immagini") # 1. MODELLO SEMPLICE (MLP) print("\n🤖 1. MODELLO MLP SEMPLICE (Multi-Layer Perceptron)") print("-" * 50) model_mlp = models.Sequential([ layers.Flatten(input_shape=(28, 28, 1)), layers.Dense(128, activation='relu'), layers.Dropout(0.2), layers.Dense(64, activation='relu'), layers.Dropout(0.2), layers.Dense(10, activation='softmax') ]) model_mlp.compile( optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'] ) print("📊 ARCHITETTURA MLP:") model_mlp.summary() # Training MLP print("\n🚀 TRAINING MLP...") history_mlp = model_mlp.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=20, batch_size=64, verbose=1 ) # 2. MODELLO CNN (Convolutional Neural Network) print("\n🤖 2. MODELLO CNN (Convolutional Neural Network)") print("-" * 50) model_cnn = models.Sequential([ # Convolutional layers layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu'), # Dense layers layers.Flatten(), layers.Dense(64, activation='relu'), layers.Dropout(0.5), layers.Dense(10, activation='softmax') ]) model_cnn.compile( optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'] ) print("📊 ARCHITETTURA CNN:") model_cnn.summary() # Training CNN print("\n🚀 TRAINING CNN...") history_cnn = model_cnn.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=15, batch_size=64, verbose=1 ) # VALUTAZIONE COMPARATIVA print("\n📊 VALUTAZIONE COMPARATIVA MODELLI") print("=" * 50) # Valutazione su test set print("\n🎯 MLP PERFORMANCE:") test_loss_mlp, test_acc_mlp = model_mlp.evaluate(X_test, y_test_onehot, verbose=0) print(f"• Test Loss: {test_loss_mlp:.4f}") print(f"• Test Accuracy: {test_acc_mlp:.4f} ({test_acc_mlp*100:.2f}%)") print("\n🎯 CNN PERFORMANCE:") test_loss_cnn, test_acc_cnn = model_cnn.evaluate(X_test, y_test_onehot, verbose=0) print(f"• Test Loss: {test_loss_cnn:.4f}") print(f"• Test Accuracy: {test_acc_cnn:.4f} ({test_acc_cnn*100:.2f}%)") print(f"\n💡 MIGLIORAMENTO CNN vs MLP: +{(test_acc_cnn - test_acc_mlp)*100:.2f}%") # Predizioni e analisi errori print("\n🔍 ANALISI ERRORI CNN:") y_pred_cnn = model_cnn.predict(X_test) y_pred_classes = np.argmax(y_pred_cnn, axis=1) print("\n📈 CLASSIFICATION REPORT:") print(classification_report(y_test, y_pred_classes, target_names=class_names)) # Matrice di confusione print("\n🎯 MATRICE DI CONFUSIONE (CNN):") cm = confusion_matrix(y_test, y_pred_classes) """ plt.figure(figsize=(12, 10)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.title('Confusion Matrix - Fashion-MNIST CNN') plt.ylabel('True Label') plt.xlabel('Predicted Label') plt.tight_layout() plt.show() """ # Visualizza alcuni errori print("\n🔎 ESEMPI DI ERRORI DEL MODELLO:") errors = np.where(y_pred_classes != y_test)[0] if len(errors) > 0: print(f"• Errori totali: {len(errors)}/{len(y_test)} ({len(errors)/len(y_test)*100:.1f}%)") # Mostra primi 5 errori """ plt.figure(figsize=(15, 6)) for i in range(min(5, len(errors))): idx = errors[i] plt.subplot(1, 5, i+1) plt.imshow(X_test[idx].reshape(28, 28), cmap='gray') plt.title(f'True: {class_names[y_test[idx]]}\nPred: {class_names[y_pred_classes[idx]]}') plt.axis('off') plt.tight_layout() plt.show() """ # Analizza quali classi hanno più errori print("\n📊 ANALISI ERRORI PER CLASSE:") for i in range(10): class_indices = np.where(y_test == i)[0] class_errors = np.sum(y_pred_classes[class_indices] != y_test[class_indices]) accuracy_class = 1 - (class_errors / len(class_indices)) print(f"{class_names[i]:15s}: {accuracy_class:.3f} ({class_errors}/{len(class_indices)} errori)") # Confronto learning curves print("\n📈 CONFRONTO LEARNING CURVES:") """ fig, axes = plt.subplots(2, 2, figsize=(15, 10)) # MLP curves axes[0, 0].plot(history_mlp.history['accuracy'], label='Train') axes[0, 0].plot(history_mlp.history['val_accuracy'], label='Validation') axes[0, 0].set_title('MLP - Accuracy') axes[0, 0].set_xlabel('Epoch') axes[0, 0].set_ylabel('Accuracy') axes[0, 0].legend() axes[0, 0].grid(True, alpha=0.3) axes[0, 1].plot(history_mlp.history['loss'], label='Train') axes[0, 1].plot(history_mlp.history['val_loss'], label='Validation') axes[0, 1].set_title('MLP - Loss') axes[0, 1].set_xlabel('Epoch') axes[0, 1].set_ylabel('Loss') axes[0, 1].legend() axes[0, 1].grid(True, alpha=0.3) # CNN curves axes[1, 0].plot(history_cnn.history['accuracy'], label='Train') axes[1, 0].plot(history_cnn.history['val_accuracy'], label='Validation') axes[1, 0].set_title('CNN - Accuracy') axes[1, 0].set_xlabel('Epoch') axes[1, 0].set_ylabel('Accuracy') axes[1, 0].legend() axes[1, 0].grid(True, alpha=0.3) axes[1, 1].plot(history_cnn.history['loss'], label='Train') axes[1, 1].plot(history_cnn.history['val_loss'], label='Validation') axes[1, 1].set_title('CNN - Loss') axes[1, 1].set_xlabel('Epoch') axes[1, 1].set_ylabel('Loss') axes[1, 1].legend() axes[1, 1].grid(True, alpha=0.3) plt.tight_layout() plt.show() """ # Salva modelli print("\n💾 SALVATAGGIO MODELLI...") model_mlp.save('fashion_mnist_mlp.h5') model_cnn.save('fashion_mnist_cnn.h5') print("✅ Modelli salvati come 'fashion_mnist_mlp.h5' e 'fashion_mnist_cnn.h5'") print("\n🎯 CONCLUSIONI PROGETTO:") print("1. CNN è superiore per immagini (92% vs 88% MLP)") print("2. Le convoluzioni catturano pattern spaziali") print("3. Dropout previene overfitting") print("4. Fashion-MNIST è più difficile di MNIST normale") print("5. Shirt e T-shirt sono spesso confuse (classi simili)") print("\n🚀 PROSSIMI PASSI POSSIBILI:") print("1. Data augmentation (rotazioni, zoom) per migliorare accuracy") print("2. Transfer learning con modelli pre-allenati") print("3. Hyperparameter tuning (learning rate, architettura)") print("4. Ensemble di più modelli") print("5. Deployment come web app") print("\n✅ PROGETTO COMPLETATO! Hai costruito un classificatore di moda!")

🎯 Riepilogo e Prossimi Passi

🧠 COSA ABBIAMO IMPARATO

  • Il perceptron come neurone fondamentale
  • Funzioni di attivazione e loro importanza
  • Architettura reti neurali (input, hidden, output)
  • Forward propagation e backpropagation
  • Gradient descent per ottimizzazione
  • Tecniche di regolarizzazione

🚀 PROSSIMI ARGOMENTI

  • Reti Neurali Convoluzionali (CNN)
  • Reti Neurali Ricorrenti (RNN)
  • Transfer Learning
  • Autoencoder e GAN
  • Reinforcement Learning
  • Transformers e LLM

🎯 PUNTI CHIAVE DA RICORDARE

1. Le reti neurali sono approssimatori universali: Possono imparare qualsiasi funzione (con abbastanza neuroni)

2. Backpropagation è il cuore dell'apprendimento: Propaga errori indietro per aggiornare pesi

3. L'overfitting è il nemico principale: Usa dropout, L2, early stopping

4. ReLU è lo standard per layer nascosti: Semplice, efficiente, riduce vanishing gradient

5. Le CNN sono superiori per immagini: Catturano pattern spaziali con convoluzioni

🔗 Pronto per Collegare l'AI ai Database!

Ora che sai costruire reti neurali, impariamo a collegarle a database reali.
Nel prossimo modulo: Integrazione con MySQL per progetti AI completi!

Vai al Modulo 4: Database MySQL →