▶ 展示 · 回索引

03-evaluation-design.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>03 評估方式 · Evaluation Design</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700;900&display=swap" rel="stylesheet">
<style>
:root{--bg:#dfe6ea}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#10181e}}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg)}
body{display:grid;place-items:center;min-height:100vh}
.stage{position:relative;width:min(100vw,177.7778vh);height:min(56.25vw,100vh);aspect-ratio:16/9}
canvas{display:block;width:100%;height:100%;touch-action:none}
canvas:focus-visible{outline:3px solid #e39b0b;outline-offset:-3px}
</style>
</head>
<body>
<div class="stage" id="stage"><canvas id="canvas" width="1600" height="900" tabindex="0" role="img" aria-label="評估方式互動資訊圖表:交叉驗證與評分指標"></canvas></div>
<script>
(function(){
"use strict";
const W=1600,H=900,cv=document.getElementById('canvas'),g=cv.getContext('2d');
const FONT='"Noto Sans TC","PingFang TC","Microsoft JhengHei",sans-serif';
const P={bg:'#f3f6f8',ink:'#14232e',mut:'#55656f',card:'#ffffff',line:'#cfd8de',
  teal:'#1b6f7f',amber:'#e39b0b',rose:'#c2455a',violet:'#6a4fb3',green:'#2f8f5b',grey:'#6b7c86'};
function rr(x,y,w,h,r,fill,stroke,lw){g.beginPath();g.roundRect(x,y,w,h,r);
  if(fill){g.fillStyle=fill;g.fill();} if(stroke){g.lineWidth=lw||2;g.strokeStyle=stroke;g.stroke();}}
function tx(s,x,y,size,weight,color,align){g.font=weight+' '+size+'px '+FONT;g.fillStyle=color;
  g.textAlign=align||'left';g.textBaseline='alphabetic';g.fillText(s,x,y);}
const OPEN=/^[(「『(]$/, CLOSE=/^[,。、;:)」』!?,.;:)]$/;
function lines(t,maxW,size,weight){
  t=String(t).replace(/\\n/g,"\n");g.font=weight+' '+size+'px '+FONT;
  const out=[];
  for(const para of t.split("\n")){
    const raw=para.match(/[A-Za-z0-9._\-²%\/+×=<>'’&]+|\s+|[^\sA-Za-z0-9]/g)||[];
    const toks=[];let pre='';
    for(const r of raw){
      if(OPEN.test(r)){pre+=r;continue;}
      if(CLOSE.test(r)&&toks.length){toks[toks.length-1]+=r;continue;}
      toks.push(pre+r);pre='';}
    if(pre)toks.push(pre);
    let line='';
    for(const k of toks){
      if(g.measureText(line+k).width>maxW&&line.trim()){out.push(line.trimEnd());line=k.trimStart();}
      else line+=k;}
    out.push(line.trimEnd());}
  return out;}
function wrap(t,x,y,maxW,lh,size,weight,color,align){
  const ls=lines(t,maxW,size,weight);ls.forEach((l,i)=>tx(l,x,y+i*lh,size,weight,color,align||'left'));
  return y+ls.length*lh;}
function wrapCenter(t,cx,y,maxW,lh,size,weight,color){return wrap(t,cx,y,maxW,lh,size,weight,color,'center');}
function arrow(x1,y1,x2,y2,color,lw){const a=Math.atan2(y2-y1,x2-x1),L=16;
  g.strokeStyle=color;g.fillStyle=color;g.lineWidth=lw||5;g.lineCap='round';
  g.beginPath();g.moveTo(x1,y1);g.lineTo(x2-Math.cos(a)*L*.7,y2-Math.sin(a)*L*.7);g.stroke();
  g.beginPath();g.moveTo(x2,y2);g.lineTo(x2-L*Math.cos(a-.45),y2-L*Math.sin(a-.45));
  g.lineTo(x2-L*Math.cos(a+.45),y2-L*Math.sin(a+.45));g.closePath();g.fill();}
function chip(x,y,w,h,s,color,size){rr(x,y,w,h,h/2,color);tx(s,x+w/2,y+h/2+(size||18)*.35,size||18,'700','#fff','center');}
function circle(x,y,r,fill,stroke,lw){g.beginPath();g.arc(x,y,r,0,Math.PI*2);
  if(fill){g.fillStyle=fill;g.fill();} if(stroke){g.lineWidth=lw||2;g.strokeStyle=stroke;g.stroke();}}
// two-line block: zh + en, centred
function block(x,y,w,h,zh,en,color,fill,zs,es){rr(x,y,w,h,14,fill||'#fff',color,3);
  const a=wrapCenter(zh,x+w/2,y+(zs||20)+10,w-16,(zs||20)+4,zs||20,'700',P.ink);
  if(en)wrapCenter(en,x+w/2,a+2,w-16,(es||14)+4,es||14,'400',P.mut);}
let hits=[];
function button(id,x,y,w,h,zh,en,on,color){
  rr(x,y,w,h,12,on?color:'#fff',color,3);
  if(en){tx(zh,x+w/2,y+h/2-1,22,'700',on?'#fff':color,'center');tx(en,x+w/2,y+h/2+22,15,'400',on?'#fff':P.mut,'center');}
  else tx(zh,x+w/2,y+h/2+8,22,'700',on?'#fff':color,'center');
  hits.push({id,x,y,w,h});}
function header(zh,en,n){
  const gr=g.createLinearGradient(0,0,W,0);gr.addColorStop(0,'#17495a');gr.addColorStop(1,'#1b6f7f');
  g.fillStyle=gr;g.fillRect(0,0,W,100);
  tx(zh,50,54,40,'900','#fff');tx(en,50,86,22,'500','rgba(255,255,255,.85)');
  tx('備案研究方向 Fallback plan · '+n+' / 4',W-50,50,22,'700','#fff','right');
  tx('徐小惠 · 2026-10-03 · 教授已核定  Approved by professor',W-50,82,17,'400','rgba(255,255,255,.8)','right');}
function footer(zh,en){tx(zh+'  ·  '+en,W/2,884,16,'400',P.mut,'center');}
function pos(e){const r=cv.getBoundingClientRect();return{x:(e.clientX-r.left)*W/r.width,y:(e.clientY-r.top)*H/r.height};}
function hitAt(p){for(let i=hits.length-1;i>=0;i--){const h=hits[i];if(p.x>=h.x&&p.x<=h.x+h.w&&p.y>=h.y&&p.y<=h.y+h.h)return h;}return null;}
let drag=null;
function render(){hits=[];g.clearRect(0,0,W,H);g.fillStyle=P.bg;g.fillRect(0,0,W,H);draw();}
cv.addEventListener('pointerdown',e=>{const p=pos(e),h=hitAt(p);if(!h)return;
  if(h.slider){drag=h;cv.setPointerCapture(e.pointerId);} onHit(h.id,p,h);render();});
cv.addEventListener('pointermove',e=>{const p=pos(e);
  if(drag){onHit(drag.id,p,drag);render();return;}
  cv.style.cursor=hitAt(p)?'pointer':'default';});
cv.addEventListener('pointerup',()=>{drag=null;});
cv.addEventListener('keydown',e=>{if(typeof step==='function'){
  if(e.key==='ArrowRight'||e.key==='ArrowDown'){step(1);render();e.preventDefault();}
  if(e.key==='ArrowLeft'||e.key==='ArrowUp'){step(-1);render();e.preventDefault();}}});

let testFold=3,inner=0,noise=0.6;
function step(d){testFold=(testFold-1+d+5)%5+1;}
function setState(k,v){if(k==='fold')testFold=v;if(k==='noise')noise=v;if(k==='inner')inner=v;}
function onHit(id,p,h){
  if(id.startsWith('t'))testFold=+id.slice(1);
  if(id==='inner')inner=(inner+1)%4;
  if(id==='noise'){noise=Math.max(0,Math.min(2,(p.x-h.x)/h.w*2));}}
// seeded standard-normal data, fixed so the figure is reproducible
function mk(n){let s=12345;const r=()=>{s=(s*1664525+1013904223)%4294967296;return s/4294967296;};
  const a=[];for(let i=0;i<n;i++){const u=r()||1e-9,v=r();const z1=Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);
    const z2=Math.sqrt(-2*Math.log(u))*Math.sin(2*Math.PI*v);a.push([z1,z2]);}return a;}
const DATA=mk(70);
function stats(s){let sst=0,sse=0,mt=0,mp=0;const n=DATA.length;const T=DATA.map(d=>d[0]),Pd=DATA.map(d=>d[0]+s*d[1]);
  T.forEach(v=>mt+=v);mt/=n;Pd.forEach(v=>mp+=v);mp/=n;let sxy=0,sxx=0,syy=0;
  for(let i=0;i<n;i++){sst+=(T[i]-mt)**2;sse+=(T[i]-Pd[i])**2;sxy+=(T[i]-mt)*(Pd[i]-mp);sxx+=(T[i]-mt)**2;syy+=(Pd[i]-mp)**2;}
  return{r2:1-sse/sst,corr:sxy/Math.sqrt(sxx*syy),rmse:Math.sqrt(sse/n),T,Pd};}
function draw(){
  header('評估方式','How the features are compared fairly',3);
  // left: cross-validation
  rr(50,115,700,740,18,'#fff',P.line,2);
  tx('交叉驗證  Cross-validation',74,155,26,'900',P.violet);
  tx('PMEmo 596 首 · 以「整首歌」為單位分組  Split by whole songs',74,184,17,'500',P.mut);
  tx('選擇測試份  Choose the test fold',74,225,18,'700',P.ink);
  for(let i=1;i<=5;i++){button('t'+i,70+(i-1)*136,240,116,56,'第 '+i+' 份',null,i==testFold,P.violet);}
  for(let i=1;i<=5;i++){const x=70+(i-1)*136,t=i==testFold;rr(x,312,116,86,12,t?P.rose:'#e6ecef');
    tx(t?'測試':'訓練',x+58,350,24,'900',t?'#fff':P.grey,'center');tx(t?'Test':'Train',x+58,374,15,'400',t?'#fff':P.grey,'center');}
  tx('每次 4 份訓練、1 份測試,輪流 5 次  4 train + 1 test, 5 rounds',74,432,18,'500',P.ink);
  arrow(400,444,400,486,P.violet,5);
  rr(70,492,660,238,16,'#f6f2fd',P.violet,3);
  tx('巢狀:挑選設定只在訓練的 4 份內部輪流',90,524,20,'700',P.violet);tx('Nested: tune only inside the 4 training folds',90,547,15,'400',P.mut);
  for(let i=0;i<4;i++){const x=90+i*155,v=i==inner;rr(x,564,140,64,12,v?P.amber:'#fff',v?P.amber:P.line,2);
    tx(v?'內層驗證':'內層訓練',x+70,594,19,'700',v?'#fff':P.grey,'center');tx(v?'Inner val':'Inner train',x+70,614,13,'400',v?'#fff':P.grey,'center');}
  button('inner',90,644,300,64,'換內層驗證份','Next inner validation fold',false,P.violet);
  rr(70,748,660,92,14,'#fbeef1',P.rose,3);
  wrap('測試份在挑設定的過程中完全不碰。 The test fold is never touched while choosing settings.',90,782,620,30,20,'700','#8a2a3c');
  // right: scoring
  rr(780,115,770,740,18,'#fff',P.line,2);
  tx('評分  Scoring',804,155,26,'900',P.teal);
  tx('每首歌一個點:人工標註(橫)vs 預測(縱)· valence / arousal 連續值',804,184,17,'500',P.mut);
  const px=824,py=205,ps=400,S=stats(noise),lim=3.2;
  rr(px,py,ps,ps,10,'#f7fafb',P.line,2);
  const X=v=>px+(Math.max(-lim,Math.min(lim,v))+lim)/(2*lim)*ps,Y=v=>py+ps-(Math.max(-lim,Math.min(lim,v))+lim)/(2*lim)*ps;
  g.strokeStyle='#9fb0b9';g.lineWidth=2;g.setLineDash([8,6]);g.beginPath();g.moveTo(X(-lim),Y(-lim));g.lineTo(X(lim),Y(lim));g.stroke();g.setLineDash([]);
  g.strokeStyle=P.rose;g.lineWidth=3;g.setLineDash([3,6]);g.beginPath();g.moveTo(px,Y(0));g.lineTo(px+ps,Y(0));g.stroke();g.setLineDash([]);
  tx('基準 baseline',px+8,Y(0)-8,15,'700',P.rose);
  S.T.forEach((t,i)=>circle(X(t),Y(S.Pd[i]),5,'rgba(27,111,127,.75)'));
  tx('人工標註 Human label →',px+ps/2,py+ps+23,16,'500',P.mut,'center');
  g.save();g.translate(px-14,py+ps/2);g.rotate(-Math.PI/2);tx('預測 Prediction →',0,0,16,'500',P.mut,'center');g.restore();
  const mx=1270,ms=[['R²','解釋多少變化',S.r2.toFixed(2),S.r2<0?P.rose:P.teal],['相關係數 r','方向是否一致',S.corr.toFixed(2),P.violet],['RMSE','平均誤差',S.rmse.toFixed(2),P.amber]];
  ms.forEach((m,i)=>{const y=205+i*136;rr(mx,y,250,122,14,'#fff',m[3],3);
    tx(m[0],mx+16,y+34,20,'700',m[3]);tx(m[1],mx+16,y+56,15,'400',P.mut);tx(m[2],mx+16,y+104,44,'900',P.ink);});
  tx('預測雜訊  Prediction noise',824,660,20,'700',P.ink);
  rr(824,676,700,14,7,'#dfe6ea');rr(824,676,700*noise/2,14,7,P.teal);circle(824+700*noise/2,683,16,'#fff',P.teal,4);
  hits.push({id:'noise',x:804,y:656,w:740,h:46,slider:true});
  tx('小 low',824,714,16,'400',P.mut);tx('大 high',1524,714,16,'400',P.mut,'right');
  const worse=S.r2<0;
  rr(804,726,730,120,14,worse?'#fbeef1':'#e9f6ee',worse?P.rose:P.green,3);
  wrap(worse?'R² 低於 0:比「一律猜平均」還差,這個特徵沒有價值。 R² < 0 means worse than always guessing the mean.'
    :'R² = 1 完美、0 等於只猜平均值;任何特徵都必須先勝過基準。 R² = 1 is perfect, 0 equals the mean-guess baseline.',824,756,690,28,19,'700',worse?'#8a2a3c':'#1f6b44');
  wrap('穩定性:不同隨機種子重複多次,並用同一首歌配對比較。 Several seeds; paired by song.',824,818,690,21,15,'400',P.mut);
  footer('可選:再用另一份人工標註公開資料集(例如 DEAM)檢查結論能否推廣','Optional: test on another labeled dataset such as DEAM to check generalization');}

render();
if(document.fonts&&document.fonts.ready)document.fonts.ready.then(render);
window.__render=render;window.__set=function(k,v){try{setState(k,v);}catch(_){} render();};
})();
</script>
</body>
</html>