Pulizia codice
This commit is contained in:
parent
9653eada03
commit
ed8080e2d6
56
src/env.zig
56
src/env.zig
|
|
@ -1,16 +1,13 @@
|
|||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const GRID_SIZE = 100; // Mappa Gigante!
|
||||
pub const NUM_ANTS = 20; // La Colonia
|
||||
pub const GRID_SIZE = 100;
|
||||
pub const NUM_ANTS = 20;
|
||||
|
||||
pub const TILE_EMPTY = 0;
|
||||
pub const TILE_WALL = 1;
|
||||
pub const TILE_FOOD = 2;
|
||||
|
||||
// Non c'è più TILE_ANT nella griglia statica, perché le formiche si muovono sopra
|
||||
// Useremo una lista dinamica.
|
||||
|
||||
pub const Ant = struct {
|
||||
x: usize,
|
||||
y: usize,
|
||||
|
|
@ -20,8 +17,8 @@ pub const Ant = struct {
|
|||
|
||||
pub const World = struct {
|
||||
grid: [GRID_SIZE][GRID_SIZE]u8,
|
||||
pheromones: [GRID_SIZE][GRID_SIZE]f32, // 0.0 = Neutro, 1.0 = Appena visitato
|
||||
ants: [NUM_ANTS]Ant, // Array fisso di formiche
|
||||
pheromones: [GRID_SIZE][GRID_SIZE]f32,
|
||||
ants: [NUM_ANTS]Ant,
|
||||
food_x: usize,
|
||||
food_y: usize,
|
||||
max_steps: usize,
|
||||
|
|
@ -34,7 +31,7 @@ pub const World = struct {
|
|||
.ants = undefined,
|
||||
.food_x = 0,
|
||||
.food_y = 0,
|
||||
.max_steps = 1000, // Più passi per mappa grande
|
||||
.max_steps = 1000,
|
||||
.prng = std.Random.DefaultPrng.init(seed),
|
||||
};
|
||||
w.reset();
|
||||
|
|
@ -42,8 +39,6 @@ pub const World = struct {
|
|||
}
|
||||
|
||||
pub fn reset(self: *World) void {
|
||||
//const random = self.prng.random();
|
||||
|
||||
for (0..GRID_SIZE) |y| {
|
||||
for (0..GRID_SIZE) |x| {
|
||||
self.pheromones[y][x] = 0.0;
|
||||
|
|
@ -55,10 +50,8 @@ pub const World = struct {
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Spawn Cibo
|
||||
self.respawnFood();
|
||||
|
||||
// 3. Spawn Formiche (Tutte al centro, come un formicaio)
|
||||
const center = GRID_SIZE / 2;
|
||||
for (0..NUM_ANTS) |i| {
|
||||
self.ants[i] = Ant{
|
||||
|
|
@ -72,7 +65,6 @@ pub const World = struct {
|
|||
|
||||
fn respawnFood(self: *World) void {
|
||||
const random = self.prng.random();
|
||||
// Semplice loop per trovare posto libero
|
||||
while (true) {
|
||||
const rx = random.intRangeAtMost(usize, 1, GRID_SIZE - 2);
|
||||
const ry = random.intRangeAtMost(usize, 1, GRID_SIZE - 2);
|
||||
|
|
@ -84,8 +76,6 @@ pub const World = struct {
|
|||
}
|
||||
}
|
||||
|
||||
// Calcola l'olfatto (Distanza inversa dal cibo)
|
||||
// 0.0 (Lontanissimo) -> 1.0 (Sopra il cibo)
|
||||
fn getScent(self: *World, x: usize, y: usize) f32 {
|
||||
const dx = if (x > self.food_x) x - self.food_x else self.food_x - x;
|
||||
const dy = if (y > self.food_y) y - self.food_y else self.food_y - y;
|
||||
|
|
@ -93,7 +83,6 @@ pub const World = struct {
|
|||
return 1.0 / (dist + 1.0);
|
||||
}
|
||||
|
||||
// Controlla se una cella è occupata da UN'ALTRA formica
|
||||
fn isOccupied(self: *World, x: usize, y: usize) bool {
|
||||
for (self.ants) |ant| {
|
||||
if (ant.alive and ant.x == x and ant.y == y) return true;
|
||||
|
|
@ -101,14 +90,12 @@ pub const World = struct {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Esegue step per UNA formica specifica
|
||||
pub fn stepAnt(self: *World, ant_idx: usize, action: usize) struct { f32, bool } {
|
||||
var ant = &self.ants[ant_idx];
|
||||
if (!ant.alive) return .{ 0.0, true };
|
||||
|
||||
ant.steps += 1;
|
||||
|
||||
// Salviamo lo stato PRECEDENTE per fare i confronti
|
||||
const old_dist_x = if (ant.x > self.food_x) ant.x - self.food_x else self.food_x - ant.x;
|
||||
const old_dist_y = if (ant.y > self.food_y) ant.y - self.food_y else self.food_y - ant.y;
|
||||
const old_dist = old_dist_x + old_dist_y;
|
||||
|
|
@ -116,18 +103,14 @@ pub const World = struct {
|
|||
var new_x = ant.x;
|
||||
var new_y = ant.y;
|
||||
|
||||
if (action == 0) new_y -= 1; // UP
|
||||
if (action == 1) new_y += 1; // DOWN
|
||||
if (action == 2) new_x -= 1; // LEFT
|
||||
if (action == 3) new_x += 1; // RIGHT
|
||||
if (action == 0) new_y -= 1;
|
||||
if (action == 1) new_y += 1;
|
||||
if (action == 2) new_x -= 1;
|
||||
if (action == 3) new_x += 1;
|
||||
|
||||
// --- 1. MURI (Limiti Mappa) ---
|
||||
if (self.grid[new_y][new_x] == TILE_WALL) return .{ -5.0, false };
|
||||
|
||||
// --- 2. COLLISIONI TRA FORMICHE ---
|
||||
// Se c'è traffico, penalità leggera ma non bloccante se c'è cibo vicino
|
||||
if (self.isOccupied(new_x, new_y)) {
|
||||
// Se siamo vicini al cibo, spingi! Altrimenti aspetta.
|
||||
if (old_dist > 5) return .{ -0.5, false };
|
||||
}
|
||||
|
||||
|
|
@ -188,16 +171,14 @@ pub const World = struct {
|
|||
}
|
||||
}
|
||||
|
||||
// Input aumentati a 15
|
||||
pub fn getAntObservation(self: *World, allocator: Allocator, ant_idx: usize) ![]f32 {
|
||||
var obs = try allocator.alloc(f32, 15); // AUMENTATO A 15
|
||||
var obs = try allocator.alloc(f32, 15);
|
||||
const ant = self.ants[ant_idx];
|
||||
var idx: usize = 0;
|
||||
|
||||
const ax = @as(i32, @intCast(ant.x));
|
||||
const ay = @as(i32, @intCast(ant.y));
|
||||
|
||||
// 1. Visione 3x3 (0-8)
|
||||
var dy: i32 = -1;
|
||||
while (dy <= 1) : (dy += 1) {
|
||||
var dx: i32 = -1;
|
||||
|
|
@ -209,27 +190,22 @@ pub const World = struct {
|
|||
if (self.grid[py][px] == TILE_WALL) {
|
||||
val = -1.0;
|
||||
} else if (px == self.food_x and py == self.food_y) {
|
||||
val = 1.0; // Vedo il cibo vicino!
|
||||
val = 1.0;
|
||||
} else if (self.isOccupied(px, py) and (dx != 0 or dy != 0)) {
|
||||
val = -0.5; // Vedo una sorella
|
||||
val = -0.5;
|
||||
}
|
||||
obs[idx] = val;
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Sensi Chimici (9-10)
|
||||
obs[9] = self.getScent(ant.x, ant.y);
|
||||
obs[10] = self.pheromones[ant.y][ant.x];
|
||||
|
||||
// 3. BUSSOLA BINARIA (11-14)
|
||||
// Risponde alla domanda: "In che quadrante è il cibo?"
|
||||
// È molto più facile da capire per l'IA rispetto a un float.
|
||||
|
||||
obs[11] = if (self.food_y < ant.y) 1.0 else 0.0; // Cibo è SOPRA?
|
||||
obs[12] = if (self.food_y > ant.y) 1.0 else 0.0; // Cibo è SOTTO?
|
||||
obs[13] = if (self.food_x < ant.x) 1.0 else 0.0; // Cibo è SINISTRA?
|
||||
obs[14] = if (self.food_x > ant.x) 1.0 else 0.0; // Cibo è DESTRA?
|
||||
obs[11] = if (self.food_y < ant.y) 1.0 else 0.0;
|
||||
obs[12] = if (self.food_y > ant.y) 1.0 else 0.0;
|
||||
obs[13] = if (self.food_x < ant.x) 1.0 else 0.0;
|
||||
obs[14] = if (self.food_x > ant.x) 1.0 else 0.0;
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub const DenseLayer = struct {
|
|||
output: []f32,
|
||||
inputs_count: usize,
|
||||
neurons_count: usize,
|
||||
use_sigmoid: bool, // NUOVO CAMPO
|
||||
use_sigmoid: bool,
|
||||
allocator: Allocator,
|
||||
|
||||
fn sigmoid(x: f32) f32 {
|
||||
|
|
@ -21,7 +21,6 @@ pub const DenseLayer = struct {
|
|||
return x * (1.0 - x);
|
||||
}
|
||||
|
||||
// Aggiunto parametro 'use_sigmoid'
|
||||
pub fn init(allocator: Allocator, inputs: usize, neurons: usize, seed: u64, use_sigmoid: bool) !DenseLayer {
|
||||
const weights = try allocator.alloc(f32, inputs * neurons);
|
||||
const biases = try allocator.alloc(f32, neurons);
|
||||
|
|
@ -30,7 +29,6 @@ pub const DenseLayer = struct {
|
|||
var prng = std.Random.DefaultPrng.init(seed);
|
||||
const random = prng.random();
|
||||
|
||||
// Pesi più piccoli per stabilità iniziale
|
||||
for (weights) |*w| w.* = (random.float(f32) * 2.0 - 1.0) * 0.1;
|
||||
for (biases) |*b| b.* = 0.0;
|
||||
|
||||
|
|
@ -56,7 +54,6 @@ pub const DenseLayer = struct {
|
|||
var sum: f32 = self.biases[n];
|
||||
const w_start = n * self.inputs_count;
|
||||
|
||||
// SIMD
|
||||
var vec_sum: Vec = @splat(0.0);
|
||||
var i: usize = 0;
|
||||
while (i + SimdWidth <= self.inputs_count) : (i += SimdWidth) {
|
||||
|
|
@ -66,12 +63,10 @@ pub const DenseLayer = struct {
|
|||
}
|
||||
sum += @reduce(.Add, vec_sum);
|
||||
|
||||
// Tail Loop
|
||||
while (i < self.inputs_count) : (i += 1) {
|
||||
sum += input[i] * self.weights[w_start + i];
|
||||
}
|
||||
|
||||
// CORREZIONE: Se non usiamo sigmoide, è lineare (passa 'sum' diretto)
|
||||
if (self.use_sigmoid) {
|
||||
self.output[n] = sigmoid(sum);
|
||||
} else {
|
||||
|
|
@ -86,9 +81,6 @@ pub const DenseLayer = struct {
|
|||
@memset(input_gradient, 0.0);
|
||||
|
||||
for (0..self.neurons_count) |n| {
|
||||
// CORREZIONE DERIVATA:
|
||||
// Se Sigmoide: f'(x) = out * (1 - out)
|
||||
// Se Lineare: f'(x) = 1.0
|
||||
var derivative: f32 = 1.0;
|
||||
if (self.use_sigmoid) {
|
||||
derivative = sigmoidDerivative(self.output[n]);
|
||||
|
|
@ -99,7 +91,6 @@ pub const DenseLayer = struct {
|
|||
|
||||
self.biases[n] -= learning_rate * delta;
|
||||
|
||||
// SIMD LOOP (Backprop)
|
||||
const v_delta: Vec = @splat(delta);
|
||||
const v_lr: Vec = @splat(learning_rate);
|
||||
const v_change_factor = v_delta * v_lr;
|
||||
|
|
|
|||
23
src/main.zig
23
src/main.zig
|
|
@ -64,14 +64,10 @@ pub fn main() !void {
|
|||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
// 1. Inizializza Ambiente 100x100 e Rete
|
||||
var world = World.init(12345);
|
||||
var net = Network.init(allocator);
|
||||
defer net.deinit();
|
||||
|
||||
// ARCHITETTURA RETE
|
||||
// Input 11: (9 Vista + 1 Olfatto + 1 Feromone)
|
||||
// Output 4: (Su, Giù, Sx, Dx) LINEARE
|
||||
try net.addLayer(15, 40, 111, true);
|
||||
try net.addLayer(40, 4, 222, false);
|
||||
|
||||
|
|
@ -84,19 +80,13 @@ pub fn main() !void {
|
|||
var global_step: usize = 0;
|
||||
var epsilon: f32 = EPSILON_START;
|
||||
|
||||
// Ciclo infinito (o molto lungo)
|
||||
while (true) {
|
||||
|
||||
// 1. Evaporazione Feromoni (Memoria collettiva che sbiadisce)
|
||||
world.evaporatePheromones();
|
||||
|
||||
// 2. Turno di ogni formica
|
||||
for (0..env.NUM_ANTS) |i| {
|
||||
// A. OSSERVAZIONE
|
||||
const current_obs = try world.getAntObservation(allocator, i);
|
||||
defer allocator.free(current_obs);
|
||||
|
||||
// B. SCEGLI AZIONE (Epsilon-Greedy)
|
||||
var action: usize = 0;
|
||||
const q_values = net.forward(current_obs);
|
||||
|
||||
|
|
@ -106,24 +96,16 @@ pub fn main() !void {
|
|||
action = argmax(q_values);
|
||||
}
|
||||
|
||||
// C. ESEGUI AZIONE
|
||||
const result = world.stepAnt(i, action);
|
||||
const reward = result[0];
|
||||
// Nota: in multi-agente 'done' è meno rilevante per il loop principale,
|
||||
// perché se una formica "finisce" (mangia), respawna o continua.
|
||||
|
||||
// D. ADDESTRAMENTO (Hive Mind Learning)
|
||||
// Calcoliamo target Q-Value
|
||||
var target_val = reward;
|
||||
|
||||
// Se non è uno stato terminale (morte o vittoria netta), aggiungiamo stima futuro
|
||||
// Consideriamo la vittoria (mangiare) come continuativa qui per non rompere il flusso
|
||||
const next_obs = try world.getAntObservation(allocator, i);
|
||||
defer allocator.free(next_obs);
|
||||
const next_q_values = net.forward(next_obs);
|
||||
target_val += GAMMA * maxVal(next_q_values);
|
||||
|
||||
// Backpropagation
|
||||
var target_vector = try allocator.alloc(f32, 4);
|
||||
defer allocator.free(target_vector);
|
||||
for (0..4) |j| target_vector[j] = q_values[j];
|
||||
|
|
@ -132,24 +114,19 @@ pub fn main() !void {
|
|||
_ = try net.train(current_obs, target_vector, LR);
|
||||
}
|
||||
|
||||
// 3. Gestione Loop Globale
|
||||
global_step += 1;
|
||||
|
||||
// Decadimento Epsilon
|
||||
if (epsilon > EPSILON_END) {
|
||||
epsilon -= DECAY_RATE;
|
||||
}
|
||||
|
||||
// 4. Export e Log (Ogni 10 step globali per fluidità)
|
||||
if (global_step % 10 == 0) {
|
||||
try exportAntJSON(&world, "ant_state.json", global_step, epsilon);
|
||||
|
||||
// Log console ogni 100 step
|
||||
if (global_step % 100 == 0) {
|
||||
std.debug.print("Step: {d} | Epsilon: {d:.3} | Cibo: [{d},{d}]\r", .{ global_step, epsilon, world.food_x, world.food_y });
|
||||
}
|
||||
|
||||
// Pausa per vedere l'animazione (rimuovi per training ultra-veloce)
|
||||
std.Thread.sleep(100 * 1_000_000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@
|
|||
background-color: #111;
|
||||
color: #eee;
|
||||
font-family: monospace;
|
||||
overflow: auto; /* Abilita lo scroll */
|
||||
overflow: auto;
|
||||
}
|
||||
#info {
|
||||
position: fixed; /* Rimane fisso mentre scrolli */
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
|
|
@ -24,8 +24,7 @@
|
|||
h1 { margin: 0; font-size: 1.2em; color: #f4a261; }
|
||||
.stat { font-size: 0.9em; color: #aaa; margin-top: 5px; }
|
||||
canvas {
|
||||
display: block;
|
||||
/* Il canvas non ha dimensioni fisse CSS, le decide JS */
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
|
@ -50,20 +49,16 @@
|
|||
const epochEl = document.getElementById('epoch');
|
||||
const lossEl = document.getElementById('loss');
|
||||
|
||||
// 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 scale = 4;
|
||||
const startX = 20;
|
||||
const startY = 120; // Sotto le scritte di info
|
||||
const startY = 120;
|
||||
|
||||
// Sfondo nero
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillRect(startX - 2, startY - 2, (28 * scale) + 4, (28 * scale) + 4);
|
||||
ctx.strokeStyle = "#f4a261";
|
||||
|
|
@ -72,18 +67,16 @@
|
|||
|
||||
for (let i = 0; i < 784; i++) {
|
||||
const val = pixels[i];
|
||||
if (val > 0.1) { // Disegna solo se non è nero
|
||||
if (val > 0.1) {
|
||||
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);
|
||||
|
|
@ -104,13 +97,12 @@
|
|||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// --- 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
|
||||
const layerWidth = (canvas.width - 200) / structure.length;
|
||||
const offsetX = 150;
|
||||
|
||||
let nodePositions = [];
|
||||
|
||||
|
|
@ -129,7 +121,6 @@
|
|||
nodePositions.push(layerNodes);
|
||||
});
|
||||
|
||||
// Disegno connessioni
|
||||
const DRAW_THRESHOLD = 0.05;
|
||||
layers.forEach((layer, lIdx) => {
|
||||
const sourceNodes = nodePositions[lIdx];
|
||||
|
|
@ -145,7 +136,7 @@
|
|||
ctx.moveTo(source.x, source.startY);
|
||||
ctx.lineTo(target.x, target.startY);
|
||||
|
||||
const alpha = Math.min(Math.abs(weight), 0.5); // Più trasparente per chiarezza
|
||||
const alpha = Math.min(Math.abs(weight), 0.5);
|
||||
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})` : `rgba(255, 60, 60, ${alpha})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
|
@ -153,15 +144,12 @@
|
|||
});
|
||||
});
|
||||
|
||||
// Disegno Nodi
|
||||
nodePositions.forEach((layerNodes, lIdx) => {
|
||||
const nodeRadius = Math.max(3, Math.min(15, 300 / layerNodes.length));
|
||||
|
||||
// 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
|
||||
|
|
@ -172,10 +160,8 @@
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue