Spaces:
Sleeping
Sleeping
| <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 & 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> | |
| ; | |
| /* ββ 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> | |