lm-playground-api / web /chapters /interpretability.html
deploy
Deploy LM Playground API
f0a527a
Raw
History Blame Contribute Delete
33.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>30. Interpretability β€” LM Playground</title>
<link rel="stylesheet" href="/platform/platform.css"/>
<script type="module" src="/platform/platform.js"></script>
<style>
:root{
--bg:#0f1117; --panel:#181b24; --panel-2:#1f2330; --border:#2a2f3d;
--text:#e6e8ee; --muted:#9aa3b2; --accent:#7c8cff; --accent-2:#5be0c0;
--good:#5be08a; --warn:#ffc06b; --bad:#ff9090;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0 20px 80px;background:radial-gradient(1200px 600px at 50% -10%,#1a1f2e 0%,var(--bg) 55%);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.5;-webkit-font-smoothing:antialiased}
header{max-width:980px;margin:0 auto;padding:34px 0 0}
.header-inner{display:flex;justify-content:space-between;align-items:flex-start;padding-bottom:12px}
h1{font-size:30px;margin:0 0 6px;letter-spacing:-.5px}
.sub{color:var(--muted);max-width:780px;margin:0;font-size:14.5px;line-height:1.6}
.sub b{color:var(--text);font-weight:600}
.page-tabs{display:flex;border-bottom:1px solid var(--border)}
.page-tab{background:none;border:none;color:var(--muted);padding:11px 22px 10px;font-size:14px;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:.12s}
.page-tab:hover{color:var(--text)}.page-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.tab-panel{display:none}.tab-panel.active{display:block}
.guide{max-width:980px;margin:0 auto;padding:30px 0 80px;display:grid;grid-template-columns:1fr 220px;gap:0 48px}
.guide-toc{grid-column:2;grid-row:1/20;position:sticky;top:64px;align-self:start;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px}
.toc-label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);font-weight:700;margin-bottom:6px}
.guide-toc a{color:var(--muted);text-decoration:none;font-size:12.5px;padding:5px 8px;border-radius:6px;transition:.1s}
.guide-toc a:hover{color:var(--text);background:var(--panel-2)}
.chapter{grid-column:1;padding:0 0 48px;border-bottom:1px solid var(--border);margin-bottom:48px}
.chapter:last-of-type{border-bottom:none}
.chapter h2{font-size:22px;margin:0 0 18px;display:flex;align-items:center;gap:12px;letter-spacing:-.3px}
.ch-num{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:99px;background:var(--accent-2);color:#0b0d14;font-size:14px;font-weight:800;flex-shrink:0}
.chapter h3{font-size:15px;margin:24px 0 8px;color:var(--accent-2);font-weight:700}
.chapter p{margin:0 0 14px;line-height:1.7;color:#ced3de;font-size:15px}
.chapter strong{color:var(--text)}.chapter em{font-style:italic}
code{background:var(--panel-2);border:1px solid var(--border);border-radius:5px;padding:1px 5px;font-family:var(--mono);font-size:12.5px;color:var(--accent-2)}
.callout{border-radius:10px;padding:14px 16px;margin:20px 0;font-size:14px;line-height:1.65;color:#ced3de}
.callout strong{display:block;margin-bottom:5px;font-size:12px;text-transform:uppercase;letter-spacing:.6px}
.callout.insight{background:#7c8cff18;border-left:3px solid var(--accent)}.callout.insight strong{color:var(--accent)}
.callout.warn{background:#ffc06b18;border-left:3px solid var(--warn)}.callout.warn strong{color:var(--warn)}
.try-it{display:inline-block;margin-top:10px;padding:10px 18px;border-radius:9px;background:var(--panel-2);border:1px solid var(--border);color:var(--accent);font-size:13.5px;font-weight:600;cursor:pointer;transition:.14s;font-family:inherit}
.try-it:hover{background:#7c8cff22;border-color:var(--accent)}
.try-it.large{font-size:15px;padding:14px 28px;background:var(--accent-2);color:#0b0d14;border-color:var(--accent-2)}
.try-it.large:hover{filter:brightness(1.08)}
.guide-end{text-align:center;padding:18px 0 0}.guide-end p{color:var(--muted);margin-bottom:14px}
.panel-guide-item{display:flex;gap:12px;align-items:baseline;padding:9px 0;border-bottom:1px dashed var(--border)}
.panel-guide-item:last-child{border-bottom:none}
.pgi-label{font-size:13px;font-weight:800;color:var(--accent-2);min-width:20px;font-family:var(--mono)}
.panel-guide-item p{margin:0;font-size:13.5px;color:#ced3de;line-height:1.55}
.wrap{max-width:980px;margin:20px auto 0}
.panel{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;margin-bottom:16px}
.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px}
.panel-label{font-size:13px;font-weight:700;color:var(--text)}.panel-note{color:var(--muted);font-size:12px}
.ctrl-row{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin:4px 0 6px}
.chips{display:flex;flex-wrap:wrap;gap:6px}
.chip{font-size:12px;border:1px solid var(--border);background:var(--panel-2);color:var(--muted);border-radius:20px;padding:5px 12px;cursor:pointer;font-family:inherit}
.chip.on{border-color:var(--accent-2);color:#0b0d14;background:var(--accent-2);font-weight:700}
svg{display:block;width:100%;height:auto;overflow:visible}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
.card{background:var(--panel-2);border:1px solid var(--border);border-radius:11px;padding:14px}
.card h4{margin:0 0 4px;font-size:13px}.card .cap{color:var(--muted);font-size:11.5px;margin:0 0 8px;line-height:1.5}
.neuronbtns{display:flex;flex-wrap:wrap;gap:5px;margin-top:8px}
.neuronbtns button{font-family:var(--mono);font-size:11px;border:1px solid var(--border);background:var(--panel-2);color:var(--muted);border-radius:6px;padding:4px 8px;cursor:pointer}
.neuronbtns button.on{border-color:var(--accent);color:var(--text)}
.featlist{display:flex;flex-direction:column;gap:4px}
.fr{display:flex;align-items:center;gap:8px;font-size:12px}
.fr .fn{width:96px;color:#ced3de}.fr .fb{flex:1;height:12px;border-radius:6px;background:var(--bg);overflow:hidden}
.fr .ff{height:100%;border-radius:6px}.fr .fv{font-family:var(--mono);font-size:10.5px;color:var(--muted);width:36px;text-align:right}
.fr.active .fn{color:var(--accent-2);font-weight:700}
.verdict{font-size:12.5px;margin-top:12px;padding:9px 12px;border-radius:7px;font-weight:600;line-height:1.5;background:var(--panel-2);border:1px solid var(--border);color:#ced3de}
.hint{color:var(--muted);font-size:12.5px;margin-top:10px;line-height:1.5}
.lenscol{display:flex;flex-direction:column;gap:6px}
.lenslayer{display:flex;align-items:center;gap:10px;font-size:12px}
.lenslayer .ll{width:54px;font-family:var(--mono);color:var(--muted);font-size:11px}
.lenslayer .toks{display:flex;gap:5px;flex-wrap:wrap}
.lt{font-family:var(--mono);font-size:11px;padding:3px 7px;border-radius:5px;border:1px solid var(--border);color:var(--muted)}
.lt.top{border-color:var(--accent-2);color:var(--accent-2);background:#5be0c012}
.fig{margin:22px 0;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 16px 12px}
.fig svg{display:block;width:100%;height:auto;overflow:visible}
.fig figcaption{margin-top:10px;font-size:12px;color:var(--muted);text-align:center;line-height:1.55}
footer{max-width:980px;margin:30px auto 0;color:var(--muted);font-size:12px;text-align:center;line-height:1.6}
@media(max-width:760px){.guide{grid-template-columns:1fr}.guide-toc{grid-column:1;grid-row:auto;position:static;display:grid;grid-template-columns:1fr 1fr}.toc-label{grid-column:1/-1}.grid2{grid-template-columns:1fr}}
</style>
</head>
<body data-chapter="interpretability">
<header>
<div class="header-inner">
<div>
<h1>Interpretability</h1>
<p class="sub">
People call a neural network a black box, but that's not quite right. Every weight is sitting
right there, fully visible β€” the problem isn't access, it's <b>reading</b>. The activations are a
dense, tangled code, and the work of interpretability is finding the tools that pry it open:
project a hidden layer back to words, find the direction that means "France," and pull a neuron
that fires for a dozen unrelated things apart into the clean features hiding inside it.
</p>
</div>
</div>
<nav class="page-tabs" role="tablist">
<button class="page-tab active" data-tab="guide">β‘  Guide</button>
<button class="page-tab" data-tab="playground">β‘‘ Playground</button>
</nav>
</header>
<!-- ═══════════════════════════════════════════════════════════════ GUIDE -->
<section id="guide-tab" class="tab-panel active">
<article class="guide">
<nav class="guide-toc">
<span class="toc-label">Contents</span>
<a href="#ch1">1 Β· Not a black box</a>
<a href="#ch2">2 Β· The logit lens</a>
<a href="#ch3">3 Β· Features as directions</a>
<a href="#ch4">4 Β· Circuits</a>
<a href="#ch5">5 Β· Superposition &amp; SAEs</a>
<a href="#ch6">6 Β· Reading the playground</a>
</nav>
<!-- 1 -->
<section class="chapter" id="ch1">
<h2><span class="ch-num">1</span> Not a black box, just hard to read</h2>
<p>
Unlike a human brain, a trained model hides nothing. Every weight, every activation, every
attention pattern is a number you can print out and stare at. In principle the entire computation
is open to inspection. The reason it still feels opaque is that the representation is alien β€”
meaning is spread across thousands of dimensions in a code no one designed to be read, and reading
it is a research problem in its own right.
</p>
<figure class="fig"><svg viewBox="0 0 700 200" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,Segoe UI,Roboto,sans-serif"><defs><marker id="arin" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="var(--muted)"/></marker></defs>
<text x="130" y="24" text-anchor="middle" fill="var(--bad)" font-size="11" font-weight="700">one neuron β€” polysemantic</text>
<circle cx="130" cy="96" r="30" fill="var(--bad)" opacity="0.2" stroke="var(--bad)"/>
<text x="130" y="101" text-anchor="middle" fill="var(--text)" font-size="12" font-weight="700">n7</text>
<text x="240" y="56" fill="var(--muted)" font-size="10">France</text><line x1="160" y1="96" x2="234" y2="52" stroke="var(--bad)" opacity="0.4" marker-end="url(#arin)"/><text x="240" y="82" fill="var(--muted)" font-size="10">anger</text><line x1="160" y1="96" x2="234" y2="78" stroke="var(--bad)" opacity="0.4" marker-end="url(#arin)"/><text x="240" y="108" fill="var(--muted)" font-size="10">Python</text><line x1="160" y1="96" x2="234" y2="104" stroke="var(--bad)" opacity="0.4" marker-end="url(#arin)"/><text x="240" y="134" fill="var(--muted)" font-size="10">the colour red</text><line x1="160" y1="96" x2="234" y2="130" stroke="var(--bad)" opacity="0.4" marker-end="url(#arin)"/>
<line x1="330" y1="40" x2="330" y2="156" stroke="var(--border)"/>
<text x="430" y="24" fill="var(--accent-2)" font-size="11" font-weight="700">SAE β†’</text>
<text x="540" y="24" text-anchor="middle" fill="var(--accent-2)" font-size="11" font-weight="700">features β€” monosemantic</text>
<rect x="430" y="42" width="230" height="22" rx="6" fill="var(--accent-2)" opacity="0.18" stroke="var(--accent-2)"/><text x="442" y="57" fill="var(--accent-2)" font-size="10.5" font-weight="700">f1</text><text x="470" y="57" fill="var(--muted)" font-size="10">= France</text><rect x="430" y="72" width="230" height="22" rx="6" fill="var(--accent-2)" opacity="0.18" stroke="var(--accent-2)"/><text x="442" y="87" fill="var(--accent-2)" font-size="10.5" font-weight="700">f2</text><text x="470" y="87" fill="var(--muted)" font-size="10">= anger</text><rect x="430" y="102" width="230" height="22" rx="6" fill="var(--accent-2)" opacity="0.18" stroke="var(--accent-2)"/><text x="442" y="117" fill="var(--accent-2)" font-size="10.5" font-weight="700">f3</text><text x="470" y="117" fill="var(--muted)" font-size="10">= Python</text><rect x="430" y="132" width="230" height="22" rx="6" fill="var(--accent-2)" opacity="0.18" stroke="var(--accent-2)"/><text x="442" y="147" fill="var(--accent-2)" font-size="10.5" font-weight="700">f4</text><text x="470" y="147" fill="var(--muted)" font-size="10">= the colour red</text>
<line x1="356" y1="96" x2="426" y2="96" stroke="var(--accent-2)" stroke-width="2" marker-end="url(#arin)"/>
</svg><figcaption>Individual neurons are usually polysemantic β€” one neuron fires for many unrelated concepts at once, so you can't read it. A sparse autoencoder untangles the activation into many features, each of which lights up for a single, nameable concept, turning an opaque vector into something you can actually interpret.</figcaption></figure>
<p>
Mechanistic interpretability is the attempt to reverse-engineer that code: to go from "the model
does the right thing" to "<em>here</em> is the algorithm it learned, written in attention heads and
MLP neurons." It matters for the safety themes running through this course β€” you can't fully trust
a system you can't inspect β€” and it's quietly one of the most beautiful corners of the field,
because trained networks turn out to contain structure far cleaner than you'd expect.
</p>
<div class="callout insight">
<strong>Everything is visible; nothing is legible β€” yet.</strong>
The challenge isn't access to the weights, it's decoding what they compute. Interpretability builds
the instruments that turn a wall of numbers into something a person can understand.
</div>
<button class="try-it" data-action="open">β–Ά Read a hidden layer; split a neuron apart</button>
</section>
<!-- 2 -->
<section class="chapter" id="ch2">
<h2><span class="ch-num">2</span> The logit lens</h2>
<p>
The simplest instrument is wonderfully direct. Recall the residual stream from Chapter 6 β€” the
running vector each layer reads and writes. Normally only the <em>final</em> layer's vector gets
multiplied by the output matrix to produce next-token probabilities. The <strong>logit lens</strong>
asks: what if we apply that same output matrix to an <em>intermediate</em> layer's vector? You get
a prediction the model would make if it stopped thinking right there.
</p>
<p>
Do this at every layer and you can watch the model's "draft" of the answer form. Early layers
produce a vague, spread-out guess; as the residual stream accumulates evidence, the distribution
sharpens, until by the last layers the correct token is firmly on top. It's a window onto the
model thinking out loud, built from nothing but the weights you already have.
</p>
<div class="callout insight">
<strong>Any layer can be read out as a prediction.</strong>
The logit lens reuses the model's own output matrix to decode intermediate states. Watching the
answer sharpen layer by layer turns the residual stream from a mystery into a visible chain of
reasoning.
</div>
<button class="try-it" data-action="lens">β–Ά Watch a prediction form by layer</button>
</section>
<!-- 3 -->
<section class="chapter" id="ch3">
<h2><span class="ch-num">3</span> Features as directions</h2>
<p>
A central finding is that concepts tend to be represented as <strong>directions</strong> in
activation space. The model doesn't dedicate one neuron to "this text is in French"; instead there's
a particular direction such that the more the activation points along it, the more French the
context. You saw the seed of this back in Chapter 2 β€” king minus man plus woman β€” and it runs all
the way up: a direction for sentiment, for code, for "the user is asking a question."
</p>
<p>
Because these are linear directions, you can do more than observe them. You can <em>probe</em> β€”
train a simple linear classifier to read a concept off the activations β€” and you can <em>steer</em>,
adding a feature's direction to the residual stream to make the model more French, or more cautious,
or more cheerful, mid-generation. Concepts as directions is what makes the model's internals
manipulable, not just visible.
</p>
<div class="callout insight">
<strong>Meaning lives in directions, not neurons.</strong>
A concept is a vector you can detect and add. That linear structure is the foundation everything
else in interpretability builds on β€” and the reason steering a model is as simple as nudging its
activations along the right axis.
</div>
</section>
<!-- 4 -->
<section class="chapter" id="ch4">
<h2><span class="ch-num">4</span> Circuits</h2>
<p>
Zoom in further and you find <strong>circuits</strong>: small, specific combinations of attention
heads and MLP neurons that together implement a recognizable algorithm. The famous example is the
<em>induction head</em> from Chapter 5 β€” a pair of heads that, working together, notice "this token
appeared before, and was followed by <em>that</em>" and copy the continuation. It's a concrete
piece of machinery you can locate, knock out, and watch the corresponding behaviour vanish.
</p>
<p>
Finding circuits is painstaking β€” tracing how information flows from specific heads through the
residual stream into others β€” but when it works it yields a genuine explanation: not "the model
tends to do X," but "<em>this</em> head reads <em>that</em> signal and writes it <em>there</em>,
causing X." It's the difference between describing a behaviour and understanding the mechanism.
</p>
<div class="callout insight">
<strong>Behaviours are built from reusable little machines.</strong>
Circuits are the model's learned subroutines, written in heads and neurons. Reverse-engineering one
turns a black-box capability into an algorithm you can point at.
</div>
</section>
<!-- 5 -->
<section class="chapter" id="ch5">
<h2><span class="ch-num">5</span> Superposition and sparse autoencoders</h2>
<p>
Here's the complication that makes individual neurons so hard to read: a model wants to represent
far more features than it has dimensions, so it packs them in <strong>superposition</strong> β€”
many features sharing the same dimensions, distinguishable only because any given input activates
just a few of them. The side effect is that a single neuron becomes <em>polysemantic</em>: it fires
for a jumble of unrelated concepts, because several feature directions happen to overlap on its
axis. Stare at one neuron and you see noise.
</p>
<p>
The breakthrough tool here is the <strong>sparse autoencoder</strong>. Train a wide, sparse layer to
reconstruct the activations using many more units than there are dimensions, and it learns a
<em>dictionary</em> of directions that pulls the superposition apart β€” each learned feature now
lighting up for one clean, human-nameable concept. Suddenly the tangled six numbers become a sparse
list: "this activation is "Golden Gate Bridge" plus "uncertainty" plus "legal language."" It's the
closest thing yet to reading the model's mind one concept at a time.
</p>
<div class="callout insight">
<strong>Neurons are polysemantic; the features hidden in them aren't.</strong>
Superposition is why a neuron looks like noise, and sparse autoencoders are how you decode it β€” a
dictionary of monosemantic features recovered from the entangled activation. It's the playground's
centrepiece.
</div>
<button class="try-it" data-action="sae">β–Ά Pull a polysemantic neuron apart</button>
</section>
<!-- 6 -->
<section class="chapter" id="ch6">
<h2><span class="ch-num">6</span> Reading the playground</h2>
<p>
The superposition demo is real linear algebra: ten named features packed into six dimensions, an
activation built by activating a few of them, and a sparse decoder that recovers exactly which were
on. The logit lens is an illustrative residual stream read out to a small vocabulary layer by layer.
</p>
<div class="panel-guide-item"><span class="pgi-label">β–¦</span>
<p>Turn on a couple of concepts and watch them get crammed into six entangled neurons β€” then pick a
neuron and see the unrelated concepts that all make it fire.</p></div>
<div class="panel-guide-item"><span class="pgi-label">✧</span>
<p>Run the sparse autoencoder over the same activation and watch the tangle resolve into a clean,
sparse list of named features β€” only the ones you switched on.</p></div>
<div class="panel-guide-item"><span class="pgi-label">↑</span>
<p>The logit lens: a hidden state read out as a vocabulary prediction at each layer, sharpening
toward the answer as it rises.</p></div>
<div class="guide-end">
<p>The reading is the setup. The playground is the point.</p>
<button class="try-it large" data-action="open">β–Ά Open the Playground</button>
</div>
</section>
</article>
</section>
<!-- ═══════════════════════════════════════════════════════════════ PLAYGROUND -->
<section id="playground-tab" class="tab-panel">
<div class="wrap">
<div class="panel">
<div class="panel-head"><span class="panel-label">Activate some concepts</span>
<span class="panel-note">10 features packed into 6 neurons (superposition)</span></div>
<div class="chips" id="featChips"></div>
<p class="hint">Switch on one or two concepts. They get encoded into a 6-dimensional activation β€”
fewer dimensions than features, so they have to share.</p>
</div>
<div class="panel" id="panelSae">
<div class="panel-head"><span class="panel-label">Step 1 Β· the tangle, and the way out</span>
<span class="panel-note">raw neurons vs sparse-autoencoder features</span></div>
<div class="grid2">
<div class="card">
<h4>Raw neurons (6)</h4>
<p class="cap">The activation as the model stores it. Dense and entangled β€” no neuron means one
thing. Click a neuron to see the unrelated concepts that fire it.</p>
<svg id="neuronPlot" viewBox="0 0 380 130"></svg>
<div class="neuronbtns" id="neuronBtns"></div>
<div class="verdict" id="neuronVerdict" style="margin-top:8px"></div>
</div>
<div class="card">
<h4>SAE features (10)</h4>
<p class="cap">The same activation decoded by a sparse autoencoder's dictionary. Only the
concepts you switched on light up β€” each feature is monosemantic and nameable.</p>
<div class="featlist" id="featList"></div>
<div class="verdict good" id="saeVerdict" style="margin-top:10px"></div>
</div>
</div>
</div>
<div class="panel" id="panelLens">
<div class="panel-head"><span class="panel-label">Step 2 Β· the logit lens</span>
<span class="panel-note">read the residual stream as a prediction, layer by layer</span></div>
<div class="ctrl-row">
<span style="font-size:12px;color:var(--muted)">context:</span>
<div class="chips" id="lensChips"></div>
</div>
<div class="lenscol" id="lensCol"></div>
<p class="hint">An illustrative residual stream is projected to a small vocabulary at each layer
(the model's own output matrix applied to an intermediate state). Early layers are vague; the answer
sharpens to the top as the stream rises β€” the mechanism is exactly the logit lens, on a toy model.</p>
</div>
<footer>
The superposition demo is exact linear algebra β€” ten unit feature directions in a 6-D space, an
activation built by summing a few, and a sparse decoder that recovers them. The logit lens is an
illustrative residual stream read out to a toy vocabulary; the mechanism is real, the model is a stand-in.
</footer>
</div>
</section>
<script>
"use strict";
/* ── tabs / toc / try-it ── */
function switchTab(name){document.querySelectorAll(".page-tab").forEach(b=>{const on=b.dataset.tab===name;b.classList.toggle("active",on);b.setAttribute("aria-selected",on);});document.querySelectorAll(".tab-panel").forEach(p=>p.classList.toggle("active",p.id===name+"-tab"));}
document.querySelectorAll(".page-tab").forEach(b=>b.addEventListener("click",()=>switchTab(b.dataset.tab)));
document.querySelectorAll(".guide-toc a").forEach(a=>a.addEventListener("click",e=>{e.preventDefault();document.querySelector(a.getAttribute("href"))?.scrollIntoView({behavior:"smooth",block:"start"});}));
document.querySelectorAll(".try-it[data-action]").forEach(b=>b.addEventListener("click",()=>{const a=b.dataset.action;switchTab("playground");window.scrollTo({top:0,behavior:"smooth"});const t=(a==="lens")?"panelLens":(a==="open"?null:"panelSae");if(t)setTimeout(()=>document.getElementById(t)?.scrollIntoView({behavior:"smooth",block:"start"}),350);}));
/* ════════════════ ENGINE β€” superposition + sparse recovery (real) ════════════════ */
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const FEATURES=["France","sentiment","code","question","past tense","legal","Golden Gate","colour","number","uncertainty"];
const NF=FEATURES.length, D=6;
function vdot(u,v){let s=0;for(let i=0;i<u.length;i++)s+=u[i]*v[i];return s;}
function vnorm(v){const n=Math.sqrt(vdot(v,v))||1;return v.map(x=>x/n);}
// dictionary of NF feature directions in D dims. NF>D forces superposition. The directions
// are spread apart by repulsion toward an equiangular frame (low coherence β‰ˆ 0.29) so that a
// sparse autoencoder can recover them β€” which is exactly what a well-trained SAE learns to do.
const DICT=(()=>{const r=mulberry32(10);let M=[];for(let f=0;f<NF;f++){const v=[];for(let d=0;d<D;d++){const u=r()||1e-9,w=r();v.push(Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*w));}M.push(vnorm(v));}
for(let it=0;it<900;it++){const F=M.map(()=>new Array(D).fill(0));
for(let i=0;i<NF;i++)for(let j=0;j<NF;j++){if(i===j)continue;const c=vdot(M[i],M[j]),g=c*Math.abs(c);for(let d=0;d<D;d++)F[i][d]-=g*M[j][d];}
for(let i=0;i<NF;i++){for(let d=0;d<D;d++)M[i][d]+=0.15*F[i][d];M[i]=vnorm(M[i]);}}
return M;})();
function activation(active){const a=new Array(D).fill(0);active.forEach(f=>{for(let d=0;d<D;d++)a[d]+=DICT[f][d];});return a;}
// solve a small support-restricted least squares (Gram-matrix Gaussian elimination)
function solveLS(support,a){const k=support.length,G=[],b=[];for(let i=0;i<k;i++){b.push(vdot(DICT[support[i]],a));const row=[];for(let j=0;j<k;j++)row.push(vdot(DICT[support[i]],DICT[support[j]]));G.push(row);}
for(let i=0;i<k;i++)G[i].push(b[i]);
for(let col=0;col<k;col++){let piv=col;for(let r=col+1;r<k;r++)if(Math.abs(G[r][col])>Math.abs(G[piv][col]))piv=r;[G[col],G[piv]]=[G[piv],G[col]];const dd=G[col][col]||1e-9;for(let r=0;r<k;r++){if(r===col)continue;const ff=G[r][col]/dd;for(let c=col;c<=k;c++)G[r][c]-=ff*G[col][c];}}
const c=[];for(let i=0;i<k;i++)c.push(G[i][k]/(G[i][i]||1e-9));return c;}
// SAE sparse decode by orthogonal matching pursuit: greedily pick the best-correlated feature,
// refit by least squares, subtract, repeat β€” recovering exactly the features that were active.
function saeDecode(a){let resid=a.slice();const support=[],coeffs=new Array(NF).fill(0);
for(let it=0;it<4;it++){let best=-1,bv=0.2;for(let f=0;f<NF;f++){if(support.includes(f))continue;const dt=vdot(DICT[f],resid);if(dt>bv){bv=dt;best=f;}}
if(best<0)break;support.push(best);const c=solveLS(support,a);
resid=a.slice();support.forEach((f,k)=>{for(let d=0;d<D;d++)resid[d]-=c[k]*DICT[f][d];});
support.forEach((f,k)=>coeffs[f]=Math.max(0,c[k]));
let rn=0;for(const x of resid)rn+=x*x;if(Math.sqrt(rn)<0.05)break;}
return coeffs;}
// which features fire a given neuron (axis j): |DICT[f][j]|
function neuronFeatures(j){return DICT.map((dir,f)=>({f,w:dir[j]})).sort((a,b)=>Math.abs(b.w)-Math.abs(a.w));}
/* logit lens (illustrative): residual stream builds toward an answer over L layers */
const LVOCAB=["paris","france","the","city","of","river","king","and"];
const LENS=[
{ctx:"the capital of france is",ans:0}, // paris
{ctx:"a stone bridge over the",ans:5}, // river
{ctx:"long live the",ans:6}, // king
];
function lensLayers(ansIdx,L){
// residual_l = base + (progress)*answerDir + noise; readout = softmax(UΒ·resid)
const r=mulberry32(99+ansIdx),U=LVOCAB.map(()=>{const v=[];let n=0;for(let d=0;d<D;d++){const u=r()||1e-9,w=r();const g=Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*w);v.push(g);n+=g*g;}n=Math.sqrt(n)||1;return v.map(x=>x/n);});
const ansDir=U[ansIdx],base=U[(ansIdx+3)%LVOCAB.length].map(x=>x*0.3);
const out=[];
for(let l=0;l<=L;l++){const prog=l/L,resid=base.map((b,d)=>b*(1-prog)+ansDir[d]*prog*3.0);
const logits=U.map(u=>{let s=0;for(let d=0;d<D;d++)s+=u[d]*resid[d];return s;});
let m=Math.max(...logits),Z=0;const p=logits.map(x=>{const e=Math.exp((x-m)*1.4);Z+=e;return e;});
const probs=p.map(x=>x/Z);const order=[...probs.keys()].sort((a,b)=>probs[b]-probs[a]).slice(0,4);
out.push({layer:l,top:order.map(i=>({tok:LVOCAB[i],p:probs[i]}))});}
return out;
}
/* ════════════════ UI ════════════════ */
const $=id=>document.getElementById(id);
const css=v=>getComputedStyle(document.documentElement).getPropertyValue(v).trim();
const S={active:new Set([0,6]),neuron:0,lens:0};
function render(){
$('featChips').innerHTML=FEATURES.map((f,i)=>'<button class="chip'+(S.active.has(i)?' on':'')+'" data-i="'+i+'">'+f+'</button>').join('');
const a=activation([...S.active]),sae=saeDecode(a);
// raw neurons
const W=380,H=130,padL=8,padB=22,padT=8,bw=(W-padL*2)/D,mid=(H-padB+padT)/2+6;let mx=1e-9;a.forEach(v=>mx=Math.max(mx,Math.abs(v)));
let s='<line x1="'+padL+'" y1="'+mid+'" x2="'+(W-padL)+'" y2="'+mid+'" stroke="'+css('--border')+'"/>';
a.forEach((v,j)=>{const h=(Math.abs(v)/(mx||1))*(H-padT-padB)/2,x=padL+j*bw,on=j===S.neuron;
s+='<rect x="'+(x+8)+'" y="'+((v>=0?mid-h:mid))+'" width="'+(bw-16)+'" height="'+h.toFixed(1)+'" rx="2" fill="'+(on?css('--warn'):css('--accent'))+'"/>';
s+='<text x="'+(x+bw/2)+'" y="'+(H-7)+'" font-size="9" text-anchor="middle" fill="'+(on?css('--warn'):css('--muted'))+'">n'+j+'</text>';});
$('neuronPlot').innerHTML=s;
$('neuronBtns').innerHTML=Array.from({length:D},(_,j)=>'<button class="'+(j===S.neuron?'on':'')+'" data-n="'+j+'">neuron '+j+'</button>').join('');
// neuron polysemanticity
const nf=neuronFeatures(S.neuron).slice(0,4);
$('neuronVerdict').innerHTML='Neuron '+S.neuron+' responds to: '+nf.map(x=>'<b style="color:var(--text)">'+FEATURES[x.f]+'</b> ('+(x.w>=0?'+':'')+x.w.toFixed(2)+')').join(', ')+' β€” unrelated concepts sharing one axis. <span style="color:var(--bad)">Polysemantic.</span>';
// SAE features
const smx=Math.max(...sae,0.01);
$('featList').innerHTML=FEATURES.map((f,i)=>{const on=sae[i]>0.45*smx;return '<div class="fr'+(on?' active':'')+'"><span class="fn">'+f+'</span><span class="fb"><span class="ff" style="width:'+Math.round(100*sae[i]/smx)+'%;background:'+(on?css('--accent-2'):css('--muted'))+'"></span></span><span class="fv">'+sae[i].toFixed(2)+'</span></div>';}).join('');
const recovered=FEATURES.filter((f,i)=>sae[i]>0.45*smx);
const truth=[...S.active].map(i=>FEATURES[i]);
const ok=recovered.length===truth.length&&truth.every(t=>recovered.includes(t));
$('saeVerdict').className="verdict good";
$('saeVerdict').innerHTML=S.active.size===0?'Switch on a concept to see the SAE recover it.':'SAE recovered: <b>'+recovered.join(', ')+'</b> β€” '+(ok?'exactly the concepts you switched on. <span style="color:var(--good)">Monosemantic βœ“</span>':'a sparse, named set. Each feature means one thing.');
renderLens();
}
function renderLens(){
$('lensChips').innerHTML=LENS.map((l,i)=>'<button class="chip'+(i===S.lens?' on':'')+'" data-l="'+i+'">"'+l.ctx+'"</button>').join('');
const layers=lensLayers(LENS[S.lens].ans,6),ans=LVOCAB[LENS[S.lens].ans];
$('lensCol').innerHTML=layers.slice().reverse().map(L=>{
const isOut=L.layer===6;
return '<div class="lenslayer"><span class="ll">'+(L.layer===0?'embed':isOut?'output':'layer '+L.layer)+'</span><div class="toks">'
+L.top.map((t,r)=>'<span class="lt'+(t.tok===ans&&r===0?' top':'')+'">'+t.tok+' '+Math.round(t.p*100)+'%</span>').join('')+'</div></div>';
}).join('');
}
/* ── events ── */
$('featChips').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;const i=+b.dataset.i;if(S.active.has(i))S.active.delete(i);else{if(S.active.size>=2)S.active.delete([...S.active][0]);S.active.add(i);}render();});
$('neuronBtns').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;S.neuron=+b.dataset.n;render();});
$('lensChips').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;S.lens=+b.dataset.l;renderLens();});
render();
</script>
</body>
</html>