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.

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 all 42 scored games (42 committed §R records), not the single example shown above. Tradition: dim_reduction. The example figure (Pong) is one of those records.

0.102F — faithfulness vs oracle (mean over 42 games, ±0.057 CI95)
0.751S — sufficiency: held-out predictive (n/a where the paper does not define this axis)
0.966M — minimality: true-minimal-set / named-set (n/a otherwise)
42committed 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 42 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 5 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
Boxing0.502n/an/a0.8301.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
Centipede0.273n/an/a0.9891.000
Yars' Revenge0.218n/an/a0.2921.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/a0.3731.000no true-cause signal
Alien0.000n/an/a1.0000.333no true-cause signal
Atlantis0.000n/an/a0.9921.000no true-cause signal
Bank Heist0.000n/an/a0.6271.000no true-cause signal
Beam Rider0.000n/an/a1.0000.250no 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.9861.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.8101.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
Pitfall0.000n/an/a0.9601.000no true-cause signal
Private Eye0.000n/an/a1.0001.000no true-cause signal
Road Runner0.000n/an/a0.7691.000no true-cause signal
Seaquest0.000n/an/a0.6521.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.