Gestione della Memoria e Vettori C-Style
#include <iostream>
#include <cmath>
#include <iomanip>
double relu(double x) { return (x > 0.0) ? x : 0.0; }
double relu_derivative(double x) { return (x > 0.0) ? 1.0 : 0.0; }
double sigmoid(double x) { return 1.0 / (1.0 + std::exp(-x)); }
double sigmoid_derivative(double x) {
double sig = sigmoid(x);
return sig * (1.0 - sig); // si riusa sigmoid(x): non si riscrive la formula
}
int main() {
std::cout << std::fixed << std::setprecision(4);
std::cout << "x\t| ReLU\t\tReLU'\t| Sigmoid\tSigmoid'" << std::endl;
for (double x = -5.0; x <= 5.0; x += 1.0) {
std::cout << std::setw(4) << x << "\t| "
<< std::setw(6) << relu(x) << "\t"
<< std::setw(6) << relu_derivative(x) << "\t| "
<< std::setw(7) << sigmoid(x) << "\t"
<< std::setw(7) << sigmoid_derivative(x) << std::endl;
}
}Due cose da notare: sigmoid_derivative riusa sigmoid(x) invece di riscrivere l’esponenziale, e <iomanip> (setprecision, setw) serve a incolonnare i numeri — con la tabulazione secca le colonne si disallineano appena un numero cambia di lunghezza.
double sigmoid_numerical_derivative(double x, double h) {
return (sigmoid(x + h) - sigmoid(x - h)) / (2.0 * h);
}
// nel main: massimo errore su una griglia di x
double h = 1.0e-5, max_err = 0.0;
for (double x = -5.0; x <= 5.0; x += 0.5) {
double e = std::fabs(sigmoid_derivative(x) - sigmoid_numerical_derivative(x, h));
if (e > max_err) max_err = e;
}Con h = 1e-5 l’errore massimo è circa \(10^{-11}\): le formule sono giuste. La tabella al variare di h è la parte istruttiva:
h |
\(10^{-1}\) | \(10^{-3}\) | \(10^{-5}\) | \(10^{-7}\) | \(10^{-9}\) | \(10^{-11}\) | \(10^{-13}\) |
|---|---|---|---|---|---|---|---|
| errore | \(6\cdot10^{-5}\) | \(6\cdot10^{-9}\) | \(2\cdot10^{-12}\) | \(4\cdot10^{-10}\) | \(2\cdot10^{-8}\) | \(3\cdot10^{-6}\) | \(5\cdot10^{-4}\) |
A sinistra domina l’errore di troncamento (\(\propto h^2\)); a destra la cancellazione: f(x+h) e f(x-h) coincidono nelle prime 13 cifre e la sottrazione lascia solo rumore. Il double ha 16 cifre, non infinite.
Per comprendere a fondo il C++ moderno e giustificare la necessità delle Classi (OOP), dobbiamo prima affrontare la gestione cruda e manuale della memoria (C-Style). Simuleremo i “pesi” di un singolo neurone usando array dinamici; poi passeremo a uno strato di neuroni, che richiede una matrice — e la scriveremo come si farebbe in C, con una struct e funzioni sciolte. Il Laboratorio 05 prenderà esattamente quel codice e lo chiuderà in una classe.
Dimentica i vector e gli array a dimensione fissa. Immagina di dover caricare \(N\) pesi in memoria, dove \(N\) è una variabile decisa a runtime (es. inserita dall’utente tramite std::cin).
double grandi \(N\): uno per i pesi (Weights) e uno per gli input (X).new double[N].Scrivi un ciclo for che riempia questi due array con valori fittizi. - Per gli input: riempi con il valore \(1.0\). - Per i pesi: riempi con valori piccoli casuali o costanti (es. \(0.01\)).
Ricorda: Puoi accedere agli elementi di un array dinamico usando la notazione pesi[i].
L’equazione di un singolo neurone (prima della funzione di attivazione) è un prodotto scalare: \(Z = (\sum_{i=0}^{N-1} W_i \cdot X_i) + b\).
b (bias) inizializzata a \(0.5\).for che moltiplichi ogni peso per il rispettivo input e accumuli il risultato in una variabile z.z e stampa il risultato finale.La regola d’oro del C++ è: “Per ogni new[], ci deve essere un delete[]”.
main, dealloca esplicitamente i due array usando delete[] pesi; e delete[] input;.delete[] e metti l’intero programma in un ciclo infinito che fa addestramento?Un neurone ha \(N\) pesi: un array. Uno strato di \(M\) neuroni ne ha \(M \times N\): una matrice. In C una matrice dinamica è tre informazioni che devono viaggiare insieme — quante righe, quante colonne, dove sono i numeri:
CMatrix cmat_alloc(int rows, int cols): riempie i due interi e alloca new double[rows * cols].void cmat_free(CMatrix& m): delete[], poi azzera data (a nullptr), rows e cols, così una seconda chiamata per sbaglio non fa danni.double cmat_get(const CMatrix& m, int i, int j) e void cmat_set(CMatrix& m, int i, int j, double val). L’elemento \((i, j)\) sta in data[i * cols + j]: la riga \(i\) occupa cols celle consecutive. Row-major — tenetelo a mente, tornerà con Eigen.Con \(W\) di dimensione \(M \times N\), l’uscita del neurone \(j\) è \(z_j = \sum_{i} W_{ji}\, x_i + b_j\): un prodotto matrice-vettore.
void layer_forward(const CMatrix& W, const double* x, const double* b, double* z): due cicli annidati, quello esterno sui neuroni (W.rows), quello interno sugli input (W.cols). Usa cmat_get, non l’aritmetica degli indici a mano.main, con \(M = 3\) e la stessa \(N\) dell’utente: alloca W con cmat_alloc, e bias, x, z_out con new[]. Riempi x con \(1.0\), i bias con \(0.5\), e la riga \(j\) di W con \(0.01 \cdot (j+1)\).layer_forward e stampa z_out. Atteso: \(z_j = 0.01\,(j+1)\,N + 0.5\). Con \(N = 784\): \(8.34\), \(16.18\), \(24.02\).cmat_free(W) e tre delete[].Guardate il main: quattro new, quattro delete, tre numeri da passare a ogni funzione. E ora due domande a cui rispondere prima di venire al Laboratorio 05:
CMatrix W2 = W; seguito da cmat_free(W); cmat_free(W2);? (Suggerimento: cosa copia l’assegnamento di una struct che contiene un puntatore?)x abbia davvero W.cols elementi quando lo passate a layer_forward? Nessuno. Il compilatore non può saperlo.Le risposte sono il motivo per cui esistono le classi. Il Laboratorio 05 prende CMatrix e le sue quattro funzioni e le trasforma in class Matrix: i tre dati diventano private, cmat_alloc diventa il costruttore, cmat_get/cmat_set diventano metodi.
La soluzione (con i commenti sui pericoli di questo approccio) verrà discussa all’inizio della prossima lezione!