Aggiunti layer dinamici e visualizer

This commit is contained in:
Riccardo Forese 2026-02-03 11:08:49 +01:00
parent 98f557370f
commit 1e648fe436
6 changed files with 417 additions and 23 deletions

30
network_state.json Normal file
View file

@ -0,0 +1,30 @@
{
"epoch": 50000,
"loss": 0.000361,
"layers": [
{
"layer_index": 0,
"neurons": 8,
"inputs": 2,
"weights": [-1.8084, -1.7892, 1.0432, 1.0532, -1.7995, -0.5142, 2.9979, 2.8154, 3.0978, 3.0040, 1.6499, 1.7288, -0.8001, -1.0591, -1.2208, -1.9297]
},
{
"layer_index": 1,
"neurons": 8,
"inputs": 8,
"weights": [1.0021, -0.4447, 1.1029, -2.8633, -4.0170, -1.9685, 1.5763, 1.0256, 0.8235, 0.2697, 0.5515, -2.2983, -1.6703, -0.6068, 0.0814, 0.4504, 2.5507, -1.0496, 1.5386, -0.0850, -0.5666, -1.9280, 0.2215, 2.1025, 1.6812, 0.2858, 1.1396, -0.7851, -0.0887, -1.5677, -0.1539, 0.7152, 1.0596, -0.5767, 0.3283, -2.3845, -1.9909, -1.5075, 0.4927, 1.1269, 1.4759, -0.9386, 1.2690, -0.0751, 0.0958, -1.0085, 0.6329, 0.4874, -0.1151, 0.9539, -0.7792, -0.2333, -0.3964, -0.5055, -0.9239, -0.8561, -2.4293, 1.3624, -0.5428, 0.7666, 1.2711, 0.4876, -0.9042, -2.7514]
},
{
"layer_index": 2,
"neurons": 4,
"inputs": 8,
"weights": [5.3152, 2.4064, -2.7071, -0.1956, 2.6623, -1.8726, 0.7450, 1.0220, 0.9021, 0.6682, -1.5896, 0.0101, 0.9790, -0.4701, -0.2676, 1.4321, -0.2409, 0.4403, -0.0691, -0.4393, -0.8146, -0.6531, -0.2773, -1.0688, -0.6154, 0.5529, 2.8170, 2.4077, 0.5548, 1.3671, -0.1945, -3.9858]
},
{
"layer_index": 3,
"neurons": 1,
"inputs": 4,
"weights": [-8.7801, -2.4135, 0.4840, 6.1344]
}
]
}

14
src/activations.zig Normal file
View file

@ -0,0 +1,14 @@
const std = @import("std");
pub const Sigmoid = struct {
pub fn apply(x: f32) f32 {
return 1.0 / (1.0 + std.math.exp(-x));
}
pub fn derivative(y: f32) f32 {
// Nota: qui assumiamo che 'y' sia già il risultato della sigmoide
return y * (1.0 - y);
}
};
// Possiamo aggiungere ReLU, Tanh, ecc. in futuro

93
src/layer.zig Normal file
View file

@ -0,0 +1,93 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Sigmoid = @import("activations.zig").Sigmoid;
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!
var prng = std.Random.DefaultPrng.init(seed);
const rand = prng.random();
for (weights) |*w| w.* = rand.float(f32) * 2.0 - 1.0;
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,
.allocator = allocator,
};
}
pub fn deinit(self: *DenseLayer) void {
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 {
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];
}
// Aggiungiamo bias e applichiamo Sigmoide
self.output[n] = Sigmoid.apply(sum + self.biases[n]);
}
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)
for (0..self.neurons_count) |n| {
const derivative = Sigmoid.derivative(self.output[n]);
self.deltas[n] = next_layer_gradients[n] * derivative;
}
// 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];
}
}
// 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;
}
};

View file

@ -1,39 +1,67 @@
const std = @import("std"); const std = @import("std");
const SimpleNetwork = @import("network.zig").SimpleNetwork; const Network = @import("modular_network.zig").Network;
pub fn main() !void { pub fn main() !void {
var net = SimpleNetwork.init(1234); var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
const inputs = [_][2]f32{ var net = Network.init(allocator);
.{ 0.0, 0.0 }, defer net.deinit();
.{ 0.0, 1.0 },
.{ 1.0, 0.0 },
.{ 1.0, 1.0 },
};
const targets = [_]f32{ 0.0, 1.0, 1.0, 0.0 }; // --- ARCHITETTURA ---
// Input(2) -> Hidden(8) -> Hidden(8) -> Hidden(4) -> Output(1)
try net.addLayer(2, 8, 123);
try net.addLayer(8, 8, 456);
try net.addLayer(8, 4, 789);
try net.addLayer(4, 1, 101);
const lr: f32 = 0.5; net.printTopology();
std.debug.print("--- TRAINING XOR (2 LAYERS) ---\n", .{}); // Dati XOR
const inputs = [_][]const f32{ &.{ 0.0, 0.0 }, &.{ 0.0, 1.0 }, &.{ 1.0, 0.0 }, &.{ 1.0, 1.0 } };
const targets = [_][]const f32{ &.{0.0}, &.{1.0}, &.{1.0}, &.{0.0} };
std.debug.print("--- TRAINING DEEP CON VISUALIZER E DEBUG --- \n", .{});
// --- CONFIGURAZIONE ---
const lr: f32 = 0.2;
const max_epochs = 50000;
const slow_mode = true; // Attiva il rallentatore
const export_step = 100; // Ogni quante epoche aggiorniamo
const delay_ms = 25; // Ritardo in millisecondi
var epoch: usize = 0; var epoch: usize = 0;
while (epoch < 10000) : (epoch += 1) { while (epoch <= max_epochs) : (epoch += 1) {
var total_loss: f32 = 0.0; var total_loss: f32 = 0.0;
for (inputs, 0..) |inp, i| { // Training step
total_loss += net.train(inp, targets[i], lr); for (0..4) |i| {
total_loss += try net.train(inputs[i], targets[i], lr);
} }
if (epoch % 1000 == 0) { // --- ZONA OUTPUT E EXPORT ---
std.debug.print("Epoca {d}: Loss = {d:.6}\n", .{ epoch, total_loss / 4.0 }); if (epoch % export_step == 0) {
}
}
std.debug.print("\n--- TEST XOR ---\n", .{}); // 1. Stampiamo HEADER con Epoca e Loss
for (inputs) |inp| { std.debug.print("\n=== EPOCA {d} | Loss: {d:.6} ===\n", .{ epoch, total_loss });
const out = net.forward(inp);
const bit: u8 = if (out > 0.5) 1 else 0; // 2. Stampiamo le PREVISIONI attuali per i 4 casi
std.debug.print("In: {d:.0},{d:.0} -> Out: {d:.4} -> {d}\n", .{ inp[0], inp[1], out, bit }); for (inputs) |inp| {
const out = net.forward(inp);
// Stampa formattata: Input -> Output
std.debug.print("In: [{d:.0}, {d:.0}] -> Out: {d:.4}\n", .{ inp[0], inp[1], out[0] });
}
// 3. Esportiamo il JSON per il browser
try net.exportJSON("network_state.json", epoch, total_loss);
// 4. Delay per l'animazione
if (slow_mode) {
// Ricorda: std.Thread.sleep vuole nanosecondi
std.Thread.sleep(delay_ms * 1_000_000);
}
}
} }
} }

100
src/modular_network.zig Normal file
View file

@ -0,0 +1,100 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const DenseLayer = @import("layer.zig").DenseLayer;
pub const Network = struct {
layers: std.ArrayList(DenseLayer),
allocator: Allocator,
pub fn init(allocator: Allocator) Network {
return Network{
.layers = std.ArrayList(DenseLayer){},
.allocator = allocator,
};
}
pub fn deinit(self: *Network) void {
for (self.layers.items) |*layer| {
layer.deinit();
}
self.layers.deinit(self.allocator);
}
pub fn addLayer(self: *Network, input_size: usize, output_size: usize, seed: u64) !void {
const layer = try DenseLayer.init(self.allocator, input_size, output_size, seed);
try self.layers.append(self.allocator, layer);
}
pub fn forward(self: *Network, input: []const f32) []const f32 {
var current_input = input;
for (self.layers.items) |*layer| {
current_input = layer.forward(current_input);
}
return current_input;
}
pub fn printTopology(self: *Network) void {
std.debug.print("Architettura Rete: [Input]", .{});
for (self.layers.items) |layer| {
std.debug.print(" -> [Dense:{d}]", .{layer.neurons_count});
}
std.debug.print("\n", .{});
}
pub fn exportJSON(self: *Network, file_path: []const u8, epoch: usize, loss: f32) !void {
const file = try std.fs.cwd().createFile(file_path, .{});
defer file.close();
const Utils = struct {
fn print(f: std.fs.File, comptime fmt: []const u8, args: anytype) !void {
var buf: [256]u8 = undefined;
const text = try std.fmt.bufPrint(&buf, fmt, args);
try f.writeAll(text);
}
};
try Utils.print(file, "{{\n \"epoch\": {d},\n \"loss\": {d:.6},\n \"layers\": [\n", .{ epoch, loss });
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| {
try Utils.print(file, "{d:.4}", .{w});
if (w_idx < layer.weights.len - 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;
}
};

129
visualizer.html Normal file
View file

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Zig AI Visualizer</title>
<style>
body { margin: 0; background-color: #111; color: #eee; font-family: monospace; overflow: hidden; }
#info { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; pointer-events: none; }
h1 { margin: 0; font-size: 1.2em; color: #f4a261; }
.stat { font-size: 0.9em; color: #aaa; }
canvas { display: block; }
</style>
</head>
<body>
<div id="info">
<h1>Zig Neural Network</h1>
<div class="stat">Epoca: <span id="epoch">Waiting...</span></div>
<div class="stat">Loss: <span id="loss">Waiting...</span></div>
<div class="stat" style="font-size: 0.8em; margin-top:5px">Verde: Positivo | Rosso: Negativo</div>
</div>
<canvas id="netCanvas"></canvas>
<script>
const canvas = document.getElementById('netCanvas');
const ctx = canvas.getContext('2d');
const epochEl = document.getElementById('epoch');
const lossEl = document.getElementById('loss');
// Adatta il canvas alla finestra
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
window.addEventListener('resize', resize);
resize();
// Funzione per disegnare la rete
function drawNetwork(data) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Calcoliamo la struttura completa (Input Layer + Hidden Layers)
// Il JSON ha solo i layer "Dense", dobbiamo dedurre l'input layer dal primo strato
const layers = data.layers;
const structure = [layers[0].inputs]; // Aggiungiamo il numero di input come primo "strato visivo"
layers.forEach(l => structure.push(l.neurons));
const layerWidth = canvas.width / structure.length;
const maxNeurons = Math.max(...structure);
// Coordinate dei nodi per disegnare le linee dopo
let nodePositions = [];
// 1. Calcoliamo le posizioni dei nodi
structure.forEach((neuronCount, layerIdx) => {
const x = (layerIdx * layerWidth) + (layerWidth / 2);
const layerNodes = [];
for (let i = 0; i < neuronCount; i++) {
// Centriamo verticalmente
const y = (canvas.height / 2) - ((neuronCount * 60) / 2) + (i * 60);
layerNodes.push({x, y});
}
nodePositions.push(layerNodes);
});
// 2. Disegniamo le CONNESSIONI (Pesi)
// Iteriamo sui layer "reali" (dal secondo array di posizioni in poi)
layers.forEach((layer, lIdx) => {
const sourceNodes = nodePositions[lIdx]; // Nodi input per questo layer
const targetNodes = nodePositions[lIdx + 1]; // Nodi di questo layer
targetNodes.forEach((target, neuronIdx) => {
sourceNodes.forEach((source, inputIdx) => {
// Recuperiamo il peso dal JSON array piatto
// Indice = (NeuroneCorrente * NumeroInput) + InputCorrente
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
const weight = layer.weights[weightIdx];
ctx.beginPath();
ctx.moveTo(source.x, source.y);
ctx.lineTo(target.x, target.y);
// Stile Linea
const intensity = Math.min(Math.abs(weight), 2); // Cap a 2 per non esplodere
ctx.lineWidth = intensity * 2;
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 100, ${Math.min(Math.abs(weight), 1)})`
: `rgba(255, 50, 50, ${Math.min(Math.abs(weight), 1)})`;
ctx.stroke();
});
});
});
// 3. Disegniamo i NODI (Cerchi) sopra le linee
nodePositions.forEach((layerNodes, lIdx) => {
layerNodes.forEach((node, nIdx) => {
ctx.beginPath();
ctx.arc(node.x, node.y, 15, 0, Math.PI * 2);
ctx.fillStyle = '#2a2a2a';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
});
});
}
// Loop principale
async function update() {
try {
// Aggiungiamo un timestamp per evitare che il browser usi la cache
const response = await fetch('network_state.json?t=' + new Date().getTime());
if (!response.ok) throw new Error("File non trovato");
const data = await response.json();
epochEl.innerText = data.epoch;
lossEl.innerText = data.loss;
drawNetwork(data);
} catch (e) {
console.log("In attesa di dati...", e);
}
}
// Aggiorna ogni 200ms
setInterval(update, 50);
</script>
</body>
</html>