|
| 1 | +// Grounding: arXiv:2609.06986 Sections 1 and 3; official source README.md, |
| 2 | +// data/*/manifest.json, evaluate.py, and analysis/summarize.py. |
| 3 | +import { CAMERA_HOME, Camera, Timeline, cameraInterp, colors, ease } from '../../core'; |
| 4 | +import type { CameraState, SceneState } from '../../core'; |
| 5 | + |
| 6 | +const clamp01 = (v: number) => Math.max(0, Math.min(1, v)); |
| 7 | +const TASKS = Array.from({ length: 100 }, (_, i) => ({ i, x: 106 + i * 10.7 })); |
| 8 | +const DATASETS = [ |
| 9 | + { label: 'Symbol-QA', detail: '100 × 100 arbitrary associations', color: colors.ACCENT }, |
| 10 | + { label: 'LLM-QA', detail: '100 × 100 fictional facts', color: colors.SECONDARY }, |
| 11 | + { label: 'Real-QA', detail: '100 × 50 filtered questions', color: colors.POSITIVE }, |
| 12 | +]; |
| 13 | + |
| 14 | +export function buildScene() { |
| 15 | + const tl = new Timeline(); |
| 16 | + const cam = tl.channel<CameraState>('camera', CAMERA_HOME, cameraInterp); |
| 17 | + const memoryU = tl.channel('first memory', 0); |
| 18 | + const tapeU = tl.channel('hundred task tape', 0); |
| 19 | + const overwriteU = tl.channel('sequential overwrite', 0); |
| 20 | + const datasetsU = tl.channel('three datasets', 0); |
| 21 | + const noIdU = tl.channel('no task identifier', 0); |
| 22 | + const foldU = tl.channel('tape folds into matrix', 0); |
| 23 | + const matrixU = tl.channel('temporal accuracy matrix', 0); |
| 24 | + const diagonalU = tl.channel('immediate acquisition diagonal', 0); |
| 25 | + const finalU = tl.channel('final retention row', 0); |
| 26 | + const metricU = tl.channel('paper metrics', 0); |
| 27 | + const closeU = tl.channel('closing question', 0); |
| 28 | + |
| 29 | + tl.caption({ at: 0.4, dur: 5.5, text: 'Teach a language model one new answer, then keep teaching. The first memory now has ninety-nine chances to disappear.' }); |
| 30 | + tl.tween(memoryU, 1, { at: 0.8, dur: 0.7, ease: ease.pop }); |
| 31 | + tl.tween(tapeU, 0.12, { at: 1.6, dur: 1.5, ease: ease.draw }); |
| 32 | + tl.tween(cam, { x: 300, y: 300, k: 1.08 }, { at: 2.0, dur: 1.2, ease: ease.move }); |
| 33 | + |
| 34 | + tl.caption({ at: 6.4, dur: 5.5, text: 'The paper turns that worry into a one-hundred-task memorization setting, learned in sequence with no old examples kept.' }); |
| 35 | + tl.tween(tapeU, 1, { at: 6.9, dur: 4.0, ease: ease.linear }); |
| 36 | + tl.tween(overwriteU, 1, { at: 8.0, dur: 3.0, ease: ease.linear }); |
| 37 | + tl.tween(cam, CAMERA_HOME, { at: 9.8, dur: 1.2, ease: ease.move }); |
| 38 | + |
| 39 | + tl.caption({ at: 12.4, dur: 5.5, text: 'The model receives only the question at inference. No task label tells it which chapter of its past to search.' }); |
| 40 | + tl.tween(noIdU, 1, { at: 12.9, dur: 1.0, ease: ease.enter }); |
| 41 | + |
| 42 | + tl.caption({ at: 18.4, dur: 5.5, text: 'Three streams make the test harder to fake: random symbols, invented facts, and natural questions the base model could not answer.' }); |
| 43 | + tl.tween(datasetsU, 3, { at: 18.9, dur: 2.0, ease: ease.enter }); |
| 44 | + tl.tween(cam, { x: 640, y: 350, k: 1.06 }, { at: 19.5, dur: 1.2, ease: ease.move }); |
| 45 | + |
| 46 | + tl.caption({ at: 24.4, dur: 5.5, text: 'After every task, evaluation revisits the whole learned stream. The tape folds into a temporal accuracy matrix.' }); |
| 47 | + tl.tween(datasetsU, 0, { at: 24.7, dur: 0.6, ease: ease.enter }); |
| 48 | + tl.tween(foldU, 1, { at: 25.0, dur: 1.4, ease: ease.move }); |
| 49 | + tl.tween(matrixU, 1, { at: 26.1, dur: 2.2, ease: ease.draw }); |
| 50 | + tl.tween(cam, { x: 640, y: 350, k: 1.05 }, { at: 26.2, dur: 1.2, ease: ease.move }); |
| 51 | + |
| 52 | + tl.caption({ at: 30.4, dur: 5.5, text: 'Each new row asks what the current checkpoint remembers about every task it has already seen.' }); |
| 53 | + tl.tween(diagonalU, 1, { at: 31.0, dur: 1.6, ease: ease.enter }); |
| 54 | + |
| 55 | + tl.caption({ at: 36.4, dur: 5.5, text: 'The diagonal measures immediate acquisition. Bright cells there only prove that each answer was learned once.' }); |
| 56 | + tl.tween(cam, { x: 600, y: 350, k: 1.08 }, { at: 37.0, dur: 1.2, ease: ease.move }); |
| 57 | + |
| 58 | + tl.caption({ at: 42.4, dur: 5.5, text: 'The final row is the expensive claim. Its mean is final retention after the hundredth update.' }); |
| 59 | + tl.tween(finalU, 1, { at: 42.9, dur: 1.3, ease: ease.pop }); |
| 60 | + tl.tween(cam, { x: 640, y: 360, k: 1.08 }, { at: 43.0, dur: 1.2, ease: ease.move }); |
| 61 | + |
| 62 | + tl.caption({ at: 48.4, dur: 5.5, text: 'The same matrix yields forgetting by comparing each task at its best moment with that last row.' }); |
| 63 | + tl.tween(metricU, 1, { at: 49.0, dur: 1.5, ease: ease.enter }); |
| 64 | + tl.tween(cam, CAMERA_HOME, { at: 51.0, dur: 1.2, ease: ease.move }); |
| 65 | + |
| 66 | + tl.caption({ at: 54.4, dur: 5.5, text: 'Naive sequential fine-tuning leaves almost nothing at this horizon. One mechanism at a time is not enough.' }); |
| 67 | + tl.tween(overwriteU, 1.7, { at: 55.0, dur: 1.5, ease: ease.move }); |
| 68 | + |
| 69 | + tl.caption({ at: 60.4, dur: 5.8, text: 'So the paper asks a sharper question: which protections preserve different parts of memory, and what happens when they work together?' }); |
| 70 | + tl.tween(closeU, 1, { at: 61.0, dur: 1.3, ease: ease.move }); |
| 71 | + tl.hold(66.2, 1.0); |
| 72 | + |
| 73 | + return { tl, cam, memoryU, tapeU, overwriteU, datasetsU, noIdU, foldU, matrixU, diagonalU, finalU, metricU, closeU }; |
| 74 | +} |
| 75 | + |
| 76 | +const scene = buildScene(); |
| 77 | + |
| 78 | +function MemoryCard({ u, fade }: { u: number; fade: number }) { |
| 79 | + return <g transform={`translate(118 112) scale(${0.86 + 0.14 * u})`} opacity={u}> |
| 80 | + <rect width="262" height="92" rx="24" fill="#151f31" stroke={colors.WARM} strokeWidth="3" /> |
| 81 | + <text x="22" y="32" fill={colors.MUTED} fontSize="12" fontFamily={colors.font.mono}>Question: oHBvRP</text> |
| 82 | + <text x="22" y="65" fill={colors.WARM} fontSize="18" fontWeight="800" fontFamily={colors.font.mono} opacity={fade}>Answer: OIvG</text> |
| 83 | + </g>; |
| 84 | +} |
| 85 | + |
| 86 | +export function Render({ s }: { s: SceneState }) { |
| 87 | + const close = s.get(scene.closeU); |
| 88 | + const tape = s.get(scene.tapeU); |
| 89 | + const overwrite = s.get(scene.overwriteU); |
| 90 | + const fold = s.get(scene.foldU); |
| 91 | + const matrix = s.get(scene.matrixU); |
| 92 | + const diagonal = s.get(scene.diagonalU); |
| 93 | + const final = s.get(scene.finalU); |
| 94 | + const memoryFade = Math.max(0.08, 1 - 0.82 * clamp01(overwrite)); |
| 95 | + const tapeFade = 1 - fold; |
| 96 | + |
| 97 | + return <Camera {...s.get(scene.cam)}> |
| 98 | + <g opacity={1 - close}> |
| 99 | + <text x="640" y="60" textAnchor="middle" fill={colors.TEXT} fontSize="34" fontWeight="850">the hundred-task horizon</text> |
| 100 | + <g opacity={tapeFade}> |
| 101 | + <MemoryCard u={s.get(scene.memoryU)} fade={memoryFade} /> |
| 102 | + <path d="M112 292 H1172" stroke={colors.GRID} strokeWidth="7" strokeLinecap="round" /> |
| 103 | + {TASKS.map((task) => { |
| 104 | + const u = clamp01(tape * 100 - task.i); |
| 105 | + const old = task.i === 0; |
| 106 | + return <g key={task.i} opacity={u}> |
| 107 | + <rect x={task.x} y={270} width="7" height={old ? 44 : 28} rx="3.5" fill={old ? colors.WARM : colors.ACCENT} opacity={old ? memoryFade : 0.25 + 0.55 * u} /> |
| 108 | + {(task.i === 0 || task.i === 9 || task.i === 49 || task.i === 99) && <text x={task.x + 3.5} y="335" textAnchor="middle" fill={old ? colors.WARM : colors.MUTED} fontSize="10">{task.i + 1}</text>} |
| 109 | + </g>; |
| 110 | + })} |
| 111 | + <g opacity={s.get(scene.noIdU)}> |
| 112 | + <rect x="432" y="158" width="416" height="70" rx="22" fill="#251823" stroke={colors.NEGATIVE} strokeWidth="2.5" /> |
| 113 | + <text x="640" y="187" textAnchor="middle" fill={colors.NEGATIVE} fontSize="14" fontWeight="800">NO TASK IDENTIFIER AT INFERENCE</text> |
| 114 | + <text x="640" y="211" textAnchor="middle" fill={colors.MUTED} fontSize="12">one question must identify one answer globally</text> |
| 115 | + </g> |
| 116 | + <g> |
| 117 | + {DATASETS.map((d, i) => { |
| 118 | + const u = clamp01(s.get(scene.datasetsU) - i); |
| 119 | + return <g key={d.label} transform={`translate(${152 + i * 360} 430)`} opacity={u}> |
| 120 | + <rect width="312" height="106" rx="26" fill="#101827" stroke={d.color} strokeWidth="2.5" /> |
| 121 | + <circle cx="38" cy="53" r="19" fill={d.color} opacity="0.28" /> |
| 122 | + <text x="72" y="44" fill={d.color} fontSize="17" fontWeight="800">{d.label}</text> |
| 123 | + <text x="72" y="70" fill={colors.MUTED} fontSize="12">{d.detail}</text> |
| 124 | + </g>; |
| 125 | + })} |
| 126 | + </g> |
| 127 | + </g> |
| 128 | + |
| 129 | + <g opacity={matrix} transform={`translate(${fold * 135} ${fold * 8})`}> |
| 130 | + <text x="368" y="116" fill={colors.MUTED} fontSize="12" fontFamily={colors.font.mono}>evaluate.py → accuracy_matrix</text> |
| 131 | + {Array.from({ length: 10 }, (_, row) => Array.from({ length: 10 }, (_, col) => { |
| 132 | + if (col > row) return null; |
| 133 | + const idx = row * 10 + col; |
| 134 | + const u = clamp01(matrix * 100 - idx); |
| 135 | + const isDiag = row === col; |
| 136 | + const isFinal = row === 9; |
| 137 | + const x = 338 + col * 48; |
| 138 | + const y = 142 + row * 42; |
| 139 | + const dim = isDiag ? 0.48 + 0.52 * diagonal : isFinal ? 0.24 + 0.76 * final : 0.22; |
| 140 | + const color = isDiag ? colors.ACCENT : isFinal ? colors.WARM : colors.MUTED; |
| 141 | + return <rect key={`${row}-${col}`} x={x} y={y} width="38" height="32" rx="7" fill={color} opacity={u * dim} stroke={color} strokeWidth={isFinal && final > 0 ? 2.5 : 0.5} />; |
| 142 | + }))} |
| 143 | + <text x="590" y="590" textAnchor="middle" fill={colors.WARM} fontSize="13" opacity={final}>final row → mean final retention</text> |
| 144 | + <text x="826" y="340" textAnchor="middle" fill={colors.ACCENT} fontSize="12" transform="rotate(-90 826 340)" opacity={diagonal}>diagonal → immediate acquisition</text> |
| 145 | + <g opacity={s.get(scene.metricU)}> |
| 146 | + <rect x="865" y="190" width="282" height="226" rx="30" fill="#101827" stroke={colors.SECONDARY} strokeWidth="2.5" /> |
| 147 | + <text x="1006" y="232" textAnchor="middle" fill={colors.TEXT} fontSize="18" fontWeight="800">the matrix answers</text> |
| 148 | + {['final retention', 'immediate acquisition', 'forgetting', 'forward transfer'].map((label, i) => <g key={label} transform={`translate(902 ${270 + i * 34})`}> |
| 149 | + <circle r="6" fill={[colors.WARM, colors.ACCENT, colors.NEGATIVE, colors.POSITIVE][i]} /> |
| 150 | + <text x="18" y="5" fill={colors.MUTED} fontSize="13">{label}</text> |
| 151 | + </g>)} |
| 152 | + </g> |
| 153 | + </g> |
| 154 | + </g> |
| 155 | + |
| 156 | + <g opacity={close}> |
| 157 | + <rect x="180" y="122" width="920" height="444" rx="54" fill={colors.BG} stroke={colors.WARM} strokeWidth="4" /> |
| 158 | + <MemoryCard u={1} fade={0.16} /> |
| 159 | + <text x="640" y="252" textAnchor="middle" fill={colors.TEXT} fontSize="46" fontWeight="880">one memory · one hundred updates</text> |
| 160 | + <path d="M286 360 H994" stroke={colors.GRID} strokeWidth="8" strokeLinecap="round" /> |
| 161 | + {['data', 'function', 'weight', 'allocation'].map((label, i) => <g key={label} transform={`translate(${330 + i * 205} 360)`}> |
| 162 | + <circle r="42" fill="#101827" stroke={[colors.ACCENT, colors.SECONDARY, colors.WARM, colors.POSITIVE][i]} strokeWidth="4" /> |
| 163 | + <text y="5" textAnchor="middle" fill={colors.TEXT} fontSize="12">{label}</text> |
| 164 | + </g>)} |
| 165 | + <text x="640" y="474" textAnchor="middle" fill={colors.MUTED} fontSize="16">which protections compose?</text> |
| 166 | + </g> |
| 167 | + </Camera>; |
| 168 | +} |
| 169 | + |
| 170 | +export const vizScene = () => scene; |
0 commit comments