| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>GMM Simulator - Learn Gaussian Mixture Models Visually</title>
|
| <script src="https://cdn.tailwindcss.com"></script>
|
| <script src="https://unpkg.com/lucide@latest"></script>
|
| <style>
|
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap');
|
|
|
| :root {
|
| --primary: 221.2 83.2% 53.3%;
|
| --accent: 142.1 76.2% 36.3%;
|
| --cluster-1: 15 85% 60%;
|
| --cluster-2: 195 80% 50%;
|
| --cluster-3: 45 90% 55%;
|
| --cluster-4: 280 65% 60%;
|
| }
|
|
|
| body {
|
| font-family: 'Inter', sans-serif;
|
| background-color: #f8fafc;
|
| }
|
|
|
| .font-mono { font-family: 'JetBrains Mono', monospace; }
|
|
|
| .shadow-soft {
|
| box-shadow: 0 2px 15px -3px rgba(0,0,0,0.07), 0 4px 6px -2px rgba(0,0,0,0.05);
|
| }
|
|
|
| .animate-pulse-soft {
|
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| }
|
|
|
| @keyframes pulse {
|
| 0%, 100% { opacity: 1; }
|
| 50% { opacity: .7; }
|
| }
|
|
|
| .tab-content { display: none; }
|
| .tab-content.active { display: block; }
|
|
|
| .tab-trigger.active {
|
| background: white;
|
| box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| color: black;
|
| }
|
|
|
| .step-node.active { background-color: rgb(59 130 246); color: white; }
|
| .step-node.done { background-color: #f1f5f9; color: #64748b; }
|
| .step-node.converged { background-color: #10b981; color: white; }
|
|
|
| .modal-overlay {
|
| background: rgba(15, 23, 42, 0.6);
|
| backdrop-filter: blur(4px);
|
| }
|
| </style>
|
| </head>
|
| <body class="text-slate-900">
|
|
|
| <div id="app" class="min-h-screen p-4 md:p-6">
|
| <div class="max-w-7xl mx-auto space-y-6">
|
|
|
|
|
| <header class="text-center space-y-2">
|
| <h1 class="text-2xl md:text-4xl font-extrabold tracking-tight">
|
| EM Algorithm & <span class="text-blue-600">Gaussian Mixture Models</span>
|
| </h1>
|
| <p class="text-slate-500 text-sm md:text-base max-w-2xl mx-auto">
|
| Learn how machines find hidden groups in data. Watch points get assigned and clusters adapt in real-time!
|
| </p>
|
| </header>
|
|
|
|
|
| <div class="flex justify-center">
|
| <div class="bg-slate-200/50 p-1 rounded-lg inline-flex w-full max-w-md">
|
| <button onclick="switchTab('simulator')" id="btn-tab-simulator" class="tab-trigger flex-1 py-1.5 px-3 rounded-md text-sm font-medium transition-all active flex items-center justify-center gap-2">
|
| <i data-lucide="play" class="w-4 h-4"></i> Interactive Simulator
|
| </button>
|
| <button onclick="switchTab('learn')" id="btn-tab-learn" class="tab-trigger flex-1 py-1.5 px-3 rounded-md text-sm font-medium transition-all flex items-center justify-center gap-2 text-slate-600">
|
| <i data-lucide="book-open" class="w-4 h-4"></i> Learn EM Algorithm
|
| </button>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="tab-simulator" class="tab-content active space-y-6">
|
|
|
|
|
| <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
| <div class="bg-white rounded-xl border p-4 shadow-soft">
|
| <i data-lucide="book-open" class="w-6 h-6 mb-2 text-orange-500"></i>
|
| <h4 class="font-bold text-sm mb-1">GMM = Soft Clustering</h4>
|
| <p class="text-xs text-slate-500">Points can partially belong to multiple clusters.</p>
|
| </div>
|
| <div class="bg-white rounded-xl border p-4 shadow-soft">
|
| <i data-lucide="zap" class="w-6 h-6 mb-2 text-blue-500"></i>
|
| <h4 class="font-bold text-sm mb-1">EM is Iterative</h4>
|
| <p class="text-xs text-slate-500">Alternates between E-step and M-step.</p>
|
| </div>
|
| <div class="bg-white rounded-xl border p-4 shadow-soft">
|
| <i data-lucide="eye" class="w-6 h-6 mb-2 text-emerald-500"></i>
|
| <h4 class="font-bold text-sm mb-1">Watch the Ellipses</h4>
|
| <p class="text-xs text-slate-500">Ellipses show the statistical shape of clusters.</p>
|
| </div>
|
| <div class="bg-white rounded-xl border p-4 shadow-soft">
|
| <i data-lucide="brain" class="w-6 h-6 mb-2 text-indigo-500"></i>
|
| <h4 class="font-bold text-sm mb-1">Convergence</h4>
|
| <p class="text-xs text-slate-500">Algorithm stops when centers stabilize.</p>
|
| </div>
|
| </div>
|
|
|
| <div class="grid lg:grid-cols-3 gap-6">
|
|
|
| <div class="lg:col-span-2 space-y-4">
|
|
|
|
|
| <div class="bg-white rounded-xl border p-4 shadow-soft">
|
| <h4 class="font-bold text-sm mb-4 text-center">EM Algorithm Flow</h4>
|
| <div class="flex items-center justify-between">
|
| <div class="flex flex-col items-center flex-1 text-center">
|
| <div id="step-init" class="step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all">
|
| <i data-lucide="circle" class="w-5 h-5"></i>
|
| </div>
|
| <span class="text-xs font-medium mt-2">Initialize</span>
|
| </div>
|
| <div class="h-0.5 flex-1 mx-2 bg-slate-200" id="line-1"></div>
|
| <div class="flex flex-col items-center flex-1 text-center">
|
| <div id="step-e" class="step-node w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center font-bold text-sm transition-all">
|
| <i data-lucide="circle" class="w-5 h-5"></i>
|
| </div>
|
| <span class="text-xs font-medium mt-2">E-Step</span>
|
| </div>
|
| <div class="h-0.5 flex-1 mx-2 bg-slate-200" id="line-2"></div>
|
| <div class="flex flex-col items-center flex-1 text-center">
|
| <div id="step-m" class="step-node w-10 h-10 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center font-bold text-sm transition-all">
|
| <i data-lucide="refresh-cw" class="w-5 h-5"></i>
|
| </div>
|
| <span class="text-xs font-medium mt-2">M-Step</span>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="bg-white rounded-2xl shadow-soft border overflow-hidden">
|
| <div class="p-3 border-b bg-slate-50 flex flex-wrap gap-4" id="legend">
|
|
|
| </div>
|
| <div class="relative p-2 flex justify-center items-center overflow-hidden" style="height: 400px;" id="canvas-container">
|
| <svg id="gmm-svg" width="100%" height="100%" class="rounded-lg">
|
| <defs>
|
| <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
| <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#e2e8f0" stroke-width="0.5" />
|
| </pattern>
|
| </defs>
|
| <rect width="100%" height="100%" fill="url(#grid)" />
|
| <g id="ellipses-group"></g>
|
| <g id="points-group"></g>
|
| </svg>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="bg-white rounded-xl p-5 border shadow-soft space-y-6">
|
| <div class="flex flex-wrap items-center gap-3">
|
| <button id="btn-play" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| <i data-lucide="play" class="w-5 h-5"></i> <span id="btn-play-text">Auto Play</span>
|
| </button>
|
| <button id="btn-step" class="bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| <i data-lucide="skip-forward" class="w-5 h-5"></i> Next Step
|
| </button>
|
| <button id="btn-reset" class="border border-slate-200 hover:bg-slate-50 text-slate-800 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| <i data-lucide="rotate-ccw" class="w-5 h-5"></i> Reset
|
| </button>
|
| <button id="btn-generate" class="border border-emerald-200 text-emerald-600 hover:bg-emerald-50 font-bold py-2.5 px-6 rounded-lg inline-flex items-center gap-2 transition-colors">
|
| <i data-lucide="sparkles" class="w-5 h-5"></i> New Data
|
| </button>
|
| </div>
|
|
|
| <div class="flex flex-wrap items-center justify-between gap-6 pt-2 border-t border-slate-100">
|
| <div class="flex items-center gap-4">
|
| <span class="text-sm font-medium text-slate-500 whitespace-nowrap">Speed:</span>
|
| <input type="range" id="speed-slider" min="500" max="3000" step="100" value="1500" class="w-32 accent-blue-600">
|
| <span id="speed-text" class="text-xs text-slate-400 w-12 text-center">Medium</span>
|
| </div>
|
|
|
| <div class="flex items-center gap-4">
|
| <div class="bg-slate-100 px-4 py-2 rounded-lg flex items-center gap-2">
|
| <span class="text-sm font-medium text-slate-500">Iteration:</span>
|
| <span id="counter-text" class="font-mono text-lg font-bold">0 / 20</span>
|
| </div>
|
| <div id="status-badge" class="hidden px-4 py-2 rounded-lg font-semibold text-sm"></div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="space-y-4">
|
|
|
| <div id="explanation-card" class="p-5 rounded-xl border-2 bg-blue-50/50 border-blue-200 shadow-soft transition-all">
|
| <div class="flex items-start gap-4">
|
| <div class="p-3 rounded-lg bg-white text-blue-600" id="expl-icon">
|
| <i data-lucide="lightbulb" class="w-6 h-6"></i>
|
| </div>
|
| <div class="flex-1">
|
| <h3 class="font-bold text-lg mb-2" id="expl-title">Ready to Start! 🚀</h3>
|
| <ul class="space-y-2" id="expl-content">
|
| <li class="flex items-start gap-2 text-sm text-slate-600">
|
| <i data-lucide="arrow-right" class="w-4 h-4 mt-0.5 text-emerald-500 flex-shrink-0"></i>
|
| <span>Press 'Next Step' to see how the EM algorithm works.</span>
|
| </li>
|
| <li class="flex items-start gap-2 text-sm text-slate-600">
|
| <i data-lucide="arrow-right" class="w-4 h-4 mt-0.5 text-emerald-500 flex-shrink-0"></i>
|
| <span>Watch points get assigned to the most likely cluster center.</span>
|
| </li>
|
| </ul>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="bg-white rounded-xl border p-4 shadow-soft">
|
| <div class="flex items-center justify-between mb-3">
|
| <div class="flex items-center gap-2">
|
| <i data-lucide="trending-up" class="w-5 h-5 text-blue-600"></i>
|
| <h4 class="font-bold">Log-Likelihood</h4>
|
| </div>
|
| <div id="converged-badge" class="hidden items-center gap-1 text-emerald-600 text-sm font-medium">
|
| <i data-lucide="check-circle-2" class="w-4 h-4"></i> Converged!
|
| </div>
|
| </div>
|
| <p class="text-xs text-slate-500 mb-4">
|
| Measures model fit. Higher values = better grouping.
|
| </p>
|
| <div class="h-32 w-full flex items-end gap-1 px-1" id="chart-container">
|
|
|
| <div class="flex-1 h-full flex items-center justify-center text-slate-300 text-xs text-center px-4">
|
| Start simulation to see progress...
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="bg-white rounded-xl p-5 border shadow-soft">
|
| <h4 class="font-bold mb-3">Key Terms 📚</h4>
|
| <div class="space-y-4 text-sm">
|
| <div>
|
| <span class="font-semibold text-orange-500">E-Step:</span>
|
| <p class="text-slate-500 mt-0.5">Calculating the "responsibility" (probability) that each point belongs to each cluster center.</p>
|
| </div>
|
| <div>
|
| <span class="font-semibold text-blue-500">M-Step:</span>
|
| <p class="text-slate-500 mt-0.5">Moving and stretching clusters to better fit the points assigned to them.</p>
|
| </div>
|
| <div>
|
| <span class="font-semibold text-emerald-500">Convergence:</span>
|
| <p class="text-slate-500 mt-0.5">The point where the clusters stop moving because they've found the optimal mathematical fit.</p>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="tab-learn" class="tab-content space-y-8">
|
|
|
| <section class="bg-white rounded-2xl border p-6 shadow-soft">
|
| <div class="flex items-center gap-3 mb-4">
|
| <div class="p-3 rounded-xl bg-blue-50 text-blue-600">
|
| <i data-lucide="book-open" class="w-6 h-6"></i>
|
| </div>
|
| <h2 class="text-xl font-bold">What is the EM Algorithm?</h2>
|
| </div>
|
| <div class="space-y-4 text-slate-600">
|
| <p>
|
| <strong class="text-slate-900">EM (Expectation-Maximization)</strong> is a powerful iterative method used to find maximum likelihood estimates of parameters in statistical models, where the model depends on unobserved latent variables.
|
| </p>
|
| <div class="bg-slate-50 rounded-xl p-4 border border-slate-100">
|
| <h4 class="font-semibold text-slate-900 mb-2">🎯 Real-Life Analogy</h4>
|
| <p class="text-sm">
|
| Imagine you have a bag of colored marbles, but you're colorblind! You can feel their sizes and weights, but you can't see the colors. EM helps you figure out which marbles are likely the same color based on their shared physical properties.
|
| </p>
|
| </div>
|
| <p>The algorithm works by alternating between two main steps until it finds the best mathematical grouping.</p>
|
| </div>
|
| </section>
|
|
|
|
|
| <div class="grid md:grid-cols-2 gap-6">
|
|
|
| <section class="bg-gradient-to-br from-orange-50/50 to-white rounded-2xl border border-orange-100 p-6 shadow-sm">
|
| <div class="flex items-center gap-3 mb-4">
|
| <div class="p-3 rounded-xl bg-orange-100 text-orange-600">
|
| <i data-lucide="target" class="w-6 h-6"></i>
|
| </div>
|
| <div>
|
| <h3 class="text-lg font-bold">E-Step (Expectation)</h3>
|
| <p class="text-xs text-orange-500 font-medium">Assignment Phase</p>
|
| </div>
|
| </div>
|
| <div class="space-y-4">
|
| <p class="text-sm text-slate-600"><strong>Goal:</strong> Calculate the probability (responsibility) of each cluster for each data point.</p>
|
| <div class="space-y-2">
|
| <h4 class="font-semibold text-sm">Process:</h4>
|
| <ul class="space-y-2 text-sm text-slate-500">
|
| <li class="flex items-start gap-2"><span class="text-orange-600">•</span> Each point "looks" at all current cluster curves.</li>
|
| <li class="flex items-start gap-2"><span class="text-orange-600">•</span> It asks: "Given my location, which cluster would likely have produced me?"</li>
|
| <li class="flex items-start gap-2"><span class="text-orange-600">•</span> Points get colored by their most likely cluster.</li>
|
| </ul>
|
| </div>
|
| <div class="bg-white/80 rounded-lg p-3 border border-orange-100">
|
| <p class="text-xs text-slate-500 font-mono">responsibility = (fit to cluster) / (total fit to all clusters)</p>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section class="bg-gradient-to-br from-blue-50/50 to-white rounded-2xl border border-blue-100 p-6 shadow-sm">
|
| <div class="flex items-center gap-3 mb-4">
|
| <div class="p-3 rounded-xl bg-blue-100 text-blue-600">
|
| <i data-lucide="bar-chart-3" class="w-6 h-6"></i>
|
| </div>
|
| <div>
|
| <h3 class="text-lg font-bold">M-Step (Maximization)</h3>
|
| <p class="text-xs text-blue-500 font-medium">Update Phase</p>
|
| </div>
|
| </div>
|
| <div class="space-y-4">
|
| <p class="text-sm text-slate-600"><strong>Goal:</strong> Update cluster parameters (center, shape, weight) based on assigned points.</p>
|
| <div class="space-y-2">
|
| <h4 class="font-semibold text-sm">Process:</h4>
|
| <ul class="space-y-2 text-sm text-slate-500">
|
| <li class="flex items-start gap-2"><span class="text-blue-600">•</span> Cluster centers move to the weighted average of points.</li>
|
| <li class="flex items-start gap-2"><span class="text-blue-600">•</span> The ellipse stretches/shrinks to cover its assigned points better.</li>
|
| <li class="flex items-start gap-2"><span class="text-blue-600">•</span> Cluster "popularity" (weight) is updated.</li>
|
| </ul>
|
| </div>
|
| <div class="bg-white/80 rounded-lg p-3 border border-blue-100">
|
| <p class="text-xs text-slate-500 font-mono">new_mean = average(points × their_responsibilities)</p>
|
| </div>
|
| </div>
|
| </section>
|
| </div>
|
|
|
|
|
| <section class="bg-white rounded-2xl border p-6 shadow-soft">
|
| <div class="flex items-center gap-3 mb-6">
|
| <div class="p-3 rounded-xl bg-indigo-50 text-indigo-600">
|
| <i data-lucide="refresh-cw" class="w-6 h-6"></i>
|
| </div>
|
| <h2 class="text-xl font-bold">The Complete Cycle</h2>
|
| </div>
|
| <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 relative">
|
| <div class="bg-slate-50 p-4 rounded-xl text-center border">
|
| <i data-lucide="shuffle" class="w-8 h-8 mx-auto mb-2 text-slate-400"></i>
|
| <h4 class="font-bold text-sm">1. Initialize</h4>
|
| <p class="text-xs text-slate-500 mt-1">Random centers and circular shapes</p>
|
| </div>
|
| <div class="bg-orange-50 p-4 rounded-xl text-center border border-orange-100">
|
| <i data-lucide="target" class="w-8 h-8 mx-auto mb-2 text-orange-500"></i>
|
| <h4 class="font-bold text-sm">2. E-Step</h4>
|
| <p class="text-xs text-slate-500 mt-1">Assign data points to clusters</p>
|
| </div>
|
| <div class="bg-blue-50 p-4 rounded-xl text-center border border-blue-100">
|
| <i data-lucide="bar-chart-3" class="w-8 h-8 mx-auto mb-2 text-blue-500"></i>
|
| <h4 class="font-bold text-sm">3. M-Step</h4>
|
| <p class="text-xs text-slate-500 mt-1">Update cluster parameters</p>
|
| </div>
|
| <div class="bg-emerald-50 p-4 rounded-xl text-center border border-emerald-100">
|
| <i data-lucide="check-circle-2" class="w-8 h-8 mx-auto mb-2 text-emerald-500"></i>
|
| <h4 class="font-bold text-sm">4. Converged?</h4>
|
| <p class="text-xs text-slate-500 mt-1">Repeat 2 & 3 until stable</p>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section class="bg-white rounded-2xl border p-6 shadow-soft">
|
| <div class="flex items-center gap-3 mb-6">
|
| <div class="p-3 rounded-xl bg-amber-50 text-amber-600">
|
| <i data-lucide="lightbulb" class="w-6 h-6"></i>
|
| </div>
|
| <h2 class="text-xl font-bold">Why Does EM Work?</h2>
|
| </div>
|
| <div class="grid md:grid-cols-2 gap-8">
|
| <div class="space-y-4">
|
| <h4 class="font-bold text-slate-900">The Chicken-and-Egg Problem 🐔🥚</h4>
|
| <p class="text-sm text-slate-600 leading-relaxed">
|
| We have a classic loop: To find the centers, we need to know point assignments. To find point assignments, we need to know the centers.
|
| EM solves this by starting with a "best guess" and iteratively refining it.
|
| </p>
|
| </div>
|
| <div class="space-y-4">
|
| <h4 class="font-bold text-slate-900">Guaranteed Improvement 📈</h4>
|
| <p class="text-sm text-slate-600 leading-relaxed">
|
| Mathematically, each iteration of EM is guaranteed to increase the <strong>Log-Likelihood</strong> of the model (or leave it unchanged). This means the model always gets better at explaining the data until it hits a maximum.
|
| </p>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section class="bg-white rounded-2xl border p-6 shadow-soft overflow-hidden">
|
| <h2 class="text-xl font-bold mb-6">GMM vs K-Means: The Difference</h2>
|
| <div class="overflow-x-auto">
|
| <table class="w-full text-sm">
|
| <thead class="bg-slate-50">
|
| <tr class="text-left">
|
| <th class="py-3 px-4 font-bold border-b">Feature</th>
|
| <th class="py-3 px-4 font-bold border-b text-blue-600">K-Means</th>
|
| <th class="py-3 px-4 font-bold border-b text-orange-600">GMM (EM)</th>
|
| </tr>
|
| </thead>
|
| <tbody class="divide-y text-slate-600">
|
| <tr>
|
| <td class="py-4 px-4 font-semibold text-slate-900">Assignment</td>
|
| <td class="py-4 px-4">Hard (0 or 1)</td>
|
| <td class="py-4 px-4">Soft (Probabilities)</td>
|
| </tr>
|
| <tr>
|
| <td class="py-4 px-4 font-semibold text-slate-900">Cluster Shape</td>
|
| <td class="py-4 px-4">Always circular/spherical</td>
|
| <td class="py-4 px-4">Flexible ellipses (any orientation)</td>
|
| </tr>
|
| <tr>
|
| <td class="py-4 px-4 font-semibold text-slate-900">Model Type</td>
|
| <td class="py-4 px-4">Distance-based</td>
|
| <td class="py-4 px-4">Distribution-based</td>
|
| </tr>
|
| <tr>
|
| <td class="py-4 px-4 font-semibold text-slate-900">Use Case</td>
|
| <td class="py-4 px-4">Simple, distinct groups</td>
|
| <td class="py-4 px-4">Overlapping, varied group shapes</td>
|
| </tr>
|
| </tbody>
|
| </table>
|
| </div>
|
| </section>
|
|
|
|
|
| <section class="bg-slate-900 text-white rounded-2xl p-8 shadow-xl">
|
| <h2 class="text-2xl font-bold mb-6">🌍 Real-World Applications</h2>
|
| <div class="grid sm:grid-cols-2 md:grid-cols-3 gap-6">
|
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| <div class="text-2xl mb-2">🖼️</div>
|
| <h4 class="font-bold text-sm">Image Segmentation</h4>
|
| <p class="text-xs text-slate-400 mt-1">Grouping pixels by color/texture to separate objects in photos.</p>
|
| </div>
|
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| <div class="text-2xl mb-2">🔊</div>
|
| <h4 class="font-bold text-sm">Speech Recognition</h4>
|
| <p class="text-xs text-slate-400 mt-1">Identifying different speakers in an audio stream using voice patterns.</p>
|
| </div>
|
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| <div class="text-2xl mb-2">📊</div>
|
| <h4 class="font-bold text-sm">Customer Segmentation</h4>
|
| <p class="text-xs text-slate-400 mt-1">Finding groups of customers with similar shopping behaviors.</p>
|
| </div>
|
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| <div class="text-2xl mb-2">🧬</div>
|
| <h4 class="font-bold text-sm">Genetics</h4>
|
| <p class="text-xs text-slate-400 mt-1">Clustering gene expression data to find functional biological groups.</p>
|
| </div>
|
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| <div class="text-2xl mb-2">🌤️</div>
|
| <h4 class="font-bold text-sm">Meteorology</h4>
|
| <p class="text-xs text-slate-400 mt-1">Classifying climate zones based on temperature and humidity data.</p>
|
| </div>
|
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
|
| <div class="text-2xl mb-2">📧</div>
|
| <h4 class="font-bold text-sm">Spam Detection</h4>
|
| <p class="text-xs text-slate-400 mt-1">Clustering emails into 'Ham' and 'Spam' based on content features.</p>
|
| </div>
|
| </div>
|
| </section>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="intro-modal" class="fixed inset-0 z-50 flex items-center justify-center p-4 modal-overlay">
|
| <div class="bg-white rounded-2xl shadow-2xl max-w-2xl w-full p-6 md:p-8 relative">
|
| <button onclick="closeModal()" class="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-100 transition-colors">
|
| <i data-lucide="x" class="w-5 h-5"></i>
|
| </button>
|
| <div class="text-center mb-8">
|
| <h2 class="text-2xl md:text-3xl font-extrabold mb-2">Welcome to the <span class="text-blue-600">EM Simulator</span></h2>
|
| <p class="text-slate-500">Discover how AI learns hidden patterns in data</p>
|
| </div>
|
| <div class="space-y-4 mb-8">
|
| <div class="flex items-start gap-4 p-4 rounded-xl bg-slate-50">
|
| <div class="p-3 rounded-lg bg-white text-orange-500 shadow-sm"><i data-lucide="sparkles" class="w-6 h-6"></i></div>
|
| <div><h3 class="font-bold">What is GMM?</h3><p class="text-sm text-slate-500">A statistical model that groups data points into distinct probability curves (clusters).</p></div>
|
| </div>
|
| <div class="flex items-start gap-4 p-4 rounded-xl bg-slate-50">
|
| <div class="p-3 rounded-lg bg-white text-blue-500 shadow-sm"><i data-lucide="zap" class="w-6 h-6"></i></div>
|
| <div><h3 class="font-bold">How it learns</h3><p class="text-sm text-slate-500">It "cycles" between assigning points (E-Step) and updating groups (M-Step) until stable.</p></div>
|
| </div>
|
| </div>
|
| <button onclick="closeModal()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl transition-all shadow-lg flex items-center justify-center gap-2">
|
| Start Learning <i data-lucide="arrow-right" class="w-5 h-5"></i>
|
| </button>
|
| </div>
|
|
|
| <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="/gaussian-mixture-models" 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>
|
|
|
| <script>
|
|
|
| const CLUSTER_COLORS = [
|
| { fill: 'rgba(239, 115, 87, 0.15)', stroke: 'hsl(15, 85%, 60%)', label: 'Coral' },
|
| { fill: 'rgba(50, 180, 205, 0.15)', stroke: 'hsl(195, 80%, 50%)', label: 'Teal' },
|
| { fill: 'rgba(245, 185, 66, 0.15)', stroke: 'hsl(45, 90%, 55%)', label: 'Amber' },
|
| { fill: 'rgba(175, 100, 200, 0.15)', stroke: 'hsl(280, 65%, 60%)', label: 'Violet' }
|
| ];
|
| const MAX_ITERATIONS = 20;
|
| const NUM_POINTS = 100;
|
|
|
|
|
| let state = {
|
| points: [],
|
| clusters: [],
|
| iteration: 0,
|
| stepType: 'none',
|
| isPlaying: false,
|
| converged: false,
|
| logLikelihoods: [],
|
| speed: 1500,
|
| timer: null
|
| };
|
|
|
|
|
| function multivariateNormalPDF(x, y, mean, cov) {
|
| const dx = x - mean.x;
|
| const dy = y - mean.y;
|
| const det = cov.xx * cov.yy - cov.xy * cov.xy;
|
| const inv = {
|
| xx: cov.yy / det,
|
| xy: -cov.xy / det,
|
| yy: cov.xx / det
|
| };
|
| const exponent = -0.5 * (dx * (dx * inv.xx + dy * inv.xy) + dy * (dx * inv.xy + dy * inv.yy));
|
| return (1 / (2 * Math.PI * Math.sqrt(Math.abs(det)))) * Math.exp(exponent);
|
| }
|
|
|
| function getEllipseParams(cov) {
|
| const trace = cov.xx + cov.yy;
|
| const det = cov.xx * cov.yy - cov.xy * cov.xy;
|
| const sqrtDisc = Math.sqrt(Math.pow(trace / 2, 2) - det);
|
| const lambda1 = trace / 2 + sqrtDisc;
|
| const lambda2 = trace / 2 - sqrtDisc;
|
|
|
| const a = Math.sqrt(Math.max(0, lambda1)) * 2;
|
| const b = Math.sqrt(Math.max(0, lambda2)) * 2;
|
| const angle = 0.5 * Math.atan2(2 * cov.xy, cov.xx - cov.yy) * (180 / Math.PI);
|
|
|
| return { a, b, angle };
|
| }
|
|
|
|
|
| function generateData() {
|
| const points = [];
|
| const centers = [
|
| { x: 150, y: 150 },
|
| { x: 450, y: 250 },
|
| { x: 250, y: 350 }
|
| ];
|
|
|
| for (let i = 0; i < NUM_POINTS; i++) {
|
| const center = centers[Math.floor(Math.random() * centers.length)];
|
| points.push({
|
| x: center.x + (Math.random() - 0.5) * 150,
|
| y: center.y + (Math.random() - 0.5) * 150,
|
| responsibilities: []
|
| });
|
| }
|
| state.points = points;
|
| initClusters();
|
| resetSimState();
|
| render();
|
| }
|
|
|
| function initClusters() {
|
| state.clusters = [];
|
| for (let i = 0; i < 3; i++) {
|
| state.clusters.push({
|
| mean: { x: Math.random() * 600, y: Math.random() * 400 },
|
| covariance: { xx: 2500, xy: 0, yy: 2500 },
|
| weight: 1 / 3,
|
| color: CLUSTER_COLORS[i]
|
| });
|
| }
|
| }
|
|
|
| function resetSimState() {
|
| state.iteration = 0;
|
| state.stepType = 'none';
|
| state.converged = false;
|
| state.logLikelihoods = [];
|
| state.points.forEach(p => delete p.clusterIndex);
|
| stopAutoPlay();
|
| updateUI();
|
| }
|
|
|
| function eStep() {
|
| let totalLogLikelihood = 0;
|
|
|
| state.points.forEach(p => {
|
| let responsibilities = state.clusters.map(c => {
|
| const prob = multivariateNormalPDF(p.x, p.y, c.mean, c.covariance);
|
| return c.weight * prob;
|
| });
|
|
|
| const sum = responsibilities.reduce((a, b) => a + b, 0);
|
| totalLogLikelihood += Math.log(sum || 1e-10);
|
|
|
| if (sum > 0) {
|
| responsibilities = responsibilities.map(r => r / sum);
|
| } else {
|
| responsibilities = state.clusters.map(() => 1 / state.clusters.length);
|
| }
|
|
|
| p.responsibilities = responsibilities;
|
|
|
|
|
| let maxIdx = 0;
|
| responsibilities.forEach((r, idx) => { if (r > responsibilities[maxIdx]) maxIdx = idx; });
|
| p.clusterIndex = maxIdx;
|
| });
|
|
|
| state.logLikelihoods.push(totalLogLikelihood);
|
| state.stepType = 'e-step';
|
|
|
|
|
| if (state.logLikelihoods.length > 2) {
|
| const diff = Math.abs(state.logLikelihoods[state.logLikelihoods.length - 1] - state.logLikelihoods[state.logLikelihoods.length - 2]);
|
| if (diff < 0.1) state.converged = true;
|
| }
|
| }
|
|
|
| function mStep() {
|
| const N = state.points.length;
|
|
|
| state.clusters.forEach((c, j) => {
|
| const sumResp = state.points.reduce((acc, p) => acc + p.responsibilities[j], 0);
|
|
|
|
|
| c.weight = sumResp / N;
|
|
|
|
|
| if (sumResp > 0) {
|
| const newMean = state.points.reduce((acc, p) => {
|
| acc.x += p.responsibilities[j] * p.x;
|
| acc.y += p.responsibilities[j] * p.y;
|
| return acc;
|
| }, { x: 0, y: 0 });
|
| c.mean.x = newMean.x / sumResp;
|
| c.mean.y = newMean.y / sumResp;
|
|
|
|
|
| const newCov = state.points.reduce((acc, p) => {
|
| const dx = p.x - c.mean.x;
|
| const dy = p.y - c.mean.y;
|
| acc.xx += p.responsibilities[j] * dx * dx;
|
| acc.xy += p.responsibilities[j] * dx * dy;
|
| acc.yy += p.responsibilities[j] * dy * dy;
|
| return acc;
|
| }, { xx: 0, xy: 0, yy: 0 });
|
|
|
|
|
| c.covariance.xx = newCov.xx / sumResp + 10;
|
| c.covariance.xy = newCov.xy / sumResp;
|
| c.covariance.yy = newCov.yy / sumResp + 10;
|
| }
|
| });
|
|
|
| state.iteration++;
|
| state.stepType = 'm-step';
|
| if (state.iteration >= MAX_ITERATIONS) state.converged = true;
|
| }
|
|
|
|
|
| function render() {
|
| const pointsGroup = document.getElementById('points-group');
|
| const ellipsesGroup = document.getElementById('ellipses-group');
|
|
|
|
|
| pointsGroup.innerHTML = '';
|
| ellipsesGroup.innerHTML = '';
|
|
|
|
|
| state.clusters.forEach((c, i) => {
|
| const params = getEllipseParams(c.covariance);
|
| const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
|
| const ellipse = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
|
| ellipse.setAttribute('cx', c.mean.x);
|
| ellipse.setAttribute('cy', c.mean.y);
|
| ellipse.setAttribute('rx', params.a);
|
| ellipse.setAttribute('ry', params.b);
|
| ellipse.setAttribute('fill', c.color.fill);
|
| ellipse.setAttribute('stroke', c.color.stroke);
|
| ellipse.setAttribute('stroke-width', '2');
|
| ellipse.setAttribute('transform', `rotate(${params.angle}, ${c.mean.x}, ${c.mean.y})`);
|
| if (state.isPlaying) ellipse.classList.add('animate-pulse-soft');
|
|
|
| const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
| center.setAttribute('cx', c.mean.x);
|
| center.setAttribute('cy', c.mean.y);
|
| center.setAttribute('r', '6');
|
| center.setAttribute('fill', c.color.stroke);
|
| center.setAttribute('stroke', 'white');
|
| center.setAttribute('stroke-width', '2');
|
|
|
| g.appendChild(ellipse);
|
| g.appendChild(center);
|
| ellipsesGroup.appendChild(g);
|
| });
|
|
|
|
|
| state.points.forEach((p, i) => {
|
| const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
| circle.setAttribute('cx', p.x);
|
| circle.setAttribute('cy', p.y);
|
| circle.setAttribute('r', '5');
|
| circle.setAttribute('stroke', 'white');
|
| circle.setAttribute('stroke-width', '1');
|
|
|
| const color = p.clusterIndex !== undefined ? CLUSTER_COLORS[p.clusterIndex % CLUSTER_COLORS.length].stroke : '#94a3b8';
|
| circle.setAttribute('fill', color);
|
| pointsGroup.appendChild(circle);
|
| });
|
|
|
| updateUI();
|
| }
|
|
|
| function updateUI() {
|
|
|
| const legend = document.getElementById('legend');
|
| if (legend) {
|
| legend.innerHTML = state.clusters.map((c, i) => `
|
| <div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white border shadow-sm">
|
| <div class="w-3 h-3 rounded-full" style="background: ${c.color.stroke}"></div>
|
| <span class="font-medium text-xs">${c.color.label}</span>
|
| <span class="text-[10px] text-slate-400 font-mono">(${(c.weight * 100).toFixed(0)}%)</span>
|
| </div>
|
| `).join('');
|
| }
|
|
|
|
|
| const sInit = document.getElementById('step-init');
|
| const sE = document.getElementById('step-e');
|
| const sM = document.getElementById('step-m');
|
| const l1 = document.getElementById('line-1');
|
| const l2 = document.getElementById('line-2');
|
|
|
| if (sInit && sE && sM) {
|
| [sInit, sE, sM].forEach(el => el.className = 'step-node w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm transition-all bg-slate-100 text-slate-400');
|
| if (l1 && l2) [l1, l2].forEach(el => el.className = 'h-0.5 flex-1 mx-2 bg-slate-200');
|
|
|
| if (state.converged) {
|
| [sInit, sE, sM].forEach(el => el.classList.add('converged'));
|
| if (l1 && l2) [l1, l2].forEach(el => el.classList.add('bg-emerald-500'));
|
| } else if (state.stepType === 'e-step') {
|
| sE.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
|
| sInit.className = 'step-node done w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-slate-200 text-slate-500';
|
| if (l1) l1.className = 'h-0.5 flex-1 mx-2 bg-blue-200';
|
| } else if (state.stepType === 'm-step') {
|
| sM.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
|
| sE.className = 'step-node done w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-slate-200 text-slate-500';
|
| if (l2) l2.className = 'h-0.5 flex-1 mx-2 bg-blue-200';
|
| } else {
|
| sInit.className = 'step-node active w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm bg-blue-600 text-white';
|
| }
|
| }
|
|
|
|
|
| const badge = document.getElementById('status-badge');
|
| if (badge) {
|
| if (state.converged) {
|
| badge.className = 'px-4 py-2 rounded-lg font-semibold text-sm bg-emerald-100 text-emerald-700 block';
|
| badge.innerText = 'Converged!';
|
| } else if (state.stepType !== 'none') {
|
| badge.className = `px-4 py-2 rounded-lg font-semibold text-sm block ${state.stepType === 'e-step' ? 'bg-orange-100 text-orange-700' : 'bg-blue-100 text-blue-700'}`;
|
| badge.innerText = state.stepType === 'e-step' ? 'E-Step' : 'M-Step';
|
| } else {
|
| badge.className = 'hidden';
|
| }
|
| }
|
|
|
|
|
| const explCard = document.getElementById('explanation-card');
|
| const explTitle = document.getElementById('expl-title');
|
| const explContent = document.getElementById('expl-content');
|
| const explIcon = document.getElementById('expl-icon');
|
|
|
| if (explCard && explTitle && explContent && explIcon) {
|
| if (state.converged) {
|
| explCard.className = 'p-5 rounded-xl border-2 bg-emerald-50 border-emerald-200 shadow-soft';
|
| explTitle.innerText = "Converged! 🎉";
|
| explIcon.className = "p-3 rounded-lg bg-white text-emerald-600";
|
| explIcon.innerHTML = '<i data-lucide="check-circle-2" class="w-6 h-6"></i>';
|
| explContent.innerHTML = `
|
| <li class="flex items-start gap-2 text-sm text-emerald-800 font-medium">Clusters have stopped moving. Optimal fit found!</li>
|
| <li class="flex items-start gap-2 text-sm text-slate-600">Total iterations: ${state.iteration}</li>
|
| `;
|
| } else if (state.stepType === 'e-step') {
|
| explCard.className = 'p-5 rounded-xl border-2 bg-orange-50 border-orange-200 shadow-soft';
|
| explTitle.innerText = "E-Step: Expectations 🤔";
|
| explIcon.className = "p-3 rounded-lg bg-white text-orange-600";
|
| explIcon.innerHTML = '<i data-lucide="target" class="w-6 h-6"></i>';
|
| explContent.innerHTML = `
|
| <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-orange-500"></i> Each point calculates probabilities for all clusters.</li>
|
| <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-orange-500"></i> Points change color based on the highest probability.</li>
|
| `;
|
| } else if (state.stepType === 'm-step') {
|
| explCard.className = 'p-5 rounded-xl border-2 bg-blue-50 border-blue-200 shadow-soft';
|
| explTitle.innerText = "M-Step: Update 📊";
|
| explIcon.className = "p-3 rounded-lg bg-white text-blue-600";
|
| explIcon.innerHTML = '<i data-lucide="bar-chart-3" class="w-6 h-6"></i>';
|
| explContent.innerHTML = `
|
| <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-blue-500"></i> Centers move to the heart of their assigned points.</li>
|
| <li class="flex items-start gap-2 text-sm text-slate-600"><i data-lucide="arrow-right" class="w-4 h-4 text-blue-500"></i> Ellipses stretch to cover the point spread.</li>
|
| `;
|
| } else {
|
| explCard.className = 'p-5 rounded-xl border-2 bg-slate-50 border-slate-200 shadow-soft';
|
| explTitle.innerText = "Ready to Start! 🚀";
|
| explIcon.className = "p-3 rounded-lg bg-white text-blue-600";
|
| explIcon.innerHTML = '<i data-lucide="lightbulb" class="w-6 h-6"></i>';
|
| explContent.innerHTML = `
|
| <li class="flex items-start gap-2 text-sm text-slate-600">Press 'Next Step' or 'Auto Play' to begin.</li>
|
| `;
|
| }
|
| }
|
|
|
|
|
| const counterText = document.getElementById('counter-text');
|
| if (counterText) counterText.innerText = `${state.iteration} / ${MAX_ITERATIONS}`;
|
|
|
| const btnPlayText = document.getElementById('btn-play-text');
|
| if (btnPlayText) btnPlayText.innerText = state.isPlaying ? 'Pause' : 'Auto Play';
|
|
|
| const btnPlay = document.getElementById('btn-play');
|
| if (btnPlay) {
|
| const icon = btnPlay.querySelector('i, svg');
|
| if (icon) icon.setAttribute('data-lucide', state.isPlaying ? 'pause' : 'play');
|
| }
|
|
|
|
|
| renderChart();
|
| if (window.lucide) lucide.createIcons();
|
| }
|
|
|
| function renderChart() {
|
| const container = document.getElementById('chart-container');
|
| if (!container) return;
|
| if (state.logLikelihoods.length === 0) return;
|
|
|
| const min = Math.min(...state.logLikelihoods);
|
| const max = Math.max(...state.logLikelihoods);
|
| const range = max - min || 10;
|
|
|
| container.innerHTML = state.logLikelihoods.map((val, i) => {
|
| const height = ((val - min) / range * 80) + 10;
|
| return `<div class="bg-blue-500 rounded-t-sm flex-1 transition-all duration-300" style="height: ${height}%" title="Iter ${i}: ${val.toFixed(2)}"></div>`;
|
| }).join('');
|
|
|
| const convergedBadge = document.getElementById('converged-badge');
|
| if (convergedBadge) {
|
| if (state.converged) {
|
| convergedBadge.classList.replace('hidden', 'flex');
|
| } else {
|
| convergedBadge.classList.replace('flex', 'hidden');
|
| }
|
| }
|
| }
|
|
|
|
|
| function performStep() {
|
| if (state.converged) return;
|
| if (state.stepType === 'none' || state.stepType === 'm-step') {
|
| eStep();
|
| } else {
|
| mStep();
|
| }
|
| render();
|
| }
|
|
|
| function toggleAutoPlay() {
|
| if (state.isPlaying) {
|
| stopAutoPlay();
|
| } else {
|
| if (state.converged) return;
|
| state.isPlaying = true;
|
| state.timer = setInterval(() => {
|
| performStep();
|
| if (state.converged) stopAutoPlay();
|
| }, state.speed);
|
| updateUI();
|
| }
|
| }
|
|
|
| function stopAutoPlay() {
|
| state.isPlaying = false;
|
| clearInterval(state.timer);
|
| updateUI();
|
| }
|
|
|
| function switchTab(tabId) {
|
| document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
| document.querySelectorAll('.tab-trigger').forEach(el => el.classList.remove('active', 'text-slate-900'));
|
| document.querySelectorAll('.tab-trigger').forEach(el => el.classList.add('text-slate-600'));
|
|
|
| const tab = document.getElementById(`tab-${tabId}`);
|
| if (tab) tab.classList.add('active');
|
|
|
| const btn = document.getElementById(`btn-tab-${tabId}`);
|
| if (btn) {
|
| btn.classList.add('active', 'text-slate-900');
|
| btn.classList.remove('text-slate-600');
|
| }
|
| if (window.lucide) lucide.createIcons();
|
| }
|
|
|
| function closeModal() {
|
| const modal = document.getElementById('intro-modal');
|
| if (modal) modal.classList.add('hidden');
|
| }
|
|
|
|
|
| window.onload = () => {
|
| generateData();
|
|
|
|
|
| const btnPlay = document.getElementById('btn-play');
|
| if (btnPlay) btnPlay.addEventListener('click', toggleAutoPlay);
|
|
|
| const btnStep = document.getElementById('btn-step');
|
| if (btnStep) btnStep.addEventListener('click', performStep);
|
|
|
| const btnReset = document.getElementById('btn-reset');
|
| if (btnReset) btnReset.addEventListener('click', () => {
|
| initClusters();
|
| resetSimState();
|
| render();
|
| });
|
|
|
| const btnGenerate = document.getElementById('btn-generate');
|
| if (btnGenerate) btnGenerate.addEventListener('click', generateData);
|
|
|
| const speedSlider = document.getElementById('speed-slider');
|
| if (speedSlider) {
|
| speedSlider.addEventListener('input', (e) => {
|
| state.speed = 3500 - parseInt(e.target.value);
|
| const text = state.speed > 2000 ? 'Slow' : state.speed > 1000 ? 'Medium' : 'Fast';
|
| const speedText = document.getElementById('speed-text');
|
| if (speedText) speedText.innerText = text;
|
| if (state.isPlaying) {
|
| stopAutoPlay();
|
| toggleAutoPlay();
|
| }
|
| });
|
| }
|
|
|
| if (window.lucide) lucide.createIcons();
|
| };
|
| </script>
|
| </body>
|
| </html> |