Ora è ottimizzato per la CPU
This commit is contained in:
parent
f070e1c985
commit
56f9afd031
File diff suppressed because one or more lines are too long
135
src/layer.zig
135
src/layer.zig
|
|
@ -1,41 +1,47 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const Sigmoid = @import("activations.zig").Sigmoid;
|
|
||||||
|
// Definiamo la larghezza del vettore SIMD (8 float alla volta = 256 bit)
|
||||||
|
const SimdWidth = 8;
|
||||||
|
const Vec = @Vector(SimdWidth, f32);
|
||||||
|
|
||||||
pub const DenseLayer = struct {
|
pub const DenseLayer = struct {
|
||||||
weights: []f32,
|
weights: []f32,
|
||||||
biases: []f32,
|
biases: []f32,
|
||||||
output: []f32,
|
output: []f32,
|
||||||
|
|
||||||
// Buffer per calcoli intermedi (per non riallocare memoria ogni volta)
|
|
||||||
deltas: []f32, // L'errore di questo strato
|
|
||||||
input_gradient: []f32, // L'errore da passare allo strato precedente
|
|
||||||
|
|
||||||
inputs_count: usize,
|
inputs_count: usize,
|
||||||
neurons_count: usize,
|
neurons_count: usize,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, in_size: usize, out_size: usize, seed: u64) !DenseLayer {
|
fn sigmoid(x: f32) f32 {
|
||||||
const weights = try allocator.alloc(f32, in_size * out_size);
|
return 1.0 / (1.0 + std.math.exp(-x));
|
||||||
const biases = try allocator.alloc(f32, out_size);
|
}
|
||||||
const output = try allocator.alloc(f32, out_size);
|
|
||||||
const deltas = try allocator.alloc(f32, out_size);
|
|
||||||
const input_gradient = try allocator.alloc(f32, in_size); // Importante!
|
|
||||||
|
|
||||||
|
fn sigmoidDerivative(x: f32) f32 {
|
||||||
|
return x * (1.0 - x);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, inputs: usize, neurons: usize, seed: u64) !DenseLayer {
|
||||||
|
const weights = try allocator.alloc(f32, inputs * neurons);
|
||||||
|
const biases = try allocator.alloc(f32, neurons);
|
||||||
|
const output = try allocator.alloc(f32, neurons);
|
||||||
|
|
||||||
|
// --- CORREZIONE QUI SOTTO ---
|
||||||
|
// Prima era: std.rand.DefaultPrng
|
||||||
|
// Ora è: std.Random.DefaultPrng
|
||||||
var prng = std.Random.DefaultPrng.init(seed);
|
var prng = std.Random.DefaultPrng.init(seed);
|
||||||
const rand = prng.random();
|
const random = prng.random();
|
||||||
|
|
||||||
for (weights) |*w| w.* = rand.float(f32) * 2.0 - 1.0;
|
// Inizializzazione Xavier/Glorot
|
||||||
|
for (weights) |*w| w.* = (random.float(f32) * 2.0 - 1.0) * 0.5;
|
||||||
for (biases) |*b| b.* = 0.0;
|
for (biases) |*b| b.* = 0.0;
|
||||||
|
|
||||||
return DenseLayer{
|
return DenseLayer{
|
||||||
.weights = weights,
|
.weights = weights,
|
||||||
.biases = biases,
|
.biases = biases,
|
||||||
.output = output,
|
.output = output,
|
||||||
.deltas = deltas,
|
.inputs_count = inputs,
|
||||||
.input_gradient = input_gradient,
|
.neurons_count = neurons,
|
||||||
.inputs_count = in_size,
|
|
||||||
.neurons_count = out_size,
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -44,50 +50,77 @@ pub const DenseLayer = struct {
|
||||||
self.allocator.free(self.weights);
|
self.allocator.free(self.weights);
|
||||||
self.allocator.free(self.biases);
|
self.allocator.free(self.biases);
|
||||||
self.allocator.free(self.output);
|
self.allocator.free(self.output);
|
||||||
self.allocator.free(self.deltas);
|
|
||||||
self.allocator.free(self.input_gradient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn forward(self: *DenseLayer, input: []const f32) []f32 {
|
// --- FORWARD PASS CON SIMD ---
|
||||||
|
pub fn forward(self: *DenseLayer, input: []const f32) []const f32 {
|
||||||
for (0..self.neurons_count) |n| {
|
for (0..self.neurons_count) |n| {
|
||||||
var sum: f32 = 0.0;
|
var sum: f32 = self.biases[n];
|
||||||
// Prodotto scalare (Input * Pesi)
|
const w_start = n * self.inputs_count;
|
||||||
for (0..self.inputs_count) |i| {
|
|
||||||
sum += input[i] * self.weights[n * self.inputs_count + i];
|
// 1. Processiamo a blocchi di 8 (SIMD)
|
||||||
|
var vec_sum: Vec = @splat(0.0);
|
||||||
|
var i: usize = 0;
|
||||||
|
|
||||||
|
while (i + SimdWidth <= self.inputs_count) : (i += SimdWidth) {
|
||||||
|
const v_in: Vec = input[i..][0..SimdWidth].*;
|
||||||
|
const v_w: Vec = self.weights[w_start + i ..][0..SimdWidth].*;
|
||||||
|
vec_sum += v_in * v_w;
|
||||||
}
|
}
|
||||||
// Aggiungiamo bias e applichiamo Sigmoide
|
|
||||||
self.output[n] = Sigmoid.apply(sum + self.biases[n]);
|
sum += @reduce(.Add, vec_sum);
|
||||||
|
|
||||||
|
// 2. Tail Loop
|
||||||
|
while (i < self.inputs_count) : (i += 1) {
|
||||||
|
sum += input[i] * self.weights[w_start + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.output[n] = sigmoid(sum);
|
||||||
}
|
}
|
||||||
return self.output;
|
return self.output;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Questa è la parte magica
|
// --- BACKWARD PASS CON SIMD ---
|
||||||
pub fn backward(self: *DenseLayer, next_layer_gradients: []const f32, prev_layer_input: []const f32, lr: f32) []f32 {
|
pub fn backward(self: *DenseLayer, output_gradient: []const f32, input_vals: []const f32, learning_rate: f32) []f32 {
|
||||||
// 1. Calcoliamo i "Delta" (Errore locale * derivata attivazione)
|
const input_gradient = self.allocator.alloc(f32, self.inputs_count) catch @panic("OOM");
|
||||||
for (0..self.neurons_count) |n| {
|
@memset(input_gradient, 0.0);
|
||||||
const derivative = Sigmoid.derivative(self.output[n]);
|
|
||||||
self.deltas[n] = next_layer_gradients[n] * derivative;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Calcoliamo il gradiente da passare indietro (Input Gradient)
|
for (0..self.neurons_count) |n| {
|
||||||
// Questo serve allo strato PRECEDENTE per correggersi
|
const delta = output_gradient[n] * sigmoidDerivative(self.output[n]);
|
||||||
@memset(self.input_gradient, 0.0);
|
const w_start = n * self.inputs_count;
|
||||||
for (0..self.inputs_count) |i| {
|
|
||||||
for (0..self.neurons_count) |n| {
|
self.biases[n] -= learning_rate * delta;
|
||||||
// Sommiamo il contributo di ogni neurone connesso a questo input
|
|
||||||
self.input_gradient[i] += self.deltas[n] * self.weights[n * self.inputs_count + i];
|
// SIMD LOOP
|
||||||
|
const v_delta: Vec = @splat(delta);
|
||||||
|
const v_lr: Vec = @splat(learning_rate);
|
||||||
|
const v_change_factor = v_delta * v_lr;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i + SimdWidth <= self.inputs_count) : (i += SimdWidth) {
|
||||||
|
var v_w: Vec = self.weights[w_start + i ..][0..SimdWidth].*;
|
||||||
|
const v_in: Vec = input_vals[i..][0..SimdWidth].*;
|
||||||
|
|
||||||
|
// Backprop error
|
||||||
|
var v_in_grad: Vec = input_gradient[i..][0..SimdWidth].*;
|
||||||
|
v_in_grad += v_w * v_delta;
|
||||||
|
input_gradient[i..][0..SimdWidth].* = v_in_grad;
|
||||||
|
|
||||||
|
// Update weights
|
||||||
|
v_w -= v_in * v_change_factor;
|
||||||
|
self.weights[w_start + i ..][0..SimdWidth].* = v_w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TAIL LOOP
|
||||||
|
while (i < self.inputs_count) : (i += 1) {
|
||||||
|
const w_idx = w_start + i;
|
||||||
|
const old_weight = self.weights[w_idx];
|
||||||
|
|
||||||
|
input_gradient[i] += old_weight * delta;
|
||||||
|
self.weights[w_idx] -= input_vals[i] * delta * learning_rate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Aggiorniamo i pesi e i bias (Gradient Descent)
|
return input_gradient;
|
||||||
for (0..self.neurons_count) |n| {
|
|
||||||
for (0..self.inputs_count) |i| {
|
|
||||||
// Peso -= LearningRate * Delta * InputOriginale
|
|
||||||
self.weights[n * self.inputs_count + i] -= lr * self.deltas[n] * prev_layer_input[i];
|
|
||||||
}
|
|
||||||
self.biases[n] -= lr * self.deltas[n];
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.input_gradient;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
99
src/main.zig
99
src/main.zig
|
|
@ -8,56 +8,85 @@ pub fn main() !void {
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
|
|
||||||
std.debug.print("--- CARICAMENTO MNIST ---\n", .{});
|
std.debug.print("--- CARICAMENTO MNIST ---\n", .{});
|
||||||
|
var dataset = try MnistData.init(allocator, "data/train-images-idx3-ubyte", "data/train-labels-idx1-ubyte", 5000);
|
||||||
// Carichiamo solo 1000 immagini per iniziare (per vedere se funziona veloce)
|
|
||||||
// I file sono nella cartella "data/"
|
|
||||||
var dataset = try MnistData.init(allocator, "data/train-images-idx3-ubyte", "data/train-labels-idx1-ubyte", 2000);
|
|
||||||
defer dataset.deinit();
|
defer dataset.deinit();
|
||||||
|
|
||||||
std.debug.print("Caricate {d} immagini.\n", .{dataset.images.len});
|
const save_path = "brain.bin";
|
||||||
|
var net: Network = undefined;
|
||||||
|
var needs_training = true;
|
||||||
|
|
||||||
// --- ARCHITETTURA RETE ---
|
// Load or Init
|
||||||
var net = Network.init(allocator);
|
if (Network.load(allocator, save_path)) |loaded_net| {
|
||||||
|
std.debug.print(">>> SALVATAGGIO TROVATO! Skip training.\n", .{});
|
||||||
|
net = loaded_net;
|
||||||
|
needs_training = false;
|
||||||
|
} else |_| {
|
||||||
|
std.debug.print(">>> NUOVA RETE.\n", .{});
|
||||||
|
net = Network.init(allocator);
|
||||||
|
try net.addLayer(784, 64, 111);
|
||||||
|
try net.addLayer(64, 32, 222);
|
||||||
|
try net.addLayer(32, 10, 333);
|
||||||
|
}
|
||||||
defer net.deinit();
|
defer net.deinit();
|
||||||
|
|
||||||
// 784 Input -> 64 Hidden -> 32 Hidden -> 10 Output
|
// --- TRAINING ---
|
||||||
try net.addLayer(784, 64, 111);
|
if (needs_training) {
|
||||||
try net.addLayer(64, 32, 222);
|
std.debug.print("--- INIZIO TRAINING (SIMD ACCELERATED) ---\n", .{});
|
||||||
try net.addLayer(32, 10, 333);
|
const lr: f32 = 0.1;
|
||||||
|
const epochs = 10; // Bastano meno epoche ora
|
||||||
|
|
||||||
net.printTopology();
|
var epoch: usize = 0;
|
||||||
|
while (epoch < epochs) : (epoch += 1) {
|
||||||
|
var total_loss: f32 = 0.0;
|
||||||
|
var correct: usize = 0;
|
||||||
|
|
||||||
std.debug.print("--- INIZIO TRAINING MNIST ---\n", .{});
|
for (dataset.images, 0..) |img, i| {
|
||||||
|
total_loss += try net.train(img, dataset.labels[i], lr);
|
||||||
const lr: f32 = 0.1;
|
const out = net.forward(img);
|
||||||
const epochs = 50; // Meno epoche, ma ogni epoca elabora 2000 immagini!
|
if (argmax(out) == argmax(dataset.labels[i])) correct += 1;
|
||||||
|
|
||||||
var epoch: usize = 0;
|
|
||||||
while (epoch < epochs) : (epoch += 1) {
|
|
||||||
var total_loss: f32 = 0.0;
|
|
||||||
var correct: usize = 0;
|
|
||||||
|
|
||||||
for (dataset.images, 0..) |img, i| {
|
|
||||||
// Training
|
|
||||||
total_loss += try net.train(img, dataset.labels[i], lr);
|
|
||||||
|
|
||||||
// Calcolo precisione (Accuracy) al volo
|
|
||||||
const out = net.forward(img);
|
|
||||||
if (argmax(out) == argmax(dataset.labels[i])) {
|
|
||||||
correct += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accuracy = @as(f32, @floatFromInt(correct)) / @as(f32, @floatFromInt(dataset.images.len)) * 100.0;
|
||||||
|
std.debug.print("Epoca {d}: Acc: {d:.2}%\n", .{ epoch, accuracy });
|
||||||
|
|
||||||
|
// Durante il training passiamo 'null' come immagine per non rallentare troppo
|
||||||
|
try net.exportJSON("network_state.json", epoch, total_loss / 5000.0, null);
|
||||||
}
|
}
|
||||||
|
try net.save(save_path);
|
||||||
|
}
|
||||||
|
|
||||||
const accuracy = @as(f32, @floatFromInt(correct)) / @as(f32, @floatFromInt(dataset.images.len)) * 100.0;
|
// --- SHOWCASE MODE (Il Gran Finale) ---
|
||||||
|
std.debug.print("\n--- AVVIO DEMO VISUALE ---\n", .{});
|
||||||
|
std.debug.print("Guarda il browser! (CTRL+C per uscire)\n", .{});
|
||||||
|
|
||||||
std.debug.print("Epoca {d}: Loss {d:.4} | Accuracy: {d:.2}%\n", .{ epoch, total_loss / @as(f32, @floatFromInt(dataset.images.len)), accuracy });
|
var prng = std.Random.DefaultPrng.init(0);
|
||||||
|
const random = prng.random();
|
||||||
|
|
||||||
// Salviamo lo stato per il visualizer (vedrai un "cervello" molto complesso!)
|
while (true) {
|
||||||
try net.exportJSON("network_state.json", epoch, total_loss);
|
// 1. Pesca un'immagine a caso
|
||||||
|
const idx = random.intRangeAtMost(usize, 0, dataset.images.len - 1);
|
||||||
|
const img = dataset.images[idx];
|
||||||
|
const label = argmax(dataset.labels[idx]);
|
||||||
|
|
||||||
|
// 2. Fai la previsione
|
||||||
|
const out = net.forward(img);
|
||||||
|
const prediction = argmax(out);
|
||||||
|
|
||||||
|
// Calcoliamo una "Loss" finta solo per il grafico
|
||||||
|
const loss: f32 = if (prediction == label) 0.0 else 1.0;
|
||||||
|
|
||||||
|
// 3. Stampa su console
|
||||||
|
const result_str = if (prediction == label) "CORRETTO" else "SBAGLIATO";
|
||||||
|
std.debug.print("Input: {d} | AI Dice: {d} -> {s}\r", .{ label, prediction, result_str });
|
||||||
|
|
||||||
|
// 4. ESPORTA TUTTO (inclusa l'immagine) per il browser
|
||||||
|
try net.exportJSON("network_state.json", 999, loss, img);
|
||||||
|
|
||||||
|
// 5. Aspetta un secondo per farci godere la scena
|
||||||
|
std.Thread.sleep(1000 * 1_000_000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funzione helper per trovare l'indice del valore più alto (es: quale numero è?)
|
|
||||||
fn argmax(slice: []const f32) usize {
|
fn argmax(slice: []const f32) usize {
|
||||||
var max_val: f32 = -1000.0;
|
var max_val: f32 = -1000.0;
|
||||||
var max_idx: usize = 0;
|
var max_idx: usize = 0;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ pub const Network = struct {
|
||||||
return current_input;
|
return current_input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- QUESTA ERA LA FUNZIONE MANCANTE ---
|
||||||
pub fn printTopology(self: *Network) void {
|
pub fn printTopology(self: *Network) void {
|
||||||
std.debug.print("Architettura Rete: [Input]", .{});
|
std.debug.print("Architettura Rete: [Input]", .{});
|
||||||
for (self.layers.items) |layer| {
|
for (self.layers.items) |layer| {
|
||||||
|
|
@ -40,8 +41,131 @@ pub const Network = struct {
|
||||||
}
|
}
|
||||||
std.debug.print("\n", .{});
|
std.debug.print("\n", .{});
|
||||||
}
|
}
|
||||||
|
// ---------------------------------------
|
||||||
|
|
||||||
pub fn exportJSON(self: *Network, file_path: []const u8, epoch: usize, loss: f32) !void {
|
pub fn train(self: *Network, input: []const f32, target: []const f32, lr: f32) !f32 {
|
||||||
|
_ = self.forward(input);
|
||||||
|
|
||||||
|
const last_layer_idx = self.layers.items.len - 1;
|
||||||
|
const last_layer = &self.layers.items[last_layer_idx];
|
||||||
|
|
||||||
|
// Questo è il buffer iniziale degli errori
|
||||||
|
var output_errors = try self.allocator.alloc(f32, last_layer.neurons_count);
|
||||||
|
defer self.allocator.free(output_errors); // Zig pulirà questo automaticamente alla fine
|
||||||
|
|
||||||
|
var total_loss: f32 = 0.0;
|
||||||
|
for (0..last_layer.neurons_count) |i| {
|
||||||
|
const err = last_layer.output[i] - target[i];
|
||||||
|
output_errors[i] = err;
|
||||||
|
total_loss += err * err;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next_gradients = output_errors;
|
||||||
|
var i: usize = self.layers.items.len;
|
||||||
|
|
||||||
|
while (i > 0) {
|
||||||
|
i -= 1;
|
||||||
|
var layer = &self.layers.items[i];
|
||||||
|
const prev_input = if (i == 0) input else self.layers.items[i - 1].output;
|
||||||
|
|
||||||
|
// 1. Calcoliamo i nuovi gradienti (nuova allocazione in memoria)
|
||||||
|
const new_gradients = layer.backward(next_gradients, prev_input, lr);
|
||||||
|
|
||||||
|
// 2. CHECK ANTI-LEAK:
|
||||||
|
// Se il vecchio next_gradients NON è output_errors (che è protetto dal defer),
|
||||||
|
// allora è un buffer intermedio creato dal layer precedente. Dobbiamo distruggerlo.
|
||||||
|
if (next_gradients.ptr != output_errors.ptr) {
|
||||||
|
self.allocator.free(next_gradients);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Aggiorniamo il puntatore per il prossimo giro
|
||||||
|
next_gradients = new_gradients;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Pulizia Finale:
|
||||||
|
// L'ultimo buffer creato dal primo layer (input layer) è rimasto in mano nostra.
|
||||||
|
// Dobbiamo liberare anche quello.
|
||||||
|
if (next_gradients.ptr != output_errors.ptr) {
|
||||||
|
self.allocator.free(next_gradients);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total_loss;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SALVATAGGIO BINARIO ---
|
||||||
|
pub fn save(self: *Network, file_path: []const u8) !void {
|
||||||
|
const file = try std.fs.cwd().createFile(file_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const MagicNumber: u64 = 0xDEADBEEF;
|
||||||
|
|
||||||
|
// Scriviamo Header
|
||||||
|
try file.writeAll(std.mem.asBytes(&MagicNumber));
|
||||||
|
|
||||||
|
const layer_count = @as(u64, self.layers.items.len);
|
||||||
|
try file.writeAll(std.mem.asBytes(&layer_count));
|
||||||
|
|
||||||
|
// Scriviamo Layers
|
||||||
|
for (self.layers.items) |layer| {
|
||||||
|
const inputs = @as(u64, layer.inputs_count);
|
||||||
|
const neurons = @as(u64, layer.neurons_count);
|
||||||
|
try file.writeAll(std.mem.asBytes(&inputs));
|
||||||
|
try file.writeAll(std.mem.asBytes(&neurons));
|
||||||
|
|
||||||
|
// Scriviamo Pesi e Bias
|
||||||
|
const weights_bytes = std.mem.sliceAsBytes(layer.weights);
|
||||||
|
try file.writeAll(weights_bytes);
|
||||||
|
|
||||||
|
const biases_bytes = std.mem.sliceAsBytes(layer.biases);
|
||||||
|
try file.writeAll(biases_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CARICAMENTO BINARIO ---
|
||||||
|
pub fn load(allocator: Allocator, file_path: []const u8) !Network {
|
||||||
|
const file = try std.fs.cwd().openFile(file_path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
var net = Network.init(allocator);
|
||||||
|
var success = false;
|
||||||
|
|
||||||
|
defer if (!success) net.deinit();
|
||||||
|
|
||||||
|
// Header Check
|
||||||
|
var magic: u64 = 0;
|
||||||
|
_ = try file.readAll(std.mem.asBytes(&magic));
|
||||||
|
if (magic != 0xDEADBEEF) return error.InvalidNetworkFile;
|
||||||
|
|
||||||
|
var layer_count: u64 = 0;
|
||||||
|
_ = try file.readAll(std.mem.asBytes(&layer_count));
|
||||||
|
|
||||||
|
// Layers Loop
|
||||||
|
var i: u64 = 0;
|
||||||
|
while (i < layer_count) : (i += 1) {
|
||||||
|
var inputs: u64 = 0;
|
||||||
|
var neurons: u64 = 0;
|
||||||
|
|
||||||
|
_ = try file.readAll(std.mem.asBytes(&inputs));
|
||||||
|
_ = try file.readAll(std.mem.asBytes(&neurons));
|
||||||
|
|
||||||
|
try net.addLayer(@intCast(inputs), @intCast(neurons), 0);
|
||||||
|
|
||||||
|
const layer = &net.layers.items[net.layers.items.len - 1];
|
||||||
|
|
||||||
|
const weights_bytes = std.mem.sliceAsBytes(layer.weights);
|
||||||
|
_ = try file.readAll(weights_bytes);
|
||||||
|
|
||||||
|
const biases_bytes = std.mem.sliceAsBytes(layer.biases);
|
||||||
|
_ = try file.readAll(biases_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
success = true;
|
||||||
|
return net;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- EXPORT JSON CON IMMAGINE INPUT ---
|
||||||
|
// Aggiungiamo il parametro 'input_snapshot' (può essere null se non vogliamo stampare nulla)
|
||||||
|
pub fn exportJSON(self: *Network, file_path: []const u8, epoch: usize, loss: f32, input_snapshot: ?[]const f32) !void {
|
||||||
const file = try std.fs.cwd().createFile(file_path, .{});
|
const file = try std.fs.cwd().createFile(file_path, .{});
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
||||||
|
|
@ -53,48 +177,35 @@ pub const Network = struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
try Utils.print(file, "{{\n \"epoch\": {d},\n \"loss\": {d:.6},\n \"layers\": [\n", .{ epoch, loss });
|
try Utils.print(file, "{{\n \"epoch\": {d},\n \"loss\": {d:.6},\n", .{ epoch, loss });
|
||||||
|
|
||||||
|
// SEZIONE NUOVA: Stampiamo l'array dei pixel se presente
|
||||||
|
if (input_snapshot) |pixels| {
|
||||||
|
try file.writeAll(" \"input_pixels\": [");
|
||||||
|
for (pixels, 0..) |p, idx| {
|
||||||
|
// Arrotondiamo a 2 decimali per risparmiare spazio
|
||||||
|
try Utils.print(file, "{d:.2}", .{p});
|
||||||
|
if (idx < pixels.len - 1) try file.writeAll(",");
|
||||||
|
}
|
||||||
|
try file.writeAll("],\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
try file.writeAll(" \"layers\": [\n");
|
||||||
|
|
||||||
for (self.layers.items, 0..) |layer, i| {
|
for (self.layers.items, 0..) |layer, i| {
|
||||||
try Utils.print(file, " {{\n \"layer_index\": {d},\n \"neurons\": {d},\n \"inputs\": {d},\n \"weights\": [", .{ i, layer.neurons_count, layer.inputs_count });
|
try Utils.print(file, " {{\n \"layer_index\": {d},\n \"neurons\": {d},\n \"inputs\": {d},\n \"weights\": [", .{ i, layer.neurons_count, layer.inputs_count });
|
||||||
|
|
||||||
for (layer.weights, 0..) |w, w_idx| {
|
// Salviamo solo un sottoinsieme dei pesi per non intasare il file
|
||||||
|
const max_w = if (layer.weights.len > 1000) 100 else layer.weights.len;
|
||||||
|
|
||||||
|
for (layer.weights[0..max_w], 0..) |w, w_idx| {
|
||||||
try Utils.print(file, "{d:.4}", .{w});
|
try Utils.print(file, "{d:.4}", .{w});
|
||||||
if (w_idx < layer.weights.len - 1) try file.writeAll(", ");
|
if (w_idx < max_w - 1) try file.writeAll(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
try file.writeAll("]\n }");
|
try file.writeAll("]\n }");
|
||||||
if (i < self.layers.items.len - 1) try file.writeAll(",\n");
|
if (i < self.layers.items.len - 1) try file.writeAll(",\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
try file.writeAll("\n ]\n}\n");
|
try file.writeAll("\n ]\n}\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn train(self: *Network, input: []const f32, target: []const f32, lr: f32) !f32 {
|
|
||||||
_ = self.forward(input);
|
|
||||||
|
|
||||||
const last_layer_idx = self.layers.items.len - 1;
|
|
||||||
const last_layer = &self.layers.items[last_layer_idx];
|
|
||||||
|
|
||||||
var output_errors = try self.allocator.alloc(f32, last_layer.neurons_count);
|
|
||||||
defer self.allocator.free(output_errors);
|
|
||||||
|
|
||||||
var total_loss: f32 = 0.0;
|
|
||||||
for (0..last_layer.neurons_count) |i| {
|
|
||||||
const err = last_layer.output[i] - target[i];
|
|
||||||
output_errors[i] = err;
|
|
||||||
total_loss += err * err;
|
|
||||||
}
|
|
||||||
|
|
||||||
var next_gradients = output_errors;
|
|
||||||
var i: usize = self.layers.items.len;
|
|
||||||
while (i > 0) {
|
|
||||||
i -= 1;
|
|
||||||
var layer = &self.layers.items[i];
|
|
||||||
const prev_input = if (i == 0) input else self.layers.items[i - 1].output;
|
|
||||||
next_gradients = layer.backward(next_gradients, prev_input, lr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return total_loss;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
125
visualizer.html
125
visualizer.html
|
|
@ -50,24 +50,53 @@
|
||||||
const epochEl = document.getElementById('epoch');
|
const epochEl = document.getElementById('epoch');
|
||||||
const lossEl = document.getElementById('loss');
|
const lossEl = document.getElementById('loss');
|
||||||
|
|
||||||
// Configurazione Spaziatura
|
// Configurazione
|
||||||
const MIN_NODE_SPACING = 20; // Spazio minimo verticale tra i nodi (pixel)
|
const MIN_NODE_SPACING = 20;
|
||||||
const LAYER_PADDING = 100; // Margine sopra e sotto
|
const LAYER_PADDING = 100;
|
||||||
|
|
||||||
|
// --- NUOVO: Funzione per disegnare la cifra input ---
|
||||||
|
function drawInputImage(pixels) {
|
||||||
|
if (!pixels || pixels.length === 0) return;
|
||||||
|
|
||||||
|
// Disegniamo un box in alto a sinistra
|
||||||
|
const scale = 4; // Zoom 4x
|
||||||
|
const startX = 20;
|
||||||
|
const startY = 120; // Sotto le scritte di info
|
||||||
|
|
||||||
|
// Sfondo nero
|
||||||
|
ctx.fillStyle = "#000";
|
||||||
|
ctx.fillRect(startX - 2, startY - 2, (28 * scale) + 4, (28 * scale) + 4);
|
||||||
|
ctx.strokeStyle = "#f4a261";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeRect(startX - 2, startY - 2, (28 * scale) + 4, (28 * scale) + 4);
|
||||||
|
|
||||||
|
for (let i = 0; i < 784; i++) {
|
||||||
|
const val = pixels[i];
|
||||||
|
if (val > 0.1) { // Disegna solo se non è nero
|
||||||
|
const x = (i % 28);
|
||||||
|
const y = Math.floor(i / 28);
|
||||||
|
|
||||||
|
// Scala di grigi basata sul valore
|
||||||
|
const brightness = Math.floor(val * 255);
|
||||||
|
ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`;
|
||||||
|
ctx.fillRect(startX + (x * scale), startY + (y * scale), scale, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etichetta
|
||||||
|
ctx.fillStyle = "#fff";
|
||||||
|
ctx.font = "14px monospace";
|
||||||
|
ctx.fillText("AI INPUT:", startX, startY - 10);
|
||||||
|
}
|
||||||
|
|
||||||
function drawNetwork(data) {
|
function drawNetwork(data) {
|
||||||
const layers = data.layers;
|
const layers = data.layers;
|
||||||
// Ricostruiamo la struttura completa [Input, Hidden1, Hidden2, Output]
|
|
||||||
const structure = [layers[0].inputs];
|
const structure = [layers[0].inputs];
|
||||||
layers.forEach(l => structure.push(l.neurons));
|
layers.forEach(l => structure.push(l.neurons));
|
||||||
|
|
||||||
// 1. CALCOLO DIMENSIONI CANVAS
|
|
||||||
// Troviamo il layer più grande (probabilmente l'input con 784)
|
|
||||||
const maxNeurons = Math.max(...structure);
|
const maxNeurons = Math.max(...structure);
|
||||||
|
|
||||||
// Calcoliamo l'altezza necessaria
|
|
||||||
const requiredHeight = (maxNeurons * MIN_NODE_SPACING) + (LAYER_PADDING * 2);
|
const requiredHeight = (maxNeurons * MIN_NODE_SPACING) + (LAYER_PADDING * 2);
|
||||||
|
|
||||||
// Ridimensioniamo il canvas se necessario (evita flickering se la size non cambia)
|
|
||||||
if (canvas.height !== requiredHeight || canvas.width !== window.innerWidth) {
|
if (canvas.height !== requiredHeight || canvas.width !== window.innerWidth) {
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = Math.max(window.innerHeight, requiredHeight);
|
canvas.height = Math.max(window.innerHeight, requiredHeight);
|
||||||
|
|
@ -75,38 +104,33 @@
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layerWidth = canvas.width / structure.length;
|
// --- DISEGNA L'INPUT PRIMA DI TUTTO ---
|
||||||
|
if (data.input_pixels) {
|
||||||
|
drawInputImage(data.input_pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerWidth = (canvas.width - 200) / structure.length; // Lasciamo spazio a sx per l'immagine
|
||||||
|
const offsetX = 150; // Spostiamo tutto a destra
|
||||||
|
|
||||||
let nodePositions = [];
|
let nodePositions = [];
|
||||||
|
|
||||||
// 2. CALCOLO POSIZIONI NODI
|
|
||||||
structure.forEach((neuronCount, layerIdx) => {
|
structure.forEach((neuronCount, layerIdx) => {
|
||||||
const x = (layerIdx * layerWidth) + (layerWidth / 2);
|
const x = offsetX + (layerIdx * layerWidth) + (layerWidth / 2);
|
||||||
const layerNodes = [];
|
let spacing = (canvas.height - LAYER_PADDING * 2) / neuronCount;
|
||||||
|
|
||||||
// Calcoliamo lo spazio disponibile per QUESTO layer
|
|
||||||
// Se sono pochi neuroni, li spalmiamo su tutta l'altezza del canvas per estetica
|
|
||||||
// Se sono tanti, usiamo lo spacing minimo
|
|
||||||
let availableHeight = canvas.height - (LAYER_PADDING * 2);
|
|
||||||
let spacing = availableHeight / neuronCount;
|
|
||||||
|
|
||||||
// Cap sulla spaziatura massima per non averli troppo distanti nei layer piccoli
|
|
||||||
if (spacing > 100) spacing = 100;
|
if (spacing > 100) spacing = 100;
|
||||||
|
|
||||||
// Altezza totale occupata da questo layer
|
|
||||||
const layerTotalHeight = spacing * neuronCount;
|
const layerTotalHeight = spacing * neuronCount;
|
||||||
const startY = (canvas.height / 2) - (layerTotalHeight / 2);
|
const startY = (canvas.height / 2) - (layerTotalHeight / 2);
|
||||||
|
|
||||||
|
const layerNodes = [];
|
||||||
for (let i = 0; i < neuronCount; i++) {
|
for (let i = 0; i < neuronCount; i++) {
|
||||||
const y = startY + (i * spacing);
|
layerNodes.push({x, startY: startY + (i * spacing)});
|
||||||
layerNodes.push({x, y});
|
|
||||||
}
|
}
|
||||||
nodePositions.push(layerNodes);
|
nodePositions.push(layerNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. DISEGNO CONNESSIONI (Pesi)
|
// Disegno connessioni
|
||||||
// Per MNIST, disegniamo solo connessioni forti per non uccidere la CPU del browser
|
const DRAW_THRESHOLD = 0.05;
|
||||||
const DRAW_THRESHOLD = 0.05; // Disegna solo se peso > 0.05
|
|
||||||
|
|
||||||
layers.forEach((layer, lIdx) => {
|
layers.forEach((layer, lIdx) => {
|
||||||
const sourceNodes = nodePositions[lIdx];
|
const sourceNodes = nodePositions[lIdx];
|
||||||
const targetNodes = nodePositions[lIdx + 1];
|
const targetNodes = nodePositions[lIdx + 1];
|
||||||
|
|
@ -115,35 +139,48 @@
|
||||||
sourceNodes.forEach((source, inputIdx) => {
|
sourceNodes.forEach((source, inputIdx) => {
|
||||||
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
|
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
|
||||||
const weight = layer.weights[weightIdx];
|
const weight = layer.weights[weightIdx];
|
||||||
|
|
||||||
// Ottimizzazione: salta pesi quasi nulli
|
|
||||||
if (Math.abs(weight) < DRAW_THRESHOLD) return;
|
if (Math.abs(weight) < DRAW_THRESHOLD) return;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(source.x, source.y);
|
ctx.moveTo(source.x, source.startY);
|
||||||
ctx.lineTo(target.x, target.y);
|
ctx.lineTo(target.x, target.startY);
|
||||||
|
|
||||||
const alpha = Math.min(Math.abs(weight), 0.8);
|
const alpha = Math.min(Math.abs(weight), 0.5); // Più trasparente per chiarezza
|
||||||
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})`
|
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})` : `rgba(255, 60, 60, ${alpha})`;
|
||||||
: `rgba(255, 60, 60, ${alpha})`;
|
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. DISEGNO NODI
|
// Disegno Nodi
|
||||||
nodePositions.forEach((layerNodes, lIdx) => {
|
nodePositions.forEach((layerNodes, lIdx) => {
|
||||||
// Dimensione dinamica del nodo: più ce ne sono, più sono piccoli
|
|
||||||
const nodeRadius = Math.max(3, Math.min(15, 300 / layerNodes.length));
|
const nodeRadius = Math.max(3, Math.min(15, 300 / layerNodes.length));
|
||||||
|
|
||||||
layerNodes.forEach((node) => {
|
// Evidenziamo l'output attivo nell'ultimo layer!
|
||||||
|
let maxOutIdx = -1;
|
||||||
|
let maxOutVal = -999;
|
||||||
|
|
||||||
|
// Se siamo nell'ultimo layer, cerchiamo il vincitore
|
||||||
|
if (lIdx === nodePositions.length - 1) {
|
||||||
|
// Nota: Non abbiamo i valori di output nel JSON, solo i pesi statici.
|
||||||
|
// Quindi coloriamo solo i nodi genericamente
|
||||||
|
}
|
||||||
|
|
||||||
|
layerNodes.forEach((node, nIdx) => {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
|
ctx.arc(node.x, node.startY, nodeRadius, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = '#222';
|
ctx.fillStyle = '#222';
|
||||||
|
|
||||||
|
// Hack visivo: Se siamo nell'ultimo layer, coloriamo i nodi in base all'indice (0-9)
|
||||||
|
if (lIdx === nodePositions.length - 1) {
|
||||||
|
ctx.fillStyle = '#444';
|
||||||
|
// Aggiungiamo il numero dentro il nodo
|
||||||
|
ctx.fillText(nIdx, node.x + 20, node.startY + 5);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.strokeStyle = '#fff';
|
ctx.strokeStyle = '#fff';
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -155,15 +192,13 @@
|
||||||
if (!response.ok) throw new Error("File missing");
|
if (!response.ok) throw new Error("File missing");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
epochEl.innerText = data.epoch;
|
epochEl.innerText = (data.epoch === 999) ? "DEMO MODE" : data.epoch;
|
||||||
lossEl.innerText = data.loss;
|
lossEl.innerText = data.loss;
|
||||||
drawNetwork(data);
|
drawNetwork(data);
|
||||||
} catch (e) {
|
} catch (e) { }
|
||||||
// console.log("Waiting...", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(update, 100); // Aggiorna ogni 500ms
|
setInterval(update, 500);
|
||||||
window.addEventListener('resize', () => canvas.width = window.innerWidth);
|
window.addEventListener('resize', () => canvas.width = window.innerWidth);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue