Download templates/Naive-Bayes-Simulator.html from deedrop1140/machinelearningalgorithms: direct link, hf CLI and curl.
- Browser
- Download file 35.6 kB
-
https://huggingface.co/spaces/deedrop1140/machinelearningalgorithms/resolve/main/templates/Naive-Bayes-Simulator.html
- Command line
-
hf download hf://spaces/deedrop1140/machinelearningalgorithms/templates/Naive-Bayes-Simulator.html
-
curl -L -o Naive-Bayes-Simulator.html https://huggingface.co/spaces/deedrop1140/machinelearningalgorithms/resolve/main/templates/Naive-Bayes-Simulator.html
35.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Naive Bayes Visualizer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --background: #05080a; | |
| --primary: #ff0055; /* Apple Red */ | |
| --secondary: #ffcc00; /* Orange Orange */ | |
| --accent: #00ffff; | |
| --surface: #0a1014; | |
| --border: #1e293b; | |
| } | |
| body { | |
| background-color: var(--background); | |
| color: #f8fafc; | |
| font-family: 'Space Grotesk', sans-serif; | |
| margin: 0; | |
| overflow-x: hidden; | |
| } | |
| .glass { | |
| background: rgba(10, 16, 20, 0.85); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(30, 41, 59, 0.5); | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); | |
| } | |
| .canvas-container { | |
| width: 100%; | |
| height: 50vh; /* Responsive height */ | |
| min-height: 400px; | |
| max-height: 600px; | |
| position: relative; | |
| background: radial-gradient(circle at center, #111827 0%, #000000 100%); | |
| border-radius: 1rem; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| cursor: grab; | |
| } | |
| .canvas-container:active { cursor: grabbing; } | |
| .learning-log { | |
| height: 120px; | |
| overflow-y: auto; | |
| scrollbar-width: thin; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: #0f172a; } | |
| ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; } | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 6px; | |
| cursor: pointer; | |
| background: #1e293b; | |
| border-radius: 3px; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| height: 18px; | |
| width: 18px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| cursor: pointer; | |
| -webkit-appearance: none; | |
| margin-top: -6px; | |
| box-shadow: 0 0 10px rgba(0, 255, 255, 0.5); | |
| } | |
| .math-box { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| } | |
| .hidden { display: none; } | |
| </style> | |
| </head> | |
| <body class="p-4 md:p-6 lg:p-8"> | |
| <div class="max-w-7xl mx-auto grid lg:grid-cols-[1fr_380px] gap-6 lg:gap-8"> | |
| <!-- Left Side: Visualizer --> | |
| <div class="flex flex-col gap-4"> | |
| <header> | |
| <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4"> | |
| <div> | |
| <h1 class="text-3xl md:text-4xl font-bold text-white mb-2">3D Naive Bayes <span class="text-cyan-400">Viz</span></h1> | |
| <p class="text-slate-400 text-sm max-w-xl"> | |
| A classifier that learns by shaping 3D probability clouds. It assumes dimensions (Weight, Sweetness, Color) are independent. | |
| </p> | |
| <!-- Centered Button --> <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="/naive_bayes" 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> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="relative"> | |
| <!-- 3D Canvas --> | |
| <div class="canvas-container shadow-2xl" id="container"> | |
| <div id="three-canvas" class="w-full h-full"></div> | |
| <!-- Top Overlays (Inside Canvas) --> | |
| <div class="absolute top-4 left-4 pointer-events-none flex flex-col gap-2 z-10 max-w-[200px]"> | |
| <div class="glass px-3 py-2 rounded-lg"> | |
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Status</div> | |
| <div id="status-text" class="text-xs font-mono text-cyan-400 mt-1 uppercase font-bold animate-pulse">Waiting to Learn</div> | |
| </div> | |
| <div class="glass px-3 py-2 rounded-lg flex flex-col gap-1"> | |
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest mb-1">Legend</div> | |
| <div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-[#ff0055] shadow-[0_0_8px_#ff0055]"></span> Apple Class</div> | |
| <div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-[#ffcc00] shadow-[0_0_8px_#ffcc00]"></span> Orange Class</div> | |
| <div class="flex items-center gap-2 text-[10px] font-bold text-white"><span class="w-2.5 h-2.5 rounded-full bg-white border border-slate-500"></span> Mystery Fruit</div> | |
| </div> | |
| </div> | |
| <!-- Measurement Label (Inside Canvas) --> | |
| <div class="absolute top-4 right-4 pointer-events-none text-right z-10"> | |
| <div class="glass px-3 py-2 rounded-lg"> | |
| <div class="text-[10px] text-slate-500 font-mono uppercase tracking-widest">Input Features</div> | |
| <div id="pos-display" class="text-xs font-mono text-cyan-400 mt-1 uppercase">W:0.0 S:0.0 C:0.0</div> | |
| </div> | |
| </div> | |
| <!-- Instructions Overlay (Mobile/Desktop) --> | |
| <div class="absolute bottom-4 left-4 pointer-events-none z-10 hidden md:block"> | |
| <div class="glass px-3 py-2 rounded-lg text-[10px] text-slate-400"> | |
| <b>Left-Click</b> Rotate | <b>Right-Click</b> Pan | <b>Shift+Drag</b> Lift Y-Axis | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Result Card: Below on Mobile, Absolute Overlay on Desktop --> | |
| <div class="mt-4 md:mt-0 md:absolute md:bottom-4 md:right-4 z-20 w-full md:w-80"> | |
| <div class="glass p-4 rounded-xl shadow-xl border border-slate-700/50"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <div class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Prediction Engine</div> | |
| <div id="uncertainty-badge" class="hidden text-[9px] bg-slate-700 text-white px-2 py-0.5 rounded font-mono">UNCERTAIN</div> | |
| </div> | |
| <!-- Apple Calc --> | |
| <div class="space-y-1 mb-3"> | |
| <div class="flex justify-between text-[10px] text-[#ff0055] font-bold uppercase items-center"> | |
| <span>Apple Likelihood</span> | |
| <span id="prob-a-val" class="text-xs">50%</span> | |
| </div> | |
| <div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden"> | |
| <div id="bar-a" class="h-full bg-[#ff0055] transition-all duration-300" style="width: 50%"></div> | |
| </div> | |
| <div class="math-box text-[9px] text-slate-500 flex justify-between px-1"> | |
| <span>Prior(<span id="prior-a">.5</span>)</span> | |
| <span>×</span> | |
| <span>L(<span id="total-l-a">0</span>)</span> | |
| </div> | |
| </div> | |
| <!-- Orange Calc --> | |
| <div class="space-y-1 mb-4"> | |
| <div class="flex justify-between text-[10px] text-[#ffcc00] font-bold uppercase items-center"> | |
| <span>Orange Likelihood</span> | |
| <span id="prob-b-val" class="text-xs">50%</span> | |
| </div> | |
| <div class="w-full bg-slate-800 h-1.5 rounded-full overflow-hidden"> | |
| <div id="bar-b" class="h-full bg-[#ffcc00] transition-all duration-300" style="width: 50%"></div> | |
| </div> | |
| <div class="math-box text-[9px] text-slate-500 flex justify-between px-1"> | |
| <span>Prior(<span id="prior-b">.5</span>)</span> | |
| <span>×</span> | |
| <span>L(<span id="total-l-b">0</span>)</span> | |
| </div> | |
| </div> | |
| <div class="pt-3 border-t border-slate-700/50 text-center"> | |
| <div class="text-[9px] text-slate-500 uppercase font-bold mb-1">Final Classification</div> | |
| <div id="result-text" class="text-2xl font-bold text-white uppercase tracking-tighter">NEUTRAL</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- The Learning Log --> | |
| <div class="glass rounded-xl p-4 flex flex-col h-40"> | |
| <h3 class="text-xs font-bold text-white mb-2 flex items-center gap-2 shrink-0"> | |
| <span class="text-cyan-400">⚡</span> System Logs | |
| </h3> | |
| <div id="learning-log" class="learning-log text-xs font-mono text-slate-400 space-y-1.5 pr-2"> | |
| <div>> System initialized.</div> | |
| <div>> Waiting for training data...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Side: Controls --> | |
| <aside class="space-y-6"> | |
| <div class="glass rounded-xl p-5 space-y-6"> | |
| <h2 class="text-lg font-bold text-white flex items-center gap-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-cyan-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.532 1.532 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.532 1.532 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /></svg> | |
| Control Panel | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <div class="flex justify-between items-center mb-2"> | |
| <label class="text-xs font-bold text-slate-400 uppercase">Search Variance (Alpha)</label> | |
| <span id="alpha-value" class="text-cyan-400 font-mono text-sm bg-cyan-950 px-2 rounded">1.00</span> | |
| </div> | |
| <input type="range" id="alpha-slider" min="0.1" max="5.0" step="0.1" value="1.0"> | |
| <p class="text-[10px] text-slate-500 mt-2"> | |
| Higher alpha = wider probability clouds (High Bias). Lower alpha = tighter clouds (High Variance). | |
| </p> | |
| </div> | |
| <div class="grid grid-cols-1 gap-3 pt-2"> | |
| <button id="btn-toggle" class="group relative flex items-center justify-center gap-2 py-3 rounded-lg font-bold text-black bg-cyan-400 hover:bg-cyan-300 transition-all overflow-hidden"> | |
| <div class="absolute inset-0 bg-white/20 translate-y-full group-hover:translate-y-0 transition-transform duration-300"></div> | |
| <span id="toggle-icon">▶</span> <span id="toggle-text">Start Training</span> | |
| </button> | |
| <button id="btn-reset" class="flex items-center justify-center gap-2 py-3 rounded-lg border border-slate-700 hover:bg-slate-800 hover:border-slate-600 transition-all font-bold text-sm text-slate-300"> | |
| Reset Simulation | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Scenario Selection --> | |
| <div class="glass rounded-xl p-5 space-y-3"> | |
| <h3 class="text-xs font-bold text-slate-500 uppercase tracking-widest mb-1">Data Scenario</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button onclick="selectScenario(0)" id="scen-0" class="text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all">Easy (Clustered)</button> | |
| <button onclick="selectScenario(1)" id="scen-1" class="text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all">Hard (Scattered)</button> | |
| </div> | |
| <p class="text-[10px] text-slate-500 leading-relaxed pt-2 border-t border-slate-800"> | |
| Switching scenarios resets the robot's memory. | |
| </p> | |
| </div> | |
| <!-- Learning Concept Card --> | |
| <div class="glass rounded-xl p-5 border-l-2 border-cyan-400"> | |
| <h3 class="text-xs font-bold text-cyan-400 uppercase tracking-widest mb-2">How it works</h3> | |
| <p class="text-[11px] text-slate-300 leading-relaxed"> | |
| The robot calculates the center (mean) and spread (variance) of the points it sees. | |
| <br><br> | |
| To predict the mystery fruit, it measures the distance to each cloud center relative to the cloud's size. | |
| <br><br> | |
| <span class="text-white font-bold">Naive Assumption:</span> It processes Width, Sweetness, and Color completely separately, then multiplies the results. | |
| </p> | |
| </div> | |
| </aside> | |
| </div> | |
| <script> | |
| // --- DATA CONFIG --- | |
| const SCENARIOS = [ | |
| { name: 'Clustered', points: 80, spread: 1.2 }, | |
| { name: 'Scattered', points: 200, spread: 3.5 } | |
| ]; | |
| let currentScenario = SCENARIOS[0]; | |
| let dataPoints = []; | |
| let processedIdx = 0; | |
| let isRunning = false; | |
| let loopId = null; | |
| let alpha = 1.0; | |
| // Model State | |
| let model = [ | |
| { name: 'Apple', color: 0xff0055, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 }, | |
| { name: 'Orange', color: 0xffcc00, count: 0, mean: {x:0, y:0, z:0}, var: {x:1, y:1, z:1}, score: 0 } | |
| ]; | |
| // --- THREE.JS SETUP --- | |
| const container = document.getElementById('container'); | |
| const scene = new THREE.Scene(); | |
| // Dark gradient background effect via clear color not possible easily, handled via CSS | |
| const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000); | |
| let cameraRadius = 35; | |
| let cameraPhi = Math.PI / 2.5; | |
| let cameraTheta = Math.PI / 4; | |
| function updateCameraPosition() { | |
| camera.position.x = cameraRadius * Math.sin(cameraPhi) * Math.sin(cameraTheta); | |
| camera.position.y = cameraRadius * Math.cos(cameraPhi); | |
| camera.position.z = cameraRadius * Math.sin(cameraPhi) * Math.cos(cameraTheta); | |
| camera.lookAt(0, 0, 0); | |
| } | |
| updateCameraPosition(); | |
| const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| document.getElementById('three-canvas').appendChild(renderer.domElement); | |
| // Lighting | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.7)); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| dirLight.position.set(10, 20, 10); | |
| scene.add(dirLight); | |
| // Axes & Grid | |
| const grid = new THREE.GridHelper(30, 30, 0x1e293b, 0x0f172a); | |
| grid.position.y = -5; | |
| scene.add(grid); | |
| // Custom Axes | |
| function createAxis(start, end, color) { | |
| const points = [start, end]; | |
| const geometry = new THREE.BufferGeometry().setFromPoints(points); | |
| const material = new THREE.LineBasicMaterial({ color: color }); | |
| return new THREE.Line(geometry, material); | |
| } | |
| // X (Red), Y (Green), Z (Blue) | |
| scene.add(createAxis(new THREE.Vector3(-15, -5, 0), new THREE.Vector3(15, -5, 0), 0x334155)); | |
| scene.add(createAxis(new THREE.Vector3(0, -15, 0), new THREE.Vector3(0, 15, 0), 0x334155)); | |
| scene.add(createAxis(new THREE.Vector3(0, -5, -15), new THREE.Vector3(0, -5, 15), 0x334155)); | |
| // Data Points Group | |
| const pointGroup = new THREE.Group(); | |
| scene.add(pointGroup); | |
| // Mystery Fruit (Interactive) | |
| const mysteryFruitMesh = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.7, 32, 32), | |
| new THREE.MeshStandardMaterial({ | |
| color: 0xffffff, | |
| roughness: 0.2, | |
| metalness: 0.1, | |
| emissive: 0xffffff, | |
| emissiveIntensity: 0.2 | |
| }) | |
| ); | |
| mysteryFruitMesh.position.set(0, 0, 0); // Start neutral | |
| scene.add(mysteryFruitMesh); | |
| // Ring indicator for mystery fruit | |
| const ringGeo = new THREE.RingGeometry(0.8, 0.9, 32); | |
| const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }); | |
| const ring = new THREE.Mesh(ringGeo, ringMat); | |
| ring.rotation.x = -Math.PI / 2; | |
| mysteryFruitMesh.add(ring); | |
| // Measurement Lines | |
| const measureLines = new THREE.LineSegments( | |
| new THREE.BufferGeometry(), | |
| new THREE.LineDashedMaterial({ color: 0x00ffff, dashSize: 0.4, gapSize: 0.2, opacity: 0.5, transparent: true }) | |
| ); | |
| // Fixed: Removed premature call to computeLineDistances() on empty geometry | |
| scene.add(measureLines); | |
| // Gaussian Clouds (Visualizing Variance) | |
| const clouds = model.map(m => { | |
| const mesh = new THREE.Mesh( | |
| new THREE.SphereGeometry(1, 32, 32), | |
| new THREE.MeshBasicMaterial({ | |
| color: m.color, | |
| transparent: true, | |
| opacity: 0.1, | |
| wireframe: true, | |
| depthWrite: false | |
| }) | |
| ); | |
| mesh.visible = false; // Hide until training starts | |
| scene.add(mesh); | |
| return mesh; | |
| }); | |
| // --- MATH & LOGIC --- | |
| function gaussian(x, mean, variance) { | |
| // Prevent division by zero or extremely small variance | |
| const v = Math.max(variance, 0.1) + (alpha * 0.2); | |
| // Add tiny epsilon to avoid pure 0 underflow in exp | |
| return (1 / Math.sqrt(2 * Math.PI * v)) * Math.exp(-Math.pow(x - mean, 2) / (2 * v)) + 1e-9; | |
| } | |
| function generateData() { | |
| dataPoints = []; | |
| pointGroup.clear(); | |
| // Offsets to make them distinct | |
| const appleCenter = { x: -6, y: -2, z: -4 }; | |
| const orangeCenter = { x: 6, y: 3, z: 4 }; | |
| model.forEach((m, i) => { | |
| const center = i === 0 ? appleCenter : orangeCenter; | |
| for (let j = 0; j < currentScenario.points / 2; j++) { | |
| const x = center.x + (Math.random() - 0.5) * currentScenario.spread * 5; | |
| const y = center.y + (Math.random() - 0.5) * currentScenario.spread * 5; | |
| const z = center.z + (Math.random() - 0.5) * currentScenario.spread * 5; | |
| dataPoints.push({ x, y, z, classId: i }); | |
| const p = new THREE.Mesh( | |
| new THREE.SphereGeometry(0.2, 8, 8), | |
| new THREE.MeshBasicMaterial({ color: m.color, transparent: true, opacity: 0.6 }) | |
| ); | |
| p.position.set(x, y, z); | |
| p.visible = false; | |
| pointGroup.add(p); | |
| } | |
| }); | |
| // Shuffle | |
| dataPoints.sort(() => Math.random() - 0.5); | |
| } | |
| function trainStep() { | |
| if (processedIdx >= dataPoints.length) { | |
| stopTraining(); | |
| addLog("Training complete. Model optimized."); | |
| return; | |
| } | |
| // Process a batch for speed | |
| const batchSize = 2; | |
| for(let k=0; k<batchSize && processedIdx < dataPoints.length; k++) { | |
| const p = dataPoints[processedIdx]; | |
| const m = model[p.classId]; | |
| // Online Mean/Variance Update (Welford's algorithm simplified) | |
| m.count++; | |
| const lr = 1.0 / (m.count + 5); // Decaying learning rate for stability | |
| // Update Mean | |
| const oldMean = { ...m.mean }; | |
| m.mean.x += lr * (p.x - m.mean.x); | |
| m.mean.y += lr * (p.y - m.mean.y); | |
| m.mean.z += lr * (p.z - m.mean.z); | |
| // Update Variance (Approximation for visualizer) | |
| const varLr = 0.1; | |
| m.var.x = (1 - varLr) * m.var.x + varLr * Math.pow(p.x - m.mean.x, 2); | |
| m.var.y = (1 - varLr) * m.var.y + varLr * Math.pow(p.y - m.mean.y, 2); | |
| m.var.z = (1 - varLr) * m.var.z + varLr * Math.pow(p.z - m.mean.z, 2); | |
| pointGroup.children[processedIdx].visible = true; | |
| processedIdx++; | |
| } | |
| if (processedIdx % 10 === 0) updateVisuals(); | |
| predict(); | |
| } | |
| function updateVisuals() { | |
| model.forEach((m, i) => { | |
| clouds[i].visible = true; | |
| clouds[i].position.set(m.mean.x, m.mean.y, m.mean.z); | |
| // Visual scale based on standard deviation (sqrt of variance) | |
| // Add base size so it doesn't disappear | |
| const sx = Math.sqrt(m.var.x) * 2.5 + alpha; | |
| const sy = Math.sqrt(m.var.y) * 2.5 + alpha; | |
| const sz = Math.sqrt(m.var.z) * 2.5 + alpha; | |
| clouds[i].scale.set(sx, sy, sz); | |
| }); | |
| } | |
| function predict() { | |
| const p = mysteryFruitMesh.position; | |
| const totalPoints = Math.max(1, processedIdx); | |
| model.forEach(m => { | |
| // Naive Bayes Formula: P(Class|Features) ∝ P(Class) * P(F1|Class) * P(F2|Class)... | |
| // 1. Prior: Frequency of class (laplace smoothing) | |
| const prior = (m.count + 1) / (totalPoints + 2); | |
| // 2. Likelihoods for each dimension (Gaussian) | |
| const lW = gaussian(p.x, m.mean.x, m.var.x); | |
| const lC = gaussian(p.y, m.mean.y, m.var.y); | |
| const lS = gaussian(p.z, m.mean.z, m.var.z); | |
| // Store for UI | |
| m.lastCalc = { prior, lW, lS, lC }; | |
| m.score = prior * lW * lS * lC; | |
| }); | |
| // Normalize probabilities | |
| const totalScore = model[0].score + model[1].score; | |
| let probA = 0, probB = 0; | |
| if (totalScore > 0) { | |
| probA = (model[0].score / totalScore) * 100; | |
| probB = (model[1].score / totalScore) * 100; | |
| } else { | |
| // Handle 0 score / underflow case | |
| probA = 50; | |
| probB = 50; | |
| } | |
| updateUI(probA, probB); | |
| updateMeasurementLines(); | |
| } | |
| function updateUI(probA, probB) { | |
| // Bars | |
| document.getElementById('bar-a').style.width = `${probA}%`; | |
| document.getElementById('bar-b').style.width = `${probB}%`; | |
| // Text | |
| document.getElementById('prob-a-val').innerText = probA.toFixed(1) + '%'; | |
| document.getElementById('prob-b-val').innerText = probB.toFixed(1) + '%'; | |
| // Details | |
| document.getElementById('prior-a').innerText = model[0].lastCalc?.prior.toFixed(2) || '0.5'; | |
| document.getElementById('prior-b').innerText = model[1].lastCalc?.prior.toFixed(2) || '0.5'; | |
| // Just showing one likelihood sum for brevity in UI | |
| const totLA = (model[0].lastCalc?.lW + model[0].lastCalc?.lC + model[0].lastCalc?.lS) || 0; | |
| const totLB = (model[1].lastCalc?.lW + model[1].lastCalc?.lC + model[1].lastCalc?.lS) || 0; | |
| document.getElementById('total-l-a').innerText = totLA.toFixed(2); | |
| document.getElementById('total-l-b').innerText = totLB.toFixed(2); | |
| // Winner Logic (with Neutral zone) | |
| const resultText = document.getElementById('result-text'); | |
| const badge = document.getElementById('uncertainty-badge'); | |
| if (Math.abs(probA - probB) < 2) { | |
| // Too close to call | |
| resultText.innerText = "NEUTRAL"; | |
| resultText.style.color = "#94a3b8"; // slate-400 | |
| badge.classList.remove('hidden'); | |
| } else if (probA > probB) { | |
| resultText.innerText = "APPLE"; | |
| resultText.style.color = "#ff0055"; | |
| badge.classList.add('hidden'); | |
| } else { | |
| resultText.innerText = "ORANGE"; | |
| resultText.style.color = "#ffcc00"; | |
| badge.classList.add('hidden'); | |
| } | |
| } | |
| function updateMeasurementLines() { | |
| const p = mysteryFruitMesh.position; | |
| // Lines projecting to axes for visual reference | |
| const points = [ | |
| p.x, p.y, p.z, p.x, -5, p.z, // To floor | |
| p.x, -5, p.z, p.x, -5, 0, // On floor to X-axis | |
| p.x, -5, p.z, 0, -5, p.z // On floor to Z-axis | |
| ]; | |
| const vertices = []; | |
| for(let i=0; i<points.length; i+=3) { | |
| vertices.push(new THREE.Vector3(points[i], points[i+1], points[i+2])); | |
| } | |
| measureLines.geometry.setFromPoints(vertices); | |
| measureLines.computeLineDistances(); | |
| document.getElementById('pos-display').innerText = | |
| `W:${p.x.toFixed(1)} S:${p.z.toFixed(1)} C:${p.y.toFixed(1)}`; | |
| } | |
| // --- CONTROLS --- | |
| function startTraining() { | |
| if (isRunning) return; | |
| isRunning = true; | |
| document.getElementById('toggle-text').innerText = 'Pause'; | |
| document.getElementById('toggle-icon').innerText = '⏸'; | |
| document.getElementById('status-text').innerText = 'Training...'; | |
| document.getElementById('status-text').classList.remove('animate-pulse'); | |
| loopId = setInterval(trainStep, 50); | |
| } | |
| function stopTraining() { | |
| isRunning = false; | |
| document.getElementById('toggle-text').innerText = 'Resume'; | |
| document.getElementById('toggle-icon').innerText = '▶'; | |
| document.getElementById('status-text').innerText = 'Paused / Idle'; | |
| clearInterval(loopId); | |
| } | |
| function reset() { | |
| stopTraining(); | |
| processedIdx = 0; | |
| // Reset math model | |
| model.forEach(m => { | |
| m.count = 0; | |
| m.mean = {x:0, y:0, z:0}; | |
| m.var = {x:1, y:1, z:1}; | |
| m.score = 0; | |
| }); | |
| clouds.forEach(c => c.visible = false); | |
| document.getElementById('toggle-text').innerText = 'Start Training'; | |
| document.getElementById('toggle-icon').innerText = '▶'; | |
| document.getElementById('status-text').innerText = 'Ready'; | |
| document.getElementById('status-text').classList.add('animate-pulse'); | |
| // Reset position | |
| mysteryFruitMesh.position.set(0, 0, 0); | |
| generateData(); | |
| addLog("Memory wiped. System reset."); | |
| predict(); | |
| } | |
| function selectScenario(idx) { | |
| currentScenario = SCENARIOS[idx]; | |
| // Update UI buttons | |
| document.getElementById('scen-0').className = idx === 0 | |
| ? "text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all shadow-[0_0_10px_rgba(34,211,238,0.2)]" | |
| : "text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all"; | |
| document.getElementById('scen-1').className = idx === 1 | |
| ? "text-[10px] py-2.5 px-2 rounded border border-cyan-500/50 bg-cyan-500/10 text-cyan-400 font-bold uppercase transition-all shadow-[0_0_10px_rgba(34,211,238,0.2)]" | |
| : "text-[10px] py-2.5 px-2 rounded border border-slate-700 bg-transparent text-slate-400 hover:bg-slate-800 font-bold uppercase transition-all"; | |
| reset(); | |
| } | |
| function addLog(msg) { | |
| const log = document.getElementById('learning-log'); | |
| const div = document.createElement('div'); | |
| const time = new Date().toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit'}); | |
| div.innerHTML = `<span class="text-slate-600">[${time}]</span> ${msg}`; | |
| log.prepend(div); | |
| } | |
| // --- EVENTS --- | |
| document.getElementById('btn-toggle').onclick = () => isRunning ? stopTraining() : startTraining(); | |
| document.getElementById('btn-reset').onclick = reset; | |
| document.getElementById('alpha-slider').oninput = (e) => { | |
| alpha = parseFloat(e.target.value); | |
| document.getElementById('alpha-value').innerText = alpha.toFixed(2); | |
| updateVisuals(); | |
| predict(); | |
| }; | |
| // 3D Interaction Logic | |
| let isMouseDown = false; | |
| let mouseButton = 0; | |
| let prevMouse = { x: 0, y: 0 }; | |
| container.addEventListener('mousedown', (e) => { | |
| isMouseDown = true; | |
| mouseButton = e.button; | |
| prevMouse = { x: e.clientX, y: e.clientY }; | |
| }); | |
| window.addEventListener('mouseup', () => isMouseDown = false); | |
| container.addEventListener('contextmenu', (e) => e.preventDefault()); | |
| container.addEventListener('mousemove', (e) => { | |
| if (!isMouseDown) return; | |
| const dx = e.clientX - prevMouse.x; | |
| const dy = e.clientY - prevMouse.y; | |
| // Left Click (0): Rotate Camera | |
| if (mouseButton === 0 && !e.shiftKey) { | |
| cameraTheta -= dx * 0.01; | |
| cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi - dy * 0.01)); | |
| updateCameraPosition(); | |
| } | |
| // Shift + Drag: Lift Object | |
| else if (e.shiftKey) { | |
| mysteryFruitMesh.position.y -= dy * 0.1; | |
| mysteryFruitMesh.position.y = Math.max(-10, Math.min(10, mysteryFruitMesh.position.y)); | |
| predict(); | |
| } | |
| // Right Click (2): Move Object Plane | |
| else if (mouseButton === 2 || (mouseButton === 0 && e.ctrlKey)) { | |
| // Move relative to camera view roughly | |
| const speed = 0.1; | |
| const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion); | |
| forward.y = 0; forward.normalize(); | |
| const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion); | |
| right.y = 0; right.normalize(); | |
| mysteryFruitMesh.position.addScaledVector(right, dx * speed); | |
| mysteryFruitMesh.position.addScaledVector(forward, dy * speed); | |
| // Bounds | |
| ['x', 'y', 'z'].forEach(axis => { | |
| mysteryFruitMesh.position[axis] = Math.max(-14, Math.min(14, mysteryFruitMesh.position[axis])); | |
| }); | |
| predict(); | |
| } | |
| prevMouse = { x: e.clientX, y: e.clientY }; | |
| }); | |
| // Zoom | |
| container.addEventListener('wheel', (e) => { | |
| e.preventDefault(); | |
| cameraRadius = Math.max(10, Math.min(60, cameraRadius + e.deltaY * 0.05)); | |
| updateCameraPosition(); | |
| }, { passive: false }); | |
| window.addEventListener('resize', () => { | |
| camera.aspect = container.clientWidth / container.clientHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(container.clientWidth, container.clientHeight); | |
| }); | |
| // Animation Loop | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| // Spin the ring | |
| ring.rotation.z -= 0.02; | |
| renderer.render(scene, camera); | |
| } | |
| // Init | |
| generateData(); | |
| predict(); | |
| animate(); | |
| updateUI(50, 50); // Initial neutral state | |
| </script> | |
| </body> | |
| </html> |