The demo below runs the CE method on a bumpy one-dimensional function. A Gaussian population (blue dots) is drawn each round, scored, and the top 20 % elites (red dots) are kept. The Gaussian is then refit to those elites — watch the mean and spread collapse toward the global minimum.
<div class="controls">
<label>Elite %: <span id="eliteVal">20</span>%
<input type="range" id="elitePct" min="5" max="40" value="20" step="5">
</label>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="cv" width="560" height="220"></canvas>
<div id="info" class="info">Generation 0 — click Step or Run</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: .7rem 1.2rem; margin-bottom: .5rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 110px; accent-color: #1d3557; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 8px; background: #f4f7fa;
border: 1px solid #dde4ec; width: 100%; max-width: 560px; }
.info { font-size: .88rem; font-weight: 600; margin-top: .45rem; min-height: 1.3em; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cross-Entropy Method demo — minimize a bumpy 1-D function.
// f(x) = sin(2.5x) + sin(3.7x) + 0.4*sin(6x) on [-4, 4]
// True global minimum at approximately x ≈ -0.40 (f ≈ -2.108)
// CE loop: sample N points from Gaussian(mu, sigma), keep elite fraction,
// refit mu = mean(elites), sigma = std(elites).
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const XMIN = -4, XMAX = 4;
const N = 80; // population size
function f(x) {
return Math.sin(2.5 * x) + Math.sin(3.7 * x) + 0.4 * Math.sin(6 * x);
}
// Map domain x -> pixel x
function px(x) { return (x - XMIN) / (XMAX - XMIN) * (W - 40) + 20; }
// Map f value -> pixel y (f range roughly -2.2 to 2.2)
const FMIN = -2.4, FMAX = 2.4;
function py(v) { return H - 20 - (v - FMIN) / (FMAX - FMIN) * (H - 40); }
// Gaussian sampler (Box-Muller)
function randn() {
let u, v;
do { u = Math.random(); } while (u === 0);
v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
let mu, sigma, gen, running, timer;
function init() {
mu = 0; sigma = 3.5; gen = 0; running = false;
clearInterval(timer);
document.getElementById('btnRun').textContent = 'Run';
draw([], []);
document.getElementById('info').textContent = 'Generation 0 — click Step or Run';
}
function step() {
const elitePct = Number(document.getElementById('elitePct').value) / 100;
// Sample
const xs = Array.from({length: N}, () => mu + sigma * randn());
// Evaluate
const scored = xs.map(x => ({x, y: f(x)})).sort((a, b) => a.y - b.y);
// Select elites (lowest f = best)
const k = Math.max(2, Math.floor(N * elitePct));
const elites = scored.slice(0, k);
// Refit
const newMu = elites.reduce((s, e) => s + e.x, 0) / k;
const newSigma = Math.sqrt(elites.reduce((s, e) => s + (e.x - newMu) ** 2, 0) / k) + 0.05;
mu = newMu; sigma = newSigma; gen++;
draw(scored, elites);
const best = elites[0];
document.getElementById('info').textContent =
fmt('Generation %gen% — best x ≈ %x%, f(x) ≈ %y% | μ=%mu%, σ=%sigma%', { gen: gen, x: best.x.toFixed(3), y: best.y.toFixed(3), mu: mu.toFixed(3), sigma: sigma.toFixed(3) });
}
function draw(all, elites) {
ctx.clearRect(0, 0, W, H);
// Draw function curve
ctx.beginPath(); ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.8;
for (let i = 0; i <= 400; i++) {
const x = XMIN + (XMAX - XMIN) * i / 400;
const y = f(x);
i === 0 ? ctx.moveTo(px(x), py(y)) : ctx.lineTo(px(x), py(y));
}
ctx.stroke();
// Draw axes
ctx.strokeStyle = '#bbc8d4'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(20, py(0)); ctx.lineTo(W-20, py(0)); ctx.stroke();
ctx.beginPath(); ctx.moveTo(px(0), 15); ctx.lineTo(px(0), H-15); ctx.stroke();
// Draw Gaussian density band (shaded)
const sig3 = 3 * sigma;
const gxLeft = Math.max(XMIN, mu - sig3), gxRight = Math.min(XMAX, mu + sig3);
const grad = ctx.createLinearGradient(px(gxLeft), 0, px(gxRight), 0);
grad.addColorStop(0, 'rgba(29,53,87,0)');
grad.addColorStop(0.5, 'rgba(29,53,87,0.12)');
grad.addColorStop(1, 'rgba(29,53,87,0)');
ctx.fillStyle = grad;
ctx.fillRect(px(gxLeft), 15, px(gxRight) - px(gxLeft), H - 30);
// Draw mu line
ctx.strokeStyle = 'rgba(29,53,87,0.5)'; ctx.lineWidth = 1.5; ctx.setLineDash([4,3]);
ctx.beginPath(); ctx.moveTo(px(mu), 15); ctx.lineTo(px(mu), H-15); ctx.stroke();
ctx.setLineDash([]);
// Draw all sample dots
all.forEach(({x, y}) => {
ctx.beginPath(); ctx.arc(px(x), py(y), 3, 0, 2*Math.PI);
ctx.fillStyle = 'rgba(100,150,220,0.55)'; ctx.fill();
});
// Draw elite dots
elites.forEach(({x, y}) => {
ctx.beginPath(); ctx.arc(px(x), py(y), 4, 0, 2*Math.PI);
ctx.fillStyle = '#e63946'; ctx.fill();
});
// Best marker
if (elites.length) {
const best = elites[0];
ctx.beginPath(); ctx.arc(px(best.x), py(best.y), 6, 0, 2*Math.PI);
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2; ctx.stroke();
}
}
document.getElementById('btnStep').addEventListener('click', step);
document.getElementById('btnRun').addEventListener('click', function() {
if (running) {
running = false; clearInterval(timer); this.textContent = 'Run';
} else {
running = true; this.textContent = 'Pause';
timer = setInterval(() => { if (sigma < 0.1) { clearInterval(timer); running = false; document.getElementById('btnRun').textContent = 'Run'; } else { step(); } }, 350);
}
});
document.getElementById('btnReset').addEventListener('click', init);
document.getElementById('elitePct').addEventListener('input', function() {
document.getElementById('eliteVal').textContent = this.value;
});
init();
<div class="controls">
<label>Elite %: <span id="eliteVal">20</span>%
<input type="range" id="elitePct" min="5" max="40" value="20" step="5">
</label>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="cv" width="560" height="220"></canvas>
<div id="info" class="info">Generation 0 — click Step or Run</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: #fff; }
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: .7rem 1.2rem; margin-bottom: .5rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 110px; accent-color: #1d3557; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border-radius: 8px; background: #f4f7fa;
border: 1px solid #dde4ec; width: 100%; max-width: 560px; }
.info { font-size: .88rem; font-weight: 600; margin-top: .45rem; min-height: 1.3em; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Cross-Entropy Method demo — minimize a bumpy 1-D function.
// f(x) = sin(2.5x) + sin(3.7x) + 0.4*sin(6x) on [-4, 4]
// True global minimum at approximately x ≈ -0.40 (f ≈ -2.108)
// CE loop: sample N points from Gaussian(mu, sigma), keep elite fraction,
// refit mu = mean(elites), sigma = std(elites).
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const XMIN = -4, XMAX = 4;
const N = 80; // population size
function f(x) {
return Math.sin(2.5 * x) + Math.sin(3.7 * x) + 0.4 * Math.sin(6 * x);
}
// Map domain x -> pixel x
function px(x) { return (x - XMIN) / (XMAX - XMIN) * (W - 40) + 20; }
// Map f value -> pixel y (f range roughly -2.2 to 2.2)
const FMIN = -2.4, FMAX = 2.4;
function py(v) { return H - 20 - (v - FMIN) / (FMAX - FMIN) * (H - 40); }
// Gaussian sampler (Box-Muller)
function randn() {
let u, v;
do { u = Math.random(); } while (u === 0);
v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
let mu, sigma, gen, running, timer;
function init() {
mu = 0; sigma = 3.5; gen = 0; running = false;
clearInterval(timer);
document.getElementById('btnRun').textContent = 'Run';
draw([], []);
document.getElementById('info').textContent = 'Generation 0 — click Step or Run';
}
function step() {
const elitePct = Number(document.getElementById('elitePct').value) / 100;
// Sample
const xs = Array.from({length: N}, () => mu + sigma * randn());
// Evaluate
const scored = xs.map(x => ({x, y: f(x)})).sort((a, b) => a.y - b.y);
// Select elites (lowest f = best)
const k = Math.max(2, Math.floor(N * elitePct));
const elites = scored.slice(0, k);
// Refit
const newMu = elites.reduce((s, e) => s + e.x, 0) / k;
const newSigma = Math.sqrt(elites.reduce((s, e) => s + (e.x - newMu) ** 2, 0) / k) + 0.05;
mu = newMu; sigma = newSigma; gen++;
draw(scored, elites);
const best = elites[0];
document.getElementById('info').textContent =
fmt('Generation %gen% — best x ≈ %x%, f(x) ≈ %y% | μ=%mu%, σ=%sigma%', { gen: gen, x: best.x.toFixed(3), y: best.y.toFixed(3), mu: mu.toFixed(3), sigma: sigma.toFixed(3) });
}
function draw(all, elites) {
ctx.clearRect(0, 0, W, H);
// Draw function curve
ctx.beginPath(); ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1.8;
for (let i = 0; i <= 400; i++) {
const x = XMIN + (XMAX - XMIN) * i / 400;
const y = f(x);
i === 0 ? ctx.moveTo(px(x), py(y)) : ctx.lineTo(px(x), py(y));
}
ctx.stroke();
// Draw axes
ctx.strokeStyle = '#bbc8d4'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(20, py(0)); ctx.lineTo(W-20, py(0)); ctx.stroke();
ctx.beginPath(); ctx.moveTo(px(0), 15); ctx.lineTo(px(0), H-15); ctx.stroke();
// Draw Gaussian density band (shaded)
const sig3 = 3 * sigma;
const gxLeft = Math.max(XMIN, mu - sig3), gxRight = Math.min(XMAX, mu + sig3);
const grad = ctx.createLinearGradient(px(gxLeft), 0, px(gxRight), 0);
grad.addColorStop(0, 'rgba(29,53,87,0)');
grad.addColorStop(0.5, 'rgba(29,53,87,0.12)');
grad.addColorStop(1, 'rgba(29,53,87,0)');
ctx.fillStyle = grad;
ctx.fillRect(px(gxLeft), 15, px(gxRight) - px(gxLeft), H - 30);
// Draw mu line
ctx.strokeStyle = 'rgba(29,53,87,0.5)'; ctx.lineWidth = 1.5; ctx.setLineDash([4,3]);
ctx.beginPath(); ctx.moveTo(px(mu), 15); ctx.lineTo(px(mu), H-15); ctx.stroke();
ctx.setLineDash([]);
// Draw all sample dots
all.forEach(({x, y}) => {
ctx.beginPath(); ctx.arc(px(x), py(y), 3, 0, 2*Math.PI);
ctx.fillStyle = 'rgba(100,150,220,0.55)'; ctx.fill();
});
// Draw elite dots
elites.forEach(({x, y}) => {
ctx.beginPath(); ctx.arc(px(x), py(y), 4, 0, 2*Math.PI);
ctx.fillStyle = '#e63946'; ctx.fill();
});
// Best marker
if (elites.length) {
const best = elites[0];
ctx.beginPath(); ctx.arc(px(best.x), py(best.y), 6, 0, 2*Math.PI);
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 2; ctx.stroke();
}
}
document.getElementById('btnStep').addEventListener('click', step);
document.getElementById('btnRun').addEventListener('click', function() {
if (running) {
running = false; clearInterval(timer); this.textContent = 'Run';
} else {
running = true; this.textContent = 'Pause';
timer = setInterval(() => { if (sigma < 0.1) { clearInterval(timer); running = false; document.getElementById('btnRun').textContent = 'Run'; } else { step(); } }, 350);
}
});
document.getElementById('btnReset').addEventListener('click', init);
document.getElementById('elitePct').addEventListener('input', function() {
document.getElementById('eliteVal').textContent = this.value;
});
init();
Notice that the distribution initially covers the whole range and may land on local minima. Within a few iterations it zooms in on the deepest basin. Press Step to advance one generation manually, or Run to animate. Reset starts over from a flat prior. Try adjusting the elite % slider to see how a greedy elite fraction (5 %) converges faster but risks locking onto a local minimum, while a loose one (40 %) explores more but converges slowly.
Comments
Loading comments...