Below, a target distribution (shown in blue) represents the true posterior we want to approximate — imagine it as the real answer hidden behind an impossible integral. The variational distribution (shown in orange) is a Gaussian that we are free to move and reshape.
<p class="hint">The <b style="color:#2176ae">blue curve</b> is the target posterior (a bimodal mixture of two Gaussians). The <b style="color:#e07b39">orange curve</b> is a Gaussian approximation. Each step minimizes KL divergence, making the orange curve hug the blue one.</p>
<canvas id="c" width="480" height="200"></canvas>
<div class="info" id="info">KL divergence: —</div>
<div class="btns">
<button id="step">Step</button>
<button id="run">Run</button>
<button id="stop" disabled>Stop</button>
<button id="reset" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #dde3ea; border-radius: 8px; background: #f7f9fb; }
.info { font-size: .92rem; font-weight: 600; margin: .4rem 0; color: #1d3557; min-height: 1.4em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .4rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Variational Inference demo
// Target p(x): bimodal mixture 0.5*N(-2, 0.7²) + 0.5*N(2, 0.7²)
// Variational q(x): Gaussian N(mu, sigma²)
// We minimize KL(q||p) using the reparameterization trick.
//
// Exact reparam gradient (derived analytically):
// grad_mu KL = E_eps[ -d log p(x) / dx ] where x = mu + sigma*eps
// grad_logS KL = E_eps[ -1 - sigma*eps * d log p / dx ]
// (sigma = exp(logSigma), eps ~ N(0,1))
const W = 480, H = 200, XMIN = -5.5, XMAX = 5.5;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function gaussPdf(x, mu, sig) {
return Math.exp(-0.5 * ((x - mu) / sig) ** 2) / (sig * Math.sqrt(2 * Math.PI));
}
function targetLogP(x) {
return Math.log(0.5 * gaussPdf(x, -2, 0.7) + 0.5 * gaussPdf(x, 2, 0.7) + 1e-300);
}
function randn() {
const u = Math.random() || 1e-10, v = Math.random() || 1e-10;
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Start from a bad approximation: narrow spike at center
let mu = 0.0, logSig = Math.log(0.5);
const LR_MU = 0.03, LR_LSIG = 0.03, N_MC = 400;
function step() {
const sig = Math.exp(logSig);
const dh = 1e-5;
let gMu = 0, gLs = 0;
for (let i = 0; i < N_MC; i++) {
const eps = randn();
const x = mu + sig * eps;
// Numerical gradient of log p at x
const dlp = (targetLogP(x + dh) - targetLogP(x - dh)) / (2 * dh);
gMu += -dlp; // grad_mu KL
gLs += -1 - sig * eps * dlp; // grad_logS KL
}
mu -= LR_MU * gMu / N_MC;
logSig -= LR_LSIG * gLs / N_MC;
logSig = Math.max(-3, Math.min(2, logSig));
draw();
}
function klEstimate() {
const sig = Math.exp(logSig);
let kl = 0;
for (let i = 0; i < N_MC; i++) {
const eps = randn();
const x = mu + sig * eps;
const logQ = -0.5 * eps * eps - logSig - 0.5 * Math.log(2 * Math.PI);
kl += logQ - targetLogP(x);
}
return kl / N_MC;
}
function xPx(x) { return (x - XMIN) / (XMAX - XMIN) * W; }
function yPx(v, maxV) { return H - 8 - (v / maxV) * (H - 20); }
function draw() {
ctx.clearRect(0, 0, W, H);
const sig = Math.exp(logSig);
const STEPS = 300, dx = (XMAX - XMIN) / STEPS;
let maxV = 0;
for (let i = 0; i <= STEPS; i++) {
const x = XMIN + i * dx;
const tp = Math.exp(targetLogP(x));
const qp = gaussPdf(x, mu, sig);
if (tp > maxV) maxV = tp;
if (qp > maxV) maxV = qp;
}
maxV *= 1.1;
// Target (blue)
ctx.beginPath(); ctx.strokeStyle = '#2176ae'; ctx.lineWidth = 2.5;
for (let i = 0; i <= STEPS; i++) {
const x = XMIN + i * dx;
const y = yPx(Math.exp(targetLogP(x)), maxV);
i === 0 ? ctx.moveTo(xPx(x), y) : ctx.lineTo(xPx(x), y);
}
ctx.stroke();
// Variational q (orange)
ctx.beginPath(); ctx.strokeStyle = '#e07b39'; ctx.lineWidth = 2.5;
for (let i = 0; i <= STEPS; i++) {
const x = XMIN + i * dx;
const y = yPx(gaussPdf(x, mu, sig), maxV);
i === 0 ? ctx.moveTo(xPx(x), y) : ctx.lineTo(xPx(x), y);
}
ctx.stroke();
// Legend
ctx.font = '12px system-ui'; ctx.lineWidth = 1;
ctx.fillStyle = '#2176ae'; ctx.fillRect(10, 10, 18, 4);
ctx.fillText('Target p(x)', 32, 18);
ctx.fillStyle = '#e07b39'; ctx.fillRect(10, 26, 18, 4);
ctx.fillText('Variational q(x)', 32, 34);
const kl = klEstimate();
document.getElementById('info').textContent =
fmt('KL ≈ %kl% μ = %mu% σ = %sig%', { kl: kl.toFixed(3), mu: mu.toFixed(2), sig: sig.toFixed(2) });
}
let timer = null;
document.getElementById('step').addEventListener('click', step);
document.getElementById('run').addEventListener('click', () => {
document.getElementById('stop').disabled = false;
document.getElementById('run').disabled = true;
timer = setInterval(step, 60);
});
document.getElementById('stop').addEventListener('click', () => {
clearInterval(timer); timer = null;
document.getElementById('stop').disabled = true;
document.getElementById('run').disabled = false;
});
document.getElementById('reset').addEventListener('click', () => {
clearInterval(timer); timer = null;
mu = 0.0; logSig = Math.log(0.5);
document.getElementById('stop').disabled = true;
document.getElementById('run').disabled = false;
draw();
});
draw();
<p class="hint">The <b style="color:#2176ae">blue curve</b> is the target posterior (a bimodal mixture of two Gaussians). The <b style="color:#e07b39">orange curve</b> is a Gaussian approximation. Each step minimizes KL divergence, making the orange curve hug the blue one.</p>
<canvas id="c" width="480" height="200"></canvas>
<div class="info" id="info">KL divergence: —</div>
<div class="btns">
<button id="step">Step</button>
<button id="run">Run</button>
<button id="stop" disabled>Stop</button>
<button id="reset" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #dde3ea; border-radius: 8px; background: #f7f9fb; }
.info { font-size: .92rem; font-weight: 600; margin: .4rem 0; color: #1d3557; min-height: 1.4em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .4rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Variational Inference demo
// Target p(x): bimodal mixture 0.5*N(-2, 0.7²) + 0.5*N(2, 0.7²)
// Variational q(x): Gaussian N(mu, sigma²)
// We minimize KL(q||p) using the reparameterization trick.
//
// Exact reparam gradient (derived analytically):
// grad_mu KL = E_eps[ -d log p(x) / dx ] where x = mu + sigma*eps
// grad_logS KL = E_eps[ -1 - sigma*eps * d log p / dx ]
// (sigma = exp(logSigma), eps ~ N(0,1))
const W = 480, H = 200, XMIN = -5.5, XMAX = 5.5;
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
function gaussPdf(x, mu, sig) {
return Math.exp(-0.5 * ((x - mu) / sig) ** 2) / (sig * Math.sqrt(2 * Math.PI));
}
function targetLogP(x) {
return Math.log(0.5 * gaussPdf(x, -2, 0.7) + 0.5 * gaussPdf(x, 2, 0.7) + 1e-300);
}
function randn() {
const u = Math.random() || 1e-10, v = Math.random() || 1e-10;
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Start from a bad approximation: narrow spike at center
let mu = 0.0, logSig = Math.log(0.5);
const LR_MU = 0.03, LR_LSIG = 0.03, N_MC = 400;
function step() {
const sig = Math.exp(logSig);
const dh = 1e-5;
let gMu = 0, gLs = 0;
for (let i = 0; i < N_MC; i++) {
const eps = randn();
const x = mu + sig * eps;
// Numerical gradient of log p at x
const dlp = (targetLogP(x + dh) - targetLogP(x - dh)) / (2 * dh);
gMu += -dlp; // grad_mu KL
gLs += -1 - sig * eps * dlp; // grad_logS KL
}
mu -= LR_MU * gMu / N_MC;
logSig -= LR_LSIG * gLs / N_MC;
logSig = Math.max(-3, Math.min(2, logSig));
draw();
}
function klEstimate() {
const sig = Math.exp(logSig);
let kl = 0;
for (let i = 0; i < N_MC; i++) {
const eps = randn();
const x = mu + sig * eps;
const logQ = -0.5 * eps * eps - logSig - 0.5 * Math.log(2 * Math.PI);
kl += logQ - targetLogP(x);
}
return kl / N_MC;
}
function xPx(x) { return (x - XMIN) / (XMAX - XMIN) * W; }
function yPx(v, maxV) { return H - 8 - (v / maxV) * (H - 20); }
function draw() {
ctx.clearRect(0, 0, W, H);
const sig = Math.exp(logSig);
const STEPS = 300, dx = (XMAX - XMIN) / STEPS;
let maxV = 0;
for (let i = 0; i <= STEPS; i++) {
const x = XMIN + i * dx;
const tp = Math.exp(targetLogP(x));
const qp = gaussPdf(x, mu, sig);
if (tp > maxV) maxV = tp;
if (qp > maxV) maxV = qp;
}
maxV *= 1.1;
// Target (blue)
ctx.beginPath(); ctx.strokeStyle = '#2176ae'; ctx.lineWidth = 2.5;
for (let i = 0; i <= STEPS; i++) {
const x = XMIN + i * dx;
const y = yPx(Math.exp(targetLogP(x)), maxV);
i === 0 ? ctx.moveTo(xPx(x), y) : ctx.lineTo(xPx(x), y);
}
ctx.stroke();
// Variational q (orange)
ctx.beginPath(); ctx.strokeStyle = '#e07b39'; ctx.lineWidth = 2.5;
for (let i = 0; i <= STEPS; i++) {
const x = XMIN + i * dx;
const y = yPx(gaussPdf(x, mu, sig), maxV);
i === 0 ? ctx.moveTo(xPx(x), y) : ctx.lineTo(xPx(x), y);
}
ctx.stroke();
// Legend
ctx.font = '12px system-ui'; ctx.lineWidth = 1;
ctx.fillStyle = '#2176ae'; ctx.fillRect(10, 10, 18, 4);
ctx.fillText('Target p(x)', 32, 18);
ctx.fillStyle = '#e07b39'; ctx.fillRect(10, 26, 18, 4);
ctx.fillText('Variational q(x)', 32, 34);
const kl = klEstimate();
document.getElementById('info').textContent =
fmt('KL ≈ %kl% μ = %mu% σ = %sig%', { kl: kl.toFixed(3), mu: mu.toFixed(2), sig: sig.toFixed(2) });
}
let timer = null;
document.getElementById('step').addEventListener('click', step);
document.getElementById('run').addEventListener('click', () => {
document.getElementById('stop').disabled = false;
document.getElementById('run').disabled = true;
timer = setInterval(step, 60);
});
document.getElementById('stop').addEventListener('click', () => {
clearInterval(timer); timer = null;
document.getElementById('stop').disabled = true;
document.getElementById('run').disabled = false;
});
document.getElementById('reset').addEventListener('click', () => {
clearInterval(timer); timer = null;
mu = 0.0; logSig = Math.log(0.5);
document.getElementById('stop').disabled = true;
document.getElementById('run').disabled = false;
draw();
});
draw();
Click Step to perform one gradient-descent update, or Run to let it converge automatically. Watch the orange curve slide toward the blue one, minimizing the KL divergence at each step. Notice that the Gaussian can never perfectly capture a multi-modal target — that gap is the approximation error that variational inference always carries.
Comments
Loading comments...