Laboratorio 05

Programmazione a Oggetti — la classe Matrix e il DenseLayer

Docente

Soluzione del Laboratorio 04

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.

Soluzione del Laboratorio 04: la matrice alla C

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.

Obiettivo di Oggi

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:

  1. 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).
  2. DenseLayer — il primo strato vero della rete, che invece usa Eigen.

Task 1: L’interfaccia di Matrix

Crea matrix.hpp. Sono i tre campi di CMatrix, ma private: nessuno dall’esterno può toccare il puntatore, né cambiare rows senza cambiare data.

#ifndef MATRIX_HPP
#define MATRIX_HPP

class Matrix {
private:
    int rows;
    int cols;
    double* data;        // un unico blocco contiguo, row-major

public:
    Matrix(int r, int c);

    double get(int r, int c) const;
    void set(int r, int c, double val);

    int getRows() const;
    int getCols() const;
};

#endif

Notate che get è marcato const: leggere non modifica la matrice.

Task 2: L’implementazione

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.

Una bomba a orologeria (voluta)

La nostra Matrix alloca con new[] e non ha un distruttore.

{
    Matrix m(1000, 1000);   // 8 MB allocati
}                           // fine dello scope... e gli 8 MB?

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

Task 3: DenseLayer con Eigen

Per 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;
};

#endif

Warning

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.

Task 4: L’implementazione di DenseLayer

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

Task 5: Il Test

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

Task da Completare

La soluzione di riferimento verrà discussa all’inizio della prossima lezione.