| | <!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> |