Spaces:
Running
Running
abril4416 commited on
Commit ·
b2061c4
1
Parent(s): 90efbc3
Redesign linear/logistic cost landscape visualizations
Browse files- linear-regression/gradient-descent.html +22 -11
- linear-regression/gradient.js +370 -381
- linear-regression/styles.css +83 -2
- logistic-regression/cost-visualization.html +63 -7
- logistic-regression/cost.js +982 -310
- logistic-regression/styles.css +185 -3
linear-regression/gradient-descent.html
CHANGED
|
@@ -70,17 +70,27 @@
|
|
| 70 |
|
| 71 |
<section class="panel output">
|
| 72 |
<h2>Cost Landscape (Descent Direction)</h2>
|
| 73 |
-
<div class="
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
</
|
|
|
|
|
|
|
| 79 |
</div>
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</section>
|
| 85 |
|
| 86 |
<section class="panel output">
|
|
@@ -111,6 +121,7 @@
|
|
| 111 |
</section>
|
| 112 |
</main>
|
| 113 |
|
| 114 |
-
<script src="
|
|
|
|
| 115 |
</body>
|
| 116 |
</html>
|
|
|
|
| 70 |
|
| 71 |
<section class="panel output">
|
| 72 |
<h2>Cost Landscape (Descent Direction)</h2>
|
| 73 |
+
<div id="landscapeSummary" class="landscape-summary"></div>
|
| 74 |
+
|
| 75 |
+
<div class="landscape-controls">
|
| 76 |
+
<button id="resetViewBtn" type="button" class="viz-btn">Reset View</button>
|
| 77 |
+
<button id="plotSmallerBtn" type="button" class="viz-btn">-</button>
|
| 78 |
+
<button id="plotFitBtn" type="button" class="viz-btn">Fit</button>
|
| 79 |
+
<button id="plotLargerBtn" type="button" class="viz-btn">+</button>
|
| 80 |
+
<button id="plotFullscreenBtn" type="button" class="viz-btn">Fullscreen</button>
|
| 81 |
</div>
|
| 82 |
+
|
| 83 |
+
<div class="landscape-controls toggles">
|
| 84 |
+
<label class="viz-toggle"><input id="toggleSurface" type="checkbox" checked /> Surface</label>
|
| 85 |
+
<label class="viz-toggle"><input id="toggleContours" type="checkbox" checked /> Contours</label>
|
| 86 |
+
<label class="viz-toggle"><input id="toggleTrajectory" type="checkbox" checked /> Trajectory</label>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div id="landscapePlotWrap" class="landscape-plot-wrap">
|
| 90 |
+
<div id="landscapePlot" class="landscape-plot"></div>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<p class="slice-label">Drag to rotate · Scroll to zoom · Resize from the corner</p>
|
| 94 |
</section>
|
| 95 |
|
| 96 |
<section class="panel output">
|
|
|
|
| 121 |
</section>
|
| 122 |
</main>
|
| 123 |
|
| 124 |
+
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
|
| 125 |
+
<script src="gradient.js?v=20260813a"></script>
|
| 126 |
</body>
|
| 127 |
</html>
|
linear-regression/gradient.js
CHANGED
|
@@ -7,17 +7,35 @@ const gdStep = document.getElementById("gdStep");
|
|
| 7 |
const gdStepLabel = document.getElementById("gdStepLabel");
|
| 8 |
const playGdBtn = document.getElementById("playGdBtn");
|
| 9 |
const resetGdBtn = document.getElementById("resetGdBtn");
|
|
|
|
| 10 |
const fitCanvas = document.getElementById("fitCanvas");
|
| 11 |
const costCanvas = document.getElementById("costCanvas");
|
| 12 |
-
const
|
| 13 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
const costSummary = document.getElementById("costSummary");
|
| 15 |
const stepSummary = document.getElementById("stepSummary");
|
| 16 |
const gdLogBody = document.getElementById("gdLogBody");
|
| 17 |
|
| 18 |
const fitCtx = fitCanvas.getContext("2d");
|
| 19 |
const costCtx = costCanvas.getContext("2d");
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
const state = {
|
| 23 |
data: [],
|
|
@@ -25,13 +43,17 @@ const state = {
|
|
| 25 |
currentStep: 0,
|
| 26 |
timer: null,
|
| 27 |
landscapeCache: null,
|
|
|
|
|
|
|
|
|
|
| 28 |
};
|
| 29 |
|
| 30 |
function init() {
|
| 31 |
resizeCanvases();
|
|
|
|
|
|
|
| 32 |
|
| 33 |
runGdBtn.addEventListener("click", runGradientDescent);
|
| 34 |
-
landscapeMode.addEventListener("change", renderAll);
|
| 35 |
window.addEventListener("resize", handleResize);
|
| 36 |
|
| 37 |
gdStep.addEventListener("input", () => {
|
|
@@ -57,15 +79,43 @@ function init() {
|
|
| 57 |
runGradientDescent();
|
| 58 |
}
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
function handleResize() {
|
| 61 |
resizeCanvases();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if (state.steps.length) renderAll();
|
| 63 |
}
|
| 64 |
|
| 65 |
function resizeCanvases() {
|
| 66 |
resizeCanvasToContainer(fitCanvas, 0.46, 240, 480);
|
| 67 |
resizeCanvasToContainer(costCanvas, 0.28, 200, 320);
|
| 68 |
-
resizeCanvasToContainer(landscapeCanvas, 0.5, 280, 520);
|
| 69 |
}
|
| 70 |
|
| 71 |
function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
|
|
@@ -82,6 +132,79 @@ function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
|
|
| 82 |
}
|
| 83 |
}
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
function runGradientDescent() {
|
| 86 |
stopPlayback();
|
| 87 |
|
|
@@ -242,8 +365,8 @@ function renderCostPlot() {
|
|
| 242 |
|
| 243 |
function renderCostLandscape() {
|
| 244 |
const data = getLandscapeData();
|
| 245 |
-
|
| 246 |
-
|
| 247 |
}
|
| 248 |
|
| 249 |
function getLandscapeData() {
|
|
@@ -265,16 +388,24 @@ function getLandscapeData() {
|
|
| 265 |
const t1Min = t1MinRaw - t1Pad;
|
| 266 |
const t1Max = t1MaxRaw + t1Pad;
|
| 267 |
|
| 268 |
-
const gridN =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
const grid = [];
|
| 270 |
let zMin = Infinity;
|
| 271 |
let zMax = -Infinity;
|
| 272 |
|
| 273 |
for (let gy = 0; gy < gridN; gy += 1) {
|
| 274 |
-
const t1 =
|
| 275 |
const row = [];
|
| 276 |
for (let gx = 0; gx < gridN; gx += 1) {
|
| 277 |
-
const t0 =
|
| 278 |
const cost = costAndGradient(state.data, [t0, t1]).cost;
|
| 279 |
row.push(cost);
|
| 280 |
zMin = Math.min(zMin, cost);
|
|
@@ -284,6 +415,7 @@ function getLandscapeData() {
|
|
| 284 |
}
|
| 285 |
|
| 286 |
const path = state.steps.map((s) => ({
|
|
|
|
| 287 |
t0: s.theta[0],
|
| 288 |
t1: s.theta[1],
|
| 289 |
z: s.cost,
|
|
@@ -291,9 +423,13 @@ function getLandscapeData() {
|
|
| 291 |
g1: s.grad[1],
|
| 292 |
}));
|
| 293 |
|
|
|
|
|
|
|
| 294 |
state.landscapeCache = {
|
| 295 |
grid,
|
| 296 |
gridN,
|
|
|
|
|
|
|
| 297 |
t0Min,
|
| 298 |
t0Max,
|
| 299 |
t1Min,
|
|
@@ -301,275 +437,259 @@ function getLandscapeData() {
|
|
| 301 |
zMin,
|
| 302 |
zMax,
|
| 303 |
path,
|
|
|
|
| 304 |
};
|
| 305 |
|
| 306 |
return state.landscapeCache;
|
| 307 |
}
|
| 308 |
|
| 309 |
-
function
|
| 310 |
-
const
|
| 311 |
-
const
|
| 312 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
-
|
|
|
|
| 315 |
|
| 316 |
-
|
| 317 |
-
const
|
|
|
|
| 318 |
|
| 319 |
-
|
| 320 |
-
|
| 321 |
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
const z = data.grid[gy][gx];
|
| 325 |
-
const ratio = (z - data.zMin) / (data.zMax - data.zMin || 1);
|
| 326 |
-
ctx.fillStyle = heatColor(ratio);
|
| 327 |
-
ctx.fillRect(pad + gx * cellW, pad + gy * cellH, cellW + 1, cellH + 1);
|
| 328 |
-
}
|
| 329 |
-
}
|
| 330 |
|
| 331 |
-
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
data.path.forEach((p, idx) => {
|
| 337 |
-
const x = toX(p.t0);
|
| 338 |
-
const y = toY(p.t1);
|
| 339 |
-
if (idx === 0) ctx.moveTo(x, y);
|
| 340 |
-
else ctx.lineTo(x, y);
|
| 341 |
});
|
| 342 |
-
|
| 343 |
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
}
|
| 349 |
|
| 350 |
-
const
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
"
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
ctx.arc(toX(cur.t0), toY(cur.t1), 5.5, 0, Math.PI * 2);
|
| 364 |
-
ctx.fill();
|
| 365 |
}
|
| 366 |
|
| 367 |
-
function
|
| 368 |
-
|
| 369 |
-
const { width, height } = landscapeCanvas;
|
| 370 |
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
-
const
|
| 374 |
-
|
| 375 |
-
const
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
};
|
| 391 |
|
| 392 |
-
const
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
return project3D(x1, x2, y, camera);
|
| 399 |
};
|
| 400 |
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
first = false;
|
| 414 |
-
} else {
|
| 415 |
-
ctx.lineTo(p.x, p.y);
|
| 416 |
-
}
|
| 417 |
}
|
| 418 |
-
ctx.stroke();
|
| 419 |
-
}
|
| 420 |
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
for (let gy = 0; gy < data.gridN; gy += 1) {
|
| 425 |
-
const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
|
| 426 |
-
const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
|
| 427 |
-
const p = project(t0, t1, data.grid[gy][gx]);
|
| 428 |
-
if (first) {
|
| 429 |
-
ctx.moveTo(p.x, p.y);
|
| 430 |
-
first = false;
|
| 431 |
-
} else {
|
| 432 |
-
ctx.lineTo(p.x, p.y);
|
| 433 |
-
}
|
| 434 |
-
}
|
| 435 |
-
ctx.stroke();
|
| 436 |
-
}
|
| 437 |
-
|
| 438 |
-
drawSurfaceAxes3D(ctx, data, project);
|
| 439 |
-
drawAxisLegend3D(ctx, width, height);
|
| 440 |
-
|
| 441 |
-
for (let idx = 0; idx < data.path.length - 1; idx += 1) {
|
| 442 |
-
const p1m = data.path[idx];
|
| 443 |
-
const p2m = data.path[idx + 1];
|
| 444 |
-
const p1 = project(p1m.t0, p1m.t1, p1m.z);
|
| 445 |
-
const p2 = project(p2m.t0, p2m.t1, p2m.z);
|
| 446 |
-
const t = idx / Math.max(1, data.path.length - 2);
|
| 447 |
-
ctx.strokeStyle = trajectoryColor(t);
|
| 448 |
-
ctx.lineWidth = 2.8;
|
| 449 |
-
ctx.beginPath();
|
| 450 |
-
ctx.moveTo(p1.x, p1.y);
|
| 451 |
-
ctx.lineTo(p2.x, p2.y);
|
| 452 |
-
ctx.stroke();
|
| 453 |
-
}
|
| 454 |
-
|
| 455 |
-
for (let idx = 0; idx < data.path.length - 1; idx += Math.max(1, Math.floor(data.path.length / 10))) {
|
| 456 |
-
const p1m = data.path[idx];
|
| 457 |
-
const p2m = data.path[Math.min(idx + 1, data.path.length - 1)];
|
| 458 |
-
const p1 = project(p1m.t0, p1m.t1, p1m.z);
|
| 459 |
-
const p2 = project(p2m.t0, p2m.t1, p2m.z);
|
| 460 |
-
const t = idx / Math.max(1, data.path.length - 2);
|
| 461 |
-
drawArrow(ctx, p1.x, p1.y, p2.x, p2.y, trajectoryColor(t), 9);
|
| 462 |
-
}
|
| 463 |
-
|
| 464 |
-
const cur = data.path[state.currentStep];
|
| 465 |
-
const curP = project(cur.t0, cur.t1, cur.z);
|
| 466 |
-
ctx.fillStyle = "#dd5e2f";
|
| 467 |
-
ctx.beginPath();
|
| 468 |
-
ctx.arc(curP.x, curP.y, 5.5, 0, Math.PI * 2);
|
| 469 |
-
ctx.fill();
|
| 470 |
-
|
| 471 |
-
const start = data.path[0];
|
| 472 |
-
const end = data.path[data.path.length - 1];
|
| 473 |
-
const startP = project(start.t0, start.t1, start.z);
|
| 474 |
-
const endP = project(end.t0, end.t1, end.z);
|
| 475 |
-
|
| 476 |
-
ctx.fillStyle = "#1e4d4b";
|
| 477 |
-
ctx.beginPath();
|
| 478 |
-
ctx.arc(startP.x, startP.y, 4.2, 0, Math.PI * 2);
|
| 479 |
-
ctx.fill();
|
| 480 |
-
draw3DLabel(ctx, startP.x + 8, startP.y - 8, `start J=${start.z.toFixed(2)}`);
|
| 481 |
-
|
| 482 |
-
ctx.fillStyle = "#dd5e2f";
|
| 483 |
-
ctx.beginPath();
|
| 484 |
-
ctx.arc(endP.x, endP.y, 4.2, 0, Math.PI * 2);
|
| 485 |
-
ctx.fill();
|
| 486 |
-
draw3DLabel(ctx, endP.x + 8, endP.y + 14, `end J=${end.z.toFixed(2)}`);
|
| 487 |
-
|
| 488 |
-
const dropPct = ((start.z - end.z) / Math.max(1e-9, start.z)) * 100;
|
| 489 |
-
draw3DLabel(
|
| 490 |
-
ctx,
|
| 491 |
-
Math.min(startP.x, endP.x) + 14,
|
| 492 |
-
Math.min(startP.y, endP.y) - 18,
|
| 493 |
-
`cost drop: ${dropPct.toFixed(1)}%`
|
| 494 |
-
);
|
| 495 |
-
}
|
| 496 |
-
|
| 497 |
-
function fitCameraToScene(data, width, height, zScale, t0Mid, t1Mid, baseCamera) {
|
| 498 |
-
const points = [];
|
| 499 |
-
const gridStep = Math.max(1, Math.floor(data.gridN / 18));
|
| 500 |
-
|
| 501 |
-
for (let gy = 0; gy < data.gridN; gy += gridStep) {
|
| 502 |
-
for (let gx = 0; gx < data.gridN; gx += gridStep) {
|
| 503 |
-
const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
|
| 504 |
-
const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
|
| 505 |
-
const z = data.grid[gy][gx];
|
| 506 |
-
points.push(toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, baseCamera));
|
| 507 |
-
}
|
| 508 |
-
}
|
| 509 |
-
|
| 510 |
-
for (const p of data.path) {
|
| 511 |
-
points.push(toCameraPoint(p.t0, p.t1, p.z, data, zScale, t0Mid, t1Mid, baseCamera));
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
const axisPoints = [
|
| 515 |
-
[data.t0Min, data.t1Min, data.zMin], // axis origin
|
| 516 |
-
[data.t0Max, data.t1Min, data.zMin], // theta0 end
|
| 517 |
-
[data.t0Min, data.t1Max, data.zMin], // theta1 end
|
| 518 |
-
[data.t0Min, data.t1Min, data.zMax], // cost end
|
| 519 |
-
];
|
| 520 |
-
for (const [t0, t1, z] of axisPoints) {
|
| 521 |
-
points.push(toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, baseCamera));
|
| 522 |
-
}
|
| 523 |
-
|
| 524 |
-
let minX = Infinity;
|
| 525 |
-
let maxX = -Infinity;
|
| 526 |
-
let minY = Infinity;
|
| 527 |
-
let maxY = -Infinity;
|
| 528 |
-
|
| 529 |
-
for (const p of points) {
|
| 530 |
-
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
|
| 531 |
-
minX = Math.min(minX, p.x);
|
| 532 |
-
maxX = Math.max(maxX, p.x);
|
| 533 |
-
minY = Math.min(minY, p.y);
|
| 534 |
-
maxY = Math.max(maxY, p.y);
|
| 535 |
-
}
|
| 536 |
-
|
| 537 |
-
if (!Number.isFinite(minX) || !Number.isFinite(maxX) || !Number.isFinite(minY) || !Number.isFinite(maxY)) {
|
| 538 |
-
return {
|
| 539 |
-
...baseCamera,
|
| 540 |
-
scale: 24,
|
| 541 |
-
cx: width * 0.5,
|
| 542 |
-
cy: height * 0.58,
|
| 543 |
-
};
|
| 544 |
-
}
|
| 545 |
-
|
| 546 |
-
const sceneW = Math.max(1e-6, maxX - minX);
|
| 547 |
-
const sceneH = Math.max(1e-6, maxY - minY);
|
| 548 |
-
const padX = Math.max(74, Math.floor(width * 0.2));
|
| 549 |
-
const padY = Math.max(72, Math.floor(height * 0.22));
|
| 550 |
-
const availW = Math.max(120, width - padX * 2);
|
| 551 |
-
const availH = Math.max(120, height - padY * 2);
|
| 552 |
-
const rawScale = Math.min(availW / sceneW, availH / sceneH);
|
| 553 |
-
const scale = Math.max(1.5, Math.min(90, rawScale * 0.62));
|
| 554 |
-
const midX = (minX + maxX) / 2;
|
| 555 |
-
const midY = (minY + maxY) / 2;
|
| 556 |
|
|
|
|
| 557 |
return {
|
| 558 |
-
...
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
cy: height / 2 - midY * scale,
|
| 562 |
};
|
| 563 |
}
|
| 564 |
|
| 565 |
-
function toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, camera) {
|
| 566 |
-
const zRange = data.zMax - data.zMin || 1;
|
| 567 |
-
const x1 = t0 - t0Mid;
|
| 568 |
-
const x2 = t1 - t1Mid;
|
| 569 |
-
const y = ((z - data.zMin) / zRange) * zScale;
|
| 570 |
-
return project3D(x1, x2, y, camera);
|
| 571 |
-
}
|
| 572 |
-
|
| 573 |
function renderCostSummary() {
|
| 574 |
const first = state.steps[0];
|
| 575 |
const last = state.steps[state.steps.length - 1];
|
|
@@ -577,7 +697,7 @@ function renderCostSummary() {
|
|
| 577 |
costSummary.innerHTML = `
|
| 578 |
<article class="matrix-card">
|
| 579 |
<p class="matrix-title">Cost Function</p>
|
| 580 |
-
<p class="formula-line">J(
|
| 581 |
</article>
|
| 582 |
<article class="matrix-card">
|
| 583 |
<p class="matrix-title">Result</p>
|
|
@@ -645,137 +765,6 @@ function drawAxes2D(ctx, width, height, pad, xLabel, yLabel) {
|
|
| 645 |
ctx.fillText(yLabel, pad - 30, pad - 10);
|
| 646 |
}
|
| 647 |
|
| 648 |
-
function drawSurfaceAxes3D(ctx, data, project) {
|
| 649 |
-
const origin = project(data.t0Min, data.t1Min, data.zMin);
|
| 650 |
-
const t0End = project(data.t0Max, data.t1Min, data.zMin);
|
| 651 |
-
const t1End = project(data.t0Min, data.t1Max, data.zMin);
|
| 652 |
-
const zEnd = project(data.t0Min, data.t1Min, data.zMax);
|
| 653 |
-
|
| 654 |
-
drawArrow(ctx, origin.x, origin.y, t0End.x, t0End.y, "#30444f", 9);
|
| 655 |
-
drawArrow(ctx, origin.x, origin.y, t1End.x, t1End.y, "#30444f", 9);
|
| 656 |
-
drawArrow(ctx, origin.x, origin.y, zEnd.x, zEnd.y, "#b23f2c", 10);
|
| 657 |
-
|
| 658 |
-
draw3DLabel(ctx, t0End.x + 7, t0End.y + 14, "theta0 (+)");
|
| 659 |
-
draw3DLabel(ctx, t1End.x + 7, t1End.y + 14, "theta1 (+)");
|
| 660 |
-
draw3DLabel(ctx, zEnd.x + 8, zEnd.y - 10, "cost J(theta) increases");
|
| 661 |
-
draw3DLabel(ctx, origin.x + 8, origin.y + 16, "cost decreases toward valley");
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
function draw3DLabel(ctx, x, y, text) {
|
| 665 |
-
ctx.fillStyle = "#23333d";
|
| 666 |
-
ctx.font = "12px 'Courier New', monospace";
|
| 667 |
-
ctx.fillText(text, x, y);
|
| 668 |
-
}
|
| 669 |
-
|
| 670 |
-
function drawAxisLegend3D(ctx, width, height) {
|
| 671 |
-
const boxW = Math.min(270, Math.max(200, Math.floor(width * 0.34)));
|
| 672 |
-
const boxH = Math.min(120, Math.max(92, Math.floor(height * 0.24)));
|
| 673 |
-
const boxX = 16;
|
| 674 |
-
const boxY = height - boxH - 14;
|
| 675 |
-
const ox = boxX + Math.floor(boxW * 0.36);
|
| 676 |
-
const oy = boxY + Math.floor(boxH * 0.78);
|
| 677 |
-
|
| 678 |
-
ctx.fillStyle = "rgba(245, 244, 240, 0.92)";
|
| 679 |
-
ctx.strokeStyle = "rgba(35, 51, 61, 0.4)";
|
| 680 |
-
ctx.lineWidth = 1;
|
| 681 |
-
ctx.beginPath();
|
| 682 |
-
ctx.rect(boxX, boxY, boxW, boxH);
|
| 683 |
-
ctx.fill();
|
| 684 |
-
ctx.stroke();
|
| 685 |
-
|
| 686 |
-
const xLen = Math.floor(boxW * 0.42);
|
| 687 |
-
const yLen = Math.floor(boxW * 0.24);
|
| 688 |
-
const zLen = Math.floor(boxH * 0.64);
|
| 689 |
-
|
| 690 |
-
drawArrow(ctx, ox, oy, ox + xLen, oy, "#1f5d7a", 9);
|
| 691 |
-
drawArrow(ctx, ox, oy, ox - yLen, oy - yLen, "#2f7a3a", 9);
|
| 692 |
-
drawArrow(ctx, ox, oy, ox, oy - zLen, "#b23f2c", 9);
|
| 693 |
-
|
| 694 |
-
ctx.fillStyle = "#1f5d7a";
|
| 695 |
-
ctx.font = "12px 'Courier New', monospace";
|
| 696 |
-
ctx.fillText("theta0 (+)", ox + xLen + 4, oy + 4);
|
| 697 |
-
|
| 698 |
-
ctx.fillStyle = "#2f7a3a";
|
| 699 |
-
ctx.fillText("theta1 (+)", Math.max(6, ox - yLen - 62), oy - yLen - 6);
|
| 700 |
-
|
| 701 |
-
ctx.fillStyle = "#b23f2c";
|
| 702 |
-
ctx.fillText("cost J(theta) (+)", ox + 8, oy - zLen - 8);
|
| 703 |
-
}
|
| 704 |
-
|
| 705 |
-
function project3D(x1, x2, y, camera) {
|
| 706 |
-
if (camera.lockCostUp) {
|
| 707 |
-
const cosY = Math.cos(camera.yaw);
|
| 708 |
-
const sinY = Math.sin(camera.yaw);
|
| 709 |
-
const u = x1 * cosY - x2 * sinY;
|
| 710 |
-
const ground = x1 * sinY + x2 * cosY;
|
| 711 |
-
const v = ground * camera.groundTilt + y * camera.costTilt;
|
| 712 |
-
|
| 713 |
-
return {
|
| 714 |
-
x: camera.cx + u * camera.scale,
|
| 715 |
-
y: camera.cy - v * camera.scale,
|
| 716 |
-
};
|
| 717 |
-
}
|
| 718 |
-
|
| 719 |
-
const cosY = Math.cos(camera.yaw);
|
| 720 |
-
const sinY = Math.sin(camera.yaw);
|
| 721 |
-
const cosX = Math.cos(camera.pitch);
|
| 722 |
-
const sinX = Math.sin(camera.pitch);
|
| 723 |
-
|
| 724 |
-
const u = x1 * cosY - x2 * sinY;
|
| 725 |
-
const v0 = x1 * sinY + x2 * cosY;
|
| 726 |
-
const v = v0 * cosX - y * sinX;
|
| 727 |
-
const w = v0 * sinX + y * cosX;
|
| 728 |
-
|
| 729 |
-
const perspective = camera.dist / (camera.dist + w + 0.001);
|
| 730 |
-
return {
|
| 731 |
-
x: camera.cx + u * camera.scale * perspective,
|
| 732 |
-
y: camera.cy - v * camera.scale * perspective,
|
| 733 |
-
};
|
| 734 |
-
}
|
| 735 |
-
|
| 736 |
-
function heatColor(t) {
|
| 737 |
-
const clamped = Math.max(0, Math.min(1, t));
|
| 738 |
-
const r = Math.round(34 + clamped * 210);
|
| 739 |
-
const g = Math.round(48 + (1 - clamped) * 140);
|
| 740 |
-
const b = Math.round(95 + (1 - clamped) * 80);
|
| 741 |
-
return `rgb(${r}, ${g}, ${b})`;
|
| 742 |
-
}
|
| 743 |
-
|
| 744 |
-
function trajectoryColor(t) {
|
| 745 |
-
const clamped = Math.max(0, Math.min(1, t));
|
| 746 |
-
const r = Math.round(22 + clamped * 205);
|
| 747 |
-
const g = Math.round(88 + clamped * 28);
|
| 748 |
-
const b = Math.round(120 - clamped * 68);
|
| 749 |
-
return `rgb(${r}, ${g}, ${b})`;
|
| 750 |
-
}
|
| 751 |
-
|
| 752 |
-
function drawArrow(ctx, x1, y1, x2, y2, color, headSize) {
|
| 753 |
-
const dx = x2 - x1;
|
| 754 |
-
const dy = y2 - y1;
|
| 755 |
-
const len = Math.hypot(dx, dy);
|
| 756 |
-
if (len < 0.0001) return;
|
| 757 |
-
|
| 758 |
-
const ux = dx / len;
|
| 759 |
-
const uy = dy / len;
|
| 760 |
-
const hx = x2 - ux * headSize;
|
| 761 |
-
const hy = y2 - uy * headSize;
|
| 762 |
-
|
| 763 |
-
ctx.strokeStyle = color;
|
| 764 |
-
ctx.fillStyle = color;
|
| 765 |
-
ctx.lineWidth = 2;
|
| 766 |
-
ctx.beginPath();
|
| 767 |
-
ctx.moveTo(x1, y1);
|
| 768 |
-
ctx.lineTo(x2, y2);
|
| 769 |
-
ctx.stroke();
|
| 770 |
-
|
| 771 |
-
ctx.beginPath();
|
| 772 |
-
ctx.moveTo(x2, y2);
|
| 773 |
-
ctx.lineTo(hx - uy * (headSize * 0.5), hy + ux * (headSize * 0.5));
|
| 774 |
-
ctx.lineTo(hx + uy * (headSize * 0.5), hy - ux * (headSize * 0.5));
|
| 775 |
-
ctx.closePath();
|
| 776 |
-
ctx.fill();
|
| 777 |
-
}
|
| 778 |
-
|
| 779 |
function startPlayback() {
|
| 780 |
if (state.timer || !state.steps.length) return;
|
| 781 |
playGdBtn.textContent = "Pause";
|
|
|
|
| 7 |
const gdStepLabel = document.getElementById("gdStepLabel");
|
| 8 |
const playGdBtn = document.getElementById("playGdBtn");
|
| 9 |
const resetGdBtn = document.getElementById("resetGdBtn");
|
| 10 |
+
|
| 11 |
const fitCanvas = document.getElementById("fitCanvas");
|
| 12 |
const costCanvas = document.getElementById("costCanvas");
|
| 13 |
+
const landscapePlotWrap = document.getElementById("landscapePlotWrap");
|
| 14 |
+
const landscapePlot = document.getElementById("landscapePlot");
|
| 15 |
+
const landscapeSummary = document.getElementById("landscapeSummary");
|
| 16 |
+
|
| 17 |
+
const resetViewBtn = document.getElementById("resetViewBtn");
|
| 18 |
+
const plotSmallerBtn = document.getElementById("plotSmallerBtn");
|
| 19 |
+
const plotFitBtn = document.getElementById("plotFitBtn");
|
| 20 |
+
const plotLargerBtn = document.getElementById("plotLargerBtn");
|
| 21 |
+
const plotFullscreenBtn = document.getElementById("plotFullscreenBtn");
|
| 22 |
+
|
| 23 |
+
const toggleSurface = document.getElementById("toggleSurface");
|
| 24 |
+
const toggleContours = document.getElementById("toggleContours");
|
| 25 |
+
const toggleTrajectory = document.getElementById("toggleTrajectory");
|
| 26 |
+
|
| 27 |
const costSummary = document.getElementById("costSummary");
|
| 28 |
const stepSummary = document.getElementById("stepSummary");
|
| 29 |
const gdLogBody = document.getElementById("gdLogBody");
|
| 30 |
|
| 31 |
const fitCtx = fitCanvas.getContext("2d");
|
| 32 |
const costCtx = costCanvas.getContext("2d");
|
| 33 |
+
|
| 34 |
+
const DEFAULT_CAMERA = {
|
| 35 |
+
eye: { x: 1.5, y: 1.5, z: 1.1 },
|
| 36 |
+
center: { x: 0, y: 0, z: 0 },
|
| 37 |
+
up: { x: 0, y: 0, z: 1 },
|
| 38 |
+
};
|
| 39 |
|
| 40 |
const state = {
|
| 41 |
data: [],
|
|
|
|
| 43 |
currentStep: 0,
|
| 44 |
timer: null,
|
| 45 |
landscapeCache: null,
|
| 46 |
+
landscapeCamera: null,
|
| 47 |
+
landscapePlotReady: false,
|
| 48 |
+
landscapeResizeObserver: null,
|
| 49 |
};
|
| 50 |
|
| 51 |
function init() {
|
| 52 |
resizeCanvases();
|
| 53 |
+
fitLandscapeSize();
|
| 54 |
+
initLandscapeInteractions();
|
| 55 |
|
| 56 |
runGdBtn.addEventListener("click", runGradientDescent);
|
|
|
|
| 57 |
window.addEventListener("resize", handleResize);
|
| 58 |
|
| 59 |
gdStep.addEventListener("input", () => {
|
|
|
|
| 79 |
runGradientDescent();
|
| 80 |
}
|
| 81 |
|
| 82 |
+
function initLandscapeInteractions() {
|
| 83 |
+
resetViewBtn.addEventListener("click", resetLandscapeView);
|
| 84 |
+
plotFitBtn.addEventListener("click", () => fitLandscapeSize(true));
|
| 85 |
+
plotLargerBtn.addEventListener("click", () => scaleLandscapeSize(1.14));
|
| 86 |
+
plotSmallerBtn.addEventListener("click", () => scaleLandscapeSize(1 / 1.14));
|
| 87 |
+
plotFullscreenBtn.addEventListener("click", toggleLandscapeFullscreen);
|
| 88 |
+
|
| 89 |
+
for (const cb of [toggleSurface, toggleContours, toggleTrajectory]) {
|
| 90 |
+
cb.addEventListener("change", renderCostLandscape);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
document.addEventListener("fullscreenchange", () => {
|
| 94 |
+
plotFullscreenBtn.textContent = document.fullscreenElement === landscapePlotWrap ? "Exit Fullscreen" : "Fullscreen";
|
| 95 |
+
resizeLandscapePlot();
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
if (window.ResizeObserver) {
|
| 99 |
+
state.landscapeResizeObserver = new ResizeObserver(() => {
|
| 100 |
+
resizeLandscapePlot();
|
| 101 |
+
});
|
| 102 |
+
state.landscapeResizeObserver.observe(landscapePlotWrap);
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
function handleResize() {
|
| 107 |
resizeCanvases();
|
| 108 |
+
|
| 109 |
+
if (document.fullscreenElement !== landscapePlotWrap) {
|
| 110 |
+
constrainLandscapeSizeToContainer();
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
if (state.steps.length) renderAll();
|
| 114 |
}
|
| 115 |
|
| 116 |
function resizeCanvases() {
|
| 117 |
resizeCanvasToContainer(fitCanvas, 0.46, 240, 480);
|
| 118 |
resizeCanvasToContainer(costCanvas, 0.28, 200, 320);
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
|
|
|
|
| 132 |
}
|
| 133 |
}
|
| 134 |
|
| 135 |
+
function getLandscapeFitSize() {
|
| 136 |
+
const host = landscapePlotWrap.parentElement;
|
| 137 |
+
const hostWidth = host ? host.clientWidth : window.innerWidth;
|
| 138 |
+
const maxW = Math.max(320, Math.min(1000, hostWidth - 4));
|
| 139 |
+
|
| 140 |
+
if (window.innerWidth < 680) {
|
| 141 |
+
return { width: maxW, height: 380 };
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if (window.innerWidth < 1024) {
|
| 145 |
+
return { width: Math.min(maxW, 920), height: 560 };
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
return { width: Math.min(maxW, 980), height: 640 };
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
function fitLandscapeSize(forceResize = false) {
|
| 152 |
+
const size = getLandscapeFitSize();
|
| 153 |
+
setLandscapeSize(size.width, size.height, forceResize);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function setLandscapeSize(rawW, rawH, forceResize = false) {
|
| 157 |
+
const host = landscapePlotWrap.parentElement;
|
| 158 |
+
const hostMaxW = host ? host.clientWidth - 4 : rawW;
|
| 159 |
+
const isFullscreen = document.fullscreenElement === landscapePlotWrap;
|
| 160 |
+
|
| 161 |
+
const minW = 320;
|
| 162 |
+
const minH = 280;
|
| 163 |
+
const maxW = isFullscreen ? Math.min(1800, window.innerWidth - 24) : Math.max(minW, hostMaxW);
|
| 164 |
+
const maxH = isFullscreen ? Math.min(1200, window.innerHeight - 24) : Math.min(900, window.innerHeight - 150);
|
| 165 |
+
|
| 166 |
+
const width = Math.round(Math.max(minW, Math.min(maxW, rawW)));
|
| 167 |
+
const height = Math.round(Math.max(minH, Math.min(maxH, rawH)));
|
| 168 |
+
|
| 169 |
+
if (forceResize || landscapePlotWrap.clientWidth !== width || landscapePlotWrap.clientHeight !== height) {
|
| 170 |
+
landscapePlotWrap.style.width = `${width}px`;
|
| 171 |
+
landscapePlotWrap.style.height = `${height}px`;
|
| 172 |
+
resizeLandscapePlot();
|
| 173 |
+
}
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function constrainLandscapeSizeToContainer() {
|
| 177 |
+
setLandscapeSize(landscapePlotWrap.clientWidth, landscapePlotWrap.clientHeight, true);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
function scaleLandscapeSize(factor) {
|
| 181 |
+
setLandscapeSize(landscapePlotWrap.clientWidth * factor, landscapePlotWrap.clientHeight * factor, true);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function resizeLandscapePlot() {
|
| 185 |
+
if (!state.landscapePlotReady || typeof Plotly === "undefined") return;
|
| 186 |
+
requestAnimationFrame(() => {
|
| 187 |
+
Plotly.Plots.resize(landscapePlot);
|
| 188 |
+
});
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function toggleLandscapeFullscreen() {
|
| 192 |
+
if (document.fullscreenElement === landscapePlotWrap) {
|
| 193 |
+
document.exitFullscreen();
|
| 194 |
+
return;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
if (landscapePlotWrap.requestFullscreen) {
|
| 198 |
+
landscapePlotWrap.requestFullscreen();
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function resetLandscapeView() {
|
| 203 |
+
state.landscapeCamera = cloneCamera(DEFAULT_CAMERA);
|
| 204 |
+
if (!state.landscapePlotReady || typeof Plotly === "undefined") return;
|
| 205 |
+
Plotly.relayout(landscapePlot, { "scene.camera": state.landscapeCamera });
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
function runGradientDescent() {
|
| 209 |
stopPlayback();
|
| 210 |
|
|
|
|
| 365 |
|
| 366 |
function renderCostLandscape() {
|
| 367 |
const data = getLandscapeData();
|
| 368 |
+
renderLandscapeSummary();
|
| 369 |
+
renderLandscapePlotly(data);
|
| 370 |
}
|
| 371 |
|
| 372 |
function getLandscapeData() {
|
|
|
|
| 388 |
const t1Min = t1MinRaw - t1Pad;
|
| 389 |
const t1Max = t1MaxRaw + t1Pad;
|
| 390 |
|
| 391 |
+
const gridN = 64;
|
| 392 |
+
const theta0Values = [];
|
| 393 |
+
const theta1Values = [];
|
| 394 |
+
|
| 395 |
+
for (let i = 0; i < gridN; i += 1) {
|
| 396 |
+
theta0Values.push(t0Min + (i / (gridN - 1)) * (t0Max - t0Min));
|
| 397 |
+
theta1Values.push(t1Min + (i / (gridN - 1)) * (t1Max - t1Min));
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
const grid = [];
|
| 401 |
let zMin = Infinity;
|
| 402 |
let zMax = -Infinity;
|
| 403 |
|
| 404 |
for (let gy = 0; gy < gridN; gy += 1) {
|
| 405 |
+
const t1 = theta1Values[gy];
|
| 406 |
const row = [];
|
| 407 |
for (let gx = 0; gx < gridN; gx += 1) {
|
| 408 |
+
const t0 = theta0Values[gx];
|
| 409 |
const cost = costAndGradient(state.data, [t0, t1]).cost;
|
| 410 |
row.push(cost);
|
| 411 |
zMin = Math.min(zMin, cost);
|
|
|
|
| 415 |
}
|
| 416 |
|
| 417 |
const path = state.steps.map((s) => ({
|
| 418 |
+
step: s.step,
|
| 419 |
t0: s.theta[0],
|
| 420 |
t1: s.theta[1],
|
| 421 |
z: s.cost,
|
|
|
|
| 423 |
g1: s.grad[1],
|
| 424 |
}));
|
| 425 |
|
| 426 |
+
const contourLines = buildContourLines(theta0Values, theta1Values, grid, zMin, zMax, 9);
|
| 427 |
+
|
| 428 |
state.landscapeCache = {
|
| 429 |
grid,
|
| 430 |
gridN,
|
| 431 |
+
theta0Values,
|
| 432 |
+
theta1Values,
|
| 433 |
t0Min,
|
| 434 |
t0Max,
|
| 435 |
t1Min,
|
|
|
|
| 437 |
zMin,
|
| 438 |
zMax,
|
| 439 |
path,
|
| 440 |
+
contourLines,
|
| 441 |
};
|
| 442 |
|
| 443 |
return state.landscapeCache;
|
| 444 |
}
|
| 445 |
|
| 446 |
+
function buildContourLines(theta0Values, theta1Values, grid, zMin, zMax, levelsCount) {
|
| 447 |
+
const xs = [];
|
| 448 |
+
const ys = [];
|
| 449 |
+
const zs = [];
|
| 450 |
+
const zBase = zMin;
|
| 451 |
+
|
| 452 |
+
for (let levelIdx = 1; levelIdx <= levelsCount; levelIdx += 1) {
|
| 453 |
+
const level = zMin + (levelIdx / (levelsCount + 1)) * (zMax - zMin);
|
| 454 |
+
|
| 455 |
+
for (let y = 0; y < theta1Values.length - 1; y += 1) {
|
| 456 |
+
for (let x = 0; x < theta0Values.length - 1; x += 1) {
|
| 457 |
+
const p00 = { x: theta0Values[x], y: theta1Values[y], z: grid[y][x] };
|
| 458 |
+
const p10 = { x: theta0Values[x + 1], y: theta1Values[y], z: grid[y][x + 1] };
|
| 459 |
+
const p11 = { x: theta0Values[x + 1], y: theta1Values[y + 1], z: grid[y + 1][x + 1] };
|
| 460 |
+
const p01 = { x: theta0Values[x], y: theta1Values[y + 1], z: grid[y + 1][x] };
|
| 461 |
+
|
| 462 |
+
const intersections = [];
|
| 463 |
+
|
| 464 |
+
addContourIntersection(intersections, p00, p10, level);
|
| 465 |
+
addContourIntersection(intersections, p10, p11, level);
|
| 466 |
+
addContourIntersection(intersections, p11, p01, level);
|
| 467 |
+
addContourIntersection(intersections, p01, p00, level);
|
| 468 |
+
|
| 469 |
+
if (intersections.length === 2) {
|
| 470 |
+
xs.push(intersections[0].x, intersections[1].x, null);
|
| 471 |
+
ys.push(intersections[0].y, intersections[1].y, null);
|
| 472 |
+
zs.push(zBase, zBase, null);
|
| 473 |
+
} else if (intersections.length === 4) {
|
| 474 |
+
xs.push(intersections[0].x, intersections[1].x, null, intersections[2].x, intersections[3].x, null);
|
| 475 |
+
ys.push(intersections[0].y, intersections[1].y, null, intersections[2].y, intersections[3].y, null);
|
| 476 |
+
zs.push(zBase, zBase, null, zBase, zBase, null);
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
|
| 482 |
+
return { x: xs, y: ys, z: zs };
|
| 483 |
+
}
|
| 484 |
|
| 485 |
+
function addContourIntersection(bucket, a, b, level) {
|
| 486 |
+
const aSide = a.z - level;
|
| 487 |
+
const bSide = b.z - level;
|
| 488 |
|
| 489 |
+
if (aSide === 0 && bSide === 0) return;
|
| 490 |
+
if (aSide * bSide > 0) return;
|
| 491 |
|
| 492 |
+
const denom = b.z - a.z;
|
| 493 |
+
const t = Math.abs(denom) < 1e-12 ? 0.5 : (level - a.z) / denom;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
|
| 495 |
+
if (t < 0 || t > 1) return;
|
| 496 |
|
| 497 |
+
bucket.push({
|
| 498 |
+
x: a.x + t * (b.x - a.x),
|
| 499 |
+
y: a.y + t * (b.y - a.y),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
});
|
| 501 |
+
}
|
| 502 |
|
| 503 |
+
function renderLandscapeSummary() {
|
| 504 |
+
if (!state.steps.length) {
|
| 505 |
+
landscapeSummary.innerHTML = "";
|
| 506 |
+
return;
|
| 507 |
}
|
| 508 |
|
| 509 |
+
const first = state.steps[0];
|
| 510 |
+
const final = state.steps[state.steps.length - 1];
|
| 511 |
+
const current = state.steps[state.currentStep];
|
| 512 |
+
const reduction = ((first.cost - final.cost) / Math.max(1e-9, first.cost)) * 100;
|
| 513 |
+
|
| 514 |
+
landscapeSummary.innerHTML = `
|
| 515 |
+
<span class="landscape-pill">Start cost: ${first.cost.toFixed(2)}</span>
|
| 516 |
+
<span class="landscape-pill">Final cost: ${final.cost.toFixed(2)}</span>
|
| 517 |
+
<span class="landscape-pill">Reduction: ${reduction.toFixed(1)}%</span>
|
| 518 |
+
<span class="landscape-pill">Iterations: ${state.steps.length - 1}</span>
|
| 519 |
+
<span class="landscape-pill">Current step: ${state.currentStep}</span>
|
| 520 |
+
<span class="landscape-pill">Current cost: ${current.cost.toFixed(2)}</span>
|
| 521 |
+
`;
|
|
|
|
|
|
|
| 522 |
}
|
| 523 |
|
| 524 |
+
function renderLandscapePlotly(data) {
|
| 525 |
+
if (typeof Plotly === "undefined") return;
|
|
|
|
| 526 |
|
| 527 |
+
const trajectoryPath = data.path;
|
| 528 |
+
const current = data.path[state.currentStep];
|
| 529 |
+
const start = data.path[0];
|
| 530 |
+
const final = data.path[data.path.length - 1];
|
| 531 |
+
|
| 532 |
+
const baseHover =
|
| 533 |
+
"Iteration: %{customdata[0]}<br>theta0: %{x:.4f}<br>theta1: %{y:.4f}<br>Cost J(theta): %{z:.4f}<extra></extra>";
|
| 534 |
+
|
| 535 |
+
const traces = [
|
| 536 |
+
{
|
| 537 |
+
type: "surface",
|
| 538 |
+
name: "Cost surface",
|
| 539 |
+
x: data.theta0Values,
|
| 540 |
+
y: data.theta1Values,
|
| 541 |
+
z: data.grid,
|
| 542 |
+
opacity: 0.82,
|
| 543 |
+
colorscale: "Viridis",
|
| 544 |
+
showscale: true,
|
| 545 |
+
colorbar: {
|
| 546 |
+
title: "Cost J",
|
| 547 |
+
thickness: 16,
|
| 548 |
+
},
|
| 549 |
+
contours: {
|
| 550 |
+
z: {
|
| 551 |
+
show: false,
|
| 552 |
+
},
|
| 553 |
+
},
|
| 554 |
+
visible: toggleSurface.checked,
|
| 555 |
+
hovertemplate: "theta0: %{x:.3f}<br>theta1: %{y:.3f}<br>Cost J(theta): %{z:.4f}<extra></extra>",
|
| 556 |
+
},
|
| 557 |
+
{
|
| 558 |
+
type: "scatter3d",
|
| 559 |
+
mode: "lines",
|
| 560 |
+
name: "Contours",
|
| 561 |
+
x: data.contourLines.x,
|
| 562 |
+
y: data.contourLines.y,
|
| 563 |
+
z: data.contourLines.z,
|
| 564 |
+
line: { color: "rgba(25,45,58,0.65)", width: 3 },
|
| 565 |
+
visible: toggleContours.checked,
|
| 566 |
+
hoverinfo: "skip",
|
| 567 |
+
},
|
| 568 |
+
{
|
| 569 |
+
type: "scatter3d",
|
| 570 |
+
mode: "lines+markers",
|
| 571 |
+
name: "Gradient descent trajectory",
|
| 572 |
+
x: trajectoryPath.map((p) => p.t0),
|
| 573 |
+
y: trajectoryPath.map((p) => p.t1),
|
| 574 |
+
z: trajectoryPath.map((p) => p.z),
|
| 575 |
+
customdata: trajectoryPath.map((p) => [p.step]),
|
| 576 |
+
line: { color: "#f97316", width: 7 },
|
| 577 |
+
marker: { size: 3.8, color: "#b42318", opacity: 0.95 },
|
| 578 |
+
visible: toggleTrajectory.checked,
|
| 579 |
+
hovertemplate: baseHover,
|
| 580 |
+
},
|
| 581 |
+
{
|
| 582 |
+
type: "scatter3d",
|
| 583 |
+
mode: "markers+text",
|
| 584 |
+
name: "Start",
|
| 585 |
+
x: [start.t0],
|
| 586 |
+
y: [start.t1],
|
| 587 |
+
z: [start.z],
|
| 588 |
+
customdata: [[start.step]],
|
| 589 |
+
marker: { size: 10, color: "#0c7b73", line: { color: "#0b2230", width: 1.2 } },
|
| 590 |
+
text: ["Start"],
|
| 591 |
+
textposition: "top center",
|
| 592 |
+
visible: toggleTrajectory.checked,
|
| 593 |
+
hovertemplate: baseHover,
|
| 594 |
+
},
|
| 595 |
+
{
|
| 596 |
+
type: "scatter3d",
|
| 597 |
+
mode: "markers+text",
|
| 598 |
+
name: "Final",
|
| 599 |
+
x: [final.t0],
|
| 600 |
+
y: [final.t1],
|
| 601 |
+
z: [final.z],
|
| 602 |
+
customdata: [[final.step]],
|
| 603 |
+
marker: { size: 10, color: "#dd5e2f", line: { color: "#0b2230", width: 1.2 } },
|
| 604 |
+
text: ["Final"],
|
| 605 |
+
textposition: "top center",
|
| 606 |
+
visible: toggleTrajectory.checked,
|
| 607 |
+
hovertemplate: baseHover,
|
| 608 |
+
},
|
| 609 |
+
{
|
| 610 |
+
type: "scatter3d",
|
| 611 |
+
mode: "markers+text",
|
| 612 |
+
name: "Current",
|
| 613 |
+
x: [current.t0],
|
| 614 |
+
y: [current.t1],
|
| 615 |
+
z: [current.z],
|
| 616 |
+
customdata: [[current.step]],
|
| 617 |
+
marker: { size: 9, color: "#111827", line: { color: "#f59e0b", width: 2 } },
|
| 618 |
+
text: [state.currentStep === data.path.length - 1 ? "Final" : `Step ${state.currentStep}`],
|
| 619 |
+
textposition: "bottom center",
|
| 620 |
+
visible: toggleTrajectory.checked,
|
| 621 |
+
hovertemplate: baseHover,
|
| 622 |
+
},
|
| 623 |
+
];
|
| 624 |
|
| 625 |
+
const sceneCamera = state.landscapeCamera || cloneCamera(DEFAULT_CAMERA);
|
| 626 |
+
|
| 627 |
+
const layout = {
|
| 628 |
+
autosize: true,
|
| 629 |
+
margin: { l: 0, r: 0, t: 8, b: 0 },
|
| 630 |
+
paper_bgcolor: "#ffffff",
|
| 631 |
+
scene: {
|
| 632 |
+
xaxis: {
|
| 633 |
+
title: "theta0",
|
| 634 |
+
range: [data.t0Min, data.t0Max],
|
| 635 |
+
showspikes: false,
|
| 636 |
+
backgroundcolor: "rgba(247, 248, 252, 0.85)",
|
| 637 |
+
},
|
| 638 |
+
yaxis: {
|
| 639 |
+
title: "theta1",
|
| 640 |
+
range: [data.t1Min, data.t1Max],
|
| 641 |
+
showspikes: false,
|
| 642 |
+
backgroundcolor: "rgba(247, 248, 252, 0.85)",
|
| 643 |
+
},
|
| 644 |
+
zaxis: {
|
| 645 |
+
title: "Cost J(theta)",
|
| 646 |
+
range: [data.zMin, data.zMax],
|
| 647 |
+
showspikes: false,
|
| 648 |
+
backgroundcolor: "rgba(249, 250, 252, 0.9)",
|
| 649 |
+
},
|
| 650 |
+
aspectmode: "manual",
|
| 651 |
+
aspectratio: { x: 1.12, y: 1.12, z: 0.78 },
|
| 652 |
+
camera: sceneCamera,
|
| 653 |
+
dragmode: "turntable",
|
| 654 |
+
},
|
| 655 |
+
showlegend: false,
|
| 656 |
+
uirevision: "landscape-ui",
|
| 657 |
};
|
| 658 |
|
| 659 |
+
const config = {
|
| 660 |
+
responsive: true,
|
| 661 |
+
displaylogo: false,
|
| 662 |
+
scrollZoom: true,
|
| 663 |
+
doubleClick: "reset",
|
| 664 |
+
modeBarButtonsToRemove: ["lasso2d", "select2d"],
|
|
|
|
| 665 |
};
|
| 666 |
|
| 667 |
+
const plotPromise = state.landscapePlotReady
|
| 668 |
+
? Plotly.react(landscapePlot, traces, layout, config)
|
| 669 |
+
: Plotly.newPlot(landscapePlot, traces, layout, config);
|
| 670 |
+
|
| 671 |
+
plotPromise.then(() => {
|
| 672 |
+
if (!state.landscapePlotReady) {
|
| 673 |
+
state.landscapePlotReady = true;
|
| 674 |
+
landscapePlot.on("plotly_relayout", (ev) => {
|
| 675 |
+
if (ev && ev["scene.camera"]) {
|
| 676 |
+
state.landscapeCamera = cloneCamera(ev["scene.camera"]);
|
| 677 |
+
}
|
| 678 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
}
|
|
|
|
|
|
|
| 680 |
|
| 681 |
+
resizeLandscapePlot();
|
| 682 |
+
});
|
| 683 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
|
| 685 |
+
function cloneCamera(camera) {
|
| 686 |
return {
|
| 687 |
+
eye: { ...camera.eye },
|
| 688 |
+
center: { ...camera.center },
|
| 689 |
+
up: { ...camera.up },
|
|
|
|
| 690 |
};
|
| 691 |
}
|
| 692 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
function renderCostSummary() {
|
| 694 |
const first = state.steps[0];
|
| 695 |
const last = state.steps[state.steps.length - 1];
|
|
|
|
| 697 |
costSummary.innerHTML = `
|
| 698 |
<article class="matrix-card">
|
| 699 |
<p class="matrix-title">Cost Function</p>
|
| 700 |
+
<p class="formula-line">J(theta) = (1/2m) sum((theta0 + theta1*x1 - y)^2)</p>
|
| 701 |
</article>
|
| 702 |
<article class="matrix-card">
|
| 703 |
<p class="matrix-title">Result</p>
|
|
|
|
| 765 |
ctx.fillText(yLabel, pad - 30, pad - 10);
|
| 766 |
}
|
| 767 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
function startPlayback() {
|
| 769 |
if (state.timer || !state.steps.length) return;
|
| 770 |
playGdBtn.textContent = "Pause";
|
linear-regression/styles.css
CHANGED
|
@@ -368,14 +368,89 @@ input[type="checkbox"] {
|
|
| 368 |
}
|
| 369 |
|
| 370 |
#fitCanvas,
|
| 371 |
-
#costCanvas
|
| 372 |
-
#landscapeCanvas {
|
| 373 |
width: 100%;
|
| 374 |
border: 1px solid var(--line);
|
| 375 |
border-radius: 12px;
|
| 376 |
background: #fff;
|
| 377 |
}
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
.log-wrap {
|
| 380 |
overflow: auto;
|
| 381 |
border: 1px solid var(--line);
|
|
@@ -434,4 +509,10 @@ input[type="checkbox"] {
|
|
| 434 |
.gd-slider-row {
|
| 435 |
grid-template-columns: 1fr;
|
| 436 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
}
|
|
|
|
| 368 |
}
|
| 369 |
|
| 370 |
#fitCanvas,
|
| 371 |
+
#costCanvas {
|
|
|
|
| 372 |
width: 100%;
|
| 373 |
border: 1px solid var(--line);
|
| 374 |
border-radius: 12px;
|
| 375 |
background: #fff;
|
| 376 |
}
|
| 377 |
|
| 378 |
+
.landscape-summary {
|
| 379 |
+
display: grid;
|
| 380 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 381 |
+
gap: 10px;
|
| 382 |
+
margin: 6px 0 12px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.landscape-pill {
|
| 386 |
+
border: 1px solid var(--line);
|
| 387 |
+
border-radius: 10px;
|
| 388 |
+
padding: 8px 10px;
|
| 389 |
+
background: #fff;
|
| 390 |
+
font-size: 0.9rem;
|
| 391 |
+
color: #24323a;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.landscape-controls {
|
| 395 |
+
display: flex;
|
| 396 |
+
flex-wrap: wrap;
|
| 397 |
+
gap: 8px;
|
| 398 |
+
margin-bottom: 10px;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.viz-btn {
|
| 402 |
+
padding: 8px 12px;
|
| 403 |
+
border-radius: 9px;
|
| 404 |
+
border: 1px solid #c7c0b2;
|
| 405 |
+
background: #fff;
|
| 406 |
+
color: #24323a;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.viz-btn:hover {
|
| 410 |
+
border-color: var(--accent-2);
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.landscape-controls.toggles {
|
| 414 |
+
margin-bottom: 12px;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.viz-toggle {
|
| 418 |
+
display: inline-flex;
|
| 419 |
+
align-items: center;
|
| 420 |
+
gap: 8px;
|
| 421 |
+
min-width: 0;
|
| 422 |
+
font-weight: 600;
|
| 423 |
+
border: 1px solid var(--line);
|
| 424 |
+
border-radius: 999px;
|
| 425 |
+
padding: 6px 10px;
|
| 426 |
+
background: #fff;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.viz-toggle input {
|
| 430 |
+
margin: 0;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.landscape-plot-wrap {
|
| 434 |
+
width: min(100%, 980px);
|
| 435 |
+
height: 620px;
|
| 436 |
+
min-width: 320px;
|
| 437 |
+
min-height: 320px;
|
| 438 |
+
max-width: 1400px;
|
| 439 |
+
max-height: 900px;
|
| 440 |
+
resize: both;
|
| 441 |
+
overflow: auto;
|
| 442 |
+
border: 1px solid var(--line);
|
| 443 |
+
border-radius: 12px;
|
| 444 |
+
background: #fff;
|
| 445 |
+
position: relative;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.landscape-plot {
|
| 449 |
+
width: 100%;
|
| 450 |
+
height: 100%;
|
| 451 |
+
min-height: 100%;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
.log-wrap {
|
| 455 |
overflow: auto;
|
| 456 |
border: 1px solid var(--line);
|
|
|
|
| 509 |
.gd-slider-row {
|
| 510 |
grid-template-columns: 1fr;
|
| 511 |
}
|
| 512 |
+
|
| 513 |
+
.landscape-plot-wrap {
|
| 514 |
+
width: 100%;
|
| 515 |
+
height: 420px;
|
| 516 |
+
min-height: 280px;
|
| 517 |
+
}
|
| 518 |
}
|
logistic-regression/cost-visualization.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>DDW Interface - Cost
|
| 7 |
<link rel="stylesheet" href="styles.css" />
|
| 8 |
</head>
|
| 9 |
<body>
|
|
@@ -34,10 +34,11 @@
|
|
| 34 |
|
| 35 |
<p class="hint" id="formulaText"></p>
|
| 36 |
|
| 37 |
-
<label for="lr">Learning rate: <span id="lrValue">0.
|
| 38 |
-
<input id="lr" type="range" min="0.001" max="
|
| 39 |
<label for="lrInput">Learning rate input</label>
|
| 40 |
-
<input id="lrInput" type="number" min="0.001" max="
|
|
|
|
| 41 |
|
| 42 |
<label for="iters">Iterations: <span id="iterValue">80</span></label>
|
| 43 |
<input id="iters" type="range" min="5" max="220" step="1" value="80" />
|
|
@@ -68,12 +69,67 @@
|
|
| 68 |
</section>
|
| 69 |
|
| 70 |
<section class="panel chart-panel">
|
| 71 |
-
<h2>
|
| 72 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
<div class="stats" id="costStats"></div>
|
| 74 |
</section>
|
| 75 |
</main>
|
| 76 |
|
| 77 |
-
<script src="
|
|
|
|
| 78 |
</body>
|
| 79 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Logistic Cost Landscape</title>
|
| 7 |
<link rel="stylesheet" href="styles.css" />
|
| 8 |
</head>
|
| 9 |
<body>
|
|
|
|
| 34 |
|
| 35 |
<p class="hint" id="formulaText"></p>
|
| 36 |
|
| 37 |
+
<label for="lr">Learning rate: <span id="lrValue">0.100</span></label>
|
| 38 |
+
<input id="lr" type="range" min="0.001" max="10.000" step="0.001" value="0.100" />
|
| 39 |
<label for="lrInput">Learning rate input</label>
|
| 40 |
+
<input id="lrInput" type="number" min="0.001" max="10.000" step="0.001" value="0.100" />
|
| 41 |
+
<p class="hint">If you use z-normalization, you can generally use a larger learning rate than with unscaled features.</p>
|
| 42 |
|
| 43 |
<label for="iters">Iterations: <span id="iterValue">80</span></label>
|
| 44 |
<input id="iters" type="range" min="5" max="220" step="1" value="80" />
|
|
|
|
| 69 |
</section>
|
| 70 |
|
| 71 |
<section class="panel chart-panel">
|
| 72 |
+
<h2>Cost Landscape (Gradient Descent)</h2>
|
| 73 |
+
<div id="modelDefinition" class="model-definition"></div>
|
| 74 |
+
|
| 75 |
+
<p class="hint">
|
| 76 |
+
The height represents Binary Cross-Entropy (BCE) cost J(theta). Lower points correspond to parameter values that better fit class labels.
|
| 77 |
+
</p>
|
| 78 |
+
<p class="hint">
|
| 79 |
+
Each trajectory point is one gradient-descent update: horizontal movement = parameter changes, vertical movement = BCE change.
|
| 80 |
+
</p>
|
| 81 |
+
|
| 82 |
+
<div id="landscapeSummary" class="landscape-summary"></div>
|
| 83 |
+
|
| 84 |
+
<div class="landscape-controls">
|
| 85 |
+
<label for="landscapeView">Landscape View</label>
|
| 86 |
+
<select id="landscapeView">
|
| 87 |
+
<option value="surface3d">3D Surface + Trajectory</option>
|
| 88 |
+
<option value="contour2d">2D Contour + Trajectory</option>
|
| 89 |
+
<option value="surface3dContours">3D Surface + Contours</option>
|
| 90 |
+
</select>
|
| 91 |
+
<button id="resetViewBtn" type="button" class="viz-btn">Reset View</button>
|
| 92 |
+
<button id="plotSmallerBtn" type="button" class="viz-btn">-</button>
|
| 93 |
+
<button id="plotFitBtn" type="button" class="viz-btn">Fit</button>
|
| 94 |
+
<button id="plotLargerBtn" type="button" class="viz-btn">+</button>
|
| 95 |
+
<button id="plotFullscreenBtn" type="button" class="viz-btn">Fullscreen</button>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<div class="landscape-controls toggles">
|
| 99 |
+
<label class="viz-toggle"><input id="toggleSurface" type="checkbox" checked /> Surface</label>
|
| 100 |
+
<label class="viz-toggle"><input id="toggleContours" type="checkbox" checked /> Contours</label>
|
| 101 |
+
<label class="viz-toggle"><input id="toggleTrajectory" type="checkbox" checked /> Trajectory</label>
|
| 102 |
+
<label class="viz-toggle"><input id="toggleCurrent" type="checkbox" checked /> Current Point</label>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<div class="viz-info-row">
|
| 106 |
+
<div class="viz-chip" id="clipInfo">Surface clipping: none</div>
|
| 107 |
+
<div class="viz-chip" id="minimumInfo">Lowest point in shown region: --</div>
|
| 108 |
+
<div class="viz-chip warning" id="stabilityWarning">Stability: normal</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div id="landscapePlotWrap" class="landscape-plot-wrap">
|
| 112 |
+
<div id="landscapePlot" class="landscape-plot"></div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<p class="slice-label">Drag to rotate · Scroll to zoom · Hover to inspect · Resize from the corner</p>
|
| 116 |
+
|
| 117 |
+
<div class="iteration-block">
|
| 118 |
+
<label for="landscapeStep">Gradient Descent Step</label>
|
| 119 |
+
<input id="landscapeStep" type="range" min="0" max="0" value="0" />
|
| 120 |
+
<span id="landscapeStepLabel">0 / 0</span>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<div class="classifier-panel">
|
| 124 |
+
<h3>Classifier at This Step</h3>
|
| 125 |
+
<canvas id="classifierCanvas" width="940" height="310" aria-label="Current classifier view"></canvas>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
<div class="stats" id="costStats"></div>
|
| 129 |
</section>
|
| 130 |
</main>
|
| 131 |
|
| 132 |
+
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
|
| 133 |
+
<script src="cost.js?v=20260813b"></script>
|
| 134 |
</body>
|
| 135 |
</html>
|
logistic-regression/cost.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
-
const
|
| 2 |
-
const
|
|
|
|
|
|
|
| 3 |
|
| 4 |
const controls = {
|
| 5 |
caseSelect: document.getElementById("caseSelect"),
|
|
@@ -15,6 +17,17 @@ const controls = {
|
|
| 15 |
restartBtn: document.getElementById("restartBtn"),
|
| 16 |
regenBtn: document.getElementById("regenBtn"),
|
| 17 |
clearBtn: document.getElementById("clearBtn"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
const labels = {
|
|
@@ -24,44 +37,87 @@ const labels = {
|
|
| 24 |
p1: document.getElementById("p1Value"),
|
| 25 |
p2: document.getElementById("p2Value"),
|
| 26 |
speed: document.getElementById("speedValue"),
|
|
|
|
| 27 |
};
|
| 28 |
|
|
|
|
|
|
|
| 29 |
const stats = document.getElementById("costStats");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
const
|
| 32 |
-
const
|
| 33 |
-
const GRID_N = 26;
|
| 34 |
|
| 35 |
const caseMeta = {
|
| 36 |
case1: {
|
| 37 |
p1Name: "b0",
|
| 38 |
p2Name: "b1",
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
},
|
| 41 |
case2: {
|
| 42 |
p1Name: "w1",
|
| 43 |
p2Name: "w2",
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
},
|
| 46 |
};
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
function
|
| 56 |
-
|
| 57 |
}
|
| 58 |
|
| 59 |
-
function
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
-
function
|
| 64 |
-
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
function randNormal(mean = 0, std = 1) {
|
|
@@ -71,51 +127,76 @@ function randNormal(mean = 0, std = 1) {
|
|
| 71 |
return mean + z0 * std;
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
function generateSyntheticData() {
|
| 75 |
const case1 = [];
|
| 76 |
const case2 = [];
|
| 77 |
|
| 78 |
-
const trueB0 = -0.
|
| 79 |
-
const trueB1 =
|
| 80 |
for (let i = 0; i < 160; i += 1) {
|
| 81 |
-
const center = i < 80 ? -
|
| 82 |
-
const x = randNormal(center, 1.
|
| 83 |
const p = sigmoid(trueB0 + trueB1 * x);
|
| 84 |
const y = Math.random() < p ? 1 : 0;
|
| 85 |
case1.push({ x, y });
|
| 86 |
}
|
| 87 |
|
| 88 |
-
const trueW1 =
|
| 89 |
-
const trueW2 =
|
| 90 |
-
for (let i = 0; i <
|
| 91 |
-
const center1 = i <
|
| 92 |
-
const center2 = i <
|
| 93 |
-
const x1 = randNormal(center1, 1.
|
| 94 |
-
const x2 = randNormal(center2 + 0.
|
| 95 |
const p = sigmoid(trueW1 * x1 + trueW2 * x2);
|
| 96 |
const y = Math.random() < p ? 1 : 0;
|
| 97 |
case2.push({ x1, x2, y });
|
| 98 |
}
|
| 99 |
|
| 100 |
-
datasets = { case1, case2 };
|
| 101 |
-
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
function costFor(caseKey, params) {
|
| 105 |
-
const data = datasets[caseKey];
|
| 106 |
let total = 0;
|
| 107 |
|
| 108 |
if (caseKey === "case1") {
|
| 109 |
const [b0, b1] = params;
|
| 110 |
for (const row of data) {
|
| 111 |
-
const
|
| 112 |
-
total +=
|
| 113 |
}
|
| 114 |
} else {
|
| 115 |
const [w1, w2] = params;
|
| 116 |
for (const row of data) {
|
| 117 |
-
const
|
| 118 |
-
total +=
|
| 119 |
}
|
| 120 |
}
|
| 121 |
|
|
@@ -123,7 +204,7 @@ function costFor(caseKey, params) {
|
|
| 123 |
}
|
| 124 |
|
| 125 |
function gradFor(caseKey, params) {
|
| 126 |
-
const data = datasets[caseKey];
|
| 127 |
let g1 = 0;
|
| 128 |
let g2 = 0;
|
| 129 |
|
|
@@ -149,321 +230,836 @@ function gradFor(caseKey, params) {
|
|
| 149 |
return [g1 / m, g2 / m];
|
| 150 |
}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
function buildTrajectory(caseKey, start, lr, iters) {
|
| 153 |
const path = [];
|
| 154 |
let params = [start[0], start[1]];
|
|
|
|
| 155 |
|
| 156 |
-
path.push({ p1: params[0], p2: params[1], cost: costFor(caseKey, params),
|
| 157 |
|
| 158 |
for (let i = 0; i < iters; i += 1) {
|
| 159 |
const grad = gradFor(caseKey, params);
|
| 160 |
-
const
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
-
return path;
|
| 166 |
}
|
| 167 |
|
| 168 |
-
function
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
const vals = [];
|
|
|
|
| 174 |
let min = Infinity;
|
| 175 |
let max = -Infinity;
|
|
|
|
|
|
|
| 176 |
|
| 177 |
for (let j = 0; j < GRID_N; j += 1) {
|
| 178 |
const row = [];
|
| 179 |
-
const p2 =
|
|
|
|
| 180 |
for (let i = 0; i < GRID_N; i += 1) {
|
| 181 |
-
const p1 =
|
| 182 |
const c = costFor(caseKey, [p1, p2]);
|
| 183 |
row.push(c);
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
if (c > max) max = c;
|
| 186 |
}
|
|
|
|
| 187 |
vals.push(row);
|
| 188 |
}
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
function colorScale(t) {
|
| 195 |
-
const clamped = Math.max(0, Math.min(1, t));
|
| 196 |
-
const r = Math.round(35 + 220 * clamped);
|
| 197 |
-
const g = Math.round(90 + 160 * (1 - Math.abs(clamped - 0.5) * 2));
|
| 198 |
-
const b = Math.round(240 - 220 * clamped);
|
| 199 |
-
return `rgb(${r},${g},${b})`;
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
function project3D(x, y, z) {
|
| 203 |
-
const yaw = -0.75;
|
| 204 |
-
const pitch = 0.75;
|
| 205 |
-
|
| 206 |
-
const cy = Math.cos(yaw);
|
| 207 |
-
const sy = Math.sin(yaw);
|
| 208 |
-
const cp = Math.cos(pitch);
|
| 209 |
-
const sp = Math.sin(pitch);
|
| 210 |
-
|
| 211 |
-
const xr = x * cy - y * sy;
|
| 212 |
-
const yr = x * sy + y * cy;
|
| 213 |
-
|
| 214 |
-
const y2 = yr * cp - z * sp;
|
| 215 |
-
const z2 = yr * sp + z * cp;
|
| 216 |
-
|
| 217 |
-
const perspective = 1 / (1 + z2 * 0.09);
|
| 218 |
-
const scale = 56;
|
| 219 |
|
| 220 |
return {
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
};
|
| 225 |
}
|
| 226 |
|
| 227 |
-
function
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
}
|
| 230 |
|
| 231 |
-
function
|
| 232 |
-
const
|
| 233 |
-
|
| 234 |
-
|
|
|
|
| 235 |
|
| 236 |
-
|
| 237 |
-
const
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 241 |
-
ctx.fillStyle = "#ffffff";
|
| 242 |
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 243 |
-
|
| 244 |
-
for (let j = 0; j < GRID_N - 1; j += 1) {
|
| 245 |
-
for (let i = 0; i < GRID_N - 1; i += 1) {
|
| 246 |
-
const p1a = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 247 |
-
const p2a = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 248 |
-
const p1b = P_MIN + ((i + 1) / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 249 |
-
const p2b = P_MIN + ((j + 1) / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 250 |
-
|
| 251 |
-
const z00 = mapCost(vals[j][i], min, max);
|
| 252 |
-
const z10 = mapCost(vals[j][i + 1], min, max);
|
| 253 |
-
const z11 = mapCost(vals[j + 1][i + 1], min, max);
|
| 254 |
-
const z01 = mapCost(vals[j + 1][i], min, max);
|
| 255 |
-
|
| 256 |
-
const q00 = project3D(mapParam(p1a), mapParam(p2a), z00);
|
| 257 |
-
const q10 = project3D(mapParam(p1b), mapParam(p2a), z10);
|
| 258 |
-
const q11 = project3D(mapParam(p1b), mapParam(p2b), z11);
|
| 259 |
-
const q01 = project3D(mapParam(p1a), mapParam(p2b), z01);
|
| 260 |
-
|
| 261 |
-
const t = ((vals[j][i] + vals[j][i + 1] + vals[j + 1][i + 1] + vals[j + 1][i]) / 4 - min) / Math.max(1e-9, max - min);
|
| 262 |
-
ctx.fillStyle = colorScale(t);
|
| 263 |
-
ctx.globalAlpha = 0.48;
|
| 264 |
-
ctx.beginPath();
|
| 265 |
-
ctx.moveTo(q00.sx, q00.sy);
|
| 266 |
-
ctx.lineTo(q10.sx, q10.sy);
|
| 267 |
-
ctx.lineTo(q11.sx, q11.sy);
|
| 268 |
-
ctx.lineTo(q01.sx, q01.sy);
|
| 269 |
-
ctx.closePath();
|
| 270 |
-
ctx.fill();
|
| 271 |
-
}
|
| 272 |
-
}
|
| 273 |
-
ctx.globalAlpha = 1;
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
const x = mapParam(p1);
|
| 281 |
-
const y = mapParam(p2);
|
| 282 |
-
const z = mapCost(vals[j][i], min, max);
|
| 283 |
-
const pr = project3D(x, y, z);
|
| 284 |
-
if (i === 0) ctx.moveTo(pr.sx, pr.sy);
|
| 285 |
-
else ctx.lineTo(pr.sx, pr.sy);
|
| 286 |
-
}
|
| 287 |
-
ctx.strokeStyle = "rgba(13, 66, 83, 0.34)";
|
| 288 |
-
ctx.lineWidth = 1;
|
| 289 |
-
ctx.stroke();
|
| 290 |
-
}
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
}
|
| 304 |
-
ctx.strokeStyle = "rgba(13, 66, 83, 0.26)";
|
| 305 |
-
ctx.lineWidth = 1;
|
| 306 |
-
ctx.stroke();
|
| 307 |
}
|
| 308 |
|
| 309 |
-
|
| 310 |
-
drawHeatLegend(min, max);
|
| 311 |
}
|
| 312 |
|
| 313 |
-
function
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
-
|
| 317 |
-
const
|
| 318 |
-
const
|
| 319 |
-
const
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
ctx.lineTo(zA.sx, zA.sy);
|
| 337 |
-
ctx.stroke();
|
| 338 |
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
|
|
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
|
|
|
| 347 |
}
|
| 348 |
|
| 349 |
-
function
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
const h = 180;
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
ctx.moveTo(x, y + i);
|
| 360 |
-
ctx.lineTo(x + w, y + i);
|
| 361 |
-
ctx.stroke();
|
| 362 |
}
|
|
|
|
|
|
|
| 363 |
|
| 364 |
-
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
| 372 |
}
|
| 373 |
|
| 374 |
-
function
|
| 375 |
-
|
| 376 |
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
const p = project3D(
|
| 387 |
-
mapParam(node.p1),
|
| 388 |
-
mapParam(node.p2),
|
| 389 |
-
mapCost(node.cost, grid.min, grid.max)
|
| 390 |
-
);
|
| 391 |
-
if (i === 0) ctx.moveTo(p.sx, p.sy);
|
| 392 |
-
else ctx.lineTo(p.sx, p.sy);
|
| 393 |
-
}
|
| 394 |
-
ctx.stroke();
|
| 395 |
-
|
| 396 |
-
for (let i = 0; i <= current; i += 1) {
|
| 397 |
-
const node = trajectory[i];
|
| 398 |
-
const p = project3D(
|
| 399 |
-
mapParam(node.p1),
|
| 400 |
-
mapParam(node.p2),
|
| 401 |
-
mapCost(node.cost, grid.min, grid.max)
|
| 402 |
-
);
|
| 403 |
-
const isNow = i === current;
|
| 404 |
-
ctx.fillStyle = isNow ? "#102a32" : "#d8534f";
|
| 405 |
-
ctx.beginPath();
|
| 406 |
-
ctx.arc(p.sx, p.sy, isNow ? 5.4 : 3.2, 0, Math.PI * 2);
|
| 407 |
-
ctx.fill();
|
| 408 |
}
|
| 409 |
|
| 410 |
-
const
|
| 411 |
-
const
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
}
|
| 416 |
|
| 417 |
function renderStats(caseKey) {
|
| 418 |
const meta = caseMeta[caseKey];
|
| 419 |
-
const
|
| 420 |
|
| 421 |
-
if (trajectory.length === 0) {
|
| 422 |
stats.innerHTML = `
|
| 423 |
<span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
|
| 424 |
-
<span>
|
| 425 |
<span>No trajectory yet. Click Run Gradient Descent.</span>
|
| 426 |
-
<span>
|
|
|
|
|
|
|
| 427 |
`;
|
| 428 |
return;
|
| 429 |
}
|
| 430 |
|
| 431 |
-
const idx = Math.min(stepIndex, trajectory.length - 1);
|
| 432 |
-
const
|
| 433 |
-
const start = trajectory[0];
|
| 434 |
-
const last = trajectory[trajectory.length - 1];
|
| 435 |
|
| 436 |
stats.innerHTML = `
|
| 437 |
<span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
|
| 438 |
-
<span>
|
| 439 |
-
<span>Current step: ${idx}
|
| 440 |
-
<span>
|
| 441 |
-
<span>
|
| 442 |
-
<span>
|
| 443 |
`;
|
| 444 |
}
|
| 445 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
function render() {
|
| 447 |
const caseKey = controls.caseSelect.value;
|
|
|
|
| 448 |
|
| 449 |
-
labels.formula.textContent =
|
| 450 |
labels.lr.textContent = Number(controls.lr.value).toFixed(3);
|
| 451 |
labels.iters.textContent = controls.iters.value;
|
| 452 |
labels.p1.textContent = Number(controls.p1.value).toFixed(2);
|
| 453 |
labels.p2.textContent = Number(controls.p2.value).toFixed(2);
|
| 454 |
labels.speed.textContent = controls.speed.value;
|
| 455 |
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
renderStats(caseKey);
|
|
|
|
| 459 |
}
|
| 460 |
|
| 461 |
function stopAnimation() {
|
| 462 |
-
if (
|
| 463 |
-
clearInterval(animationTimer);
|
| 464 |
-
animationTimer = null;
|
| 465 |
controls.autoBtn.textContent = "Automatic Run";
|
|
|
|
| 466 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
}
|
| 468 |
|
| 469 |
function runTrajectory() {
|
|
@@ -474,70 +1070,63 @@ function runTrajectory() {
|
|
| 474 |
const lr = Number(controls.lr.value);
|
| 475 |
const iters = Number(controls.iters.value);
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
render();
|
| 480 |
}
|
| 481 |
|
| 482 |
function nextStep() {
|
| 483 |
-
if (trajectory.length === 0) {
|
| 484 |
runTrajectory();
|
| 485 |
return;
|
| 486 |
}
|
| 487 |
-
|
| 488 |
-
|
|
|
|
| 489 |
render();
|
| 490 |
}
|
| 491 |
}
|
| 492 |
|
| 493 |
-
function
|
| 494 |
-
if (
|
| 495 |
stopAnimation();
|
| 496 |
return;
|
| 497 |
}
|
| 498 |
|
| 499 |
-
if (trajectory.length === 0)
|
| 500 |
-
runTrajectory();
|
| 501 |
-
}
|
| 502 |
|
| 503 |
controls.autoBtn.textContent = "Stop Auto";
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
if (stepIndex >= trajectory.length - 1) {
|
| 507 |
stopAnimation();
|
| 508 |
return;
|
| 509 |
}
|
| 510 |
-
|
|
|
|
| 511 |
render();
|
| 512 |
}, Number(controls.speed.value));
|
| 513 |
}
|
| 514 |
|
| 515 |
function restartPath() {
|
| 516 |
stopAnimation();
|
| 517 |
-
if (trajectory.length > 0) {
|
| 518 |
-
stepIndex = 0;
|
| 519 |
}
|
| 520 |
render();
|
| 521 |
}
|
| 522 |
|
| 523 |
function clearPath() {
|
| 524 |
stopAnimation();
|
| 525 |
-
trajectory = [];
|
| 526 |
-
stepIndex = 0;
|
| 527 |
-
|
| 528 |
-
}
|
| 529 |
-
|
| 530 |
-
function syncLrFromSlider() {
|
| 531 |
-
controls.lrInput.value = Number(controls.lr.value).toFixed(3);
|
| 532 |
-
render();
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
function syncLrFromInput() {
|
| 536 |
-
let v = Number(controls.lrInput.value);
|
| 537 |
-
if (!Number.isFinite(v)) v = 0.2;
|
| 538 |
-
v = Math.max(0.001, Math.min(1.0, v));
|
| 539 |
-
controls.lr.value = v;
|
| 540 |
-
controls.lrInput.value = v.toFixed(3);
|
| 541 |
render();
|
| 542 |
}
|
| 543 |
|
|
@@ -545,37 +1134,120 @@ function applyCaseDefaults(caseKey) {
|
|
| 545 |
if (caseKey === "case1") {
|
| 546 |
controls.p1.value = -4.0;
|
| 547 |
controls.p2.value = 4.0;
|
| 548 |
-
controls.lr.value = 0.
|
| 549 |
} else {
|
| 550 |
controls.p1.value = -2.2;
|
| 551 |
controls.p2.value = -2.0;
|
| 552 |
-
controls.lr.value = 0.
|
| 553 |
}
|
|
|
|
| 554 |
controls.lrInput.value = Number(controls.lr.value).toFixed(3);
|
| 555 |
controls.iters.value = 80;
|
| 556 |
controls.speed.value = 90;
|
|
|
|
| 557 |
clearPath();
|
| 558 |
}
|
| 559 |
|
| 560 |
-
|
| 561 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
}
|
| 563 |
|
| 564 |
-
|
| 565 |
-
controls.
|
| 566 |
-
controls.
|
| 567 |
-
|
| 568 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
|
| 570 |
-
controls.
|
| 571 |
-
controls.
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
controls.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
generateSyntheticData();
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
|
|
|
|
|
|
|
|
|
| 579 |
|
| 580 |
-
|
| 581 |
-
applyCaseDefaults("case1");
|
|
|
|
| 1 |
+
const plotWrap = document.getElementById("landscapePlotWrap");
|
| 2 |
+
const plotEl = document.getElementById("landscapePlot");
|
| 3 |
+
const classifierCanvas = document.getElementById("classifierCanvas");
|
| 4 |
+
const classifierCtx = classifierCanvas.getContext("2d");
|
| 5 |
|
| 6 |
const controls = {
|
| 7 |
caseSelect: document.getElementById("caseSelect"),
|
|
|
|
| 17 |
restartBtn: document.getElementById("restartBtn"),
|
| 18 |
regenBtn: document.getElementById("regenBtn"),
|
| 19 |
clearBtn: document.getElementById("clearBtn"),
|
| 20 |
+
landscapeView: document.getElementById("landscapeView"),
|
| 21 |
+
resetViewBtn: document.getElementById("resetViewBtn"),
|
| 22 |
+
plotSmallerBtn: document.getElementById("plotSmallerBtn"),
|
| 23 |
+
plotFitBtn: document.getElementById("plotFitBtn"),
|
| 24 |
+
plotLargerBtn: document.getElementById("plotLargerBtn"),
|
| 25 |
+
plotFullscreenBtn: document.getElementById("plotFullscreenBtn"),
|
| 26 |
+
toggleSurface: document.getElementById("toggleSurface"),
|
| 27 |
+
toggleContours: document.getElementById("toggleContours"),
|
| 28 |
+
toggleTrajectory: document.getElementById("toggleTrajectory"),
|
| 29 |
+
toggleCurrent: document.getElementById("toggleCurrent"),
|
| 30 |
+
stepSlider: document.getElementById("landscapeStep"),
|
| 31 |
};
|
| 32 |
|
| 33 |
const labels = {
|
|
|
|
| 37 |
p1: document.getElementById("p1Value"),
|
| 38 |
p2: document.getElementById("p2Value"),
|
| 39 |
speed: document.getElementById("speedValue"),
|
| 40 |
+
stepLabel: document.getElementById("landscapeStepLabel"),
|
| 41 |
};
|
| 42 |
|
| 43 |
+
const modelDefinition = document.getElementById("modelDefinition");
|
| 44 |
+
const landscapeSummary = document.getElementById("landscapeSummary");
|
| 45 |
const stats = document.getElementById("costStats");
|
| 46 |
+
const clipInfo = document.getElementById("clipInfo");
|
| 47 |
+
const minimumInfo = document.getElementById("minimumInfo");
|
| 48 |
+
const stabilityWarning = document.getElementById("stabilityWarning");
|
| 49 |
+
|
| 50 |
+
const DEFAULT_CAMERA = {
|
| 51 |
+
eye: { x: 1.5, y: 1.5, z: 1.1 },
|
| 52 |
+
center: { x: 0, y: 0, z: 0 },
|
| 53 |
+
up: { x: 0, y: 0, z: 1 },
|
| 54 |
+
};
|
| 55 |
|
| 56 |
+
const GRID_N = 60;
|
| 57 |
+
const MIN_SPAN = 2.0;
|
|
|
|
| 58 |
|
| 59 |
const caseMeta = {
|
| 60 |
case1: {
|
| 61 |
p1Name: "b0",
|
| 62 |
p2Name: "b1",
|
| 63 |
+
p1Label: "b0 (intercept)",
|
| 64 |
+
p2Label: "b1 (weight for x)",
|
| 65 |
+
model: "z = b0 + b1*x",
|
| 66 |
+
explain: [
|
| 67 |
+
"Cost landscape: J(b0, b1)",
|
| 68 |
+
"b0 = intercept, b1 = coefficient of x",
|
| 69 |
+
],
|
| 70 |
+
formula: "Case 1 logistic: p(y=1|x) = 1 / (1 + exp(-(b0 + b1x)))",
|
| 71 |
},
|
| 72 |
case2: {
|
| 73 |
p1Name: "w1",
|
| 74 |
p2Name: "w2",
|
| 75 |
+
p1Label: "w1 (weight for x1)",
|
| 76 |
+
p2Label: "w2 (weight for x2)",
|
| 77 |
+
model: "z = w1*x1 + w2*x2",
|
| 78 |
+
explain: [
|
| 79 |
+
"Cost landscape: J(w1, w2)",
|
| 80 |
+
"w1 = coefficient of x1, w2 = coefficient of x2",
|
| 81 |
+
"No intercept is used in this model.",
|
| 82 |
+
],
|
| 83 |
+
formula: "Case 2 logistic: p(y=1|x1,x2) = 1 / (1 + exp(-(w1x1 + w2x2)))",
|
| 84 |
},
|
| 85 |
};
|
| 86 |
|
| 87 |
+
const state = {
|
| 88 |
+
datasets: { case1: [], case2: [] },
|
| 89 |
+
dataVersion: 0,
|
| 90 |
+
trajectory: [],
|
| 91 |
+
stepIndex: 0,
|
| 92 |
+
timer: null,
|
| 93 |
+
unstableWarning: "",
|
| 94 |
+
gridCache: { case1: null, case2: null },
|
| 95 |
+
plotReady: false,
|
| 96 |
+
camera3d: null,
|
| 97 |
+
resizeObserver: null,
|
| 98 |
+
};
|
| 99 |
|
| 100 |
+
function hasPlotly() {
|
| 101 |
+
return typeof window !== "undefined" && typeof window.Plotly !== "undefined";
|
| 102 |
+
}
|
| 103 |
|
| 104 |
+
function showPlotFallback(message) {
|
| 105 |
+
plotEl.innerHTML = `<div class="plot-fallback">${message}</div>`;
|
| 106 |
}
|
| 107 |
|
| 108 |
+
function sigmoid(z) {
|
| 109 |
+
if (z >= 0) {
|
| 110 |
+
const e = Math.exp(-z);
|
| 111 |
+
return 1 / (1 + e);
|
| 112 |
+
}
|
| 113 |
+
const e = Math.exp(z);
|
| 114 |
+
return e / (1 + e);
|
| 115 |
}
|
| 116 |
|
| 117 |
+
function softplus(z) {
|
| 118 |
+
if (z > 35) return z;
|
| 119 |
+
if (z < -35) return Math.exp(z);
|
| 120 |
+
return Math.log1p(Math.exp(z));
|
| 121 |
}
|
| 122 |
|
| 123 |
function randNormal(mean = 0, std = 1) {
|
|
|
|
| 127 |
return mean + z0 * std;
|
| 128 |
}
|
| 129 |
|
| 130 |
+
function percentile(sortedValues, q) {
|
| 131 |
+
if (sortedValues.length === 0) return NaN;
|
| 132 |
+
const pos = (sortedValues.length - 1) * q;
|
| 133 |
+
const lo = Math.floor(pos);
|
| 134 |
+
const hi = Math.min(sortedValues.length - 1, lo + 1);
|
| 135 |
+
const t = pos - lo;
|
| 136 |
+
return sortedValues[lo] * (1 - t) + sortedValues[hi] * t;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
function cloneCamera(camera) {
|
| 140 |
+
return {
|
| 141 |
+
eye: { ...camera.eye },
|
| 142 |
+
center: { ...camera.center },
|
| 143 |
+
up: { ...camera.up },
|
| 144 |
+
};
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
function formatCost(v) {
|
| 148 |
+
if (!Number.isFinite(v)) return "--";
|
| 149 |
+
if (Math.abs(v) < 1) return v.toFixed(4);
|
| 150 |
+
if (Math.abs(v) < 100) return v.toFixed(3);
|
| 151 |
+
return v.toFixed(2);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
function generateSyntheticData() {
|
| 155 |
const case1 = [];
|
| 156 |
const case2 = [];
|
| 157 |
|
| 158 |
+
const trueB0 = -0.15;
|
| 159 |
+
const trueB1 = 1.05;
|
| 160 |
for (let i = 0; i < 160; i += 1) {
|
| 161 |
+
const center = i < 80 ? -1.5 : 1.5;
|
| 162 |
+
const x = randNormal(center, 1.9);
|
| 163 |
const p = sigmoid(trueB0 + trueB1 * x);
|
| 164 |
const y = Math.random() < p ? 1 : 0;
|
| 165 |
case1.push({ x, y });
|
| 166 |
}
|
| 167 |
|
| 168 |
+
const trueW1 = 1.1;
|
| 169 |
+
const trueW2 = 0.95;
|
| 170 |
+
for (let i = 0; i < 200; i += 1) {
|
| 171 |
+
const center1 = i < 100 ? -1.4 : 1.4;
|
| 172 |
+
const center2 = i < 100 ? -1.2 : 1.2;
|
| 173 |
+
const x1 = randNormal(center1, 1.7);
|
| 174 |
+
const x2 = randNormal(center2 + 0.25 * (x1 - center1), 1.55);
|
| 175 |
const p = sigmoid(trueW1 * x1 + trueW2 * x2);
|
| 176 |
const y = Math.random() < p ? 1 : 0;
|
| 177 |
case2.push({ x1, x2, y });
|
| 178 |
}
|
| 179 |
|
| 180 |
+
state.datasets = { case1, case2 };
|
| 181 |
+
state.dataVersion += 1;
|
| 182 |
+
state.gridCache = { case1: null, case2: null };
|
| 183 |
}
|
| 184 |
|
| 185 |
function costFor(caseKey, params) {
|
| 186 |
+
const data = state.datasets[caseKey];
|
| 187 |
let total = 0;
|
| 188 |
|
| 189 |
if (caseKey === "case1") {
|
| 190 |
const [b0, b1] = params;
|
| 191 |
for (const row of data) {
|
| 192 |
+
const z = b0 + b1 * row.x;
|
| 193 |
+
total += softplus(z) - row.y * z;
|
| 194 |
}
|
| 195 |
} else {
|
| 196 |
const [w1, w2] = params;
|
| 197 |
for (const row of data) {
|
| 198 |
+
const z = w1 * row.x1 + w2 * row.x2;
|
| 199 |
+
total += softplus(z) - row.y * z;
|
| 200 |
}
|
| 201 |
}
|
| 202 |
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
function gradFor(caseKey, params) {
|
| 207 |
+
const data = state.datasets[caseKey];
|
| 208 |
let g1 = 0;
|
| 209 |
let g2 = 0;
|
| 210 |
|
|
|
|
| 230 |
return [g1 / m, g2 / m];
|
| 231 |
}
|
| 232 |
|
| 233 |
+
function estimateAnchorPoint(caseKey) {
|
| 234 |
+
let params = [0, 0];
|
| 235 |
+
const lr = 0.1;
|
| 236 |
+
|
| 237 |
+
for (let i = 0; i < 220; i += 1) {
|
| 238 |
+
const grad = gradFor(caseKey, params);
|
| 239 |
+
const next = [params[0] - lr * grad[0], params[1] - lr * grad[1]];
|
| 240 |
+
if (!Number.isFinite(next[0]) || !Number.isFinite(next[1])) break;
|
| 241 |
+
params = next;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
return params;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
function buildTrajectory(caseKey, start, lr, iters) {
|
| 248 |
const path = [];
|
| 249 |
let params = [start[0], start[1]];
|
| 250 |
+
let unstable = false;
|
| 251 |
|
| 252 |
+
path.push({ step: 0, p1: params[0], p2: params[1], cost: costFor(caseKey, params), dCost: 0 });
|
| 253 |
|
| 254 |
for (let i = 0; i < iters; i += 1) {
|
| 255 |
const grad = gradFor(caseKey, params);
|
| 256 |
+
const next = [params[0] - lr * grad[0], params[1] - lr * grad[1]];
|
| 257 |
+
|
| 258 |
+
if (!Number.isFinite(next[0]) || !Number.isFinite(next[1])) {
|
| 259 |
+
unstable = true;
|
| 260 |
+
break;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const nextCost = costFor(caseKey, next);
|
| 264 |
+
if (!Number.isFinite(nextCost) || nextCost > 1e6) {
|
| 265 |
+
unstable = true;
|
| 266 |
+
break;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
const prevCost = path[path.length - 1].cost;
|
| 270 |
+
params = next;
|
| 271 |
+
path.push({ step: i + 1, p1: params[0], p2: params[1], cost: nextCost, dCost: nextCost - prevCost });
|
| 272 |
}
|
| 273 |
|
| 274 |
+
return { path, unstable };
|
| 275 |
}
|
| 276 |
|
| 277 |
+
function getTrajectoryBounds(caseKey) {
|
| 278 |
+
const anchor = estimateAnchorPoint(caseKey);
|
| 279 |
+
const p1s = [anchor[0], Number(controls.p1.value)];
|
| 280 |
+
const p2s = [anchor[1], Number(controls.p2.value)];
|
| 281 |
+
|
| 282 |
+
for (const s of state.trajectory) {
|
| 283 |
+
p1s.push(s.p1);
|
| 284 |
+
p2s.push(s.p2);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
let p1Min = Math.min(...p1s);
|
| 288 |
+
let p1Max = Math.max(...p1s);
|
| 289 |
+
let p2Min = Math.min(...p2s);
|
| 290 |
+
let p2Max = Math.max(...p2s);
|
| 291 |
+
|
| 292 |
+
let span1 = p1Max - p1Min;
|
| 293 |
+
let span2 = p2Max - p2Min;
|
| 294 |
+
|
| 295 |
+
if (span1 < MIN_SPAN) {
|
| 296 |
+
const add = (MIN_SPAN - span1) / 2;
|
| 297 |
+
p1Min -= add;
|
| 298 |
+
p1Max += add;
|
| 299 |
+
span1 = MIN_SPAN;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
if (span2 < MIN_SPAN) {
|
| 303 |
+
const add = (MIN_SPAN - span2) / 2;
|
| 304 |
+
p2Min -= add;
|
| 305 |
+
p2Max += add;
|
| 306 |
+
span2 = MIN_SPAN;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
const pad1 = Math.max(0.4 * span1, 0.9);
|
| 310 |
+
const pad2 = Math.max(0.4 * span2, 0.9);
|
| 311 |
+
|
| 312 |
+
return {
|
| 313 |
+
p1Min: p1Min - pad1,
|
| 314 |
+
p1Max: p1Max + pad1,
|
| 315 |
+
p2Min: p2Min - pad2,
|
| 316 |
+
p2Max: p2Max + pad2,
|
| 317 |
+
};
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
function getGridCacheKey(caseKey) {
|
| 321 |
+
const first = state.trajectory[0];
|
| 322 |
+
const last = state.trajectory[state.trajectory.length - 1];
|
| 323 |
+
return [
|
| 324 |
+
state.dataVersion,
|
| 325 |
+
caseKey,
|
| 326 |
+
controls.p1.value,
|
| 327 |
+
controls.p2.value,
|
| 328 |
+
state.trajectory.length,
|
| 329 |
+
first ? `${first.p1.toFixed(4)}:${first.p2.toFixed(4)}` : "none",
|
| 330 |
+
last ? `${last.p1.toFixed(4)}:${last.p2.toFixed(4)}` : "none",
|
| 331 |
+
].join("|");
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
function buildGrid(caseKey) {
|
| 335 |
+
const bounds = getTrajectoryBounds(caseKey);
|
| 336 |
+
const p1Values = [];
|
| 337 |
+
const p2Values = [];
|
| 338 |
+
|
| 339 |
+
for (let i = 0; i < GRID_N; i += 1) {
|
| 340 |
+
p1Values.push(bounds.p1Min + (i / (GRID_N - 1)) * (bounds.p1Max - bounds.p1Min));
|
| 341 |
+
p2Values.push(bounds.p2Min + (i / (GRID_N - 1)) * (bounds.p2Max - bounds.p2Min));
|
| 342 |
}
|
| 343 |
|
| 344 |
const vals = [];
|
| 345 |
+
const flat = [];
|
| 346 |
let min = Infinity;
|
| 347 |
let max = -Infinity;
|
| 348 |
+
let minI = 0;
|
| 349 |
+
let minJ = 0;
|
| 350 |
|
| 351 |
for (let j = 0; j < GRID_N; j += 1) {
|
| 352 |
const row = [];
|
| 353 |
+
const p2 = p2Values[j];
|
| 354 |
+
|
| 355 |
for (let i = 0; i < GRID_N; i += 1) {
|
| 356 |
+
const p1 = p1Values[i];
|
| 357 |
const c = costFor(caseKey, [p1, p2]);
|
| 358 |
row.push(c);
|
| 359 |
+
flat.push(c);
|
| 360 |
+
|
| 361 |
+
if (c < min) {
|
| 362 |
+
min = c;
|
| 363 |
+
minI = i;
|
| 364 |
+
minJ = j;
|
| 365 |
+
}
|
| 366 |
if (c > max) max = c;
|
| 367 |
}
|
| 368 |
+
|
| 369 |
vals.push(row);
|
| 370 |
}
|
| 371 |
|
| 372 |
+
const sorted = flat.slice().sort((a, b) => a - b);
|
| 373 |
+
let clipMax = percentile(sorted, 0.975);
|
| 374 |
+
if (!Number.isFinite(clipMax)) clipMax = max;
|
| 375 |
+
clipMax = Math.min(max, Math.max(min + 1e-6, clipMax));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
return {
|
| 378 |
+
key: "",
|
| 379 |
+
p1Values,
|
| 380 |
+
p2Values,
|
| 381 |
+
vals,
|
| 382 |
+
min,
|
| 383 |
+
max,
|
| 384 |
+
clipMax,
|
| 385 |
+
p1Min: bounds.p1Min,
|
| 386 |
+
p1Max: bounds.p1Max,
|
| 387 |
+
p2Min: bounds.p2Min,
|
| 388 |
+
p2Max: bounds.p2Max,
|
| 389 |
+
minPoint: {
|
| 390 |
+
p1: p1Values[minI],
|
| 391 |
+
p2: p2Values[minJ],
|
| 392 |
+
cost: min,
|
| 393 |
+
},
|
| 394 |
};
|
| 395 |
}
|
| 396 |
|
| 397 |
+
function getGrid(caseKey) {
|
| 398 |
+
const key = getGridCacheKey(caseKey);
|
| 399 |
+
const cached = state.gridCache[caseKey];
|
| 400 |
+
if (cached && cached.key === key) return cached;
|
| 401 |
+
|
| 402 |
+
const built = buildGrid(caseKey);
|
| 403 |
+
built.key = key;
|
| 404 |
+
state.gridCache[caseKey] = built;
|
| 405 |
+
return built;
|
| 406 |
}
|
| 407 |
|
| 408 |
+
function collectEdgeHit(bucket, a, b, level) {
|
| 409 |
+
const da = a.z - level;
|
| 410 |
+
const db = b.z - level;
|
| 411 |
+
if (da === 0 && db === 0) return;
|
| 412 |
+
if (da * db > 0) return;
|
| 413 |
|
| 414 |
+
const den = b.z - a.z;
|
| 415 |
+
const t = Math.abs(den) < 1e-12 ? 0.5 : (level - a.z) / den;
|
| 416 |
+
if (t < 0 || t > 1) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
|
| 418 |
+
bucket.push({
|
| 419 |
+
x: a.x + t * (b.x - a.x),
|
| 420 |
+
y: a.y + t * (b.y - a.y),
|
| 421 |
+
});
|
| 422 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
+
function buildContourSegments3D(grid) {
|
| 425 |
+
const xs = [];
|
| 426 |
+
const ys = [];
|
| 427 |
+
const zs = [];
|
| 428 |
+
const levels = 9;
|
| 429 |
+
const zFloor = grid.min;
|
| 430 |
+
|
| 431 |
+
for (let levelIdx = 1; levelIdx <= levels; levelIdx += 1) {
|
| 432 |
+
const level = grid.min + (levelIdx / (levels + 1)) * (grid.clipMax - grid.min);
|
| 433 |
+
|
| 434 |
+
for (let j = 0; j < GRID_N - 1; j += 1) {
|
| 435 |
+
for (let i = 0; i < GRID_N - 1; i += 1) {
|
| 436 |
+
const p00 = { x: grid.p1Values[i], y: grid.p2Values[j], z: grid.vals[j][i] };
|
| 437 |
+
const p10 = { x: grid.p1Values[i + 1], y: grid.p2Values[j], z: grid.vals[j][i + 1] };
|
| 438 |
+
const p11 = { x: grid.p1Values[i + 1], y: grid.p2Values[j + 1], z: grid.vals[j + 1][i + 1] };
|
| 439 |
+
const p01 = { x: grid.p1Values[i], y: grid.p2Values[j + 1], z: grid.vals[j + 1][i] };
|
| 440 |
+
|
| 441 |
+
const hits = [];
|
| 442 |
+
collectEdgeHit(hits, p00, p10, level);
|
| 443 |
+
collectEdgeHit(hits, p10, p11, level);
|
| 444 |
+
collectEdgeHit(hits, p11, p01, level);
|
| 445 |
+
collectEdgeHit(hits, p01, p00, level);
|
| 446 |
+
|
| 447 |
+
if (hits.length === 2) {
|
| 448 |
+
xs.push(hits[0].x, hits[1].x, null);
|
| 449 |
+
ys.push(hits[0].y, hits[1].y, null);
|
| 450 |
+
zs.push(zFloor, zFloor, null);
|
| 451 |
+
} else if (hits.length === 4) {
|
| 452 |
+
xs.push(hits[0].x, hits[1].x, null, hits[2].x, hits[3].x, null);
|
| 453 |
+
ys.push(hits[0].y, hits[1].y, null, hits[2].y, hits[3].y, null);
|
| 454 |
+
zs.push(zFloor, zFloor, null, zFloor, zFloor, null);
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
}
|
|
|
|
|
|
|
|
|
|
| 458 |
}
|
| 459 |
|
| 460 |
+
return { x: xs, y: ys, z: zs };
|
|
|
|
| 461 |
}
|
| 462 |
|
| 463 |
+
function buildHover(meta, point) {
|
| 464 |
+
return [
|
| 465 |
+
`Iteration: ${point.step}`,
|
| 466 |
+
`${meta.p1Name}: ${point.p1.toFixed(4)}`,
|
| 467 |
+
`${meta.p2Name}: ${point.p2.toFixed(4)}`,
|
| 468 |
+
`BCE cost: ${point.cost.toFixed(4)}`,
|
| 469 |
+
].join("<br>");
|
| 470 |
+
}
|
| 471 |
|
| 472 |
+
function getFitSize() {
|
| 473 |
+
const host = plotWrap.parentElement;
|
| 474 |
+
const hostW = host ? host.clientWidth : window.innerWidth;
|
| 475 |
+
const maxW = Math.max(320, Math.min(1000, hostW - 4));
|
| 476 |
|
| 477 |
+
if (window.innerWidth < 680) return { width: maxW, height: 390 };
|
| 478 |
+
if (window.innerWidth < 1024) return { width: Math.min(maxW, 920), height: 560 };
|
| 479 |
+
return { width: Math.min(maxW, 980), height: 640 };
|
| 480 |
+
}
|
| 481 |
|
| 482 |
+
function setPlotSize(rawW, rawH, forceResize = false) {
|
| 483 |
+
const host = plotWrap.parentElement;
|
| 484 |
+
const hostMaxW = host ? host.clientWidth - 4 : rawW;
|
| 485 |
+
const inFullscreen = document.fullscreenElement === plotWrap;
|
| 486 |
|
| 487 |
+
const minW = 320;
|
| 488 |
+
const minH = 300;
|
| 489 |
+
const maxW = inFullscreen ? Math.min(1800, window.innerWidth - 24) : Math.max(minW, hostMaxW);
|
| 490 |
+
const maxH = inFullscreen ? Math.min(1200, window.innerHeight - 24) : Math.min(900, window.innerHeight - 140);
|
| 491 |
|
| 492 |
+
const w = Math.round(Math.max(minW, Math.min(maxW, rawW)));
|
| 493 |
+
const h = Math.round(Math.max(minH, Math.min(maxH, rawH)));
|
|
|
|
|
|
|
| 494 |
|
| 495 |
+
if (forceResize || plotWrap.clientWidth !== w || plotWrap.clientHeight !== h) {
|
| 496 |
+
plotWrap.style.width = `${w}px`;
|
| 497 |
+
plotWrap.style.height = `${h}px`;
|
| 498 |
+
resizePlot();
|
| 499 |
+
}
|
| 500 |
+
}
|
| 501 |
|
| 502 |
+
function fitPlotSize(forceResize = false) {
|
| 503 |
+
const size = getFitSize();
|
| 504 |
+
setPlotSize(size.width, size.height, forceResize);
|
| 505 |
}
|
| 506 |
|
| 507 |
+
function resizePlot() {
|
| 508 |
+
if (!state.plotReady || !hasPlotly()) return;
|
| 509 |
+
requestAnimationFrame(() => window.Plotly.Plots.resize(plotEl));
|
| 510 |
+
}
|
|
|
|
| 511 |
|
| 512 |
+
function toggleFullscreen() {
|
| 513 |
+
if (document.fullscreenElement === plotWrap) {
|
| 514 |
+
document.exitFullscreen();
|
| 515 |
+
return;
|
|
|
|
|
|
|
|
|
|
| 516 |
}
|
| 517 |
+
if (plotWrap.requestFullscreen) plotWrap.requestFullscreen();
|
| 518 |
+
}
|
| 519 |
|
| 520 |
+
function updateStepSlider() {
|
| 521 |
+
const max = Math.max(0, state.trajectory.length - 1);
|
| 522 |
+
controls.stepSlider.max = String(max);
|
| 523 |
+
controls.stepSlider.value = String(Math.min(state.stepIndex, max));
|
| 524 |
+
labels.stepLabel.textContent = `${Math.min(state.stepIndex, max)} / ${max}`;
|
| 525 |
+
}
|
| 526 |
|
| 527 |
+
function updateModelDefinition(caseKey) {
|
| 528 |
+
const meta = caseMeta[caseKey];
|
| 529 |
+
modelDefinition.innerHTML = `
|
| 530 |
+
<p><strong>Model:</strong> ${meta.model}</p>
|
| 531 |
+
<p>${meta.explain[0]}</p>
|
| 532 |
+
<p>${meta.explain[1]}</p>
|
| 533 |
+
${meta.explain[2] ? `<p>${meta.explain[2]}</p>` : ""}
|
| 534 |
+
`;
|
| 535 |
}
|
| 536 |
|
| 537 |
+
function updateSummary(caseKey) {
|
| 538 |
+
const meta = caseMeta[caseKey];
|
| 539 |
|
| 540 |
+
if (state.trajectory.length === 0) {
|
| 541 |
+
landscapeSummary.innerHTML = `
|
| 542 |
+
<span class="landscape-pill">Start BCE: --</span>
|
| 543 |
+
<span class="landscape-pill">Current BCE: --</span>
|
| 544 |
+
<span class="landscape-pill">Final BCE: --</span>
|
| 545 |
+
<span class="landscape-pill">Reduction: --</span>
|
| 546 |
+
<span class="landscape-pill">Iterations: 0 / 0</span>
|
| 547 |
+
`;
|
| 548 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
}
|
| 550 |
|
| 551 |
+
const idx = Math.min(state.stepIndex, state.trajectory.length - 1);
|
| 552 |
+
const start = state.trajectory[0];
|
| 553 |
+
const current = state.trajectory[idx];
|
| 554 |
+
const final = state.trajectory[state.trajectory.length - 1];
|
| 555 |
+
const reduction = ((start.cost - final.cost) / Math.max(1e-9, start.cost)) * 100;
|
| 556 |
+
|
| 557 |
+
landscapeSummary.innerHTML = `
|
| 558 |
+
<span class="landscape-pill">Start BCE: ${start.cost.toFixed(4)}</span>
|
| 559 |
+
<span class="landscape-pill">Current BCE: ${current.cost.toFixed(4)}</span>
|
| 560 |
+
<span class="landscape-pill">Final BCE: ${final.cost.toFixed(4)}</span>
|
| 561 |
+
<span class="landscape-pill">Reduction: ${reduction.toFixed(1)}%</span>
|
| 562 |
+
<span class="landscape-pill">Iterations: ${idx} / ${state.trajectory.length - 1}</span>
|
| 563 |
+
<span class="landscape-pill">${meta.p1Name}: ${current.p1.toFixed(3)}, ${meta.p2Name}: ${current.p2.toFixed(3)}</span>
|
| 564 |
+
`;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
function updateInfoChips(caseKey, grid) {
|
| 568 |
+
const meta = caseMeta[caseKey];
|
| 569 |
+
|
| 570 |
+
if (grid.clipMax < grid.max - 1e-9) {
|
| 571 |
+
clipInfo.textContent = `Surface clipping (visual only): J <= ${formatCost(grid.clipMax)} (true max ${formatCost(grid.max)})`;
|
| 572 |
+
} else {
|
| 573 |
+
clipInfo.textContent = "Surface clipping: none";
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
minimumInfo.textContent = `Lowest shown point: ${meta.p1Name}=${grid.minPoint.p1.toFixed(3)}, ${meta.p2Name}=${grid.minPoint.p2.toFixed(3)}, J=${grid.minPoint.cost.toFixed(4)}`;
|
| 577 |
+
|
| 578 |
+
if (state.unstableWarning) {
|
| 579 |
+
stabilityWarning.textContent = state.unstableWarning;
|
| 580 |
+
stabilityWarning.classList.add("warning");
|
| 581 |
+
} else {
|
| 582 |
+
stabilityWarning.textContent = "Stability: normal";
|
| 583 |
+
stabilityWarning.classList.remove("warning");
|
| 584 |
+
}
|
| 585 |
}
|
| 586 |
|
| 587 |
function renderStats(caseKey) {
|
| 588 |
const meta = caseMeta[caseKey];
|
| 589 |
+
const m = state.datasets[caseKey].length;
|
| 590 |
|
| 591 |
+
if (state.trajectory.length === 0) {
|
| 592 |
stats.innerHTML = `
|
| 593 |
<span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
|
| 594 |
+
<span>Samples: ${m}</span>
|
| 595 |
<span>No trajectory yet. Click Run Gradient Descent.</span>
|
| 596 |
+
<span>Current step: 0</span>
|
| 597 |
+
<span>${meta.p1Label}: --</span>
|
| 598 |
+
<span>${meta.p2Label}: --</span>
|
| 599 |
`;
|
| 600 |
return;
|
| 601 |
}
|
| 602 |
|
| 603 |
+
const idx = Math.min(state.stepIndex, state.trajectory.length - 1);
|
| 604 |
+
const current = state.trajectory[idx];
|
|
|
|
|
|
|
| 605 |
|
| 606 |
stats.innerHTML = `
|
| 607 |
<span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
|
| 608 |
+
<span>Samples: ${m}</span>
|
| 609 |
+
<span>Current step: ${idx}</span>
|
| 610 |
+
<span>BCE: ${current.cost.toFixed(4)}</span>
|
| 611 |
+
<span>${meta.p1Label}: ${current.p1.toFixed(4)}</span>
|
| 612 |
+
<span>${meta.p2Label}: ${current.p2.toFixed(4)}</span>
|
| 613 |
`;
|
| 614 |
}
|
| 615 |
|
| 616 |
+
function renderLandscape(caseKey, grid) {
|
| 617 |
+
if (!hasPlotly()) {
|
| 618 |
+
showPlotFallback("Interactive 3D view unavailable (Plotly not loaded). Other page controls remain active.");
|
| 619 |
+
state.plotReady = false;
|
| 620 |
+
return;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
const meta = caseMeta[caseKey];
|
| 624 |
+
const view = controls.landscapeView.value;
|
| 625 |
+
const idx = Math.min(state.stepIndex, Math.max(0, state.trajectory.length - 1));
|
| 626 |
+
|
| 627 |
+
const showSurface = controls.toggleSurface.checked;
|
| 628 |
+
const showContours = controls.toggleContours.checked;
|
| 629 |
+
const showTrajectory = controls.toggleTrajectory.checked;
|
| 630 |
+
const showCurrent = controls.toggleCurrent.checked;
|
| 631 |
+
|
| 632 |
+
const fullPath = state.trajectory;
|
| 633 |
+
const current = fullPath[idx];
|
| 634 |
+
const start = fullPath[0];
|
| 635 |
+
const final = fullPath[fullPath.length - 1];
|
| 636 |
+
|
| 637 |
+
const traces = [];
|
| 638 |
+
|
| 639 |
+
if (view === "contour2d") {
|
| 640 |
+
traces.push({
|
| 641 |
+
type: "contour",
|
| 642 |
+
x: grid.p1Values,
|
| 643 |
+
y: grid.p2Values,
|
| 644 |
+
z: grid.vals,
|
| 645 |
+
ncontours: 16,
|
| 646 |
+
contours: {
|
| 647 |
+
coloring: showSurface ? "heatmap" : "lines",
|
| 648 |
+
},
|
| 649 |
+
colorscale: "Viridis",
|
| 650 |
+
showscale: showSurface,
|
| 651 |
+
visible: showSurface || showContours,
|
| 652 |
+
colorbar: { title: "BCE", thickness: 14 },
|
| 653 |
+
hovertemplate: `${meta.p1Name}: %{x:.4f}<br>${meta.p2Name}: %{y:.4f}<br>BCE: %{z:.4f}<extra></extra>`,
|
| 654 |
+
});
|
| 655 |
+
|
| 656 |
+
if (showTrajectory && fullPath.length > 0) {
|
| 657 |
+
traces.push({
|
| 658 |
+
type: "scatter",
|
| 659 |
+
mode: "lines+markers",
|
| 660 |
+
x: fullPath.map((p) => p.p1),
|
| 661 |
+
y: fullPath.map((p) => p.p2),
|
| 662 |
+
customdata: fullPath.map((p) => p.step),
|
| 663 |
+
line: { color: "#d8534f", width: 3 },
|
| 664 |
+
marker: { size: 4, color: "#9a3412" },
|
| 665 |
+
text: fullPath.map((p) => buildHover(meta, p)),
|
| 666 |
+
hovertemplate: "%{text}<extra></extra>",
|
| 667 |
+
});
|
| 668 |
+
|
| 669 |
+
traces.push({
|
| 670 |
+
type: "scatter",
|
| 671 |
+
mode: "markers+text",
|
| 672 |
+
x: [start.p1],
|
| 673 |
+
y: [start.p2],
|
| 674 |
+
customdata: [start.step],
|
| 675 |
+
marker: { size: 10, color: "#0a8f7b", line: { color: "#102a32", width: 1 } },
|
| 676 |
+
text: ["Start"],
|
| 677 |
+
textposition: "top center",
|
| 678 |
+
hovertemplate: `${buildHover(meta, start)}<extra></extra>`,
|
| 679 |
+
});
|
| 680 |
+
|
| 681 |
+
traces.push({
|
| 682 |
+
type: "scatter",
|
| 683 |
+
mode: "markers+text",
|
| 684 |
+
x: [final.p1],
|
| 685 |
+
y: [final.p2],
|
| 686 |
+
customdata: [final.step],
|
| 687 |
+
marker: { size: 10, color: "#dd5e2f", line: { color: "#102a32", width: 1 } },
|
| 688 |
+
text: ["Current solution"],
|
| 689 |
+
textposition: "top center",
|
| 690 |
+
hovertemplate: `${buildHover(meta, final)}<extra></extra>`,
|
| 691 |
+
});
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
if (showCurrent && fullPath.length > 0) {
|
| 695 |
+
traces.push({
|
| 696 |
+
type: "scatter",
|
| 697 |
+
mode: "markers+text",
|
| 698 |
+
x: [current.p1],
|
| 699 |
+
y: [current.p2],
|
| 700 |
+
customdata: [current.step],
|
| 701 |
+
marker: { size: 10, color: "#111827", line: { color: "#f2b84b", width: 2 } },
|
| 702 |
+
text: [`Step ${idx}`],
|
| 703 |
+
textposition: "bottom center",
|
| 704 |
+
hovertemplate: `${buildHover(meta, current)}<extra></extra>`,
|
| 705 |
+
});
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
const layout2d = {
|
| 709 |
+
autosize: true,
|
| 710 |
+
margin: { l: 58, r: 16, t: 8, b: 50 },
|
| 711 |
+
xaxis: { title: meta.p1Label },
|
| 712 |
+
yaxis: { title: meta.p2Label },
|
| 713 |
+
showlegend: false,
|
| 714 |
+
uirevision: "logistic-contour",
|
| 715 |
+
paper_bgcolor: "#fff",
|
| 716 |
+
plot_bgcolor: "#fff",
|
| 717 |
+
};
|
| 718 |
+
|
| 719 |
+
const config2d = {
|
| 720 |
+
responsive: true,
|
| 721 |
+
displaylogo: false,
|
| 722 |
+
scrollZoom: true,
|
| 723 |
+
modeBarButtonsToRemove: ["lasso2d", "select2d"],
|
| 724 |
+
};
|
| 725 |
+
|
| 726 |
+
const p2d = state.plotReady
|
| 727 |
+
? window.Plotly.react(plotEl, traces, layout2d, config2d)
|
| 728 |
+
: window.Plotly.newPlot(plotEl, traces, layout2d, config2d);
|
| 729 |
+
|
| 730 |
+
p2d.then(() => {
|
| 731 |
+
if (!state.plotReady) {
|
| 732 |
+
state.plotReady = true;
|
| 733 |
+
bindPlotClick();
|
| 734 |
+
}
|
| 735 |
+
resizePlot();
|
| 736 |
+
});
|
| 737 |
+
return;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
const clippedSurface = grid.vals.map((row) => row.map((v) => Math.min(v, grid.clipMax)));
|
| 741 |
+
|
| 742 |
+
traces.push({
|
| 743 |
+
type: "surface",
|
| 744 |
+
x: grid.p1Values,
|
| 745 |
+
y: grid.p2Values,
|
| 746 |
+
z: clippedSurface,
|
| 747 |
+
opacity: 0.84,
|
| 748 |
+
colorscale: "Viridis",
|
| 749 |
+
showscale: showSurface,
|
| 750 |
+
visible: showSurface,
|
| 751 |
+
colorbar: { title: "BCE", thickness: 14 },
|
| 752 |
+
hovertemplate: `${meta.p1Name}: %{x:.4f}<br>${meta.p2Name}: %{y:.4f}<br>BCE: %{z:.4f}<extra></extra>`,
|
| 753 |
+
});
|
| 754 |
+
|
| 755 |
+
const contour3d = buildContourSegments3D(grid);
|
| 756 |
+
traces.push({
|
| 757 |
+
type: "scatter3d",
|
| 758 |
+
mode: "lines",
|
| 759 |
+
x: contour3d.x,
|
| 760 |
+
y: contour3d.y,
|
| 761 |
+
z: contour3d.z,
|
| 762 |
+
line: { color: "rgba(16,54,76,0.7)", width: 3 },
|
| 763 |
+
visible: showContours || view === "surface3dContours",
|
| 764 |
+
hoverinfo: "skip",
|
| 765 |
+
});
|
| 766 |
+
|
| 767 |
+
if (showTrajectory && fullPath.length > 0) {
|
| 768 |
+
traces.push({
|
| 769 |
+
type: "scatter3d",
|
| 770 |
+
mode: "lines+markers",
|
| 771 |
+
x: fullPath.map((p) => p.p1),
|
| 772 |
+
y: fullPath.map((p) => p.p2),
|
| 773 |
+
z: fullPath.map((p) => Math.min(p.cost, grid.clipMax)),
|
| 774 |
+
customdata: fullPath.map((p) => p.step),
|
| 775 |
+
line: { color: "#d8534f", width: 6 },
|
| 776 |
+
marker: { size: 4, color: "#9a3412" },
|
| 777 |
+
text: fullPath.map((p) => buildHover(meta, p)),
|
| 778 |
+
hovertemplate: "%{text}<extra></extra>",
|
| 779 |
+
});
|
| 780 |
+
|
| 781 |
+
traces.push({
|
| 782 |
+
type: "scatter3d",
|
| 783 |
+
mode: "markers+text",
|
| 784 |
+
x: [start.p1],
|
| 785 |
+
y: [start.p2],
|
| 786 |
+
z: [Math.min(start.cost, grid.clipMax)],
|
| 787 |
+
customdata: [start.step],
|
| 788 |
+
marker: { size: 10, color: "#0a8f7b", line: { color: "#102a32", width: 1 } },
|
| 789 |
+
text: ["Start"],
|
| 790 |
+
textposition: "top center",
|
| 791 |
+
hovertemplate: `${buildHover(meta, start)}<extra></extra>`,
|
| 792 |
+
});
|
| 793 |
+
|
| 794 |
+
traces.push({
|
| 795 |
+
type: "scatter3d",
|
| 796 |
+
mode: "markers+text",
|
| 797 |
+
x: [final.p1],
|
| 798 |
+
y: [final.p2],
|
| 799 |
+
z: [Math.min(final.cost, grid.clipMax)],
|
| 800 |
+
customdata: [final.step],
|
| 801 |
+
marker: { size: 10, color: "#dd5e2f", line: { color: "#102a32", width: 1 } },
|
| 802 |
+
text: ["Current solution"],
|
| 803 |
+
textposition: "top center",
|
| 804 |
+
hovertemplate: `${buildHover(meta, final)}<extra></extra>`,
|
| 805 |
+
});
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
if (showCurrent && fullPath.length > 0) {
|
| 809 |
+
traces.push({
|
| 810 |
+
type: "scatter3d",
|
| 811 |
+
mode: "markers+text",
|
| 812 |
+
x: [current.p1],
|
| 813 |
+
y: [current.p2],
|
| 814 |
+
z: [Math.min(current.cost, grid.clipMax)],
|
| 815 |
+
customdata: [current.step],
|
| 816 |
+
marker: { size: 9, color: "#111827", line: { color: "#f2b84b", width: 2 } },
|
| 817 |
+
text: [`Step ${idx}`],
|
| 818 |
+
textposition: "bottom center",
|
| 819 |
+
hovertemplate: `${buildHover(meta, current)}<extra></extra>`,
|
| 820 |
+
});
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
const layout3d = {
|
| 824 |
+
autosize: true,
|
| 825 |
+
margin: { l: 0, r: 0, t: 8, b: 0 },
|
| 826 |
+
scene: {
|
| 827 |
+
xaxis: { title: meta.p1Label, range: [grid.p1Min, grid.p1Max], showspikes: false },
|
| 828 |
+
yaxis: { title: meta.p2Label, range: [grid.p2Min, grid.p2Max], showspikes: false },
|
| 829 |
+
zaxis: { title: "Binary Cross-Entropy J(theta)", range: [grid.min, grid.clipMax], showspikes: false },
|
| 830 |
+
aspectmode: "manual",
|
| 831 |
+
aspectratio: { x: 1.14, y: 1.12, z: 0.82 },
|
| 832 |
+
camera: state.camera3d || cloneCamera(DEFAULT_CAMERA),
|
| 833 |
+
dragmode: "turntable",
|
| 834 |
+
},
|
| 835 |
+
showlegend: false,
|
| 836 |
+
uirevision: "logistic-3d",
|
| 837 |
+
paper_bgcolor: "#fff",
|
| 838 |
+
};
|
| 839 |
+
|
| 840 |
+
const config3d = {
|
| 841 |
+
responsive: true,
|
| 842 |
+
displaylogo: false,
|
| 843 |
+
scrollZoom: true,
|
| 844 |
+
doubleClick: "reset",
|
| 845 |
+
modeBarButtonsToRemove: ["lasso2d", "select2d"],
|
| 846 |
+
};
|
| 847 |
+
|
| 848 |
+
const p3d = state.plotReady
|
| 849 |
+
? window.Plotly.react(plotEl, traces, layout3d, config3d)
|
| 850 |
+
: window.Plotly.newPlot(plotEl, traces, layout3d, config3d);
|
| 851 |
+
|
| 852 |
+
p3d.then(() => {
|
| 853 |
+
if (!state.plotReady) {
|
| 854 |
+
state.plotReady = true;
|
| 855 |
+
bindPlotClick();
|
| 856 |
+
plotEl.on("plotly_relayout", (ev) => {
|
| 857 |
+
if (ev && ev["scene.camera"]) {
|
| 858 |
+
state.camera3d = cloneCamera(ev["scene.camera"]);
|
| 859 |
+
}
|
| 860 |
+
});
|
| 861 |
+
}
|
| 862 |
+
resizePlot();
|
| 863 |
+
});
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
function bindPlotClick() {
|
| 867 |
+
if (!plotEl || !plotEl.on) return;
|
| 868 |
+
|
| 869 |
+
plotEl.on("plotly_click", (ev) => {
|
| 870 |
+
if (!ev || !ev.points || ev.points.length === 0) return;
|
| 871 |
+
const pt = ev.points[0];
|
| 872 |
+
const raw = pt.customdata;
|
| 873 |
+
|
| 874 |
+
let step = null;
|
| 875 |
+
if (typeof raw === "number") step = raw;
|
| 876 |
+
if (Array.isArray(raw) && typeof raw[0] === "number") step = raw[0];
|
| 877 |
+
|
| 878 |
+
if (Number.isInteger(step) && step >= 0 && step < state.trajectory.length) {
|
| 879 |
+
state.stepIndex = step;
|
| 880 |
+
render();
|
| 881 |
+
}
|
| 882 |
+
});
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
function drawClassifier(caseKey) {
|
| 886 |
+
const parent = classifierCanvas.parentElement;
|
| 887 |
+
const width = Math.max(320, Math.floor(parent.clientWidth - 2));
|
| 888 |
+
const height = 310;
|
| 889 |
+
if (classifierCanvas.width !== width || classifierCanvas.height !== height) {
|
| 890 |
+
classifierCanvas.width = width;
|
| 891 |
+
classifierCanvas.height = height;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
const ctx = classifierCtx;
|
| 895 |
+
ctx.clearRect(0, 0, width, height);
|
| 896 |
+
ctx.fillStyle = "#fff";
|
| 897 |
+
ctx.fillRect(0, 0, width, height);
|
| 898 |
+
|
| 899 |
+
const pad = 42;
|
| 900 |
+
|
| 901 |
+
if (state.trajectory.length === 0) {
|
| 902 |
+
ctx.fillStyle = "#2a3d46";
|
| 903 |
+
ctx.font = "14px 'Avenir Next', sans-serif";
|
| 904 |
+
ctx.fillText("Run Gradient Descent to view classifier changes by iteration.", 22, 42);
|
| 905 |
+
return;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
const idx = Math.min(state.stepIndex, state.trajectory.length - 1);
|
| 909 |
+
const step = state.trajectory[idx];
|
| 910 |
+
const data = state.datasets[caseKey];
|
| 911 |
+
|
| 912 |
+
if (caseKey === "case1") {
|
| 913 |
+
const xs = data.map((d) => d.x);
|
| 914 |
+
const xMin = Math.min(...xs) - 0.6;
|
| 915 |
+
const xMax = Math.max(...xs) + 0.6;
|
| 916 |
+
|
| 917 |
+
const toX = (x) => pad + ((x - xMin) / (xMax - xMin || 1)) * (width - 2 * pad);
|
| 918 |
+
const toY = (p) => height - pad - p * (height - 2 * pad);
|
| 919 |
+
|
| 920 |
+
ctx.strokeStyle = "#7b8f98";
|
| 921 |
+
ctx.lineWidth = 1.1;
|
| 922 |
+
ctx.beginPath();
|
| 923 |
+
ctx.moveTo(pad, height - pad);
|
| 924 |
+
ctx.lineTo(width - pad, height - pad);
|
| 925 |
+
ctx.stroke();
|
| 926 |
+
ctx.beginPath();
|
| 927 |
+
ctx.moveTo(pad, height - pad);
|
| 928 |
+
ctx.lineTo(pad, pad);
|
| 929 |
+
ctx.stroke();
|
| 930 |
+
|
| 931 |
+
ctx.setLineDash([5, 4]);
|
| 932 |
+
ctx.strokeStyle = "#8aa0ad";
|
| 933 |
+
ctx.beginPath();
|
| 934 |
+
ctx.moveTo(pad, toY(0.5));
|
| 935 |
+
ctx.lineTo(width - pad, toY(0.5));
|
| 936 |
+
ctx.stroke();
|
| 937 |
+
ctx.setLineDash([]);
|
| 938 |
+
|
| 939 |
+
ctx.fillStyle = "#0a8f7b";
|
| 940 |
+
for (const row of data) {
|
| 941 |
+
const yJ = row.y === 1 ? 0.93 : 0.07;
|
| 942 |
+
ctx.beginPath();
|
| 943 |
+
ctx.arc(toX(row.x), toY(yJ), 3.4, 0, Math.PI * 2);
|
| 944 |
+
ctx.fill();
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
ctx.strokeStyle = "#d8534f";
|
| 948 |
+
ctx.lineWidth = 2.6;
|
| 949 |
+
ctx.beginPath();
|
| 950 |
+
for (let i = 0; i <= 120; i += 1) {
|
| 951 |
+
const x = xMin + (i / 120) * (xMax - xMin);
|
| 952 |
+
const p = sigmoid(step.p1 + step.p2 * x);
|
| 953 |
+
const sx = toX(x);
|
| 954 |
+
const sy = toY(p);
|
| 955 |
+
if (i === 0) ctx.moveTo(sx, sy);
|
| 956 |
+
else ctx.lineTo(sx, sy);
|
| 957 |
+
}
|
| 958 |
+
ctx.stroke();
|
| 959 |
+
|
| 960 |
+
if (Math.abs(step.p2) > 1e-9) {
|
| 961 |
+
const xb = -step.p1 / step.p2;
|
| 962 |
+
if (xb >= xMin && xb <= xMax) {
|
| 963 |
+
ctx.setLineDash([4, 4]);
|
| 964 |
+
ctx.strokeStyle = "#111827";
|
| 965 |
+
ctx.beginPath();
|
| 966 |
+
ctx.moveTo(toX(xb), pad);
|
| 967 |
+
ctx.lineTo(toX(xb), height - pad);
|
| 968 |
+
ctx.stroke();
|
| 969 |
+
ctx.setLineDash([]);
|
| 970 |
+
}
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
ctx.fillStyle = "#334b56";
|
| 974 |
+
ctx.font = "13px 'Avenir Next', sans-serif";
|
| 975 |
+
ctx.fillText("x", width - pad - 10, height - pad + 20);
|
| 976 |
+
ctx.fillText("Probability", pad - 28, pad - 10);
|
| 977 |
+
} else {
|
| 978 |
+
const x1s = data.map((d) => d.x1);
|
| 979 |
+
const x2s = data.map((d) => d.x2);
|
| 980 |
+
const x1Min = Math.min(...x1s) - 0.7;
|
| 981 |
+
const x1Max = Math.max(...x1s) + 0.7;
|
| 982 |
+
const x2Min = Math.min(...x2s) - 0.7;
|
| 983 |
+
const x2Max = Math.max(...x2s) + 0.7;
|
| 984 |
+
|
| 985 |
+
const toX = (x) => pad + ((x - x1Min) / (x1Max - x1Min || 1)) * (width - 2 * pad);
|
| 986 |
+
const toY = (y) => height - pad - ((y - x2Min) / (x2Max - x2Min || 1)) * (height - 2 * pad);
|
| 987 |
+
|
| 988 |
+
ctx.strokeStyle = "#7b8f98";
|
| 989 |
+
ctx.lineWidth = 1.1;
|
| 990 |
+
ctx.beginPath();
|
| 991 |
+
ctx.moveTo(pad, height - pad);
|
| 992 |
+
ctx.lineTo(width - pad, height - pad);
|
| 993 |
+
ctx.stroke();
|
| 994 |
+
ctx.beginPath();
|
| 995 |
+
ctx.moveTo(pad, height - pad);
|
| 996 |
+
ctx.lineTo(pad, pad);
|
| 997 |
+
ctx.stroke();
|
| 998 |
+
|
| 999 |
+
for (const row of data) {
|
| 1000 |
+
ctx.fillStyle = row.y === 1 ? "#0a8f7b" : "#d8534f";
|
| 1001 |
+
ctx.beginPath();
|
| 1002 |
+
ctx.arc(toX(row.x1), toY(row.x2), 3.2, 0, Math.PI * 2);
|
| 1003 |
+
ctx.fill();
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
ctx.strokeStyle = "#111827";
|
| 1007 |
+
ctx.lineWidth = 2.4;
|
| 1008 |
+
|
| 1009 |
+
if (Math.abs(step.p2) > 1e-9) {
|
| 1010 |
+
const xa = x1Min;
|
| 1011 |
+
const xb = x1Max;
|
| 1012 |
+
const ya = -(step.p1 / step.p2) * xa;
|
| 1013 |
+
const yb = -(step.p1 / step.p2) * xb;
|
| 1014 |
+
ctx.beginPath();
|
| 1015 |
+
ctx.moveTo(toX(xa), toY(ya));
|
| 1016 |
+
ctx.lineTo(toX(xb), toY(yb));
|
| 1017 |
+
ctx.stroke();
|
| 1018 |
+
} else {
|
| 1019 |
+
ctx.beginPath();
|
| 1020 |
+
ctx.moveTo(toX(0), pad);
|
| 1021 |
+
ctx.lineTo(toX(0), height - pad);
|
| 1022 |
+
ctx.stroke();
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
ctx.fillStyle = "#334b56";
|
| 1026 |
+
ctx.font = "13px 'Avenir Next', sans-serif";
|
| 1027 |
+
ctx.fillText("x1", width - pad - 12, height - pad + 20);
|
| 1028 |
+
ctx.fillText("x2", pad - 22, pad - 10);
|
| 1029 |
+
}
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
function render() {
|
| 1033 |
const caseKey = controls.caseSelect.value;
|
| 1034 |
+
const meta = caseMeta[caseKey];
|
| 1035 |
|
| 1036 |
+
labels.formula.textContent = meta.formula;
|
| 1037 |
labels.lr.textContent = Number(controls.lr.value).toFixed(3);
|
| 1038 |
labels.iters.textContent = controls.iters.value;
|
| 1039 |
labels.p1.textContent = Number(controls.p1.value).toFixed(2);
|
| 1040 |
labels.p2.textContent = Number(controls.p2.value).toFixed(2);
|
| 1041 |
labels.speed.textContent = controls.speed.value;
|
| 1042 |
|
| 1043 |
+
updateModelDefinition(caseKey);
|
| 1044 |
+
updateStepSlider();
|
| 1045 |
+
|
| 1046 |
+
const grid = getGrid(caseKey);
|
| 1047 |
+
updateInfoChips(caseKey, grid);
|
| 1048 |
+
updateSummary(caseKey);
|
| 1049 |
+
renderLandscape(caseKey, grid);
|
| 1050 |
renderStats(caseKey);
|
| 1051 |
+
drawClassifier(caseKey);
|
| 1052 |
}
|
| 1053 |
|
| 1054 |
function stopAnimation() {
|
| 1055 |
+
if (!state.timer) {
|
|
|
|
|
|
|
| 1056 |
controls.autoBtn.textContent = "Automatic Run";
|
| 1057 |
+
return;
|
| 1058 |
}
|
| 1059 |
+
|
| 1060 |
+
clearInterval(state.timer);
|
| 1061 |
+
state.timer = null;
|
| 1062 |
+
controls.autoBtn.textContent = "Automatic Run";
|
| 1063 |
}
|
| 1064 |
|
| 1065 |
function runTrajectory() {
|
|
|
|
| 1070 |
const lr = Number(controls.lr.value);
|
| 1071 |
const iters = Number(controls.iters.value);
|
| 1072 |
|
| 1073 |
+
const { path, unstable } = buildTrajectory(caseKey, start, lr, iters);
|
| 1074 |
+
state.trajectory = path;
|
| 1075 |
+
state.stepIndex = 0;
|
| 1076 |
+
state.unstableWarning = unstable
|
| 1077 |
+
? "The learning rate may be too large; gradient descent became unstable."
|
| 1078 |
+
: "";
|
| 1079 |
+
|
| 1080 |
+
state.gridCache[caseKey] = null;
|
| 1081 |
render();
|
| 1082 |
}
|
| 1083 |
|
| 1084 |
function nextStep() {
|
| 1085 |
+
if (state.trajectory.length === 0) {
|
| 1086 |
runTrajectory();
|
| 1087 |
return;
|
| 1088 |
}
|
| 1089 |
+
|
| 1090 |
+
if (state.stepIndex < state.trajectory.length - 1) {
|
| 1091 |
+
state.stepIndex += 1;
|
| 1092 |
render();
|
| 1093 |
}
|
| 1094 |
}
|
| 1095 |
|
| 1096 |
+
function autoRun() {
|
| 1097 |
+
if (state.timer) {
|
| 1098 |
stopAnimation();
|
| 1099 |
return;
|
| 1100 |
}
|
| 1101 |
|
| 1102 |
+
if (state.trajectory.length === 0) runTrajectory();
|
|
|
|
|
|
|
| 1103 |
|
| 1104 |
controls.autoBtn.textContent = "Stop Auto";
|
| 1105 |
+
state.timer = setInterval(() => {
|
| 1106 |
+
if (state.stepIndex >= state.trajectory.length - 1) {
|
|
|
|
| 1107 |
stopAnimation();
|
| 1108 |
return;
|
| 1109 |
}
|
| 1110 |
+
|
| 1111 |
+
state.stepIndex += 1;
|
| 1112 |
render();
|
| 1113 |
}, Number(controls.speed.value));
|
| 1114 |
}
|
| 1115 |
|
| 1116 |
function restartPath() {
|
| 1117 |
stopAnimation();
|
| 1118 |
+
if (state.trajectory.length > 0) {
|
| 1119 |
+
state.stepIndex = 0;
|
| 1120 |
}
|
| 1121 |
render();
|
| 1122 |
}
|
| 1123 |
|
| 1124 |
function clearPath() {
|
| 1125 |
stopAnimation();
|
| 1126 |
+
state.trajectory = [];
|
| 1127 |
+
state.stepIndex = 0;
|
| 1128 |
+
state.unstableWarning = "";
|
| 1129 |
+
state.gridCache = { case1: null, case2: null };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1130 |
render();
|
| 1131 |
}
|
| 1132 |
|
|
|
|
| 1134 |
if (caseKey === "case1") {
|
| 1135 |
controls.p1.value = -4.0;
|
| 1136 |
controls.p2.value = 4.0;
|
| 1137 |
+
controls.lr.value = 0.1;
|
| 1138 |
} else {
|
| 1139 |
controls.p1.value = -2.2;
|
| 1140 |
controls.p2.value = -2.0;
|
| 1141 |
+
controls.lr.value = 0.1;
|
| 1142 |
}
|
| 1143 |
+
|
| 1144 |
controls.lrInput.value = Number(controls.lr.value).toFixed(3);
|
| 1145 |
controls.iters.value = 80;
|
| 1146 |
controls.speed.value = 90;
|
| 1147 |
+
|
| 1148 |
clearPath();
|
| 1149 |
}
|
| 1150 |
|
| 1151 |
+
function resetView() {
|
| 1152 |
+
if (!hasPlotly() || !state.plotReady) return;
|
| 1153 |
+
|
| 1154 |
+
if (controls.landscapeView.value === "contour2d") {
|
| 1155 |
+
window.Plotly.relayout(plotEl, {
|
| 1156 |
+
"xaxis.autorange": true,
|
| 1157 |
+
"yaxis.autorange": true,
|
| 1158 |
+
});
|
| 1159 |
+
return;
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
state.camera3d = cloneCamera(DEFAULT_CAMERA);
|
| 1163 |
+
window.Plotly.relayout(plotEl, { "scene.camera": state.camera3d });
|
| 1164 |
}
|
| 1165 |
|
| 1166 |
+
function bindEvents() {
|
| 1167 |
+
controls.lr.addEventListener("input", () => {
|
| 1168 |
+
controls.lrInput.value = Number(controls.lr.value).toFixed(3);
|
| 1169 |
+
render();
|
| 1170 |
+
});
|
| 1171 |
+
|
| 1172 |
+
controls.lrInput.addEventListener("change", () => {
|
| 1173 |
+
let v = Number(controls.lrInput.value);
|
| 1174 |
+
if (!Number.isFinite(v)) v = 0.1;
|
| 1175 |
+
v = Math.max(0.001, Math.min(10.0, v));
|
| 1176 |
+
controls.lr.value = v;
|
| 1177 |
+
controls.lrInput.value = v.toFixed(3);
|
| 1178 |
+
render();
|
| 1179 |
+
});
|
| 1180 |
+
|
| 1181 |
+
for (const el of [controls.iters, controls.p1, controls.p2, controls.speed]) {
|
| 1182 |
+
el.addEventListener("input", render);
|
| 1183 |
+
}
|
| 1184 |
|
| 1185 |
+
controls.caseSelect.addEventListener("change", () => {
|
| 1186 |
+
applyCaseDefaults(controls.caseSelect.value);
|
| 1187 |
+
});
|
| 1188 |
+
|
| 1189 |
+
controls.runBtn.addEventListener("click", runTrajectory);
|
| 1190 |
+
controls.stepBtn.addEventListener("click", nextStep);
|
| 1191 |
+
controls.autoBtn.addEventListener("click", autoRun);
|
| 1192 |
+
controls.restartBtn.addEventListener("click", restartPath);
|
| 1193 |
+
controls.clearBtn.addEventListener("click", clearPath);
|
| 1194 |
+
|
| 1195 |
+
controls.regenBtn.addEventListener("click", () => {
|
| 1196 |
+
generateSyntheticData();
|
| 1197 |
+
clearPath();
|
| 1198 |
+
});
|
| 1199 |
+
|
| 1200 |
+
controls.landscapeView.addEventListener("change", render);
|
| 1201 |
+
controls.toggleSurface.addEventListener("change", render);
|
| 1202 |
+
controls.toggleContours.addEventListener("change", render);
|
| 1203 |
+
controls.toggleTrajectory.addEventListener("change", render);
|
| 1204 |
+
controls.toggleCurrent.addEventListener("change", render);
|
| 1205 |
+
|
| 1206 |
+
controls.stepSlider.addEventListener("input", () => {
|
| 1207 |
+
const next = Number(controls.stepSlider.value);
|
| 1208 |
+
if (!Number.isFinite(next)) return;
|
| 1209 |
+
state.stepIndex = Math.max(0, Math.min(next, Math.max(0, state.trajectory.length - 1)));
|
| 1210 |
+
render();
|
| 1211 |
+
});
|
| 1212 |
+
|
| 1213 |
+
controls.resetViewBtn.addEventListener("click", resetView);
|
| 1214 |
+
controls.plotFitBtn.addEventListener("click", () => fitPlotSize(true));
|
| 1215 |
+
controls.plotLargerBtn.addEventListener("click", () => setPlotSize(plotWrap.clientWidth * 1.14, plotWrap.clientHeight * 1.14, true));
|
| 1216 |
+
controls.plotSmallerBtn.addEventListener("click", () => setPlotSize(plotWrap.clientWidth / 1.14, plotWrap.clientHeight / 1.14, true));
|
| 1217 |
+
controls.plotFullscreenBtn.addEventListener("click", toggleFullscreen);
|
| 1218 |
+
|
| 1219 |
+
document.addEventListener("fullscreenchange", () => {
|
| 1220 |
+
controls.plotFullscreenBtn.textContent = document.fullscreenElement === plotWrap ? "Exit Fullscreen" : "Fullscreen";
|
| 1221 |
+
resizePlot();
|
| 1222 |
+
});
|
| 1223 |
+
|
| 1224 |
+
window.addEventListener("resize", () => {
|
| 1225 |
+
if (document.fullscreenElement !== plotWrap) {
|
| 1226 |
+
setPlotSize(plotWrap.clientWidth, plotWrap.clientHeight, true);
|
| 1227 |
+
}
|
| 1228 |
+
drawClassifier(controls.caseSelect.value);
|
| 1229 |
+
resizePlot();
|
| 1230 |
+
});
|
| 1231 |
+
|
| 1232 |
+
if (window.ResizeObserver) {
|
| 1233 |
+
state.resizeObserver = new window.ResizeObserver(() => {
|
| 1234 |
+
resizePlot();
|
| 1235 |
+
drawClassifier(controls.caseSelect.value);
|
| 1236 |
+
});
|
| 1237 |
+
state.resizeObserver.observe(plotWrap);
|
| 1238 |
+
state.resizeObserver.observe(classifierCanvas.parentElement);
|
| 1239 |
+
}
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
function init() {
|
| 1243 |
+
fitPlotSize(true);
|
| 1244 |
+
bindEvents();
|
| 1245 |
generateSyntheticData();
|
| 1246 |
+
applyCaseDefaults("case1");
|
| 1247 |
+
|
| 1248 |
+
if (!hasPlotly()) {
|
| 1249 |
+
showPlotFallback("Interactive 3D view unavailable (Plotly script did not load).");
|
| 1250 |
+
}
|
| 1251 |
+
}
|
| 1252 |
|
| 1253 |
+
init();
|
|
|
logistic-regression/styles.css
CHANGED
|
@@ -156,14 +156,138 @@ select {
|
|
| 156 |
margin: 0;
|
| 157 |
}
|
| 158 |
|
| 159 |
-
.
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
border: 1px solid var(--line);
|
| 163 |
border-radius: 10px;
|
| 164 |
background: #fff;
|
| 165 |
}
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
.stats {
|
| 168 |
margin-top: 0.8rem;
|
| 169 |
display: grid;
|
|
@@ -186,6 +310,58 @@ select {
|
|
| 186 |
font-size: 0.9rem;
|
| 187 |
}
|
| 188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
.page-copy {
|
| 190 |
max-width: 760px;
|
| 191 |
margin: 1.2rem auto;
|
|
@@ -196,6 +372,12 @@ select {
|
|
| 196 |
.layout {
|
| 197 |
grid-template-columns: 1fr;
|
| 198 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
.home-page {
|
|
|
|
| 156 |
margin: 0;
|
| 157 |
}
|
| 158 |
|
| 159 |
+
.model-definition {
|
| 160 |
+
border: 1px solid var(--line);
|
| 161 |
+
border-radius: 10px;
|
| 162 |
+
padding: 0.7rem 0.8rem;
|
| 163 |
+
background: #fcfeff;
|
| 164 |
+
font-size: 0.9rem;
|
| 165 |
+
color: #23343b;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.model-definition p {
|
| 169 |
+
margin: 0.18rem 0;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.landscape-summary {
|
| 173 |
+
margin: 0.7rem 0 0.8rem;
|
| 174 |
+
display: grid;
|
| 175 |
+
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
| 176 |
+
gap: 0.5rem;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.landscape-pill {
|
| 180 |
+
border: 1px solid var(--line);
|
| 181 |
+
border-radius: 8px;
|
| 182 |
+
padding: 0.42rem 0.55rem;
|
| 183 |
+
background: #fcfeff;
|
| 184 |
+
font-size: 0.88rem;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.landscape-controls {
|
| 188 |
+
display: flex;
|
| 189 |
+
flex-wrap: wrap;
|
| 190 |
+
align-items: center;
|
| 191 |
+
gap: 0.5rem;
|
| 192 |
+
margin-bottom: 0.7rem;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.landscape-controls label {
|
| 196 |
+
min-width: 0;
|
| 197 |
+
margin: 0;
|
| 198 |
+
width: auto;
|
| 199 |
+
font-weight: 700;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.landscape-controls select {
|
| 203 |
+
min-width: 220px;
|
| 204 |
+
width: auto;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.viz-btn {
|
| 208 |
+
border: 1px solid var(--line);
|
| 209 |
+
background: #fff;
|
| 210 |
+
color: var(--ink);
|
| 211 |
+
border-radius: 9px;
|
| 212 |
+
padding: 0.45rem 0.7rem;
|
| 213 |
+
font-weight: 600;
|
| 214 |
+
width: auto;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.viz-btn:hover {
|
| 218 |
+
border-color: var(--accent);
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.landscape-controls.toggles {
|
| 222 |
+
margin-bottom: 0.8rem;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.viz-toggle {
|
| 226 |
+
display: inline-flex;
|
| 227 |
+
align-items: center;
|
| 228 |
+
gap: 0.45rem;
|
| 229 |
+
border: 1px solid var(--line);
|
| 230 |
+
border-radius: 999px;
|
| 231 |
+
background: #fff;
|
| 232 |
+
padding: 0.36rem 0.62rem;
|
| 233 |
+
font-size: 0.88rem;
|
| 234 |
+
font-weight: 600;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.viz-toggle input {
|
| 238 |
+
margin: 0;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.landscape-plot-wrap {
|
| 242 |
+
width: min(100%, 980px);
|
| 243 |
+
height: 620px;
|
| 244 |
+
min-width: 320px;
|
| 245 |
+
min-height: 320px;
|
| 246 |
+
max-width: 1400px;
|
| 247 |
+
max-height: 900px;
|
| 248 |
+
resize: both;
|
| 249 |
+
overflow: auto;
|
| 250 |
border: 1px solid var(--line);
|
| 251 |
border-radius: 10px;
|
| 252 |
background: #fff;
|
| 253 |
}
|
| 254 |
|
| 255 |
+
.landscape-plot {
|
| 256 |
+
width: 100%;
|
| 257 |
+
height: 100%;
|
| 258 |
+
min-height: 100%;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.plot-fallback {
|
| 262 |
+
height: 100%;
|
| 263 |
+
min-height: 240px;
|
| 264 |
+
display: grid;
|
| 265 |
+
place-items: center;
|
| 266 |
+
text-align: center;
|
| 267 |
+
color: #2a3c45;
|
| 268 |
+
padding: 1rem;
|
| 269 |
+
font-size: 0.92rem;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.viz-info-row {
|
| 273 |
+
margin: 0.6rem 0 0.8rem;
|
| 274 |
+
display: grid;
|
| 275 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 276 |
+
gap: 0.55rem;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.viz-chip {
|
| 280 |
+
border: 1px solid var(--line);
|
| 281 |
+
border-radius: 8px;
|
| 282 |
+
padding: 0.45rem 0.6rem;
|
| 283 |
+
background: #fcfeff;
|
| 284 |
+
font-size: 0.88rem;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.viz-chip.warning {
|
| 288 |
+
border-color: #f2b84b;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
.stats {
|
| 292 |
margin-top: 0.8rem;
|
| 293 |
display: grid;
|
|
|
|
| 310 |
font-size: 0.9rem;
|
| 311 |
}
|
| 312 |
|
| 313 |
+
.slice-label {
|
| 314 |
+
margin: 0.65rem 0 0;
|
| 315 |
+
color: var(--muted);
|
| 316 |
+
font-size: 0.85rem;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.iteration-block {
|
| 320 |
+
margin-top: 0.9rem;
|
| 321 |
+
display: grid;
|
| 322 |
+
grid-template-columns: auto 1fr auto;
|
| 323 |
+
align-items: center;
|
| 324 |
+
gap: 0.55rem;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.iteration-block label {
|
| 328 |
+
margin: 0;
|
| 329 |
+
min-width: 0;
|
| 330 |
+
font-size: 0.9rem;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.iteration-block input[type="range"] {
|
| 334 |
+
width: 100%;
|
| 335 |
+
accent-color: var(--accent);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.iteration-block span {
|
| 339 |
+
font-size: 0.86rem;
|
| 340 |
+
color: #324b55;
|
| 341 |
+
font-weight: 600;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.classifier-panel {
|
| 345 |
+
margin-top: 0.9rem;
|
| 346 |
+
border: 1px solid var(--line);
|
| 347 |
+
border-radius: 10px;
|
| 348 |
+
padding: 0.65rem;
|
| 349 |
+
background: #fff;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.classifier-panel h3 {
|
| 353 |
+
margin: 0 0 0.5rem;
|
| 354 |
+
font-size: 0.98rem;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
#classifierCanvas {
|
| 358 |
+
width: 100%;
|
| 359 |
+
height: auto;
|
| 360 |
+
border: 1px solid var(--line);
|
| 361 |
+
border-radius: 10px;
|
| 362 |
+
background: #fff;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
.page-copy {
|
| 366 |
max-width: 760px;
|
| 367 |
margin: 1.2rem auto;
|
|
|
|
| 372 |
.layout {
|
| 373 |
grid-template-columns: 1fr;
|
| 374 |
}
|
| 375 |
+
|
| 376 |
+
.landscape-plot-wrap {
|
| 377 |
+
width: 100%;
|
| 378 |
+
height: 420px;
|
| 379 |
+
min-height: 280px;
|
| 380 |
+
}
|
| 381 |
}
|
| 382 |
|
| 383 |
.home-page {
|