|
|
<!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>
|
|
|
|
|
|
<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%;
|
|
|
|
|
|
|
|
|
--node-input: 199 89% 48%;
|
|
|
--node-hidden: 280 65% 55%;
|
|
|
--node-positive: 142 71% 45%;
|
|
|
--node-negative: 350 89% 60%;
|
|
|
|
|
|
--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;
|
|
|
}
|
|
|
|
|
|
|
|
|
@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;
|
|
|
}
|
|
|
|
|
|
|
|
|
::-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">
|
|
|
|
|
|
|
|
|
<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 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 class="relative z-10 max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
|
|
|
|
|
|
|
<div class="lg:col-span-3 space-y-6">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
<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">
|
|
|
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<div class="lg:col-span-6 space-y-6">
|
|
|
|
|
|
<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">
|
|
|
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
<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">
|
|
|
|
|
|
</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>
|
|
|
|
|
|
class SimpleNeuralNetwork {
|
|
|
constructor(inputSize, hiddenSize, outputSize, learningRate) {
|
|
|
this.inputSize = inputSize;
|
|
|
this.hiddenSize = hiddenSize;
|
|
|
this.outputSize = outputSize;
|
|
|
this.learningRate = learningRate;
|
|
|
|
|
|
|
|
|
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]; }
|
|
|
}
|
|
|
|
|
|
|
|
|
let state = {
|
|
|
dataPoints: [],
|
|
|
currentClass: 1,
|
|
|
hiddenNeurons: 4,
|
|
|
learningRate: 0.5,
|
|
|
isTraining: false,
|
|
|
network: null,
|
|
|
epoch: 0,
|
|
|
accuracy: 0,
|
|
|
activations: null,
|
|
|
predictions: [],
|
|
|
logs: [],
|
|
|
lastProbe: { x: 0, y: 0 }
|
|
|
};
|
|
|
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
|
|
|
function init() {
|
|
|
lucide.createIcons();
|
|
|
state.network = new SimpleNeuralNetwork(2, state.hiddenNeurons, 1, state.learningRate);
|
|
|
state.predictions = generatePredictions();
|
|
|
setupCanvas();
|
|
|
renderUI();
|
|
|
|
|
|
|
|
|
state.activations = [[0,0], Array(state.hiddenNeurons).fill(0), [0]];
|
|
|
updateExplainers();
|
|
|
}
|
|
|
|
|
|
|
|
|
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();
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
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();
|
|
|
|
|
|
|
|
|
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');
|
|
|
}
|
|
|
|
|
|
|
|
|
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();
|
|
|
});
|
|
|
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 };
|
|
|
document.getElementById('points-count').innerText = state.dataPoints.length;
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
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([]);
|
|
|
|
|
|
|
|
|
const x = (clickX / (canvasSize / 2)) - 1;
|
|
|
const y = 1 - (clickY / (canvasSize / 2));
|
|
|
state.lastProbe = { x, y };
|
|
|
|
|
|
if (state.network) {
|
|
|
state.activations = state.network.forward([x, y]).activations;
|
|
|
updateExplainers();
|
|
|
renderNetwork();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function renderCanvas() {
|
|
|
|
|
|
ctx.fillStyle = 'hsl(222, 47%, 8%)';
|
|
|
ctx.fillRect(0, 0, canvasSize, canvasSize);
|
|
|
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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();
|
|
|
}
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
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();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
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;">`;
|
|
|
|
|
|
|
|
|
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
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
|
|
|
nodePositions.forEach(node => {
|
|
|
let activation = 0;
|
|
|
if(state.activations) {
|
|
|
activation = state.activations[node.layer][node.index];
|
|
|
}
|
|
|
|
|
|
let color = 'hsl(280, 65%, 55%)';
|
|
|
if(node.layer === 0) color = 'hsl(199, 89%, 48%)';
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
|
|
|
function updateExplainers() {
|
|
|
if(!state.activations) return;
|
|
|
|
|
|
|
|
|
document.getElementById('val-x').innerText = state.activations[0][0].toFixed(2);
|
|
|
document.getElementById('val-y').innerText = state.activations[0][1].toFixed(2);
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
const outVal = state.activations[2][0];
|
|
|
document.getElementById('val-raw').innerText = outVal.toFixed(4);
|
|
|
const classEl = document.getElementById('val-class');
|
|
|
|
|
|
|
|
|
if(outVal > 0.5) {
|
|
|
classEl.innerText = "Class A";
|
|
|
classEl.style.color = "hsl(142, 71%, 45%)";
|
|
|
} else {
|
|
|
classEl.innerText = "Class B";
|
|
|
classEl.style.color = "hsl(350, 89%, 60%)";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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++;
|
|
|
|
|
|
|
|
|
|
|
|
state.activations = state.network.forward([state.lastProbe.x, state.lastProbe.y]).activations;
|
|
|
updateExplainers();
|
|
|
renderNetwork();
|
|
|
|
|
|
if(state.epoch % 5 === 0) {
|
|
|
|
|
|
state.predictions = generatePredictions();
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
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 + "%";
|
|
|
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
|
|
|
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 });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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();
|
|
|
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) {
|
|
|
|
|
|
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();
|
|
|
}
|
|
|
|
|
|
|
|
|
window.onload = init;
|
|
|
</script>
|
|
|
</body>
|
|
|
</html> |