Aggiunti layer dinamici e visualizer
This commit is contained in:
parent
98f557370f
commit
1e648fe436
30
network_state.json
Normal file
30
network_state.json
Normal 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
14
src/activations.zig
Normal 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
93
src/layer.zig
Normal 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;
|
||||
}
|
||||
};
|
||||
74
src/main.zig
74
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
100
src/modular_network.zig
Normal file
100
src/modular_network.zig
Normal 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
129
visualizer.html
Normal 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>
|
||||
Loading…
Reference in a new issue