Primo Programma C++: Discesa del Gradiente e Backpropagation
CMakeLists.txtCMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(BasicNeuralPlusPlus)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
find_package(Eigen3 QUIET)
find_package(OpenCV REQUIRED)
message(STATUS "OpenCV ${OpenCV_VERSION} trovata")
add_executable(basic_neural main.cpp)
if(Eigen3_FOUND)
1 target_link_libraries(basic_neural Eigen3::Eigen)
else()
message(STATUS "Eigen non trovata: uso libs/eigen")
target_include_directories(basic_neural PRIVATE libs/eigen)
endif()
target_link_libraries(basic_neural ${OpenCV_LIBS})find_package non basta: senza questa, la compilazione muore con fatal error: Eigen/Dense: No such file or directory anche con Eigen installata.
main.cppmain.cpp
#include <cstdlib> // std::srand
#include <ctime> // std::time
#include <iostream>
#include <Eigen/Dense>
#include <opencv2/opencv.hpp>
int main() {
1 // std::srand(std::time(nullptr));
Eigen::MatrixXf W = Eigen::MatrixXf::Random(3, 4);
Eigen::VectorXf x = Eigen::VectorXf::Random(4);
Eigen::VectorXf z = W * x;
std::cout << "z e' " << z.rows() << "x" << z.cols() << ":\n" << z << "\n\n";
2 Eigen::ArrayXf z_relu = z.array().max(0.0f);
std::cout << "dopo la ReLU:\n" << z_relu << "\n\n";
// ... segue l'immagine OpenCVRandom() usa std::rand(): senza un seme la sequenza è sempre la stessa. Togliendo il commento i numeri cambiano a ogni esecuzione. nullptr è il NULL del C++ (Lezione 05).
main.cpp (segue)main.cpp
cv::Mat img(28, 28, CV_8UC1, cv::Scalar(128));
std::cout << "immagine " << img.rows << "x" << img.cols
<< ", canali: " << img.channels() << "\n";
1 cv::imwrite("test.png", img);
cv::imshow("Test MNIST size", img);
2 while (cv::waitKey(100) < 0 &&
cv::getWindowProperty("Test MNIST size", cv::WND_PROP_VISIBLE) >= 1) {}
}imwrite: funziona anche senza finestre grafiche, quindi il PNG c’è anche dove imshow fallisce.
waitKey(0) aspetterebbe solo un tasto: chiudendo la finestra col mouse il programma resterebbe in attesa. Così esce in entrambi i casi. È lo stesso ciclo di display() nel framework.
z e' 3x1:
-1.0027
0.0832618
-0.192788
dopo la ReLU:
0
0.0832618
0
immagine 28x28, canali: 1
I vostri numeri possono essere diversi da questi, perché std::rand() cambia da un sistema all’altro, ma a ogni esecuzione saranno gli stessi finché non date un seme con std::srand. La forma invece no: z deve essere \(3 \times 1\), e dopo la ReLU i negativi devono essere esattamente 0 e i positivi invariati. Se le dimensioni non tornano, avete invertito W e x.
Il progetto del Laboratorio 01 compila: oggi lo usiamo per il primo programma C++ vero, con quello che avete visto nelle Lezioni 03 e 04.
for.Bastano double, std::exp, for, if e std::cout. Le funzioni le conoscete dal C, e in C++ le riprendiamo nella Lezione 05: oggi, apposta, tutto sta nel main().
g++ --version (Linux e WSL) o clang++ --version (macOS), e cmake --version. Se uno non risponde, le istruzioni sono nel Laboratorio 01.basic_neural_plusplus: da qui in avanti è il progetto del framework. Il suo CMakeLists.txt cresce, non si riscrive.main.cpp con uno che stampi “Inizio progetto BASIC_NEURAL++!” e compilate da zero:oppure con i pulsanti di CMake Tools in VS Code. Eigen e OpenCV oggi non servono, ma un solo file di build evita di ritrovarsi con due progetti diversi.
Nel main.cpp dichiarate, stampate e combinate:
int (per esempio il numero di epoche) e un double (il learning rate);5 / 2 fra due int: cosa stampa, e perché? (come in C; Lezione 04)static_cast<double>(a) / b.Il quesito 1 della Lezione 03: loss \(E(x) = x^2\), partenza \(x_0 = 1\), passo \(x \leftarrow x - \eta\,E'(x)\) con \(E'(x) = 2x\). Per quali \(\eta\) il metodo converge, e quale centra il minimo in un solo passo?
for che faccia cinque passi e stampi \(x\) dopo ognuno.Il contatore è un int e \(\eta\) si ricava da lui: un ciclo for (double eta = 0.1; ...; eta += 0.1) accumulerebbe errori di arrotondamento, perché \(0{,}1\) non è rappresentabile esattamente in binario.
L’esercizio in classe della Lezione 03: due neuroni sigmoidi in fila, senza bias.

\[ x \;\to\; z_1 = w_1 x \;\to\; a_1 = \sigma(z_1) \;\to\; z_2 = w_2 a_1 \;\to\; a_2 = \sigma(z_2), \qquad E = (a_2 - y)^2 \]
con \(\sigma(z) = 1/(1 + e^{-z})\) e \(\sigma'(z) = \sigma(z)\,\big(1 - \sigma(z)\big)\). La backpropagation dà
\[ \delta_2 = 2(a_2 - y)\,\sigma'(z_2), \quad \delta_1 = w_2\,\delta_2\,\sigma'(z_1), \quad \frac{\partial E}{\partial w_2} = \delta_2\, a_1, \quad \frac{\partial E}{\partial w_1} = \delta_1\, x . \]
Con \(x = 1\), \(w_1 = w_2 = 1\), \(y = 1\) dovete ritrovare \(E = 0{,}1056\) e \(\partial E/\partial w_1 = -0{,}0280\).
Con \(x = 1\), \(w_1 = w_2 = 1\), \(y = 1\):
double per ciascuna di \(z_1, a_1, z_2, a_2\) ed \(E\). La sigmoide è 1.0 / (1.0 + std::exp(-z)), con #include <cmath>."\t") e confrontatela con la soluzione nella Lezione 03.Il forward va ricopiato due volte: è scomodo, ed è voluto. Nel Laboratorio 03 lo risolveremo con una funzione, double sigmoid(double z).
Ora i pesi si aggiornano: discesa del gradiente su \(w_1\) e \(w_2\), con un solo esempio e \(\eta = 1\):
I due gradienti vanno calcolati entrambi prima di aggiornare i pesi, nello stesso punto (Lezione 03): aggiornare w2 e poi calcolare delta1 con il w2 nuovo è un errore silenzioso. A \(t = 0\) deve uscire \(E = 0{,}1056\), come nel Task 4.
Buon lavoro! La soluzione di riferimento verrà discussa all’inizio del prossimo laboratorio.