Ora è ottimizzato per la CPU

This commit is contained in:
Riccardo Forese 2026-02-03 12:54:33 +01:00
parent f070e1c985
commit 56f9afd031
6 changed files with 378 additions and 169 deletions

BIN
brain.bin Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -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;
}
};

View file

@ -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;

View file

@ -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;
}
};

View file

@ -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>