▶ 展示 · 回索引

backprop-gradient.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<title>反向傳播 Backpropagation — 多層網路怎麼算梯度?</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={bg:'#0b1220',panel:'#16213a',panel2:'#0f1a30',line:'#2c3b5c',txt:'#e8eefc',dim:'#9fb0d0',
  cyan:'#38bdf8',green:'#4ade80',orange:'#fb923c',purple:'#a78bfa',pink:'#f472b6',gray:'#94a3b8'};

/* ---------- 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;
}
/* M: math / mixed text. items: string or array of string | {f:[num,den]} */
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 arrow(x1,y1,x2,y2,col,lw,head){
  head=head||12;const a=Math.atan2(y2-y1,x2-x1);
  ctx.strokeStyle=col;ctx.fillStyle=col;ctx.lineWidth=lw||2.5;
  ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2-Math.cos(a)*head*0.7,y2-Math.sin(a)*head*0.7);ctx.stroke();
  ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-head*Math.cos(a-0.4),y2-head*Math.sin(a-0.4));ctx.lineTo(x2-head*Math.cos(a+0.4),y2-head*Math.sin(a+0.4));ctx.closePath();ctx.fill();
}

/* ---------- network geometry (panel 3) ---------- */
const L1=[[1290,255],[1290,305],[1290,355]],L2=[[1470,255],[1470,305],[1470,355]],L3=[[1650,280],[1650,330]];
function edges(A,B,col,alpha,lw){
  ctx.strokeStyle=col;ctx.globalAlpha=alpha;ctx.lineWidth=lw;
  for(const a of A)for(const b of B){ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();}
  ctx.globalAlpha=1;
}
function dots(A,B,t,col){
  ctx.fillStyle=col;
  for(const a of A)for(const b of B){
    const x=a[0]+(b[0]-a[0])*t,y=a[1]+(b[1]-a[1])*t;
    ctx.globalAlpha=.25;ctx.beginPath();ctx.arc(x,y,10,0,7);ctx.fill();
    ctx.globalAlpha=1;ctx.beginPath();ctx.arc(x,y,5,0,7);ctx.fill();
  }
}
function label(s,x,y,size,col){
  const w=runsW(s,size,'400');
  rr(x-w/2-6,y-size*0.85,w+12,size*1.25,6);ctx.fillStyle=C.panel;ctx.fill();
  M(s,x,y,size,col,'center');
}

/* ---------- 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('多層網路怎麼算梯度?反向傳播',50,80,{size:54,weight:'800',color:'#ffffff'});
  T('How Do Multilayer Networks Compute Gradients? — Backpropagation (Rumelhart et al.)',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('梯度下降 + 遞迴求導',1600,72,{size:30,weight:'700',color:C.orange,align:'center',maxW:500});
  T('Gradient descent + recursive differentiation',1600,104,{size:22,color:C.dim,align:'center',maxW:500});

  /* ===== Panel 1: forward ===== */
  let px=50,py=150,pw=560,ph=440;
  panel(px,py,pw,ph,1,'單一神經元:前向計算','Single Neuron: Forward Pass',C.cyan);
  const zs=[[110,270,'z_{1}'],[110,315,'z_{ℓ}'],[110,360,'z_{m}']],ai=[300,315],sg=[380,315],zi=[520,315];
  for(const z of zs){
    const hi=z[2]==='z_{ℓ}';
    arrow(z[0]+24,z[1],ai[0]-38,ai[1]+(z[1]-315)*0.25,hi?C.cyan:'#3b6a86',hi?3:2,10);
    circle(z[0],z[1],22,C.panel2,hi?C.cyan:C.line,2);
    M(z[2],z[0],z[1]+8,22,C.txt,'center');
  }
  label('w_{iℓ}',203,297,24,C.cyan);
  circle(ai[0],ai[1],36,C.panel2,C.cyan,3);M('Σ',ai[0],ai[1]+11,32,C.cyan,'center');
  M('a_{i}',ai[0],ai[1]+64,26,C.txt,'center');
  arrow(ai[0]+38,ai[1],sg[0]-32,ai[1],C.cyan,3,10);
  rr(sg[0]-32,ai[1]-28,64,56,10);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.cyan;ctx.lineWidth=2;ctx.stroke();
  M('σ_{i}',sg[0],ai[1]+9,26,C.cyan,'center');
  arrow(sg[0]+34,ai[1],zi[0]-32,ai[1],C.cyan,3,10);
  circle(zi[0],zi[1],30,C.panel2,C.cyan,3);M('z_{i}',zi[0],zi[1]+9,26,C.txt,'center');
  T('前一層 previous layer',70,410,{size:22,color:C.dim});
  T('神經元 i  Neuron i',430,410,{size:22,color:C.dim,align:'center'});
  M('(9)',70,462,26,C.cyan);M('a_{i} = Σ_{ℓ=1}^{m} w_{iℓ} z_{ℓ}',135,462,30,C.txt);
  T('激活前加權和|weighted input sum',135,506,{size:22,color:C.dim,maxW:460});
  M('(10)',70,543,26,C.cyan);M('z_{i} := σ_{i}(a_{i})',135,543,30,C.txt);
  T('激活後輸出|σ_i is the activation function',135,577,{size:22,color:C.dim,maxW:460});

  /* ===== Panel 2: chain rule ===== */
  px=630;pw=560;
  panel(px,py,pw,ph,2,'鏈式法則與 δ','Chain Rule and δ',C.green);
  M('(11)',px+20,300,26,C.green);
  M([{f:['∂e','∂w_{iℓ}']},' = ',{f:['∂e','∂a_{i}']},' × ',{f:['∂a_{i}','∂w_{iℓ}']}],px+75,300,28,C.txt,'left',px+pw-20-(px+75));
  M('= δ_{i} × z_{ℓ}',px+75,388,38,C.green,'left',400,'700');
  M('(12)',px+20,462,26,C.green);
  M(['δ_{i} := ',{f:['∂e','∂a_{i}']}],px+75,462,30,C.txt);
  T('誤差對激活前輸入的敏感度',px+245,458,{size:22,color:C.dim,maxW:300});
  T('sensitivity of error to a_i',px+245,484,{size:22,color:C.dim,maxW:300});
  rr(px+20,498,pw-40,80,12);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.green;ctx.lineWidth=1.5;ctx.stroke();
  M('因 ∂a_{i}/∂w_{iℓ} = z_{ℓ},故梯度 = δ_{i} × z_{ℓ}',px+pw/2,530,24,C.txt,'center',pw-60);
  T('gradient = local δ × incoming activation',px+pw/2,565,{size:22,color:C.green,align:'center',maxW:pw-60});

  /* ===== Panel 3: hidden delta ===== */
  px=1210;pw=660;
  panel(px,py,pw,ph,3,'隱藏層的 δ:往後傳','Hidden-Layer δ: Propagate Backward',C.orange);
  const cyc=(ts||0)%7000/7000,fwd=cyc<0.5,q=(fwd?cyc:cyc-0.5)*2;
  edges(L1,L2,C.cyan,.28,1.5);edges(L2,L3,C.cyan,.28,1.5);
  ctx.strokeStyle=C.cyan;ctx.lineWidth=3.5;ctx.beginPath();ctx.moveTo(L1[1][0],L1[1][1]);ctx.lineTo(L2[1][0],L2[1][1]);ctx.stroke();
  ctx.strokeStyle=C.orange;ctx.beginPath();ctx.moveTo(L2[1][0],L2[1][1]);ctx.lineTo(L3[0][0],L3[0][1]);ctx.stroke();
  if(fwd){const t1=Math.min(1,q*2),t2=Math.max(0,q*2-1);
    if(q<0.5)dots(L1,L2,t1,C.cyan);else dots(L2,L3,t2,C.cyan);}
  else{const t1=Math.min(1,q*2),t2=Math.max(0,q*2-1);
    if(q<0.5)dots(L3,L2,t1,C.orange);else dots(L2,L1,t2,C.orange);}
  for(const n of L1)circle(n[0],n[1],19,C.panel2,C.cyan,2);
  for(const n of L2)circle(n[0],n[1],19,C.panel2,C.line,2);
  circle(L2[1][0],L2[1][1],19,C.panel2,C.orange,3.5);
  for(const n of L3)circle(n[0],n[1],19,C.panel2,C.orange,2);
  M('ℓ',L1[1][0],L1[1][1]+8,22,C.txt,'center');
  M('i',L2[1][0],L2[1][1]+8,22,C.txt,'center');
  M('j',L3[0][0],L3[0][1]+8,22,C.txt,'center');
  label('w_{iℓ}',1380,283,22,C.cyan);
  label('w_{ji}',1560,287,22,C.orange);
  T('輸入層 Input',1290,398,{size:22,color:C.dim,align:'center'});
  T('隱藏層 Hidden',1470,398,{size:22,color:C.dim,align:'center'});
  T('輸出層 Output',1650,398,{size:22,color:C.dim,align:'center'});
  arrow(1240,432,1290,432,C.cyan,3,10);T('前向 forward (a, z)',1300,439,{size:22,color:C.cyan});
  arrow(1600,432,1550,432,C.orange,3,10);T('反向 backward (δ)',1610,439,{size:22,color:C.orange,maxW:250});
  M('(14)',px+20,482,24,C.orange);M('∂a_{j}/∂a_{i} = w_{ji} σ′(a_{i})',px+80,482,26,C.txt);
  M('(15)',px+20,527,24,C.orange);M('δ_{i} = Σ_{j} δ_{j} · w_{ji} σ′(a_{i})',px+80,527,28,C.txt,'left',pw-100);
  M('(16)',px+20,573,24,C.orange);M('∂e/∂w_{iℓ} = z_{ℓ} σ′(a_{i}) Σ_{j} δ_{j} w_{ji}',px+80,573,26,C.orange,'left',pw-100);

  /* ===== Panel 4: update table ===== */
  px=50;py=610;pw=950;ph=350;
  panel(px,py,pw,ph,4,'權重更新:輸出層 vs 隱藏層','Weight Update: Output vs Hidden Layer',C.purple);
  const rows=[
    {zh:'通用',en:'General (17)',col:C.gray,
     f:'w_{iℓ}^{(k+1)} := w_{iℓ}^{(k)} − η^{(k)} ∂e/∂w_{iℓ},  ∀i, ℓ',
     n:'通用更新規則|gradient-descent step for every weight'},
    {zh:'輸出層',en:'Output (18)',col:C.cyan,
     f:'w_{iℓ}^{(k+1)} := w_{iℓ}^{(k)} − η^{(k)} z_{ℓ} ∂e/∂a_{i}',
     n:'δ_i 可直接算|δ_i is computed directly'},
    {zh:'隱藏層',en:'Hidden (19)',col:C.orange,
     f:'w_{iℓ}^{(k+1)} := w_{iℓ}^{(k)} − η^{(k)} z_{ℓ} σ′(a_{i}) Σ_{j} δ_{j} w_{ji}',
     n:'多乘 σ′(a_i),下游 δ_j 加權傳回|extra σ′ + weighted δ_j'}];
  rows.forEach((r,i)=>{
    const ry=686+i*86;
    rr(px+16,ry,pw-32,78,12);ctx.fillStyle=C.panel2;ctx.fill();
    rr(px+26,ry+9,170,60,30);ctx.fillStyle=r.col;ctx.globalAlpha=.22;ctx.fill();ctx.globalAlpha=1;ctx.strokeStyle=r.col;ctx.lineWidth=2;ctx.stroke();
    T(r.zh,px+111,ry+37,{size:26,weight:'700',color:r.col,align:'center'});
    T(r.en,px+111,ry+61,{size:22,color:C.txt,align:'center'});
    M(r.f,px+216,ry+36,26,C.txt,'left',pw-250);
    T(r.n,px+216,ry+66,{size:22,color:C.dim,maxW:pw-250});
  });

  /* ===== Panel 5: algorithm ===== */
  px=1020;pw=850;
  panel(px,py,pw,ph,5,'Algorithm 2 流程','Algorithm 2: Training Workflow',C.pink);
  function step(n,x,y,w,zh,en,col){
    rr(x,y,w,72,12);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=col;ctx.lineWidth=2;ctx.stroke();
    circle(x+30,y+36,17,col);T(String(n),x+30,y+45,{size:22,weight:'700',color:'#0b1220',align:'center'});
    M(zh,x+58,y+31,25,C.txt,'left',w-72);
    M(en,x+58,y+59,22,C.dim,'left',w-72);
  }
  step(1,px+20,690,400,'初始化權重 w^{(0)}','Initialize weights w^{(0)}',C.pink);
  step(2,px+440,690,390,'每輪 k:設定學習率 η^{(k)}','Each round k: set learning rate η^{(k)}',C.pink);
  ctx.fillStyle=C.pink;ctx.beginPath();ctx.moveTo(px+421,716);ctx.lineTo(px+439,726);ctx.lineTo(px+421,736);ctx.fill();
  step(3,px+20,776,810,'由最後一層 r 往回,對每個 i、ℓ:末層用 (18),其餘用 (19)','From layer r back to 1: eq. (18) at the last layer, otherwise eq. (19)',C.orange);
  step(4,px+20,862,810,'檢查收斂;未收斂則調整 η^{(k)} 後重複 ↺','Check convergence; if not converged, adjust η^{(k)} and repeat',C.green);
  for(const yy of [764,850]){ctx.fillStyle=C.pink;ctx.beginPath();ctx.moveTo(px+415,yy);ctx.lineTo(px+435,yy);ctx.lineTo(px+425,yy+11);ctx.fill();}

  /* ===== 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('口語版:先 forward 算 a, z 與 loss → 輸出層算 δ → 一層層往回傳 δ → 用 δ_{i} z_{ℓ}(隱藏層加 σ′ 修正)更新每個 w_{iℓ}',80,1016,27,C.txt,'left',1760,'600');
  M('In short: forward pass (a, z, loss) → output-layer δ → propagate δ backward → update every w_{iℓ} with δ_{i} z_{ℓ} (with σ′ correction in hidden layers)',80,1046,22,C.dim,'left',1760);
}
function loop(ts){draw(ts);requestAnimationFrame(loop);}
(document.fonts&&document.fonts.ready?document.fonts.ready:Promise.resolve()).then(()=>requestAnimationFrame(loop));
</script>
</body>
</html>