Eccezioni C++ e Design Pattern nel Deep Learning
<stdexcept>)<stdexcept>Il C++ fornisce una gerarchia di eccezioni standard pronte all’uso. Invece di inventare classi custom per ogni errore, possiamo usare queste.
Tutte le eccezioni standard espongono il metodo what() per ottenere la stringa di errore.
Pessima Pratica:
Lanciare stringhe o interi rende impossibile filtrare gli errori nel catch. Lancia sempre oggetti derivati da std::exception.
Buona Pratica:
std::invalid_argument (Parametri errati)Usata per segnalare che uno degli argomenti passati a una funzione non è valido.
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::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).
Quando catturi un’eccezione, fai sempre il catch per referenza costante (const auto& o const Exception&).
Se catturi per valore, subirai l’Object Slicing: se viene lanciata una classe derivata, verrà tagliata e convertita nella classe base, perdendo il polimorfismo!
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";
}.what()Tutte le eccezioni standard implementano una funzione virtuale:
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.
Durante l’addestramento di un modello di Deep Learning, molti errori possono verificarsi, soprattutto quando si combinano layer (strati) diversi.
NaN (Not a Number) che esplodono durante i gradienti.Le eccezioni ci permettono di interrompere l’addestramento in modo pulito prima di sprecare ore di calcolo.
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");
}
// ...
}
};Nel Machine Learning, caricherete costantemente dataset. I file possono mancare, o avere formati errati.
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";
}
}
}Matrix che hai scritto nei laboratori precedenti.std::invalid_argument nell’operatore di somma matriciale (operator+), lanciando eccezione se le dimensioni non combaciano.main(), forza un errore di dimensione e catturalo con un blocco try-catch, stampando il messaggio di errore con .what().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.
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.
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.
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);
}
};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.
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!
}
}
}
};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.
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");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++.