<div class="hint">Two distributions live in this world: the <b>unconditioned</b> mixture (dashed gray, two peaks) and the <b>target</b> (solid blue, right peak). Guidance extrapolates the score away from the unconditioned distribution toward the target. Drag the scale and sample to see the effect.</div>
<div class="controls">
<label for="guidance">Guidance scale w = <span id="wval">1.0</span></label>
<input type="range" id="guidance" min="0" max="20" step="0.5" value="1">
</div>
<div class="controls">
<label for="nsamples">Number of samples = <span id="nsval">40</span></label>
<input type="range" id="nsamples" min="5" max="80" step="5" value="40">
</div>
<canvas id="canvas" width="480" height="220"></canvas>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-sample" type="button">Sample</button>
<button id="btn-reset" type="button" 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 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .4rem; font-size: .9rem; }
.controls label { min-width: 160px; }
input[type=range] { flex: 1; accent-color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; width: 100%; max-width: 480px; margin: .5rem 0; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Classifier-Free Guidance (CFG) demo — 1-D toy diffusion.
// Unconditioned distribution = mixture of two Gaussians at -0.6 and 0.6.
const MU_UNCOND = 0.0;
const MU_TARGET = 1.0;
const SIGMA = 0.28;
// Score function = gradient of log-probability. Used by Langevin dynamics.
function scoreUncond(x) {
// Mixture-of-Gaussians score (unnormalized; proportional to gradient of log p).
const w1 = 0.5, w2 = 0.5;
const mu1 = -0.6, mu2 = 0.6;
const s1 = Math.exp(-0.5 * Math.pow((x - mu1) / SIGMA, 2));
const s2 = Math.exp(-0.5 * Math.pow((x - mu2) / SIGMA, 2));
const denom = w1 * s1 + w2 * s2 + 1e-9;
return (w1 * s1 * (mu1 - x) + w2 * s2 * (mu2 - x)) / (denom * SIGMA * SIGMA);
}
function scoreCond(x) {
// Conditional score = gradient of log p(x | c) for the right-peak target.
return (MU_TARGET - x) / (SIGMA * SIGMA);
}
function cfgScore(x, w) {
// CFG extrapolation: uncond_score + w * (cond_score - uncond_score).
return scoreUncond(x) + w * (scoreCond(x) - scoreUncond(x));
}
function sampleLangevin(w, n) {
// Simple Langevin sampler: x += dt * score(x, w) + noise.
const dt = 0.05;
const steps = 60;
const noise = 0.18;
const xs = [];
for (let i = 0; i < n; i++) {
let x = (Math.random() - 0.5) * 3.0;
for (let s = 0; s < steps; s++) {
x += dt * cfgScore(x, w) + noise * Math.sqrt(2 * dt) * (Math.random() * 2 - 1);
}
xs.push(x);
}
return xs;
}
function gaussian(x, mu, sigma) {
return Math.exp(-0.5 * Math.pow((x - mu) / sigma, 2)) / (sigma * Math.sqrt(2 * Math.PI));
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const XMIN = -2.0, XMAX = 2.0;
const YMAX = 1.6;
function toPixel(x, y) {
return [
Math.round(((x - XMIN) / (XMAX - XMIN)) * W),
Math.round((1 - y / YMAX) * (H - 30) + 5)
];
}
function drawDistributions() {
// Clear canvas before redraw.
ctx.clearRect(0, 0, W, H);
// Draw unconditioned distribution as dashed gray curve.
ctx.beginPath();
ctx.strokeStyle = '#adb5bd';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
for (let px = 0; px < W; px++) {
const x = XMIN + (px / W) * (XMAX - XMIN);
const y = 0.5 * gaussian(x, -0.6, SIGMA) + 0.5 * gaussian(x, 0.6, SIGMA);
const [cx, cy] = toPixel(x, y);
px === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
}
ctx.stroke();
ctx.setLineDash([]);
// Draw target (conditional) distribution as solid blue curve.
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
for (let px = 0; px < W; px++) {
const x = XMIN + (px / W) * (XMAX - XMIN);
const y = gaussian(x, MU_TARGET, SIGMA);
const [cx, cy] = toPixel(x, y);
px === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
}
ctx.stroke();
// Draw x-axis baseline.
ctx.beginPath();
ctx.strokeStyle = '#888';
ctx.lineWidth = 1;
const [, axY] = toPixel(0, 0);
ctx.moveTo(0, axY);
ctx.lineTo(W, axY);
ctx.stroke();
}
function drawSamples(xs) {
// Draw histogram of sampled particles as red bars.
const bins = new Array(40).fill(0);
for (const x of xs) {
const b = Math.floor(((x - XMIN) / (XMAX - XMIN)) * bins.length);
if (b >= 0 && b < bins.length) bins[b]++;
}
const maxB = Math.max(...bins, 1);
ctx.fillStyle = 'rgba(230, 57, 70, 0.55)';
for (let i = 0; i < bins.length; i++) {
if (!bins[i]) continue;
const xL = XMIN + (i / bins.length) * (XMAX - XMIN);
const xR = XMIN + ((i + 1) / bins.length) * (XMAX - XMIN);
const barH = (bins[i] / maxB) * YMAX * 0.85;
const [px1] = toPixel(xL, 0);
const [px2, py] = toPixel(xR, barH);
const [, y0] = toPixel(xL, 0);
ctx.fillRect(px1, py, px2 - px1 - 1, y0 - py);
}
}
// Draw legend text in the top-left corner.
function drawLegend(w) {
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#adb5bd';
ctx.fillText('--- unconditioned (mixture)', 8, 16);
ctx.fillStyle = '#1d3557';
ctx.fillText('--- target distribution', 8, 30);
ctx.fillStyle = 'rgba(230,57,70,0.8)';
ctx.fillText(i18nFmt('=== samples (w = %w%)', { w: w.toFixed(1) }), 8, 44);
}
let lastSamples = null;
function draw(samples, w) {
drawDistributions();
if (samples) drawSamples(samples);
drawLegend(w);
}
const wSlider = document.getElementById('guidance');
const wVal = document.getElementById('wval');
const nsSlider = document.getElementById('nsamples');
const nsVal = document.getElementById('nsval');
const statusEl = document.getElementById('status');
wSlider.addEventListener('input', () => {
wVal.textContent = parseFloat(wSlider.value).toFixed(1);
draw(lastSamples, parseFloat(wSlider.value));
});
nsSlider.addEventListener('input', () => { nsVal.textContent = nsSlider.value; });
document.getElementById('btn-sample').addEventListener('click', () => {
const w = parseFloat(wSlider.value);
const n = parseInt(nsSlider.value, 10);
lastSamples = sampleLangevin(w, n);
const mean = lastSamples.reduce((a, b) => a + b, 0) / n;
draw(lastSamples, w);
const inTarget = lastSamples.filter(x => x > 0.2).length;
statusEl.className = 'status ok';
statusEl.textContent = i18nFmt('%n% samples drawn at w = %w% — %pct%% landed near the target.', { n: n, pct: Math.round(inTarget / n * 100), w: w.toFixed(1) });
});
document.getElementById('btn-reset').addEventListener('click', () => {
lastSamples = null;
wSlider.value = 1;
wVal.textContent = '1.0';
nsSlider.value = 40;
nsVal.textContent = '40';
statusEl.className = 'status';
statusEl.textContent = '';
draw(null, 1.0);
});
draw(null, 1.0);
<div class="hint">Two distributions live in this world: the <b>unconditioned</b> mixture (dashed gray, two peaks) and the <b>target</b> (solid blue, right peak). Guidance extrapolates the score away from the unconditioned distribution toward the target. Drag the scale and sample to see the effect.</div>
<div class="controls">
<label for="guidance">Guidance scale w = <span id="wval">1.0</span></label>
<input type="range" id="guidance" min="0" max="20" step="0.5" value="1">
</div>
<div class="controls">
<label for="nsamples">Number of samples = <span id="nsval">40</span></label>
<input type="range" id="nsamples" min="5" max="80" step="5" value="40">
</div>
<canvas id="canvas" width="480" height="220"></canvas>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-sample" type="button">Sample</button>
<button id="btn-reset" type="button" 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 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; margin-bottom: .4rem; font-size: .9rem; }
.controls label { min-width: 160px; }
input[type=range] { flex: 1; accent-color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; width: 100%; max-width: 480px; margin: .5rem 0; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Classifier-Free Guidance (CFG) demo — 1-D toy diffusion.
// Unconditioned distribution = mixture of two Gaussians at -0.6 and 0.6.
const MU_UNCOND = 0.0;
const MU_TARGET = 1.0;
const SIGMA = 0.28;
// Score function = gradient of log-probability. Used by Langevin dynamics.
function scoreUncond(x) {
// Mixture-of-Gaussians score (unnormalized; proportional to gradient of log p).
const w1 = 0.5, w2 = 0.5;
const mu1 = -0.6, mu2 = 0.6;
const s1 = Math.exp(-0.5 * Math.pow((x - mu1) / SIGMA, 2));
const s2 = Math.exp(-0.5 * Math.pow((x - mu2) / SIGMA, 2));
const denom = w1 * s1 + w2 * s2 + 1e-9;
return (w1 * s1 * (mu1 - x) + w2 * s2 * (mu2 - x)) / (denom * SIGMA * SIGMA);
}
function scoreCond(x) {
// Conditional score = gradient of log p(x | c) for the right-peak target.
return (MU_TARGET - x) / (SIGMA * SIGMA);
}
function cfgScore(x, w) {
// CFG extrapolation: uncond_score + w * (cond_score - uncond_score).
return scoreUncond(x) + w * (scoreCond(x) - scoreUncond(x));
}
function sampleLangevin(w, n) {
// Simple Langevin sampler: x += dt * score(x, w) + noise.
const dt = 0.05;
const steps = 60;
const noise = 0.18;
const xs = [];
for (let i = 0; i < n; i++) {
let x = (Math.random() - 0.5) * 3.0;
for (let s = 0; s < steps; s++) {
x += dt * cfgScore(x, w) + noise * Math.sqrt(2 * dt) * (Math.random() * 2 - 1);
}
xs.push(x);
}
return xs;
}
function gaussian(x, mu, sigma) {
return Math.exp(-0.5 * Math.pow((x - mu) / sigma, 2)) / (sigma * Math.sqrt(2 * Math.PI));
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const XMIN = -2.0, XMAX = 2.0;
const YMAX = 1.6;
function toPixel(x, y) {
return [
Math.round(((x - XMIN) / (XMAX - XMIN)) * W),
Math.round((1 - y / YMAX) * (H - 30) + 5)
];
}
function drawDistributions() {
// Clear canvas before redraw.
ctx.clearRect(0, 0, W, H);
// Draw unconditioned distribution as dashed gray curve.
ctx.beginPath();
ctx.strokeStyle = '#adb5bd';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
for (let px = 0; px < W; px++) {
const x = XMIN + (px / W) * (XMAX - XMIN);
const y = 0.5 * gaussian(x, -0.6, SIGMA) + 0.5 * gaussian(x, 0.6, SIGMA);
const [cx, cy] = toPixel(x, y);
px === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
}
ctx.stroke();
ctx.setLineDash([]);
// Draw target (conditional) distribution as solid blue curve.
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
for (let px = 0; px < W; px++) {
const x = XMIN + (px / W) * (XMAX - XMIN);
const y = gaussian(x, MU_TARGET, SIGMA);
const [cx, cy] = toPixel(x, y);
px === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
}
ctx.stroke();
// Draw x-axis baseline.
ctx.beginPath();
ctx.strokeStyle = '#888';
ctx.lineWidth = 1;
const [, axY] = toPixel(0, 0);
ctx.moveTo(0, axY);
ctx.lineTo(W, axY);
ctx.stroke();
}
function drawSamples(xs) {
// Draw histogram of sampled particles as red bars.
const bins = new Array(40).fill(0);
for (const x of xs) {
const b = Math.floor(((x - XMIN) / (XMAX - XMIN)) * bins.length);
if (b >= 0 && b < bins.length) bins[b]++;
}
const maxB = Math.max(...bins, 1);
ctx.fillStyle = 'rgba(230, 57, 70, 0.55)';
for (let i = 0; i < bins.length; i++) {
if (!bins[i]) continue;
const xL = XMIN + (i / bins.length) * (XMAX - XMIN);
const xR = XMIN + ((i + 1) / bins.length) * (XMAX - XMIN);
const barH = (bins[i] / maxB) * YMAX * 0.85;
const [px1] = toPixel(xL, 0);
const [px2, py] = toPixel(xR, barH);
const [, y0] = toPixel(xL, 0);
ctx.fillRect(px1, py, px2 - px1 - 1, y0 - py);
}
}
// Draw legend text in the top-left corner.
function drawLegend(w) {
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#adb5bd';
ctx.fillText('--- unconditioned (mixture)', 8, 16);
ctx.fillStyle = '#1d3557';
ctx.fillText('--- target distribution', 8, 30);
ctx.fillStyle = 'rgba(230,57,70,0.8)';
ctx.fillText(i18nFmt('=== samples (w = %w%)', { w: w.toFixed(1) }), 8, 44);
}
let lastSamples = null;
function draw(samples, w) {
drawDistributions();
if (samples) drawSamples(samples);
drawLegend(w);
}
const wSlider = document.getElementById('guidance');
const wVal = document.getElementById('wval');
const nsSlider = document.getElementById('nsamples');
const nsVal = document.getElementById('nsval');
const statusEl = document.getElementById('status');
wSlider.addEventListener('input', () => {
wVal.textContent = parseFloat(wSlider.value).toFixed(1);
draw(lastSamples, parseFloat(wSlider.value));
});
nsSlider.addEventListener('input', () => { nsVal.textContent = nsSlider.value; });
document.getElementById('btn-sample').addEventListener('click', () => {
const w = parseFloat(wSlider.value);
const n = parseInt(nsSlider.value, 10);
lastSamples = sampleLangevin(w, n);
const mean = lastSamples.reduce((a, b) => a + b, 0) / n;
draw(lastSamples, w);
const inTarget = lastSamples.filter(x => x > 0.2).length;
statusEl.className = 'status ok';
statusEl.textContent = i18nFmt('%n% samples drawn at w = %w% — %pct%% landed near the target.', { n: n, pct: Math.round(inTarget / n * 100), w: w.toFixed(1) });
});
document.getElementById('btn-reset').addEventListener('click', () => {
lastSamples = null;
wSlider.value = 1;
wVal.textContent = '1.0';
nsSlider.value = 40;
nsVal.textContent = '40';
statusEl.className = 'status';
statusEl.textContent = '';
draw(null, 1.0);
});
draw(null, 1.0);
Comments
Loading comments...