Programmazione Scientifica++: Lezione 11

File I/O, Parsing CSV e Namespace negli Header

Docente

Leggere il Dataset: I File in C++

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).

Aprire un File (ifstream)

Usiamo ifstream per aprire un canale di comunicazione con il disco fisso.

#include <fstream>
#include <iostream>

int main() {
    // 1. Creo l'oggetto e gli dico quale file aprire
1    std::ifstream dataset("mnist_test.csv");
    
    // ... leggo i dati ...
    
    // 2. Chiudo la connessione al disco! Fondamentale!
2    dataset.close();
    
    return 0;
}
1
Come per std::cin per la tastiera, ifstream funge da “tubo” verso il file.
2
Usare il metodo .close() (o lasciare che intervenga il distruttore RAII) è obbligatorio.

Controllare l’integrità del File

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;
}
1
is_open() restituisce true solo se il file esiste e abbiamo i permessi di lettura.

Leggere il Dataset MNIST (Formato CSV)

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.

Estrarre intere righe (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";
    
}
1
Fantastico per iterare sull’intero dataset! Ora in riga abbiamo "5, 0, 0, 128, ..." come unica enorme stringa.

Il Problema della Virgola

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!

La Magia di std::stringstream

Includendo <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!
}
1
ss è ora un tubo che pompa la nostra stringa.
2
Il terzo parametro opzionale di getline è il Delimitatore.

Estrazione dei singoli Pixel

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++;
}
1
Ora avete il vero e proprio intero!

Esercizio: Parse MNIST (10 min)

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:

3, 0, 128, 255
1, 0, 0, 255

(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”.

Scrivere su File (ofstream)

Leggere non basta: il training deve salvare i pesi e la storia della loss. La classe simmetrica a ifstream è std::ofstream.

#include <fstream>

std::ofstream os("loss_history.csv");
if (!os.is_open())
    throw std::runtime_error("Impossibile scrivere il file");

os << "epoch,loss,accuracy\n";      // header
os << 1 << "," << 2.301 << "," << 0.112 << "\n";

os.close();   // opzionale: il distruttore lo fa da solo (RAII, Lezione 9)

Il file si chiude quando l’oggetto esce dallo scope: è lo stesso RAII della Lezione 9.

Precisione e Portabilità dei Numeri

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.

os.precision(17);   // round-trip esatto per un double
                    // (9 cifre sono sufficienti per un float)

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;

Il Futuro: L’Analisi Dati nel CERN (ROOT)

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.

Plottare la Loss Function in C++

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.

Codice Reale: Istogramma 1D in ROOT

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); 
    
    // ...
}
1
Notate come la conoscenza profonda dei Costruttori in C++ renda immediata la lettura di questo codice professionale.

Salvare il Plot (TCanvas)

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;
}
1
Costruttore della classe TCanvas.
2
Metodo per triggerare il rendering.
3
Potete salvare in .pdf, .png, .eps, .root.

Il Peccato Capitale degli Header

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:

using namespace std;

Ci salvava dal dover digitare std::cout, std::string ecc.

Namespace Clashes: Perché è il Male

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').

Usare bene i Namespace

La regola professionale:

  1. Nei file Header (.hpp): Digita sempre il nome per esteso (es. std::vector, std::string). Proteggi l’ecosistema.
  2. Nei file Sorgente (.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.