Phase C · mechanistic interpretability

NMF/PCA dictionaries

—

← back to the method catalogue

What it does?

Dictionary learning with NMF or PCA is a simpler cousin of the sparse autoencoder. It decomposes the recorded state into a small dictionary of components: PCA finds orthogonal directions of largest variance, NMF finds additive, non-negative parts. Each component is then matched to a known variable, on the hope that the decomposition recovers the machine's true internal factors without supervision. This is a classic, cheap way to look for structure in high-dimensional activity, and it predates the neural sparse-autoencoder approach. We run it on the VCS state and measure the fraction of dictionary components that match a known, verified variable. It does partially well: NMF's non-negativity separates the additive register basis better than PCA, which tends to mix several variables into one component. So NMF recovers cleaner named components while PCA blurs them. The deeper limit is the same one that defeats the sparse autoencoder. A component can match a variable by correlation while playing little causal role, so a good matched fraction does not guarantee the component is causally used. The VCS lets us check both the match and the causal use, and the two often disagree. Dictionary learning is thus a useful, interpretable baseline whose named components must still be tested by intervention before they can be trusted.

NMF/PCA dictionaries result

Left: the frame. Right: the recovered structure/effect against the ground truth (matched components or preserved behaviour), with the RAM cells labelled. nmf_causal_effect_agreement_spearman = 0.300 — this example only (pong, state gameplay(seed=0,prefix=90)+15); the audit aggregate is below.

Mechanistic family · classical dictionary learning

Learn the method — a beginner's guide

New to explainable AI? This block explains the method as it is used on neural networks, then how we run the very same method on an Atari 2600, where — unlike for a network — we know the right answer. Every implementation step links to the code. See also the XAI primer for the shared vocabulary.

The idea in plain words

Suppose you record a network's activations for thousands of inputs and stack them into one big table: one row per input, one column per neuron. A dictionary is a short list of “building-block” patterns, called atoms or components, such that every row can be written as a weighted sum of a few atoms. If the atoms line up with real concepts, you have found the concepts without any labels. This is dictionary learning, and it is much older than deep learning: neuroscientists used it in the 1990s to explain how the visual cortex codes images.

Two classical recipes are fast and need no training loop. PCA (principal component analysis) finds the directions along which the data varies most; they are orthogonal and may have negative entries. NMF (non-negative matrix factorisation) only allows non-negative atoms and weights, so everything is built by adding parts, never by cancelling them. That often gives “parts” a human can recognise. The sparse autoencoder is the modern, learned, sparse member of the same family. Running the classical ones first shows how much the extra machinery buys.

How it works on a neural network

Put the data in a matrix \(X\) with \(T\) rows (samples) and \(n\) columns (neurons). Both methods approximate it by a product of a thin matrix of activations \(A\) (\(T\times r\)) and a short matrix of atoms \(H\) (\(r\times n\)), with \(r \ll n\):

\[ X \;\approx\; \mu + A H \quad(\text{PCA, } H H^\top = I), \qquad X \;\approx\; A H \quad(\text{NMF, } A \ge 0,\ H \ge 0). \]

PCA takes \(\mu\) = the column means and \(H\) = the top \(r\) right singular vectors of the centred data. NMF minimises \(\|X - AH\|^2\) with the Lee–Seung multiplicative updates, which keep every entry non-negative. An SAE is the same picture with a learned, non-linear, sparse encoder for \(A\).

Toy example. Three RAM-like cells \((a, b, c)\). Cells \(a, b\) move together (a ball's x and a copy of it); cell \(c\) is a score. Four frames: \((4,2,0)\), \((2,1,0)\), \((0,0,5)\), \((4,2,5)\). NMF with \(r=2\) finds the exact answer \(H_1=(2,1,0)\) (“ball”), \(H_2=(0,0,1)\) (“score”), with weights \((2,0), (1,0), (0,5), (2,5)\). Each atom is one variable. PCA on the same data gives a first component of roughly \(0.92\cdot\)score \(-\,0.38\cdot\)ball: both variables vary a lot, so the direction of largest variance mixes them, with a sign. Same data, same rank; only the constraint differs.

Recipe

  1. Record many activation vectors and stack them into a samples × units matrix.
  2. Choose the number of atoms \(r\) (try several).
  3. Fit PCA (singular value decomposition of the centred matrix) and NMF (multiplicative updates on the raw, non-negative matrix).
  4. Inspect each atom's loadings: which units does it weigh, and do they belong to one concept?
  5. Count the share of atoms that match a known concept (the matched fraction).
  6. Test causally: remove one atom from the reconstruction, put the state back into the model, and measure the change in output.

From a neural network to the Atari

Activation table \(X\)The RAM tape: one row per video frame, 128 columns (one per RAM byte).
Atom / componentA pattern over the 128 bytes. A clean atom loads on the bytes of one game variable.
Concept labelThe verified concept↔cell map (for Pong: scores, paddle and ball positions), grouped into families such as “ball” or “score”.
Structural signals (no network analogue)Known machine-level signals: the frame clock, how many bytes change per frame, and a fitted periodic phase.
Removing an atomZero its weight, rebuild the 128 bytes, round them, write them into the real machine, replay 15 frames.
Ground truthThe bit-exact re-run itself: an atom is causally used only if removing it changes the screen.

How we apply it to the VCS

Our runner dictionaries.jl applies PCA, NMF and a small sparse autoencoder to the same RAM tape and scores all three with the same tests. That makes it a fair three-way comparison.

The triad: F is the Spearman correlation between how strongly each atom matches a variable and its causal effect (“are the named atoms the used ones?”). S correlates the effect with the held-out-tail effect. M is one minus the share of matched atoms whose removal does nothing: naming a causally dead atom is an over-claim.

Why NMF tends to win on the VCS: RAM is literally a list of separate bytes that the program adds to and overwrites. That is an additive, non-negative “parts” structure, which is what NMF assumes. PCA's orthogonality has no reason to hold, so its components blend variables that happen to vary together.

Read the code

XAI the interpretability method   substrate the bit-exact emulator it runs on (jutari, Julia).

Reading the result

The figure compares the matched fraction of PCA, NMF and the SAE. Expect a partial result: NMF recovers more cleanly named atoms than PCA, because its atoms are additive parts, while PCA folds several variables into one signed direction. Then look at causal use. Several matched atoms do little or nothing when removed, while some unmatched ones matter. So a good matched fraction is evidence that a variable is present in the data, not that the program uses that atom. This is the same gap the SAE page shows.

Pitfalls

Try it yourself

julia --project=jutari tools/xai_study/phaseC_mechanistic/dictionaries.jl --games core

Writes dictionaries_<game>.{json,npz} and dictionaries_core_summary.json into tools/xai_study/phaseC_mechanistic/out/. Use --rank 8 for the headline rank only.

Original papers & further reading

Read next: Sparse autoencoders · A7 · Dimensionality reduction (NMF/PCA) · Linear probing + control tasks · Interchange interventions / DAS

How it's scored

The score is the fraction of NMF or PCA dictionary components that match a known, verified variable. We decompose the state into a small dictionary, match each component to a known variable, and count the matched fraction. The grading is always against the intervention oracle. Faithfulness for this method is that matched fraction, and it is partial: NMF's non-negativity separates the additive register basis better than PCA, so NMF yields cleaner named components while PCA blurs several variables into one component. The deeper limit, which the matched fraction alone does not expose, is causal use. A component can match a variable by correlation while playing little causal role, so a good matched fraction sits beside near-zero causal use, exactly as it does for the sparse autoencoder. There is no sprite-position hard-case here; the output is a set of components. The grading is always against the oracle, never against another method. The audit box reports the measured triad across the scored games, so the matched fraction is an average over the scored set. Dictionary learning is a useful interpretable baseline, but its named components must be tested by intervention before they can be trusted, and the score reflects that gap.

The score is measured against the §1 intervention oracle — never against another interpretability method. F (faithful) is always vs the oracle; see the execution stack. How each of F / S / M is actually computed for this method (and whether it matches the paper) is in the box just below; the numbers are in the In the audit box under it.

How F, S, M are computed here

The exact formula this method uses for each score, read from its runner, and whether it matches the paper's §3 (F ∧ S ∧ M triad) definition. From the committed audit fsm_math_phaseC.json.

AxisFormula (as computed) What it measuresMatches §3?
F
faithfulness
\(F = \rho_{\mathrm{Spearman}}\!\left(\mathrm{attr}_{\mathrm{atom}},\ \Delta y(u)\right)\)Spearman correlation between each matched NMF atom's strength and its true ablation causal effect: do matched dictionary atoms causally drive the output?✓ matches dictionaries.jl now emits top-level metric_name=nmf_causal_effect_agreement_spearman with extra.triad.F = Spearman(matched-strength, ablation causal effect); it replaced the matched-component NAMING fraction. This is exactly §3's causal-effect-agreement correlation vs {Delta_y(u)}. Record: dictionaries_pong.json extra.triad.F=0.3 (F_note '...causal-agreement F, not the correlational matched-component fraction').
how it's measured — call stack:
  1. F = Spearman(matched-atom strength, ablation causal effect) — dictionaries.jl:762
  2. score_triad over the matched dictionary atoms — dictionaries.jl:759
  3. true causal effect Δy(u) by bit-exact re-run — oracle_intervene.jl:242
S
sufficiency
\(S = \rho_{\mathrm{Pearson}}\!\left(\Delta y(u),\ \Delta y_{\mathrm{held\text{-}out}}(u)\right) \in [-1,1]\)Held-out sufficiency: Pearson correlation of each atom's ablation causal effect with its effect on an unseen continuation (bit-exact re-run); can be negative.✓ matches dictionaries.jl (S = _pearson(cu.effect, cu.held_out_effect)); a held-out predictive correlation in [-1,1], the §3 (sec:triad) form. Now surfaced in extra.triad.S (dictionaries_pong.json S=0.932).
how it's measured — call stack:
  1. S = Pearson(atom effect, held-out continuation effect) — dictionaries.jl:763
  2. true causal effect Δy(u) by bit-exact re-run — oracle_intervene.jl:242
M
minimality
\(M = |U^{\star}| / |\hat{U}| \in (0,1]\quad U^{\star}=\{u:\Delta y(u)>0\},\ \hat{U}=\{u:|\mathrm{attr}(u)|>\tau\}\)Oracle-mover count over the number of matched atoms the method names above threshold.✓ matches dictionaries.jl now emits M = |U*|/|U_hat| in extra.triad.M (M_note '|U*|=8 (oracle movers) / |U_hat|=5 (method above-threshold)'), the paper's set-size ratio. Record: dictionaries_pong.json extra.triad.M=1.0. Replaced the former 1 − over-claim rate.
how it's measured — call stack:
  1. minimality_score(atom strength, ablation effect) — dictionaries.jl:1099
  2. minimality_score: |U*| oracle movers / |U_hat| matched atoms — triad_sm.jl:57
  3. true causal effect Δy(u) by bit-exact re-run — oracle_intervene.jl:242

✓ matches = the same quantity as §3; ◐ partial = the same kind of estimator but a differing detail; ✗ does not match = a different quantity (see the note). Definitions: F = agreement with the oracle's true causal effects Δy(u); S = held-out predictive score in [−1, 1]; M = |U*|/|Û| (true-minimal-set size / named-set size).

In the audit

This is the method's entry in the actual cross-method audit — scored on the paper's correctness triad, each axis a mean over 44 of the 44 scored games (44 committed §R records), not the single example shown above. Tradition: dim_reduction. The example figure (Pong) is one of those records.

0.129F — faithfulness vs oracle (mean over 44 games, ±0.062 CI95)
0.725S — sufficiency: held-out predictive (n/a where the paper does not define this axis)
0.985M — minimality: true-minimal-set / named-set (n/a otherwise)
44committed records aggregated
0.00human-plausibility proxy

F faithfulness (scored vs the oracle for every method) · S sufficiency (held-out predictive score in [−1, 1]; a negative value means the explanation predicts held-out interventions worse than the unperturbed baseline; reported for the predictive methods across all three phases where the calibration/held-out split is defined) · M minimality (true-minimal-set / named-set; where the method names a cause set) — n/a otherwise, per the paper's F ∧ S ∧ M triad.

Source: leaderboard.json · the whole leaderboard is on the methods page and the Paper 2 audit.

Results per game

This method's faithfulness on each of the 44 scored games (all-regime F; this method has no content-vs-position split (see the note below)). Click a header to sort. Every number is read from site_data.json (methods.dictionaries.per_game).

content-F and position-F are n/a for this method. The content-vs-position split applies only to pixel-attribution methods — those that produce an attribution map over the framebuffer, whose score can be read separately on the smooth content outputs and the discrete position outputs (where the naive gradient is provably zero). This method does not attribute to a pixel output; it is graded by its own fidelity metric (nmf_causal_effect_agreement_spearman), reported as the F column, so there is no content or position regime to separate.

S is negative on 8 of these games. Sufficiency asks whether the explanation can predict the outcome of held-out interventions it was not fit on; for this method S is the held-out predictive correlation between its claim and the true effect, so it lies in [−1, 1]. A value near 0 means the explanation predicts no better than the unperturbed output; a negative value means it predicts worse — its held-out claim is anti-correlated with the truth (named yet causally unused). The aggregate S is the mean over the scored games.

GameF (all)content-Fposition-FSMNote
Krull0.733n/an/a1.0001.000
Frostbite0.659n/an/a-0.8641.000
Enduro0.657n/an/a-0.5531.000
Boxing0.502n/an/a0.8301.000
Yars' Revenge0.464n/an/a-0.3551.000
Demon Attack0.432n/an/a0.9231.000
Ice Hockey0.353n/an/a0.9531.000
Assault0.312n/an/a0.9081.000
Pong0.300n/an/a0.9321.000
Private Eye0.291n/an/a0.9991.000
Centipede0.273n/an/a0.9891.000
Pitfall0.203n/an/a0.9671.000
Pacman0.156n/an/a0.9971.000
Q*bert0.127n/an/a0.1321.000
Breakout0.109n/an/a0.9371.000
Kung-Fu Master0.058n/an/a0.9971.000
Riverraid0.038n/an/a-0.0511.000no true-cause signal
Air Raid0.000n/an/a-0.8341.000no true-cause signal
Alien0.000n/an/a1.0000.333no true-cause signal
Asterix0.000n/an/a0.9881.000no true-cause signal
Atlantis0.000n/an/a0.9921.000no true-cause signal
Bank Heist0.000n/an/a0.6271.000no true-cause signal
Berzerk0.000n/an/a0.9891.000no true-cause signal
Bowling0.000n/an/a0.8001.000no true-cause signal
Carnival0.000n/an/a0.9851.000no true-cause signal
Chopper Command0.000n/an/a0.9951.000no true-cause signal
Double Dunk0.000n/an/a0.9981.000no true-cause signal
Fishing Derby0.000n/an/a0.9571.000no true-cause signal
Freeway0.000n/an/a-0.2911.000no true-cause signal
Gopher0.000n/an/a0.9611.000no true-cause signal
Hero0.000n/an/a0.9391.000no true-cause signal
Jamesbond0.000n/an/a0.9151.000no true-cause signal
Kangaroo0.000n/an/a-0.7591.000no true-cause signal
Montezuma's Revenge0.000n/an/a0.5241.000no true-cause signal
Ms. Pac-Man0.000n/an/a0.8351.000no true-cause signal
Name This Game0.000n/an/a0.9771.000no true-cause signal
Phoenix0.000n/an/a-0.9281.000no true-cause signal
Road Runner0.000n/an/a0.7691.000no true-cause signal
Seaquest0.000n/an/a0.6521.000no true-cause signal
Skiing0.000n/an/a0.8541.000no true-cause signal
Space Invaders0.000n/an/a0.8811.000no true-cause signal
Tennis0.000n/an/a0.9671.000no true-cause signal
Venture0.000n/an/a1.0001.000no true-cause signal
Video Pinball0.000n/an/a0.7111.000no true-cause signal

Call stack — how it runs on jutari / jaxtari

The path from this method's runner (compute_game) into the bit-exact VCS substrate, and the computation it involves. Every step links to the exact source on main. From callstack_phaseC.json.

Applied on the substrate

  1. Boot the real ROM with the xitari-parity reset (60 NOOP + 4 RESET) — env_reset!(env; boot_noop_steps = 60, boot_reset_steps = 4) jutari
  2. Deterministically step the emulator one action per env-frame (also drives the RAM-trajectory recording) — env_step!(env::StellaEnvironment, action::Integer) jutari
  3. Assemble the shared gameplay-state testbed via build_dict_shared_state — build_shared_testbed(game; ... intervene_ram = J.intervene_ram!, run_intervention = O.run_intervention, soft_ram_peek = soft_ram_peek, ...) runner
  4. Freeze byte-exact RAM + screen out of the env at a frame — snapshot(env::StellaEnvironment, frame::Integer) jutari

The computation

  1. Record the RAM trajectory (per-frame 128-byte tape) the dictionary is fit on — record_ram(game, frames, ...) -> env_step! runner
  2. Fit the dictionary on the RAM tape: PCA / NMF / SAE — run_pca(tape, rank) / run_nmf(tape, rank) / run_sae(tape, hidden) runner run_nmf @ :464, run_sae @ :487
  3. CAUSAL-USE test: ablate atom k (zero its activation), DECODE to a full 128-byte RAM vector, WRITE it into the env over all cells and re-run; effect = screen change vs the reconstruction control — causal_use(m, checkpoint, at_target, tail, ...) -> rerun_with_ram(checkpoint, bk, tail) runner rerun_with_ram writes the decoded RAM over all cells via intervene_ram! (dictionaries.jl:671 -> :674)

Scored against the exact intervention oracle Δy(u): run_intervention(checkpoint, actions, target_frame, horizon, cause).

Implementation
tools/xai_study/phaseC_mechanistic/dictionaries.jl
Reference
—
Record
dictionaries_pong.json
All records
phaseC_mechanistic/out

The figure is generated from the committed record by docs/gen_method_figures.py; the game frame and each RAM cell's screen footprint are produced by render_scenes.jl / cell_footprints.jl.