MachineLearningAlgorithms / templates /Neural-Networks-for-Classification-three.html
deedrop1140's picture
Upload 18 files
42001a3 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neural Network Playground</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
:root {
--background: 222 47% 6%;
--foreground: 210 40% 96%;
--card: 222 47% 8%;
--primary: 199 89% 48%;
--primary-foreground: 222 47% 6%;
--secondary: 280 65% 55%;
--secondary-foreground: 210 40% 98%;
--muted: 217 33% 15%;
--muted-foreground: 215 20% 55%;
--accent: 142 71% 45%;
--destructive: 0 84% 60%;
--destructive-foreground: 210 40% 98%;
--border: 217 33% 20%;
/* Custom neural network colors */
--node-input: 199 89% 48%;
--node-hidden: 280 65% 55%;
--node-positive: 142 71% 45%; /* Class A (Green) */
--node-negative: 350 89% 60%; /* Class B (Red) */
--radius: 0.75rem;
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: system-ui, -apple-system, sans-serif;
}
.glass-panel {
background-color: hsla(var(--card), 0.6);
backdrop-filter: blur(16px);
border: 1px solid hsla(var(--border), 0.5);
border-radius: var(--radius);
box-shadow: 0 0 30px hsl(199 89% 48% / 0.1);
}
.gradient-text {
background: linear-gradient(135deg, hsl(199 89% 48%) 0%, hsl(280 65% 55%) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Animation Utilities */
@keyframes flow {
0% { stroke-dashoffset: 20; }
100% { stroke-dashoffset: 0; }
}
.animate-flow {
animation: flow 1s linear infinite;
}
@keyframes pulse-glow {
0%, 100% { opacity: 1; filter: brightness(1); }
50% { opacity: 0.7; filter: brightness(1.2); }
}
.animate-node-pulse {
animation: pulse-glow 2s ease-in-out infinite;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: hsl(var(--background)); }
::-webkit-scrollbar-thumb { background: hsl(var(--muted)); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground)); }
input[type=range] {
-webkit-appearance: none;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: hsl(var(--primary));
cursor: pointer;
margin-top: -6px;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: hsl(var(--muted));
border-radius: 2px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
transition-colors: 0.15s;
cursor: pointer;
}
.btn:disabled {
opacity: 0.5;
pointer-events: none;
}
.btn-glass { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: white; }
.btn-glass:hover { background: rgba(255,255,255,0.1); }
.btn-accent { background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); }
.btn-destructive { background: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
.btn-glow {
background: hsl(var(--primary));
color: white;
box-shadow: 0 0 15px hsl(var(--primary)/0.5);
}
.btn-glow:hover { box-shadow: 0 0 25px hsl(var(--primary)/0.6); }
.tab-btn {
flex: 1;
padding: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.375rem;
transition: all 0.2s;
color: hsl(var(--muted-foreground));
}
.tab-btn.active {
background-color: hsl(var(--card));
color: hsl(var(--primary));
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
</style>
</head>
<body class="min-h-screen p-4 selection:bg-[hsl(var(--primary))] selection:text-white">
<!-- Background Ambience -->
<div class="fixed inset-0 pointer-events-none overflow-hidden -z-10">
<div class="absolute top-0 left-1/4 w-96 h-96 bg-[hsl(var(--primary)/0.1)] rounded-full blur-[100px]"></div>
<div class="absolute bottom-0 right-1/4 w-96 h-96 bg-[hsl(var(--secondary)/0.1)] rounded-full blur-[100px]"></div>
</div>
<!-- Header -->
<header class="relative z-10 border-b border-white/10 bg-[hsl(var(--background)/0.8)] backdrop-blur-md sticky top-0 mb-8 rounded-xl">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 rounded-xl bg-[hsl(var(--primary)/0.2)] animate-pulse">
<i data-lucide="brain" class="h-6 w-6 text-[hsl(var(--primary))]"></i>
</div>
<div>
<h1 class="text-xl font-bold gradient-text">Neural Network Playground</h1>
<div class="absolute left-1/2 -translate-x-1/2 flex items-center"> <audio id="clickSound" src="https://www.soundjay.com/buttons/sounds/button-3.mp3"></audio> <a href="/neural-network-classification" onclick="playSound(); return false;" class="inline-flex items-center justify-center text-center leading-none bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 px-6 rounded-xl text-sm transition-all duration-150 shadow-[0_4px_0_rgb(29,78,216)] active:shadow-none active:translate-y-[4px] uppercase tracking-wider"> Back to Core </a> </div>
<p class="text-xs text-[hsl(var(--muted-foreground))]">Interactive Classification Visualizer</p>
<p class="text-xxl p-3 text-[hsl(var(--muted-foreground))]">After training you cant train agian and cant change output so if you want add a custom data in predefine data so add before training</p>
</div>
</div>
<div class="hidden md:flex items-center gap-4 text-sm text-[hsl(var(--muted-foreground))]">
<div class="flex items-center gap-2 bg-white/5 px-3 py-1.5 rounded-full">
<i data-lucide="layers" class="h-4 w-4"></i>
<span id="header-neurons-count">4 hidden neurons</span>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="relative z-10 max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Left Sidebar: Controls -->
<div class="lg:col-span-3 space-y-6">
<!-- Control Panel -->
<div class="glass-panel p-5 space-y-5">
<div>
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">Data Class</h3>
<div class="flex gap-2">
<button onclick="setClass(1)" id="btn-class-a" class="btn btn-accent h-9 px-3 flex-1 text-sm">
<div class="w-3 h-3 rounded-full bg-[hsl(var(--node-positive))] mr-2"></div> Class A
</button>
<button onclick="setClass(0)" id="btn-class-b" class="btn btn-glass h-9 px-3 flex-1 text-sm">
<div class="w-3 h-3 rounded-full bg-[hsl(var(--node-negative))] mr-2"></div> Class B
</button>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">
Hidden Neurons: <span id="neurons-display" class="text-[hsl(var(--primary))]">4</span>
</h3>
<div class="flex items-center gap-3">
<button onclick="changeNeurons(-1)" class="btn btn-glass h-10 w-10 p-0"><i data-lucide="minus" class="h-4 w-4"></i></button>
<input type="range" min="1" max="8" value="4" class="flex-1" id="neurons-slider" oninput="changeNeuronsFromSlider(this.value)">
<button onclick="changeNeurons(1)" class="btn btn-glass h-10 w-10 p-0"><i data-lucide="plus" class="h-4 w-4"></i></button>
</div>
</div>
<div>
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))] mb-3">
Learning Rate: <span id="lr-display" class="text-[hsl(var(--secondary))]">0.50</span>
</h3>
<input type="range" min="1" max="100" value="50" class="w-full" id="lr-slider" oninput="changeLR(this.value)">
</div>
<div class="flex gap-2">
<button id="btn-train" onclick="toggleTraining()" class="btn btn-glow flex-1 h-10 px-4">
<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network
</button>
<button onclick="resetApp()" class="btn btn-glass h-10 w-10 p-0">
<i data-lucide="rotate-ccw" class="h-4 w-4"></i>
</button>
</div>
<div id="accuracy-panel" class="text-center py-3 rounded-lg bg-white/5 hidden">
<span class="text-sm text-[hsl(var(--muted-foreground))]">Accuracy: </span>
<span id="accuracy-display" class="text-lg font-bold">0.0%</span>
</div>
</div>
<!-- Presets -->
<div class="glass-panel p-4 space-y-3">
<h3 class="text-sm font-medium text-[hsl(var(--muted-foreground))]">Presets</h3>
<div class="grid grid-cols-2 gap-2">
<button onclick="loadPreset('Linear')" class="btn btn-glass flex flex-col h-auto py-3">
<i data-lucide="waves" class="h-4 w-4 mb-1"></i> <span class="text-xs">Linear</span>
</button>
<button onclick="loadPreset('XOR')" class="btn btn-glass flex flex-col h-auto py-3">
<i data-lucide="target" class="h-4 w-4 mb-1"></i> <span class="text-xs">XOR</span>
</button>
<button onclick="loadPreset('Circle')" class="btn btn-glass flex flex-col h-auto py-3">
<i data-lucide="circle" class="h-4 w-4 mb-1"></i> <span class="text-xs">Circle</span>
</button>
<button onclick="loadPreset('Spiral')" class="btn btn-glass flex flex-col h-auto py-3">
<i data-lucide="sparkles" class="h-4 w-4 mb-1"></i> <span class="text-xs">Spiral</span>
</button>
</div>
</div>
<!-- Logs -->
<div class="glass-panel p-4 space-y-3">
<div class="flex justify-between items-center text-sm font-medium text-[hsl(var(--muted-foreground))]">
<span class="flex items-center gap-2"><i data-lucide="clock" class="h-4 w-4"></i> Training Log</span>
<span id="epoch-display" class="text-[hsl(var(--primary))] animate-pulse hidden">Epoch 0</span>
</div>
<div class="w-full bg-white/10 rounded-full h-1.5 overflow-hidden">
<div id="progress-bar" class="h-full bg-gradient-to-r from-[hsl(var(--primary))] to-[hsl(var(--secondary))]" style="width: 0%"></div>
</div>
<div id="logs-container" class="space-y-1 max-h-32 overflow-y-auto">
<!-- Logs go here -->
</div>
</div>
</div>
<!-- Center: Visualizations -->
<div class="lg:col-span-6 space-y-6">
<!-- Network Vis -->
<div class="glass-panel p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold flex items-center gap-2">
<i data-lucide="brain" class="h-5 w-5 text-[hsl(var(--primary))]"></i> Network Architecture
</h2>
</div>
<div class="flex justify-center" id="network-container">
<!-- SVG Network goes here -->
</div>
</div>
<!-- Data Canvas -->
<div class="glass-panel p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold">Data & Decision Boundary</h2>
<span class="text-xs px-2 py-1 bg-white/10 rounded text-[hsl(var(--primary))] font-mono">Points: <span id="points-count">0</span></span>
</div>
<div class="flex justify-center relative">
<div class="relative">
<canvas id="main-canvas" width="300" height="300" class="rounded-lg border border-white/10 cursor-crosshair shadow-2xl bg-black"></canvas>
<div class="absolute -bottom-6 left-0 right-0 text-center text-xs text-muted-foreground text-[hsl(var(--muted-foreground))]">X Coordinate</div>
<div class="absolute -left-6 top-1/2 -translate-y-1/2 -rotate-90 text-xs text-muted-foreground text-[hsl(var(--muted-foreground))]">Y Coordinate</div>
</div>
</div>
<div class="mt-4 flex flex-wrap justify-center gap-4 text-xs text-[hsl(var(--muted-foreground))]">
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-[hsl(var(--node-positive))]"></div> Class A</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 rounded-full bg-[hsl(var(--node-negative))]"></div> Class B</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[hsl(var(--node-positive))/0.3]"></div> Prediction A</div>
<div class="flex items-center gap-2"><div class="w-3 h-3 bg-[hsl(var(--node-negative))/0.3]"></div> Prediction B</div>
</div>
</div>
</div>
<!-- Right Sidebar: Explainers -->
<div class="lg:col-span-3 space-y-6">
<div class="w-full">
<div class="flex bg-white/5 p-1 rounded-lg mb-4">
<button onclick="switchTab('howItWorks')" id="tab-howItWorks" class="tab-btn active"><i data-lucide="lightbulb" class="h-3 w-3 mr-1 inline"></i> How It Works</button>
<button onclick="switchTab('learn')" id="tab-learn" class="tab-btn"><i data-lucide="sparkles" class="h-3 w-3 mr-1 inline"></i> Learn</button>
</div>
<div id="content-howItWorks" class="glass-panel p-5 space-y-4">
<h3 class="text-lg font-semibold gradient-text">Live Prediction (Hover)</h3>
<div class="space-y-4 text-sm">
<div>
<div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--node-input))]">
<span class="w-5 h-5 rounded-full bg-[hsl(var(--node-input))/0.2] flex items-center justify-center text-xs">1</span> Input
</div>
<div class="bg-white/5 p-3 rounded-lg border border-white/10 font-mono text-xs">
X: <span id="val-x">0.00</span><br>
Y: <span id="val-y">0.00</span>
</div>
</div>
<div>
<div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--node-hidden))]">
<span class="w-5 h-5 rounded-full bg-[hsl(var(--node-hidden))/0.2] flex items-center justify-center text-xs">2</span> Hidden Layer
</div>
<div id="val-hidden" class="bg-white/5 p-3 rounded-lg border border-white/10 font-mono text-xs grid grid-cols-4 gap-1">
<!-- Hidden values -->
</div>
</div>
<div>
<div class="flex items-center gap-2 mb-2 font-medium text-[hsl(var(--accent))]">
<span class="w-5 h-5 rounded-full bg-[hsl(var(--accent))/0.2] flex items-center justify-center text-xs">3</span> Output
</div>
<div class="bg-white/5 p-3 rounded-lg border border-white/10">
<div class="flex justify-between items-center">
<span class="text-xs text-gray-400">Raw: <span id="val-raw">0.0000</span></span>
<span id="val-class" class="font-bold text-gray-500">-</span>
</div>
</div>
</div>
</div>
</div>
<div id="content-learn" class="glass-panel p-5 space-y-4 hidden">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-[hsl(var(--primary))/0.2]"><i data-lucide="brain" class="h-5 w-5 text-[hsl(var(--primary))]"></i></div>
<h3 class="font-semibold gradient-text">Training Process</h3>
</div>
<p class="text-sm text-gray-300 leading-relaxed">
The network learns by "Backpropagation". It compares its guess to the real label, finds the error, and adjusts the weights backwards from output to input.
</p>
<div class="p-3 rounded-lg bg-white/5 text-xs text-gray-400 border border-white/10">
💡 <strong>Tip:</strong> If the network gets stuck, try increasing neurons or clicking "Reset" to randomize weights.
</div>
</div>
</div>
</div>
</main>
<script>
// --- Neural Network Logic ---
class SimpleNeuralNetwork {
constructor(inputSize, hiddenSize, outputSize, learningRate) {
this.inputSize = inputSize;
this.hiddenSize = hiddenSize;
this.outputSize = outputSize;
this.learningRate = learningRate;
// Xavier initialization
const scale1 = Math.sqrt(2 / (this.inputSize + this.hiddenSize));
this.w1 = Array(this.hiddenSize).fill(0).map(() =>
Array(this.inputSize).fill(0).map(() => (Math.random() * 2 - 1) * scale1)
);
this.b1 = Array(this.hiddenSize).fill(0);
const scale2 = Math.sqrt(2 / (this.hiddenSize + this.outputSize));
this.w2 = Array(this.outputSize).fill(0).map(() =>
Array(this.hiddenSize).fill(0).map(() => (Math.random() * 2 - 1) * scale2)
);
this.b2 = Array(this.outputSize).fill(0);
}
sigmoid(x) { return 1 / (1 + Math.exp(-x)); }
sigmoidDeriv(y) { return y * (1 - y); }
forward(inputs) {
const hActivations = this.w1.map((weights, i) =>
this.sigmoid(weights.reduce((acc, w, j) => acc + w * inputs[j], 0) + this.b1[i])
);
const outputs = this.w2.map((weights, i) =>
this.sigmoid(weights.reduce((acc, w, j) => acc + w * hActivations[j], 0) + this.b2[i])
);
return { activations: [[...inputs], hActivations, outputs], output: outputs[0] };
}
predict(x, y) { return this.forward([x, y]).output; }
train(data, batchSize) {
for(let k = 0; k < batchSize * 5; k++) {
const point = data[Math.floor(Math.random() * data.length)];
const inputs = [point.x, point.y];
const target = [point.label];
const { activations } = this.forward(inputs);
const hActivations = activations[1];
const outputs = activations[2];
const outputErrors = outputs.map((o, i) => target[i] - o);
const outputGradients = outputs.map((o, i) => outputErrors[i] * this.sigmoidDeriv(o));
const hiddenErrors = this.w1.map((_, i) =>
this.w2.reduce((acc, weights, j) => acc + weights[i] * outputGradients[j], 0)
);
const hiddenGradients = hActivations.map((h, i) => hiddenErrors[i] * this.sigmoidDeriv(h));
for(let i=0; i<this.outputSize; i++) {
for(let j=0; j<this.hiddenSize; j++) {
this.w2[i][j] += this.learningRate * outputGradients[i] * hActivations[j];
}
this.b2[i] += this.learningRate * outputGradients[i];
}
for(let i=0; i<this.hiddenSize; i++) {
for(let j=0; j<this.inputSize; j++) {
this.w1[i][j] += this.learningRate * hiddenGradients[i] * inputs[j];
}
this.b1[i] += this.learningRate * hiddenGradients[i];
}
}
}
getWeights() { return [this.w1, this.w2]; }
}
// --- Application State ---
let state = {
dataPoints: [],
currentClass: 1,
hiddenNeurons: 4,
learningRate: 0.5,
isTraining: false,
network: null,
epoch: 0,
accuracy: 0,
activations: null,
predictions: [], // Store heatmap data here
logs: [],
lastProbe: { x: 0, y: 0 } // Track last cursor position for live updates
};
// --- Helper: Generate Grid Predictions ---
function generatePredictions() {
const gridSize = 30;
const grid = [];
for (let i = 0; i < gridSize; i++) {
const row = [];
for (let j = 0; j < gridSize; j++) {
const x = (j / gridSize) * 2 - 1;
const y = 1 - (i / gridSize) * 2;
row.push(state.network.predict(x, y));
}
grid.push(row);
}
return grid;
}
// --- Initialization ---
function init() {
lucide.createIcons();
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
state.predictions = generatePredictions(); // Initial heatmap
setupCanvas();
renderUI();
// Set initial dummy activations
state.activations = [[0,0], Array(state.hiddenNeurons).fill(0), [0]];
updateExplainers();
}
// --- UI Updates ---
function setClass(c) {
state.currentClass = c;
const btnA = document.getElementById('btn-class-a');
const btnB = document.getElementById('btn-class-b');
if(c === 1) {
btnA.classList.remove('btn-glass'); btnA.classList.add('btn-accent');
btnB.classList.add('btn-glass'); btnB.classList.remove('btn-destructive');
} else {
btnA.classList.add('btn-glass'); btnA.classList.remove('btn-accent');
btnB.classList.remove('btn-glass'); btnB.classList.add('btn-destructive');
}
}
function changeNeurons(delta) {
const newVal = Math.max(1, Math.min(8, state.hiddenNeurons + delta));
state.hiddenNeurons = newVal;
document.getElementById('neurons-slider').value = newVal;
updateNeuronsUI();
}
function changeNeuronsFromSlider(val) {
state.hiddenNeurons = parseInt(val);
updateNeuronsUI();
}
function updateNeuronsUI() {
document.getElementById('neurons-display').innerText = state.hiddenNeurons;
document.getElementById('header-neurons-count').innerText = state.hiddenNeurons + " hidden neurons";
resetApp(); // Rebuild network on architecture change
}
function changeLR(val) {
state.learningRate = val / 100;
document.getElementById('lr-display').innerText = state.learningRate.toFixed(2);
if(state.network) state.network.learningRate = state.learningRate;
}
function resetApp() {
state.dataPoints = [];
state.isTraining = false;
state.epoch = 0;
state.accuracy = 0;
state.logs = [];
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
state.predictions = generatePredictions(); // Generate initial random boundary
state.lastProbe = { x: 0, y: 0 };
document.getElementById('points-count').innerText = "0";
document.getElementById('accuracy-panel').classList.add('hidden');
document.getElementById('epoch-display').classList.add('hidden');
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('logs-container').innerHTML = '';
const btnTrain = document.getElementById('btn-train');
btnTrain.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
lucide.createIcons();
renderCanvas();
renderNetwork();
// Recalc activations for default probe
state.activations = state.network.forward([0, 0]).activations;
updateExplainers();
}
function switchTab(tab) {
document.getElementById('tab-howItWorks').classList.remove('active');
document.getElementById('tab-learn').classList.remove('active');
document.getElementById('content-howItWorks').classList.add('hidden');
document.getElementById('content-learn').classList.add('hidden');
document.getElementById('tab-' + tab).classList.add('active');
document.getElementById('content-' + tab).classList.remove('hidden');
}
// --- Canvas Logic ---
const canvas = document.getElementById('main-canvas');
const ctx = canvas.getContext('2d');
const canvasSize = 300;
const gridSize = 30;
function setupCanvas() {
canvas.addEventListener('mousedown', handleCanvasClick);
canvas.addEventListener('mousemove', handleCanvasHover);
canvas.addEventListener('mouseleave', () => {
renderCanvas(); // clear hover
});
renderCanvas();
}
function handleCanvasClick(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
const x = (clickX / (canvasSize / 2)) - 1;
const y = 1 - (clickY / (canvasSize / 2));
const point = {
x: Math.max(-1, Math.min(1, x)),
y: Math.max(-1, Math.min(1, y)),
label: state.currentClass
};
state.dataPoints.push(point);
state.lastProbe = { x, y }; // Update probe
document.getElementById('points-count').innerText = state.dataPoints.length;
// Forward pass for viz
state.activations = state.network.forward([point.x, point.y]).activations;
updateExplainers();
renderCanvas();
renderNetwork();
}
function handleCanvasHover(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
renderCanvas();
// Draw hover cursor
ctx.beginPath();
ctx.arc(clickX, clickY, 8, 0, Math.PI * 2);
ctx.strokeStyle = state.currentClass === 1 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
ctx.setLineDash([4, 4]);
ctx.stroke();
ctx.setLineDash([]);
// LIVE UPDATE: Calculate network output for current mouse position
const x = (clickX / (canvasSize / 2)) - 1;
const y = 1 - (clickY / (canvasSize / 2));
state.lastProbe = { x, y }; // Update probe tracker
if (state.network) {
state.activations = state.network.forward([x, y]).activations;
updateExplainers(); // Update the "Output" panel text
renderNetwork(); // Update the node visualizations/colors
}
}
function renderCanvas() {
// Background
ctx.fillStyle = 'hsl(222, 47%, 8%)';
ctx.fillRect(0, 0, canvasSize, canvasSize);
// Heatmap - Draw if predictions exist
if (state.predictions && state.predictions.length > 0) {
const cellSize = canvasSize / gridSize;
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
const pred = state.predictions[i][j];
const hue = pred > 0.5 ? 142 : 350;
const lightness = 20 + Math.abs(pred - 0.5) * 40;
const alpha = 0.3 + Math.abs(pred - 0.5) * 0.4;
ctx.fillStyle = `hsla(${hue}, 70%, ${lightness}%, ${alpha})`;
ctx.fillRect(j * cellSize, i * cellSize, cellSize, cellSize);
}
}
}
// Grid
ctx.strokeStyle = 'hsla(217, 33%, 40%, 0.2)';
ctx.lineWidth = 1;
for (let i = 0; i <= canvasSize; i += 30) {
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvasSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvasSize, i); ctx.stroke();
}
// Axes
ctx.strokeStyle = 'hsla(217, 33%, 50%, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(canvasSize / 2, 0); ctx.lineTo(canvasSize / 2, canvasSize); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, canvasSize / 2); ctx.lineTo(canvasSize, canvasSize / 2); ctx.stroke();
// Points
state.dataPoints.forEach(point => {
const drawX = (point.x + 1) * (canvasSize / 2);
const drawY = (1 - point.y) * (canvasSize / 2);
ctx.beginPath();
ctx.arc(drawX, drawY, 6, 0, Math.PI * 2);
ctx.fillStyle = point.label === 1 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
ctx.fill();
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
ctx.stroke();
});
}
// --- Network Visualizer (SVG) ---
function renderNetwork() {
const container = document.getElementById('network-container');
const width = 500;
const height = 300;
const layers = [2, state.hiddenNeurons, 1];
const layerSpacing = (width - 100) / (layers.length - 1);
let svgHtml = `<svg width="${width}" height="${height}" style="overflow: visible;">`;
// Calculate positions
const nodePositions = [];
layers.forEach((count, layerIdx) => {
const x = 50 + layerIdx * layerSpacing;
const maxNodes = Math.max(...layers);
const vSpacing = (height - 100) / (maxNodes + 1);
const offset = ((maxNodes - count) * vSpacing) / 2;
for(let i=0; i<count; i++) {
nodePositions.push({
x: x,
y: 50 + offset + (i+1) * vSpacing,
layer: layerIdx,
index: i
});
}
});
// Draw Connections
const weights = state.network.getWeights();
let fromIndex = 0;
for(let l=0; l<layers.length-1; l++) {
const fromCount = layers[l];
const toCount = layers[l+1];
const toStartIndex = fromIndex + fromCount;
for(let i=0; i<fromCount; i++) {
for(let j=0; j<toCount; j++) {
const w = weights[l][j][i];
const fromNode = nodePositions[fromIndex + i];
const toNode = nodePositions[toStartIndex + j];
const opacity = Math.min(0.8, 0.1 + Math.abs(w) * 0.3);
const color = w > 0 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
const dash = state.isTraining ? 'stroke-dasharray="4 4" class="animate-flow"' : '';
svgHtml += `<line x1="${fromNode.x}" y1="${fromNode.y}" x2="${toNode.x}" y2="${toNode.y}" stroke="${color}" stroke-width="${1 + Math.abs(w)}" stroke-opacity="${opacity}" ${dash} />`;
}
}
fromIndex += fromCount;
}
// Draw Nodes
nodePositions.forEach(node => {
let activation = 0;
if(state.activations) {
activation = state.activations[node.layer][node.index];
}
let color = 'hsl(280, 65%, 55%)'; // hidden
if(node.layer === 0) color = 'hsl(199, 89%, 48%)'; // input
if(node.layer === layers.length - 1) {
color = activation > 0.5 ? 'hsl(142, 71%, 45%)' : 'hsl(350, 89%, 60%)';
}
const r = 10 + (activation * 5);
const pulseClass = state.isTraining ? 'class="animate-node-pulse"' : '';
svgHtml += `<circle cx="${node.x}" cy="${node.y}" r="${r+4}" fill="none" stroke="${color}" stroke-opacity="0.3" ${pulseClass} />`;
svgHtml += `<circle cx="${node.x}" cy="${node.y}" r="${r}" fill="${color}" />`;
svgHtml += `<text x="${node.x}" y="${node.y - r - 5}" text-anchor="middle" fill="white" font-size="9">${activation.toFixed(2)}</text>`;
});
svgHtml += `</svg>`;
container.innerHTML = svgHtml;
}
// --- Explainers ---
function updateExplainers() {
if(!state.activations) return;
// Input
document.getElementById('val-x').innerText = state.activations[0][0].toFixed(2);
document.getElementById('val-y').innerText = state.activations[0][1].toFixed(2);
// Hidden
const hiddenContainer = document.getElementById('val-hidden');
let hiddenHtml = '';
state.activations[1].forEach(v => {
const cls = v > 0.5 ? 'bg-white/10 text-white' : 'text-gray-500';
hiddenHtml += `<div class="text-center p-1 rounded ${cls}">${v.toFixed(1)}</div>`;
});
hiddenContainer.innerHTML = hiddenHtml;
// Output
const outVal = state.activations[2][0];
document.getElementById('val-raw').innerText = outVal.toFixed(4);
const classEl = document.getElementById('val-class');
// Direct style application
if(outVal > 0.5) {
classEl.innerText = "Class A";
classEl.style.color = "hsl(142, 71%, 45%)"; // Green
} else {
classEl.innerText = "Class B";
classEl.style.color = "hsl(350, 89%, 60%)"; // Red
}
}
// --- Training Loop ---
function toggleTraining() {
if(state.dataPoints.length < 2) {
alert("Please add at least 2 data points first!");
return;
}
state.isTraining = !state.isTraining;
const btn = document.getElementById('btn-train');
if(state.isTraining) {
btn.innerHTML = '<i data-lucide="zap" class="h-4 w-4 animate-pulse mr-2"></i> Stop';
document.getElementById('epoch-display').classList.remove('hidden');
document.getElementById('accuracy-panel').classList.remove('hidden');
trainStep();
} else {
btn.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
}
lucide.createIcons();
}
function trainStep() {
if(!state.isTraining) return;
if(state.epoch >= 100) {
toggleTraining();
return;
}
state.network.train(state.dataPoints, 10);
state.epoch++;
// Recalculate predictions for the current cursor position ("lastProbe")
// This ensures the "Live Prediction" text updates instantly as the network learns
state.activations = state.network.forward([state.lastProbe.x, state.lastProbe.y]).activations;
updateExplainers();
renderNetwork(); // Update the nodes/weights visual
if(state.epoch % 5 === 0) {
// Update predictions every 5 epochs
state.predictions = generatePredictions();
// Calc accuracy
let correct = 0;
state.dataPoints.forEach(p => {
if ((state.network.predict(p.x, p.y) > 0.5 ? 1 : 0) === p.label) correct++;
});
state.accuracy = correct / state.dataPoints.length;
// Update UI
document.getElementById('epoch-display').innerText = "Epoch " + state.epoch;
document.getElementById('accuracy-display').innerText = (state.accuracy * 100).toFixed(1) + "%";
document.getElementById('accuracy-display').className = "text-lg font-bold " + (state.accuracy > 0.8 ? 'text-[hsl(var(--accent))]' : state.accuracy > 0.5 ? 'text-[hsl(var(--secondary))]' : 'text-[hsl(var(--destructive))]');
document.getElementById('progress-bar').style.width = state.epoch + "%";
// Add log
const logItem = `<div class="flex justify-between text-xs py-1 border-b border-white/5 last:border-0">
<span class="text-[hsl(var(--muted-foreground))]">Epoch ${state.epoch}</span>
<span class="${state.accuracy > 0.8 ? 'text-[hsl(var(--accent))]' : 'text-white'}">${(state.accuracy * 100).toFixed(1)}%</span>
</div>`;
document.getElementById('logs-container').insertAdjacentHTML('afterbegin', logItem);
renderCanvas();
}
requestAnimationFrame(trainStep);
}
// --- Presets ---
function loadPreset(type) {
let points = [];
if (type === 'XOR') {
for(let i=0; i<20; i++) {
points.push({ x: -0.5 + Math.random()*0.3, y: 0.5 + Math.random()*0.3, label: 1 });
points.push({ x: 0.5 + Math.random()*0.3, y: -0.5 - Math.random()*0.3, label: 1 });
points.push({ x: 0.5 + Math.random()*0.3, y: 0.5 + Math.random()*0.3, label: 0 });
points.push({ x: -0.5 + Math.random()*0.3, y: -0.5 - Math.random()*0.3, label: 0 });
}
} else if (type === 'Circle') {
for(let i=0; i<40; i++) {
const angle = Math.random() * Math.PI * 2;
const r1 = Math.random() * 0.4;
points.push({ x: Math.cos(angle)*r1, y: Math.sin(angle)*r1, label: 1 });
const r2 = 0.6 + Math.random() * 0.3;
points.push({ x: Math.cos(angle)*r2, y: Math.sin(angle)*r2, label: 0 });
}
} else if (type === 'Linear') {
for(let i=0; i<30; i++) {
points.push({ x: -0.4 - Math.random()*0.4, y: Math.random()*1.6 - 0.8, label: 1 });
points.push({ x: 0.4 + Math.random()*0.4, y: Math.random()*1.6 - 0.8, label: 0 });
}
} else if (type === 'Spiral') {
for (let i = 0; i < 60; i++) {
const r = i / 60;
const t = 1.75 * i / 60 * 2 * Math.PI;
points.push({ x: r * Math.sin(t), y: r * Math.cos(t), label: 1 });
points.push({ x: r * Math.sin(t + Math.PI), y: r * Math.cos(t + Math.PI), label: 0 });
}
}
// Reset but keep new points and generate initial map
state.dataPoints = points;
state.isTraining = false;
state.epoch = 0;
state.accuracy = 0;
state.logs = [];
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
state.predictions = generatePredictions(); // Generate immediate prediction map
state.lastProbe = { x: 0, y: 0 };
document.getElementById('points-count').innerText = points.length;
document.getElementById('accuracy-panel').classList.add('hidden');
document.getElementById('epoch-display').classList.add('hidden');
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('logs-container').innerHTML = '';
const btnTrain = document.getElementById('btn-train');
btnTrain.innerHTML = '<i data-lucide="play" class="h-4 w-4 mr-2"></i> Train Network';
lucide.createIcons();
renderCanvas();
renderNetwork();
if(points.length) {
// Set probe to first point to avoid 0,0 default
state.lastProbe = { x: points[0].x, y: points[0].y };
state.activations = state.network.forward([points[0].x, points[0].y]).activations;
updateExplainers();
}
}
function renderUI() {
renderNetwork();
}
// Start
window.onload = init;
</script>
</body>
</html>