diff --git a/apps/course/catalog.json b/apps/course/catalog.json index 64f5e3c..9add826 100644 --- a/apps/course/catalog.json +++ b/apps/course/catalog.json @@ -28,8 +28,8 @@ "order": 3, "title": "一个词如何参考其他词?", "outcome": "从 QK 分数到因果遮罩、注意力权重与 V 的加权结果。", - "status": "planned", - "route": null, + "status": "reviewed", + "route": "/learn/attention/", "published": false }, { diff --git a/apps/site/learn/attention/LESSON.md b/apps/site/learn/attention/LESSON.md new file mode 100644 index 0000000..44c6c36 --- /dev/null +++ b/apps/site/learn/attention/LESSON.md @@ -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. diff --git a/apps/site/learn/attention/SCENE_SPEC.json b/apps/site/learn/attention/SCENE_SPEC.json new file mode 100644 index 0000000..7f7167d --- /dev/null +++ b/apps/site/learn/attention/SCENE_SPEC.json @@ -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"]} diff --git a/apps/site/learn/attention/app.mjs b/apps/site/learn/attention/app.mjs new file mode 100644 index 0000000..0af031a --- /dev/null +++ b/apps/site/learn/attention/app.mjs @@ -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=timeseek(+e.target.value);$('#back').onclick=()=>seek(STARTS.filter(t=>tseek(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); diff --git a/apps/site/learn/attention/index.html b/apps/site/learn/attention/index.html new file mode 100644 index 0000000..b69fe8e --- /dev/null +++ b/apps/site/learn/attention/index.html @@ -0,0 +1,2 @@ +一个词如何参考其他词? · Explain3D
e· Explain3DAI 原理课堂 / 03

ONE QUERY · A WEIGHTED MIXTURE

注意力,怎样汇成一个结果?

从匹配分数,到可见信息的加权和。

手写 Q/K/V · 单头单行 · 未运行语言模型
六个步骤 · 从查询到加权和00:00 / 00:48
Explain3D · AI 原理课堂Q/K 决定权重 · V 提供内容
+

SCALED DOT-PRODUCT ATTENTION

权重和内容,是两件事。

本页计算因果自注意力的一行:当前位置 C(从 0 开始索引 2),可以参考 A/B/C,不能参考未来的 D。Q、K、V 在真实网络中由输入表示经过学习到的线性投影得到;本页直接给定这些参数,不演示投影、训练、多头、位置编码、输出投影、残差或下一词采样。

q = [√2 × α, 0];α ∈ {−1, 0, 1}
K = [ [−1,0], [0,0], [1,0], [2,0] ]
V = [0, 10, 20, 未来值]
s = qKᵀ / √2 = [−α, 0, α, 2α]
s(遮罩后)= [−α, 0, α, −∞]
pⱼ = exp(sⱼ) / Σ exp(sₖ)
o = Σ pⱼVⱼ

V 为一维,只为看清乘法与求和。真实 V 通常是向量,逐分量使用同一组权重。因果遮罩包含当前位置自身,先应用遮罩再做 Softmax。没有 dropout。本页色彩只标识位置 A/B/C/D;卡片宽度不编码数值。概率条宽度表示权重,最终彩色条长度表示各项贡献(总长对应输出,刻度 0…20)。连接线表示依赖关系,不表示网络真实空间或耗时。

手写数字与符号 A/B/C/D 不代表任何真实语言模型或自然语言词义。本页的注意力权重也不是“解释模型推理”的充分证据。查询归零时,可见位置均分权重,输出为 (0+10+20)/3 = 10,未来值仍被排除。

原论文 §3.2 ↗ · PyTorch 官方公式与因果遮罩实现 ↗

PREDICT · TRY · EXPLAIN

查询归零,输出会是多少?

q = [0,0],可见位置 A/B/C 的 V 为 0/10/20;未来 D 的 V 为 1000。因果遮罩仍然启用。

\ No newline at end of file diff --git a/apps/site/learn/attention/model.mjs b/apps/site/learn/attention/model.mjs new file mode 100644 index 0000000..f3c9788 --- /dev/null +++ b/apps/site/learn/attention/model.mjs @@ -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];} diff --git a/apps/site/learn/attention/model.test.mjs b/apps/site/learn/attention/model.test.mjs new file mode 100644 index 0000000..9c11a08 --- /dev/null +++ b/apps/site/learn/attention/model.test.mjs @@ -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.'); diff --git a/apps/site/learn/attention/stage.mjs b/apps/site/learn/attention/stage.mjs new file mode 100644 index 0000000..f386ca2 --- /dev/null +++ b/apps/site/learn/attention/stage.mjs @@ -0,0 +1,30 @@ +import {TOKENS,COLORS} from './model.mjs'; +const NS='http://www.w3.org/2000/svg'; +export function createStage(host,{reducedMotion=()=>false}={}){ + const svg=document.createElementNS(NS,'svg');host.append(svg);svg.style.cssText='width:100%;height:100%;overflow:visible';svg.setAttribute('role','img');let last,key='',anchor=[250,0]; + function render(f){last=f;const mobile=host.clientWidth<600,W=mobile?500:900,H=360,cw=(W-70)/4,p=reducedMotion()?1:Math.min(1,f.progress*1.7),k=[W,f.phase,f.alpha,f.future,f.phase===4?Math.round(p*120):''].join('|');if(k===key)return;key=k;svg.setAttribute('viewBox',`0 0 ${W} ${H}`);svg.replaceChildren(); + const n=(tag,attrs)=>{const el=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))el.setAttribute(k,v);svg.append(el);return el;}; + const text=(x,y,s,size=18,color='#6b6478',align='middle')=>{const el=n('text',{x,y,'font-family':'ui-monospace,monospace','font-size':size,fill:color,'text-anchor':align});el.textContent=s;}; + const rect=(x,y,w,h,fill,stroke='#ded8e3',rx=5)=>n('rect',{x,y,width:w,height:h,rx,fill,stroke,'stroke-width':.8}); + const line=(x,y,a,b,c='#d4cadc',dash='')=>n('path',{d:`M${x} ${y}L${a} ${b}`,stroke:c,fill:'none','stroke-dasharray':dash,'stroke-width':1}); + rect(W/2-140,3,280,33,'#f4f0f7');text(W/2,26,`查询 C:q = [${f.q[0].toFixed(2)}, 0]`,18,'#80669f'); + for(let i=0;i<4;i++){const x=35+i*cw,center=x+cw/2,c=COLORS[i],blocked=i===3&&f.phase>=2; + n('path',{d:`M${x} 58l5 -4h${cw-10}l-5 4Z`,stroke:'#d8d3df',fill:'#fff','stroke-width':.7});rect(x,58,cw-10,87,blocked?'#eeebe9':'#fffdfa',blocked?'#c9c3bd':c); + text(center-5,81,TOKENS[i]+(i===2?' · 当前':i===3?' · 未来':''),16,blocked?'#a2988f':c); + text(center-5,108,`K=[${f.keys[i].join(',')}]`,mobile?15:17,blocked?'#b4aaa2':'#7d7488');text(center-5,134,`V=${f.values[i]}`,18,blocked?'#b4aaa2':c); + if(f.phase>=1){text(center-5,172,blocked?'−∞':f.scores[i].toFixed(1),22,blocked?'#a99c91':c);if(blocked){line(x+8,92,x+cw-18,118,'#c8b9ae');text(center-5,195,'已遮罩',13,'#a8998e');}} + if(f.phase>=3){const bw=cw-24;rect(x+3,204,bw,10,'#eeebef');rect(x+3,204,bw*f.probs[i],10,c,c,2);text(center-5,239,`${(f.probs[i]*100).toFixed(1)}%`,19,blocked?'#ad9f93':c);} + if(f.phase===3)text(center-5,267,`exp=${i===3?'0':Math.exp(f.scores[i]).toFixed(3)}`,14,blocked?'#ad9f93':c); + if(f.phase>=4){const q=f.phase===4?p:1;line(center-5,249,center-5,249+11*q,c,blocked?'2 4':'');text(center-5,281,`${f.terms[i].toFixed(3)}`,17,blocked?'#ad9f93':c);} + } + if(f.phase===0)text(W/2,213,'输入:一条查询 + 四组 K/V',18,'#8d7c9e'); + if(f.phase===1)text(W/2,223,'分数 sⱼ = q·kⱼ / √2',21,'#8d7c9e'); + if(f.phase===2)text(W/2,245,'未来 D:exp(−∞) = 0',21,'#9a8474'); + if(f.phase===3)text(W/2,329,`p = exp(s) / ${f.scores.slice(0,3).reduce((s,v)=>s+Math.exp(v),0).toFixed(3)}`,17,'#8d7c9e'); + if(f.phase>=3){text(W/2,f.phase===3?286:303,f.phase===3?'各条宽度:权重 0 → 100%':'每一项:pⱼ × Vⱼ',15,'#9b89a9');} + if(f.phase>=4){const bx=65,bw=W-130,q=f.phase===4?p:1;let offset=0;for(let i=0;i<4;i++){const w=bw*f.terms[i]/20*q;rect(bx+offset,314,w,17,COLORS[i],COLORS[i],0);offset+=w;}line(bx,336,bx+bw,336);text(bx,354,'0',12,'#aa9bb3');text(bx+bw,354,'20',12,'#aa9bb3');text(W/2,354,`输出 o = ${f.output.toFixed(3)}`,19,'#80669f');} + anchor=f.phase===0?[W/2,3]:f.phase===1?[28,165]:f.phase===2?[W-31,166]:f.phase===3?[30,209]:f.phase===4?[28,276]:[65,322]; + svg.setAttribute('aria-label',`当前位置 C,查询 ${f.alpha},阶段 ${f.phase+1};${f.phase>=3?'权重 '+f.probs.map(x=>x.toFixed(4)).join('、'):''}${f.phase>=4?';输出 '+f.output.toFixed(3):''}`); + } + return {render,projectAnchor(){const m=svg.getScreenCTM(),r=host.getBoundingClientRect();if(!m||!r.width)return null;const pt=svg.createSVGPoint();pt.x=anchor[0];pt.y=anchor[1];const z=pt.matrixTransform(m);return {x:(z.x-r.left)/r.width,y:(z.y-r.top)/r.height,routeLeft:last.phase>0&&last.phase!==2,bendX:(m.a*12+m.e-r.left)/r.width};},resize(){key='';if(last)render(last);},dispose(){svg.remove();}}; +} diff --git a/apps/site/learn/attention/style.css b/apps/site/learn/attention/style.css new file mode 100644 index 0000000..6948a2f --- /dev/null +++ b/apps/site/learn/attention/style.css @@ -0,0 +1,22 @@ +:root{--paper:#f7f5ef;--ink:#343743;--muted:#817b87;--accent:#8066a6;--line:#e8e3da;font-family:Inter,'PingFang SC','Microsoft YaHei',system-ui,sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,a{-webkit-tap-highlight-color:transparent}button{font:inherit;cursor:pointer;border:0}button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid #8e74b5;outline-offset:4px}button:disabled{opacity:.3;cursor:default}a{color:inherit;text-decoration:none}.app{height:100svh;min-height:790px;max-width:1600px;margin:auto;padding:0 40px;display:flex;flex-direction:column}header{height:58px;display:flex;gap:24px;align-items:center;flex-shrink:0}.brand{font-weight:700;font-size:19px;letter-spacing:-.6px}.brand i{font:italic bold 28px Georgia,serif;color:var(--accent);margin-right:4px}header>span{font-size:10px;letter-spacing:1px;color:#928a98}header>.quiet{margin-left:auto}.quiet{background:transparent;color:#82768b;border-radius:7px;font-size:11px;padding:8px 10px;white-space:nowrap}.quiet:hover{color:#554362;background:#eee8ee}.intro{display:flex;align-items:flex-end;justify-content:space-between;max-height:112px;padding:10px 0 23px;overflow:hidden;flex-shrink:0}.eyebrow{font-size:9px;letter-spacing:2px;font-weight:600;color:#8e7c9b;margin:0 0 10px}h1{margin:0;font-size:clamp(27px,2.6vw,38px);font-weight:650;letter-spacing:-1.2px;line-height:1.35}h1 span{color:#7c6c8e}.intro-copy{font-size:12px;color:#817788;margin:0 0 4px}.controls{height:60px;max-height:60px;display:flex;align-items:center;gap:17px;border:1px solid #e6e0d7;background:#efede6;padding:0 18px;border-radius:13px;margin-bottom:17px;flex-shrink:0;overflow:hidden}.temperature{display:flex;align-items:center;gap:20px}.temperature label{font-size:11px;white-space:nowrap;color:#756c7c}.temperature output{font-family:ui-monospace,monospace;color:var(--accent);margin-left:12px;font-size:12px}input[type=range]{appearance:none;background:transparent;height:24px;margin:0;cursor:pointer}input[type=range]::-webkit-slider-runnable-track{height:3px;border-radius:4px;background:linear-gradient(to right,#9b85b9 var(--fill,0%),#d9d1df var(--fill,0%))}input[type=range]::-webkit-slider-thumb{appearance:none;width:12px;height:12px;border-radius:50%;background:#8b73ac;margin-top:-4.5px;box-shadow:0 0 0 3px #f7f5ef}input[type=range]::-moz-range-track{height:3px;background:#d9d1df}input[type=range]::-moz-range-progress{height:3px;background:#9b85b9}input[type=range]::-moz-range-thumb{height:12px;width:12px;border-radius:50%;border:0;background:#8b73ac}#temperature{width:155px}.presets{display:flex;gap:3px}.presets button{font-size:10px;padding:8px 11px;background:none;color:#7f7187;border-radius:6px;white-space:nowrap}.presets button[aria-pressed=true],.presets button:hover{background:#fffcf8;color:#6f5387}.divider{height:22px;width:1px;background:#ded7df;margin-left:auto}#new-draw span{margin-right:4px;font-size:15px}.experiment{font-size:11px;padding:10px 14px;background:#8b749e;color:white;border-radius:7px;white-space:nowrap}.experiment:hover{background:#78618e}.exhibit{position:relative;flex:1;min-height:420px;background:#fffdfa;border:1px solid #e9e4db;border-radius:18px;overflow:hidden;isolation:isolate}.exhibit-meta{position:absolute;top:21px;left:25px;right:25px;display:flex;justify-content:space-between;color:#84798c;font-size:10px;letter-spacing:.6px;z-index:2}#live-temperature{font-family:ui-monospace,monospace;color:#8a73a2}.context{position:absolute;top:65px;left:27px;z-index:2;max-height:80px;overflow:hidden}.context>span{font-size:9px;color:#94899c}.context p{font:14px Georgia,serif;color:#7c7186;margin:9px 0}.context b{color:#a28dbb}.callout{position:absolute;top:56px;width:380px;left:50%;transform:translateX(-50%);padding:15px 19px 11px;border:1px solid #e7dfee;background:#fffffff7;border-radius:13px;box-shadow:0 8px 26px #88779905;z-index:4}.callout>span{font-size:9px;letter-spacing:.8px;color:#9a80b4}.callout h2{font-size:18px;font-weight:650;line-height:1.5;letter-spacing:-.25px;margin:7px 0}.callout p{font-size:12px;line-height:1.8;color:#766a7e;margin:0}.calculation{font-size:10px;color:#9579ac;background:transparent;padding:8px 0 0}.calculation:hover{color:#674280}.leader{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;color:#af97c5}.leader path{fill:none;stroke:currentColor;stroke-width:1.1}.leader circle{fill:#fffdfa;stroke:currentColor;stroke-width:1.1}.artwork{position:absolute;left:50%;bottom:39px;transform:translateX(-50%);width:min(96%,1160px);height:calc(100% - 208px);display:flex;align-items:center;justify-content:center;transition:transform .4s}.exhibit.is-detailed .artwork{transform:translateX(-50%) scale(1.3);transform-origin:center 70%}.stage-bottom{position:absolute;bottom:13px;left:25px;right:17px;display:flex;align-items:center;justify-content:space-between;z-index:5;color:#8a7f92;font-size:9px}.stage-bottom .quiet{font-size:10px}.transport{height:70px;display:flex;gap:18px;align-items:center;flex-shrink:0}.play{height:42px;width:134px;display:flex;align-items:center;justify-content:center;gap:11px;border-radius:9px;background:#3c3543;color:#fff;font-size:11px;flex-shrink:0}.play:hover{background:#62516f}#play-icon{font-size:10px}.track{flex:1;min-width:0}.track input{display:block;width:100%}.track>div{display:flex;justify-content:space-between;color:#8c7d98;font-size:9px;font-variant-numeric:tabular-nums}footer{height:30px;max-height:30px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;overflow:hidden;font-size:9px;color:#a497ac}.secondary{transition:opacity .35s,max-height .6s cubic-bezier(.22,1,.36,1),padding .6s,margin .6s}.is-playing .secondary{max-height:0;opacity:0;padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;pointer-events:none}.is-playing .controls{border-width:0}.is-playing .exhibit{border-color:transparent}.is-playing .calculation,.is-playing #detail{display:none}.is-playing .artwork{height:calc(100% - 185px);bottom:35px}.is-playing .stage-bottom{bottom:19px}.is-playing .callout{padding-bottom:15px} +dialog{border:1px solid #e5dce9;background:#fffdf8;border-radius:18px;max-width:720px;width:94%;max-height:88svh;padding:34px;color:#4d4058;box-shadow:0 25px 100px #3e2c4829}dialog::backdrop{background:#34263e55;backdrop-filter:blur(4px)}dialog h2{font-size:25px;line-height:1.4;margin:0 0 16px;letter-spacing:-.6px}dialog p{font-size:13px;line-height:1.9;color:#81718d}dialog a{color:#8b6aa4;text-decoration:underline;text-underline-offset:3px}.close{position:absolute;right:16px;top:10px;background:none;color:#9883a6;font-size:26px}.fine{font-size:11px!important;color:#9786a2!important}.formula{font:14px/1.8 ui-monospace,monospace;background:#f0ebf3;border:1px solid #e4dce9;border-radius:8px;padding:12px 16px;margin:16px 0;color:#7b5e98;overflow-x:auto}.table-scroll{overflow-x:auto}table{width:100%;border-collapse:collapse;text-align:right;font-variant-numeric:tabular-nums;font-size:12px}th{font-size:10px;color:#95819f;font-weight:500;white-space:nowrap;padding:9px 10px;border-bottom:1px solid #e7dfeb}td{padding:14px 10px;border-bottom:1px solid #eee8f1;color:#6b567a}td:first-child,th:first-child{text-align:left}td:first-child i{width:7px;height:7px;display:inline-block;border-radius:50%;margin-right:7px}.calculation-total{font-size:12px;color:#79628a;text-align:right} +@media(max-width:1100px){.app{padding:0 24px}.intro-copy{display:none}.controls{gap:10px}.temperature{gap:12px}#temperature{width:120px}.presets button{padding:8px}.context{display:none}.divider{margin-left:auto}} +@media(max-width:700px){.app{padding:0 12px;min-height:790px}header{height:48px;gap:14px}.brand{font-size:17px}.brand i{font-size:25px}header>span{font-size:8px;letter-spacing:0}header>.quiet{font-size:9px;padding-right:0}.intro{padding:9px 0 18px;max-height:105px}h1{font-size:24px;line-height:1.45;letter-spacing:-.7px}.eyebrow{font-size:8px;margin-bottom:7px}.controls{height:98px;max-height:98px;display:grid;grid-template-columns:1fr auto;gap:8px 4px;padding:10px 12px;margin-bottom:11px}.temperature{grid-column:1/-1;grid-row:1;gap:17px}.temperature label{font-size:11px}#temperature{flex:1;width:auto}.presets{grid-row:2;gap:0}.presets button{font-size:9px;padding:7px 9px}.divider{display:none}#new-draw{display:none}.experiment{font-size:9px;padding:9px 10px}.exhibit{min-height:408px;border-radius:13px}.exhibit-meta{top:17px;left:17px;right:17px;font-size:9px}.callout{top:47px;width:calc(100% - 34px);max-width:380px;padding:13px 16px 10px}.callout h2{font-size:16px;line-height:1.5}.callout p{font-size:11px;line-height:1.7}.callout>span{font-size:8px}.calculation{font-size:9px}.artwork{width:100%;height:calc(100% - 192px);bottom:37px}.stage-bottom{left:17px;right:7px;bottom:10px;font-size:8px}.stage-bottom .quiet{font-size:9px}.transport{height:62px;gap:6px}.play{width:110px;height:39px;font-size:10px}.transport>.quiet{padding:6px}.track>div span:first-child{display:none}.track>div{justify-content:flex-end}.track input{height:23px}footer{height:27px;max-height:27px;font-size:8px}footer>span:last-child{max-width:150px;text-align:right}.is-playing{min-height:600px}.is-playing .artwork{height:calc(100% - 190px);bottom:40px}.is-playing .stage-bottom{bottom:16px}dialog{padding:30px 19px}dialog h2{font-size:22px}td,th{padding:10px 6px}td{font-size:11px}.formula{font-size:12px}} +@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}[hidden]{display:none!important} + +/* Reserve the callout before fitting the vector diagram. */ +.artwork,.is-playing .artwork{top:220px;bottom:35px;height:auto}.stage-actions{display:flex;align-items:center;gap:5px} +@media(max-width:700px){.artwork,.is-playing .artwork{top:205px;bottom:36px;height:auto}} + +.challenge-answers{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:20px 0}.challenge-answers button{background:#f1ebf5;color:#725389;border:1px solid #dfd3e8;border-radius:8px;padding:13px 8px;font:20px Georgia,serif}.challenge-answers button[aria-pressed=true]{background:#8b749e;color:white;border-color:#8b749e}#challenge-feedback:empty{display:none}#challenge-feedback{padding:15px;background:#f2f1ed;border-radius:8px;color:#665873;line-height:1.9}.stage-actions #challenge{color:#80609e} + +.is-playing #challenge{display:none} + +#rules{width:110px}.controls label{font-size:11px;white-space:nowrap;color:#83708e}.controls output{color:#8066a6;margin-left:8px}.is-playing #show-rules{display:none}.challenge-answers{grid-template-columns:repeat(3,1fr)}#feedback:empty{display:none}#feedback{background:#f0ebf3;border-radius:8px;padding:15px}.artwork svg{display:block} @media(max-width:700px){.controls{grid-template-columns:auto 1fr auto;height:102px;max-height:102px}.presets{grid-column:1/-1;grid-row:1}.controls label{grid-row:2;font-size:10px}#rules{grid-row:2;width:100%}#experiment{grid-row:2;font-size:9px}.presets button{padding:7px 12px}.stage-bottom{font-size:8px}} + +/* Embedding is an indexed table and an exact front-plane vector plot. */ +.controls{gap:14px}.controls .divider{margin-left:auto}.controls .hint{font-size:11px;color:#8a7d91}.presets button{padding:9px 14px}.challenge-answers{grid-template-columns:1fr}.challenge-answers button{font:13px ui-monospace,monospace;text-align:left;padding:14px}.artwork svg text{paint-order:stroke;stroke:#fffdfa;stroke-width:.4px;stroke-linejoin:round}@media(max-width:700px){.controls{display:flex;flex-wrap:wrap;height:83px;max-height:83px;gap:3px 9px;padding:8px 12px}.presets{width:100%;justify-content:space-between}.presets button{font-size:10px;padding:8px 12px}.controls .hint{font-size:9px;flex:1}.controls #experiment{font-size:9px;padding:7px 9px}.callout{min-height:132px}.exhibit{min-height:430px}.stage-bottom>span{max-width:220px;line-height:1.5}.artwork svg text{stroke-width:0}} + +.reduce-motion *{animation:none!important;transition:none!important}#reduce-motion{accent-color:#8b749e;margin-right:8px} + +#future{width:100px}.controls label{display:flex;align-items:center;gap:8px}.controls output{min-width:25px}.controls .presets button{padding:8px 10px}@media(max-width:700px){.controls{height:100px;max-height:100px}.controls label{font-size:10px;flex:1}#future{width:75px}.presets{justify-content:space-around}.controls .hint{display:none}.exhibit{min-height:435px}} diff --git a/docs/ai-learning/STATUS.md b/docs/ai-learning/STATUS.md index 5d6a12b..2532116 100644 --- a/docs/ai-learning/STATUS.md +++ b/docs/ai-learning/STATUS.md @@ -36,3 +36,7 @@ Route /learn/tokenization/. A 46-second fixed-rule character-level BPE teaching ## Published lesson 02: Embedding (3 / 10 total) Route `/learn/embedding/`. One question: how an integer ID retrieves one row from a fixed parameter table. A four-row, two-dimensional authored example connects indexing, unchanged row copy, equal-scale coordinates and a controlled cat/car/dog comparison. Same ID is an address, not a semantic magnitude. Pausing restores configuration; one routed annotation avoids the table and negative-coordinate labels. Independent numerical/seek tests and desktop/narrow-width browser checks passed; reduced-motion manual toggle and controlled hidden-event path were exercised. This is teaching review, not learner-effect research. Published at https://explain.funagent.app/learn/embedding/ . PR #3 head 9b28b2ff7bf87d7025bd83dbea9f02e35e2af910 passed CI run 40464108178; READY preview dpl_2GDcHZ7FDhN5h2PUJUkAb5XCjTVu served the exact reviewed HTML/app/stage source. Merge 2420d77c370a0e888e64c0c13e87f923b5cefb9c reached production READY as dpl_6L6uGjakR9Mz1i7masoE4f19FEbb; main CI run 40464227142 passed. Production browser verified course 3/10, entry navigation, dog vector [1.8,1.2], distance .283/cosine .992, correct quiz feedback and return to the same diagram. Next: Attention. + +## Reviewed Attention candidate — 2026-10-28 + +Single causal attention row at C: authored Q/K/V, scaled dot products, future mask, exponential normalization, weighted V and sum. Query direction changes output (4.248 / 10 / 15.752); masked future V leaves output invariant. Tests and actual desktop/narrow-browser teaching review passed as detailed in LESSON.md. Candidate only until branch CI/preview and matching production deployment are verified. No new Skill or host-compatibility claim; uses the shared playback, annotation and evidence method. Next after acceptance: Transformer layer. diff --git a/docs/ai-learning/roadmap.json b/docs/ai-learning/roadmap.json index 01dded6..0fdc335 100644 --- a/docs/ai-learning/roadmap.json +++ b/docs/ai-learning/roadmap.json @@ -46,8 +46,8 @@ "order": 3, "title": "一个词如何参考其他词?", "outcome": "从 QK 分数到因果遮罩、注意力权重与 V 的加权结果。", - "status": "planned", - "route": null, + "status": "reviewed", + "route": "/learn/attention/", "published": false }, { diff --git a/evals/browser/attention-lifecycle.html b/evals/browser/attention-lifecycle.html new file mode 100644 index 0000000..382a3a6 --- /dev/null +++ b/evals/browser/attention-lifecycle.html @@ -0,0 +1,5 @@ +Attention lifecycle QA

开发测试:向实际课堂文档分发受控 visibilitychange,验证其监听流程。此测试不等于真实操作系统切换标签页。

diff --git a/package.json b/package.json index bf2b3d2..3e1cbe1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "node scripts/build.mjs", - "test": "node evals/verify.mjs && node apps/site/learn/token-choice/model.test.mjs && node evals/verify-token-choice.mjs && node evals/verify-course.mjs && node apps/site/learn/tokenization/model.test.mjs && node apps/site/learn/embedding/model.test.mjs", + "test": "node evals/verify.mjs && node apps/site/learn/token-choice/model.test.mjs && node evals/verify-token-choice.mjs && node evals/verify-course.mjs && node apps/site/learn/tokenization/model.test.mjs && node apps/site/learn/embedding/model.test.mjs && node apps/site/learn/attention/model.test.mjs", "check": "npm test && npm run build" }, "engines": { diff --git a/scripts/build.mjs b/scripts/build.mjs index 9df719f..ddfd0da 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -22,7 +22,7 @@ await write('start',page('让 Agent,多一个本领。','安装 Explain3D Skil await write('docs',page('怎么做,也怎么验证。','Explain3D 的结构、工作流、模板边界、自动发布与验证记录。',`

能力如何组成

Skill 负责把一个问题拆成对象、关系、变量、约束与叙事。运行时负责绝对时间采样、播放状态、视角、参数和配置退场。场景适配器负责各自的几何与计算。

产品采用可静态部署的网站,核心演示使用 Three.js;无 WebGL 时使用同一模型的 CPU 投影。官网不存储 API Key,也不直接运行用户提示词。

创建一个新场景

  1. 限定一个闭环问题与适合的受众。
  2. 写场景规格,注明来源、约束与简化。
  3. 选择合适的模板,重建新对象与计算关系。
  4. 共用时间、播放和交互状态,制作十秒讲解。
  5. 分别验证数学、交互、画面、键盘、手机与性能。
  6. 交付源码、提示词、运行方法与验证记录。

当前验证到哪里

已执行模型约束、时间线与静态构建检查,并在当前浏览器检查交互和响应式。当前云端验收浏览器禁用 WebGL,画面使用 CPU 兼容渲染。真实手机 GPU、方向传感器与持续帧率尚未实机验收。

标准 Skill 包已提供。Claude Code、WorkBuddy 和 DeepSeek 宿主的独立复现尚未完成;网站不会把文档适配标为验证通过。

自动发布

GitHub 保存网站、Skill、模板与提示词。Vercel Git 集成负责分支预览和正式分支发布;生产构建先执行 npm run check,模型检查失败时构建中止。每个版本可按 Git 提交追溯,正式站点绑定 explain.funagent.app。

Skill 的本地检查完成后再同步到发布目录。依赖、模型假设和第三方许可证保留在源码中。社交文案在素材页准备,发布到社交账号单独执行。

深度效果的边界

透视、遮挡、光照和运动视差能在普通屏幕中形成明显深度感;它们不让普通屏幕自动变成双眼立体硬件。倾斜控制依赖方向传感器,完整行星齿轮版可使用;其他模板支持拖动观察。

参与

提交新模板时,附带可运行场景、参数、公式或事件模型、简化说明和测试。请通过 GitHub Issues 提出问题或 Pull Request 分享作品。

仓库与贡献指南 ↗

`,'/docs/')); await write('press',page('把可以操作的理解,分享出去。','Explain3D 的真实演示、发布文案、品牌说明和素材。',`
Explain3D 实际交互作品预览

产品一句话

让 AI 把一个原理,做成可以亲手探索的 3D 作品。

可使用的宣传材料

小红书、短视频、公众号、X、社区与 GitHub 发布文案 ↗

下载分享封面 / 下载行星齿轮场景图

演示素材的准确表述

这是可操作的实时三维原理讲解。可以说“普通屏幕里的深度感”“可拖动、可拆解”“AI 辅助生成源码”。不能据此宣称普通手机拥有真正的双眼裸眼立体显示,也不能把未验证平台说成一键必成。

`,'/press/')); await buildCourse(root,out); -const urls=['/','/learn/','/learn/token-choice/','/learn/tokenization/','/learn/embedding/','/templates/','/create/','/start/','/docs/','/press/',...templates.map(s=>`/templates/${s.id}/`)]; +const urls=['/','/learn/','/learn/token-choice/','/learn/tokenization/','/learn/embedding/','/learn/attention/','/templates/','/create/','/start/','/docs/','/press/',...templates.map(s=>`/templates/${s.id}/`)]; await fs.writeFile(path.join(out,'sitemap.xml'),`${urls.map(u=>`https://explain.funagent.app${u}`).join('')}`); await fs.writeFile(path.join(out,'robots.txt'),'User-agent: *\nAllow: /\nSitemap: https://explain.funagent.app/sitemap.xml\n'); await fs.writeFile(path.join(out,'favicon.svg'),'e');