Programmazione Scientifica++: Lezione 20

Eccezioni C++ e Design Pattern nel Deep Learning

Docente

Argomenti di Oggi

  • La Gerarchia delle Eccezioni (<stdexcept>)
  • Validazione dei Parametri
  • Design Pattern (GoF) applicati al Deep Learning
  • Conclusione del Corso

La Gerarchia Standard: <stdexcept>

Il C++ fornisce una gerarchia di eccezioni standard pronte all’uso. Invece di inventare classi custom per ogni errore, possiamo usare queste.

#include <stdexcept>

// Derivano tutte da std::exception
std::logic_error e("Errore di logica del programmatore");
std::runtime_error r("Errore a runtime (es. file non trovato)");

Tutte le eccezioni standard espongono il metodo what() per ottenere la stringa di errore.

Non lanciare mai stringhe!

Pessima Pratica:

if (x < 0) {
    throw "Il valore non può essere negativo!"; // Lancia un const char*
}

Lanciare stringhe o interi rende impossibile filtrare gli errori nel catch. Lancia sempre oggetti derivati da std::exception.

Buona Pratica:

if (x < 0) {
    throw std::invalid_argument("Il valore non può essere negativo!");
}

std::invalid_argument (Parametri errati)

Usata per segnalare che uno degli argomenti passati a una funzione non è valido.

void set_learning_rate(double lr) {
    if (lr <= 0.0 || lr >= 1.0) {
        throw std::invalid_argument("Il learning rate deve essere in (0, 1)");
    }
    this->lr = lr;
}

Perfetto per validare i costruttori o i setter delle nostre classi Layer o Optimizer.

std::out_of_range (Fuori dai limiti)

Usata per segnalare che si sta tentando di accedere a un elemento fuori dai limiti di una struttura dati.

std::vector<double> v = {1.0, 2.0, 3.0};

try {
    // Il metodo .at() lancia std::out_of_range se l'indice non è valido
    double val = v.at(10); 
} catch (const std::out_of_range& e) {
    std::cerr << "Indice non valido: " << e.what() << '\n';
}

std::runtime_error (Errori di esecuzione)

Usata per errori che non possono essere previsti controllando il codice, ma dipendono dall’ambiente (es. file system, rete, hardware).

void load_dataset(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("Impossibile aprire il dataset: " + path);
    }
    // ...
}

Catturare per Referenza Costante

Quando catturi un’eccezione, fai sempre il catch per referenza costante (const auto& o const Exception&).

try {
    fai_qualcosa_di_pericoloso();
} catch (const std::exception& e) {  // CORRETTO
    std::cerr << "Errore: " << e.what() << '\n';
} catch (std::exception e) {         // SBAGLIATO (Slicing)
    std::cerr << "Errore: " << e.what() << '\n';
}

Se catturi per valore, subirai l’Object Slicing: se viene lanciata una classe derivata, verrà tagliata e convertita nella classe base, perdendo il polimorfismo!

Il Polimorfismo nel Catch

Puoi avere più blocchi catch, ordinati dal più specifico al più generico.

try {
    rischio();
} catch (const std::invalid_argument& e) {
    std::cerr << "Errore di programmazione: " << e.what() << '\n';
} catch (const std::runtime_error& e) {
    std::cerr << "Errore di esecuzione: " << e.what() << '\n';
} catch (const std::exception& e) {
    std::cerr << "Altro errore standard: " << e.what() << '\n';
} catch (...) {
    std::cerr << "Errore sconosciuto non-standard!\n";
}

Il metodo magico .what()

Tutte le eccezioni standard implementano una funzione virtuale:

virtual const char* what() const noexcept;

Questo significa che, grazie al polimorfismo (se catturate per referenza const std::exception&), il metodo .what() restituirà sempre il messaggio corretto della classe derivata specifica che è stata lanciata.

Applicazioni al Machine Learning

Durante l’addestramento di un modello di Deep Learning, molti errori possono verificarsi, soprattutto quando si combinano layer (strati) diversi.

  • Dimensioni delle matrici non compatibili.
  • Valori NaN (Not a Number) che esplodono durante i gradienti.
  • File CSV o immagini di training corrotti.

Le eccezioni ci permettono di interrompere l’addestramento in modo pulito prima di sprecare ore di calcolo.

Validare i Parametri della Rete Neurale

class DenseLayer {
public:
    DenseLayer(int input_size, int output_size) {
        if (input_size <= 0 || output_size <= 0) {
            throw std::invalid_argument("Le dimensioni del Layer devono essere > 0");
        }
        // alloca matrici
    }
    
    Matrix forward(const Matrix& input) {
        if (input.rows() != this->input_size) {
            throw std::invalid_argument("Dimensione input mismatch nel Forward Pass");
        }
        // ...
    }
};

Il Disastro dei File: Leggere il Dataset

Nel Machine Learning, caricherete costantemente dataset. I file possono mancare, o avere formati errati.

class DatasetLoader {
public:
    DatasetLoader(const std::string& filepath) {
        std::ifstream f(filepath);
        if (!f) {
            throw std::runtime_error("Dataset non trovato a: " + filepath);
        }
        // parsa CSV
    }
};

L’approccio Sicuro (Riprovare dal main)

int main() {
    bool loaded = false;
    while (!loaded) {
        std::string path;
        std::cout << "Inserisci path dataset: ";
        std::cin >> path;
        
        try {
            DatasetLoader loader(path);
            loaded = true;
            std::cout << "Caricato!\n";
        } catch (const std::runtime_error& e) {
            std::cerr << "Errore: " << e.what() << " Riprova.\n";
        }
    }
}

Esercizio Pratico: Proteggere la Rete (15 min)

  1. Prendi il codice della classe Matrix che hai scritto nei laboratori precedenti.
  2. Aggiungi la validazione con std::invalid_argument nell’operatore di somma matriciale (operator+), lanciando eccezione se le dimensioni non combaciano.
  3. Nel main(), forza un errore di dimensione e catturalo con un blocco try-catch, stampando il messaggio di errore con .what().

Architettare il Framework (Software Engineering)

Un framework di Machine Learning (come PyTorch o TensorFlow) non è solo “tanta matematica”. È un gigantesco progetto di Ingegneria del Software.

Come organizzare: - Decine di Layer diversi? - Decine di Ottimizzatori (Adam, SGD, RMSprop)? - Diverse funzioni Loss (MSE, CrossEntropy)?

La risposta risiede nei Design Pattern.

La Gang of Four (GoF)

I Design Pattern sono soluzioni riutilizzabili a problemi comuni di architettura del software. I più famosi furono formalizzati nel libro “Design Patterns: Elements of Reusable Object-Oriented Software” dalla Gang of Four (GoF).

Nel C++, i pattern più eleganti fanno un uso massiccio di classi astratte (Interfacce) e Polimorfismo. Ne vedremo tre fondamentali per il nostro framework.

Pattern 1: Strategy

Problema: Vogliamo che la Rete Neurale possa cambiare l’algoritmo di ottimizzazione (es. da SGD ad Adam) senza dover riscrivere la rete stessa. Soluzione: Definiamo un’interfaccia Optimizer e la passiamo alla rete.

Questo è il Pattern Strategy: iniettare l’algoritmo dall’esterno.

Strategy in Azione (C++)

class IOptimizer {
public:
    virtual void step(Matrix& weights, const Matrix& gradients) = 0;
    virtual ~IOptimizer() = default;
};

class Adam : public IOptimizer {
    void step(Matrix& w, const Matrix& g) override { /* magia di Adam */ }
};

class NeuralNetwork {
    std::unique_ptr<IOptimizer> optimizer;
public:
    void set_optimizer(std::unique_ptr<IOptimizer> opt) {
        optimizer = std::move(opt);
    }
    void train_step() {
        // Usa l'ottimizzatore, non importa quale sia!
        optimizer->step(W, dW);
    }
};

Pattern 2: Observer

Problema: Durante l’addestramento, vogliamo stampare i log a schermo, salvarli su file CSV, o mandarli a un server remoto (es. TensorBoard), ma non vogliamo riempire il Trainer di codice di I/O. Soluzione: Il Trainer “notifica” tutti gli “osservatori” iscritti ogni volta che finisce un’epoca.

Questo è l’Observer Pattern: un sistema di iscrizione e notifica.

Sganciare il Logger dal Trainer

class IObserver {
public:
    virtual void on_epoch_end(int epoch, double loss) = 0;
};

class Trainer {
    std::vector<IObserver*> observers;
public:
    void attach(IObserver* obs) { observers.push_back(obs); }
    
    void train() {
        for(int epoch=0; epoch<100; ++epoch) {
            double loss = /* calcolo */;
            for(auto* obs : observers) {
                obs->on_epoch_end(epoch, loss); // Notifica tutti!
            }
        }
    }
};

Pattern 3: Factory Method

Problema: L’utente vuole caricare la struttura della Rete Neurale da un file JSON. Come istanziamo dinamicamente il giusto Layer (Dense, Conv, Dropout) a runtime leggendo una stringa? Soluzione: Una classe “Fabbrica” (Factory) che prende in input una stringa e restituisce un puntatore alla classe base creata.

Questo è il Factory Pattern.

Factory in Azione (C++)

class LayerFactory {
public:
    static std::unique_ptr<Layer> create(const std::string& type) {
        if (type == "Dense") {
            return std::make_unique<DenseLayer>();
        } else if (type == "Dropout") {
            return std::make_unique<DropoutLayer>();
        }
        throw std::invalid_argument("Layer sconosciuto: " + type);
    }
};

// Uso:
auto l = LayerFactory::create("Dense");

Conclusione del Corso Teorico-Pratico

Congratulazioni! Avete completato l’intero percorso.

Ora possedete: 1. Le basi solide e avanzate del C++ Moderno. 2. La comprensione della teoria del Deep Learning. 3. I principi dell’Ingegneria del Software (Design Patterns).

Siete pronti per costruire, espandere e dominare il vostro framework BASIC_NEURAL++.