Salta al contenuto
Note per Studenti Esercizio - Rete feed-forward su dati tabulari sbilanciati (appello)

Esercizio - Rete feed-forward su dati tabulari sbilanciati (appello)

Esame

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 500500 righe.

  1. Esplorare il dataset e commentare perché ogni analisi serve al modello.
  2. Eseguire il preprocessing necessario, motivando le scelte.
  3. Allenare una rete feed-forward: ReLU negli strati nascosti, un neurone di uscita con sigmoide (probabilità della classe 11), Adam con learning rate 0,0010{,}001, 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.
  4. (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: 1313 NaN su motor_temp_celsius (e motor_temp_kelvin) e 22 su vibration_level. Sono pochi: si eliminano le righe o si imputano con la media (le variabili sono numeriche e a campana).
  • Tipi: operating_mode e maintenance_flag sono categoriche, il resto numeriche.
  • Correlazioni: motor_temp_celsius e motor_temp_kelvin sono la stessa informazione (differiscono per una costante): se ne tiene una. coolant_temp e la temperatura del motore sono molto correlate: ridondanza. Con soli 500500 campioni ridurre le variabili riduce il rischio di overfitting. ambient_humidity ha correlazione circa zero con il target (distribuzione uniforme): non porta informazione.
  • Distribuzioni e outlier: la maggior parte delle variabili ha forma a campana; power_consumption mostra outlier nel boxplot.
  • Categoriche contro target: la proporzione di guasti cambia tra le categorie, per esempio maintenance_flag: 0,2850{,}285 con Base, 0,0910{,}091 con Done, 0,9780{,}978 con Overdue (manutenzione necessaria ma non eseguita, quasi sempre guasto); operating_mode: 0,3030{,}303 Auto, 0,3660{,}366 Idle, 0,1900{,}190 Manual. Se la proporzione cambia da categoria a categoria la variabile è informativa (se fosse costante no).
  • Target: la classe positiva è circa il 29%29\%: sbilanciato. Conseguenze: split stratificato, l'accuratezza non è la metrica giusta (un modello che dice sempre «nessun guasto» avrebbe circa 71%71\%), meglio precision, recall, F1, AUC; eventuali tecniche di bilanciamento.

2. Preprocessing

python
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, Overdue

operating_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

python
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 500500 righe (7⋅64+64=5127\cdot64+64=512, 64⋅32+32=2 08064\cdot32+32=2\,080 e 3333 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 101101 campioni, 3030 positivi): accuratezza 0,8320{,}832, precision 0,8820{,}882, recall 0,5000{,}500, AUC 0,9370{,}937. Dai valori si ricostruisce la matrice di confusione: precision=TP/(TP+FP)=15/17=0,882\text{precision}=TP/(TP+FP)=15/17=0{,}882 e recall=TP/30=0,5\text{recall}=TP/30=0{,}5, quindi TP=15TP=15, FP=2FP=2, FN=15FN=15 e TN=101−15−2−15=69TN=101-15-2-15=69 (accuratezza (15+69)/101=0,832(15+69)/101=0{,}832 ✓). F1 =2⋅0,882⋅0,5/(0,882+0,5)=0,638=2\cdot0{,}882\cdot0{,}5/(0{,}882+0{,}5)=0{,}638.

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 0,50{,}5 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 1515 su 1717, come ricavato qui.)

4. Bonus: loss pesata

Si moltiplica per un peso w+>1w_+>1 il termine della classe positiva nella cross-entropy: ℓ=−[w+ ylog⁡y^+(1−y)log⁡(1−y^)]\ell=-\big[w_+\,y\log\hat y+(1-y)\log(1-\hat y)\big]. Con w+=1/yˉw_+=1/\bar y (l'inverso della frazione di positivi nel training, circa 1/0,29≈3,41/0{,}29\approx3{,}4) un errore su un guasto pesa come 3,43{,}4 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.

Teoria collegata