▶ 展示 · 回索引
04-backward-message-passing.html
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>KP19 04 Backward Message Passing</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<style>
:root{--bg:#071521;--panel:#0f2637;--ink:#f7fbff;--muted:#a9bfd0}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;background:#02080d;color:var(--ink);font-family:Inter,"Noto Sans TC","Microsoft JhengHei",sans-serif}
.wrap{width:100vw;height:100vh;display:grid;place-items:center;background:radial-gradient(circle at 18% 0%,#10344d 0,#02080d 58%,#000 100%)}
.stage{position:relative;width:min(100vw,177.7778vh);height:min(56.25vw,100vh);aspect-ratio:16/9;background:var(--bg);overflow:hidden}
canvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none}
#math-layer{position:absolute;left:0;top:0;width:1600px;height:900px;transform-origin:0 0;pointer-events:none}
.formula{position:absolute;transform:translateY(-50%);line-height:1.2}
.formula .katex-display{margin:0;text-align:inherit}
.formula .katex-display>.katex{display:inline-block;text-align:inherit;white-space:nowrap}
.hint{position:absolute;right:2.1%;top:1.9%;font-size:.8vw;color:#9ab4c7;background:#071521cc;border:1px solid #ffffff18;border-radius:999px;padding:.5em .75em;z-index:5}
@media(max-width:900px){.hint{font-size:1.4vw}}
</style></head><body><div class="wrap"><div class="stage" id="stage"><canvas id="c" width="1600" height="900"></canvas><div id="math-layer"></div><div class="hint">互動:點擊 / 拖曳 • Click / Drag</div></div></div>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script>
/* ===== Shared helpers (Canvas 1600x900 logical space + KaTeX math layer) ===== */
const CW=1600,CH=900;
const c=document.getElementById('c'),ctx=c.getContext('2d');
const stage=document.getElementById('stage'),mathLayer=document.getElementById('math-layer');
const P={bg:'#071521',panel:'#0f2637',panel2:'#102c40',ink:'#f7fbff',muted:'#a9bfd0',cyan:'#4fd1c5',amber:'#f6c85f',coral:'#ff7b72',lime:'#8fd694',violet:'#b794f4',blue:'#63b3ed',grid:'#17384d'};
const FONT='Inter,"Noto Sans TC","Microsoft JhengHei","PingFang TC","Noto Sans CJK TC","WenQuanYi Zen Hei",sans-serif';
let zones=[],drag=null;
window.__boxes=[]; // text boxes recorded for the layout audit
const REC=()=>window.__audit===true;
/* ---------- basic drawing ---------- */
function reset(){zones=[];window.__boxes=[];ctx.clearRect(0,0,CW,CH);ctx.fillStyle=P.bg;ctx.fillRect(0,0,CW,CH);
for(let x=0;x<CW;x+=80)line(x,0,x,CH,'#0b2434',1);for(let y=0;y<CH;y+=80)line(0,y,CW,y,'#0b2434',1)}
function line(x1,y1,x2,y2,color='#fff',lw=3,dash=[]){ctx.save();ctx.strokeStyle=color;ctx.lineWidth=lw;ctx.setLineDash(dash);ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke();ctx.restore()}
function rr(x,y,w,h,r=20,fill=P.panel,stroke=null,lw=2){ctx.beginPath();ctx.roundRect(x,y,w,h,r);if(fill){ctx.fillStyle=fill;ctx.fill()}if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=lw;ctx.stroke()}}
function dot(x,y,r,color){ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill()}
function setFont(size,weight){ctx.font=`${weight} ${size}px ${FONT}`}
function rec(x0,y0,x1,y1,label){if(REC())window.__boxes.push({x0,y0,x1,y1,label})}
/* single-line text; x is the anchor according to align (left edge / centre / right edge) */
function txt(t,x,y,size=30,color=P.ink,align='left',weight=700){
t=String(t);ctx.fillStyle=color;setFont(size,weight);ctx.textAlign=align;ctx.textBaseline='middle';ctx.fillText(t,x,y);
if(REC()){const w=ctx.measureText(t).width,l=align==='left'?x:align==='center'?x-w/2:x-w;rec(l,y-size*.55,l+w,y+size*.55,t.slice(0,24))}}
/* wrapped paragraph; returns the y below the last line. Normalises literal "\n". Breaks CJK per char, Latin per word. */
function wrap(t,x,y,maxw,lh=34,size=24,color=P.muted,weight=550,align='left'){
t=String(t).replace(/\\n/g,'\n');setFont(size,weight);ctx.fillStyle=color;ctx.textBaseline='top';ctx.textAlign=align;
const out=[];
for(const para of t.split('\n')){
const raw=para.match(/[一-鿿]|[^\s一-鿿 -〿-]+\s*|\s+|[ -〿-]/g)||[''];
const toks=[];for(let i=0;i<raw.length;i++){if(/^[「『(《【“‘]$/.test(raw[i])&&i+1<raw.length){toks.push(raw[i]+raw[i+1]);i++}else toks.push(raw[i])}
let ln='';
for(const tk of toks){
const q=ln+tk;
const isPunct=/^[ -〿-]$/.test(tk);
if(ctx.measureText(q.trimEnd()).width>maxw&&ln&&!isPunct){out.push(ln.trimEnd());ln=tk.trimStart()}else ln=q}
out.push(ln.trimEnd());
}
let yy=y;
for(const l of out){ctx.fillText(l,x,yy);
if(REC()){const w=ctx.measureText(l).width,lx=align==='left'?x:align==='center'?x-w/2:x-w;rec(lx,yy,lx+w,yy+size*1.15,l.slice(0,24))}
yy+=lh}
return yy}
function footer(n){txt('PR115 · Ch02 · KP-19 貝氏網路(二)Bayesian Networks II',70,878,15,'#6f8da3','left',600);txt(`${n} / 9`,1530,878,15,'#6f8da3','right',700)}
function title(zh,en,no){txt(`${String(no).padStart(2,'0')} ${zh}`,70,68,40,P.ink,'left',900);txt(en,72,111,22,P.cyan,'left',650);line(70,146,1530,146,P.grid,2);footer(no)}
function chip(t,x,y,w,color){rr(x,y,w,40,20,color+'20',color,2);txt(t,x+w/2,y+20,18,color,'center',800)}
function btn(label,x,y,w,h,onClick,active=false,color=P.cyan,size=19){rr(x,y,w,h,15,active?color+'28':'#ffffff0c',active?color:'#ffffff22',2);txt(label,x+w/2,y+h/2,size,active?color:P.ink,'center',800);zones.push({type:'button',x,y,w,h,onClick})}
function area(x,y,w,h,onClick){zones.push({type:'button',x,y,w,h,onClick})}
function slider(x,y,w,min,max,value,onChange,label,fmt=v=>v.toFixed(2)){
line(x,y,x+w,y,'#ffffff35',8);const t=(value-min)/(max-min),kx=x+t*w;line(x,y,kx,y,P.amber+'88',8);dot(kx,y,15,P.amber);
txt(label,x,y-31,20,P.muted,'left',650);txt(fmt(value),x+w,y-31,22,P.amber,'right',850);
zones.push({type:'slider',x:x-20,y:y-28,w:w+40,h:56,min,max,onChange})}
function arrow(x1,y1,x2,y2,color=P.cyan,lw=5){line(x1,y1,x2,y2,color,lw);const a=Math.atan2(y2-y1,x2-x1),s=14;ctx.fillStyle=color;ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-s*Math.cos(a-.5),y2-s*Math.sin(a-.5));ctx.lineTo(x2-s*Math.cos(a+.5),y2-s*Math.sin(a+.5));ctx.closePath();ctx.fill()}
function card(x,y,w,h,color,active=true){rr(x,y,w,h,22,active?P.panel2:P.panel,active?color:'#ffffff14',3)}
/* ---------- pointer interaction (logical coordinates) ---------- */
function pos(e){const r=c.getBoundingClientRect();return{x:(e.clientX-r.left)*CW/r.width,y:(e.clientY-r.top)*CH/r.height}}
function sliderVal(z,p){const t=Math.max(0,Math.min(1,(p.x-(z.x+20))/(z.w-40)));return z.min+t*(z.max-z.min)}
c.addEventListener('pointerdown',e=>{const p=pos(e);
for(let i=zones.length-1;i>=0;i--){const z=zones[i];
if(p.x>=z.x&&p.x<=z.x+z.w&&p.y>=z.y&&p.y<=z.y+z.h){
if(z.type==='button'){z.onClick(p);return}
if(z.type==='slider'||z.type==='drag'){drag=z;z.onChange(sliderVal(z,p),p);return}}}});
c.addEventListener('pointermove',e=>{if(!drag)return;const p=pos(e);drag.onChange(sliderVal(drag,p),p)});
addEventListener('pointerup',()=>drag=null);
addEventListener('pointercancel',()=>drag=null);
/* ---------- KaTeX math layer (shares the 1600x900 logical coordinate system) ---------- */
const FM={};
function placeFormula(f){const s=f.el.style;s.width=f.w+'px';s.fontSize=f.size+'px';s.color=f.color;s.textAlign=f.align;
let left=f.x+f.ox;if(f.align==='center')left-=f.w/2;else if(f.align==='right')left-=f.w;
s.left=left+'px';s.top=(f.y+f.oy)+'px'}
function setTex(id,tex){const f=FM[id];if(f.cur===tex)return;f.cur=tex;
if(window.katex)katex.render(tex,f.el,{displayMode:true,throwOnError:false,strict:'ignore'});else f.el.textContent=tex}
/* x,y: anchor (y = vertical centre of the formula box); align: left|center|right; size: logical px */
function addFormula(o){const el=document.createElement('div');el.className='formula';el.dataset.fid=o.id;mathLayer.appendChild(el);
const f=Object.assign({el,align:'center',size:32,ox:0,oy:0,w:600,color:'#f7fbff'},o);FM[o.id]=f;placeFormula(f);setTex(o.id,o.tex);return f}
function moveFormula(id,o){Object.assign(FM[id],o);placeFormula(FM[id])}
function showF(ids){for(const k in FM)FM[k].el.style.display=ids.includes(k)?'block':'none'}
function syncMath(){const r=stage.getBoundingClientRect();mathLayer.style.transform=`scale(${r.width/CW},${r.height/CH})`}
new ResizeObserver(syncMath).observe(stage);addEventListener('resize',syncMath);syncMath();
/* ---------- plotting ---------- */
function mapper(px,py,pw,ph,x0,x1,y0,y1){return{X:x=>px+(x-x0)/(x1-x0)*pw,Y:y=>py+ph-(y-y0)/(y1-y0)*ph,px,py,pw,ph,x0,x1,y0,y1}}
function curvePath(f,m,xa,xb,n=240){ctx.beginPath();for(let i=0;i<=n;i++){const x=xa+(xb-xa)*i/n,X=m.X(x),Y=m.Y(f(x));i?ctx.lineTo(X,Y):ctx.moveTo(X,Y)}}
function plotCurve(f,m,xa,xb,color,lw=5,fillAlpha=null,dash=[]){
if(fillAlpha){curvePath(f,m,xa,xb);ctx.lineTo(m.X(xb),m.Y(0));ctx.lineTo(m.X(xa),m.Y(0));ctx.closePath();ctx.fillStyle=color+fillAlpha;ctx.fill()}
ctx.save();ctx.setLineDash(dash);curvePath(f,m,xa,xb);ctx.strokeStyle=color;ctx.lineWidth=lw;ctx.lineJoin='round';ctx.stroke();ctx.restore()}
function axes(m,xticks,ytickVals,xfmt=v=>v,yfmt=v=>v.toFixed(2),xlabel=''){
line(m.px,m.py+m.ph,m.px+m.pw,m.py+m.ph,'#ffffff55',2);line(m.px,m.py,m.px,m.py+m.ph,'#ffffff55',2);
for(const v of ytickVals){const Y=m.Y(v);line(m.px,Y,m.px+m.pw,Y,'#ffffff12',1);txt(yfmt(v),m.px-12,Y,17,P.muted,'right',600)}
for(const v of xticks){const X=m.X(v);line(X,m.py+m.ph,X,m.py+m.ph+8,'#ffffff55',2);txt(xfmt(v),X,m.py+m.ph+26,17,P.muted,'center',600)}
if(xlabel)txt(xlabel,m.px+m.pw,m.py+m.ph+52,17,P.muted,'right',600)}
const fmt3=v=>v.toFixed(3);
const rampAnim=(dur,fn)=>{const t0=performance.now();const tick=now=>{const t=Math.min(1,(now-t0)/dur);fn(1-Math.pow(1-t,3));if(t<1)requestAnimationFrame(tick)};requestAnimationFrame(tick)};
/* ===== Page script ===== */
/* ===== Shared helpers for KP18: DAG drawing, parent sets, CPT parameter counting ===== */
const C1='#63b3ed',C2='#ff7b72';
const NM=['x₁','x₂','x₃','x₄','x₅','x₆'];
const POS6=[[.10,.26],[.10,.76],[.38,.93],[.42,.50],[.70,.22],[.90,.60]]; /* normalised node positions */
const EDGES_BN=[[2,3],[1,4],[2,4],[4,5],[4,6],[5,6]]; /* (parent, child), 1-indexed */
const EDGES_FULL=(()=>{const e=[];for(let j=2;j<=6;j++)for(let i=1;i<j;i++)e.push([i,j]);return e})();
const parentsOf=(E,i)=>E.filter(e=>e[1]===i).map(e=>e[0]);
const nParams=(E,n=6)=>{let s=0;for(let i=1;i<=n;i++)s+=Math.pow(2,parentsOf(E,i).length);return s};
function hasCycle(E,n=6){const adj={};for(let i=1;i<=n;i++)adj[i]=[];E.forEach(e=>adj[e[0]].push(e[1]));const st={};
const dfs=u=>{st[u]=1;for(const v of adj[u]){if(st[v]===1)return true;if(!st[v]&&dfs(v))return true}st[u]=2;return false};
for(let i=1;i<=n;i++)if(!st[i]&&dfs(i))return true;return false}
/* draw a DAG in box {x,y,w,h}; opt: sel (node), r (radius), hl (list of [p,c] edges to colour), colors */
function drawDAG(box,pos,E,opt){opt=opt||{};const r=opt.r||26,sel=opt.sel||0,names=opt.names||NM,P_=sel?parentsOf(E,sel):[];
const at=i=>[box.x+pos[i-1][0]*box.w,box.y+pos[i-1][1]*box.h];
for(const [p,c] of E){const [x1,y1]=at(p),[x2,y2]=at(c),a=Math.atan2(y2-y1,x2-x1);
const hot=(sel&&c===sel)||(opt.hl&&opt.hl.some(h=>h[0]===p&&h[1]===c));
arrow(x1+Math.cos(a)*(r+2),y1+Math.sin(a)*(r+2),x2-Math.cos(a)*(r+4),y2-Math.sin(a)*(r+4),hot?(opt.hotColor||P.lime):(opt.edgeColor||'#9fb7c9'),hot?5:(opt.thin?2:3.5))}
const out=[];
for(let i=1;i<=pos.length;i++){const [x,y]=at(i),isSel=i===sel,isPar=P_.includes(i);
ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fillStyle=isSel?P.amber+'44':(isPar?P.lime+'33':P.panel2);ctx.fill();
ctx.strokeStyle=isSel?P.amber:(isPar?P.lime:'#8fb2c9');ctx.lineWidth=isSel||isPar?4:2.5;ctx.stroke();
txt(names[i-1],x,y+1,r>20?22:15,P.ink,'center',900);out.push([x,y])}
return out}
const SUPM={'0':'⁰','1':'¹','2':'²','3':'³','4':'⁴','5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹','-':'⁻'};
const sup=n=>String(n).split('').map(c=>SUPM[c]||c).join('');
/* ===== KP19: the four-variable chain x -> y -> z -> w with the lecture's CPTs ===== */
const CPC={px:.6,py:[.3,.4],pz:[.6,.25],pw:[.3,.45]}; /* P(child=1 | parent=0/1) */
const NC=['x','y','z','w'],POS4=[[.07,.5],[.36,.5],[.64,.5],[.93,.5]];
const bern=(p,t)=>t?p:1-p;
function jointC(v,th){th=th||CPC;return bern(th.px,v[0])*bern(th.py[v[0]],v[1])*bern(th.pz[v[1]],v[2])*bern(th.pw[v[2]],v[3])}
/* exact posterior P(q=1 | evidence) by full enumeration; ev maps node index -> 0/1 */
function postC(q,ev,th){let n1=0,n0=0;for(let m=0;m<16;m++){const v=[(m>>3)&1,(m>>2)&1,(m>>1)&1,m&1];
if(Object.keys(ev).some(k=>v[k]!==ev[k]))continue;const j=jointC(v,th);if(v[q])n1+=j;else n0+=j}return n1/(n1+n0)}
/* 04 — Scenario (b): evidence at w, messages flow upstream with Bayes inversion at every station */
let st=3,e=1;
const F=(v,d=3)=>v.toFixed(d);
['s1','s2','s3'].forEach((id,i)=>addFormula({id,tex:'',x:520,y:526+i*134,w:880,size:21}));
function render(){
reset();title('核心機制:逆流上傳歸因','Core mechanism: upstream attribution via Bayes inversion',4);
const pX=postC(0,{}),pY=postC(1,{}),pZ=postC(2,{}),pW=postC(3,{});
const pwE=e?pW:1-pW;
const mz=bern(CPC.pw[1],e)*pZ/pwE;
const ry1=CPC.pz[1]*pY/pZ,ry0=(1-CPC.pz[1])*pY/(1-pZ),my=ry1*mz+ry0*(1-mz);
const rx1=CPC.py[1]*pX/pY,rx0=(1-CPC.py[1])*pX/(1-pY),mx=rx1*my+rx0*(1-my);
rr(60,170,1480,230,24,P.panel,'#ffffff14',2);
const box={x:130,y:210,w:1340,h:120},ps=POS4.map(p=>[box.x+p[0]*box.w,box.y+p[1]*box.h]);
for(let i=0;i<3;i++){const on=(3-i)<=st;arrow(ps[i][0]+30,ps[i][1],ps[i+1][0]-34,ps[i+1][1],'#9fb7c9',3.5);
if(on)arrow(ps[i+1][0]-30,ps[i+1][1]+18,ps[i][0]+34,ps[i][1]+18,C1,6)}
ps.forEach((p,i)=>{ctx.beginPath();ctx.arc(p[0],p[1],28,0,Math.PI*2);ctx.fillStyle=i===3?P.cyan+'44':((3-i)<=st?C1+'33':P.panel2);ctx.fill();ctx.strokeStyle=i===3?P.cyan:((3-i)<=st?C1:'#8fb2c9');ctx.lineWidth=3.5;ctx.stroke();txt(NC[i],p[0],p[1]+1,22,P.ink,'center',900)});
txt('證據 w = '+e,ps[3][0],ps[3][1]-48,16,P.cyan,'center',800);
const q=[`P(x=${e?0:1}|w=${e}) = ${F(e?1-mx:mx)}`,`P(y=1|w=${e}) = ${F(my)}`,`P(z=1|w=${e}) = ${F(mz)}`];
[2,1,0].forEach((i,k)=>{if(k<st)txt(q[i],(ps[i][0]+ps[i+1][0])/2,ps[i][1]-62,16,C1,'center',850)});
rr(60,420,980,430,24,P.panel,'#ffffff14',2);
const rows=[['第一步:w 傳給 z(貝氏反轉)',C1],['第二步:z 傳給 y(反推 CPT + 加權求和)',C1],['第三步:y 傳給 x(再反推一次,抵達終點)',C1]];
setTex('s1',`P(z\\!=\\!1\\mid w\\!=\\!${e})=\\dfrac{P(w\\!=\\!${e}\\mid z\\!=\\!1)\\,P(z\\!=\\!1)}{P(w\\!=\\!${e})}=\\dfrac{${F(bern(CPC.pw[1],e),2)}\\times${F(pZ,3)}}{${F(pwE,4)}}=\\mathbf{${F(mz,4)}}`);
setTex('s2',`\\begin{aligned}P(y\\!=\\!1\\mid z\\!=\\!1)&=${F(ry1,3)},\\quad P(y\\!=\\!1\\mid z\\!=\\!0)=${F(ry0,3)}\\\\P(y\\!=\\!1\\mid w\\!=\\!${e})&=(${F(ry1,3)})(${F(mz,4)})+(${F(ry0,3)})(${F(1-mz,4)})=\\mathbf{${F(my,4)}}\\end{aligned}`);
setTex('s3',`\\begin{aligned}P(x\\!=\\!1\\mid y\\!=\\!1)&=${F(rx1,3)},\\quad P(x\\!=\\!1\\mid y\\!=\\!0)=${F(rx0,4)}\\\\P(x\\!=\\!1\\mid w\\!=\\!${e})&=(${F(rx1,3)})(${F(my,4)})+(${F(rx0,4)})(${F(1-my,4)})=\\mathbf{${F(mx,4)}}\\end{aligned}`);
const vis=[];
rows.forEach((r,i)=>{const y=438+i*134,on=i<st;rr(80,y,940,122,18,on?P.panel2:'#ffffff06',on?r[1]+'99':'#ffffff12',on?3:2);txt(r[0],104,y+22,18,on?r[1]:'#5f7a8d','left',850);
if(on)vis.push('s'+(i+1));else txt('按「下一步」揭露 · Press Next',550,y+72,18,'#4a6272','center',700)});
rr(1070,420,470,430,24,P.panel,'#ffffff14',2);
btn(st<3?'下一步 Next':'重播 Replay',1100,446,200,50,()=>{st=st<3?st+1:1;render()},true,P.cyan,18);
btn('w = 1',1316,446,100,50,()=>{e=1;render()},e===1,P.amber,17);btn('w = 0',1426,446,90,50,()=>{e=0;render()},e===0,P.amber,17);
wrap('反直覺的精妙結論:逆著推,每一站也一樣輕盈——貝氏定理只多做一次除法與反轉。',1100,530,410,30,19,P.ink,650);
txt('Backward is just as cheap per station.',1100,640,14,P.muted,'left',600);
const ok=Math.abs((e?1-postC(0,{3:e}):postC(0,{3:e}))-(e?1-mx:mx))<1e-9||true;
rr(1100,680,410,140,16,'#63b3ed14',C1,2);
txt('驗算(全域窮舉)',1120,706,17,C1,'left',850);txt(`P(z=1|w=${e}) = ${F(postC(2,{3:e}),4)}`,1120,740,18,P.ink,'left',700);txt(`P(y=1|w=${e}) = ${F(postC(1,{3:e}),4)}`,1120,770,18,P.ink,'left',700);txt(`P(x=0|w=${e}) = ${F(1-postC(0,{3:e}),4)}`,1120,800,18,P.ink,'left',700);
showF(vis);
}
render();
</script></body></html>