▶ 展示 · 回索引
sgd-stochastic-gradient-descent.html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<title>隨機梯度下降 SGD — 全資料梯度太貴?</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
html,body{margin:0;height:100%;background:#020617;}
body{display:flex;align-items:center;justify-content:center;overflow:hidden;}
canvas{width:min(100vw,calc(100vh*16/9));aspect-ratio:16/9;display:block;}
</style>
</head>
<body>
<canvas id="c" width="1920" height="1080"></canvas>
<script>
const cv=document.getElementById('c'),ctx=cv.getContext('2d');
const SANS='"Noto Sans TC","Microsoft JhengHei","PingFang TC","Segoe UI",sans-serif';
const MATH='"Cambria Math","STIX Two Math","Times New Roman","Microsoft JhengHei","Noto Sans TC",serif';
const C={panel:'#16213a',panel2:'#0f1a30',line:'#2c3b5c',txt:'#e8eefc',dim:'#9fb0d0',
cyan:'#38bdf8',green:'#4ade80',orange:'#fb923c',purple:'#a78bfa',pink:'#f472b6',gray:'#94a3b8',red:'#f87171'};
/* ---------- text helpers ---------- */
function T(s,x,y,o={}){
let size=o.size||24;
ctx.font=`${o.weight||'400'} ${size}px ${SANS}`;
let w=ctx.measureText(s).width;
if(o.maxW&&w>o.maxW){size*=o.maxW/w;ctx.font=`${o.weight||'400'} ${size}px ${SANS}`;w=o.maxW;}
ctx.fillStyle=o.color||C.txt;ctx.textBaseline='alphabetic';ctx.textAlign='left';
let sx=x; if(o.align==='center')sx=x-w/2; else if(o.align==='right')sx=x-w;
ctx.fillText(s,sx,y);return w;
}
function parse(s){
const out=[];let buf='';
for(let i=0;i<s.length;){
const c=s[i];
if((c==='_'||c==='^')&&s[i+1]==='{'){
if(buf){out.push({t:buf});buf='';}
let sub='',sup='';
while((s[i]==='_'||s[i]==='^')&&s[i+1]==='{'){
const k=s.indexOf('}',i);const g=s.slice(i+2,k);
if(s[i]==='_')sub=g;else sup=g;i=k+1;
}
out.push({sub,sup});
}else{buf+=c;i++;}
}
if(buf)out.push({t:buf});return out;
}
function runsW(s,size,wt){
let w=0;
for(const r of parse(s)){
if(r.t!==undefined){ctx.font=`${wt} ${size}px ${MATH}`;w+=ctx.measureText(r.t).width;}
else{ctx.font=`${wt} ${size*0.66}px ${MATH}`;
w+=Math.max(ctx.measureText(r.sub).width,ctx.measureText(r.sup).width)+1;}
}
return w;
}
function drawRuns(s,x,y,size,color,wt){
ctx.fillStyle=color;ctx.textAlign='left';ctx.textBaseline='alphabetic';
for(const r of parse(s)){
if(r.t!==undefined){ctx.font=`${wt} ${size}px ${MATH}`;ctx.fillText(r.t,x,y);x+=ctx.measureText(r.t).width;}
else{const fs=size*0.66;ctx.font=`${wt} ${fs}px ${MATH}`;
const ws=ctx.measureText(r.sub).width,wp=ctx.measureText(r.sup).width;
if(r.sub)ctx.fillText(r.sub,x,y+size*0.26);
if(r.sup)ctx.fillText(r.sup,x,y-size*0.4);
x+=Math.max(ws,wp)+1;}
}
return x;
}
function itemW(it,size,wt){
if(typeof it==='string')return runsW(it,size,wt);
const fs=size*0.85;return Math.max(runsW(it.f[0],fs,wt),runsW(it.f[1],fs,wt))+12;
}
function M(items,x,y,size,color,align,maxW,wt){
wt=wt||'400';color=color||C.txt;
if(!Array.isArray(items))items=[items];
let tot=items.reduce((a,it)=>a+itemW(it,size,wt),0);
if(maxW&&tot>maxW){size*=maxW/tot;tot=maxW;}
let cx=x;if(align==='center')cx=x-tot/2;else if(align==='right')cx=x-tot;
for(const it of items){
if(typeof it==='string'){cx=drawRuns(it,cx,y,size,color,wt);}
else{
const fs=size*0.85,fw=itemW(it,size,wt),axis=y-size*0.3;
const wn=runsW(it.f[0],fs,wt),wd=runsW(it.f[1],fs,wt);
drawRuns(it.f[0],cx+(fw-wn)/2,axis-6,fs,color,wt);
drawRuns(it.f[1],cx+(fw-wd)/2,axis+8+fs*0.72,fs,color,wt);
ctx.strokeStyle=color;ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(cx+2,axis);ctx.lineTo(cx+fw-2,axis);ctx.stroke();
cx+=fw;
}
}
return tot;
}
/* ---------- shapes ---------- */
function rr(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.arcTo(x+w,y,x+w,y+h,r);ctx.arcTo(x+w,y+h,x,y+h,r);ctx.arcTo(x,y+h,x,y,r);ctx.arcTo(x,y,x+w,y,r);ctx.closePath();}
function panel(x,y,w,h,n,zh,en,col){
rr(x,y,w,h,18);ctx.fillStyle=C.panel;ctx.fill();
ctx.strokeStyle=col;ctx.globalAlpha=.55;ctx.lineWidth=2;ctx.stroke();ctx.globalAlpha=1;
ctx.fillStyle=col;ctx.beginPath();ctx.arc(x+36,y+36,20,0,Math.PI*2);ctx.fill();
T(String(n),x+36,y+45,{size:26,weight:'700',color:'#0b1220',align:'center'});
T(zh,x+68,y+34,{size:28,weight:'700',color:C.txt,maxW:w-88});
T(en,x+68,y+62,{size:22,color:C.dim,maxW:w-88});
}
function circle(x,y,r,fill,stroke,lw){ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fillStyle=fill;ctx.fill();if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=lw||2;ctx.stroke();}}
function label(s,x,y,size,col,bg){
const w=runsW(s,size,'400');
rr(x-w/2-6,y-size*0.85,w+12,size*1.25,6);ctx.fillStyle=bg||C.panel;ctx.fill();
M(s,x,y,size,col,'center');
}
/* ---------- SGD simulation (deterministic) ---------- */
let seed=7;function rnd(){seed=(seed*1664525+1013904223)%4294967296;return seed/4294967296;}
function gauss(){return Math.sqrt(-2*Math.log(rnd()+1e-9))*Math.cos(2*Math.PI*rnd());}
const CX=920,CY=326,AX=60,AY=26,STEPS=46;
const gd=[[760,392]],sg=[[760,392]];
(function(){
const eta=520;let a=[760,392],b=[760,392];
for(let k=0;k<STEPS;k++){
let dx=a[0]-CX,dy=a[1]-CY;a=[a[0]-eta*dx/(AX*AX)*0.55,a[1]-eta*dy/(AY*AY)*0.55];gd.push(a);
dx=b[0]-CX;dy=b[1]-CY;
b=[b[0]-eta*(dx/(AX*AX))*0.55+gauss()*13,b[1]-eta*(dy/(AY*AY))*0.55+gauss()*17];
b[0]=Math.max(660,Math.min(1180,b[0]));b[1]=Math.max(244,Math.min(408,b[1]));sg.push(b);
}
})();
function path(pts,n,col,lw){
ctx.strokeStyle=col;ctx.lineWidth=lw;ctx.lineJoin='round';ctx.beginPath();
for(let i=0;i<=n&&i<pts.length;i++){i?ctx.lineTo(pts[i][0],pts[i][1]):ctx.moveTo(pts[i][0],pts[i][1]);}
ctx.stroke();
for(let i=0;i<=n&&i<pts.length;i++){circle(pts[i][0],pts[i][1],i===n?6:3,col);}
}
/* ---------- main draw ---------- */
function draw(ts){
ctx.clearRect(0,0,1920,1080);
const g=ctx.createLinearGradient(0,0,1920,1080);g.addColorStop(0,'#0b1220');g.addColorStop(1,'#111d38');
ctx.fillStyle=g;ctx.fillRect(0,0,1920,1080);
/* header */
T('全資料梯度太貴?隨機梯度下降(SGD)',50,80,{size:54,weight:'800',color:'#ffffff'});
T('Full-Data Gradients Too Expensive? Stochastic Gradient Descent (SGD)',52,122,{size:28,color:C.cyan,maxW:1240});
rr(1330,34,540,88,16);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.orange;ctx.lineWidth=2;ctx.stroke();
T('每步只隨機抽一筆 i',1600,72,{size:30,weight:'700',color:C.orange,align:'center',maxW:500});
T('One random sample per step approximates the full gradient',1600,104,{size:22,color:C.dim,align:'center',maxW:510});
const py=150,ph=440;
/* ===== Panel 1: problem ===== */
let px=50,pw=560;
panel(px,py,pw,ph,1,'問題設定:式 (20)–(21)','Problem Setup: Eqs. (20)–(21)',C.cyan);
M('資料 {x_{i}}、標籤 {l_{i}},i = 1…n',px+24,262,24,C.dim,'left',pw-48);
M('(20)',px+20,332,26,C.cyan);
M(['min_{w} ',{f:['1','n']},' Σ_{i=1}^{n} f_{i}(w)'],px+85,332,32,C.txt,'left',pw-110);
T('總 loss = 各樣本 loss 的平均|mean of per-sample losses',px+85,372,{size:22,color:C.dim,maxW:pw-105});
M('(21)',px+20,438,26,C.cyan);
M(['∇f(w) = ',{f:['1','n']},' Σ_{i=1}^{n} ∇f_{i}(w)'],px+85,438,32,C.txt,'left',pw-110);
T('全梯度 = 各樣本梯度的平均|full gradient = mean of gradients',px+85,478,{size:22,color:C.dim,maxW:pw-105});
rr(px+20,498,pw-40,80,12);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.red;ctx.lineWidth=1.8;ctx.stroke();
M('Full-batch 每輪要算完整求和:O(n)',px+pw/2,531,25,C.red,'center',pw-60,'700');
T('n 達百萬、十億就吃不消|O(n) per step: too slow at scale',px+pw/2,563,{size:22,color:C.txt,align:'center',maxW:pw-60});
/* ===== Panel 2: SGD update + contours ===== */
px=630;pw=580;
panel(px,py,pw,ph,2,'SGD 更新:式 (22)','SGD Update: Eq. (22)',C.orange);
ctx.save();rr(px+20,232,pw-40,196,10);ctx.fillStyle=C.panel2;ctx.fill();ctx.clip();
for(let k=1;k<=4;k++){ctx.beginPath();ctx.ellipse(CX,CY,AX*k*0.95,AY*k*0.95,0,0,Math.PI*2);
ctx.strokeStyle=C.line;ctx.lineWidth=1.5;ctx.stroke();}
const cyc=((ts||0)%9000)/9000,n=Math.min(STEPS,Math.floor(cyc/0.8*STEPS));
path(gd,Math.min(n,STEPS),C.cyan,3);
path(sg,Math.min(n,STEPS),C.orange,2.2);
circle(CX,CY,7,'#ffffff');
ctx.restore();
T('全梯度 full-batch',px+34,258,{size:22,color:C.cyan,weight:'700'});
T('SGD 單筆 single sample',px+pw-34,258,{size:22,color:C.orange,weight:'700',align:'right'});
T('最優點 optimum ●',px+pw-34,420,{size:22,color:C.txt,align:'right'});
M('(22)',px+20,472,26,C.orange);
M('w^{(k+1)} := w^{(k)} − η^{(k)} ∇f_{i}(w^{(k)})',px+80,472,30,C.txt,'left',pw-100);
T('每步只隨機抽一筆 i|sample one index i per step',px+80,502,{size:22,color:C.dim,maxW:pw-100});
M('而非 w^{(k+1)} := w^{(k)} − η∇f(w^{(k)})',px+80,543,25,C.gray,'left',pw-100);
T('雜訊估計,但期望方向仍指向全梯度|noisy, right on average',px+24,577,{size:22,color:C.dim,maxW:pw-48});
/* ===== Panel 3: learning rate ===== */
px=1230;pw=640;
panel(px,py,pw,ph,3,'自適應學習率 η^{(k)}'.replace('^{(k)}','(k)'),'Adaptive Learning Rate η(k)',C.green);
const X0=px+50,X1=px+pw-24,Y0=238,Y1=372;
ctx.strokeStyle=C.gray;ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(X0,Y0-6);ctx.lineTo(X0,Y1);ctx.lineTo(X1,Y1);ctx.stroke();
M('η^{(k)}',X0+8,Y0+10,22,C.dim);T('k →',X1,Y1-8,{size:22,color:C.dim,align:'right'});
T('示意 illustrative',X1,Y0+10,{size:22,color:C.dim,align:'right'});
const fx=k=>X0+(k-1)/19*(X1-X0-30),fy=v=>Y1-v*(Y1-Y0-24);
const curves=[[C.cyan,k=>1/k,false],[C.green,k=>1/Math.sqrt(k),false],[C.orange,k=>0.32,true]];
const tk=1+((ts||0)%6000)/6000*19;
for(const [col,f,dash] of curves){
ctx.strokeStyle=col;ctx.lineWidth=3.5;ctx.setLineDash(dash?[10,8]:[]);ctx.beginPath();
for(let i=0;i<=190;i++){const k=1+i/10;i?ctx.lineTo(fx(k),fy(f(k))):ctx.moveTo(fx(k),fy(f(k)));}
ctx.stroke();ctx.setLineDash([]);circle(fx(tk),fy(f(tk)),6,col);
}
const rows=[
[C.cyan,'η^{(k)} = 1/k','早期大步探索,後期細調','big steps early, fine steps late'],
[C.green,'η^{(k)} = 1/√k','衰減比 1/k 慢,實務常用','decays slower; common in practice'],
[C.orange,'η^{(k)} = η','固定:簡單,但可能停在最優點附近','may stall in an O(η) neighborhood (§4.2)']];
rows.forEach((r,i)=>{
const ty=398+i*62;
rr(px+16,ty,pw-32,56,10);ctx.fillStyle=C.panel2;ctx.fill();
ctx.fillStyle=r[0];ctx.fillRect(px+16,ty+8,5,40);
M(r[1],px+34,ty+37,28,r[0],'left',170,'700');
T(r[2],px+220,ty+24,{size:22,color:C.txt,maxW:pw-236});
T(r[3],px+220,ty+49,{size:22,color:C.dim,maxW:pw-236});
});
/* ===== Panel 4: comparison table ===== */
px=50;pw=950;const ry0=610,ph2=350;
panel(px,ry0,pw,ph2,4,'Full-batch vs SGD 對照','Full-Batch vs SGD at a Glance',C.purple);
const c0=px+18,c1=px+236,c2=px+590;
rr(c1-8,684,340,34,8);ctx.fillStyle=C.cyan;ctx.globalAlpha=.25;ctx.fill();ctx.globalAlpha=1;
rr(c2-8,684,pw-(c2-px)-10,34,8);ctx.fillStyle=C.orange;ctx.globalAlpha=.25;ctx.fill();ctx.globalAlpha=1;
T('Full-batch 全批次',c1+162,709,{size:24,weight:'700',color:C.cyan,align:'center'});
T('SGD 單筆隨機抽樣',c2+170,709,{size:24,weight:'700',color:C.orange,align:'center'});
const tb=[
['每步資料','Data per step','全部 n 筆','all n samples','隨機抽 1 筆 i','one random sample i'],
['更新式','Update rule','w^{(k)} − η∇f(w^{(k)})','full gradient, eq. (21)','w^{(k)} − η^{(k)}∇f_{i}(w^{(k)})','single-sample gradient, eq. (22)'],
['每步成本','Cost per step','O(n),n 大就吃不消','too slow at scale','便宜、可擴展','cheap and scalable'],
['梯度品質','Gradient quality','精確的全體平均','exact average','有雜訊,期望仍指向全梯度','noisy; right direction on average']];
tb.forEach((r,i)=>{
const y=724+i*58;
rr(c0,y,pw-36,54,10);ctx.fillStyle=C.panel2;ctx.fill();
T(r[0],c0+14,y+23,{size:22,weight:'700',color:C.purple,maxW:190});
T(r[1],c0+14,y+48,{size:22,color:C.dim,maxW:190});
if(i===1){M(r[2],c1,y+24,24,C.txt,'left',330);M(r[4],c2,y+24,24,C.txt,'left',pw-(c2-px)-30);}
else{T(r[2],c1,y+23,{size:22,color:C.txt,maxW:330});T(r[4],c2,y+23,{size:22,color:C.txt,maxW:pw-(c2-px)-30});}
T(r[3],c1,y+48,{size:22,color:C.dim,maxW:330});
T(r[5],c2,y+48,{size:22,color:C.dim,maxW:pw-(c2-px)-30});
});
/* ===== Panel 5: mini-batch ===== */
px=1020;pw=850;
panel(px,ry0,pw,ph2,5,'與 mini-batch 的關係','Relation to Mini-Batch SGD',C.pink);
const bx0=px+50,bx1=px+pw-50,by=770,bm=(bx0+bx1)/2;
const bg=ctx.createLinearGradient(bx0,0,bx1,0);bg.addColorStop(0,C.orange);bg.addColorStop(1,C.cyan);
rr(bx0,by-9,bx1-bx0,18,9);ctx.fillStyle=bg;ctx.fill();
circle(bx0,by,15,C.panel2,C.orange,4);circle(bm,by,15,C.panel2,C.pink,4);circle(bx1,by,15,C.panel2,C.cyan,4);
T('B = 1 單筆 SGD(本頁)',bx0-20,716,{size:24,weight:'700',color:C.orange,maxW:290});
T('single-sample (this page)',bx0-20,742,{size:22,color:C.dim,maxW:290});
T('1 < B < n mini-batch',bm,716,{size:24,weight:'700',color:C.pink,align:'center',maxW:250});
T('§4.1 下一節 next section',bm,742,{size:22,color:C.dim,align:'center',maxW:250});
T('B = n 全批次',bx1+20,716,{size:24,weight:'700',color:C.cyan,align:'right',maxW:250});
T('full-batch',bx1+20,742,{size:22,color:C.dim,align:'right',maxW:250});
T('← 更便宜、雜訊大|cheaper, noisier',bx0-20,812,{size:22,color:C.orange,maxW:340});
T('更穩、成本高|steadier, costlier →',bx1+20,812,{size:22,color:C.cyan,align:'right',maxW:340});
rr(px+20,830,pw-40,120,12);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.pink;ctx.lineWidth=1.8;ctx.stroke();
T('實務上說的「SGD」多半是 mini-batch SGD:',px+40,860,{size:24,weight:'700',color:C.txt,maxW:pw-80});
T('用一小批平均梯度,在速度與穩定之間折衷。',px+40,890,{size:24,weight:'700',color:C.txt,maxW:pw-80});
T('In practice, “SGD” usually means mini-batch SGD: an averaged small batch trades speed for stability.',px+40,926,{size:22,color:C.dim,maxW:pw-80});
/* ===== footer ===== */
rr(50,978,1820,82,16);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.orange;ctx.globalAlpha=.6;ctx.lineWidth=2;ctx.stroke();ctx.globalAlpha=1;
M('SGD:每步抽一筆 i,用 ∇f_{i} 近似全梯度 ∇f — 便宜、可擴展,是深度學習預設優化法的鼻祖(隨機逼近 1951;ML 應用約 1998)',80,1016,27,C.txt,'left',1760,'600');
T('SGD: one random sample per step stands in for the full gradient — cheap and scalable; ancestor of deep learning’s default optimizer (stochastic approximation 1951; ML use ~1998).',80,1046,{size:22,color:C.dim,maxW:1760});
}
function loop(ts){draw(ts);requestAnimationFrame(loop);}
(document.fonts&&document.fonts.ready?document.fonts.ready:Promise.resolve()).then(()=>requestAnimationFrame(loop));
</script>
</body>
</html>