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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/site/learn/token-choice/LESSON.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
149 changes: 123 additions & 26 deletions apps/site/learn/token-choice/SCENE_SPEC.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
Expand All @@ -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",
Expand All @@ -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": [
"四个词构成完整的教学词表;不是从真实模型词表截取的前四项,也不是线上模型输出",
Expand All @@ -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 数学结果一致",
Expand All @@ -75,5 +169,8 @@
"播放、暂停、继续、拖动、参数改变、再次抽样、重播及完成后的配置恢复均可观察验证",
"保留单一焦点;未讲到的计算与参数不会同时占据主视觉"
],
"untested": ["真实手机硬件性能与所有浏览器组合", "独立的学习效果实验"]
"untested": [
"真实手机硬件性能与所有浏览器组合",
"独立的学习效果实验"
]
}
4 changes: 2 additions & 2 deletions apps/site/learn/token-choice/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -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();
27 changes: 27 additions & 0 deletions apps/site/learn/token-choice/geometry.mjs
Original file line number Diff line number Diff line change
@@ -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};
}
59 changes: 59 additions & 0 deletions apps/site/learn/token-choice/geometry.test.mjs
Original file line number Diff line number Diff line change
@@ -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)<t,`${a} != ${b}`);
let checked=0;
for(const mobile of [false,true])for(const temperature of [.4,.8,1,1.6,2])for(const reducedMotion of [false,true]){
const states=[];
for(let tick=0;tick<=180;tick++){
const time=27+tick/20,frame={...sample(time,{temperature}),reducedMotion};
const g=chartBandGeometry(frame,mobile);states.push(g);
g.candidates.forEach((c,i)=>{
assert.ok(c.w>0&&c.h>0);
close(g.dock.y,c.y+c.h+1);assert.ok(g.dock.x<c.x&&g.dock.end>c.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;i<states.length;i++){
const a=states[i-1].candidates[0].focus,b=states[i].candidates[0].focus;
assert.ok(Math.hypot(a.x-b.x,a.y-b.y)<8,'annotation must not jump halfway through morph');
}
}
// Explicit regression: floor's 20.6% remains paired with floor at 32 seconds.
const mid=chartBandGeometry(sample(32),false),floor=mid.candidates[1];
assert.ok(floor.valueY>mid.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.');
Loading
Loading