Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/course/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"order": 3,
"title": "一个词如何参考其他词?",
"outcome": "从 QK 分数到因果遮罩、注意力权重与 V 的加权结果。",
"status": "planned",
"route": null,
"status": "reviewed",
"route": "/learn/attention/",
"published": false
},
{
Expand Down
17 changes: 17 additions & 0 deletions apps/site/learn/attention/LESSON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# One query, a weighted mixture

Core question: how does the query at position C combine visible values? Follow authored Q/K/V → scaled dot products → future mask → Softmax → multiply V → sum. Q/K decide weights; V supplies content. The result includes every visible contribution, not just an argmax. D's future value never leaks into the result. Use symbolic positions to avoid falsely suggesting measured word semantics.

Counterfactuals: q toward A gives 4.247896; toward C gives 15.752104. q=0 gives 10. D's V can vary 0…1000 with no output change. Predict → operate → explain asks the zero-query case with D=1000; feedback explains the visible denominator and rejects max selection/four-way averaging.

Sources checked 2026-10-28: Attention Is All You Need §3.2 (https://arxiv.org/abs/1706.03762), and PyTorch official scaled_dot_product_attention documentation/source pseudocode (https://docs.pytorch.org/docs/2.14/generated/torch.nn.functional.scaled_dot_product_attention.html). One causal row is represented; d_k=2, d_v=1, no dropout or projections. Authored numbers are not trained embeddings, meaningful word relations or real model inference. Link flow is a teaching aid, not model execution timing. Attention weights are not a complete explanation of reasoning.

Numerical evidence: golden independent probabilities/output, normalization, future-value invariance, convex bounds, parameter boundaries and deterministic forward/reverse samples. Browser observations and deployment receipts remain pending. No measured learner study, physical phone, screen-reader-device or cross-engine claim.

## Browser teaching review — 2026-10-28

Managed Chrome desktop (1363×936) and 390×844 iframe viewport; not physical-phone testing. Initial input, contribution stage, Softmax exponent/denominator stage, mask stage and final result were inspected. Annotation targets were moved to empty margins to avoid crossing numeric rows; flow links stop before contribution labels. Stable identity colors remain across K/V, weights and output terms. The opening question/input and the output readout are identifiable without inspecting side panels. This is a developer teaching assessment, not measured learner evidence.

Observed: complete playback reached 48/48 and restored configuration. Backward seek 48→25→16 rebuilt earlier phases; resume/Escape pause, previous/next, Home/End, replay and reset were exercised. During playback configuration disappeared from the accessibility tree with focus on the persistent pause control. Reduced-motion toggle was enabled; t=31.1 displayed the stable full contribution bar. Query A produced 4.248; default C produced 15.752; changing future V to 1000 preserved output 15.752. The mobile quiz wrong/correct feedback and verify flow returned q=0, D=1000, output 10.000 and visible weights 1/3 each. Dialog fit in the narrow viewport.

`evals/browser/attention-lifecycle.html` tested the actual app's visibility handler using controlled document-hidden events: hidden paused at ~0.05s, configuration restored; visible did not auto-resume or jump. This is event-path testing, not actual OS hidden-tab observation. OS-level preference switching, physical devices, screen-reader devices and other engines remain untested. Full-site checks including mechanical invariants passed. Preview and actual production evidence will be recorded separately.
1 change: 1 addition & 0 deletions apps/site/learn/attention/SCENE_SPEC.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"attention","question":"当前查询如何把可见位置的 V 汇成一个输出?","durationSeconds":48,"inputs":{"alpha":[-1,0,1],"futureV":{"min":0,"max":1000,"step":10}},"queryPosition":2,"keyDimension":2,"valueDimension":1,"keys":[[-1,0],[0,0],[1,0],[2,0]],"values":[0,10,20,"futureV"],"relations":["q=[sqrt(2)*alpha,0]","s=qK^T/sqrt(2)","mask j>2 to -Infinity before Softmax","p=Softmax(masked scores)","o=sum(p_j*V_j)"],"mapping":{"color":"position identity","probabilityBar":"0..1 within each channel","contributionBar":"length of p_j*V_j; shared total scale 0..20","connections":"dependencies, not physical motion or measured timing"},"experiment":"Keep K/V fixed and change q; then vary masked future V and show output invariance.","transfer":"q=0, futureV=1000 gives mean of visible 0,10,20 = 10, not max or four-way mean.","sources":["https://arxiv.org/abs/1706.03762","https://docs.pytorch.org/docs/2.14/generated/torch.nn.functional.scaled_dot_product_attention.html"],"simplifications":["Authored Q/K/V, no model or semantic interpretation","One causal row and head; V scalar","No learned projection, positions, dropout, output projection, residual, training or token sampling","SVG teaching geometry, not actual model hardware"]}
26 changes: 26 additions & 0 deletions apps/site/learn/attention/app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {TOKENS,STARTS,DURATION,sample,narration} from './model.mjs';
import {createStage} from './stage.mjs';
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)],motion=matchMedia('(prefers-reduced-motion: reduce)');
let reduced=motion.matches;
const stage=createStage($('#artwork'),{reducedMotion:()=>reduced});
let id=1,future=100,time=0,playing=false,raf=0,last=0,lastCopy='',dead=false;
const names=['输入 Q/K/V','匹配分数','因果遮罩','分配权重','加权内容','求和与实验'];
function focus(on){$('#app').classList.toggle('is-playing',on);$$('.secondary').forEach(x=>{x.inert=on;x.setAttribute('aria-hidden',String(on));});}
function anchor(){const box=$('.exhibit').getBoundingClientRect(),bubble=$('#callout').getBoundingClientRect(),art=$('#artwork');art.style.top=Math.max(185,bubble.bottom-box.top+13)+'px';const a=art.getBoundingClientRect(),point=stage.projectAnchor();if(!point)return;const x=a.left-box.left+point.x*a.width,y=a.top-box.top+point.y*a.height,cx=Math.max(bubble.width/2+17,Math.min(box.width-bubble.width/2-17,x));$('#callout').style.left=cx+'px';$('#leader').setAttribute('viewBox',`0 0 ${box.width} ${box.height}`);const sy=bubble.bottom-box.top;const side=a.left-box.left+point.bendX*a.width,start=cx-bubble.width/2+10;$('#leader-line').setAttribute('d',point.routeLeft?`M${start} ${sy}C${side} ${sy+12},${side} ${sy+12},${side} ${sy+27}V${y-8}Q${side} ${y},${side+8} ${y}H${x-8}`:`M${cx} ${sy}C${cx} ${sy+20},${x} ${y-25},${x} ${y-8}`);$('#leader-dot').setAttribute('cx',x);$('#leader-dot').setAttribute('cy',y);}
function render(){if(dead)return;const f=sample(time,{id,future}),c=narration(f),key=JSON.stringify(c);stage.render(f);if(key!==lastCopy){lastCopy=key;$('#copy-eyebrow').textContent=c.eyebrow;$('#copy-title').textContent=c.title;$('#copy-body').textContent=c.body;}$('#phase-label').textContent=`0${f.phase+1} / 06 · ${names[f.phase]}`;$('#counts').textContent=`当前位置 C · 单头单行`;$$('[data-id]').forEach(b=>b.setAttribute('aria-pressed',String(+b.dataset.id===id)));$('#future').value=future;$('#future-value').textContent=future;$('#timeline').value=time;$('#timeline').style.setProperty('--fill',`${time/DURATION*100}%`);$('#time').textContent=`00:${String(Math.floor(time)).padStart(2,'0')} / 00:48`;$('#play-label').textContent=playing?'暂停':time>=DURATION?'再看一遍':time?'继续讲解':'播放讲解';$('#play-icon').textContent=playing?'Ⅱ':'▶';$('#play').setAttribute('aria-pressed',String(playing));$('#back').disabled=time===0;$('#next').disabled=time===DURATION;$('#play-note').textContent=playing?'配置已收起 · 空格暂停':'六个步骤 · 从查询到加权和';$('#challenge').hidden=time<DURATION;anchor();}
function advance(now){if(playing){time=Math.min(DURATION,time+Math.max(0,now-last)/1000);last=now;}}
function stop(capture=true){if(capture)advance(performance.now());playing=false;cancelAnimationFrame(raf);focus(false);render();}
function tick(now){if(!playing)return;advance(now);render();if(time===DURATION)stop(false);else raf=requestAnimationFrame(tick);}
function play(){if(playing){stop();return;}if(time===DURATION)time=0;$('#play').focus({preventScroll:true});playing=true;focus(true);last=performance.now();render();raf=requestAnimationFrame(tick);}
function seek(t){stop();time=Math.max(0,Math.min(DURATION,t));render();}
$('#play').onclick=play;$('#timeline').oninput=e=>seek(+e.target.value);$('#back').onclick=()=>seek(STARTS.filter(t=>t<time-.1).at(-1)??0);$('#next').onclick=()=>seek(STARTS.find(t=>t>time+.1)??DURATION);$('#reset').onclick=()=>{stop();time=0;id=1;future=100;render();};
$$('[data-id]').forEach(b=>b.onclick=()=>{stop();id=+b.dataset.id;render();});$('#experiment').onclick=()=>{seek(40);$('#app [data-id="1"]').focus();};
$('#future').oninput=e=>{const value=+e.target.value;stop();future=value;render();};
function setReduced(value){reduced=value;$('#reduce-motion').checked=value;$('#app').classList.toggle('reduce-motion',value);render();}
$('#reduce-motion').onchange=e=>setReduced(e.target.checked);motion.addEventListener('change',e=>setReduced(e.matches));
function info(){stop();$('#info').showModal();}$('#about').onclick=info;$('#show-table').onclick=info;$('#close-info').onclick=()=>$('#info').close();
$('#challenge').onclick=()=>{stop();$('#feedback').textContent='';$('#quiz-try').hidden=true;$$('[data-answer]').forEach(b=>b.removeAttribute('aria-pressed'));$('#quiz').showModal();};
$$('[data-answer]').forEach(b=>b.onclick=()=>{$$('[data-answer]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));$('#feedback').textContent=(b.dataset.answer==='mean'?'预测正确。':'输出为 10。')+'q 归零使所有匹配分数为 0;D 仍被遮罩,A/B/C 的权重各为 1/3。结果是 0/3 + 10/3 + 20/3 = 10,不是取最大值,也不是把未来的 1000 混入。';$('#quiz-try').hidden=false;});
$('#quiz-try').onclick=()=>{$('#quiz').close();id=0;future=1000;time=48;render();};$('#close-quiz').onclick=()=>$('#quiz').close();
document.addEventListener('visibilitychange',()=>{if(document.hidden&&playing)stop();});document.addEventListener('keydown',e=>{if($('#info').open||$('#quiz').open)return;if(e.key==='Escape')stop();if(e.code==='Space'&&!['INPUT','BUTTON','A'].includes(e.target.tagName)){e.preventDefault();play();}});
const observer=new ResizeObserver(()=>{stage.resize();anchor();});observer.observe($('#artwork'));$('.exhibit').addEventListener('transitionend',anchor);window.addEventListener('pagehide',e=>{stop();if(!e.persisted){dead=true;observer.disconnect();stage.dispose();}});setReduced(reduced);
2 changes: 2 additions & 0 deletions apps/site/learn/attention/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个词如何参考其他词? · Explain3D</title><meta name="description" content="用可暂停的查表动画理解 Token Embedding:编号、向量、坐标、距离与方向。"><link rel="stylesheet" href="./style.css"><script type="module" src="./app.mjs"></script></head><body><main class="app" id="app"><header><a class="brand" href="/learn/"><i>e·</i> Explain3D</a><span>AI 原理课堂 / 03</span><button class="quiet" id="about">模型与边界 ↗</button></header><section class="intro secondary"><div><p class="eyebrow">ONE QUERY · A WEIGHTED MIXTURE</p><h1>注意力,<span>怎样汇成一个结果?</span></h1></div><p class="intro-copy">从匹配分数,到可见信息的加权和。</p></section><section class="controls secondary" aria-label="Attention 实验"><div class="presets" role="group" aria-label="查询方向"><button data-id="-1">查询偏向 A</button><button data-id="0">查询归零</button><button data-id="1">查询偏向 C</button></div><span class="divider"></span><label>未来 D 的 V <output id="future-value">100</output><input type="range" id="future" min="0" max="1000" step="10" value="100" aria-label="未来 D 的 V"></label><button id="experiment" class="experiment">做一次对照 ↗</button></section><section class="exhibit"><div class="exhibit-meta"><span id="phase-label"></span><span id="counts"></span></div><div class="callout" id="callout" role="status" aria-live="polite"><span id="copy-eyebrow"></span><h2 id="copy-title"></h2><p id="copy-body"></p><button id="show-table" class="calculation">查表与公式 +</button></div><svg class="leader" id="leader" aria-hidden="true"><path id="leader-line"/><circle id="leader-dot" r="4"/></svg><div class="artwork" id="artwork"></div><div class="stage-bottom"><span>手写 Q/K/V · 单头单行 · 未运行语言模型</span><button class="quiet" id="challenge" hidden>检验理解 ↗</button></div></section><div class="transport"><button id="play" class="play"><span id="play-icon">▶</span><span id="play-label">播放讲解</span></button><button id="back" class="quiet" aria-label="上一步">←</button><div class="track"><input type="range" id="timeline" min="0" max="48" step="0.05" value="0" aria-label="演示进度"><div><span id="play-note">六个步骤 · 从查询到加权和</span><span id="time">00:00 / 00:48</span></div></div><button id="next" class="quiet" aria-label="下一步">→</button><button id="reset" class="quiet">重置</button></div><footer class="secondary"><span>Explain3D · AI 原理课堂</span><span>Q/K 决定权重 · V 提供内容</span></footer></main>
<dialog id="info" aria-labelledby="info-title"><button class="close" id="close-info" aria-label="关闭模型说明">×</button><p class="eyebrow">SCALED DOT-PRODUCT ATTENTION</p><h2 id="info-title">权重和内容,是两件事。</h2><p>本页计算因果自注意力的一行:当前位置 C(从 0 开始索引 2),可以参考 A/B/C,不能参考未来的 D。Q、K、V 在真实网络中由输入表示经过学习到的线性投影得到;本页直接给定这些参数,不演示投影、训练、多头、位置编码、输出投影、残差或下一词采样。</p><div class="formula">q = [√2 × α, 0];α ∈ {−1, 0, 1}<br>K = [ [−1,0], [0,0], [1,0], [2,0] ]<br>V = [0, 10, 20, 未来值]<br>s = qKᵀ / √2 = [−α, 0, α, 2α]<br>s(遮罩后)= [−α, 0, α, −∞]<br>pⱼ = exp(sⱼ) / Σ exp(sₖ)<br>o = Σ pⱼVⱼ</div><p>V 为一维,只为看清乘法与求和。真实 V 通常是向量,逐分量使用同一组权重。因果遮罩包含当前位置自身,先应用遮罩再做 Softmax。没有 dropout。本页色彩只标识位置 A/B/C/D;卡片宽度不编码数值。概率条宽度表示权重,最终彩色条长度表示各项贡献(总长对应输出,刻度 0…20)。连接线表示依赖关系,不表示网络真实空间或耗时。</p><p>手写数字与符号 A/B/C/D 不代表任何真实语言模型或自然语言词义。本页的注意力权重也不是“解释模型推理”的充分证据。查询归零时,可见位置均分权重,输出为 (0+10+20)/3 = 10,未来值仍被排除。</p><p><label><input type="checkbox" id="reduce-motion"> 减少动态效果(保留分步讲解与进度)</label></p><p><a href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener">原论文 §3.2 ↗</a> · <a href="https://docs.pytorch.org/docs/2.14/generated/torch.nn.functional.scaled_dot_product_attention.html" target="_blank" rel="noopener">PyTorch 官方公式与因果遮罩实现 ↗</a></p></dialog><dialog id="quiz" aria-labelledby="quiz-title"><button class="close" id="close-quiz" aria-label="关闭学习检验">×</button><p class="eyebrow">PREDICT · TRY · EXPLAIN</p><h2 id="quiz-title">查询归零,输出会是多少?</h2><p>q = [0,0],可见位置 A/B/C 的 V 为 0/10/20;未来 D 的 V 为 1000。因果遮罩仍然启用。</p><div class="challenge-answers"><button data-answer="mean">10 · 可见三项均分</button><button data-answer="future">257.5 · 四项均分</button><button data-answer="max">20 · 只取最大可见值</button></div><p id="feedback" role="status" aria-live="polite"></p><button class="experiment" id="quiz-try" hidden>在图中验证 ↗</button></dialog></body></html>
12 changes: 12 additions & 0 deletions apps/site/learn/attention/model.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const TOKENS=['A','B','C','D'],COLORS=['#6b8fc6','#9b84ca','#73a995','#c58b73'];
export const STARTS=[0,7,15,23,31,40],DURATION=48;
export function calculate(alpha=1,future=100){if(![-1,0,1].includes(alpha)||!Number.isFinite(future)||future<0||future>1000)throw new RangeError('Invalid teaching input');const q=[Math.SQRT2*alpha,0],keys=[[-1,0],[0,0],[1,0],[2,0]],values=[0,10,20,future],scores=keys.map(k=>(q[0]*k[0]+q[1]*k[1])/Math.sqrt(2)),masked=scores.map((s,i)=>i>2?-Infinity:s),max=Math.max(...masked),weights=masked.map(s=>Math.exp(s-max)),sum=weights.reduce((a,b)=>a+b),probs=weights.map(w=>w/sum),terms=values.map((v,i)=>v*probs[i]);return {alpha,future,q,keys,values,scores,masked,weights,probs,terms,output:terms.reduce((a,b)=>a+b)};}
export function sample(t,{id=1,future=100}={}){t=Number.isFinite(t)?Math.max(0,Math.min(DURATION,t)):0;const phase=STARTS.findLastIndex(x=>t>=x);return {...calculate(id,future),time:t,phase,progress:(t-STARTS[phase])/((STARTS[phase+1]??DURATION)-STARTS[phase])};}
export function narration(f){return [
{title:'C 正在读取谁的信息?',body:'当前查询位置是 C。A、B、C 可以参与;D 在未来。本页只计算这一行、一个头,Q/K 是二维,V 用一个数简化展示。'},
{title:'Q 和 K 相比,得到分数',body:`q = [${f.q[0].toFixed(3)}, 0];分数 sⱼ = q·kⱼ / √2。A、B、C、D 的分数为 ${f.scores.map(x=>x.toFixed(1)).join('、')}。分数还不是权重。`},
{title:'先挡住未来,再分配权重',body:`D 的分数 ${f.scores[3].toFixed(1)} 被改为 −∞,指数为 0。它不进入分母;即使它的 V 是 ${f.future},也不能参与当前输出。`},
{title:'可见位置共同分到 100%',body:`指数归一化(Softmax)后:A ${(f.probs[0]*100).toFixed(1)}%、B ${(f.probs[1]*100).toFixed(1)}%、C ${(f.probs[2]*100).toFixed(1)}%、D 0%。这里是信息混合的权重,不是在抽取下一个 Token。`},
{title:'用权重乘 V,而不是乘 K',body:`各通道贡献为 ${f.terms.map(x=>x.toFixed(3)).join(' + ')}。Q/K 决定读多少,V 提供读出的内容;这些贡献会全部相加,不只取最大的一项。`},
{title:`得到一个混合结果:${f.output.toFixed(3)}`,body:'保持 K/V 固定,切换查询方向,观察输出改变;再拖动未来 D 的 V,输出应保持不变。检验理解:查询归零后,输出会是多少?'}
].map((c,i)=>({...c,eyebrow:`0${i+1} · ${['INPUT','Q × K','CAUSAL MASK','SOFTMAX','WEIGHT × VALUE','SUM & TRY'][i]}`}))[f.phase];}
9 changes: 9 additions & 0 deletions apps/site/learn/attention/model.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import assert from 'node:assert/strict';
import {calculate,sample,DURATION} from './model.mjs';
const close=(a,b)=>assert(Math.abs(a-b)<1e-11,`${a} vs ${b}`);
const f=calculate();[.09003057317038046,.24472847105479764,.6652409557748218,0].forEach((p,i)=>close(f.probs[i],p));close(f.output,15.752103826044414);
close(calculate(-1).output,4.247896173955585);close(calculate(0).output,10);
for(const a of [-1,0,1])for(const future of [0,10,100,1000]){const f=calculate(a,future);close(f.probs.reduce((a,b)=>a+b),1);assert.equal(f.probs[3],0);assert.equal(f.terms[3],0);close(f.output,calculate(a,0).output);assert(f.output>=0&&f.output<=20);close(f.scores[0],-a);close(f.scores[2],a);const frames=[];for(let t=0;t<=DURATION;t+=.125)frames.push([t,sample(t,{id:a,future})]);for(const[t,s]of frames.reverse())assert.deepEqual(sample(t,{id:a,future}),s);}
for(const a of [-2,.5,NaN])assert.throws(()=>calculate(a),RangeError);for(const v of [-1,1001,NaN])assert.throws(()=>calculate(1,v),RangeError);
assert.equal(sample(100).time,48);assert.equal(sample(-1).time,0);assert.equal(sample(48).phase,5);
console.log('Attention: independent golden probabilities/output, mask isolation, normalization, convex bounds, index/parameter boundaries and deterministic reverse sampling passed.');
Loading
Loading