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
131
src/layer.zig
131
src/layer.zig
|
|
@ -1,41 +1,47 @@
|
|||
const std = @import("std");
|
||||
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 {
|
||||
weights: []f32,
|
||||
biases: []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,
|
||||
neurons_count: usize,
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn init(allocator: Allocator, in_size: usize, out_size: usize, seed: u64) !DenseLayer {
|
||||
const weights = try allocator.alloc(f32, in_size * out_size);
|
||||
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 sigmoid(x: f32) f32 {
|
||||
return 1.0 / (1.0 + std.math.exp(-x));
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
|
||||
return DenseLayer{
|
||||
.weights = weights,
|
||||
.biases = biases,
|
||||
.output = output,
|
||||
.deltas = deltas,
|
||||
.input_gradient = input_gradient,
|
||||
.inputs_count = in_size,
|
||||
.neurons_count = out_size,
|
||||
.inputs_count = inputs,
|
||||
.neurons_count = neurons,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
|
@ -44,50 +50,77 @@ pub const DenseLayer = struct {
|
|||
self.allocator.free(self.weights);
|
||||
self.allocator.free(self.biases);
|
||||
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| {
|
||||
var sum: f32 = 0.0;
|
||||
// Prodotto scalare (Input * Pesi)
|
||||
for (0..self.inputs_count) |i| {
|
||||
sum += input[i] * self.weights[n * self.inputs_count + i];
|
||||
var sum: f32 = self.biases[n];
|
||||
const w_start = n * self.inputs_count;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Questa è la parte magica
|
||||
pub fn backward(self: *DenseLayer, next_layer_gradients: []const f32, prev_layer_input: []const f32, lr: f32) []f32 {
|
||||
// 1. Calcoliamo i "Delta" (Errore locale * derivata attivazione)
|
||||
// --- BACKWARD PASS CON SIMD ---
|
||||
pub fn backward(self: *DenseLayer, output_gradient: []const f32, input_vals: []const f32, learning_rate: f32) []f32 {
|
||||
const input_gradient = self.allocator.alloc(f32, self.inputs_count) catch @panic("OOM");
|
||||
@memset(input_gradient, 0.0);
|
||||
|
||||
for (0..self.neurons_count) |n| {
|
||||
const derivative = Sigmoid.derivative(self.output[n]);
|
||||
self.deltas[n] = next_layer_gradients[n] * derivative;
|
||||
const delta = output_gradient[n] * sigmoidDerivative(self.output[n]);
|
||||
const w_start = n * self.inputs_count;
|
||||
|
||||
self.biases[n] -= learning_rate * delta;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 2. Calcoliamo il gradiente da passare indietro (Input Gradient)
|
||||
// Questo serve allo strato PRECEDENTE per correggersi
|
||||
@memset(self.input_gradient, 0.0);
|
||||
for (0..self.inputs_count) |i| {
|
||||
for (0..self.neurons_count) |n| {
|
||||
// Sommiamo il contributo di ogni neurone connesso a questo input
|
||||
self.input_gradient[i] += self.deltas[n] * self.weights[n * self.inputs_count + i];
|
||||
// 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)
|
||||
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;
|
||||
return input_gradient;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
79
src/main.zig
79
src/main.zig
|
|
@ -8,29 +8,32 @@ pub fn main() !void {
|
|||
defer _ = gpa.deinit();
|
||||
|
||||
std.debug.print("--- CARICAMENTO MNIST ---\n", .{});
|
||||
|
||||
// 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);
|
||||
var dataset = try MnistData.init(allocator, "data/train-images-idx3-ubyte", "data/train-labels-idx1-ubyte", 5000);
|
||||
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 ---
|
||||
var net = Network.init(allocator);
|
||||
defer net.deinit();
|
||||
|
||||
// 784 Input -> 64 Hidden -> 32 Hidden -> 10 Output
|
||||
// Load or Init
|
||||
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();
|
||||
|
||||
net.printTopology();
|
||||
|
||||
std.debug.print("--- INIZIO TRAINING MNIST ---\n", .{});
|
||||
|
||||
// --- TRAINING ---
|
||||
if (needs_training) {
|
||||
std.debug.print("--- INIZIO TRAINING (SIMD ACCELERATED) ---\n", .{});
|
||||
const lr: f32 = 0.1;
|
||||
const epochs = 50; // Meno epoche, ma ogni epoca elabora 2000 immagini!
|
||||
const epochs = 10; // Bastano meno epoche ora
|
||||
|
||||
var epoch: usize = 0;
|
||||
while (epoch < epochs) : (epoch += 1) {
|
||||
|
|
@ -38,26 +41,52 @@ pub fn main() !void {
|
|||
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;
|
||||
}
|
||||
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 });
|
||||
|
||||
std.debug.print("Epoca {d}: Loss {d:.4} | Accuracy: {d:.2}%\n", .{ epoch, total_loss / @as(f32, @floatFromInt(dataset.images.len)), 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);
|
||||
}
|
||||
|
||||
// Salviamo lo stato per il visualizer (vedrai un "cervello" molto complesso!)
|
||||
try net.exportJSON("network_state.json", epoch, total_loss);
|
||||
// --- SHOWCASE MODE (Il Gran Finale) ---
|
||||
std.debug.print("\n--- AVVIO DEMO VISUALE ---\n", .{});
|
||||
std.debug.print("Guarda il browser! (CTRL+C per uscire)\n", .{});
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(0);
|
||||
const random = prng.random();
|
||||
|
||||
while (true) {
|
||||
// 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 {
|
||||
var max_val: f32 = -1000.0;
|
||||
var max_idx: usize = 0;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ pub const Network = struct {
|
|||
return current_input;
|
||||
}
|
||||
|
||||
// --- QUESTA ERA LA FUNZIONE MANCANTE ---
|
||||
pub fn printTopology(self: *Network) void {
|
||||
std.debug.print("Architettura Rete: [Input]", .{});
|
||||
for (self.layers.items) |layer| {
|
||||
|
|
@ -40,8 +41,131 @@ pub const Network = struct {
|
|||
}
|
||||
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, .{});
|
||||
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| {
|
||||
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});
|
||||
if (w_idx < layer.weights.len - 1) try file.writeAll(", ");
|
||||
if (w_idx < max_w - 1) try file.writeAll(", ");
|
||||
}
|
||||
|
||||
try file.writeAll("]\n }");
|
||||
if (i < self.layers.items.len - 1) try file.writeAll(",\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 lossEl = document.getElementById('loss');
|
||||
|
||||
// Configurazione Spaziatura
|
||||
const MIN_NODE_SPACING = 20; // Spazio minimo verticale tra i nodi (pixel)
|
||||
const LAYER_PADDING = 100; // Margine sopra e sotto
|
||||
// Configurazione
|
||||
const MIN_NODE_SPACING = 20;
|
||||
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) {
|
||||
const layers = data.layers;
|
||||
// Ricostruiamo la struttura completa [Input, Hidden1, Hidden2, Output]
|
||||
const structure = [layers[0].inputs];
|
||||
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);
|
||||
|
||||
// Calcoliamo l'altezza necessaria
|
||||
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) {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = Math.max(window.innerHeight, requiredHeight);
|
||||
|
|
@ -75,38 +104,33 @@
|
|||
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 = [];
|
||||
|
||||
// 2. CALCOLO POSIZIONI NODI
|
||||
structure.forEach((neuronCount, layerIdx) => {
|
||||
const x = (layerIdx * layerWidth) + (layerWidth / 2);
|
||||
const layerNodes = [];
|
||||
|
||||
// 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
|
||||
const x = offsetX + (layerIdx * layerWidth) + (layerWidth / 2);
|
||||
let spacing = (canvas.height - LAYER_PADDING * 2) / neuronCount;
|
||||
if (spacing > 100) spacing = 100;
|
||||
|
||||
// Altezza totale occupata da questo layer
|
||||
const layerTotalHeight = spacing * neuronCount;
|
||||
const startY = (canvas.height / 2) - (layerTotalHeight / 2);
|
||||
|
||||
const layerNodes = [];
|
||||
for (let i = 0; i < neuronCount; i++) {
|
||||
const y = startY + (i * spacing);
|
||||
layerNodes.push({x, y});
|
||||
layerNodes.push({x, startY: startY + (i * spacing)});
|
||||
}
|
||||
nodePositions.push(layerNodes);
|
||||
});
|
||||
|
||||
// 3. DISEGNO CONNESSIONI (Pesi)
|
||||
// Per MNIST, disegniamo solo connessioni forti per non uccidere la CPU del browser
|
||||
const DRAW_THRESHOLD = 0.05; // Disegna solo se peso > 0.05
|
||||
|
||||
// Disegno connessioni
|
||||
const DRAW_THRESHOLD = 0.05;
|
||||
layers.forEach((layer, lIdx) => {
|
||||
const sourceNodes = nodePositions[lIdx];
|
||||
const targetNodes = nodePositions[lIdx + 1];
|
||||
|
|
@ -115,35 +139,48 @@
|
|||
sourceNodes.forEach((source, inputIdx) => {
|
||||
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
|
||||
const weight = layer.weights[weightIdx];
|
||||
|
||||
// Ottimizzazione: salta pesi quasi nulli
|
||||
if (Math.abs(weight) < DRAW_THRESHOLD) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(source.x, source.y);
|
||||
ctx.lineTo(target.x, target.y);
|
||||
ctx.moveTo(source.x, source.startY);
|
||||
ctx.lineTo(target.x, target.startY);
|
||||
|
||||
const alpha = Math.min(Math.abs(weight), 0.8);
|
||||
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})`
|
||||
: `rgba(255, 60, 60, ${alpha})`;
|
||||
const alpha = Math.min(Math.abs(weight), 0.5); // Più trasparente per chiarezza
|
||||
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})` : `rgba(255, 60, 60, ${alpha})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 4. DISEGNO NODI
|
||||
// Disegno Nodi
|
||||
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));
|
||||
|
||||
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.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
|
||||
ctx.arc(node.x, node.startY, nodeRadius, 0, Math.PI * 2);
|
||||
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.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
});
|
||||
});
|
||||
|
|
@ -155,15 +192,13 @@
|
|||
if (!response.ok) throw new Error("File missing");
|
||||
const data = await response.json();
|
||||
|
||||
epochEl.innerText = data.epoch;
|
||||
epochEl.innerText = (data.epoch === 999) ? "DEMO MODE" : data.epoch;
|
||||
lossEl.innerText = data.loss;
|
||||
drawNetwork(data);
|
||||
} catch (e) {
|
||||
// console.log("Waiting...", e);
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
setInterval(update, 100); // Aggiorna ogni 500ms
|
||||
setInterval(update, 500);
|
||||
window.addEventListener('resize', () => canvas.width = window.innerWidth);
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in a new issue