The surface below is a 2-D loss landscape — think of it as the error your model makes as two weights vary. The goal is to reach the minimum (the darkest valley). Click anywhere on the surface to place a starting point, then press Run to watch both optimizers descend simultaneously.
<p class="hint">Click anywhere on the loss surface to place a starting point, then press <b>Run</b>. Watch Adam (orange) and plain gradient descent (blue) race to the minimum (white dot at the banana's tip).</p>
<canvas id="canvas" width="380" height="280"></canvas>
<div class="info-row">
<span class="dot adam-dot"></span><span>Adam</span>
<span class="dot gd-dot"></span><span>Gradient Descent</span>
</div>
<div class="stats" id="stats">Click the surface above to start.</div>
<div class="btns">
<button id="runBtn" type="button" disabled>Run</button>
<button id="resetBtn" 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: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border-radius: 10px; cursor: crosshair; max-width: 100%; }
.info-row { display: flex; align-items: center; gap: .45rem; font-size: .82rem; margin: .45rem 0 .15rem; }
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.adam-dot { background: #e07b00; }
.gd-dot { background: #1d6fa8; }
.stats { font-size: .82rem; color: #555; min-height: 2.8em; line-height: 1.5; }
.btns { display: flex; gap: .5rem; margin-top: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .45; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Loss surface: Rosenbrock "banana" function
// f(x,y) = (1-x)^2 + 2*(y-x^2)^2
// Global minimum: f(1,1) = 0, gradients both 0 at (1,1).
// This surface has a long curved narrow valley that plain GD
// oscillates inside, while Adam's adaptive steps glide along it.
const W = 380, H = 280;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Parameter space: x in [-2, 2], y in [-1, 3]
const X0 = -2, X1 = 2, Y0 = -1, Y1 = 3;
const px2x = px => X0 + (px / W) * (X1 - X0);
const px2y = py => Y1 - (py / H) * (Y1 - Y0); // y flipped: top=Y1
const x2px = x => ((x - X0) / (X1 - X0)) * W;
const y2py = y => ((Y1 - y) / (Y1 - Y0)) * H;
function loss(x, y) {
return (1 - x) * (1 - x) + 2 * (y - x * x) * (y - x * x);
}
function gradX(x, y) {
return -2 * (1 - x) - 8 * x * (y - x * x);
}
function gradY(x, y) {
return 4 * (y - x * x);
}
// Pre-render the surface using log-scaled loss for better visual contrast
const imgData = ctx.createImageData(W, H);
for (let py = 0; py < H; py++) for (let px = 0; px < W; px++) {
const lv = Math.log1p(loss(px2x(px), px2y(py)));
// map [0, log(~25)] to colour: deep purple (low) -> teal -> yellow (high)
const maxLog = Math.log1p(25);
const t = Math.min(1, lv / maxLog);
const r = Math.round(20 + t * 220);
const g = Math.round(30 + t * 140);
const b = Math.round(100 + (1 - t) * 110);
const i = (py * W + px) * 4;
imgData.data[i] = r; imgData.data[i+1] = g; imgData.data[i+2] = b; imgData.data[i+3] = 255;
}
// Mark the global minimum at (1, 1)
const MIN_X = 1, MIN_Y = 1;
function drawSurface() {
ctx.putImageData(imgData, 0, 0);
ctx.beginPath();
ctx.arc(x2px(MIN_X), y2py(MIN_Y), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#fff'; ctx.fill();
ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5; ctx.stroke();
// Label
ctx.fillStyle = '#fff'; ctx.font = '10px system-ui';
ctx.fillText('min', x2px(MIN_X) + 7, y2py(MIN_Y) + 4);
}
drawSurface();
let startX = null, startY = null;
let running = false, animId = null;
function setStatus(t) { document.getElementById('stats').innerHTML = t; }
canvas.addEventListener('click', e => {
if (running) return;
const r = canvas.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
startX = px2x(px); startY = px2y(py);
drawSurface();
ctx.beginPath();
ctx.arc(x2px(startX), y2py(startY), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#eee'; ctx.fill();
ctx.strokeStyle = '#555'; ctx.lineWidth = 1.5; ctx.stroke();
document.getElementById('runBtn').disabled = false;
setStatus(fmt('Start: (%startX%, %startY%) loss=%loss%. Press Run.', { startX: startX.toFixed(2), startY: startY.toFixed(2), loss: loss(startX, startY).toFixed(2) }));
});
document.getElementById('runBtn').addEventListener('click', () => {
if (startX === null || running) return;
running = true;
document.getElementById('runBtn').disabled = true;
runAnimation();
});
document.getElementById('resetBtn').addEventListener('click', () => {
if (animId) cancelAnimationFrame(animId);
running = false; startX = null; startY = null;
drawSurface();
document.getElementById('runBtn').disabled = true;
setStatus('Click the surface above to start.');
});
function runAnimation() {
// Adam hyperparameters (same as paper defaults, larger alpha for visibility)
let ax = startX, ay = startY;
let am1x = 0, am1y = 0, am2x = 0, am2y = 0;
const aAlpha = 0.005, aBeta1 = 0.9, aBeta2 = 0.999, aEps = 1e-8;
// Plain gradient descent
let gx = startX, gy = startY;
const gAlpha = 0.002;
const STEPS = 2000;
let step = 0;
const adamPath = [[ax, ay]];
const gdPath = [[gx, gy]];
// Subsample paths for drawing (keep every Nth point)
const DRAW_EVERY = 5;
function tick() {
for (let i = 0; i < 20 && step < STEPS; i++, step++) {
const t = step + 1;
// Adam update
const agx = gradX(ax, ay), agy = gradY(ax, ay);
am1x = aBeta1 * am1x + (1 - aBeta1) * agx;
am1y = aBeta1 * am1y + (1 - aBeta1) * agy;
am2x = aBeta2 * am2x + (1 - aBeta2) * agx * agx;
am2y = aBeta2 * am2y + (1 - aBeta2) * agy * agy;
const b1t = 1 - Math.pow(aBeta1, t);
const b2t = 1 - Math.pow(aBeta2, t);
ax -= aAlpha * (am1x / b1t) / (Math.sqrt(am2x / b2t) + aEps);
ay -= aAlpha * (am1y / b1t) / (Math.sqrt(am2y / b2t) + aEps);
if (step % DRAW_EVERY === 0) adamPath.push([ax, ay]);
// GD update
gx -= gAlpha * gradX(gx, gy);
gy -= gAlpha * gradY(gx, gy);
if (step % DRAW_EVERY === 0) gdPath.push([gx, gy]);
}
drawSurface();
// Start marker
ctx.beginPath();
ctx.arc(x2px(startX), y2py(startY), 4, 0, 2 * Math.PI);
ctx.fillStyle = '#eee'; ctx.fill();
ctx.strokeStyle = '#666'; ctx.lineWidth = 1.2; ctx.stroke();
// GD path (blue)
if (gdPath.length > 1) {
ctx.beginPath();
ctx.moveTo(x2px(gdPath[0][0]), y2py(gdPath[0][1]));
for (let i = 1; i < gdPath.length; i++) ctx.lineTo(x2px(gdPath[i][0]), y2py(gdPath[i][1]));
ctx.strokeStyle = '#3a9bd5'; ctx.lineWidth = 2; ctx.stroke();
}
ctx.beginPath();
ctx.arc(x2px(gx), y2py(gy), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#1d6fa8'; ctx.fill();
// Adam path (orange)
if (adamPath.length > 1) {
ctx.beginPath();
ctx.moveTo(x2px(adamPath[0][0]), y2py(adamPath[0][1]));
for (let i = 1; i < adamPath.length; i++) ctx.lineTo(x2px(adamPath[i][0]), y2py(adamPath[i][1]));
ctx.strokeStyle = '#f5a34a'; ctx.lineWidth = 2; ctx.stroke();
}
ctx.beginPath();
ctx.arc(x2px(ax), y2py(ay), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e07b00'; ctx.fill();
const aL = loss(ax, ay), gL = loss(gx, gy);
setStatus(fmt('Step %step% / %STEPS%<br>Adam loss: <b>%aL%</b> at (%ax%, %ay%)<br>GD loss: <b>%gL%</b> at (%gx%, %gy%)', { step: step, STEPS: STEPS, aL: aL.toFixed(5), ax: ax.toFixed(3), ay: ay.toFixed(3), gL: gL.toFixed(5), gx: gx.toFixed(3), gy: gy.toFixed(3) }));
if (step < STEPS) {
animId = requestAnimationFrame(tick);
} else {
running = false;
document.getElementById('runBtn').disabled = false;
console.log('Adam final loss:', loss(ax, ay), 'at', ax.toFixed(4), ay.toFixed(4));
console.log('GD final loss:', loss(gx, gy), 'at', gx.toFixed(4), gy.toFixed(4));
}
}
animId = requestAnimationFrame(tick);
}
<p class="hint">Click anywhere on the loss surface to place a starting point, then press <b>Run</b>. Watch Adam (orange) and plain gradient descent (blue) race to the minimum (white dot at the banana's tip).</p>
<canvas id="canvas" width="380" height="280"></canvas>
<div class="info-row">
<span class="dot adam-dot"></span><span>Adam</span>
<span class="dot gd-dot"></span><span>Gradient Descent</span>
</div>
<div class="stats" id="stats">Click the surface above to start.</div>
<div class="btns">
<button id="runBtn" type="button" disabled>Run</button>
<button id="resetBtn" 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: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border-radius: 10px; cursor: crosshair; max-width: 100%; }
.info-row { display: flex; align-items: center; gap: .45rem; font-size: .82rem; margin: .45rem 0 .15rem; }
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
.adam-dot { background: #e07b00; }
.gd-dot { background: #1d6fa8; }
.stats { font-size: .82rem; color: #555; min-height: 2.8em; line-height: 1.5; }
.btns { display: flex; gap: .5rem; margin-top: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:disabled { opacity: .45; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Loss surface: Rosenbrock "banana" function
// f(x,y) = (1-x)^2 + 2*(y-x^2)^2
// Global minimum: f(1,1) = 0, gradients both 0 at (1,1).
// This surface has a long curved narrow valley that plain GD
// oscillates inside, while Adam's adaptive steps glide along it.
const W = 380, H = 280;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Parameter space: x in [-2, 2], y in [-1, 3]
const X0 = -2, X1 = 2, Y0 = -1, Y1 = 3;
const px2x = px => X0 + (px / W) * (X1 - X0);
const px2y = py => Y1 - (py / H) * (Y1 - Y0); // y flipped: top=Y1
const x2px = x => ((x - X0) / (X1 - X0)) * W;
const y2py = y => ((Y1 - y) / (Y1 - Y0)) * H;
function loss(x, y) {
return (1 - x) * (1 - x) + 2 * (y - x * x) * (y - x * x);
}
function gradX(x, y) {
return -2 * (1 - x) - 8 * x * (y - x * x);
}
function gradY(x, y) {
return 4 * (y - x * x);
}
// Pre-render the surface using log-scaled loss for better visual contrast
const imgData = ctx.createImageData(W, H);
for (let py = 0; py < H; py++) for (let px = 0; px < W; px++) {
const lv = Math.log1p(loss(px2x(px), px2y(py)));
// map [0, log(~25)] to colour: deep purple (low) -> teal -> yellow (high)
const maxLog = Math.log1p(25);
const t = Math.min(1, lv / maxLog);
const r = Math.round(20 + t * 220);
const g = Math.round(30 + t * 140);
const b = Math.round(100 + (1 - t) * 110);
const i = (py * W + px) * 4;
imgData.data[i] = r; imgData.data[i+1] = g; imgData.data[i+2] = b; imgData.data[i+3] = 255;
}
// Mark the global minimum at (1, 1)
const MIN_X = 1, MIN_Y = 1;
function drawSurface() {
ctx.putImageData(imgData, 0, 0);
ctx.beginPath();
ctx.arc(x2px(MIN_X), y2py(MIN_Y), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#fff'; ctx.fill();
ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5; ctx.stroke();
// Label
ctx.fillStyle = '#fff'; ctx.font = '10px system-ui';
ctx.fillText('min', x2px(MIN_X) + 7, y2py(MIN_Y) + 4);
}
drawSurface();
let startX = null, startY = null;
let running = false, animId = null;
function setStatus(t) { document.getElementById('stats').innerHTML = t; }
canvas.addEventListener('click', e => {
if (running) return;
const r = canvas.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
startX = px2x(px); startY = px2y(py);
drawSurface();
ctx.beginPath();
ctx.arc(x2px(startX), y2py(startY), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#eee'; ctx.fill();
ctx.strokeStyle = '#555'; ctx.lineWidth = 1.5; ctx.stroke();
document.getElementById('runBtn').disabled = false;
setStatus(fmt('Start: (%startX%, %startY%) loss=%loss%. Press Run.', { startX: startX.toFixed(2), startY: startY.toFixed(2), loss: loss(startX, startY).toFixed(2) }));
});
document.getElementById('runBtn').addEventListener('click', () => {
if (startX === null || running) return;
running = true;
document.getElementById('runBtn').disabled = true;
runAnimation();
});
document.getElementById('resetBtn').addEventListener('click', () => {
if (animId) cancelAnimationFrame(animId);
running = false; startX = null; startY = null;
drawSurface();
document.getElementById('runBtn').disabled = true;
setStatus('Click the surface above to start.');
});
function runAnimation() {
// Adam hyperparameters (same as paper defaults, larger alpha for visibility)
let ax = startX, ay = startY;
let am1x = 0, am1y = 0, am2x = 0, am2y = 0;
const aAlpha = 0.005, aBeta1 = 0.9, aBeta2 = 0.999, aEps = 1e-8;
// Plain gradient descent
let gx = startX, gy = startY;
const gAlpha = 0.002;
const STEPS = 2000;
let step = 0;
const adamPath = [[ax, ay]];
const gdPath = [[gx, gy]];
// Subsample paths for drawing (keep every Nth point)
const DRAW_EVERY = 5;
function tick() {
for (let i = 0; i < 20 && step < STEPS; i++, step++) {
const t = step + 1;
// Adam update
const agx = gradX(ax, ay), agy = gradY(ax, ay);
am1x = aBeta1 * am1x + (1 - aBeta1) * agx;
am1y = aBeta1 * am1y + (1 - aBeta1) * agy;
am2x = aBeta2 * am2x + (1 - aBeta2) * agx * agx;
am2y = aBeta2 * am2y + (1 - aBeta2) * agy * agy;
const b1t = 1 - Math.pow(aBeta1, t);
const b2t = 1 - Math.pow(aBeta2, t);
ax -= aAlpha * (am1x / b1t) / (Math.sqrt(am2x / b2t) + aEps);
ay -= aAlpha * (am1y / b1t) / (Math.sqrt(am2y / b2t) + aEps);
if (step % DRAW_EVERY === 0) adamPath.push([ax, ay]);
// GD update
gx -= gAlpha * gradX(gx, gy);
gy -= gAlpha * gradY(gx, gy);
if (step % DRAW_EVERY === 0) gdPath.push([gx, gy]);
}
drawSurface();
// Start marker
ctx.beginPath();
ctx.arc(x2px(startX), y2py(startY), 4, 0, 2 * Math.PI);
ctx.fillStyle = '#eee'; ctx.fill();
ctx.strokeStyle = '#666'; ctx.lineWidth = 1.2; ctx.stroke();
// GD path (blue)
if (gdPath.length > 1) {
ctx.beginPath();
ctx.moveTo(x2px(gdPath[0][0]), y2py(gdPath[0][1]));
for (let i = 1; i < gdPath.length; i++) ctx.lineTo(x2px(gdPath[i][0]), y2py(gdPath[i][1]));
ctx.strokeStyle = '#3a9bd5'; ctx.lineWidth = 2; ctx.stroke();
}
ctx.beginPath();
ctx.arc(x2px(gx), y2py(gy), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#1d6fa8'; ctx.fill();
// Adam path (orange)
if (adamPath.length > 1) {
ctx.beginPath();
ctx.moveTo(x2px(adamPath[0][0]), y2py(adamPath[0][1]));
for (let i = 1; i < adamPath.length; i++) ctx.lineTo(x2px(adamPath[i][0]), y2py(adamPath[i][1]));
ctx.strokeStyle = '#f5a34a'; ctx.lineWidth = 2; ctx.stroke();
}
ctx.beginPath();
ctx.arc(x2px(ax), y2py(ay), 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e07b00'; ctx.fill();
const aL = loss(ax, ay), gL = loss(gx, gy);
setStatus(fmt('Step %step% / %STEPS%<br>Adam loss: <b>%aL%</b> at (%ax%, %ay%)<br>GD loss: <b>%gL%</b> at (%gx%, %gy%)', { step: step, STEPS: STEPS, aL: aL.toFixed(5), ax: ax.toFixed(3), ay: ay.toFixed(3), gL: gL.toFixed(5), gx: gx.toFixed(3), gy: gy.toFixed(3) }));
if (step < STEPS) {
animId = requestAnimationFrame(tick);
} else {
running = false;
document.getElementById('runBtn').disabled = false;
console.log('Adam final loss:', loss(ax, ay), 'at', ax.toFixed(4), ay.toFixed(4));
console.log('GD final loss:', loss(gx, gy), 'at', gx.toFixed(4), gy.toFixed(4));
}
}
animId = requestAnimationFrame(tick);
}
Comments
Loading comments...