From 1e648fe436f99c00b11d5462ea2522cb2e501083 Mon Sep 17 00:00:00 2001 From: Riccardo Forese Date: Tue, 3 Feb 2026 11:08:49 +0100 Subject: [PATCH] Aggiunti layer dinamici e visualizer --- network_state.json | 30 ++++++++++ src/activations.zig | 14 +++++ src/layer.zig | 93 +++++++++++++++++++++++++++++ src/main.zig | 74 ++++++++++++++++------- src/modular_network.zig | 100 +++++++++++++++++++++++++++++++ visualizer.html | 129 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 417 insertions(+), 23 deletions(-) create mode 100644 network_state.json create mode 100644 src/activations.zig create mode 100644 src/layer.zig create mode 100644 src/modular_network.zig create mode 100644 visualizer.html diff --git a/network_state.json b/network_state.json new file mode 100644 index 0000000..f66212d --- /dev/null +++ b/network_state.json @@ -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] + } + ] +} diff --git a/src/activations.zig b/src/activations.zig new file mode 100644 index 0000000..a15fe86 --- /dev/null +++ b/src/activations.zig @@ -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 diff --git a/src/layer.zig b/src/layer.zig new file mode 100644 index 0000000..81288c0 --- /dev/null +++ b/src/layer.zig @@ -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; + } +}; diff --git a/src/main.zig b/src/main.zig index 250975a..05a8019 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,39 +1,67 @@ const std = @import("std"); -const SimpleNetwork = @import("network.zig").SimpleNetwork; +const Network = @import("modular_network.zig").Network; 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{ - .{ 0.0, 0.0 }, - .{ 0.0, 1.0 }, - .{ 1.0, 0.0 }, - .{ 1.0, 1.0 }, - }; + var net = Network.init(allocator); + defer net.deinit(); - 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; - while (epoch < 10000) : (epoch += 1) { + while (epoch <= max_epochs) : (epoch += 1) { var total_loss: f32 = 0.0; - for (inputs, 0..) |inp, i| { - total_loss += net.train(inp, targets[i], lr); + // Training step + for (0..4) |i| { + total_loss += try net.train(inputs[i], targets[i], lr); } - if (epoch % 1000 == 0) { - std.debug.print("Epoca {d}: Loss = {d:.6}\n", .{ epoch, total_loss / 4.0 }); - } - } + // --- ZONA OUTPUT E EXPORT --- + if (epoch % export_step == 0) { - std.debug.print("\n--- TEST XOR ---\n", .{}); - for (inputs) |inp| { - const out = net.forward(inp); - const bit: u8 = if (out > 0.5) 1 else 0; - std.debug.print("In: {d:.0},{d:.0} -> Out: {d:.4} -> {d}\n", .{ inp[0], inp[1], out, bit }); + // 1. Stampiamo HEADER con Epoca e Loss + std.debug.print("\n=== EPOCA {d} | Loss: {d:.6} ===\n", .{ epoch, total_loss }); + + // 2. Stampiamo le PREVISIONI attuali per i 4 casi + 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); + } + } } } diff --git a/src/modular_network.zig b/src/modular_network.zig new file mode 100644 index 0000000..0802211 --- /dev/null +++ b/src/modular_network.zig @@ -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; + } +}; diff --git a/visualizer.html b/visualizer.html new file mode 100644 index 0000000..56adf8a --- /dev/null +++ b/visualizer.html @@ -0,0 +1,129 @@ + + + + + Zig AI Visualizer + + + + +
+

Zig Neural Network

+
Epoca: Waiting...
+
Loss: Waiting...
+
Verde: Positivo | Rosso: Negativo
+
+ + + + + \ No newline at end of file