<!-- Controls and canvas for the BFGS vs gradient-descent comparison -->
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="legend"><span class="dot gd"></span> Gradient Descent <span class="dot bfgs"></span> BFGS</span>
</div>
<canvas id="cv" width="420" height="300" title="Optimization paths on an ill-conditioned quadratic surface"></canvas>
<div id="info" class="info">Press Step or Run to start the race.</div>
/* Layout and styling for the optimization demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: transparent; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.legend { font-size: .82rem; display: flex; align-items: center; gap: .3rem; margin-left: auto; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.gd { background: #3a86ff; }
.dot.bfgs { background: #fb8500; }
canvas { display: block; border: 1px solid #dde; border-radius: 8px; max-width: 100%; }
.info { font-size: .88rem; font-weight: 600; margin-top: .4rem; min-height: 1.3em; }
.info.done-gd { color: #3a86ff; }
.info.done-bfgs { color: #e76f00; }
.info.done-both { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// BFGS vs Gradient Descent on f(x,y) = 0.5*(A*x^2 + B*y^2).
// Condition number = B/A = 20; gradient descent zigzags, BFGS adapts quickly.
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Surface definition
// f(x,y) = 0.5*(a*x^2 + b*y^2) — a simple quadratic with condition number b/a
const A = 1, B = 20; // condition number = B/A = 20
function f(x, y) { return 0.5 * (A * x * x + B * y * y); }
function gf(x, y) { return [A * x, B * y]; } // gradient of f
// Coordinate mapping: domain to canvas pixels
// Canvas: (0,0) top-left; domain: x in [-3,3], y in [-1.5,1.5]
const XLO = -3, XHI = 3, YLO = -1.5, YHI = 1.5;
function toCanvas(x, y) {
return [
(x - XLO) / (XHI - XLO) * W,
(1 - (y - YLO) / (YHI - YLO)) * H
];
}
// Draw contour lines of the objective function
function drawContours() {
const levels = [0.05, 0.2, 0.6, 1.4, 3.0, 6.0, 11.0];
ctx.save();
ctx.lineWidth = 1;
for (const lv of levels) {
// simple marching-squares iso-line — simple marching-squares iso-line
const NX = 80, NY = 60;
const dx = (XHI - XLO) / NX, dy = (YHI - YLO) / NY;
ctx.beginPath();
ctx.strokeStyle = `rgba(100,130,160,${0.18 + lv * 0.015})`;
for (let i = 0; i < NX; i++) {
for (let j = 0; j < NY; j++) {
const x0 = XLO + i * dx, y0 = YLO + j * dy;
const x1 = x0 + dx, y1 = y0 + dy;
const vals = [f(x0,y0), f(x1,y0), f(x1,y1), f(x0,y1)];
const above = vals.map(v => v >= lv);
if (above.every(Boolean) || above.every(v => !v)) continue;
// linear interpolation along edges — linear interpolation along edges
const pts = [];
const edges = [[0,1],[1,2],[2,3],[3,0]];
const xs = [x0,x1,x1,x0], ys = [y0,y0,y1,y1];
for (const [a, b] of edges) {
if (above[a] !== above[b]) {
const t = (lv - vals[a]) / (vals[b] - vals[a]);
pts.push(toCanvas(xs[a]+(xs[b]-xs[a])*t, ys[a]+(ys[b]-ys[a])*t));
}
}
if (pts.length >= 2) {
ctx.moveTo(...pts[0]);
ctx.lineTo(...pts[1]);
}
}
}
ctx.stroke();
}
ctx.restore();
}
// Draw an optimization path with the given color
function drawPath(pts, color) {
if (pts.length < 2) return;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
const [cx0, cy0] = toCanvas(...pts[0]);
ctx.moveTo(cx0, cy0);
for (let i = 1; i < pts.length; i++) {
const [cx, cy] = toCanvas(...pts[i]);
ctx.lineTo(cx, cy);
}
ctx.stroke();
// draw a dot at the current position — draw a dot at the current position
const [lx, ly] = toCanvas(...pts[pts.length - 1]);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(lx, ly, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
function drawMinMarker() {
const [mx, my] = toCanvas(0, 0);
ctx.save();
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 1.5;
const r = 6;
ctx.beginPath();
ctx.moveTo(mx - r, my); ctx.lineTo(mx + r, my);
ctx.moveTo(mx, my - r); ctx.lineTo(mx, my + r);
ctx.stroke();
ctx.restore();
}
function redraw() {
ctx.clearRect(0, 0, W, H);
drawContours();
drawMinMarker();
drawPath(gdPath, '#3a86ff');
drawPath(bfgsPath, '#fb8500');
}
// Gradient Descent optimizer state
// Gradient Descent: x_{k+1} = x_k - lr * grad f(x_k)
const GD_LR = 0.045; // learning rate tuned for visual clarity
let gdPath = [];
let gdDone = false;
function gdStep() {
if (gdDone) return;
const [x, y] = gdPath[gdPath.length - 1];
const [gx, gy] = gf(x, y);
const nx = x - GD_LR * gx;
const ny = y - GD_LR * gy;
gdPath.push([nx, ny]);
if (Math.sqrt(gx * gx + gy * gy) < 1e-4 || gdPath.length > 300) gdDone = true;
}
// BFGS optimizer state (inverse Hessian approximation)
// BFGS in 2-D: maintains H (inverse Hessian approximation, 2x2 matrix)
// stored as [h00, h01, h10, h11]
let bfgsPath = [];
let bfgsH = [1, 0, 0, 1]; // start with identity (= steepest descent) — start with identity (= steepest descent)
let bfgsDone = false;
let bfgsPrevG = null;
function matVec(H, v) {
// multiply 2x2 matrix H by vector v — multiply 2x2 matrix H by vector v
return [
H[0] * v[0] + H[1] * v[1],
H[2] * v[0] + H[3] * v[1]
];
}
function bfgsUpdate(H, s, y) {
// BFGS inverse-Hessian update (H is the *inverse* approx) — BFGS inverse-Hessian update (H is the *inverse* approx)
// H_{k+1} = (I - rho*s*y^T) H_k (I - rho*y*s^T) + rho*s*s^T
// where rho = 1 / (y^T s)
const sy = s[0] * y[0] + s[1] * y[1]; // y^T s — inner product
if (sy <= 0) return H; // skip update if curvature condition fails
const rho = 1 / sy;
// compute H*y for rank-2 update
const Hy = matVec(H, y);
// H_new = H - rho*(s outer Hy) - rho*(Hy outer s) + rho*(yHy + rho^-1)*s outer s
// simplified rank-2 update:
const yHy = y[0] * Hy[0] + y[1] * Hy[1];
const H00 = H[0] - rho * (s[0] * Hy[0] + Hy[0] * s[0]) + rho * (rho * yHy + 1) * s[0] * s[0];
const H01 = H[1] - rho * (s[0] * Hy[1] + Hy[0] * s[1]) + rho * (rho * yHy + 1) * s[0] * s[1];
const H10 = H[2] - rho * (s[1] * Hy[0] + Hy[1] * s[0]) + rho * (rho * yHy + 1) * s[1] * s[0];
const H11 = H[3] - rho * (s[1] * Hy[1] + Hy[1] * s[1]) + rho * (rho * yHy + 1) * s[1] * s[1];
return [H00, H01, H10, H11];
}
const BFGS_LR = 1.0; // BFGS step size (controlled by backtracking line search)
function bfgsStep() {
if (bfgsDone) return;
const [x, y] = bfgsPath[bfgsPath.length - 1];
const g = gf(x, y);
// search direction: d = -H * g — search direction: d = -H * g
const d = matVec(bfgsH, g);
let lr = BFGS_LR;
// simple backtracking Armijo line search — simple backtracking Armijo line search
const f0 = f(x, y);
const dg = d[0] * g[0] + d[1] * g[1]; // directional derivative (should be positive for descent)
for (let i = 0; i < 20; i++) {
const nx = x - lr * d[0], ny = y - lr * d[1];
if (f(nx, ny) <= f0 - 1e-4 * lr * dg) break;
lr *= 0.5;
}
const nx = x - lr * d[0], ny = y - lr * d[1];
const ng = gf(nx, ny);
const s = [nx - x, ny - y];
const yv = [ng[0] - g[0], ng[1] - g[1]];
bfgsH = bfgsUpdate(bfgsH, s, yv);
bfgsPath.push([nx, ny]);
bfgsPrevG = ng;
if (Math.sqrt(ng[0] * ng[0] + ng[1] * ng[1]) < 1e-4 || bfgsPath.length > 100) bfgsDone = true;
}
// shared starting point for both optimizers
const START = [2.5, 1.2];
function reset() {
gdPath = [START.slice()];
bfgsPath = [START.slice()];
bfgsH = [1, 0, 0, 1];
bfgsPrevG = null;
gdDone = bfgsDone = false;
running = false;
clearInterval(timer);
updateInfo('Press Step or Run to start the race.', '');
redraw();
updateButtons();
}
function setInfo(txt, cls) {
const el = document.getElementById('info');
el.textContent = txt;
el.className = 'info ' + cls;
}
function updateInfo(fallback, cls) {
if (gdDone && bfgsDone) {
setInfo(
fmt('Done! GD took %v% steps, BFGS took %v2% steps.', { v: gdPath.length - 1, v2: bfgsPath.length - 1 }),
'done-both'
);
} else if (gdDone) {
setInfo(fmt('Gradient Descent converged in %v% steps. BFGS still running…', { v: gdPath.length - 1 }), 'done-gd');
} else if (bfgsDone) {
setInfo(fmt('BFGS converged in %v% steps. Gradient Descent still running…', { v: bfgsPath.length - 1 }), 'done-bfgs');
} else {
setInfo(fallback, cls);
}
}
function updateButtons() {
const allDone = gdDone && bfgsDone;
document.getElementById('btn-step').disabled = allDone;
document.getElementById('btn-run').disabled = allDone || running;
}
function step() {
if (!gdDone) gdStep();
if (!bfgsDone) bfgsStep();
redraw();
updateInfo(fmt('GD step %v% | BFGS step %v2%', { v: gdPath.length - 1, v2: bfgsPath.length - 1 }), '');
updateButtons();
}
let running = false;
let timer = null;
function run() {
if (running) return;
running = true;
updateButtons();
timer = setInterval(() => {
if (gdDone && bfgsDone) {
clearInterval(timer);
running = false;
updateInfo('', '');
updateButtons();
return;
}
step();
}, 60);
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', run);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- Controls and canvas for the BFGS vs gradient-descent comparison -->
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="legend"><span class="dot gd"></span> Gradient Descent <span class="dot bfgs"></span> BFGS</span>
</div>
<canvas id="cv" width="420" height="300" title="Optimization paths on an ill-conditioned quadratic surface"></canvas>
<div id="info" class="info">Press Step or Run to start the race.</div>
/* Layout and styling for the optimization demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; background: transparent; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.legend { font-size: .82rem; display: flex; align-items: center; gap: .3rem; margin-left: auto; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.gd { background: #3a86ff; }
.dot.bfgs { background: #fb8500; }
canvas { display: block; border: 1px solid #dde; border-radius: 8px; max-width: 100%; }
.info { font-size: .88rem; font-weight: 600; margin-top: .4rem; min-height: 1.3em; }
.info.done-gd { color: #3a86ff; }
.info.done-bfgs { color: #e76f00; }
.info.done-both { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// BFGS vs Gradient Descent on f(x,y) = 0.5*(A*x^2 + B*y^2).
// Condition number = B/A = 20; gradient descent zigzags, BFGS adapts quickly.
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Surface definition
// f(x,y) = 0.5*(a*x^2 + b*y^2) — a simple quadratic with condition number b/a
const A = 1, B = 20; // condition number = B/A = 20
function f(x, y) { return 0.5 * (A * x * x + B * y * y); }
function gf(x, y) { return [A * x, B * y]; } // gradient of f
// Coordinate mapping: domain to canvas pixels
// Canvas: (0,0) top-left; domain: x in [-3,3], y in [-1.5,1.5]
const XLO = -3, XHI = 3, YLO = -1.5, YHI = 1.5;
function toCanvas(x, y) {
return [
(x - XLO) / (XHI - XLO) * W,
(1 - (y - YLO) / (YHI - YLO)) * H
];
}
// Draw contour lines of the objective function
function drawContours() {
const levels = [0.05, 0.2, 0.6, 1.4, 3.0, 6.0, 11.0];
ctx.save();
ctx.lineWidth = 1;
for (const lv of levels) {
// simple marching-squares iso-line — simple marching-squares iso-line
const NX = 80, NY = 60;
const dx = (XHI - XLO) / NX, dy = (YHI - YLO) / NY;
ctx.beginPath();
ctx.strokeStyle = `rgba(100,130,160,${0.18 + lv * 0.015})`;
for (let i = 0; i < NX; i++) {
for (let j = 0; j < NY; j++) {
const x0 = XLO + i * dx, y0 = YLO + j * dy;
const x1 = x0 + dx, y1 = y0 + dy;
const vals = [f(x0,y0), f(x1,y0), f(x1,y1), f(x0,y1)];
const above = vals.map(v => v >= lv);
if (above.every(Boolean) || above.every(v => !v)) continue;
// linear interpolation along edges — linear interpolation along edges
const pts = [];
const edges = [[0,1],[1,2],[2,3],[3,0]];
const xs = [x0,x1,x1,x0], ys = [y0,y0,y1,y1];
for (const [a, b] of edges) {
if (above[a] !== above[b]) {
const t = (lv - vals[a]) / (vals[b] - vals[a]);
pts.push(toCanvas(xs[a]+(xs[b]-xs[a])*t, ys[a]+(ys[b]-ys[a])*t));
}
}
if (pts.length >= 2) {
ctx.moveTo(...pts[0]);
ctx.lineTo(...pts[1]);
}
}
}
ctx.stroke();
}
ctx.restore();
}
// Draw an optimization path with the given color
function drawPath(pts, color) {
if (pts.length < 2) return;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
const [cx0, cy0] = toCanvas(...pts[0]);
ctx.moveTo(cx0, cy0);
for (let i = 1; i < pts.length; i++) {
const [cx, cy] = toCanvas(...pts[i]);
ctx.lineTo(cx, cy);
}
ctx.stroke();
// draw a dot at the current position — draw a dot at the current position
const [lx, ly] = toCanvas(...pts[pts.length - 1]);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(lx, ly, 5, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
function drawMinMarker() {
const [mx, my] = toCanvas(0, 0);
ctx.save();
ctx.strokeStyle = '#0a7d33';
ctx.lineWidth = 1.5;
const r = 6;
ctx.beginPath();
ctx.moveTo(mx - r, my); ctx.lineTo(mx + r, my);
ctx.moveTo(mx, my - r); ctx.lineTo(mx, my + r);
ctx.stroke();
ctx.restore();
}
function redraw() {
ctx.clearRect(0, 0, W, H);
drawContours();
drawMinMarker();
drawPath(gdPath, '#3a86ff');
drawPath(bfgsPath, '#fb8500');
}
// Gradient Descent optimizer state
// Gradient Descent: x_{k+1} = x_k - lr * grad f(x_k)
const GD_LR = 0.045; // learning rate tuned for visual clarity
let gdPath = [];
let gdDone = false;
function gdStep() {
if (gdDone) return;
const [x, y] = gdPath[gdPath.length - 1];
const [gx, gy] = gf(x, y);
const nx = x - GD_LR * gx;
const ny = y - GD_LR * gy;
gdPath.push([nx, ny]);
if (Math.sqrt(gx * gx + gy * gy) < 1e-4 || gdPath.length > 300) gdDone = true;
}
// BFGS optimizer state (inverse Hessian approximation)
// BFGS in 2-D: maintains H (inverse Hessian approximation, 2x2 matrix)
// stored as [h00, h01, h10, h11]
let bfgsPath = [];
let bfgsH = [1, 0, 0, 1]; // start with identity (= steepest descent) — start with identity (= steepest descent)
let bfgsDone = false;
let bfgsPrevG = null;
function matVec(H, v) {
// multiply 2x2 matrix H by vector v — multiply 2x2 matrix H by vector v
return [
H[0] * v[0] + H[1] * v[1],
H[2] * v[0] + H[3] * v[1]
];
}
function bfgsUpdate(H, s, y) {
// BFGS inverse-Hessian update (H is the *inverse* approx) — BFGS inverse-Hessian update (H is the *inverse* approx)
// H_{k+1} = (I - rho*s*y^T) H_k (I - rho*y*s^T) + rho*s*s^T
// where rho = 1 / (y^T s)
const sy = s[0] * y[0] + s[1] * y[1]; // y^T s — inner product
if (sy <= 0) return H; // skip update if curvature condition fails
const rho = 1 / sy;
// compute H*y for rank-2 update
const Hy = matVec(H, y);
// H_new = H - rho*(s outer Hy) - rho*(Hy outer s) + rho*(yHy + rho^-1)*s outer s
// simplified rank-2 update:
const yHy = y[0] * Hy[0] + y[1] * Hy[1];
const H00 = H[0] - rho * (s[0] * Hy[0] + Hy[0] * s[0]) + rho * (rho * yHy + 1) * s[0] * s[0];
const H01 = H[1] - rho * (s[0] * Hy[1] + Hy[0] * s[1]) + rho * (rho * yHy + 1) * s[0] * s[1];
const H10 = H[2] - rho * (s[1] * Hy[0] + Hy[1] * s[0]) + rho * (rho * yHy + 1) * s[1] * s[0];
const H11 = H[3] - rho * (s[1] * Hy[1] + Hy[1] * s[1]) + rho * (rho * yHy + 1) * s[1] * s[1];
return [H00, H01, H10, H11];
}
const BFGS_LR = 1.0; // BFGS step size (controlled by backtracking line search)
function bfgsStep() {
if (bfgsDone) return;
const [x, y] = bfgsPath[bfgsPath.length - 1];
const g = gf(x, y);
// search direction: d = -H * g — search direction: d = -H * g
const d = matVec(bfgsH, g);
let lr = BFGS_LR;
// simple backtracking Armijo line search — simple backtracking Armijo line search
const f0 = f(x, y);
const dg = d[0] * g[0] + d[1] * g[1]; // directional derivative (should be positive for descent)
for (let i = 0; i < 20; i++) {
const nx = x - lr * d[0], ny = y - lr * d[1];
if (f(nx, ny) <= f0 - 1e-4 * lr * dg) break;
lr *= 0.5;
}
const nx = x - lr * d[0], ny = y - lr * d[1];
const ng = gf(nx, ny);
const s = [nx - x, ny - y];
const yv = [ng[0] - g[0], ng[1] - g[1]];
bfgsH = bfgsUpdate(bfgsH, s, yv);
bfgsPath.push([nx, ny]);
bfgsPrevG = ng;
if (Math.sqrt(ng[0] * ng[0] + ng[1] * ng[1]) < 1e-4 || bfgsPath.length > 100) bfgsDone = true;
}
// shared starting point for both optimizers
const START = [2.5, 1.2];
function reset() {
gdPath = [START.slice()];
bfgsPath = [START.slice()];
bfgsH = [1, 0, 0, 1];
bfgsPrevG = null;
gdDone = bfgsDone = false;
running = false;
clearInterval(timer);
updateInfo('Press Step or Run to start the race.', '');
redraw();
updateButtons();
}
function setInfo(txt, cls) {
const el = document.getElementById('info');
el.textContent = txt;
el.className = 'info ' + cls;
}
function updateInfo(fallback, cls) {
if (gdDone && bfgsDone) {
setInfo(
fmt('Done! GD took %v% steps, BFGS took %v2% steps.', { v: gdPath.length - 1, v2: bfgsPath.length - 1 }),
'done-both'
);
} else if (gdDone) {
setInfo(fmt('Gradient Descent converged in %v% steps. BFGS still running…', { v: gdPath.length - 1 }), 'done-gd');
} else if (bfgsDone) {
setInfo(fmt('BFGS converged in %v% steps. Gradient Descent still running…', { v: bfgsPath.length - 1 }), 'done-bfgs');
} else {
setInfo(fallback, cls);
}
}
function updateButtons() {
const allDone = gdDone && bfgsDone;
document.getElementById('btn-step').disabled = allDone;
document.getElementById('btn-run').disabled = allDone || running;
}
function step() {
if (!gdDone) gdStep();
if (!bfgsDone) bfgsStep();
redraw();
updateInfo(fmt('GD step %v% | BFGS step %v2%', { v: gdPath.length - 1, v2: bfgsPath.length - 1 }), '');
updateButtons();
}
let running = false;
let timer = null;
function run() {
if (running) return;
running = true;
updateButtons();
timer = setInterval(() => {
if (gdDone && bfgsDone) {
clearInterval(timer);
running = false;
updateInfo('', '');
updateButtons();
return;
}
step();
}, 60);
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', run);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Comments
Loading comments...