Esercizio - Rete feed-forward su dati tabulari sbilanciati (appello)
Questa pagina non ha ancora la versione ripasso: qui sotto c'è il testo completo.
In questa pagina 4
Testo (parte pratica del secondo appello, luglio 2025). Un dataset simula i dati dei sensori di un macchinario industriale. Si vuole prevedere se la macchina avrà un guasto entro due settimane (will_fail). Le colonne sono: motor_temp_celsius e motor_temp_kelvin (temperatura del motore), coolant_temp (di solito segue quella del motore), vibration_level, ambient_humidity, operating_mode (Idle, Auto, Manual), error_count_24h, maintenance_flag (Base, Done, Overdue), power_consumption. Si usano righe.
- Esplorare il dataset e commentare perché ogni analisi serve al modello.
- Eseguire il preprocessing necessario, motivando le scelte.
- Allenare una rete feed-forward: ReLU negli strati nascosti, un neurone di uscita con sigmoide (probabilità della classe ), Adam con learning rate , loss
binary_crossentropy; architettura, epoche e batch a scelta, framework a scelta (Keras o PyTorch). Dividere in training e test (senza cross-validation) e valutare con metriche adeguate. - (Bonus) Bilanciare precision e recall con una loss pesata.
Teoria usata: Reti neurali - neuroni e funzioni di attivazioneUn neurone calcola $\hat y=g(w_0+w^\top x)$: somma pesata degli ingressi più bias, poi una funzione di attivazione $g$ non lineare (Perceptron a soglia, sigmoide, tanh, ReLU e varianti). Senza non linearità ogni rete è equivalente a un solo modello lineare. Una rete feed-forward impila strati di neuroni: $a^{(k)}=g(W^{(k)}a^{(k-1)}+b^{(k)})$; con uno strato nascosto è già un approssimatore universale, ma più strati rappresentano funzioni complesse con molti meno neuroni e imparano feature gerarchiche (bordi, parti, oggetti). Lo strato di uscita e la loss si scelgono dal compito: lineare+MSE (regressione), sigmoide+cross-entropy binaria, softmax+cross-entropy (multiclasse). Il numero di parametri di uno strato denso è $n_{in}n_{out}+n_{out}$. Nel lab (Keras, MNIST) una rete 784-512-10 ha 407 050 parametri e supera il 98% di accuratezza.Reti neurali - neuroni e funzioni di attivazione →, Regolarizzazione delle reti neuraliUna rete con tanti parametri tende a memorizzare il training set (overfitting): la loss di training scende ma quella di validazione risale. Le tecniche di regolarizzazione limitano la capacità effettiva: penalità sui pesi ($\ell_2$: $J+\lambda|W|_2^2$, il passo diventa $W\leftarrow(1-2\eta\lambda)W-\eta\nabla J$; $\ell_1$: $J+\lambda|W|_1$, porta pesi esattamente a zero), early stopping (si ferma l'addestramento quando la validation loss smette di scendere, con pazienza e ripristino dei pesi migliori), dropout (in training si azzera a caso una frazione $p$ delle attivazioni e si riscala per $1/(1-p)$; in inferenza è spento), batch normalization (effetto collaterale) e data augmentation. Nel lab MNIST con $\lambda=0{,}01$ la penalità $\ell_1$ è troppo forte (accuratezza di test 0,844 contro 0,9815 senza regolarizzazione), mentre dropout e early stopping non peggiorano e tengono la validation loss più bassa.Regolarizzazione delle reti neurali →, Metriche di classificazioneIn classificazione binaria ogni previsione è vero positivo (TP), vero negativo (TN), falso positivo (FP, errore di tipo I) o falso negativo (FN, errore di tipo II). Da queste quattro quantità: accuracy $=\frac{TP+TN}{TP+TN+FP+FN}$, specificità $=\frac{TN}{TN+FP}$, precision $=\frac{TP}{TP+FP}$, recall $=\frac{TP}{TP+FN}$, e la loro media armonica $F_1=\frac{2PR}{P+R}$. Con dati sbilanciati l'accuracy inganna (un modello che predice sempre la classe maggioritaria ha 99%): si usano precision, recall, F1, ROC-AUC, la cross-validation stratificata e il riequilibrio con undersampling o oversampling (non SMOTE). Cambiando la soglia sulla probabilità si ottiene la curva ROC (TPR contro FPR) e l'area AUC. Approfondimento: non nel programma di Telecomunicazioni.Metriche di classificazione →, Correlazione e visualizzazione dei datiLa correlazione di Pearson $r=\sum(X_i-\bar X)(Y_i-\bar Y)/\big(\sqrt{\sum(X_i-\bar X)^2}\sqrt{\sum(Y_i-\bar Y)^2}\big)\in[-1,1]$ misura la relazione lineare tra due variabili (covarianza divisa per le deviazioni standard); correlazione non implica causalità. Serve a capire quali variabili contano per il target e a eliminare quelle quasi duplicate (|r| molto alto). Gli indicatori di sintesi non bastano (quartetto di Anscombe, Datasaurus): vanno affiancati ai grafici: istogramma, KDE, box plot, violin plot, heatmap di correlazione, scatter plot e matrice di scatter plot.Correlazione e visualizzazione dei dati →.
1. Esplorazione
Cosa si guarda e cosa si conclude:
- Valori mancanti: NaN su
motor_temp_celsius(emotor_temp_kelvin) e suvibration_level. Sono pochi: si eliminano le righe o si imputano con la media (le variabili sono numeriche e a campana). - Tipi:
operating_modeemaintenance_flagsono categoriche, il resto numeriche. - Correlazioni:
motor_temp_celsiusemotor_temp_kelvinsono la stessa informazione (differiscono per una costante): se ne tiene una.coolant_tempe la temperatura del motore sono molto correlate: ridondanza. Con soli campioni ridurre le variabili riduce il rischio di overfitting.ambient_humidityha correlazione circa zero con il target (distribuzione uniforme): non porta informazione. - Distribuzioni e outlier: la maggior parte delle variabili ha forma a campana;
power_consumptionmostra outlier nel boxplot. - Categoriche contro target: la proporzione di guasti cambia tra le categorie, per esempio
maintenance_flag: conBase, conDone, conOverdue(manutenzione necessaria ma non eseguita, quasi sempre guasto);operating_mode:Auto,Idle,Manual. Se la proporzione cambia da categoria a categoria la variabile è informativa (se fosse costante no). - Target: la classe positiva è circa il : sbilanciato. Conseguenze: split stratificato, l'accuratezza non è la metrica giusta (un modello che dice sempre «nessun guasto» avrebbe circa ), meglio precision, recall, F1, AUC; eventuali tecniche di bilanciamento.
2. Preprocessing
df = data.drop(columns=["motor_temp_kelvin", "ambient_humidity", "coolant_temp"]) # ridondanti o inutili
for c in ["motor_temp_celsius", "vibration_level"]:
df[c] = df[c].fillna(df[c].mean()) # imputazione con la media
df = pd.get_dummies(df, columns=["operating_mode"], drop_first=True) # one-hot (nominale)
df["maintenance_flag"] = df["maintenance_flag"].astype("category").cat.codes # ordinale: Base, Done, Overdueoperating_mode è nominale (nessun ordine): one-hot, eliminando una colonna per evitare la ridondanza. Le 7 colonne finali sono motor_temp_celsius, vibration_level, error_count_24h, power_consumption, maintenance_flag e le due dummy di operating_mode.
Attenzione all'ordine delle operazioni: la divisione train/test va fatta prima della normalizzazione, e media e deviazione standard si calcolano sul solo training e si applicano anche al test; altrimenti l'informazione del test «filtra» nel training (data leakage, Overfitting, ridge regression e cross-validationUna buona prestazione sul training non basta: serve stimare quella su dati nuovi. La cross-validation (K-fold: $k$ parti, ciascuna a turno come test, errore medio; Monte Carlo: $k$ divisioni casuali con quota di test $q$; leave-one-out se $k=n$) evita di dipendere da una sola divisione casuale. L'errore atteso si scompone in $\text{bias}^2+\text{varianza}+\sigma^2$: i modelli semplici fanno underfitting (bias alto), quelli complessi overfitting (varianza alta). La regolarizzazione aggiunge alla perdita una penalità: la ridge regression minimizza $|y-X\beta|^2+\lambda\sum_{j\ge1}\beta_j^2$ e ha soluzione $\beta=(X^TX+\lambda\tilde I)^{-1}X^Ty$ (l'intercetta non si penalizza, le feature si standardizzano): riduce i coefficienti, rende l'inversa stabile con feature collineari, e $\lambda$ è un iperparametro scelto con la validazione (cross-validation annidata per non contaminare il test).Overfitting, ridge regression e cross-validation →).
3. Rete e valutazione
Xdf, ydf = df.drop(columns=["will_fail"]), df["will_fail"]
X, y = Xdf.to_numpy(dtype=np.float32), ydf.to_numpy(dtype=np.int64)
X_train, X_test, y_train, y_test = stratified_train_test_split(X, y, train_prop=0.8)
num = [0, 1, 2, 3] # le quattro colonne continue
mu, sd = X_train[:, num].mean(0), X_train[:, num].std(0)
X_train[:, num] = (X_train[:, num] - mu) / sd # statistiche del solo training
X_test[:, num] = (X_test[:, num] - mu) / sd
model = Sequential([
Input(shape=(X.shape[1],)),
Dense(64, activation="relu"), BatchNormalization(), Dropout(0.2),
Dense(32, activation="relu"), BatchNormalization(),
Dense(1, activation="sigmoid"), # probabilità di guasto
])
model.compile(loss="binary_crossentropy", optimizer="adam",
metrics=["accuracy", "Precision", "Recall", "AUC"])
model.fit(X_train, y_train, epochs=50, batch_size=32, validation_split=0.2)Perché queste scelte. ReLU nei nascosti, sigmoide in uscita perché il problema è binario e si vuole una probabilità, cross-entropy binaria come loss; due strati nascosti piccoli per un dataset da righe (, e parametri nelle parti dense: pochi, per limitare l'overfitting); batch normalization e dropout come regolarizzazione. In alternativa a un neurone con sigmoide si potevano usare due neuroni con softmax e cross-entropy categorica: stesso risultato.
Risultati sul test (il test ha campioni, positivi): accuratezza , precision , recall , AUC . Dai valori si ricostruisce la matrice di confusione: e , quindi , , e (accuratezza ✓). F1 .
Commento. La precision è alta (quando segnala un guasto quasi sempre ha ragione) ma il recall è basso: la metà dei guasti reali non viene segnalata, nonostante un'AUC alta, che dice che il modello ordina bene i campioni e che la soglia non è adatta. Causa: lo sbilanciamento. La loss totale scende di più ottimizzando bene la classe maggioritaria, quindi il modello tende a prevedere «nessun guasto». In un contesto di sicurezza si preferisce un recall alto a scapito della precision (un controllo in più costa meno di un guasto non previsto): si può abbassare la soglia di decisione, o bilanciare la loss.
(Nota: nel notebook della soluzione il commento a parole cita «16 su 18» e «30 positivi»; i numeri delle metriche stampate corrispondono invece a su , come ricavato qui.)
4. Bonus: loss pesata
Si moltiplica per un peso il termine della classe positiva nella cross-entropy: . Con (l'inverso della frazione di positivi nel training, circa ) un errore su un guasto pesa come errori sulla classe maggioritaria. In TensorFlow: tf.nn.weighted_cross_entropy_with_logits(labels, logits, pos_weight=1/y_train.mean()) (richiede i logits, quindi l'ultimo strato senza sigmoide o con la formula adattata). (La soluzione del notebook lascia invece la sigmoide e passa le probabilità come se fossero logits: funziona come penalizzazione approssimata ma non è la loss pesata esatta.) L'effetto atteso è un recall più alto e una precision più bassa: si sposta il punto di funzionamento sulla curva precision-recall.