▶ 展示 · 回索引

sam-motivation.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<title>SAM — 為什麼需要 Sharpness-Aware Minimization</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',amber:'#fbbf24'};

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 mb(s,i){let d=0;for(let j=i;j<s.length;j++){if(s[j]==='{')d++;else if(s[j]==='}'){d--;if(d===0)return j;}}return s.length-1;}
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=mb(s,i+1);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{const fs=size*0.66;w+=Math.max(runsW(r.sub,fs,wt),runsW(r.sup,fs,wt))+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.fillStyle=color;ctx.fillText(r.t,x,y);x+=ctx.measureText(r.t).width;}
    else{const fs=size*0.66;
      const ws=runsW(r.sub,fs,wt),wp=runsW(r.sup,fs,wt);
      if(r.sub)drawRuns(r.sub,x,y+size*0.26,fs,color,wt);
      if(r.sup)drawRuns(r.sup,x,y-size*0.4,fs,color,wt);
      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+10+fs*0.9,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;
}
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||11;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();
}
function icon(x,y,w,h,k,col){
  ctx.save();rr(x,y,w,h,8);ctx.fillStyle=C.panel2;ctx.fill();ctx.clip();
  ctx.strokeStyle=col;ctx.lineWidth=3.5;ctx.beginPath();
  for(let dx=-w/2;dx<=w/2;dx+=2){const yy=y+h-12-Math.min(h,Math.pow(dx/k,2)*h*0.9);dx===-w/2?ctx.moveTo(x+w/2+dx,yy):ctx.lineTo(x+w/2+dx,yy);}
  ctx.stroke();circle(x+w/2,y+h-12,5,'#fff');ctx.restore();
}
const cl=(v,a,b)=>Math.max(0,Math.min(1,(v-a)/(b-a)));

function draw(ts){
  ts=ts||0;
  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);

  T('為什麼需要 SAM:平坦極小值可能泛化更好',50,80,{size:54,weight:'800',color:'#ffffff',maxW:1250});
  T('Why SAM? Flat Minima May Generalize Better — Sharpness-Aware Minimization',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:505});
  T('Minimize the worst-case loss in a ρ-ball',1600,104,{size:22,color:C.dim,align:'center',maxW:510});

  const py=150,ph=440;
  /* ===== Panel 1: motivation ===== */
  let px=50,pw=480;
  panel(px,py,pw,ph,1,'動機:平坦解可能泛化更好','Motivation: Flat Minima May Generalize Better',C.cyan);
  icon(px+30,232,200,100,140,C.green);icon(px+250,232,200,100,34,C.red);
  T('Flat 平坦',px+130,358,{size:23,weight:'700',color:C.green,align:'center'});
  T('Sharp 尖銳',px+350,358,{size:23,weight:'700',color:C.red,align:'center'});
  const mrows=[
    [C.green,'平坦:解的鄰域內損失幾乎不變','flat: loss barely changes nearby'],
    [C.red,'尖銳:單點附近損失急劇上升(kp12)','sharp: loss shoots up near one point'],
    [C.amber,'仍有爭議,但催生了 SAM','still debated, yet it inspired SAM']];
  mrows.forEach((r,i)=>{
    const y=374+i*70;
    rr(px+16,y,pw-32,62,10);ctx.fillStyle=C.panel2;ctx.fill();ctx.fillStyle=r[0];ctx.fillRect(px+16,y+8,5,46);
    T(r[1],px+34,y+26,{size:22,weight:'700',color:C.txt,maxW:pw-58});
    T(r[2],px+34,y+52,{size:22,color:C.dim,maxW:pw-58});
  });

  /* ===== Panel 2: min-max ===== */
  px=550;pw=760;
  panel(px,py,pw,ph,2,'min-max 目標:式 (47)–(49)','The Min-Max Objective: Eqs. (47)–(49)',C.orange);
  const fx=px+85,fw=pw-110;
  M('(47)',px+20,282,26,C.orange);
  M('minimize_{w}  L_{SAM}(w) + λ∥w∥_{2}^{2}',fx,282,32,C.txt,'left',fw);
  T('帶正則化形式|regularized form',fx,312,{size:22,color:C.dim});
  M('(48)',px+20,364,26,C.orange);
  M('L_{SAM}(w) := maximize_{ϵ:∥ϵ∥_{p}≤ρ}  L(w+ϵ)',fx,364,32,C.txt,'left',fw);
  T('內層:ρ-球內的最大損失|inner max over the ρ-ball',fx,394,{size:22,color:C.dim,maxW:fw});
  rr(px+16,408,pw-32,84,12);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=C.orange;ctx.lineWidth=1.8;ctx.stroke();
  M('(49)',px+30,436,26,C.orange);
  T('去掉 λ∥w∥² 的純 min-max|pure min-max',px+100,435,{size:22,weight:'700',color:C.orange,maxW:pw-130});
  M('minimize_{w}  maximize_{ϵ:∥ϵ∥_{p}≤ρ}  L(w+ϵ)',px+pw/2,477,34,C.txt,'center',pw-60);
  M('w 權重、L 經驗損失、ρ ≥ 0 鄰域半徑、p ∈ [1, ∞](建議 p = 2)',px+24,522,22,C.txt,'left',pw-48);
  M('ρ ≥ 0: neighborhood radius; p ∈ [1, ∞], p = 2 suggested',px+24,548,22,C.dim,'left',pw-48);
  T('先找最壞擾動 ϵ,再最小化它 → 整個鄰域都平坦|forces a flat neighborhood',px+24,578,{size:22,weight:'700',color:C.green,maxW:pw-48});

  /* ===== Panel 3: decomposition ===== */
  px=1330;pw=540;
  panel(px,py,pw,ph,3,'分解:經驗損失 + 銳度(式 50)','Decomposition: Empirical Loss + Sharpness (Eq. 50)',C.green);
  M('(50)',px+20,282,26,C.green);
  M('L_{SAM}(w) =',px+85,282,32,C.txt);
  function term(y,h,col,f,sz,zh,en){
    rr(px+16,y,pw-32,h,12);ctx.fillStyle=C.panel2;ctx.fill();ctx.strokeStyle=col;ctx.lineWidth=1.8;ctx.stroke();
    M(f,px+pw/2,y+34,sz,C.txt,'center',pw-60);
    T(zh,px+32,y+62,{size:22,weight:'700',color:col,maxW:pw-64});
    T(en,px+32,y+88,{size:22,color:C.dim,maxW:pw-64});
  }
  term(298,98,C.cyan,'L(w)',32,'經驗損失:平常的訓練損失','empirical loss: the usual training loss');
  T('+',px+pw/2,420,{size:32,weight:'700',color:C.green,align:'center'});
  term(430,98,C.orange,'maximize_{ϵ:∥ϵ∥_{p}≤ρ}(L(w+ϵ) − L(w))',28,'銳度:鄰域內損失最多能漲多少','sharpness: how much the loss can rise nearby');
  T('銳度越大,解越尖|greater sharpness = sharper minimum',px+pw/2,562,{size:22,weight:'700',color:C.txt,align:'center',maxW:pw-40});
  T('',0,0);

  /* ===== Panel 4: inner max ===== */
  const ry0=610,ph2=350;
  px=50;pw=960;
  panel(px,ry0,pw,ph2,4,'內層最大化 ϵ*(w):式 (51)–(52)','Inner Maximization ϵ*(w): Eqs. (51)–(52)',C.purple);
  T('一階 Taylor 近似|first-order Taylor:',px+24,722,{size:22,weight:'700',color:C.purple});
  M('L(w+ϵ) ≈ L(w) + ϵ^{⊤}∇_{w}L(w)',px+24,762,28,C.txt,'left',560);
  T('→ 在 ∥ϵ∥_p ≤ ρ 下最大化 ϵ⊤∇L:對偶範數問題|dual-norm problem',px+24,792,{size:22,color:C.dim,maxW:560});
  M('(51)',px+24,868,26,C.purple);
  M(['ϵ^{*}(w) = ',{f:['ρ sign(∇_{w}L(w)) |∇_{w}L(w)|^{q−1}','(∥∇_{w}L(w)∥_{q}^{q})^{1/p}']}],px+84,868,28,C.txt,'left',500);
  M('其中 1/p + 1/q = 1|where 1/p + 1/q = 1',px+84,934,22,C.dim,'left',500);
  ctx.strokeStyle=C.line;ctx.lineWidth=2;ctx.setLineDash([6,6]);ctx.beginPath();ctx.moveTo(px+610,700);ctx.lineTo(px+610,940);ctx.stroke();ctx.setLineDash([]);
  M('(52)',px+630,732,26,C.purple);
  T('p = 2 時大幅簡化|p = 2',px+690,731,{size:22,weight:'700',color:C.purple,maxW:250});
  M(['ϵ^{*}(w) = ρ ',{f:['∇_{w}L(w)','∥∇_{w}L(w)∥_{2}']}],px+640,806,28,C.txt,'left',300);
  T('沿梯度方向、長度為 ρ 的擾動',px+630,866,{size:22,color:C.txt,maxW:310});
  T('往損失上升最快的方向推 ρ 步',px+630,892,{size:22,color:C.txt,maxW:310});
  T('(52) is an upper bound, not exact [45]',px+630,932,{size:22,color:C.dim,maxW:310});

  /* ===== Panel 5: SAM gradient ===== */
  px=1030;pw=840;
  panel(px,ry0,pw,ph2,5,'SAM 梯度:式 (53) 與實作','The SAM Gradient: Eq. (53) and Implementation',C.pink);
  M('(53)',px+24,724,26,C.pink);
  M('∇_{w}L(w+ϵ^{*}(w)) ≈ ∇_{w}L(w)|_{w+ϵ^{*}(w)}',px+84,724,30,C.txt,'left',pw-110);
  T('忽略含 dϵ*/dw 的次要項|drops the minor dϵ*/dw term',px+84,752,{size:22,color:C.dim,maxW:pw-110});
  /* geometry */
  const gx=px+16,gy=770,gw=400,gh=178;
  ctx.save();rr(gx,gy,gw,gh,10);ctx.fillStyle=C.panel2;ctx.fill();ctx.clip();
  const Cc=[gx+225,gy+96];
  for(let k=1;k<=4;k++){ctx.beginPath();ctx.ellipse(Cc[0],Cc[1],40*k*.95,22*k*.95,0,0,7);ctx.strokeStyle=C.line;ctx.lineWidth=1.5;ctx.stroke();}
  const w0=[gx+150,gy+72],rho=34,gdir=[-0.58,-0.81],eps=[gdir[0]*rho,gdir[1]*rho];
  const w1=[w0[0]+eps[0],w0[1]+eps[1]],g1=[-0.43,-0.90];
  const t=(ts%9000)/9000;
  const a1=cl(t,0.05,0.25),a2=cl(t,0.25,0.45),a3=cl(t,0.5,0.7),a4=cl(t,0.72,0.9);
  ctx.globalAlpha=a1;ctx.setLineDash([5,5]);ctx.strokeStyle=C.pink;ctx.lineWidth=2;ctx.beginPath();ctx.arc(w0[0],w0[1],rho,0,7);ctx.stroke();ctx.setLineDash([]);ctx.globalAlpha=1;
  if(a2>0)arrow(w0[0],w0[1],w0[0]+eps[0]*a2,w0[1]+eps[1]*a2,C.orange,3.5,11);
  circle(w0[0],w0[1],7,'#fff');
  if(a2>=1)circle(w1[0],w1[1],7,C.amber,'#fff',2);
  if(a3>0){arrow(w1[0],w1[1],w1[0]+g1[0]*46*a3,w1[1]+g1[1]*46*a3,C.cyan,3,10);
    ctx.globalAlpha=.7;ctx.setLineDash([4,4]);arrow(w0[0],w0[1],w0[0]+g1[0]*46*a3,w0[1]+g1[1]*46*a3,C.cyan,2,8);ctx.setLineDash([]);ctx.globalAlpha=1;}
  if(a4>0)arrow(w0[0],w0[1],w0[0]-g1[0]*58*a4,w0[1]-g1[1]*58*a4,C.green,4,12);
  ctx.restore();
  T('w',w0[0]+12,w0[1]+22,{size:22,weight:'700',color:C.txt});
  T('w + ϵ*',w1[0]-12,w1[1]-10,{size:22,weight:'700',color:C.amber,align:'right'});
  T('更新 update',gx+gw-12,gy+gh-12,{size:22,weight:'700',color:C.green,align:'right'});
  T('示意 illustrative',gx+12,gy+gh-12,{size:22,color:C.dim});
  /* steps */
  const sx=px+436,sw=pw-452;
  const steps=[
    [C.cyan,'① 算梯度 ∇_w L(w)','compute the gradient at w'],
    [C.orange,'② 用式 (52) 得 ϵ*,上升到 w + ϵ*','ascent to w + ϵ* via Eq. (52)'],
    [C.green,'③ 在擾動點再算梯度,用式 (53) 下降更新 w','gradient at w + ϵ*, then descend (53)']];
  steps.forEach((s,i)=>{
    const y=770+i*62;
    rr(sx,y,sw,56,10);ctx.fillStyle=C.panel2;ctx.fill();ctx.fillStyle=s[0];ctx.fillRect(sx,y+8,5,40);
    T(s[1],sx+18,y+24,{size:22,weight:'700',color:C.txt,maxW:sw-30});
    T(s[2],sx+18,y+49,{size:22,color:C.dim,maxW:sw-30});
  });
  T('PyTorch 等框架可數值近似此梯度|frameworks such as PyTorch approximate it numerically',sx,958,{size:22,color:C.dim,maxW:sw});

  /* 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;
  T('SAM:在 ρ-球內找最壞擾動 ϵ*(沿梯度方向推 ρ 步),再用擾動點的梯度更新 w——迫使整個鄰域都平坦',80,1016,{size:27,weight:'600',color:C.txt,maxW:1760});
  T('SAM: find the worst-case perturbation ϵ* in a ρ-ball (ρ steps along the gradient), then update w with the gradient at w + ϵ* — forcing a flat neighborhood.',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>