Programmazione a Oggetti — la classe Matrix e il DenseLayer
int N;
std::cin >> N;
if (N <= 0) { std::cerr << "Errore: N deve essere > 0!" << std::endl; return 1; }
double* inputs = new double[N]; // ALLOCAZIONE sull'Heap
double* pesi = new double[N];
for (int i = 0; i < N; ++i) { inputs[i] = 1.0; pesi[i] = 0.01; }
double b = 0.5, z = 0.0;
for (int i = 0; i < N; ++i) z += pesi[i] * inputs[i];
z += b;
std::cout << "Risultato del calcolo (Z): " << z << std::endl;
delete[] inputs; // per ogni new[] un delete[]
delete[] pesi;Il commento che conta è sul perché il delete[] non è pedanteria: in un loop di addestramento su 60.000 immagini, dimenticarlo satura decine di GB di RAM in pochi secondi. Notate anche il controllo su N <= 0 prima di allocare.
struct CMatrix { int rows; int cols; double* data; };
CMatrix cmat_alloc(int rows, int cols) {
CMatrix m; m.rows = rows; m.cols = cols;
m.data = new double[rows * cols];
return m;
}
void cmat_free(CMatrix& m) { delete[] m.data; m.data = nullptr; m.rows = m.cols = 0; }
double cmat_get(const CMatrix& m, int i, int j) { return m.data[i * m.cols + j]; }
void cmat_set(CMatrix& m, int i, int j, double val) { m.data[i * m.cols + j] = val; }
void layer_forward(const CMatrix& W, const double* x, const double* b, double* z) {
for (int j = 0; j < W.rows; ++j) {
double acc = 0.0;
for (int i = 0; i < W.cols; ++i) acc += cmat_get(W, j, i) * x[i];
z[j] = acc + b[j];
}
}E le due domande del Task 7: CMatrix W2 = W; copia i tre campi, puntatore incluso — due struct che puntano allo stesso blocco, e il secondo cmat_free è un double free. Sulla lunghezza di x, nessuna garanzia: double* è solo un indirizzo. Tenete a mente entrambe: sono il programma di oggi e della prossima settimana.
Il problema del Lab 04 non è il delete[]: è che l’utente della nostra libreria deve ricordarselo, e che rows, cols e data sono tre variabili sciolte che chiunque può scombinare. La soluzione è l’incapsulamento: chi possiede la memoria è l’oggetto, non chi lo usa.
Oggi scriviamo due classi:
Matrix — è la CMatrix del Lab 04 chiusa in una classe: gli stessi tre dati, ma private; cmat_alloc diventa il costruttore, cmat_get/cmat_set diventano metodi. Il codice lo avete già scritto: oggi cambia chi lo controlla. Serve a capire cosa fa Eigen sotto il cofano (e cosa succede se lo fai male).DenseLayer — il primo strato vero della rete, che invece usa Eigen.MatrixCrea matrix.hpp. Sono i tre campi di CMatrix, ma private: nessuno dall’esterno può toccare il puntatore, né cambiare rows senza cambiare data.
Notate che get è marcato const: leggere non modifica la matrice.
In matrix.cpp. Due difese, entrambe con eccezioni:
#include "matrix.hpp"
#include <stdexcept>
Matrix::Matrix(int r, int c) : rows(r), cols(c) {
if (r <= 0 || c <= 0)
throw std::invalid_argument("Le dimensioni della matrice devono essere > 0");
data = new double[rows * cols];
for (int i = 0; i < rows * cols; ++i) data[i] = 0.0;
}
double Matrix::get(int r, int c) const {
if (r < 0 || r >= rows || c < 0 || c >= cols)
throw std::out_of_range("Indici di matrice fuori dai limiti!");
return data[r * cols + c];
}
void Matrix::set(int r, int c, double val) {
if (r < 0 || r >= rows || c < 0 || c >= cols)
throw std::out_of_range("Indici di matrice fuori dai limiti!");
data[r * cols + c] = val;
}
int Matrix::getRows() const { return rows; }
int Matrix::getCols() const { return cols; }La formula data[r * cols + c] è l’indicizzazione row-major: una matrice 2D vive in un array 1D. È esattamente come MNIST serializza i suoi 28×28 pixel.
Note
throw, come visto nella Lezione 10. Interrompe immediatamente la funzione e consegna al chiamante un oggetto-errore: nessun valore di ritorno fasullo, nessun errore che passa inosservato. std::invalid_argument e std::out_of_range (da <stdexcept>) descrivono cosa è andato storto. Qui non serve try/catch: se un indice è fuori dai limiti il programma deve fermarsi con un messaggio chiaro, ed è esattamente ciò che fa un’eccezione non catturata. Il meccanismo completo è nelle Lezioni 19 e 20.
La nostra Matrix alloca con new[] e non ha un distruttore.
Restano occupati per sempre. Peggio: Matrix a = b; copierebbe il puntatore, non i dati — due oggetti che credono di possedere la stessa memoria, e un crash annunciato.
Note
La cura la conoscete già: distruttore e Regola del Tre sono le lezioni di questa settimana. La applicheremo nel Laboratorio 06, e lo facciamo apposta con una settimana di distanza: prima bisogna sentire il sintomo. Chi vuole può provare subito a far crashare il programma con Matrix a = b;.
DenseLayer con EigenPer lo strato della rete non reinventiamo la ruota: usiamo Eigen, che la memoria la gestisce da sé.
Crea dense_layer.hpp:
#ifndef DENSE_LAYER_HPP
#define DENSE_LAYER_HPP
#include <Eigen/Dense>
class DenseLayer {
private:
Eigen::MatrixXd weights;
Eigen::MatrixXd biases;
public:
DenseLayer(int input_size, int output_size);
Eigen::MatrixXd forward(const Eigen::MatrixXd& input);
const Eigen::MatrixXd& getWeights() const;
const Eigen::MatrixXd& getBiases() const;
};
#endifWarning
Attenzione all’ordine: qui il costruttore è (input_size, output_size). Nel Laboratorio 09 il framework lo invertirà, e sarà una fonte di bug se non ve lo ricordate.
DenseLayerIn dense_layer.cpp:
#include "dense_layer.hpp"
DenseLayer::DenseLayer(int input_size, int output_size) {
// La matrice dei pesi ha forma [output, input]: moltiplica un input
// di output_size... no: di input_size righe, e ne produce output_size.
weights = Eigen::MatrixXd::Random(output_size, input_size);
biases = Eigen::MatrixXd::Zero(output_size, 1);
}
Eigen::MatrixXd DenseLayer::forward(const Eigen::MatrixXd& input) {
// Z = W * X + b
return (weights * input).colwise() + biases.col(0);
}
const Eigen::MatrixXd& DenseLayer::getWeights() const { return weights; }
const Eigen::MatrixXd& DenseLayer::getBiases() const { return biases; }.colwise() + biases.col(0) somma il vettore dei bias a ogni colonna dell’output: così la stessa riga di codice funziona per un campione singolo e per un batch. Si chiama broadcasting.
In main.cpp, verifichiamo che l’algebra torni:
#include <iostream>
#include <Eigen/Dense>
#include "dense_layer.hpp"
int main() {
// Simuliamo un'immagine MNIST appiattita: 784 pixel in colonna
Eigen::MatrixXd X = Eigen::MatrixXd::Random(784, 1);
DenseLayer hidden(784, 128); // 784 input -> 128 neuroni
Eigen::MatrixXd Z = hidden.forward(X);
std::cout << "Righe attese: 128 -> reali: " << Z.rows() << std::endl;
std::cout << "Colonne attese: 1 -> reali: " << Z.cols() << std::endl;
if (Z.rows() == 128 && Z.cols() == 1)
std::cout << "[SUCCESSO] L'algebra di Eigen funziona." << std::endl;
else
std::cout << "[ERRORE] Dimensioni non corrispondenti." << std::endl;
}Provate anche a passare un batch: Eigen::MatrixXd::Random(784, 32) deve produrre un output 128 × 32 senza toccare una riga di forward().
La soluzione di riferimento verrà discussa all’inizio della prossima lezione.