Tipi Enumerati, Inizializzazione dei Pesi e Gerarchia delle Attivazioni
Il SigmoidLayer, primo layer senza pesi:
E il benchmark che ha mostrato l’effetto di -O3:
Il tempo dovrebbe essere crollato di un fattore fra 5 e 20: con -O3 il compilatore fonde i cicli, elimina le temporanee e usa le istruzioni vettoriali (AVX). Senza, Eigen è più lenta di un for scritto a mano.
Finora i pesi erano MatrixXd::Random, cioè uniformi in \([-1, 1]\). È una scelta pessima: con 784 input, la somma di 784 prodotti finisce nell’ordine delle decine, la sigmoide satura e la rete non impara più (Lezione 2).
Oggi:
randnumgen.hpp).enum class.TanhLayer accanto a SigmoidLayer.La regola è mantenere la varianza del segnale costante attraversando i layer. Se cresce, le attivazioni saturano; se decresce, il segnale si spegne.
Due ricette, ognuna tarata su una famiglia di attivazioni:
| pensata per | scala \(s\) (gaussiana) | scala \(s\) (uniforme) | |
|---|---|---|---|
| He (Kaiming) | ReLU e varianti | \(\sqrt{2 / N_{in}}\) | \(\sqrt{6 / N_{in}}\) |
| Xavier (Glorot) | sigmoide, tanh | \(\sqrt{2/(N_{in}+N_{out})}\) | \(\sqrt{6/(N_{in}+N_{out})}\) |
Nel caso gaussiano si estrae da una normale di deviazione standard \(s\); nel caso uniforme da \([-s, +s]\).
randnumgenAggiungete al progetto randnumgen.hpp (fornito dal docente). Espone un oggetto globale rng basato su Mersenne Twister a 64 bit:
Tip
Durante lo sviluppo usate sempre rng.seed(42): se i pesi iniziali cambiano a ogni esecuzione non potete distinguere un bug da una fluttuazione statistica.
enum classIn dense_layer.hpp, fuori dalla classe:
// Gli enum class evitano i "magic numbers" sparsi nel codice
enum class InitType { HE, XAVIER };
enum class DistroType { UNIFORM, GAUSSIAN };
class DenseLayer : public ILayer {
private:
Eigen::MatrixXd weights;
Eigen::MatrixXd biases;
public:
DenseLayer(int input_size, int output_size,
InitType init = InitType::HE,
DistroType distro = DistroType::UNIFORM);
// ...
};Con int type, int distro nessuno vi impedirebbe di passare 3, o di invertire i due argomenti. Con enum class il compilatore rifiuta entrambi gli errori: è questo il significato di type safe.
rng.ranf() genera numeri uniformi. Per averli gaussiani serve la trasformazione di Box-Muller:
Il do/while esclude lo zero: std::log(0) è \(-\infty\).
DenseLayer::DenseLayer(int input_size, int output_size,
InitType init, DistroType distro) {
weights = Eigen::MatrixXd::Zero(output_size, input_size);
biases = Eigen::MatrixXd::Zero(output_size, 1);
rng.rseed();
double s = 0.0;
switch (init) {
case InitType::HE:
s = (distro == DistroType::UNIFORM) ? std::sqrt(6.0 / input_size)
: std::sqrt(2.0 / input_size);
break;
case InitType::XAVIER:
s = (distro == DistroType::UNIFORM)
? std::sqrt(6.0 / (input_size + output_size))
: std::sqrt(2.0 / (input_size + output_size));
break;
}
for (int i = 0; i < weights.rows(); ++i)
for (int j = 0; j < weights.cols(); ++j)
weights(i, j) = (distro == DistroType::UNIFORM)
? 2.0 * s * (rng.ranf() - 0.5) // [0,1) -> [-s, +s)
: s * gauss();
}Il vantaggio dello switch su enum class: se domani aggiungete InitType::LECUN, il compilatore vi avverte che manca un caso.
In activation_layer.hpp, inseriamo una classe intermedia fra ILayer e i layer concreti:
// Classe base astratta per raggruppare tutte le attivazioni
class ActivationLayer : public ILayer {
// Per ora vuota: diventerà utile con i template
};
class SigmoidLayer : public ActivationLayer {
public:
SigmoidLayer() = default;
Eigen::MatrixXd forward(const Eigen::MatrixXd& input) override;
ILayer* clone() const override;
};
class TanhLayer : public ActivationLayer {
public:
TanhLayer() = default;
Eigen::MatrixXd forward(const Eigen::MatrixXd& input) override;
ILayer* clone() const override;
};Sembra inutile una classe vuota, ma serve a distinguere una categoria: fra poco vorremo salvare il modello su file e chiedere “questo layer ha pesi da salvare?”. La risposta sarà data dal ramo della gerarchia, non da un if.
La tanh in Eigen è diretta:
SequentialNetwork net(4);
// Niente numeri magici: la sintassi dice cosa sta accadendo
net.add(new DenseLayer(784, 128, InitType::HE, DistroType::GAUSSIAN));
net.add(new SigmoidLayer());
net.add(new DenseLayer(128, 10, InitType::XAVIER, DistroType::UNIFORM));
net.add(new TanhLayer());
Eigen::MatrixXd X = Eigen::MatrixXd::Random(784, 1);
std::cout << net.forward(X).transpose() << std::endl;Verifica quantitativa: stampate la deviazione standard dei pesi del primo layer con net… ovvero, dal DenseLayer direttamente:
I due numeri devono coincidere a meno del 5%: è la prova che l’inizializzazione fa quello che dice.
Notate anche che l’output finale ora sta in \((-1, 1)\) e non in \((0, 1)\): l’ultimo layer è una tanh.
La soluzione di riferimento verrà discussa all’inizio della prossima lezione.