191 lines
6.9 KiB
HTML
191 lines
6.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Zig MNIST Visualizer</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
background-color: #111;
|
|
color: #eee;
|
|
font-family: monospace;
|
|
overflow: auto;
|
|
}
|
|
#info {
|
|
position: fixed;
|
|
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; }
|
|
.stat { font-size: 0.9em; color: #aaa; margin-top: 5px; }
|
|
canvas {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="info">
|
|
<h1>Zig MNIST Network</h1>
|
|
<div class="stat">Epoca: <span id="epoch" style="color: white">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: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>
|
|
|
|
<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');
|
|
|
|
const MIN_NODE_SPACING = 20;
|
|
const LAYER_PADDING = 100;
|
|
|
|
function drawInputImage(pixels) {
|
|
if (!pixels || pixels.length === 0) return;
|
|
|
|
const scale = 4;
|
|
const startX = 20;
|
|
const startY = 120;
|
|
|
|
ctx.fillStyle = "#000";
|
|
ctx.fillRect(startX - 2, startY - 2, (28 * scale) + 4, (28 * scale) + 4);
|
|
ctx.strokeStyle = "#f4a261";
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeRect(startX - 2, startY - 2, (28 * scale) + 4, (28 * scale) + 4);
|
|
|
|
for (let i = 0; i < 784; i++) {
|
|
const val = pixels[i];
|
|
if (val > 0.1) {
|
|
const x = (i % 28);
|
|
const y = Math.floor(i / 28);
|
|
|
|
const brightness = Math.floor(val * 255);
|
|
ctx.fillStyle = `rgb(${brightness}, ${brightness}, ${brightness})`;
|
|
ctx.fillRect(startX + (x * scale), startY + (y * scale), scale, scale);
|
|
}
|
|
}
|
|
|
|
ctx.fillStyle = "#fff";
|
|
ctx.font = "14px monospace";
|
|
ctx.fillText("AI INPUT:", startX, startY - 10);
|
|
}
|
|
|
|
function drawNetwork(data) {
|
|
const layers = data.layers;
|
|
const structure = [layers[0].inputs];
|
|
layers.forEach(l => structure.push(l.neurons));
|
|
|
|
const maxNeurons = Math.max(...structure);
|
|
const requiredHeight = (maxNeurons * MIN_NODE_SPACING) + (LAYER_PADDING * 2);
|
|
|
|
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);
|
|
}
|
|
|
|
if (data.input_pixels) {
|
|
drawInputImage(data.input_pixels);
|
|
}
|
|
|
|
const layerWidth = (canvas.width - 200) / structure.length;
|
|
const offsetX = 150;
|
|
|
|
let nodePositions = [];
|
|
|
|
structure.forEach((neuronCount, layerIdx) => {
|
|
const x = offsetX + (layerIdx * layerWidth) + (layerWidth / 2);
|
|
let spacing = (canvas.height - LAYER_PADDING * 2) / neuronCount;
|
|
if (spacing > 100) spacing = 100;
|
|
|
|
const layerTotalHeight = spacing * neuronCount;
|
|
const startY = (canvas.height / 2) - (layerTotalHeight / 2);
|
|
|
|
const layerNodes = [];
|
|
for (let i = 0; i < neuronCount; i++) {
|
|
layerNodes.push({x, startY: startY + (i * spacing)});
|
|
}
|
|
nodePositions.push(layerNodes);
|
|
});
|
|
|
|
const DRAW_THRESHOLD = 0.05;
|
|
layers.forEach((layer, lIdx) => {
|
|
const sourceNodes = nodePositions[lIdx];
|
|
const targetNodes = nodePositions[lIdx + 1];
|
|
|
|
targetNodes.forEach((target, neuronIdx) => {
|
|
sourceNodes.forEach((source, inputIdx) => {
|
|
const weightIdx = (neuronIdx * layer.inputs) + inputIdx;
|
|
const weight = layer.weights[weightIdx];
|
|
if (Math.abs(weight) < DRAW_THRESHOLD) return;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(source.x, source.startY);
|
|
ctx.lineTo(target.x, target.startY);
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
|
|
nodePositions.forEach((layerNodes, lIdx) => {
|
|
const nodeRadius = Math.max(3, Math.min(15, 300 / layerNodes.length));
|
|
|
|
let maxOutIdx = -1;
|
|
let maxOutVal = -999;
|
|
|
|
if (lIdx === nodePositions.length - 1) {
|
|
// Nota: Non abbiamo i valori di output nel JSON, solo i pesi statici.
|
|
// Quindi coloriamo solo i nodi genericamente
|
|
}
|
|
|
|
layerNodes.forEach((node, nIdx) => {
|
|
ctx.beginPath();
|
|
ctx.arc(node.x, node.startY, nodeRadius, 0, Math.PI * 2);
|
|
ctx.fillStyle = '#222';
|
|
|
|
if (lIdx === nodePositions.length - 1) {
|
|
ctx.fillStyle = '#444';
|
|
ctx.fillText(nIdx, node.x + 20, node.startY + 5);
|
|
}
|
|
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#fff';
|
|
ctx.stroke();
|
|
});
|
|
});
|
|
}
|
|
|
|
async function update() {
|
|
try {
|
|
const response = await fetch('network_state.json?t=' + new Date().getTime());
|
|
if (!response.ok) throw new Error("File missing");
|
|
const data = await response.json();
|
|
|
|
epochEl.innerText = (data.epoch === 999) ? "DEMO MODE" : data.epoch;
|
|
lossEl.innerText = data.loss;
|
|
drawNetwork(data);
|
|
} catch (e) { }
|
|
}
|
|
|
|
setInterval(update, 500);
|
|
window.addEventListener('resize', () => canvas.width = window.innerWidth);
|
|
</script>
|
|
</body>
|
|
</html> |