▶ 展示 · 回索引
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>