diff --git a/apps/site/learn/token-choice/LESSON.md b/apps/site/learn/token-choice/LESSON.md index b03fcc3..f3f7b96 100644 --- a/apps/site/learn/token-choice/LESSON.md +++ b/apps/site/learn/token-choice/LESSON.md @@ -67,3 +67,12 @@ node apps/site/learn/token-choice/model.test.mjs 通过开发者教学适配评审,尚未进行真实学习者效果研究。加入完成后的“检验理解”:先预测固定分数和u不变、T从.8到2时的候选,再得到累计概率区间的因果反馈,最后回到同一组参数的图形验证。错误答案不是只显示叉号;说明mat上界.4551、floor上界.7311,以及u=.6219为何属于floor。 本轮实查:课程入口可进入该场景;桌面端错误/正确选择与回到图形后T=2、floor27.6%一致;390×844手机布局下学习题完整可读。`npm run check`覆盖既有机械模板约束、新场景的独立模型测试与课程清单检查。真实设备与屏幕阅读器仍未实查。 + +## Reopened visual review: draft repair — 2026-10-28 + +The 32s probability-label defect has a source-level repair using shared geometry and ordered identity lanes. Numerical ownership/non-overlap/CDF/continuous-focus/reverse-seek checks cover3,620 poses. Full site checks pass. See `docs/ai-learning/reviews/2026-10-28-token-choice-rework.md` for precise changes and remaining gates. Browser connection was unavailable; no actual screenshots,390px legibility,lifecycle or4× craft acceptance were performed for this candidate. It is not published or visually accepted. Earlier published evidence remains historical and does not validate this new code. + + +## Follow-up visual and teaching review — 2026-10-28 + +Browser access was restored. Implemented exact operation rail, staged scale visibility, larger narrow type, continuous annotations and carrier contact repair. Actual desktop/390px and native4× review, forward/reverse intermediate frames, natural completion/replay/Escape, controlled hidden event and manual reduced-motion setting were exercised. Prediction check retained fixedu and returnedT=2/floor27.6%. See `docs/ai-learning/reviews/2026-10-28-token-choice-rework.md` for observed results/screenshots and untested limits. The initial draft's no-browser statement is historical. This is developer review, not measured learning gains or external aesthetic endorsement. diff --git a/apps/site/learn/token-choice/SCENE_SPEC.json b/apps/site/learn/token-choice/SCENE_SPEC.json index fb5cf3b..6e45b1b 100644 --- a/apps/site/learn/token-choice/SCENE_SPEC.json +++ b/apps/site/learn/token-choice/SCENE_SPEC.json @@ -4,12 +4,36 @@ "audience": "希望逐步理解 AI 原理、没有概率论背景的读者", "context": "the cat sat on the …", "objects": [ - {"id":"scores","meaning":"四个教学候选的原始 logits,保留候选身份与颜色","units":"无量纲分数"}, - {"id":"temperature","meaning":"把每个原始分数除以同一温度 T","units":"无量纲"}, - {"id":"weights","meaning":"exp(z/T) 给出的正权重,尚未归一化","units":"无量纲权重"}, - {"id":"probability","meaning":"每个权重除以全部权重之和,概率带宽度正比于概率","units":"概率,合计 1 或 100%"}, - {"id":"sampling","meaning":"0 到 1 的均匀随机数 u 在累积概率带上的位置","units":"无量纲,0 ≤ u < 1"}, - {"id":"result","meaning":"u 所在区间对应的候选词卡","units":"候选索引"} + { + "id": "scores", + "meaning": "四个教学候选的原始 logits,保留候选身份与颜色", + "units": "无量纲分数" + }, + { + "id": "temperature", + "meaning": "把每个原始分数除以同一温度 T", + "units": "无量纲" + }, + { + "id": "weights", + "meaning": "exp(z/T) 给出的正权重,尚未归一化", + "units": "无量纲权重" + }, + { + "id": "probability", + "meaning": "每个权重除以全部权重之和,概率带宽度正比于概率", + "units": "概率,合计 1 或 100%" + }, + { + "id": "sampling", + "meaning": "0 到 1 的均匀随机数 u 在累积概率带上的位置", + "units": "无量纲,0 ≤ u < 1" + }, + { + "id": "result", + "meaning": "u 所在区间对应的候选词卡", + "units": "候选索引" + } ], "relationships": [ "tokens = [mat, floor, bed, sofa];logits = [3, 2, 1, 0]", @@ -21,26 +45,82 @@ "固定输入下调节温度不改变原始 logits;固定 seed 和 draw 不改变随机数 u" ], "parameters": [ - {"id":"temperature","min":0.4,"max":2,"default":0.8,"units":"无量纲"}, - {"id":"seed","default":22,"units":"整数伪随机种子"}, - {"id":"draw","default":0,"min":0,"units":"非负整数抽样序号"} + { + "id": "temperature", + "min": 0.4, + "max": 2, + "default": 0.8, + "units": "无量纲" + }, + { + "id": "seed", + "default": 22, + "units": "整数伪随机种子" + }, + { + "id": "draw", + "default": 0, + "min": 0, + "units": "非负整数抽样序号" + } + ], + "outputs": [ + "scaled", + "weights", + "totalWeight", + "p", + "cdf", + "entropy", + "u", + "selected" ], - "outputs": ["scaled","weights","totalWeight","p","cdf","entropy","u","selected"], "durationSeconds": 50, "chapters": [ - {"id":"scores","start":0,"end":8,"message":"分数不是概率"}, - {"id":"temperature","start":8,"end":18,"message":"除以温度,缩放分数差异"}, - {"id":"weights","start":18,"end":27,"message":"取指数,把分差变成权重之比"}, - {"id":"probability","start":27,"end":36,"message":"归一化得到总和为 1 的概率"}, - {"id":"sampling","start":36,"end":45,"message":"随机数在累积概率带上定位"}, - {"id":"result","start":45,"end":50,"message":"显示这一次抽到的候选"} + { + "id": "scores", + "start": 0, + "end": 8, + "message": "分数不是概率" + }, + { + "id": "temperature", + "start": 8, + "end": 18, + "message": "除以温度,缩放分数差异" + }, + { + "id": "weights", + "start": 18, + "end": 27, + "message": "取指数,把分差变成权重之比" + }, + { + "id": "probability", + "start": 27, + "end": 36, + "message": "归一化得到总和为 1 的概率" + }, + { + "id": "sampling", + "start": 36, + "end": 45, + "message": "随机数在累积概率带上定位" + }, + { + "id": "result", + "start": 45, + "end": 50, + "message": "显示这一次抽到的候选" + } ], "timeline": { - "sampler":"sample(time, {temperature, seed, draw})", - "selectionVisibleAtSeconds":43.2, - "numericState":"所有数值从同一组输入计算,几何变形插值不改变它们的数学定义", - "playback":"实际 elapsed time,支持暂停、继续、反向拖动、重播与结束恢复;隐藏标签页暂停", - "narration":"每次仅一条气泡跟随当前数学对象;颜色在所有阶段保持候选身份一致" + "sampler": "sample(time, {temperature, seed, draw})", + "selectionVisibleAtSeconds": 43.2, + "numericState": "所有数值从同一组输入计算,几何变形插值不改变它们的数学定义", + "playback": "实际 elapsed time,支持暂停、继续、反向拖动、重播与结束恢复;隐藏标签页暂停", + "narration": "每次仅一条气泡跟随当前数学对象;颜色在所有阶段保持候选身份一致", + "probabilityMorph": "chartBandGeometry drives front faces, identity-lane values, connectors and continuous focus. During representation change, widths/heights are transitional; final band width encodes probability.", + "operationRail": "Exact operand/operator/result pointers, one candidate at a time; travel is pedagogical, not compute latency. Chapter units switch discretely." }, "invariants": [ "四个概率均为正且总和约为 1", @@ -49,7 +129,12 @@ "在这组不相等的 logits 上,提高 T 提高熵并降低最大概率", "seed、draw、temperature 和 time 相同时,状态完全可重现", "43.2 秒前 selected 为 null;之后与 inverse CDF 结果一致", - "对返回数组的修改不会污染后续 sample 或原始教学数据" + "对返回数组的修改不会污染后续 sample 或原始教学数据", + "Probability labels stay below all moving front faces and retain one ordered candidate identity lane", + "Probability connectors originate at their owning current front face; focus remains within that face", + "Final front-face band widths and cumulative positions equal p and cdf without extrusion", + "Carrier follows the shared bottom plane and never crosses the current fronts", + "Operation rail operands and outputs obey the selected candidate equation exactly" ], "simplifications": [ "四个词构成完整的教学词表;不是从真实模型词表截取的前四项,也不是线上模型输出", @@ -64,9 +149,18 @@ "高温不会保证每个低排名候选的概率都增大,也不意味着答案更正确" ], "sources": [ - {"url":"https://docs.pytorch.org/docs/2.14/generated/torch.nn.Softmax.html","supports":"指数归一化公式与输出概率和为 1"}, - {"url":"https://huggingface.co/docs/transformers/en/internal/generation_utils#transformers.TemperatureLogitsWarper","supports":"正温度对采样分布的调节;温度与随机采样的关系"}, - {"url":"https://github.com/wheresryan22/anatomy","supports":"已获用户认可的细线、等轴层次与克制配色工艺参考;本场景不复用原轮盘几何"} + { + "url": "https://docs.pytorch.org/docs/2.14/generated/torch.nn.Softmax.html", + "supports": "指数归一化公式与输出概率和为 1" + }, + { + "url": "https://huggingface.co/docs/transformers/en/internal/generation_utils#transformers.TemperatureLogitsWarper", + "supports": "正温度对采样分布的调节;温度与随机采样的关系" + }, + { + "url": "https://github.com/wheresryan22/anatomy", + "supports": "已获用户认可的细线、等轴层次与克制配色工艺参考;本场景不复用原轮盘几何" + } ], "acceptance": [ "独立 Python 黄金值与 JavaScript 数学结果一致", @@ -75,5 +169,8 @@ "播放、暂停、继续、拖动、参数改变、再次抽样、重播及完成后的配置恢复均可观察验证", "保留单一焦点;未讲到的计算与参数不会同时占据主视觉" ], - "untested": ["真实手机硬件性能与所有浏览器组合", "独立的学习效果实验"] + "untested": [ + "真实手机硬件性能与所有浏览器组合", + "独立的学习效果实验" + ] } diff --git a/apps/site/learn/token-choice/app.mjs b/apps/site/learn/token-choice/app.mjs index cbf0907..dacfd32 100644 --- a/apps/site/learn/token-choice/app.mjs +++ b/apps/site/learn/token-choice/app.mjs @@ -9,7 +9,7 @@ const percent=x=>x>0&&x<.001?'<0.1':(x*100).toFixed(1); const number=x=>x.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); function focus(on){app.classList.toggle('is-playing',on);$$('.secondary').forEach(el=>{el.inert=on;el.setAttribute('aria-hidden',String(on));});} function anchor(id){const outerRect=box.getBoundingClientRect(),bubbleRect=bubble.getBoundingClientRect();const top=Math.max(185,bubbleRect.bottom-outerRect.top+13);art.style.top=top+'px';const point=stage.projectAnchor(id);if(!point||!point.visible)return;const outer=box.getBoundingClientRect(),inner=art.getBoundingClientRect(),x=inner.left-outer.left+point.x*inner.width,y=inner.top-outer.top+point.y*inner.height;const half=bubble.offsetWidth/2,cx=Math.max(half+17,Math.min(outer.width-half-17,x));bubble.style.left=cx+'px';const sy=bubble.getBoundingClientRect().bottom-outer.top;$('#leader').setAttribute('viewBox',`0 0 ${outer.width} ${outer.height}`);$('#leader-line').setAttribute('d',`M${cx} ${sy} C${cx} ${sy+20},${x} ${Math.max(sy+20,y-24)},${x} ${y-8}`);$('#leader-dot').setAttribute('cx',x);$('#leader-dot').setAttribute('cy',y);} -function render(){if(disposed)return;const frame=sample(time,settings());frame.reducedMotion=motion.matches;stage.render(frame);const copy=narration(frame),key=JSON.stringify(copy);if(key!==lastCopy){lastCopy=key;$('#copy-eyebrow').textContent=copy.eyebrow;$('#copy-title').textContent=copy.title;$('#copy-body').textContent=copy.body;} +function render(){if(disposed)return;const frame=sample(time,settings());frame.reducedMotion=motion.matches||$('#reduce-motion').checked;app.classList.toggle('reduce-motion',frame.reducedMotion);stage.render(frame);const copy=narration(frame),key=JSON.stringify(copy);if(key!==lastCopy){lastCopy=key;$('#copy-eyebrow').textContent=copy.eyebrow;$('#copy-title').textContent=copy.title;$('#copy-body').textContent=copy.body;} $('#phase-label').textContent=`0${frame.phase+1} / 06 · ${phaseNames[frame.phase]}`;$('#live-temperature').textContent=`T = ${temperature.toFixed(2)}`; $('#temperature').value=temperature;$('#temperature-value').textContent=temperature.toFixed(2);$('#temperature').style.setProperty('--fill',((temperature-.4)/1.6*100)+'%'); $$('[data-temperature]').forEach(button=>button.setAttribute('aria-pressed',String(Math.abs(+button.dataset.temperature-temperature)<.001))); @@ -40,5 +40,5 @@ $$('[data-answer]').forEach(button=>button.onclick=()=>{const frame=sample(DURAT $('#challenge-try').onclick=()=>{$('#challenge-dialog').close();temperature=2;draw=0;time=DURATION;render();};$('#close-challenge').onclick=()=>$('#challenge-dialog').close(); $('#about').onclick=()=>{stop();$('#info').showModal();};$('#close-about').onclick=()=>$('#info').close();$('#close-calculation').onclick=()=>$('#calculation-dialog').close(); document.addEventListener('visibilitychange',()=>{if(document.hidden&&playing)stop();});document.addEventListener('keydown',e=>{if($('#info').open||$('#calculation-dialog').open||$('#challenge-dialog').open)return;if(e.key==='Escape'){stop();return;}if(e.code==='Space'&&!['INPUT','BUTTON','A'].includes(e.target.tagName)){e.preventDefault();play();}}); -motion.addEventListener('change',render);const resize=new ResizeObserver(()=>{stage.resize();anchor(sample(time,settings()).focus);});resize.observe(art);box.addEventListener('transitionend',()=>anchor(sample(time,settings()).focus)); +$('#reduce-motion').onchange=render;motion.addEventListener('change',render);const resize=new ResizeObserver(()=>{stage.resize();anchor(sample(time,settings()).focus);});resize.observe(art);box.addEventListener('transitionend',()=>anchor(sample(time,settings()).focus)); window.addEventListener('pagehide',event=>{if(event.persisted){stop();return;}disposed=true;playing=false;cancelAnimationFrame(raf);stage.dispose();resize.disconnect();motion.removeEventListener('change',render);});render(); diff --git a/apps/site/learn/token-choice/geometry.mjs b/apps/site/learn/token-choice/geometry.mjs new file mode 100644 index 0000000..ae27c4b --- /dev/null +++ b/apps/site/learn/token-choice/geometry.mjs @@ -0,0 +1,27 @@ +// Front-face coordinates only. Extrusion is decorative and never changes a value. +const clamp = x => Math.max(0, Math.min(1, x)); +const ease = x => { x = clamp(x); return x*x*(3-2*x); }; +const mix = (a,b,t) => a+(b-a)*t; +export function chartBandGeometry(frame, mobile=false) { + const width=mobile?390:1000, height=mobile?430:420; + const base=mobile?280:285, plotH=mobile?170:185, barW=mobile?32:110; + const xs=mobile?[75,153,231,309]:[130,345,560,775]; + const bandX=mobile?25:110, bandW=mobile?340:780, bandY=170, bandH=mobile?60:72; + let morph=frame.phase>3?1:frame.phase===3?ease((frame.progress-.27)/.46):0; + if(frame.reducedMotion&&frame.phase===3)morph=frame.progress>=.5?1:0; + const candidates=frame.p.map((p,i)=>{ + const h=p*plotH, x=mix(xs[i],bandX+frame.cdf[i]*bandW,morph); + const y=mix(base-h,bandY,morph), w=mix(barW,p*bandW,morph), height=mix(h,bandH,morph); + // Wide, ordered identity lanes remain readable even for a 0.05% interval. + // The connector always starts on THIS candidate's current front face. + const labelX=mix(xs[i]+barW/2,bandX+bandW*(i+.5)/4,morph); + const labelY=mix(base+48,bandY+bandH+69,morph); + return {x,y,w,h:height,labelX,labelY,valueX:labelX,valueY:labelY+22, + leader:{x:x+w/2,y:y+height+3,endX:labelX,endY:labelY-21}, + focus:{x:x+w/2,y:y+height*.3}}; + }); + const focusT=frame.phase>3?1:frame.reducedMotion?(frame.progress>=.27?1:0):ease((frame.progress-.20)/.15); + const annotation={x:mix(width/2,candidates[0].focus.x,focusT),y:mix(33,candidates[0].focus.y,focusT)}; + const dock={x:candidates[0].x-6,y:candidates[0].y+candidates[0].h+1,end:candidates[3].x+candidates[3].w+6}; + return {width,height,base,plotH,barW,xs,bandX,bandW,bandY,bandH,morph,candidates,annotation,dock}; +} diff --git a/apps/site/learn/token-choice/geometry.test.mjs b/apps/site/learn/token-choice/geometry.test.mjs new file mode 100644 index 0000000..e66daee --- /dev/null +++ b/apps/site/learn/token-choice/geometry.test.mjs @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import {sample,operation} from './model.mjs'; +import {chartBandGeometry} from './geometry.mjs'; +const close=(a,b,t=1e-8)=>assert.ok(Math.abs(a-b){ + assert.ok(c.w>0&&c.h>0); + close(g.dock.y,c.y+c.h+1);assert.ok(g.dock.xc.x+c.w,'carrier spans all four fronts without intersecting them'); + assert.ok(c.x>=0&&c.x+c.w<=g.width); + if(i<3)assert.ok(c.x+c.w<=g.candidates[i+1].x+1e-8,'front faces must not overlap'); + // Every number stays in a readable identity lane BELOW all candidate faces. + for(const other of g.candidates)assert.ok(c.valueY>other.y+other.h+20,'number inside another candidate'); + close(c.valueX,c.labelX); + close(c.leader.x,c.x+c.w/2);close(c.leader.y,c.y+c.h+3); + close(c.leader.endX,c.labelX); + assert.ok(c.focus.x>=c.x&&c.focus.x<=c.x+c.w&&c.focus.y>=c.y&&c.focus.y<=c.y+c.h); + }); + if(time===36){ + close(g.candidates.reduce((a,c)=>a+c.w,0),g.bandW); + g.candidates.forEach((c,i)=>{close(c.w/g.bandW,frame.p[i]);close((c.x-g.bandX)/g.bandW,frame.cdf[i]);}); + } + checked++; + } + // An arbitrary backward traversal reproduces the same complete pose exactly. + for(let tick=180;tick>=0;tick-=7)assert.deepEqual(chartBandGeometry({...sample(27+tick/20,{temperature}),reducedMotion},mobile),states[tick]); + if(!reducedMotion)for(let i=1;imid.candidates[0].y+mid.candidates[0].h); +assert.ok(floor.valueX>mid.candidates[0].valueX); +console.log(`Token choice geometry: ${checked} intermediate poses; ownership, non-overlap, CDF scale, continuous focus, reverse seek passed. Browser visual acceptance remains separate.`); + +for(const temperature of [.4,.8,2])for(const phase of [1,2,3])for(const progress of [0,.05,.2499,.25,.50,.7499,.99,1]){ + const starts=[0,8,18,27,36,45],t=starts[phase]+progress*(starts[phase+1]-starts[phase]); + const frame={...sample(t,{temperature}),phase,progress}; + const op=operation(frame),i=op.candidate; + assert.ok(i>=0&&i<4&&op.progress>=0&&op.progress<=1); + if(phase===1)close(op.output,op.input/temperature); + if(phase===2)close(op.output,Math.exp(op.input)); + if(phase===3)close(op.output,op.input/frame.totalWeight); +} +for(const mobile of [false,true]){ + let previous=null; + for(let i=0;i<=900;i++){ + const g=chartBandGeometry(sample(27+i/100),mobile); + if(previous)assert.ok(Math.hypot(g.annotation.x-previous.x,g.annotation.y-previous.y)<5,'rail-to-face annotation jump'); + previous=g.annotation; + } +} +console.log('Exact operation operands/results and rail-to-face annotation continuity passed.'); diff --git a/apps/site/learn/token-choice/index.html b/apps/site/learn/token-choice/index.html index dc1501d..bf54d29 100644 --- a/apps/site/learn/token-choice/index.html +++ b/apps/site/learn/token-choice/index.html @@ -9,5 +9,5 @@
Explain3D · 数学图解实验教学数据 · 实时计算 · 可暂停检查

FOLLOW THE NUMBERS

跟着同一个词,走完计算。

候选原始分数 zz / T指数权重概率

这 4 个候选构成完整的教学词表;分数由示例设定,未调用真实语言模型。每个候选按一个教学 Token 处理。现实模型的词表与分词方式更复杂。

-

A SMALL, COMPLETE EXPLANATION

看清一次选择是怎么发生的。

这节课从模型已经给出候选分数的位置开始,演示温度缩放、指数运算、概率归一化与随机采样。图形只表达这些数学关系。

输入固定为 the cat sat on the …,4 个教学候选为 mat、floor、bed、sofa,原始分数为 3、2、1、0。这里只完成一次选择,不把结果接回上下文继续生成。

温度改变分布的集中程度;较高温度不意味着答案更正确。抽样使用可复现的均匀随机数,游标移动用来展示已确定的落点。重复抽样也可能选中同一个词。

本版保留上一版参考 Anatomy 的细线、浅色阶与轻立体风格,重新绘制数学图形,没有复用轮盘主体。查看视觉参考 ↗

浏览器内实时计算 · SVG 矢量绘制 · 支持减少动态效果 · 本版讲解为同步文字气泡,无语音

+

A SMALL, COMPLETE EXPLANATION

看清一次选择是怎么发生的。

这节课从模型已经给出候选分数的位置开始,演示温度缩放、指数运算、概率归一化与随机采样。图形只表达这些数学关系。

输入固定为 the cat sat on the …,4 个教学候选为 mat、floor、bed、sofa,原始分数为 3、2、1、0。这里只完成一次选择,不把结果接回上下文继续生成。

温度改变分布的集中程度;较高温度不意味着答案更正确。抽样使用可复现的均匀随机数,游标移动用来展示已确定的落点。重复抽样也可能选中同一个词。

本版保留上一版参考 Anatomy 的细线、浅色阶与轻立体风格,重新绘制数学图形,没有复用轮盘主体。查看视觉参考 ↗

运算轨迹是讲解指针,不表示真实计算耗时;变形只是改变表示方式。未调用真实模型。

浏览器内实时计算 · SVG 矢量绘制 · 支持减少动态效果 · 本版讲解为同步文字气泡,无语音

PREDICT · TRY · EXPLAIN

先预测,再看发生了什么。

固定分数 [3, 2, 1, 0] 和随机数 u = 0.6219。T = 0.8 时选中了 mat。现在把温度提高到 2.0,这次会选中哪个候选?

检验的是分布与随机数的关系;一次选择不会因为温度高就自动更正确。

diff --git a/apps/site/learn/token-choice/model.mjs b/apps/site/learn/token-choice/model.mjs index 1922a07..cfe2fe4 100644 --- a/apps/site/learn/token-choice/model.mjs +++ b/apps/site/learn/token-choice/model.mjs @@ -43,6 +43,17 @@ export function sample(time,{temperature=.8,seed=22,draw=0}={}){ return {time:t,phase,progress,...data,u,selected,focus:FOCUS[phase]}; } +// Reveals one exact operation at a time; travel is a teaching pointer, not compute latency. +export function operation(frame){ + const local=frame.phase===3?Math.min(1,frame.progress/.27):frame.progress; + const scaled=Math.min(3.999999,Math.max(0,local)*4),candidate=Math.floor(scaled),progress=scaled-candidate; + const i=candidate; + if(frame.phase===1)return {candidate,progress,input:frame.logits[i],operator:`÷ ${frame.temperature.toFixed(2)}`,output:frame.scaled[i]}; + if(frame.phase===2)return {candidate,progress,input:frame.scaled[i],operator:'exp',output:frame.weights[i]}; + if(frame.phase===3)return {candidate,progress,input:frame.weights[i],operator:`÷ ${frame.totalWeight.toFixed(2)}`,output:frame.p[i]}; + return null; +} + const number=value=>value.toFixed(2).replace(/\.00$/,''); const percent=value=>value>0&&value<.001?'<0.1':(value*100).toFixed(1); @@ -53,7 +64,7 @@ export function narration(frame){ {eyebrow:'看这里 · 候选分数',title:'先有分数,还没有概率。',body:'同一个输入,四个候选各有一个分数。数值越大,倾向越强;它们还不是百分比。',formula:'教学分数 z = [3, 2, 1, 0]'}, {eyebrow:'看这里 · 温度缩放',title:'除以温度,改变分数之间的差距。',body:`原始分数保持不变。T = ${t} 时,相邻候选的分差从 1 变为 ${number(1/temperature)}。`,formula:`3 ÷ ${t} = ${number(scaled[0])};2 ÷ ${t} = ${number(scaled[1])}`}, {eyebrow:'看这里 · 指数权重',title:'取指数,让分差变成权重之比。',body:'每个数经过指数运算,得到一个正的权重。高分候选获得更多权重,此时总和还不是 1。',formula:`exp(${number(scaled[0])}) = ${number(weights[0])};权重总和 = ${number(totalWeight)}`}, - {eyebrow:'看这里 · 概率分布',title:frame.progress<.73?'每个权重除以总和,得到概率。':'除以总和,所有机会合成 100%。',body:frame.progress<.73?'四个概率的总和为 1。现在把这组概率重新排成一条带,候选与颜色保持不变。':'每个权重都除以同一个总和。彩色区间的宽度,就表示这个候选被抽中的概率。',formula:`P(mat) = ${number(weights[0])} ÷ ${number(totalWeight)} = ${percent(p[0])}%`}, + {eyebrow:'看这里 · 概率分布',title:frame.progress<.73?'每个权重除以总和,得到概率。':'除以总和,所有机会合成 100%。',body:frame.progress<.27?'每一项都除以同一个权重总和。运算条逐个对应四个候选。':frame.progress<.73?'同色连线始终连着同一个候选;数值不变,高度正变成区间宽度。':'四段合为 100%;区间越宽,被均匀随机数命中的机会越大。',formula:`P(mat) = ${number(weights[0])} ÷ ${number(totalWeight)} = ${percent(p[0])}%`}, {eyebrow:'看这里 · 随机数游标',title:selected===null?'一个随机数,落入一个概率区间。':`游标落定,选中了 ${TOKENS[selected]}。`,body:'先生成 0 到 1 之间的均匀随机数。游标展示它的位置,落入哪个区间,就选哪个候选。',formula:`u = ${u.toFixed(4)};选择满足 Cᵢ ≤ u < Cᵢ₊₁ 的候选`}, {eyebrow:'看这里 · 这次的选择',title:`这次得到 ${TOKENS[selected]??'一个候选'}。`,body:`${selected===null?'':ZH[selected]+' · 概率 '+percent(p[selected])+'%。'}同一个输入再抽一次,可能选到别的候选。`,formula:'固定上下文的一次采样 · 本节没有继续生成后续 Token'} ][phase]; diff --git a/apps/site/learn/token-choice/stage.mjs b/apps/site/learn/token-choice/stage.mjs index e46d8a5..b909eea 100644 --- a/apps/site/learn/token-choice/stage.mjs +++ b/apps/site/learn/token-choice/stage.mjs @@ -1,3 +1,5 @@ +import {operation as sampleOperation} from './model.mjs'; +import {chartBandGeometry} from './geometry.mjs'; /** * A mathematical scene for Explain3D. Original vector geometry, inspired by * Anatomy's disciplined strokes and surface hierarchy. No wheel geometry reused. @@ -18,18 +20,18 @@ const css=` svg{display:block;width:100%;height:100%;overflow:visible;contain:layout style} path,line,rect,circle{vector-effect:non-scaling-stroke} text{user-select:none;paint-order:stroke;stroke:none;text-rendering:geometricPrecision} - .axis{font:11px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#8290a1} + .axis{font:18px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#8290a1} .token{font:25px Georgia,"Times New Roman",serif;fill:#3a4657;letter-spacing:.1px} - .translation{font-size:11px;fill:#7e8795} + .translation{font-size:14px;fill:#7e8795} .number{font:18px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#485872;font-variant-numeric:tabular-nums} .note{font-size:12px;fill:#778292;letter-spacing:.15px} .formula{font:17px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#5a6680;font-variant-numeric:tabular-nums} - .small{font:11px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#8992a0} + .small{font:18px ui-monospace,SFMono-Regular,Consolas,monospace;fill:#8992a0} .fine{fill:none;stroke:#c3ccd5;stroke-width:.6;stroke-linecap:round;stroke-linejoin:round} .guide{fill:none;stroke:#b2bfce;stroke-width:.6;stroke-dasharray:2 4} .grid{stroke:#dfe5ea;stroke-width:.5} .bright{stroke:#fff;stroke-width:1;fill:none} - .mobile .axis{font-size:13px}.mobile .number{font-size:18px}.mobile .note{font-size:13px}.mobile .small{font-size:12px}.mobile .translation{font-size:12px}.mobile .token{font-size:23px}.mobile .formula{font-size:16px} + .mobile .axis{font-size:18px}.mobile .number{font-size:18px}.mobile .note{font-size:16px}.mobile .small{font-size:18px}.mobile .translation{font-size:16px}.mobile .token{font-size:22px}.mobile .formula{font-size:18px} `; function node(tag,attrs={},parent){const n=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,String(v));parent?.append(n);return n;} @@ -47,7 +49,11 @@ export function createStage(host){ const floorGradient=node('linearGradient',{id:'tc-floor',x1:0,y1:0,x2:0,y2:1},defs);node('stop',{offset:0,'stop-color':'#fdfefe'},floorGradient);node('stop',{offset:1,'stop-color':'#edf1f4'},floorGradient); const goldGradient=node('linearGradient',{id:'tc-gold',x1:0,y1:0,x2:0,y2:1},defs);node('stop',{offset:0,'stop-color':'#f4d799'},goldGradient);node('stop',{offset:1,'stop-color':'#d7ac57'},goldGradient); const glow=node('filter',{id:'tc-shadow',x:'-40%',y:'-100%',width:'180%',height:'300%'},defs);node('feGaussianBlur',{stdDeviation:4},glow); - const gradients=COLORS.map((c,i)=>{const g=node('linearGradient',{id:`tc-bar-${i}`,x1:0,y1:0,x2:1,y2:0},defs);const a=node('stop',{offset:0,'stop-color':c,'stop-opacity':.48},g),b=node('stop',{offset:.83,'stop-color':c,'stop-opacity':.28},g);node('stop',{offset:1,'stop-color':'#fff','stop-opacity':.7},g);return[a,b];}); + const gradients=COLORS.map((c,i)=>{const g=node('linearGradient',{id:`tc-bar-${i}`,x1:0,y1:0,x2:1,y2:0},defs);const a=node('stop',{offset:0,'stop-color':c,'stop-opacity':.65},g),b=node('stop',{offset:.83,'stop-color':c,'stop-opacity':.42},g);node('stop',{offset:1,'stop-color':'#fff','stop-opacity':.4},g);return[a,b];}); + const operationRail=node('g',{'data-operation':'exact-values'},svg),railBody=node('path',{fill:'url(#tc-floor)',stroke:'#bcc8d4','stroke-width':.7},operationRail),railLower=node('path',{fill:'none',stroke:'#d4dce3','stroke-width':1},operationRail),railDivider=node('path',{class:'fine'},operationRail); + const railToken=node('text',{class:'token','text-anchor':'middle'},operationRail),railInput=node('text',{class:'number','text-anchor':'middle'},operationRail),railOperator=node('text',{class:'number','text-anchor':'middle'},operationRail),railOutput=node('text',{class:'number','text-anchor':'middle'},operationRail),railArrow1=node('text',{class:'number','text-anchor':'middle'},operationRail),railArrow2=node('text',{class:'number','text-anchor':'middle'},operationRail); + text(railArrow1,'→');text(railArrow2,'→'); + const railFlow=node('path',{fill:'none','stroke-width':.9,'stroke-dasharray':'2 4'},operationRail),railDot=node('circle',{r:3,stroke:'#fff','stroke-width':1},operationRail); const axisGroup=node('g',{},svg),axisTicks=[]; const plotBacking=node('path',{fill:'#f5f7f9',opacity:.6},axisGroup); const gridGroup=node('g',{},axisGroup); @@ -72,6 +78,8 @@ export function createStage(host){ text(label,token);text(translation,CHINESE[i]); return{g,ghost,side,front,top,shine,seam,foot,operation,operationLine,operationDot,number,original,labelGroup,dot,label,translation}; }); + const bandDock=node('g',{opacity:0},baseGroup.parentNode),dockEdge=node('path',{fill:'#eef1f3',stroke:'#bec9d3','stroke-width':.6},bandDock),dockLip=node('path',{fill:'none',stroke:'#fff','stroke-width':1},bandDock); + svg.insertBefore(bandDock,barGroup); const bandScale=node('g',{opacity:0},svg),bandTicks=[]; for(let i=0;i<=20;i++){const tick=node('line',{class:'fine'},bandScale);const label=i%5===0?node('text',{class:'axis','text-anchor':'middle'},bandScale):null;bandTicks.push({tick,label});} const bandBounds=node('g',{},bandScale),bounds=Array.from({length:3},()=>node('path',{fill:'none',stroke:'#b0bdca','stroke-width':.65},bandBounds)); @@ -83,17 +91,16 @@ export function createStage(host){ let lastFrame=null,disposed=false,anchors={},width=1000,height=560; function render(frame){ if(disposed)return;lastFrame=frame; - const mobile=host.getBoundingClientRect().width<600; width=mobile?560:1000;height=mobile?430:420; + const mobile=host.getBoundingClientRect().width<600; + const layout=chartBandGeometry(frame,mobile); ({width,height}=layout); attr(svg,{viewBox:`0 0 ${width} ${height}`,class:mobile?'mobile':'desktop'}); const phase=clamp(frame.phase,0,5),pr=clamp(frame.progress??0),P=frame.p,T=frame.temperature; const cdf=frame.cdf?.length===5?frame.cdf:[0,...P.map((_,i)=>P.slice(0,i+1).reduce((a,b)=>a+b,0))]; - const chosen=frame.selected,base=mobile?290:285,plotH=mobile?214:225,barW=mobile?62:110,dx=mobile?7:11,dy=mobile?5:8; - const xs=mobile?[91,202,313,424]:[130,345,560,775]; - const axisX=mobile?64:87,plotEnd=xs[3]+barW+dx; - const bandX=mobile?52:110,bandW=mobile?456:780,bandY=170,bandH=mobile?68:72; - let morph=phase>3?1:phase===3?ease((pr-.27)/.46):0; - if(frame.reducedMotion&&phase===3)morph=pr>=.5?1:0; - const isBand=morph>.99,chartOpacity=1-morph; + const chosen=frame.selected,dx=mobile?5:11,dy=mobile?4:8; + const {base,plotH,barW,xs,bandX,bandW,bandY,bandH,morph}=layout; + const axisX=mobile?68:87,plotEnd=xs[3]+barW+dx; + const chartOpacity=1-ease(morph/.22); + const operation=sampleOperation(frame),showOperation=phase===1||phase===2||phase===3&&pr<.27; let values=frame.logits,max=4,axisTitle='原始分数 z'; if(phase===1){values=frame.scaled;max=8;axisTitle='缩放分数 z / T';} if(phase===2){values=frame.weights;max=Math.max(...values);axisTitle='指数权重 exp(z / T)';} @@ -101,7 +108,7 @@ export function createStage(host){ attr(axisGroup,{opacity:chartOpacity});attr(baseGroup,{opacity:chartOpacity}); attr(plotBacking,{d:poly([[axisX,base],[axisX,base-plotH],[plotEnd,base-plotH],[plotEnd,base]])}); attr(axisSpine,{d:`M${axisX} ${base-plotH-8}V${base}H${plotEnd+12}`}); - attr(axisName,{x:axisX,y:base-plotH-25});text(axisName,axisTitle); + attr(axisName,{x:plotEnd,y:base-plotH-13,'text-anchor':'end'});text(axisName,axisTitle); axisTicks.forEach((a,i)=>{const y=base-plotH*i/4;attr(a.line,{x1:axisX,y1:y,x2:plotEnd,y2:y});attr(a.tick,{x1:axisX-4,y1:y,x2:axisX,y2:y});attr(a.label,{x:axisX-12,y:y+4});text(a.label,phase>=3?`${i*25}%`:value(max*i/4));}); const bx=xs[0]-25,bw=plotEnd-bx+24,by=base+8; attr(baseShadow,{d:poly([[bx-6,by+12],[bx+bw+12,by+12],[bx+bw+19,by+26],[bx,by+26]])}); @@ -111,11 +118,13 @@ export function createStage(host){ bars.forEach((b,i)=>{ const color=frame.color===false?['#8e9bad','#9eabba','#aeb9c5','#bdc7d0'][i]:COLORS[i]; gradients[i].forEach(stop=>stop.setAttribute('stop-color',color));attr(b.side,{fill:color});attr(b.front,{stroke:color});attr(b.dot,{fill:color});attr(b.seam,{stroke:color}); - const h=Math.max(0,values[i]/max*plotH),targetX=bandX+cdf[i]*bandW,targetW=P[i]*bandW; - const x=mix(xs[i],targetX,morph),y=mix(base-h,bandY,morph),w=mix(barW,targetW,morph),hh=mix(h,bandH,morph); - const depthX=mix(dx,mobile?5:8,morph),depthY=mix(dy,mobile?4:6,morph); + const h=Math.max(0,values[i]/max*plotH); + const owned=layout.candidates[i]; + const x=phase>=3?owned.x:xs[i],y=phase>=3?owned.y:base-h,w=phase>=3?owned.w:barW,hh=phase>=3?owned.h:h; + const depthX=Math.min(w/3,mix(dx,mobile?4:8,morph)),depthY=mix(dy,mobile?3:6,morph); const active=phase>=4&&chosen!==null,muted=active&&i!==chosen; - attr(b.g,{opacity:muted?.49:1}); + attr(b.g,{opacity:muted?.72:1}); + attr(b.front,{'stroke-width':showOperation&&operation.candidate===i?1.3:.65}); attr(b.front,{d:bevel(x,y,w,Math.max(.5,hh),Math.min(2,w/5))}); attr(b.side,{d:poly([[x+w,y],[x+w+depthX,y-depthY],[x+w+depthX,y+hh-depthY],[x+w,y+hh]])}); attr(b.top,{d:poly([[x,y],[x+depthX,y-depthY],[x+w+depthX,y-depthY],[x+w,y]]),opacity:hh>.5?1:0}); @@ -127,23 +136,55 @@ export function createStage(host){ const ox=xs[i]+barW+17,oy=base-ghostH,ey=base-h,travel=frame.reducedMotion?1:ease((pr-.12)/.65); attr(b.operationLine,{d:`M${ox-4} ${f(oy)}H${ox}V${f(ey)}H${ox-4}`,stroke:color}); attr(b.operationDot,{cx:ox,cy:mix(oy,ey,travel),fill:color}); - const nx=xs[i]+barW/2;attr(b.number,{x:nx,y:base-h-18,opacity:chartOpacity});text(b.number,phase>=3?percent(P[i]):value(values[i])); + const nx=xs[i]+barW/2; + attr(b.number,{x:phase>=3?owned.valueX:nx,y:phase>=3?owned.valueY:base-h-18,opacity:1});text(b.number,phase>=3?percent(P[i]):value(values[i])); attr(b.original,{x:nx,y:base+22,opacity:phase===1?1:0});text(b.original,`z = ${frame.logits[i]}`); - const lx=mix(nx,bandX+bandW*(i+.5)/4,morph),ly=mix(base+(phase===1?53:48),bandY+bandH+69,morph); - attr(b.labelGroup,{transform:`translate(${f(lx)} ${f(ly)})`});attr(b.label,{x:0,y:0});attr(b.translation,{x:0,y:19});attr(b.dot,{cx:-(mobile?25:28),cy:-7}); - text(b.translation,isBand?percent(P[i]):CHINESE[i]); + const lx=phase>=3?owned.labelX:nx,ly=phase>=3?owned.labelY:base+(phase===1?53:48); + attr(b.labelGroup,{transform:`translate(${f(lx)} ${f(ly)})`});attr(b.label,{x:0,y:0});attr(b.translation,{x:0,y:19});attr(b.dot,{cx:-(mobile?29:35),cy:-7}); + attr(b.translation,{opacity:phase>=3?0:1});text(b.translation,CHINESE[i]); }); - attr(bandScale,{opacity:morph}); + attr(bandDock,{opacity:morph}); + const {x:dockX,y:dockY,end:dockEnd}=layout.dock; + attr(dockEdge,{d:bevel(dockX,dockY,dockEnd-dockX,12,2)}); + attr(dockLip,{d:`M${dockX+2} ${dockY+1}H${dockEnd-2}`}); + attr(bandScale,{opacity:phase===5?0:ease((morph-.8)/.2)}); bandTicks.forEach((a,i)=>{const x=bandX+bandW*i/20;attr(a.tick,{x1:x,y1:bandY-18,x2:x,y2:bandY-(i%5===0?29:23)});if(a.label){attr(a.label,{x,y:bandY-38});text(a.label,phase>=4?(i/20).toFixed(i===0||i===20?0:2):`${i*5}%`);}}); bounds.forEach((p,i)=>{const x=bandX+cdf[i+1]*bandW;attr(p,{d:`M${f(x)} ${bandY+bandH+4}V${bandY+bandH+12}`});}); - attr(bandLeaders,{opacity:morph}); - labelLeaders.forEach((leader,i)=>{const sx=bandX+(cdf[i]+P[i]/2)*bandW,ex=bandX+bandW*(i+.5)/4,sy=bandY+bandH+7,ey=bandY+bandH+40;attr(leader,{d:`M${f(sx)} ${sy}C${f(sx)} ${sy+14},${f(ex)} ${ey-13},${f(ex)} ${ey}`,stroke:frame.color===false?'#a7b3c0':COLORS[i],opacity:.55});}); - const formulaY=mobile?378:365,fw=mobile?490:570; + attr(bandLeaders,{opacity:phase>=3?1:0}); + labelLeaders.forEach((leader,i)=>{const {x:sx,y:sy,endX:ex,endY:ey}=layout.candidates[i].leader;attr(leader,{d:`M${f(sx)} ${f(sy)}C${f(sx)} ${f(sy+9)},${f(ex)} ${f(ey-9)},${f(ex)} ${f(ey)}`,stroke:frame.color===false?'#a7b3c0':COLORS[i],opacity:.85});}); + attr(operationRail,{opacity:phase===3?1-ease((pr-.22)/.05):showOperation?1:0,'aria-hidden':String(!showOperation)}); + if(operation){ + const i=operation.candidate,c=COLORS[i],rw=mobile?374:650,rx=(width-rw)/2,ry=9; + attr(railBody,{d:bevel(rx,ry,rw,49,5)});attr(railLower,{d:`M${rx+5} 61H${rx+rw-5}`}); + const points=mobile?[rx+29,rx+88,rx+141,rx+210,rx+277,rx+329]:[rx+52,rx+155,rx+237,rx+325,rx+420,rx+538]; + const [tx,ix,a1x,ox,a2x,outx]=points; + attr(railToken,{x:tx,y:39,fill:c,'font-size':mobile?19:25});text(railToken,TOKENS[i]); + attr(railDivider,{d:`M${rx+(mobile?56:100)} 18V49`}); + attr(railInput,{x:ix,y:39});text(railInput,value(operation.input)); + attr(railOperator,{x:ox,y:39,fill:c});text(railOperator,operation.operator); + attr(railOutput,{x:outx,y:39,fill:c});text(railOutput,phase===3?percent(operation.output):value(operation.output)); + attr(railArrow1,{x:a1x,y:39});attr(railArrow2,{x:a2x,y:39}); + const targetX=xs[i]+barW/2,targetY=base-values[i]/max*plotH-30; + attr(railFlow,{d:`M${ix} 47H${outx}V73H${targetX}V${targetY}`,stroke:c}); + const travel=frame.reducedMotion?1:operation.progress; + let dotX,dotY; + if(travel<.65){dotX=mix(ix,outx,ease(travel/.65));dotY=47;} + else { + const path=[[outx,47],[outx,73],[targetX,73],[targetX,targetY]]; + const lengths=path.slice(1).map((p,j)=>Math.hypot(p[0]-path[j][0],p[1]-path[j][1])); + let distance=ease((travel-.65)/.35)*lengths.reduce((a,b)=>a+b,0),j=0; + while(jlengths[j])distance-=lengths[j++]; + const t=lengths[j]?distance/lengths[j]:1; + dotX=mix(path[j][0],path[j+1][0],t);dotY=mix(path[j][1],path[j+1][1],t); + } + attr(railDot,{cx:dotX,cy:dotY,fill:c}); + } + const formulaY=mobile?378:365,fw=mobile?378:570; attr(formulaBack,{d:bevel((width-fw)/2,formulaY,fw,44,7)});attr(formulaBottom,{d:`M${(width-fw)/2+7} ${formulaY+47}H${(width+fw)/2-7}`});attr(formulaLabel,{x:width/2,y:formulaY+(phase===2?18:28)}); const sum=frame.weights.reduce((a,b)=>a+b,0),selected=chosen??0; const formulas=[`z = [ ${frame.logits.join(' , ')} ]`,`z / T T = ${T.toFixed(2)}`,`wᵢ = exp(zᵢ / T)`,`pᵢ = wᵢ / Σw Σp = 100%`,`u = ${frame.u.toFixed(3)} u ∈ [0, 1)`,`本次选择 ${TOKENS[selected]} · ${percent(P[selected])}`]; text(formulaLabel,formulas[phase]); - attr(weightScaleNote,{x:width/2,y:formulaY+35,opacity:phase===2?1:0});text(weightScaleNote,`高度按最大权重 ${value(max)} 缩放 · Σw = ${value(sum)}`); + attr(weightScaleNote,{x:width/2,y:formulaY+35,opacity:phase===2?1:0});text(weightScaleNote,mobile?`量程 ${value(max)} · Σw = ${value(sum)}`:`高度按最大权重 ${value(max)} 缩放 · Σw = ${value(sum)}`); const cursorTravel=phase>4?1:frame.reducedMotion?(pr>=.8?1:0):ease(pr/.8),cursorX=bandX+frame.u*bandW*cursorTravel; attr(cursorGroup,{opacity:phase===4?1:0});attr(cursorGlow,{x1:cursorX,y1:bandY-10,x2:cursorX,y2:bandY+bandH+8});attr(cursorLine,{x1:cursorX,y1:bandY-10,x2:cursorX,y2:bandY+bandH+8});attr(cursorHead,{d:poly([[cursorX-6,bandY-19],[cursorX+6,bandY-19],[cursorX+6,bandY-11],[cursorX,bandY-5],[cursorX-6,bandY-11]])}); attr(cursorReadout,{x:width/2,y:bandY-85,opacity:0});text(cursorReadout,chosen===null?'定位随机数':'已落入对应区间');attr(cursorNumber,{x:width/2,y:bandY-60,opacity:0});text(cursorNumber,chosen===null?`x = ${(frame.u*cursorTravel).toFixed(3)}`:`u = ${frame.u.toFixed(3)}`); @@ -153,8 +194,8 @@ export function createStage(host){ attr(resultLink,{d:`M${f(resultX)} ${f(resultY+resultH)}C${f(resultX)} ${f(resultY+resultH+12)},${f(segmentX)} ${bandY-16},${f(segmentX)} ${bandY+8}`}); attr(resultShadow,{x:resultX-resultW/2+2,y:resultY+6,width:resultW,height:resultH}); attr(resultSide,{d:poly([[resultX-resultW/2,resultY+resultH-5],[resultX+resultW/2,resultY+resultH-5],[resultX+resultW/2+7,resultY+resultH-12],[resultX+resultW/2+7,resultY+resultH-4],[resultX+resultW/2,resultY+resultH+3],[resultX-resultW/2,resultY+resultH+3]])}); - attr(resultPlate,{x:resultX-resultW/2,y:resultY,width:resultW,height:resultH});attr(resultAccent,{x:resultX-resultW/2+15,y:resultY+12,width:resultW-30,fill:COLORS[selected]});attr(resultToken,{x:resultX,y:resultY+43});text(resultToken,TOKENS[selected]);attr(resultProbability,{x:resultX,y:resultY+62});text(resultProbability,`${CHINESE[selected]} · ${percent(P[selected])}`);attr(resultTitle,{x:resultX,y:resultY-13});text(resultTitle,'SELECTED TOKEN'); - anchors={scores:[xs[0]+barW*.5,base-frame.logits[0]/4*plotH*.72],temperature:[xs[0]+barW+17,base-frame.scaled[0]/8*plotH*.72],weights:[xs[0]+barW/2,base-plotH*.7],probability:morph>.5?[bandX+P[0]*bandW*.5,bandY+bandH/2]:[xs[0]+barW/2,base-P[0]*plotH*.7],cursor:[cursorX,bandY+bandH/2],sampling:[cursorX,bandY+bandH/2],result:[resultX,resultY+12]}; + attr(resultPlate,{x:resultX-resultW/2,y:resultY,width:resultW,height:resultH});attr(resultAccent,{x:resultX-resultW/2+15,y:resultY+12,width:resultW-30,fill:COLORS[selected]});attr(resultToken,{x:resultX,y:resultY+43});text(resultToken,TOKENS[selected]);attr(resultProbability,{x:resultX,y:resultY+62});text(resultProbability,`${CHINESE[selected]} · ${percent(P[selected])}`);attr(resultTitle,{x:resultX,y:resultY-13});text(resultTitle,'选中结果'); + anchors={scores:[xs[0]+barW*.5,base-frame.logits[0]/4*plotH*.72],temperature:[width/2,33],weights:[width/2,33],probability:[layout.annotation.x,layout.annotation.y],cursor:[cursorX,bandY+bandH/2],sampling:[cursorX,bandY+bandH/2],result:[resultX,resultY+12]}; const spoken=phase<3?TOKENS.map((token,i)=>`${token} ${phase===0?'原始分数':phase===1?'缩放分数':'指数权重'} ${value(values[i])}`).join(','):TOKENS.map((token,i)=>`${token} 概率 ${percent(P[i])}`).join(','); text(desc,`阶段 ${phase+1}。温度 ${T.toFixed(2)}。${spoken}。${chosen===null?'尚未完成采样。':`本次选中 ${TOKENS[chosen]}。`}`); } diff --git a/apps/site/learn/token-choice/style.css b/apps/site/learn/token-choice/style.css index ef60d00..a2c55d6 100644 --- a/apps/site/learn/token-choice/style.css +++ b/apps/site/learn/token-choice/style.css @@ -11,3 +11,10 @@ dialog{border:1px solid #e5dce9;background:#fffdf8;border-radius:18px;max-width: .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} + +/* Reserve real drawing space: a narrow screen must not shrink key type to 9px. + Initial settings may scroll; the playing stage retains pause and progress. */ +.exhibit{min-height:610px} +@media(max-width:700px){.exhibit{min-height:610px}.callout p{font-size:13px}.callout>span,.calculation{font-size:11px}.artwork{width:100%}} + +.reduce-motion .secondary,.reduce-motion .artwork{transition:none!important} diff --git a/docs/ai-learning/STATUS.md b/docs/ai-learning/STATUS.md index c876edf..0c6a7d6 100644 --- a/docs/ai-learning/STATUS.md +++ b/docs/ai-learning/STATUS.md @@ -2,9 +2,9 @@ Updated: 2026-10-28 (Asia/Shanghai). -Current total: **4 / 10 published**. **Visual acceptance reopened for all four on 2026-10-28 after user feedback.** Active work: **Token choice quality rework**, starting with misbound probability labels at 32 seconds. New topics, including Transformer, are on hold until the four-scene rework queue passes the updated review gates. +Current total: **4 / 10 published**. **Visual acceptance reopened for all four on 2026-10-28 after user feedback.** Token-choice follow-up has passed developer visual/teaching review and awaits the checked release; the other three remain open. Next rework: **Attention**. New topics, including Transformer, are on hold until the four-scene rework queue passes the updated review gates. -See [Anatomy comparison and concrete findings](reviews/2026-10-28-anatomy-quality.md). Rework order: token-choice → attention → tokenization → embedding. Keep existing routes and mathematical/release evidence. Publication status does not mean the renewed visual gate has passed. This review changes no website implementation or distributed Skill. +See [Anatomy comparison and concrete findings](reviews/2026-10-28-anatomy-quality.md). Rework order: token-choice → attention → tokenization → embedding. Keep existing routes and mathematical/release evidence. Publication status does not mean the renewed visual gate has passed. The initial comparison changed no website implementation or distributed Skill. A subsequent token-choice repair exists only on the draft branch described below. Historical “Next” statements below describe the release sequence at that time and are superseded by this active work item and roadmap. @@ -44,3 +44,12 @@ Route `/learn/embedding/`. One question: how an integer ID retrieves one row fro ## Published lesson 03: Attention — 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. Published at https://explain.funagent.app/learn/attention/ . PR #4 passed CI 40562980763 and READY preview dpl_8fpzGGqXyoq2sbd7jDRdFDc2k5hV; fetched preview HTML/app/stage matched reviewed source exactly. Merge 49086e255b9527202f467103d7d0353b81627632 reached production READY as dpl_H4cKbv114YSRUKfzEGFXcdUXfE9P. Production browser verified future V=1000 leaves output 15.752, query zero produces 10, and prediction feedback/return preserves that state. No new Skill or host-compatibility claim; uses the shared playback, annotation and evidence method. Next after acceptance: Transformer layer. + +## Token-choice repair candidate — 2026-10-28 + +Draft branch `fix/token-choice-morph-craft-20261028` repairs moving-label ownership, makes the focus continuous, and drafts narrow-layout/carrier refinements. `npm run check` and3,620 geometric poses pass. Browser bootstrap returned Unknown tool; actual1×/4×/intermediate/390px visual and lifecycle acceptance remain pending. **No production update; visual acceptance remains needs-rework and token-choice stays active.** See [candidate evidence and exact pending checks](reviews/2026-10-28-token-choice-rework.md). Skill changes and new topics remain on hold. + + +## Reviewed token-choice follow-up — 2026-10-28 + +Browser returned; actual intermediate/reverse desktop frames,390px, native4× contact detail, natural completion/replay, keyboard, manual reduced-motion and controlled hidden-event paths reviewed. Fixed newly observed carrier/front intersection and narrow tick clipping as well as the original label defect. Exact operation rail and staged scales improve causal inspection. Full check passed. Review decision closes listed token-choice defects, without asserting measured learning gains or awards; the other three scenes and Skill distribution alignment remain open. See [full follow-up record](reviews/2026-10-28-token-choice-rework.md). Deployment receipt follows only after matching production SHA/READY and route verification. diff --git a/docs/ai-learning/reviews/2026-10-28-token-choice-rework.md b/docs/ai-learning/reviews/2026-10-28-token-choice-rework.md new file mode 100644 index 0000000..d417d70 --- /dev/null +++ b/docs/ai-learning/reviews/2026-10-28-token-choice-rework.md @@ -0,0 +1,72 @@ +# Token choice rework candidate — 2026-10-28 + +Status: **implementation draft; NOT visually accepted, merged or published**. Base production/main: `0a3781278936c57f63de88dd9ab207fd1605dd2f`. All four published scenes retain `needs-rework`. This candidate is one step in token-choice rework, not a claim of Anatomy-level completion. + +## Implemented changes + +- Probability values no longer remain above the old bar positions while the rectangles move. From the probability chapter onward, candidate name and probability share one ordered identity lane. A color-matched line starts at that candidate's current front-face center/bottom and ends just above its identity. This remains meaningful for very narrow intervals. +- A pure geometry sampler owns the probability front faces, numeric-label positions, connectors and focus. The callout anchor follows a continuous point inside the moving blue face; it no longer switches between two anchor formulas at morph=.5. +- The interval band gains a shallow supporting edge; its actual widths remain probability-proportional. Extrusion shrinks with narrow interval width so depth does not overwhelm a tiny probability. These are drafted refinements, not browser-observed craft evidence. +- Narrow layout is recomposed to a 390-unit view rather than compressing 560 units; active value type remains 18 units and supporting type 16. Extra vertical drawing space is reserved. Initial settings may require vertical scrolling: the real 390px fit, active type size, viewport-height behavior and playing layout still require browser measurement. Do not infer legibility merely from declared font size. +- Narration now names the same-color correspondence and the height-to-width encoding. The interpretation is a change of representation, not probability changing during the morph. + +## Checks actually run + +`npm run check` passed, including existing independent mathematical golden values, all four AI scene tests, 721 planetary constraints and the site build. Added geometry invariants examine 3,620 poses: desktop/narrow, T=.4/.8/1/1.6/2, normal/reduced motion, 181 times between27s and36s per combination. They check disjoint front faces, values below every face, connector ownership, focus containment/continuity, exact final CDF positions and widths, and deterministic reverse seeking. Explicit default32s regression included. + +These are numerical/geometric checks, **not browser screenshots, user study results or actual device evidence**. They do not certify font metrics, painted stacking, connector collisions, reduced-motion preference handling or UI lifecycle. + +## Why the candidate remains a draft + +The Browser plugin was discovered and its prescribed bootstrap attempted. It returned `Unknown tool(browser.js)`. No CUA browser surface was exposed in this run. The managed preview instructions prohibit improvising a shell browser. No preview server, screenshot or browser acceptance is claimed. This is an environment verification limitation, not a permission request. + +Pending before merge: + +1. Actually inspect defaultT=.8 at27,29,30,31,32,33,34,36s and the reverse sequence; repeatT=.4 and2, including smallsofa intervals. Inspect line routing, neighboring labels and pale-edge contrast. +2. Capture actual1× whole stage and meaningful4× detail with no substitute generated imagery. Check the added carrier, bevels, contacts, occlusion and ownership during movement. +3. Check390px viewport: read every active number and formula, measure effective CSS font size, verify no clipped axis maximum and inspect the scrolling tradeoff. Review desktop diagram size as well. +4. Exercise play/pause/resume/seek/replay/completion, temperature changes, keyboard, reduced-motion and hidden-tab behavior. Preserve the fixedu counterfactual T=.8→mat / T=2→floor and prediction check. +5. Continue causal refinements where needed: earlier temperature/exponent/normalization transitions are still phase-based. This candidate does not close the entire continuous-causality review. +6. Check protected branch preview; only after all applicable review gates pass, merge and verify matching production SHA/READY/domain. + +No Skill distribution changed. Proven methods may be propagated after real visual verification through skill-creator, without overwriting unrelated personal Skill changes. No additional theme has started. + + +## Follow-up implementation and real browser review — 2026-10-28 + +The cloud browser became available in the subsequent user-authorized session. The earlier draft-only record above is historical. This follow-up implemented and inspected the candidate; production receipt is recorded separately after deployment. + +### Changes beyond the draft + +- The temperature/exponent/normalization chapters now carry an exact operand → operator → result rail. A deterministic pointer travels along that operation and its connection to the corresponding candidate, in mat/floor/bed/sofa order. No interpolated/fictitious mathematical values are introduced. The diagram explicitly describes this as a teaching pointer, not hardware compute latency; chapter boundaries still switch units discretely. +- Retained aligned front-face quantitative lengths, shallow side faces, bevel edges, base contacts and identity colors. Removed old chart scales early in the morph and introduced cumulative ticks only when the band is almost settled. On the result pose the ruler is intentionally withdrawn to leave room for the lifted result card; candidate probabilities remain visible below. +- Actual4× inspection revealed that the added carrier crossed the translucent fronts during the morph. Fixed it to follow the shared bottom surface, and added a non-intersection/contact invariant. A low-temperature390px playback exposed clipped left tick text; moved the axis into its reserved margin and rechecked large values. +- Raised active type, used a390-unit narrow composition and reserved drawing height. Added a manual reduced-motion setting in the lesson explanation alongside the existing OS preference support. Initial narrow settings can require vertical scrolling; playing removes them and keeps the exhibit/pause/timeline in one screen. + +### Browser evidence actually observed + +Environment: current Chrome cloud browser, desktop screenshot1363×936, same-source390×844 iframe (not physical phone), plus1200px iframe rendered at native CSS zoom4× (not an upscaled raster). Read-only DOM geometry showed key18-unit numbers at about14.89 CSS px in the paused narrow exponent layout (SVG scale .82747),22-unit candidate names at18.20px. Playing uses the available larger drawing area. Source-distribution/physical phone/OS preference coverage is not inferred from these observations. + +| Check | Observed result | +|---|---| +| Forward morph | Actual paused27,29,30,31,32,33,34,36s frames inspected. Ordered values and color connectors retain their candidate ownership. No floor20.6% inside mat. | +| Reverse morph |36→32→29 inspected; corresponding geometry/labels returned to the same states. | +| Computation |8/12/20s rails show3÷.8=3.75,2÷.8=2.5,exp(3.75)=42.52, and normalization by59.19. Moving pointer remains on the drawn path. | +| Extremes |T=.4 band and tinysofa interval inspected, plusT=.4 exponent values up to1808 on narrow screen. FinalT=2 distribution and floor27.6% checked via prediction experiment. | +| Depth/detail |4× screenshot below shows front, top/side shading, bevel, carrier contact and identity connectors after the carrier repair. It is an inspectable mathematical exhibit, not a claim to match the part complexity of a physical watch. | +| Playback |Play/pause/resume exercised; progress moved with wall time and settings disappeared/reappeared. From49.5s natural completion reached50 and restored settings/quiz. Replay restarted near0; Escape paused. Home/End range keys and chapter stepping exercised. | +| Hidden |Controlled harness visibilitychange stopped playback at36.5s, restored settings and remained paused after visible event. This is an event-path test, not an OS tab-suspension or mobile backgrounding claim. | +| Reduced motion |Manual setting checked via actual dialog; subsequent seeking/chapter navigation used the reduced-motion branch. OS-level preference toggling not performed in this run. | +| Learning check |Narrow modal: wrongmat and correctfloor feedback inspected. Return-to-diagram preservedT=2,u=.6219,floor27.6%; explainsCDF boundaries .4551/.7311. | +| Model/site |npm run check passed: independent original golden values,3,620 geometry poses, exact operation equations, rail-anchor continuity, carrier contact, existing four-scene tests and721 planetary constraints. | + +![32-second whole-stage ownership](assets/2026-10-28-token-choice/desktop-32.jpg) +![Native4× detail after carrier repair](assets/2026-10-28-token-choice/detail-4x-fixed.jpg) +![390px intermediate frame](assets/2026-10-28-token-choice/mobile-32.jpg) +![Stable desktop result with ruler withdrawn](assets/2026-10-28-token-choice/desktop-result.jpg) + +### Teaching review decision + +This is developer teaching/visual review, not measured learning-effect research or user aesthetic approval. The bounded question remains how fixed scores and one fixed uniform sample produce one chosen candidate. The first view names the input/scores and makes score≠probability explicit; the operation rail links exact values to colors; the morph changes representation without changing probability; a fixed-u temperature experiment changes the selected result and explains why. Those observations close the listed token-choice ownership, narrow-readability, continuity and contact defects for this revision. The same benchmark is still open for Attention, tokenization and embedding; no new topic is authorized to bypass that queue. + +Limits: no real learner sessions, physical phones, screen readers, other browser engines, OS preference switching or hardware performance measurements. No award/equivalent-fidelity claim. Public/working Skill alignment remains a distinct pending task; this release does not claim a new installed or cross-host-verified Skill. diff --git a/docs/ai-learning/reviews/assets/2026-10-28-token-choice/desktop-32.jpg b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/desktop-32.jpg new file mode 100644 index 0000000..14a315d Binary files /dev/null and b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/desktop-32.jpg differ diff --git a/docs/ai-learning/reviews/assets/2026-10-28-token-choice/desktop-result.jpg b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/desktop-result.jpg new file mode 100644 index 0000000..f91ac23 Binary files /dev/null and b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/desktop-result.jpg differ diff --git a/docs/ai-learning/reviews/assets/2026-10-28-token-choice/detail-4x-fixed.jpg b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/detail-4x-fixed.jpg new file mode 100644 index 0000000..f2ac3ff Binary files /dev/null and b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/detail-4x-fixed.jpg differ diff --git a/docs/ai-learning/reviews/assets/2026-10-28-token-choice/mobile-32.jpg b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/mobile-32.jpg new file mode 100644 index 0000000..08bc3fc Binary files /dev/null and b/docs/ai-learning/reviews/assets/2026-10-28-token-choice/mobile-32.jpg differ diff --git a/docs/ai-learning/roadmap.json b/docs/ai-learning/roadmap.json index 6751ef1..77ac98e 100644 --- a/docs/ai-learning/roadmap.json +++ b/docs/ai-learning/roadmap.json @@ -89,9 +89,10 @@ "verifiedDeployment": "dpl_8KskYdCLpm2uudUGezEzk3RiFJyD", "verifiedOn": "2026-10-26", "visualAcceptance": { - "status": "needs-rework", + "status": "reviewed-pass", "reviewDate": "2026-10-28", - "review": "docs/ai-learning/reviews/2026-10-28-anatomy-quality.md" + "review": "docs/ai-learning/reviews/2026-10-28-token-choice-rework.md", + "evidenceType": "developer teaching and actual browser visual review; no learner-effect study" } }, { @@ -146,13 +147,20 @@ "basis": "User requested renewed comparison against Anatomy; publication does not imply visual acceptance.", "report": "docs/ai-learning/reviews/2026-10-28-anatomy-quality.md", "holdNewTopics": true, - "activeScene": "token-choice", + "activeScene": "attention", "reworkOrder": [ "token-choice", "attention", "tokenization", "embedding" ], - "resumeAfter": "All four published scenes pass the reopened craft and causal-motion gates." + "resumeAfter": "All four published scenes pass the reopened craft and causal-motion gates.", + "candidate": { + "scene": "token-choice", + "branch": "fix/token-choice-morph-craft-20261028", + "status": "reviewed-awaiting-deployment", + "record": "docs/ai-learning/reviews/2026-10-28-token-choice-rework.md", + "productionChanged": false + } } } diff --git a/evals/browser/token-choice-review.html b/evals/browser/token-choice-review.html new file mode 100644 index 0000000..eef1593 --- /dev/null +++ b/evals/browser/token-choice-review.html @@ -0,0 +1,5 @@ +Token visual QA diff --git a/package.json b/package.json index 3e1cbe1..6323817 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 && node apps/site/learn/attention/model.test.mjs", + "test": "node evals/verify.mjs && node apps/site/learn/token-choice/model.test.mjs && node apps/site/learn/token-choice/geometry.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": {