GD-Explorer-3D / index.html
mlnjsh's picture
Fix 3D surface with BufferGeometry, add Batch GD / SGD / Mini-batch algorithms
df0772b
Raw
History Blame Contribute Delete
22.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Gradient Descent Explorer β€” ML-II Book</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0d1117;color:#c9d1d9;overflow-x:hidden}
.header{text-align:center;padding:12px 10px 6px}
.header h1{font-size:1.4rem;color:#58d68d;margin-bottom:2px}
.header p{color:#8b949e;font-size:.78rem}
.main{display:flex;gap:10px;padding:0 10px 10px;height:calc(100vh - 62px);min-height:500px}
.view-panel{flex:1;background:#161b22;border:1px solid #30363d;border-radius:10px;position:relative;overflow:hidden;min-width:0}
.view-panel .label{position:absolute;top:8px;left:12px;font-size:.72rem;color:#8b949e;font-weight:600;text-transform:uppercase;letter-spacing:.5px;z-index:10;background:rgba(22,27,34,.9);padding:2px 8px;border-radius:4px}
.controls-panel{width:270px;flex-shrink:0;background:#161b22;border:1px solid #30363d;border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:8px;overflow-y:auto}
.cg{display:flex;flex-direction:column;gap:2px}
.cg label{font-size:.7rem;color:#8b949e;font-weight:600;text-transform:uppercase;letter-spacing:.4px}
.cg select{width:100%;background:#0d1117;border:1px solid #30363d;color:#c9d1d9;padding:6px 8px;border-radius:6px;font-size:.82rem}
.row{display:flex;justify-content:space-between;align-items:center}
.vd{font-size:.85rem;color:#f0c040;font-weight:bold}
input[type=range]{width:100%;-webkit-appearance:none;height:5px;background:#30363d;border-radius:3px;outline:none;margin-top:2px}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:#58d68d;cursor:pointer}
.btn-row{display:flex;gap:5px}
.btn{padding:7px 10px;border:none;border-radius:7px;font-size:.82rem;font-weight:600;cursor:pointer;transition:.2s;flex:1}
.btn-go{background:#58d68d;color:#0d1117}.btn-go:hover{background:#48c77d}
.btn-step{background:#2471a3;color:#fff}.btn-step:hover{background:#1a5a8a}
.btn-rst{background:#30363d;color:#c9d1d9}.btn-rst:hover{background:#3d444d}
.btn-go.running{background:#e74c3c}.btn-go.running:hover{background:#c0392b}
.stats{background:#0d1117;border:1px solid #30363d;border-radius:7px;padding:8px 10px;font-size:.75rem;line-height:1.6}
.stats .lb{color:#8b949e}.stats .vl{color:#f0c040;font-weight:bold}
.legend{font-size:.68rem;color:#8b949e;line-height:1.4}
.legend .d{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:2px;vertical-align:middle}
.info{background:#1a2332;border-left:3px solid #2471a3;padding:7px 9px;border-radius:0 6px 6px 0;font-size:.72rem;color:#8b949e;line-height:1.4}
.credit{text-align:center;font-size:.6rem;color:#444;padding:4px}
#contourCanvas{width:100%!important;height:100%!important;display:block}
@media(max-width:900px){.main{flex-direction:column;height:auto}.view-panel{min-height:280px}.controls-panel{width:100%}}
</style>
</head>
<body>
<div class="header">
<h1>3D Gradient Descent Explorer</h1>
<p>ML-II Book β€” Ch03: Logistic Regression Deep Dive | Dr Milan Amrut Joshi</p>
</div>
<div class="main">
<div class="view-panel" id="threeBox">
<span class="label">3D Surface β€” drag to rotate, scroll to zoom</span>
</div>
<div class="view-panel">
<span class="label">Contour Map β€” click to set start</span>
<canvas id="contourCanvas"></canvas>
</div>
<div class="controls-panel">
<div class="cg">
<label>Loss Function</label>
<select id="funcSel">
<option value="quadratic">1. Quadratic Bowl (Convex)</option>
<option value="rosenbrock">2. Rosenbrock Valley</option>
<option value="himmelblau">3. Himmelblau (4 Minima)</option>
</select>
</div>
<div class="cg">
<label>Optimizer Algorithm</label>
<select id="algoSel">
<option value="bgd">Batch Gradient Descent</option>
<option value="sgd">Stochastic Gradient Descent (SGD)</option>
<option value="mbgd">Mini-Batch Gradient Descent</option>
</select>
</div>
<div class="cg">
<div class="row"><label>Learning Rate (Ξ±)</label><span class="vd" id="lrV">0.050</span></div>
<input type="range" id="lrS" min="-3" max="0" step=".01" value="-1.3">
</div>
<div class="cg">
<div class="row"><label>Start X</label><span class="vd" id="sxV">-3.0</span></div>
<input type="range" id="sxS" min="-4.5" max="4.5" step=".1" value="-3.0">
</div>
<div class="cg">
<div class="row"><label>Start Y</label><span class="vd" id="syV">3.0</span></div>
<input type="range" id="syS" min="-4.5" max="4.5" step=".1" value="3.0">
</div>
<div class="cg">
<div class="row"><label>Speed</label><span class="vd" id="spV">5</span></div>
<input type="range" id="spS" min="1" max="20" step="1" value="5">
</div>
<div class="btn-row">
<button class="btn btn-go" id="goBtn">β–Ά Animate</button>
<button class="btn btn-step" id="stepBtn">Step</button>
<button class="btn btn-rst" id="rstBtn">Reset</button>
</div>
<div class="stats">
<span class="lb">Step:</span> <span class="vl" id="stN">0</span> &nbsp;
<span class="lb">Algorithm:</span> <span class="vl" id="algoD">Batch GD</span><br>
<span class="lb">Position:</span> <span class="vl" id="posD">(βˆ’3.0, 3.0)</span><br>
<span class="lb">Loss:</span> <span class="vl" id="losD">β€”</span> &nbsp;
<span class="lb">|βˆ‡|:</span> <span class="vl" id="grdD">β€”</span><br>
<span class="lb">Status:</span> <span class="vl" id="statD">Ready</span>
</div>
<div class="legend">
<div><span class="d" style="background:#e74c3c"></span>Start <span class="d" style="background:#f0c040"></span>Path <span class="d" style="background:#58d68d"></span>Current</div>
<div style="margin-top:3px"><b>Batch GD</b>: exact gradient &nbsp; <b>SGD</b>: noisy (1 sample) &nbsp; <b>Mini-batch</b>: moderate noise</div>
</div>
<div class="info" id="infoBox"><b>Quadratic Bowl</b>: f(x,y) = xΒ²+yΒ². Convex, single minimum at (0,0).</div>
</div>
</div>
<div class="credit">Dr Milan Amrut Joshi β€” ML-II: Supervised Learning Classification β€” Great Learning</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ─── FUNCTIONS ───
const F={
quadratic:{
f:(x,y)=>x*x+y*y,
g:(x,y)=>[2*x,2*y],
mn:[[0,0]],
bnd:{xn:-5,xx:5,yn:-5,yx:5},
zmax:50, zclamp:50,
lvl:[.1,.5,1,2,4,7,11,16,22,30,40],
info:'<b>Quadratic Bowl</b>: f(x,y) = xΒ² + yΒ². Perfectly convex, single global minimum at (0,0). GD always converges here.',
def:{lr:-1.3,sx:-3,sy:3}
},
rosenbrock:{
f:(x,y)=>(1-x)**2+100*(y-x*x)**2,
g:(x,y)=>[-2*(1-x)-400*x*(y-x*x), 200*(y-x*x)],
mn:[[1,1]],
bnd:{xn:-2.5,xx:2.5,yn:-1.5,yx:3.5},
zmax:2500, zclamp:2500,
lvl:[.1,1,5,20,50,100,200,500,1000,2000,4000],
info:'<b>Rosenbrock Valley</b>: f(x,y) = (1βˆ’x)Β² + 100(yβˆ’xΒ²)Β². Narrow banana valley. Minimum at (1,1).',
def:{lr:-2.5,sx:-1.5,sy:2}
},
himmelblau:{
f:(x,y)=>(x*x+y-11)**2+(x+y*y-7)**2,
g:(x,y)=>[4*x*(x*x+y-11)+2*(x+y*y-7), 2*(x*x+y-11)+4*y*(x+y*y-7)],
mn:[[3,2],[-2.805,3.131],[-3.779,-3.283],[3.584,-1.848]],
bnd:{xn:-6,xx:6,yn:-6,yx:6},
zmax:890, zclamp:600,
lvl:[.5,2,5,10,25,50,100,200,400,700,1200],
info:'<b>Himmelblau</b>: f(x,y) = (xΒ²+yβˆ’11)Β² + (x+yΒ²βˆ’7)Β². Four equal minima β€” where you end up depends on your start!',
def:{lr:-1.7,sx:-4,sy:4}
}
};
let cur='quadratic', algo='bgd', path=[], animOn=false, aTmr=null;
// ─── DOM ───
const $=id=>document.getElementById(id);
const cC=$('contourCanvas'), ctx=cC.getContext('2d');
const funcSel=$('funcSel'), algoSel=$('algoSel');
const lrS=$('lrS'),sxS=$('sxS'),syS=$('syS'),spS=$('spS');
const lrV=$('lrV'),sxV=$('sxV'),syV=$('syV'),spV=$('spV');
const goBtn=$('goBtn'),stepBtn=$('stepBtn'),rstBtn=$('rstBtn');
const stN=$('stN'),algoD=$('algoD'),posD=$('posD'),losD=$('losD'),grdD=$('grdD'),statD=$('statD'),infoBox=$('infoBox');
const getLR=()=>Math.pow(10,parseFloat(lrS.value));
const getSX=()=>parseFloat(sxS.value);
const getSY=()=>parseFloat(syS.value);
const getSpd=()=>parseInt(spS.value);
// ─── THREE.JS SETUP ───
const threeBox=$('threeBox');
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.setClearColor(0x0d1117);
threeBox.appendChild(renderer.domElement);
const scene=new THREE.Scene();
const camera=new THREE.PerspectiveCamera(50,1,.1,500);
// Lights
scene.add(new THREE.AmbientLight(0x6677aa,0.5));
const dl1=new THREE.DirectionalLight(0xffffff,0.9);dl1.position.set(5,12,8);scene.add(dl1);
const dl2=new THREE.DirectionalLight(0x4488cc,0.35);dl2.position.set(-6,8,-4);scene.add(dl2);
// Camera orbit
let drag=false,pM={x:0,y:0},camT=0.8,camP=1.0,camD=22;
function setCam(){
camera.position.set(camD*Math.sin(camP)*Math.cos(camT),camD*Math.cos(camP),camD*Math.sin(camP)*Math.sin(camT));
camera.lookAt(0,1.5,0);
}
setCam();
renderer.domElement.addEventListener('pointerdown',e=>{drag=true;pM={x:e.clientX,y:e.clientY};e.preventDefault()});
window.addEventListener('pointerup',()=>drag=false);
window.addEventListener('pointermove',e=>{if(!drag)return;camT-=(e.clientX-pM.x)*.008;camP=Math.max(.25,Math.min(1.5,camP-(e.clientY-pM.y)*.008));pM={x:e.clientX,y:e.clientY};setCam()});
renderer.domElement.addEventListener('wheel',e=>{camD=Math.max(10,Math.min(40,camD+e.deltaY*.025));setCam();e.preventDefault()},{passive:false});
// ─── 3D SURFACE ───
let surfMesh=null,pathLine=null,startBall=null,curBall=null,gridH=null;
const arrowGrp=new THREE.Group();scene.add(arrowGrp);
const RES=80;
const SIZE=10;
function buildSurface(){
if(surfMesh){scene.remove(surfMesh);surfMesh.geometry.dispose();surfMesh.material.dispose();surfMesh=null}
if(gridH){scene.remove(gridH);gridH=null}
const fn=F[cur],b=fn.bnd;
// Use BufferGeometry with index
const verts=new Float32Array((RES+1)*(RES+1)*3);
const cols=new Float32Array((RES+1)*(RES+1)*3);
const indices=[];
for(let j=0;j<=RES;j++){
for(let i=0;i<=RES;i++){
const idx=j*(RES+1)+i;
const u=i/RES, v=j/RES;
const wx=b.xn+u*(b.xx-b.xn);
const wy=b.yn+v*(b.yx-b.yn);
const z=Math.min(fn.f(wx,wy),fn.zclamp);
const h=z/fn.zmax*7;
verts[idx*3+0]=(u-.5)*SIZE; // x
verts[idx*3+1]=h; // y (height)
verts[idx*3+2]=(v-.5)*SIZE; // z
// Color by height
const t=Math.min(h/7,1);
let r,g,bl;
if(t<.12){r=.04;g=.1+t*3;bl=.22+t*4}
else if(t<.3){const s=(t-.12)/.18;r=.04+s*.15;g=.46+s*.28;bl=.7-s*.22}
else if(t<.6){const s=(t-.3)/.3;r=.19+s*.58;g=.74+s*.02;bl=.48-s*.28}
else{const s=(t-.6)/.4;r=.77+s*.18;g=.76-s*.42;bl=.2-s*.1}
cols[idx*3+0]=r;cols[idx*3+1]=g;cols[idx*3+2]=bl;
}
}
for(let j=0;j<RES;j++){
for(let i=0;i<RES;i++){
const a=j*(RES+1)+i;
const b2=a+1;
const c2=a+(RES+1);
const d=c2+1;
indices.push(a,b2,c2, b2,d,c2);
}
}
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.BufferAttribute(verts,3));
geo.setAttribute('color',new THREE.BufferAttribute(cols,3));
geo.setIndex(indices);
geo.computeVertexNormals();
const mat=new THREE.MeshPhongMaterial({
vertexColors:true,side:THREE.DoubleSide,
shininess:40,transparent:true,opacity:0.9
});
surfMesh=new THREE.Mesh(geo,mat);
scene.add(surfMesh);
gridH=new THREE.GridHelper(SIZE,20,0x222244,0x181830);
gridH.position.y=-0.05;
scene.add(gridH);
}
function w3d(wx,wy){
const fn=F[cur],b=fn.bnd;
const u=(wx-b.xn)/(b.xx-b.xn),v=(wy-b.yn)/(b.yx-b.yn);
const z=Math.min(fn.f(wx,wy),fn.zclamp);
return new THREE.Vector3((u-.5)*SIZE, z/fn.zmax*7, (v-.5)*SIZE);
}
function clearPathViz(){
if(pathLine){scene.remove(pathLine);pathLine.geometry.dispose();pathLine.material.dispose();pathLine=null}
if(startBall){scene.remove(startBall);startBall=null}
if(curBall){scene.remove(curBall);curBall=null}
while(arrowGrp.children.length>0){const c2=arrowGrp.children[0];arrowGrp.remove(c2);if(c2.geometry)c2.geometry.dispose();if(c2.material)c2.material.dispose()}
}
function updatePathViz(){
clearPathViz();
if(path.length===0)return;
const pts=path.map(p=>{const v=w3d(p[0],p[1]);v.y+=.12;return v});
// Path line
const positions=new Float32Array(pts.length*3);
pts.forEach((p,i)=>{positions[i*3]=p.x;positions[i*3+1]=p.y;positions[i*3+2]=p.z});
const lineGeo=new THREE.BufferGeometry();
lineGeo.setAttribute('position',new THREE.BufferAttribute(positions,3));
const lineMat=new THREE.LineBasicMaterial({color:0xf0c040});
pathLine=new THREE.Line(lineGeo,lineMat);
scene.add(pathLine);
// Start ball
const sg=new THREE.SphereBufferGeometry(.18,12,12);
const sm=new THREE.MeshPhongMaterial({color:0xe74c3c,emissive:0x550000});
startBall=new THREE.Mesh(sg,sm);
startBall.position.copy(pts[0]);startBall.position.y+=.1;
scene.add(startBall);
// Current ball
const cg=new THREE.SphereBufferGeometry(.22,12,12);
const cm=new THREE.MeshPhongMaterial({color:0x58d68d,emissive:0x004400});
curBall=new THREE.Mesh(cg,cm);
curBall.position.copy(pts[pts.length-1]);curBall.position.y+=.12;
scene.add(curBall);
// Arrows
const astep=Math.max(1,Math.floor(path.length/18));
for(let i=0;i<pts.length-1;i+=astep){
const from=pts[i],toIdx=Math.min(i+astep,pts.length-1),to=pts[toIdx];
const dir=new THREE.Vector3().subVectors(to,from);
const len=dir.length();if(len<.02)continue;
dir.normalize();
// Cone arrowhead
const hGeo=new THREE.ConeBufferGeometry(.08,.22,6);
const hMat=new THREE.MeshPhongMaterial({color:0xf0c040,emissive:0x332200});
const head=new THREE.Mesh(hGeo,hMat);
const mid=new THREE.Vector3().addVectors(from,to).multiplyScalar(.5);
head.position.copy(mid);
const up=new THREE.Vector3(0,1,0);
head.quaternion.setFromUnitVectors(up,dir);
arrowGrp.add(head);
}
}
// ─── CONTOUR ───
function resizeContour(){const p=cC.parentElement;cC.width=p.clientWidth;cC.height=p.clientHeight}
function w2c(wx,wy){const b=F[cur].bnd;return[(wx-b.xn)/(b.xx-b.xn)*cC.width,cC.height-(wy-b.yn)/(b.yx-b.yn)*cC.height]}
function c2w(px,py){const b=F[cur].bnd;return[b.xn+(px/cC.width)*(b.xx-b.xn),b.yn+((cC.height-py)/cC.height)*(b.yx-b.yn)]}
function drawContour(){
const W=cC.width,H=cC.height;if(W<2||H<2)return;
const fn=F[cur],b=fn.bnd;
const sc=2,sw=Math.floor(W/sc),sh=Math.floor(H/sc);
const off=document.createElement('canvas');off.width=sw;off.height=sh;
const oc=off.getContext('2d');
const img=oc.createImageData(sw,sh);const d=img.data;
let mx=1;const vals=new Float32Array(sw*sh);
for(let py=0;py<sh;py++)for(let px=0;px<sw;px++){
const wx=b.xn+(px/sw)*(b.xx-b.xn),wy=b.yn+((sh-py)/sh)*(b.yx-b.yn);
const v=fn.f(wx,wy);vals[py*sw+px]=v;if(v<1e6)mx=Math.max(mx,v);
}
for(let py=0;py<sh;py++)for(let px=0;px<sw;px++){
const v=vals[py*sw+px],t=Math.min(Math.log(v+1)/Math.log(mx+1),1);
let r,g,bl;
if(t<.25){const s=t/.25;r=13;g=17+s*60|0;bl=23+s*80|0}
else if(t<.5){const s=(t-.25)/.25;r=13+s*30|0;g=77+s*80|0;bl=103-s*30|0}
else if(t<.75){const s=(t-.5)/.25;r=43+s*160|0;g=157+s*40|0;bl=73-s*50|0}
else{const s=(t-.75)/.25;r=203+s*28|0;g=197-s*120|0;bl=23-s*23|0}
const i=(py*sw+px)*4;d[i]=r;d[i+1]=g;d[i+2]=bl;d[i+3]=255;
}
oc.putImageData(img,0,0);
ctx.imageSmoothingEnabled=true;ctx.drawImage(off,0,0,W,H);
// Contour lines
ctx.fillStyle='rgba(255,255,255,.15)';
for(const lv of fn.lvl)for(let py=1;py<sh-1;py++)for(let px=1;px<sw-1;px++){
const v=vals[py*sw+px],vr=vals[py*sw+px+1],vb=vals[(py+1)*sw+px];
if((v<lv&&vr>=lv)||(v>=lv&&vr<lv)||(v<lv&&vb>=lv)||(v>=lv&&vb<lv))ctx.fillRect(px*sc,py*sc,sc,sc);
}
// Minima
ctx.strokeStyle='#fff';ctx.lineWidth=2;
for(const m of fn.mn){const[px,py]=w2c(m[0],m[1]);ctx.beginPath();ctx.moveTo(px-5,py-5);ctx.lineTo(px+5,py+5);ctx.moveTo(px+5,py-5);ctx.lineTo(px-5,py+5);ctx.stroke()}
// Axes
const[ox,oy]=w2c(0,0);ctx.strokeStyle='rgba(255,255,255,.1)';ctx.lineWidth=.5;ctx.setLineDash([4,4]);
if(ox>0&&ox<W){ctx.beginPath();ctx.moveTo(ox,0);ctx.lineTo(ox,H);ctx.stroke()}
if(oy>0&&oy<H){ctx.beginPath();ctx.moveTo(0,oy);ctx.lineTo(W,oy);ctx.stroke()}
ctx.setLineDash([]);
// Path
if(path.length>0){
ctx.strokeStyle='rgba(240,192,64,.85)';ctx.lineWidth=2;ctx.beginPath();
const[sx,sy]=w2c(path[0][0],path[0][1]);ctx.moveTo(sx,sy);
for(let i=1;i<path.length;i++){const[px,py]=w2c(path[i][0],path[i][1]);ctx.lineTo(px,py)}
ctx.stroke();
// Arrows
const as=Math.max(1,Math.floor(path.length/30));
for(let i=0;i<path.length-1;i+=as){
const[x1,y1]=w2c(path[i][0],path[i][1]);
const j=Math.min(i+as,path.length-1);const[x2,y2]=w2c(path[j][0],path[j][1]);
const dx=x2-x1,dy=y2-y1,l=Math.sqrt(dx*dx+dy*dy);if(l<3)continue;
const mx2=(x1+x2)/2,my2=(y1+y2)/2,ang=Math.atan2(dy,dx);
ctx.save();ctx.translate(mx2,my2);ctx.rotate(ang);
ctx.fillStyle='#f0c040';ctx.beginPath();ctx.moveTo(5,0);ctx.lineTo(-3,-3);ctx.lineTo(-3,3);ctx.closePath();ctx.fill();
ctx.restore();
}
// Dots
const[s0x,s0y]=w2c(path[0][0],path[0][1]);
ctx.beginPath();ctx.arc(s0x,s0y,5,0,Math.PI*2);ctx.fillStyle='#e74c3c';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=1.5;ctx.stroke();
const lp=path[path.length-1];const[lx,ly]=w2c(lp[0],lp[1]);
ctx.beginPath();ctx.arc(lx,ly,6,0,Math.PI*2);ctx.fillStyle='#58d68d';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=1.5;ctx.stroke();
}
ctx.fillStyle='#555';ctx.font='10px sans-serif';ctx.fillText('x',W-14,H-8);ctx.fillText('y',6,12);
}
// ─── GD ALGORITHMS ───
function gaussRand(){let u=0,v=0;while(!u)u=Math.random();while(!v)v=Math.random();return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v)}
function doStep(){
const fn=F[cur],lr=getLR();
if(!path.length)path.push([getSX(),getSY()]);
const last=path[path.length-1];
const g=fn.g(last[0],last[1]);
const gn=Math.sqrt(g[0]*g[0]+g[1]*g[1]);
if(gn<1e-6||path.length>5000)return false;
let gx=g[0],gy=g[1];
if(algo==='sgd'){
// SGD: large noise simulating single-sample variance
const ns=gn*0.7+0.4;
gx+=gaussRand()*ns; gy+=gaussRand()*ns;
} else if(algo==='mbgd'){
// Mini-batch: moderate noise
const ns=gn*0.25+0.12;
gx+=gaussRand()*ns; gy+=gaussRand()*ns;
}
let nx=last[0]-lr*gx, ny=last[1]-lr*gy;
const bd=fn.bnd;
nx=Math.max(bd.xn*1.5,Math.min(bd.xx*1.5,nx));
ny=Math.max(bd.yn*1.5,Math.min(bd.yx*1.5,ny));
if(isNaN(fn.f(nx,ny))||fn.f(nx,ny)>1e10)return false;
path.push([nx,ny]);return true;
}
function updateStats(){
const fn=F[cur];
const nm={bgd:'Batch GD',sgd:'SGD',mbgd:'Mini-batch GD'};
algoD.textContent=nm[algo];
if(!path.length){stN.textContent='0';posD.textContent=`(${getSX().toFixed(1)}, ${getSY().toFixed(1)})`;losD.textContent=fn.f(getSX(),getSY()).toFixed(3);grdD.textContent='β€”';statD.textContent='Ready';statD.style.color='#c9d1d9';return}
const lp=path[path.length-1],l=fn.f(lp[0],lp[1]),g=fn.g(lp[0],lp[1]),gn=Math.sqrt(g[0]*g[0]+g[1]*g[1]);
stN.textContent=path.length-1;
posD.textContent=`(${lp[0].toFixed(3)}, ${lp[1].toFixed(3)})`;
losD.textContent=l<.001?l.toExponential(2):l.toFixed(4);
grdD.textContent=gn<.001?gn.toExponential(2):gn.toFixed(4);
if(gn<1e-4){statD.textContent='Converged!';statD.style.color='#58d68d'}
else if(l>1e8){statD.textContent='Diverged!';statD.style.color='#e74c3c'}
else{statD.textContent='Descending...';statD.style.color='#f0c040'}
}
function fullRender(){drawContour();updatePathViz();updateStats()}
function reset(){stopAnim();path=[];fullRender()}
function step(){if(!path.length)path.push([getSX(),getSY()]);doStep();fullRender()}
function startAnim(){
if(animOn){stopAnim();return}
animOn=true;goBtn.textContent='⏸ Pause';goBtn.classList.add('running');
if(!path.length)path.push([getSX(),getSY()]);
function tick(){let ok=true;for(let i=0;i<getSpd()&&ok;i++)ok=doStep();fullRender();if(!ok){stopAnim();return}aTmr=requestAnimationFrame(tick)}
aTmr=requestAnimationFrame(tick);
}
function stopAnim(){animOn=false;if(aTmr)cancelAnimationFrame(aTmr);aTmr=null;goBtn.textContent='β–Ά Animate';goBtn.classList.remove('running')}
// ─── EVENTS ───
goBtn.onclick=startAnim;stepBtn.onclick=step;rstBtn.onclick=reset;
funcSel.onchange=()=>{cur=funcSel.value;infoBox.innerHTML=F[cur].info;const d=F[cur].def;lrS.value=d.lr;sxS.value=d.sx;syS.value=d.sy;updDisp();buildSurface();reset()};
algoSel.onchange=()=>{algo=algoSel.value;reset()};
lrS.oninput=()=>lrV.textContent=getLR().toFixed(4);
sxS.oninput=()=>{sxV.textContent=getSX().toFixed(1);reset()};
syS.oninput=()=>{syV.textContent=getSY().toFixed(1);reset()};
spS.oninput=()=>spV.textContent=getSpd();
function updDisp(){lrV.textContent=getLR().toFixed(4);sxV.textContent=getSX().toFixed(1);syV.textContent=getSY().toFixed(1)}
cC.onclick=e=>{const r=cC.getBoundingClientRect();const px=(e.clientX-r.left)*(cC.width/r.width),py=(e.clientY-r.top)*(cC.height/r.height);const[wx,wy]=c2w(px,py);sxS.value=wx.toFixed(1);syS.value=wy.toFixed(1);updDisp();reset()};
// ─── RESIZE & RENDER LOOP ───
function onResize(){const w=threeBox.clientWidth,h=threeBox.clientHeight;renderer.setSize(w,h);camera.aspect=w/h;camera.updateProjectionMatrix();resizeContour();fullRender()}
window.addEventListener('resize',onResize);
function animate(){
requestAnimationFrame(animate);
if(curBall&&path.length){const lp=path[path.length-1];const p3=w3d(lp[0],lp[1]);curBall.position.y=p3.y+.12+Math.sin(Date.now()*.004)*.06}
renderer.render(scene,camera);
}
// ─── INIT ───
setTimeout(()=>{onResize();buildSurface();fullRender();animate()},100);
</script>
</body>
</html>