Last active
February 16, 2026 04:43
-
-
Save kbouw/50a91c3f714fb5e40a15ae56b931b797 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Model Distillation</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { background: #111216; overflow-x: hidden; } | |
| #root { min-height: 100vh; } | |
| </style> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.9/babel.min.js"></script> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect, useRef, useCallback, useMemo } = React; | |
| // ============================================================ | |
| // DATA | |
| // ============================================================ | |
| const SCENARIOS = [ | |
| { | |
| id: "weather", | |
| prompt: "The weather today is", | |
| tokens: [ | |
| { word: "beautiful", prob: 0.28, group: "positive" }, | |
| { word: "warm", prob: 0.19, group: "positive" }, | |
| { word: "nice", prob: 0.14, group: "positive" }, | |
| { word: "perfect", prob: 0.11, group: "positive" }, | |
| { word: "cold", prob: 0.08, group: "negative" }, | |
| { word: "terrible", prob: 0.05, group: "negative" }, | |
| { word: "unpredictable", prob: 0.04, group: "neutral" }, | |
| { word: "humid", prob: 0.03, group: "neutral" }, | |
| ], | |
| }, | |
| { | |
| id: "code", | |
| prompt: "To fix this bug, you should", | |
| tokens: [ | |
| { word: "check", prob: 0.24, group: "action" }, | |
| { word: "update", prob: 0.18, group: "action" }, | |
| { word: "add", prob: 0.15, group: "action" }, | |
| { word: "remove", prob: 0.12, group: "action" }, | |
| { word: "try", prob: 0.10, group: "soft" }, | |
| { word: "consider", prob: 0.07, group: "soft" }, | |
| { word: "first", prob: 0.05, group: "ordering" }, | |
| { word: "carefully", prob: 0.03, group: "ordering" }, | |
| ], | |
| }, | |
| { | |
| id: "recipe", | |
| prompt: "Next, add the eggs and", | |
| tokens: [ | |
| { word: "mix", prob: 0.26, group: "action" }, | |
| { word: "stir", prob: 0.22, group: "action" }, | |
| { word: "whisk", prob: 0.18, group: "action" }, | |
| { word: "beat", prob: 0.12, group: "action" }, | |
| { word: "fold", prob: 0.07, group: "gentle" }, | |
| { word: "combine", prob: 0.06, group: "gentle" }, | |
| { word: "blend", prob: 0.05, group: "gentle" }, | |
| { word: "sugar", prob: 0.02, group: "ingredient" }, | |
| ], | |
| }, | |
| ]; | |
| // ============================================================ | |
| // UTILITY | |
| // ============================================================ | |
| function useSmoothed(targets, rate = 0.1) { | |
| const cur = useRef(targets.map(() => 0)); | |
| const [vals, setVals] = useState(targets); | |
| const frame = useRef(null); | |
| useEffect(() => { | |
| let on = true; | |
| const tick = () => { | |
| if (!on) return; | |
| let go = false; | |
| const n = cur.current.map((v, i) => { | |
| const d = (targets[i] || 0) - v; | |
| if (Math.abs(d) > 0.0005) { go = true; return v + d * rate; } | |
| return targets[i] || 0; | |
| }); | |
| cur.current = n; setVals([...n]); | |
| if (go) frame.current = requestAnimationFrame(tick); | |
| }; | |
| frame.current = requestAnimationFrame(tick); | |
| return () => { on = false; if (frame.current) cancelAnimationFrame(frame.current); }; | |
| }, [targets, rate]); | |
| return vals; | |
| } | |
| function useAnimVal(target, rate = 0.08) { | |
| const cur = useRef(0); | |
| const [val, setVal] = useState(0); | |
| const frame = useRef(null); | |
| useEffect(() => { | |
| let on = true; | |
| const tick = () => { | |
| if (!on) return; | |
| const d = target - cur.current; | |
| if (Math.abs(d) > 0.001) { | |
| cur.current += d * rate; | |
| setVal(cur.current); | |
| frame.current = requestAnimationFrame(tick); | |
| } else { cur.current = target; setVal(target); } | |
| }; | |
| frame.current = requestAnimationFrame(tick); | |
| return () => { on = false; if (frame.current) cancelAnimationFrame(frame.current); }; | |
| }, [target, rate]); | |
| return val; | |
| } | |
| // ============================================================ | |
| // VISUAL 1: THE PROBLEM | |
| // ============================================================ | |
| function TheProblem() { | |
| const [quality, setQuality] = useState(0.85); | |
| const [isDragging, setIsDragging] = useState(false); | |
| const trackRef = useRef(null); | |
| const cost = quality < 0.5 ? 0.05 + quality * 0.4 : 0.25 + (quality - 0.5) * 3.5; | |
| const latency = quality < 0.5 ? 80 + quality * 200 : 180 + (quality - 0.5) * 2400; | |
| const params = quality < 0.5 ? 1 + quality * 14 : 8 + (quality - 0.5) * 184; | |
| const distilledQuality = Math.min(0.97, quality + 0.12 + (1 - quality) * 0.15); | |
| const animCost = useAnimVal(cost, 0.1); | |
| const animLatency = useAnimVal(latency, 0.1); | |
| const animQ = useAnimVal(quality * 100, 0.1); | |
| const animDQ = useAnimVal(distilledQuality * 100, 0.1); | |
| const handleDrag = useCallback((cx) => { | |
| if (!trackRef.current) return; | |
| const r = trackRef.current.getBoundingClientRect(); | |
| const p = Math.max(0.1, Math.min(0.98, (cx - r.left) / r.width)); | |
| setQuality(Math.round(p * 100) / 100); | |
| }, []); | |
| useEffect(() => { | |
| if (!isDragging) return; | |
| const mv = (e) => handleDrag(e.touches ? e.touches[0].clientX : e.clientX); | |
| const up = () => setIsDragging(false); | |
| window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); | |
| window.addEventListener("touchmove", mv); window.addEventListener("touchend", up); | |
| return () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); window.removeEventListener("touchmove", mv); window.removeEventListener("touchend", up); }; | |
| }, [isDragging, handleDrag]); | |
| const modelName = quality < 0.35 ? "Tiny" : quality < 0.55 ? "Small" : quality < 0.75 ? "Medium" : quality < 0.9 ? "Large" : "Massive"; | |
| return ( | |
| <div> | |
| <div style={{ marginBottom: 24 }}> | |
| <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 8 }}> | |
| <span style={{ fontSize: 12, color: "#6a6e76" }}>Model size</span> | |
| <span style={{ fontSize: 13, color: "#c8cad0", fontFamily: "monospace" }}>{modelName} ({params.toFixed(0)}B params)</span> | |
| </div> | |
| <div ref={trackRef} style={{ position: "relative", height: 36, cursor: "pointer", touchAction: "none", userSelect: "none" }} | |
| onMouseDown={(e) => { setIsDragging(true); handleDrag(e.clientX); }} | |
| onTouchStart={(e) => { setIsDragging(true); handleDrag(e.touches[0].clientX); }}> | |
| <div style={{ position: "absolute", left: 0, right: 0, top: 14, height: 8, borderRadius: 4, background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.04)" }} /> | |
| <div style={{ position: "absolute", left: 0, top: 14, width: `${quality * 100}%`, height: 8, borderRadius: 4, background: "linear-gradient(90deg, #4ade80, #f0c674, #f07474)" }} /> | |
| <div style={{ | |
| position: "absolute", left: `${quality * 100}%`, top: 8, transform: "translateX(-50%)", | |
| width: 20, height: 20, borderRadius: "50%", background: "#e8e6e3", | |
| border: "2px solid rgba(0,0,0,0.3)", cursor: "grab", | |
| boxShadow: "0 2px 8px rgba(0,0,0,0.3)", | |
| }} /> | |
| <div style={{ position: "absolute", left: 4, top: 28, fontSize: 9, color: "#3a3e46" }}>faster, cheaper</div> | |
| <div style={{ position: "absolute", right: 4, top: 28, fontSize: 9, color: "#3a3e46" }}>smarter, slower</div> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", gap: 10, marginBottom: 20 }}> | |
| {[ | |
| { label: "Cost", value: `$${animCost.toFixed(2)}`, sub: "per 1K tokens", color: animCost > 1.0 ? "#f07474" : "#8a8f98" }, | |
| { label: "Latency", value: `${animLatency.toFixed(0)}ms`, sub: "per token", color: animLatency > 500 ? "#f0c674" : "#8a8f98" }, | |
| { label: "Quality", value: `${animQ.toFixed(1)}%`, sub: "benchmark", color: "#8a8f98" }, | |
| ].map((s) => ( | |
| <div key={s.label} style={{ flex: 1, padding: "10px 12px", background: "rgba(255,255,255,0.02)", borderRadius: 6, border: "1px solid rgba(255,255,255,0.04)" }}> | |
| <div style={{ fontSize: 10, color: "#4a4e56", textTransform: "uppercase", letterSpacing: "0.08em" }}>{s.label}</div> | |
| <div style={{ fontSize: 18, fontFamily: "monospace", color: s.color, fontWeight: 300 }}>{s.value}</div> | |
| <div style={{ fontSize: 9, color: "#3a3e46" }}>{s.sub}</div> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ padding: "14px 16px", background: "rgba(255,255,255,0.015)", borderRadius: 8, border: "1px solid rgba(255,255,255,0.04)" }}> | |
| <div style={{ fontSize: 11, color: "#5a5e66", marginBottom: 10, textTransform: "uppercase", letterSpacing: "0.06em" }}>Quality comparison at this cost/speed</div> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}> | |
| <div style={{ width: 100, fontSize: 11, color: "#6a6e76" }}>Trained normally</div> | |
| <div style={{ flex: 1, height: 16, borderRadius: 3, background: "rgba(255,255,255,0.03)", position: "relative", overflow: "hidden" }}> | |
| <div style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: `${animQ}%`, background: "rgba(140,180,255,0.4)", borderRadius: 3 }} /> | |
| </div> | |
| <div style={{ width: 44, fontSize: 12, fontFamily: "monospace", color: "#8a8f98", textAlign: "right" }}>{animQ.toFixed(1)}%</div> | |
| </div> | |
| <div style={{ display: "flex", alignItems: "center", gap: 10 }}> | |
| <div style={{ width: 100, fontSize: 11, color: "#4ade80" }}>Distilled</div> | |
| <div style={{ flex: 1, height: 16, borderRadius: 3, background: "rgba(255,255,255,0.03)", position: "relative", overflow: "hidden" }}> | |
| <div style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: `${animDQ}%`, background: "rgba(74,222,128,0.45)", borderRadius: 3 }} /> | |
| </div> | |
| <div style={{ width: 44, fontSize: 12, fontFamily: "monospace", color: "#4ade80", textAlign: "right" }}>{animDQ.toFixed(1)}%</div> | |
| </div> | |
| <div style={{ marginTop: 10, fontSize: 12, color: "#6a6e76", lineHeight: 1.6 }}> | |
| Same size, same speed, same cost. Distilled model scores <span style={{ color: "#4ade80" }}>+{(animDQ - animQ).toFixed(1)}%</span> higher by learning from a larger teacher. | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ============================================================ | |
| // VISUAL 2: WHAT THE BIG MODEL ACTUALLY KNOWS | |
| // ============================================================ | |
| function WhatItKnows() { | |
| const [scenarioIdx, setScenarioIdx] = useState(0); | |
| const [showFull, setShowFull] = useState(false); | |
| const scenario = SCENARIOS[scenarioIdx]; | |
| const hardProbs = scenario.tokens.map((t, i) => i === 0 ? 1.0 : 0.0); | |
| const softProbs = scenario.tokens.map((t) => t.prob); | |
| const displayProbs = showFull ? softProbs : hardProbs; | |
| const smoothed = useSmoothed(displayProbs, 0.12); | |
| const groupColors = { | |
| positive: "#4ade80", negative: "#f07474", neutral: "#94a3b8", | |
| action: "#60a5fa", soft: "#a78bfa", ordering: "#94a3b8", | |
| gentle: "#f0c674", ingredient: "#94a3b8", | |
| }; | |
| return ( | |
| <div> | |
| <div style={{ display: "flex", gap: 6, marginBottom: 20, flexWrap: "wrap" }}> | |
| {SCENARIOS.map((s, i) => ( | |
| <button key={s.id} onClick={() => { setScenarioIdx(i); setShowFull(false); }} style={{ | |
| padding: "7px 14px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| fontFamily: "'IBM Plex Sans', sans-serif", | |
| background: i === scenarioIdx ? "rgba(255,255,255,0.06)" : "rgba(255,255,255,0.02)", | |
| border: `1px solid ${i === scenarioIdx ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.04)"}`, | |
| color: i === scenarioIdx ? "#e8e6e3" : "#5a5e66", | |
| }}> | |
| {s.prompt}... | |
| </button> | |
| ))} | |
| </div> | |
| <div style={{ | |
| padding: "14px 18px", marginBottom: 20, background: "rgba(255,255,255,0.025)", | |
| borderRadius: 8, border: "1px solid rgba(255,255,255,0.05)", | |
| fontFamily: "monospace", fontSize: 15, color: "#c8cad0", | |
| }}> | |
| {scenario.prompt} <span style={{ color: "#4a4e56", animation: "blink 1.2s infinite" }}>▍</span> | |
| </div> | |
| <div style={{ display: "flex", gap: 8, marginBottom: 18, justifyContent: "center" }}> | |
| <button onClick={() => setShowFull(false)} style={{ | |
| padding: "9px 18px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: !showFull ? "rgba(140,180,255,0.12)" : "rgba(255,255,255,0.02)", | |
| border: `1px solid ${!showFull ? "rgba(140,180,255,0.25)" : "rgba(255,255,255,0.04)"}`, | |
| color: !showFull ? "#8cb4ff" : "#5a5e66", fontFamily: "'IBM Plex Sans', sans-serif", | |
| }}> | |
| Just the answer | |
| </button> | |
| <button onClick={() => setShowFull(true)} style={{ | |
| padding: "9px 18px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: showFull ? "rgba(74,222,128,0.12)" : "rgba(255,255,255,0.02)", | |
| border: `1px solid ${showFull ? "rgba(74,222,128,0.25)" : "rgba(255,255,255,0.04)"}`, | |
| color: showFull ? "#4ade80" : "#5a5e66", fontFamily: "'IBM Plex Sans', sans-serif", | |
| }}> | |
| Full distribution | |
| </button> | |
| </div> | |
| <div style={{ display: "flex", flexDirection: "column", gap: 5 }}> | |
| {scenario.tokens.map((token, i) => { | |
| const p = smoothed[i]; | |
| const vis = p > 0.015; | |
| const maxP = Math.max(...smoothed); | |
| const w = Math.max(p / Math.max(maxP, 0.01), 0.005) * 100; | |
| const isTop = i === 0; | |
| const color = showFull ? (groupColors[token.group] || "#8a8f98") : (isTop ? "rgba(140,180,255,0.6)" : "rgba(255,255,255,0.08)"); | |
| return ( | |
| <div key={token.word} style={{ | |
| display: "flex", alignItems: "center", gap: 10, height: 34, | |
| opacity: !showFull && i > 0 ? 0.2 : (vis ? 1 : 0.3), | |
| transition: "opacity 0.4s ease", | |
| }}> | |
| <div style={{ | |
| width: 90, fontSize: 13, fontFamily: "monospace", color: vis ? "#c8cad0" : "#3a3e46", | |
| textAlign: "right", transition: "color 0.3s", | |
| }}> | |
| "{token.word}" | |
| </div> | |
| <div style={{ flex: 1, position: "relative", height: 20, borderRadius: 3 }}> | |
| <div style={{ position: "absolute", inset: 0, background: "rgba(255,255,255,0.02)", borderRadius: 3, border: "1px solid rgba(255,255,255,0.03)" }} /> | |
| <div style={{ | |
| position: "absolute", left: 0, top: 0, bottom: 0, | |
| width: `${w}%`, background: color, borderRadius: 3, | |
| boxShadow: vis && showFull ? `0 0 12px ${color}33` : "none", | |
| }} /> | |
| </div> | |
| <div style={{ | |
| width: 44, fontSize: 12, fontFamily: "monospace", | |
| color: vis ? "#8a8f98" : "#2a2e36", textAlign: "right", | |
| }}> | |
| {(p * 100).toFixed(1)}% | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div style={{ | |
| marginTop: 18, padding: "12px 16px", | |
| background: showFull ? "rgba(74,222,128,0.04)" : "rgba(140,180,255,0.04)", | |
| borderRadius: 6, | |
| border: `1px solid ${showFull ? "rgba(74,222,128,0.1)" : "rgba(140,180,255,0.1)"}`, | |
| borderLeft: `3px solid ${showFull ? "rgba(74,222,128,0.3)" : "rgba(140,180,255,0.3)"}`, | |
| transition: "all 0.3s ease", | |
| }}> | |
| <p style={{ fontSize: 13, color: "#a0a4ac", lineHeight: 1.6, margin: 0 }}> | |
| {showFull | |
| ? <>The big model considered all of these as possible next words. "{scenario.tokens[0].word}" won, but "{scenario.tokens[1].word}" and "{scenario.tokens[2].word}" were close. That ranking IS the knowledge. A student model trained on this distribution learns which words are interchangeable and which aren't, not just which one was picked.</> | |
| : <>If you only record the final answer, all you know is "{scenario.tokens[0].word}" was chosen. Every other option the model considered, and how it ranked them, is thrown away. This is what normal training data looks like.</> | |
| } | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ============================================================ | |
| // VISUAL 3: THE TRANSFER | |
| // ============================================================ | |
| function TheTransfer() { | |
| const scenario = SCENARIOS[0]; | |
| const [mode, setMode] = useState("hard"); | |
| const [epoch, setEpoch] = useState(0); | |
| const [running, setRunning] = useState(false); | |
| const [studentProbs, setStudentProbs] = useState(scenario.tokens.map(() => 1 / scenario.tokens.length)); | |
| const iv = useRef(null); | |
| const teacherProbs = scenario.tokens.map((t) => t.prob); | |
| const hardTarget = scenario.tokens.map((t, i) => i === 0 ? 1.0 : 0.0); | |
| const target = mode === "soft" ? teacherProbs : hardTarget; | |
| const smooth = useSmoothed(studentProbs, 0.15); | |
| const similarity = 1 - Math.sqrt(studentProbs.reduce((s, p, i) => s + (p - teacherProbs[i]) ** 2, 0) / studentProbs.length); | |
| const matchPct = Math.max(0, Math.min(100, similarity * 100)); | |
| const step = useCallback(() => { | |
| setStudentProbs((prev) => { | |
| const lr = 0.15; | |
| const logits = prev.map((p) => Math.log(Math.max(p, 1e-8))); | |
| const newLogits = logits.map((l, i) => l - lr * (prev[i] - target[i])); | |
| const maxL = Math.max(...newLogits); | |
| const exps = newLogits.map((l) => Math.exp(l - maxL)); | |
| const sum = exps.reduce((a, b) => a + b, 0); | |
| return exps.map((e) => e / sum); | |
| }); | |
| setEpoch((e) => e + 1); | |
| }, [target]); | |
| const reset = useCallback(() => { | |
| setRunning(false); | |
| setEpoch(0); | |
| setStudentProbs(scenario.tokens.map(() => 1 / scenario.tokens.length)); | |
| if (iv.current) clearInterval(iv.current); | |
| }, [scenario]); | |
| useEffect(() => { | |
| if (running) iv.current = setInterval(step, 160); | |
| else if (iv.current) clearInterval(iv.current); | |
| return () => { if (iv.current) clearInterval(iv.current); }; | |
| }, [running, step]); | |
| useEffect(() => { reset(); }, [mode]); | |
| const maxP = Math.max(...smooth); | |
| return ( | |
| <div> | |
| <div style={{ marginBottom: 20, textAlign: "center" }}> | |
| <div style={{ fontSize: 12, color: "#6a6e76", marginBottom: 10 }}>Train the student model on:</div> | |
| <div style={{ display: "flex", gap: 8, justifyContent: "center" }}> | |
| <button onClick={() => setMode("hard")} style={{ | |
| padding: "9px 18px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: mode === "hard" ? "rgba(140,180,255,0.12)" : "rgba(255,255,255,0.02)", | |
| border: `1px solid ${mode === "hard" ? "rgba(140,180,255,0.25)" : "rgba(255,255,255,0.04)"}`, | |
| color: mode === "hard" ? "#8cb4ff" : "#5a5e66", fontFamily: "'IBM Plex Sans', sans-serif", | |
| }}>Just answers</button> | |
| <button onClick={() => setMode("soft")} style={{ | |
| padding: "9px 18px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: mode === "soft" ? "rgba(74,222,128,0.12)" : "rgba(255,255,255,0.02)", | |
| border: `1px solid ${mode === "soft" ? "rgba(74,222,128,0.25)" : "rgba(255,255,255,0.04)"}`, | |
| color: mode === "soft" ? "#4ade80" : "#5a5e66", fontFamily: "'IBM Plex Sans', sans-serif", | |
| }}>Teacher's full distribution</button> | |
| </div> | |
| </div> | |
| <div style={{ display: "flex", gap: 8, justifyContent: "center", marginBottom: 16 }}> | |
| <button onClick={() => setRunning(!running)} style={{ | |
| padding: "8px 18px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: running ? "rgba(240,116,116,0.12)" : "rgba(100,200,140,0.12)", | |
| border: `1px solid ${running ? "rgba(240,116,116,0.25)" : "rgba(100,200,140,0.25)"}`, | |
| color: running ? "#f07474" : "#64c88c", fontFamily: "monospace", | |
| }}>{running ? "⏸ pause" : "▶ train"}</button> | |
| <button onClick={reset} style={{ | |
| padding: "8px 18px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.06)", | |
| color: "#8a8f98", fontFamily: "monospace", | |
| }}>↺ reset</button> | |
| </div> | |
| <div style={{ display: "flex", gap: 12, justifyContent: "center", marginBottom: 16, alignItems: "center" }}> | |
| <span style={{ fontSize: 11, color: "#4a4e56", fontFamily: "monospace" }}>step {epoch}</span> | |
| <div style={{ width: 140, height: 6, borderRadius: 3, background: "rgba(255,255,255,0.04)", overflow: "hidden" }}> | |
| <div style={{ | |
| width: `${matchPct}%`, height: "100%", borderRadius: 3, | |
| background: matchPct > 85 ? "#4ade80" : matchPct > 50 ? "#f0c674" : "#64a0ff", | |
| transition: "width 0.15s, background 0.3s", | |
| }} /> | |
| </div> | |
| <span style={{ fontSize: 11, fontFamily: "monospace", color: matchPct > 85 ? "#4ade80" : "#8a8f98" }}> | |
| {matchPct.toFixed(0)}% match to teacher | |
| </span> | |
| </div> | |
| <div style={{ | |
| padding: "10px 14px", marginBottom: 14, background: "rgba(255,255,255,0.02)", | |
| borderRadius: 6, border: "1px solid rgba(255,255,255,0.04)", | |
| fontFamily: "monospace", fontSize: 13, color: "#8a8f98", | |
| }}> | |
| {scenario.prompt} <span style={{ color: "#4a4e56" }}>▍</span> | |
| </div> | |
| <div style={{ display: "flex", flexDirection: "column", gap: 4 }}> | |
| {scenario.tokens.map((token, i) => { | |
| const p = smooth[i]; | |
| const vis = p > 0.015; | |
| const w = Math.max(p / Math.max(maxP, 0.01), 0.005) * 100; | |
| const tw = Math.max(teacherProbs[i] / Math.max(...teacherProbs), 0.005) * 100; | |
| return ( | |
| <div key={token.word} style={{ display: "flex", alignItems: "center", gap: 10, height: 32 }}> | |
| <div style={{ width: 80, fontSize: 12, fontFamily: "monospace", color: vis ? "#c8cad0" : "#3a3e46", textAlign: "right" }}> | |
| "{token.word}" | |
| </div> | |
| <div style={{ flex: 1, position: "relative", height: 18, borderRadius: 3 }}> | |
| <div style={{ position: "absolute", inset: 0, background: "rgba(255,255,255,0.02)", borderRadius: 3, border: "1px solid rgba(255,255,255,0.03)" }} /> | |
| <div style={{ | |
| position: "absolute", left: 0, top: 0, bottom: 0, | |
| width: `${tw}%`, borderRadius: 3, | |
| border: "1px dashed rgba(74,222,128,0.2)", | |
| background: "transparent", | |
| }} /> | |
| <div style={{ | |
| position: "absolute", left: 0, top: 0, bottom: 0, | |
| width: `${w}%`, borderRadius: 3, | |
| background: mode === "soft" | |
| ? `rgba(74,222,128,${0.25 + p * 0.75})` | |
| : `rgba(140,180,255,${0.25 + p * 0.75})`, | |
| }} /> | |
| </div> | |
| <div style={{ width: 40, fontSize: 11, fontFamily: "monospace", color: vis ? "#8a8f98" : "#2a2e36", textAlign: "right" }}> | |
| {(p * 100).toFixed(1)} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div style={{ display: "flex", gap: 16, justifyContent: "center", marginTop: 12, fontSize: 10, color: "#4a4e56" }}> | |
| <span> | |
| <span style={{ display: "inline-block", width: 12, height: 8, borderRadius: 2, background: mode === "soft" ? "rgba(74,222,128,0.5)" : "rgba(140,180,255,0.5)", marginRight: 4, verticalAlign: "middle" }} /> | |
| student | |
| </span> | |
| <span> | |
| <span style={{ display: "inline-block", width: 12, height: 8, borderRadius: 2, border: "1px dashed rgba(74,222,128,0.3)", marginRight: 4, verticalAlign: "middle" }} /> | |
| teacher target | |
| </span> | |
| </div> | |
| <div style={{ | |
| marginTop: 16, padding: "12px 16px", | |
| background: mode === "soft" ? "rgba(74,222,128,0.04)" : "rgba(140,180,255,0.04)", | |
| borderRadius: 6, | |
| border: `1px solid ${mode === "soft" ? "rgba(74,222,128,0.1)" : "rgba(140,180,255,0.1)"}`, | |
| borderLeft: `3px solid ${mode === "soft" ? "rgba(74,222,128,0.3)" : "rgba(140,180,255,0.3)"}`, | |
| }}> | |
| <p style={{ fontSize: 13, color: "#a0a4ac", lineHeight: 1.6, margin: 0 }}> | |
| {mode === "hard" | |
| ? <>Training on just the final answer, the student learns that "beautiful" is correct but nothing about how "warm" and "nice" are also good options. Watch how it converges to a spike on one word. Switch to the teacher's full distribution to see the difference.</> | |
| : <>The student learns the full ranking: "beautiful" is most likely, "warm" and "nice" are close behind, "terrible" is unlikely. Try running both modes and comparing how closely the student matches the teacher's dashed outline.</> | |
| } | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // ============================================================ | |
| // MAIN — with MCP header style applied | |
| // ============================================================ | |
| function App() { | |
| const [activeVisual, setActiveVisual] = useState(0); | |
| const visuals = [ | |
| { | |
| id: "problem", num: "01", title: "The Problem", | |
| subtitle: "Bigger models are smarter but cost more. Can we close the gap?", | |
| component: <TheProblem />, | |
| }, | |
| { | |
| id: "knowledge", num: "02", title: "What the Big Model Knows", | |
| subtitle: "When a model generates text, it ranks every possible next word.", | |
| component: <WhatItKnows />, | |
| }, | |
| { | |
| id: "transfer", num: "03", title: "The Transfer", | |
| subtitle: "Train the small model on rankings instead of answers. Watch what happens.", | |
| component: <TheTransfer />, | |
| }, | |
| ]; | |
| return ( | |
| <div style={{ | |
| minHeight: "100vh", background: "#111216", color: "#e8e6e3", | |
| fontFamily: "'IBM Plex Sans', -apple-system, sans-serif", padding: "40px 16px", | |
| }}> | |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet" /> | |
| <style>{"@keyframes blink { 0%,50% { opacity: 1 } 51%,100% { opacity: 0 } }"}</style> | |
| {/* ---- MCP-style header: left-aligned with label, title, subtext ---- */} | |
| <div style={{ maxWidth: 640, margin: "0 auto 32px" }}> | |
| <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 11, color: "#4ade80", textTransform: "uppercase", letterSpacing: "0.12em", marginBottom: 8 }}> | |
| Interactive Explainer | |
| </div> | |
| <h1 style={{ fontSize: 28, fontWeight: 600, margin: "0 0 8px", lineHeight: 1.3 }}> | |
| Model Distillation | |
| </h1> | |
| <p style={{ fontSize: 15, color: "#6a6e76", margin: 0, lineHeight: 1.5 }}> | |
| How a small model learns from a large one. | |
| </p> | |
| </div> | |
| <div style={{ maxWidth: 640, margin: "0 auto 28px", display: "flex", gap: 6 }}> | |
| {visuals.map((v, i) => ( | |
| <button key={v.id} onClick={() => setActiveVisual(i)} style={{ | |
| flex: 1, padding: "10px 8px", borderRadius: 8, cursor: "pointer", | |
| background: i === activeVisual ? "rgba(255,255,255,0.05)" : "rgba(255,255,255,0.015)", | |
| border: `1px solid ${i === activeVisual ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.03)"}`, | |
| textAlign: "left", transition: "all 0.2s", | |
| }}> | |
| <div style={{ fontSize: 10, color: i === activeVisual ? "#4ade80" : "#3a3e46", fontFamily: "monospace", marginBottom: 2 }}>{v.num}</div> | |
| <div style={{ fontSize: 12, color: i === activeVisual ? "#e8e6e3" : "#5a5e66", fontWeight: 500 }}>{v.title}</div> | |
| </button> | |
| ))} | |
| </div> | |
| <div style={{ | |
| maxWidth: 640, margin: "0 auto", | |
| padding: "24px", background: "rgba(255,255,255,0.015)", | |
| borderRadius: 12, border: "1px solid rgba(255,255,255,0.04)", | |
| }}> | |
| <div style={{ marginBottom: 20 }}> | |
| <div style={{ fontSize: 11, color: "#4ade80", fontFamily: "monospace", marginBottom: 4 }}> | |
| {visuals[activeVisual].num} | |
| </div> | |
| <div style={{ fontSize: 18, fontWeight: 400, color: "#e8e6e3", marginBottom: 4 }}> | |
| {visuals[activeVisual].title} | |
| </div> | |
| <div style={{ fontSize: 13, color: "#6a6e76", lineHeight: 1.5 }}> | |
| {visuals[activeVisual].subtitle} | |
| </div> | |
| </div> | |
| {visuals[activeVisual].component} | |
| </div> | |
| {activeVisual < 2 && ( | |
| <div style={{ maxWidth: 640, margin: "16px auto 0", textAlign: "center" }}> | |
| <button onClick={() => setActiveVisual(activeVisual + 1)} style={{ | |
| padding: "10px 20px", fontSize: 12, borderRadius: 6, cursor: "pointer", | |
| background: "rgba(74,222,128,0.08)", border: "1px solid rgba(74,222,128,0.15)", | |
| color: "#4ade80", fontFamily: "'IBM Plex Sans', sans-serif", | |
| }}> | |
| Next: {visuals[activeVisual + 1].title} → | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById("root")).render(<App />); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment