Il visualizer ora è fatto meglio
This commit is contained in:
parent
e3f8ee037a
commit
f070e1c985
142
visualizer.html
142
visualizer.html
|
|
@ -2,23 +2,46 @@
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Zig AI Visualizer</title>
|
<title>Zig MNIST Visualizer</title>
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; background-color: #111; color: #eee; font-family: monospace; overflow: hidden; }
|
body {
|
||||||
#info { position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; pointer-events: none; }
|
margin: 0;
|
||||||
|
background-color: #111;
|
||||||
|
color: #eee;
|
||||||
|
font-family: monospace;
|
||||||
|
overflow: auto; /* Abilita lo scroll */
|
||||||
|
}
|
||||||
|
#info {
|
||||||
|
position: fixed; /* Rimane fisso mentre scrolli */
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
h1 { margin: 0; font-size: 1.2em; color: #f4a261; }
|
h1 { margin: 0; font-size: 1.2em; color: #f4a261; }
|
||||||
.stat { font-size: 0.9em; color: #aaa; }
|
.stat { font-size: 0.9em; color: #aaa; margin-top: 5px; }
|
||||||
canvas { display: block; }
|
canvas {
|
||||||
|
display: block;
|
||||||
|
/* Il canvas non ha dimensioni fisse CSS, le decide JS */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="info">
|
<div id="info">
|
||||||
<h1>Zig Neural Network</h1>
|
<h1>Zig MNIST Network</h1>
|
||||||
<div class="stat">Epoca: <span id="epoch">Waiting...</span></div>
|
<div class="stat">Epoca: <span id="epoch" style="color: white">Waiting...</span></div>
|
||||||
<div class="stat">Loss: <span id="loss">Waiting...</span></div>
|
<div class="stat">Loss: <span id="loss" style="color: white">Waiting...</span></div>
|
||||||
<div class="stat" style="font-size: 0.8em; margin-top:5px">Verde: Positivo | Rosso: Negativo</div>
|
<div class="stat" style="font-size: 0.8em; margin-top:10px; border-top: 1px solid #444; padding-top:5px">
|
||||||
|
Scrolla per vedere tutti i 784 Input!<br>
|
||||||
|
Verde: Pesi Positivi<br>
|
||||||
|
Rosso: Pesi Negativi
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<canvas id="netCanvas"></canvas>
|
<canvas id="netCanvas"></canvas>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -27,102 +50,121 @@
|
||||||
const epochEl = document.getElementById('epoch');
|
const epochEl = document.getElementById('epoch');
|
||||||
const lossEl = document.getElementById('loss');
|
const lossEl = document.getElementById('loss');
|
||||||
|
|
||||||
// Adatta il canvas alla finestra
|
// Configurazione Spaziatura
|
||||||
function resize() {
|
const MIN_NODE_SPACING = 20; // Spazio minimo verticale tra i nodi (pixel)
|
||||||
canvas.width = window.innerWidth;
|
const LAYER_PADDING = 100; // Margine sopra e sotto
|
||||||
canvas.height = window.innerHeight;
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
// Funzione per disegnare la rete
|
|
||||||
function drawNetwork(data) {
|
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 layers = data.layers;
|
||||||
const structure = [layers[0].inputs]; // Aggiungiamo il numero di input come primo "strato visivo"
|
// Ricostruiamo la struttura completa [Input, Hidden1, Hidden2, Output]
|
||||||
|
const structure = [layers[0].inputs];
|
||||||
layers.forEach(l => structure.push(l.neurons));
|
layers.forEach(l => structure.push(l.neurons));
|
||||||
|
|
||||||
const layerWidth = canvas.width / structure.length;
|
// 1. CALCOLO DIMENSIONI CANVAS
|
||||||
|
// Troviamo il layer più grande (probabilmente l'input con 784)
|
||||||
const maxNeurons = Math.max(...structure);
|
const maxNeurons = Math.max(...structure);
|
||||||
|
|
||||||
// Coordinate dei nodi per disegnare le linee dopo
|
// 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);
|
||||||
|
} else {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerWidth = canvas.width / structure.length;
|
||||||
let nodePositions = [];
|
let nodePositions = [];
|
||||||
|
|
||||||
// 1. Calcoliamo le posizioni dei nodi
|
// 2. CALCOLO POSIZIONI NODI
|
||||||
structure.forEach((neuronCount, layerIdx) => {
|
structure.forEach((neuronCount, layerIdx) => {
|
||||||
const x = (layerIdx * layerWidth) + (layerWidth / 2);
|
const x = (layerIdx * layerWidth) + (layerWidth / 2);
|
||||||
const layerNodes = [];
|
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
|
||||||
|
if (spacing > 100) spacing = 100;
|
||||||
|
|
||||||
|
// Altezza totale occupata da questo layer
|
||||||
|
const layerTotalHeight = spacing * neuronCount;
|
||||||
|
const startY = (canvas.height / 2) - (layerTotalHeight / 2);
|
||||||
|
|
||||||
for (let i = 0; i < neuronCount; i++) {
|
for (let i = 0; i < neuronCount; i++) {
|
||||||
// Centriamo verticalmente
|
const y = startY + (i * spacing);
|
||||||
const y = (canvas.height / 2) - ((neuronCount * 60) / 2) + (i * 60);
|
|
||||||
layerNodes.push({x, y});
|
layerNodes.push({x, y});
|
||||||
}
|
}
|
||||||
nodePositions.push(layerNodes);
|
nodePositions.push(layerNodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Disegniamo le CONNESSIONI (Pesi)
|
// 3. DISEGNO CONNESSIONI (Pesi)
|
||||||
// Iteriamo sui layer "reali" (dal secondo array di posizioni in poi)
|
// Per MNIST, disegniamo solo connessioni forti per non uccidere la CPU del browser
|
||||||
|
const DRAW_THRESHOLD = 0.05; // Disegna solo se peso > 0.05
|
||||||
|
|
||||||
layers.forEach((layer, lIdx) => {
|
layers.forEach((layer, lIdx) => {
|
||||||
const sourceNodes = nodePositions[lIdx]; // Nodi input per questo layer
|
const sourceNodes = nodePositions[lIdx];
|
||||||
const targetNodes = nodePositions[lIdx + 1]; // Nodi di questo layer
|
const targetNodes = nodePositions[lIdx + 1];
|
||||||
|
|
||||||
targetNodes.forEach((target, neuronIdx) => {
|
targetNodes.forEach((target, neuronIdx) => {
|
||||||
sourceNodes.forEach((source, inputIdx) => {
|
sourceNodes.forEach((source, inputIdx) => {
|
||||||
// Recuperiamo il peso dal JSON array piatto
|
|
||||||
// Indice = (NeuroneCorrente * NumeroInput) + InputCorrente
|
|
||||||
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
|
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
|
||||||
const weight = layer.weights[weightIdx];
|
const weight = layer.weights[weightIdx];
|
||||||
|
|
||||||
|
// Ottimizzazione: salta pesi quasi nulli
|
||||||
|
if (Math.abs(weight) < DRAW_THRESHOLD) return;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(source.x, source.y);
|
ctx.moveTo(source.x, source.y);
|
||||||
ctx.lineTo(target.x, target.y);
|
ctx.lineTo(target.x, target.y);
|
||||||
|
|
||||||
// Stile Linea
|
const alpha = Math.min(Math.abs(weight), 0.8);
|
||||||
const intensity = Math.min(Math.abs(weight), 2); // Cap a 2 per non esplodere
|
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 128, ${alpha})`
|
||||||
ctx.lineWidth = intensity * 2;
|
: `rgba(255, 60, 60, ${alpha})`;
|
||||||
ctx.strokeStyle = weight > 0 ? `rgba(0, 255, 100, ${Math.min(Math.abs(weight), 1)})`
|
ctx.lineWidth = 1;
|
||||||
: `rgba(255, 50, 50, ${Math.min(Math.abs(weight), 1)})`;
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Disegniamo i NODI (Cerchi) sopra le linee
|
// 4. DISEGNO NODI
|
||||||
nodePositions.forEach((layerNodes, lIdx) => {
|
nodePositions.forEach((layerNodes, lIdx) => {
|
||||||
layerNodes.forEach((node, nIdx) => {
|
// 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) => {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(node.x, node.y, 15, 0, Math.PI * 2);
|
ctx.arc(node.x, node.y, nodeRadius, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = '#2a2a2a';
|
ctx.fillStyle = '#222';
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.strokeStyle = '#fff';
|
ctx.strokeStyle = '#fff';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 1;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop principale
|
|
||||||
async function update() {
|
async function update() {
|
||||||
try {
|
try {
|
||||||
// Aggiungiamo un timestamp per evitare che il browser usi la cache
|
|
||||||
const response = await fetch('network_state.json?t=' + new Date().getTime());
|
const response = await fetch('network_state.json?t=' + new Date().getTime());
|
||||||
if (!response.ok) throw new Error("File non trovato");
|
if (!response.ok) throw new Error("File missing");
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
epochEl.innerText = data.epoch;
|
epochEl.innerText = data.epoch;
|
||||||
lossEl.innerText = data.loss;
|
lossEl.innerText = data.loss;
|
||||||
|
|
||||||
drawNetwork(data);
|
drawNetwork(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("In attesa di dati...", e);
|
// console.log("Waiting...", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(update, 20);
|
setInterval(update, 100); // Aggiorna ogni 500ms
|
||||||
|
window.addEventListener('resize', () => canvas.width = window.innerWidth);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in a new issue