▶ 展示 · 回索引

10-rmsprop-adam.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RMSProp 與 Adam:現代預設優化器|RMSProp and Adam: The Modern Defaults</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<style>
:root{--bg:#e8edf0;--fg:#14232e;--mut:#5a6a75;--teal:#1b6f7f;--card:#ffffff;--line:#c5d2d9}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:"Noto Sans TC","PingFang TC","Microsoft JhengHei","Heiti TC",sans-serif}
.wrap{max-width:1680px;margin:0 auto;padding:14px 16px 24px}
.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;margin:2px 0 10px}
.bar.t{font-size:17px;font-weight:700}
.bar.t small{display:block;font-weight:400;color:var(--mut);font-size:12.5px}
.stage{position:relative;width:100%;aspect-ratio:16/9;background:#edf1f3;border-radius:6px;overflow:hidden;box-shadow:0 2px 14px rgba(0,0,0,.16)}
canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
/* KaTeX 層:固定 1600x900 logical,與 canvas 同一座標系(SOP 18.5 問題A) */
#math-layer{position:absolute;left:0;top:0;width:1600px;height:900px;transform-origin:top left;pointer-events:none;overflow:hidden}
.formula{position:absolute;transform-origin:top left;line-height:1.25}
.controls{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;margin-top:10px;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px 16px}
.controls label{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
.controls output{font-size:14px;min-width:52px;color:var(--teal);font-weight:700}
button{font:inherit;font-size:14px;font-weight:700;border:0;border-radius:8px;padding:9px 18px;cursor:pointer;background:var(--teal);color:#fff}
button.alt{background:#fff;color:var(--fg);border:1.5px solid var(--mut)}
button.on{background:#e39b0b;color:#14232e}
input[type=range]{width:150px;accent-color:var(--teal)}
.hint{margin-top:8px;font-size:13px;color:var(--mut);line-height:1.7}
.hint b{color:var(--fg)}
</style>
</head>
<body>
<div class="wrap">
<div class="bar"><div class="t">RMSProp 與 Adam:現代預設優化器<small>RMSProp and Adam: The Modern Defaults · 16:9 互動式資訊圖表</small></div></div>
<div class="stage" id="stage">
<canvas id="c" width="3200" height="1800"></canvas>
<div id="math-layer"></div>
</div>
<div class="controls" id="controls">
<label>γ(遺忘率) <input type="range" id="sGamma" min="0.8" max="0.99" step="0.01" value="0.9"><output id="sGammav">0.90</output></label>
<label>k(步數) <input type="range" id="sK" min="1" max="30" step="1" value="1"><output id="sKv">1</output></label>
<label>g(梯度) <input type="range" id="sG" min="-5" max="5" step="0.5" value="4"><output id="sGv">4.0</output></label>
</div>
<div class="hint" id="hint"><b>操作:</b>拖動三個滑桿,觀察偏置修正因子如何把早期被系統性低估的 m 拉回真實梯度 g。預設 γ=0.9、k=1 時修正因子恰為 10,m̂ 回到 4。</div>
</div>
<script>
/* ================= 共享繪圖系統(1600x900 logical,backing store 3200x1800) ================= */
const stage=document.getElementById('stage'), cv=document.getElementById('c'), ctx=cv.getContext('2d');
const S=2, W=1600, H=900;
ctx.setTransform(S,0,0,S,0,0);
const C={bg:'#EDF1F3',ink:'#14232E',mut:'#5A6A75',teal:'#1B6F7F',tealL:'#DCEBEF',tealM:'#A9CBD3',
amb:'#E39B0B',ambL:'#FBEBC8',red:'#D64545',redL:'#F6DDD6',grn:'#1E8A49',grnL:'#DDF3E4',
blu:'#2563EB',bluL:'#DBEAFE',org:'#EA580C',orgL:'#FDEBD7',pur:'#7C3AED',purL:'#EDE7FB',
pan:'#FFFFFF',line:'#C5D2D9'};
const FAM='"Noto Sans TC","PingFang TC","Microsoft JhengHei","Heiti TC","Segoe UI",sans-serif';
const F=(s,w)=>`${w||400} ${s}px ${FAM}`;
function T(s,x,y,o){o=o||{};ctx.font=F(o.size||15,o.w||400);ctx.fillStyle=o.c||C.ink;
ctx.textAlign=o.a||'left';ctx.textBaseline=o.b||'alphabetic';ctx.fillText(s,x,y);
return ctx.measureText(s).width;}
/* 換行:先把字面 \n 正規化(SOP 18.5),再做中英混合斷詞 */
function wrap(s,maxW,size,w){s=String(s).replace(/\\n/g,'\n');ctx.font=F(size,w);
const out=[];for(const para of s.split('\n')){
const toks=para.match(/[\x21-\x7E\u00A0-\u024F]+|\s+|[^\s]/gu)||[];let line='';
for(const t of toks){if(/^\s+$/.test(t)&&line==='')continue;
if(ctx.measureText(line+t).width>maxW&&line.trim()!==''){out.push(line.trimEnd());line=/^\s+$/.test(t)?'':t;}
else line+=t;}
if(line.trim()!=='')out.push(line.trimEnd());}
return out;}
function P(s,x,y,maxW,lh,size,w,c,a){const ls=wrap(s,maxW,size,w);
ls.forEach((l,i)=>T(l,x,y+i*lh,{size:size,w:w,c:c,a:a}));return ls.length;}
/* 置中段落專用:cx 必須是容器的真正中心 x(SOP 18.5 問題B/I:勿用 wrap(...,'center') 配左緣 x) */
function wrapCenter(s,cx,y,maxW,lh,size,w,c){const ls=wrap(s,maxW,size,w);
ls.forEach((l,i)=>T(l,cx,y+i*lh,{size:size,w:w,c:c,a:'center'}));return ls.length;}
function rr(x,y,w,h,r){ctx.beginPath();ctx.roundRect(x,y,w,h,r);}
function panel(x,y,w,h,zh,en){rr(x,y,w,h,10);ctx.fillStyle=C.pan;ctx.fill();
ctx.strokeStyle=C.line;ctx.lineWidth=1.5;ctx.stroke();
const tw=T(zh,x+18,y+30,{size:17,w:700});T(en,x+18+tw+10,y+30,{size:11.5,c:C.mut});}
function chip(x,y,w,h,txt,bg,fg,size){rr(x,y,w,h,h/2);ctx.fillStyle=bg;ctx.fill();
T(txt,x+w/2,y+h/2+(size||12)*0.35,{size:size||12,w:700,c:fg||'#fff',a:'center'});}
function arrow(x1,y1,x2,y2,col,lw,hs){hs=hs||8;ctx.strokeStyle=col;ctx.fillStyle=col;
ctx.lineWidth=lw||2.5;ctx.lineCap='round';const a=Math.atan2(y2-y1,x2-x1);
ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2-Math.cos(a)*hs*0.55,y2-Math.sin(a)*hs*0.55);ctx.stroke();
ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-hs*Math.cos(a-0.42),y2-hs*Math.sin(a-0.42));
ctx.lineTo(x2-hs*Math.cos(a+0.42),y2-hs*Math.sin(a+0.42));ctx.closePath();ctx.fill();}
function dot(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 strip(x,y,w,h,zh,en,bg,fg){rr(x,y,w,h,8);ctx.fillStyle=bg||C.tealL;ctx.fill();
wrapCenter(zh,x+w/2,y+26,w-40,24,15,700,fg||C.ink);
wrapCenter(en,x+w/2,y+52,w-40,20,12,400,C.mut);}
/* ================= KaTeX hybrid(SOP 4.3;LaTeX 一律用 String.raw 單反斜線,勿雙重跳脫) ================= */
const mathLayer=document.getElementById('math-layer');
function addFormula(o){let el=document.getElementById('fx-'+o.id);
if(!el){el=document.createElement('div');el.className='formula';el.id='fx-'+o.id;mathLayer.appendChild(el);}
el.style.left=o.x+'px';el.style.top=o.y+'px';el.style.width=(o.w||600)+'px';
el.style.textAlign=o.align||'center';el.style.fontSize=(o.fontSize||28)+'px';
el.style.color=o.color||C.ink;
try{katex.render(o.tex,el,{displayMode:true,throwOnError:false});}catch(e){el.textContent='[公式渲染失敗]';}
return el;}
function syncMathLayer(){const r=stage.getBoundingClientRect();
mathLayer.style.transform=`scale(${r.width/W},${r.height/H})`;}
new ResizeObserver(syncMathLayer).observe(stage);
window.addEventListener('resize',syncMathLayer);
window.addEventListener('load',syncMathLayer);
/* ================= 頁首(每頁改標題) ================= */
function header(zh,en,tag,tagEn){
T(zh,40,60,{size:40,w:900});
T(en,40,90,{size:19,w:500,c:C.teal});
if(tag){const tw=ctx.measureText(tag).width;
rr(1560-tw-56,30,tw+56,52,26);ctx.fillStyle=C.teal;ctx.fill();
T(tag,1560-28,52,{size:15,w:700,c:'#fff',a:'center'});
if(tagEn)T(tagEn,1560-28,70,{size:10,c:'#CFE6EB',a:'center'});}
}
/* ================= 以下為頁面內容 ================= */
const state={gamma:0.9,k:1,g:4};

function drawAdaGrad(){
  panel(40,110,496,400,'AdaGrad','累積平方梯度 · 只增不減');
  T('累積量 G(只增不減)',70,178,{size:13.5,w:700,c:C.red});
  const hs=[26,52,84,118,148,172,192,206];
  for(let i=0;i<8;i++){const x=80+i*52;
    ctx.fillStyle=C.redL;ctx.fillRect(x,352-hs[i],40,hs[i]);
    ctx.strokeStyle=C.red;ctx.lineWidth=1.5;ctx.strokeRect(x,352-hs[i],40,hs[i]);}
  T('時間 →',500,370,{size:12,c:C.mut,a:'right'});
  T('有效步長 η/√G(越走越小)',70,444,{size:13.5,w:700,c:C.red});
  ctx.strokeStyle=C.red;ctx.lineWidth=3;ctx.lineCap='round';ctx.beginPath();
  for(let x=80;x<=470;x+=4){const t=(x-80)/390;const y=452+38*(1-Math.exp(-4*t));
    if(x===80)ctx.moveTo(x,y);else ctx.lineTo(x,y);}
  ctx.stroke();
  arrow(430,488.9,470,489.3,C.red,3,9);
  T('步長死亡',360,480,{size:14,w:700,c:C.red});
}

function drawRMSProp(){
  panel(552,110,496,400,'RMSProp','指數移動平均 EMA');
  T('v:梯度平方的 EMA(可升可降)',582,178,{size:13.5,w:700,c:C.org});
  ctx.strokeStyle=C.line;ctx.lineWidth=1.5;
  ctx.beginPath();ctx.moveTo(580,400);ctx.lineTo(1020,400);ctx.stroke();
  ctx.beginPath();ctx.moveTo(580,190);ctx.lineTo(580,400);ctx.stroke();
  ctx.strokeStyle=C.org;ctx.lineWidth=3.5;ctx.lineCap='round';ctx.beginPath();
  for(let x=590;x<=1010;x+=4){
    const u=(x-590)/420;
    const env=0.55+0.45*Math.sin(u*Math.PI*2.2+0.6);
    const y=295-95*Math.sin(u*Math.PI*4.4)*env;
    if(x===590)ctx.moveTo(x,y);else ctx.lineTo(x,y);}
  ctx.stroke();
  T('γ ≈ 0.9',940,214,{size:14,w:700,c:C.org});
  T('能遺忘舊梯度',900,238,{size:13,c:C.org});
  T('(40)',990,412,{size:12,c:C.mut});
}

function drawAdam(){
  panel(1064,110,496,400,'Adam(預設首選)','動量方向 × 自適應尺度');
  T('m:動量方向',1094,178,{size:13.5,w:700,c:C.blu});
  arrow(1100,262,1292,262,C.blu,10,13);
  T('沿歷史梯度平均方向走',1100,300,{size:12.5,c:C.mut});
  T('v:自適應尺度',1360,178,{size:13.5,w:700,c:C.blu});
  ctx.setLineDash([7,6]);ctx.strokeStyle=C.blu;ctx.lineWidth=2.5;
  ctx.beginPath();ctx.ellipse(1448,262,78,44,0,0,Math.PI*2);ctx.stroke();ctx.setLineDash([]);
  T('逐維縮放步長',1448,332,{size:12.5,c:C.mut,a:'center'});
  T('(42)',1520,344,{size:12,c:C.mut,a:'right'});
  T('(43)',1520,400,{size:12,c:C.mut,a:'right'});
  wrapCenter('動量方向 + 自適應尺度 = Adam',1312,492,440,22,14,700,C.blu);
}

function drawBias(){
  panel(40,530,1520,280,'偏置修正 Bias correction','m、v 從 0 出發,早期系統性偏低');
  const gamma=state.gamma,k=state.k,g=state.g;
  const f=1/(1-Math.pow(gamma,k));
  const m=(1-Math.pow(gamma,k))*g;
  T('(44)',740,602,{size:12,c:C.mut,a:'right'});
  T('(45)',740,670,{size:12,c:C.mut,a:'right'});
  T('(46)',740,731,{size:12,c:C.mut,a:'right'});
  addFormula({id:'biasF',tex:'\\frac{1}{1-'+gamma.toFixed(2)+'^{'+k+'}}='+f.toFixed(2),x:1150,y:580,w:370,fontSize:20});
  addFormula({id:'biasR',tex:'m='+m.toFixed(2)+'\\times'+f.toFixed(2)+'\\Rightarrow\\hat{m}='+(m*f).toFixed(2),x:830,y:762,w:620,fontSize:22});
  P('m、v 從 0 初始化:前幾步的期望值被系統性低估;除以修正因子拉回,即得無偏估計。',70,788,700,22,13.5,400,C.mut);
  /* 右半:互動圖 */
  T('互動圖:k = 1..30 的 m 與修正後',810,566,{size:15,w:700});
  const px0=830,px1=1520,py0=620,py1=748;
  const yOf=v=>py1-(v+5)/10*(py1-py0);
  const xOf=kk=>px0+(kk-1)/29*(px1-px0);
  ctx.strokeStyle=C.line;ctx.lineWidth=1.5;
  ctx.beginPath();ctx.moveTo(px0,yOf(0));ctx.lineTo(px1,yOf(0));ctx.stroke();
  T('k=1',px0,766,{size:11,c:C.mut});
  T('k=30',px1,766,{size:11,c:C.mut,a:'right'});
  ctx.setLineDash([7,6]);ctx.strokeStyle='#8A9AA3';ctx.lineWidth=3;ctx.beginPath();
  for(let kk=1;kk<=30;kk++){const mm=(1-Math.pow(gamma,kk))*g;const x=xOf(kk),y=yOf(mm);
    if(kk===1)ctx.moveTo(x,y);else ctx.lineTo(x,y);}
  ctx.stroke();ctx.setLineDash([]);
  ctx.strokeStyle=C.blu;ctx.lineWidth=3.5;ctx.beginPath();
  ctx.moveTo(px0,yOf(g));ctx.lineTo(px1,yOf(g));ctx.stroke();
  ctx.setLineDash([4,4]);ctx.strokeStyle=C.mut;ctx.lineWidth=1.5;
  ctx.beginPath();ctx.moveTo(xOf(k),py0);ctx.lineTo(xOf(k),py1);ctx.stroke();ctx.setLineDash([]);
  dot(xOf(k),yOf(m),6,'#8A9AA3','#fff',2);
  dot(xOf(k),yOf(g),6,C.blu,'#fff',2);
  T('灰線:未修正',px0+8,yOf((1-gamma)*g)-12,{size:12,c:'#7A8A93'});
  if(Math.abs(g)>0.05)T('藍線:已修正(回到 g 高度)',px0+8,yOf(g)-14,{size:12,w:700,c:C.blu,a:'left'});
  else T('g = 0:兩線重合',px0+8,yOf(0)-14,{size:12,w:700,c:C.blu,a:'left'});
}

function draw(){
  ctx.fillStyle=C.bg;ctx.fillRect(0,0,W,H);
  header('RMSProp 與 Adam:現代預設優化器','RMSProp and Adam: The Modern Defaults','IDL115','Ch.3 §5.2–5.3');
  drawAdaGrad();drawRMSProp();drawAdam();drawBias();
  strip(40,828,1520,56,'早期不做偏置修正,等於故意踩小油門;k 變大後修正因子趨近 1','Without bias correction the first steps barely move; the factor → 1 as k grows');
}

function initFormulas(){
  addFormula({id:'adagrad',tex:String.raw`\sum_{\tau}(\nabla_j f)^2`,x:300,y:384,w:200,fontSize:20});
  addFormula({id:'rmsprop',tex:String.raw`v^{(k+1)}:=\gamma v^{(k)}+(1-\gamma)\|\nabla f_i(\boldsymbol{w}^{(k)})\|_2^2`,x:572,y:424,w:460,fontSize:22});
  addFormula({id:'adam1',tex:String.raw`\boldsymbol{m}^{(k+1)}:=\gamma_1\boldsymbol{m}^{(k)}+(1-\gamma_1)\nabla f_i`,x:1074,y:352,w:460,fontSize:19});
  addFormula({id:'adam2',tex:String.raw`v^{(k+1)}:=\gamma_2 v^{(k)}+(1-\gamma_2)\|\nabla f_i\|_2^2`,x:1074,y:408,w:460,fontSize:19});
  addFormula({id:'f44',tex:String.raw`\widehat{\boldsymbol{m}}^{(k+1)}:=\frac{1}{1-\gamma_1^{k}}\boldsymbol{m}^{(k+1)}`,x:60,y:574,w:660,fontSize:20});
  addFormula({id:'f45',tex:String.raw`\widehat{v}^{(k+1)}:=\frac{1}{1-\gamma_2^{k}}v^{(k+1)}`,x:60,y:642,w:660,fontSize:20});
  addFormula({id:'f46',tex:String.raw`\boldsymbol{w}^{(k+1)}:=\boldsymbol{w}^{(k)}-\frac{\eta^{(k)}}{\sqrt{\varepsilon+\widehat{v}^{(k+1)}}}\widehat{\boldsymbol{m}}^{(k+1)}`,x:60,y:706,w:660,fontSize:18});
}

document.addEventListener('DOMContentLoaded',()=>{
  initFormulas();syncMathLayer();
  const sGamma=document.getElementById('sGamma'),sK=document.getElementById('sK'),sG=document.getElementById('sG');
  function upd(){
    state.gamma=+sGamma.value;state.k=+sK.value;state.g=+sG.value;
    document.getElementById('sGammav').textContent=state.gamma.toFixed(2);
    document.getElementById('sKv').textContent=String(state.k);
    document.getElementById('sGv').textContent=state.g.toFixed(1);
    draw();
  }
  [sGamma,sK,sG].forEach(s=>s.addEventListener('input',upd));
  upd();
});
</script>
</body>
</html>