Funzioni di Attivazione e Performance (-O3)
La Regola del Tre su Matrix — il punto delicato è l’ordine dei passi nell’operatore di assegnazione:
Matrix& Matrix::operator=(const Matrix& other) {
if (this == &other) return *this; // a. auto-assegnazione PRIMA di tutto
delete[] data; // b. libera
rows = other.rows; cols = other.cols;
data = new double[rows * cols]; // c. rialloca e clona
for (int i = 0; i < rows * cols; ++i) data[i] = other.data[i];
return *this; // d.
}E la deep copy polimorfica della rete, che funziona solo grazie a clone():
Chi ha commentato il copy constructor ha visto il Double Free: due reti che fanno delete sugli stessi puntatori.
La rete che abbiamo costruito è inutile: due DenseLayer in fila sono una singola trasformazione lineare (Lezione 2). Serve la non-linearità.
Oggi:
SigmoidLayer, il primo layer senza pesi.Matrix: da qui in poi solo Eigen.-O3.MatrixRimuovete matrix.hpp e matrix.cpp dal progetto e dal CMakeLists.txt.
Non è tempo perso: scrivere quella classe serviva a capire cosa c’è dentro un Eigen::MatrixXd — un blocco contiguo row-major, con la Regola del Tre già implementata da altri e ottimizzata meglio di quanto potremmo fare noi.
Note
È il ciclo normale dell’ingegneria del software: si costruisce a mano per capire, poi si adotta la libreria matura. Quello che avete imparato serve a leggere gli errori di Eigen, non a sostituirla.
SigmoidLayerA differenza del DenseLayer, un layer di attivazione non ha pesi: applica una funzione elemento per elemento.
Crea activation_layer.hpp:
In activation_layer.cpp:
Tre passaggi in una riga: .array() passa alla semantica elemento per elemento, .unaryExpr(...) applica una lambda a ogni elemento, .matrix() torna al tipo matrice.
Warning
Senza .array(), l’operatore * di Eigen è il prodotto matriciale. È l’errore più insidioso del corso: compila, gira, e dà numeri sbagliati.
Nel main.cpp, alterniamo strati densi e attivazioni:
Criterio di verifica: l’output è 10 × 1 e ogni valore è strettamente compreso tra 0 e 1 — è la sigmoide finale che lo garantisce. Se vedete valori fuori da quell’intervallo, l’ultimo layer non è stato applicato.
<chrono>)Quanto costa un forward pass? Un singolo passaggio è troppo veloce per essere misurato: lo ripetiamo 10.000 volte.
#include <chrono>
int N_ITER = 10000;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N_ITER; ++i)
net.forward(X);
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Tempo trascorso: " << ms << " ms" << std::endl;La parola chiave auto fa dedurre al compilatore quei tipi verbosi delle librerie (l’abbiamo vista oggi a lezione; le sue regole complete arrivano nella Lezione 18).
Annotate il numero: fra un minuto lo confronteremo.
-O3)Per default CMake compila senza ottimizzazioni (-O0): privilegia la velocità di compilazione, non quella del programma.
Aggiungete al CMakeLists.txt:
Ricompilate da zero e rilanciate il benchmark.
Il tempo deve crollare, tipicamente di un fattore fra 5 e 20. Il motivo è che Eigen è scritto per essere ottimizzato: con -O3 il compilatore fonde i cicli (loop fusion), elimina le matrici temporanee e usa le istruzioni vettoriali della CPU (SSE/AVX) che elaborano 4 o 8 double per ciclo di clock.
Con -O0 tutto questo è disattivato, e Eigen diventa più lenta di un ciclo for scritto a mano.
Il lab di oggi è corto apposta: il tempo va nelle compilazioni e nelle misure. Chi arriva in fondo ha tre esperimenti pronti, in ordine di difficoltà. Nessuno è obbligatorio; tutti insegnano qualcosa che userete nel Lab 15.
-O0, -O1, -O2, -O3. Il salto grosso è fra -O0 e -O1 o fra -O2 e -O3? Annotatevi i quattro tempi: il Lab 15 chiederà misure di questo tipo su tutto il training.for. Scrivete la sigmoide con due cicli annidati su (i, j) invece di .array().unaryExpr(...), e cronometrate entrambe le versioni a -O3. Chi vince, e di quanto? Poi rifatelo a -O0: la risposta cambia, e il perché è nella slide precedente.float contro double. Sostituite Eigen::MatrixXd con Eigen::MatrixXf nel solo test di velocità. Il tempo si dimezza? Quasi mai esattamente: ragionate su cosa limita davvero il calcolo, la CPU o la memoria. Fra due laboratori il framework diventerà template sul tipo numerico proprio per poter fare questa scelta a piacere.La soluzione di riferimento verrà discussa all’inizio della prossima lezione.