File I/O, Parsing CSV e Namespace negli Header
L’altro grande input della nostra rete neurale saranno i Dati di Addestramento. Per il nostro progetto useremo il dataset MNIST (immagini di numeri scritti a mano).
Questo dataset vi sarà fornito come un enorme file testuale CSV (Comma Separated Values), dove ogni riga rappresenta un’immagine, e i numeri separati da virgola sono i pixel (da 0 a 255).
Per leggere i file in C++ si include la libreria <fstream> (File Stream) e si usa l’oggetto std::ifstream (Input File Stream).
ifstream)Usiamo ifstream per aprire un canale di comunicazione con il disco fisso.
std::cin per la tastiera, ifstream funge da “tubo” verso il file.
.close() (o lasciare che intervenga il distruttore RAII) è obbligatorio.
Cosa succede se il file mnist_test.csv non esiste nella cartella, o è rinominato male? Dovete sempre verificare che il file si sia aperto con successo prima di iniziare a estrarre milioni di pixel inesistenti!
int main() {
std::ifstream dataset("mnist_test.csv");
// Controllo di sicurezza
1 if (!dataset.is_open()) {
std::cerr << "Errore CRITICO: Impossibile trovare il file CSV!\n";
return -1;
}
std::cout << "Dataset caricato con successo in RAM.\n";
dataset.close();
return 0;
}is_open() restituisce true solo se il file esiste e abbiamo i permessi di lettura.
Un file CSV del dataset MNIST è fatto da migliaia di righe. Una riga tipica è una stringa del genere: "5, 0, 0, 128, 255, 10, 0 ..." (La prima colonna è il numero raffigurato (es. 5), le altre colonne sono i 784 pixel dell’immagine).
Per analizzarlo ci servono due passaggi: 1. Leggere una riga alla volta (fino ad esaurimento righe). 2. Spacchettare (fare Parsing de) la riga in base alle virgole.
getline)Il primo passaggio è usare la funzione globale std::getline(). Questa funzione prende lo stream del file e una variabile stringa, e pompa caratteri dentro la stringa finché non incontra un \n (A capo).
Restituisce false quando il file è finito.
std::ifstream dataset("mnist_test.csv");
if(!dataset.is_open()) return -1;
std::string riga;
// Finché c'è una riga da leggere nel dataset, mettila nella variabile "riga"
1while ( std::getline(dataset, riga) ) {
std::cout << "Letta una nuova riga dal dataset!\n";
}riga abbiamo "5, 0, 0, 128, ..." come unica enorme stringa.
Ora abbiamo "5,0,0,128,255". Ma a noi non serve un blocco di testo, ci servono i numeri separati: l’intero 5 per la label, l’intero 0 per il primo pixel e così via!
Dobbiamo dire al C++: “Prendi questa stringa, e leggila fino a quando non trovi una virgola. Quello è il primo pezzo. Poi continua.”.
Per farlo usiamo una magia potentissima del C++ moderno: lo std::stringstream!
std::stringstreamIncludendo <sstream>, il C++ ci permette di inserire una variabile string dentro a uno “stream di memoria”. Lo stream si comporterà come se fosse un piccolo file virtuale di una sola riga! E potremo usare di nuovo getline su di esso!
#include <sstream>
#include <string>
// ... abbiamo appena letto la "riga" dal file ...
// 1. Infilo la mia stringa in un mini-file virtuale
1std::stringstream ss(riga);
std::string pixel_stringa;
// 2. Chiamo getline sul mini-file, e gli dico di FERMARSI ALLE VIRGOLE!
2while ( std::getline(ss, pixel_stringa, ',') ) {
// Faccio cose con i pezzi separati!
}ss è ora un tubo che pompa la nostra stringa.
getline è il Delimitatore.
Mettiamo tutto insieme per estrarre il valore numerico dei pixel.
// ... ciclo while principale del file ...
std::stringstream ss(riga);
std::string token;
int colonna = 0;
// Estraggo "parole" divise da virgole
while ( std::getline(ss, token, ',') ) {
// Converto il singolo testo "128" nell'intero 128!
1 int valore = std::stoi(token);
if (colonna == 0) {
std::cout << "L'etichetta del numero è: " << valore << "\n";
} else {
// È un pixel! (Da salvare nell'array o matrice di Eigen)
}
colonna++;
}Sei il Data Engineer del team. Nel tuo terminale (non via codice) crea a mano un file testo chiamato finto_mnist.csv. Scrivici dentro 2 o 3 righe così composte:
(Immagina che il primo numero sia l’etichetta, e gli altri 3 siano i pixel).
Nel tuo C++: apri il file con ifstream. Controlla che esista. Estrai le righe con un ciclo while(getline). Estrai i numeri usando stringstream con separatore virgola ','. Per ogni riga, stampa: “Trovata immagine del numero: X. Pixel: Y Z W”.
ofstream)Leggere non basta: il training deve salvare i pesi e la storia della loss. La classe simmetrica a ifstream è std::ofstream.
Il file si chiude quando l’oggetto esce dallo scope: è lo stesso RAII della Lezione 9.
Attenzione a un errore che costa ore di debug: per default ostream stampa solo 6 cifre significative. Salvare pesi double così li tronca, e la rete ricaricata non dà più gli stessi risultati.
Tip
Progettate i metodi di salvataggio su std::ostream&, non su std::ofstream&. Così lo stesso codice scrive su file, su std::cout o su uno std::stringstream per i test — ed è la firma che useremo nel framework: virtual void save_weights(std::ostream& os) const;
Stampare numeri sulla console va bene per piccoli script. Ma quando alleneremo la rete neurale e valuteremo la sua Loss Function, vorremo un grafico per vedere la curva che scende!
Il C++ non ha capacità di plottaggio grafiche native. In Python usate matplotlib. In Fisica delle Alte Energie (INFN, CERN, CMS, ATLAS) il C++ domina, e si usa ROOT.
ROOT è un framework gigantesco per l’analisi dati e la visualizzazione, e presto inizierete a usarlo per i vostri progetti finali.
ROOT è stato costruito negli anni 90 usando un approccio estremamente basato sull’orientamento agli Oggetti. Se capite bene Costruttori, Distruttori, Puntatori e Heap (quello che abbiamo fatto in queste lezioni), sarete dei maghi di ROOT.
Gli elementi principali per creare un grafico della “Loss” sono due oggetti: 1. Il grafico in sé: un oggetto TGraph (per punti X,Y) o TH1F (per gli istogrammi statistici 1D). 2. La “Tela”: un oggetto TCanvas su cui incollare il grafico prima di esporarlo in PDF.
Ecco un esempio reale del codice C++ che potreste scrivere nel laboratorio finale per generare 10.000 pesi neurali casuali e visualizzarli:
#include "TH1F.h" // Header dell'Istogramma di ROOT
#include "TCanvas.h"// Header della Tela di ROOT
int main() {
// Istanzio l'Istogramma (Nome interno, Titolo Esterno, Bins, Min, Max)
1 TH1F h1("h1", "Distribuzione Pesi Iniziali", 100, -3.0, 3.0);
// ROOT ha un metodo già pronto per iniettare numeri casuali (Gaussiani)!
h1.FillRandom("gaus", 10000);
// ...
}L’oggetto in RAM h1 contiene ora tutti i numeri, ma non si può guardare. Ci serve la tela su cui disegnarlo.
// ... codice precedente ...
// Istanzio la Tela (Nome, Titolo, Larghezza px, Altezza px)
1 TCanvas c1("c1", "La mia Finestra", 800, 600);
// Dico all'istogramma di disegnarsi (di base cerca la Tela attiva)
2 h1.Draw();
// Dico alla Tela di salvare il capolavoro sul mio Hard Disk!
3 c1.SaveAs("distribuzione_pesi.pdf");
return 0;
}TCanvas.
.pdf, .png, .eps, .root.
C’è un ultimo avvertimento vitale prima di iniziare a programmare file divisi e interfacciati con immense librerie esterne come Eigen e ROOT.
Finora abbiamo spesso usato questa scorciatoia magica all’inizio dei nostri file:
Ci salvava dal dover digitare std::cout, std::string ecc.
Warning
NON METTERE MAI using namespace std; DENTRO UN FILE HEADER (.hpp)!
Quando metti using namespace std; nel file Vettore.hpp, stai inquinando permanentemente lo spazio dei nomi globali. Qualsiasi programmatore che farà #include "Vettore.hpp" nel suo main, si ritroverà “iniettato” a forza il namespace std.
Se lui usa una libreria come ROOT che ha per caso una funzione chiamata max(), e std ha una funzione chiamata max(), il compilatore impazzirà con un letale Namespace Clash (Ambiguous reference to 'max').
La regola professionale:
.hpp): Digita sempre il nome per esteso (es. std::vector, std::string). Proteggi l’ecosistema..cpp): Puoi usare liberamente using namespace std; perché gli effetti resteranno confinati esclusivamente a quel singolo file isolato, e non contageranno nessuno in giro per il progetto!Risparmiarsi 5 caratteri in un Header non vale le ore di debugging di un Namespace Clash in librerie da un milione di righe di codice.