Spaces:
Running
Running
| <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> | |
| <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> | |
| <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 <b>SGD</b>: noisy (1 sample) <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> | |