We study whether sparse dictionary learning on raw physical events can discover discrete causal rules — and whether the learned representations support compositional generalization to novel multi-rule interactions never seen during training.
We construct a minimal physics simulator producing structured transition events under five rules (gravity, containment, contact, bounce, breakage). A dictionary trained exclusively on single-rule events is then evaluated on multi-rule compositions. The key finding: a ContrastiveDictionary architecture that adds explicit specialization pressure during training achieves 9/9 composition tests using only raw event tuples — no hand-designed features, no multi-rule training examples.
Composable causal primitives do emerge from unsupervised dictionary learning — but only when the right architectural pressure is applied. Standard ISTA sparse coding fails on 6 of 9 composition tests because sparsity alone is too weak a signal for rule specialization. The ContrastiveDictionary succeeds on all 9 by adding one targeted innovation: during training, each atom is penalized for activating on data from multiple rules, pushing atoms toward one-atom-per-rule specialization without changing the reconstruction architecture or inference procedure.
The critical lesson from the architecture comparison is that factoring hurts at scale. ProductOfExperts and ContrastivePoE both fail at 5 rules because their separate position codebooks activate differently on composition vs. training data, causing systematic Jaccard failures. Contrastive's unfactored reconstruction avoids this — every atom learns both spatial and causal patterns, and the contrastive loss shapes which events each atom responds to, not which dimensions it covers.
This result has broader implications for representation learning: if you want representations that compose, you need explicit pressure toward composability during training. Reconstruction loss alone is insufficient — the contrastive term is not a regularizer, it is the mechanism by which causal structure emerges.
- Conclusion
- 1. Problem Statement
- 2. Environment: Micro-World Simulator
- 3. Event Encoding
- 4. Dictionary Learning
- 5. Evaluation Protocol
- 6. Architectures
- 7. Results
- 8. Analysis
- 9. Reproducing Results
- 10. Project Structure
- 11. References
Large language models acquire world knowledge from token co-occurrence statistics, but their representations are opaque and do not decompose into discrete causal units. We ask a more fundamental question:
Given raw observations of a simple physical world governed by compositional rules, can unsupervised learning discover representations that (a) specialize to individual rules, and (b) compose to explain novel multi-rule interactions?
Concretely, a model trained only on events where one rule acts at a time must generalize to events where two or three rules act simultaneously — without any supervision, multi-rule examples, or explicit rule labels during inference.
Let
-
Reconstruction ratio:
$\text{MSE}(x_{ij}) / \text{MSE}(x_{\text{single}}) < 2.0$ — multi-rule events are not substantially harder to reconstruct than single-rule events. -
Jaccard similarity:
$|C \cap (A \cup B)| / |C \cup (A \cup B)| \geq 0.7$ — the atoms activated by the composition overlap with the union of atoms from individual rules.
Implementation: experiments/causal_dictionaries/micro_world.py
A 5x5 discrete grid populated with typed objects (ball, cup, box, shelf, table). Five deterministic physics rules govern state transitions:
| Rule | Precondition | Effect | Object types |
|---|---|---|---|
| Gravity | Object at row > 0 with no support below | Falls to nearest surface | All |
| Containment | Object A has inside=B and B moves |
A's position syncs to B's | All |
| Contact | Object receives a push (left/right) | Moves +/-1 column, clamped to [0, 4] | All |
| Bounce | Elastic object lands after gravity fall | Bounces up 1 row | Ball only |
| Breakage | Fragile object falls >= 2 rows | Object state becomes "broken" | Cup only |
Rules execute in a fixed order within each timestep: Contact → Containment → Gravity → Bounce → Breakage. This ordering enables natural compositions (e.g., pushing a cup off a high shelf triggers contact → gravity → breakage).
Object type properties create interesting interactions:
- Balls are elastic — they bounce after any gravity fall
- Cups are fragile — they break when falling >= 2 rows
- Boxes are inert — neither elastic nor fragile, useful as neutral test objects
Each event is a 7-tuple:
Event(obj_name, obj_type, pos_before, pos_after, rule, action, state_change)
pos_before,pos_after: (row, col) coordinates on the 5x5 gridrule: which physics rule generated this event (gravity,containment,contact,bounce,breakage)action: specific sub-action (gravity_fall,contained_move,push,none,bounce,break_on_impact)state_change:unchanged,broken, orintact
For each rule, we generate
- Gravity: ~1/3 positive (unsupported objects at random heights), ~2/3 negative (on floor or supported). Object types cycle through all 5 types.
- Containment: Containers (
box,cup) with aballinside are pushed left or right. - Contact: Objects at random columns receive push actions in random directions.
- Bounce: Balls placed at random heights with no support — collected bounce events after gravity.
- Breakage: Cups placed at height >= 2 with no support — collected breakage events after hard landing.
Implementation: experiments/causal_dictionaries/event_encoding.py
Events are encoded as 18-dimensional vectors using only the raw fields present in the Event dataclass — no derived features, no domain knowledge:
| Field | Dims | Representation |
|---|---|---|
| Object type | 5 | One-hot over {ball, cup, box, shelf, table} |
| Position before | 2 | Normalized (row/4, col/4) in [0, 1] |
| Position after | 2 | Normalized (row/4, col/4) in [0, 1] |
| Action | 6 | One-hot over {gravity_fall, contained_move, push, none, bounce, break_on_impact} |
| State change | 3 | One-hot over {intact, broken, unchanged} |
Critically, this encoding contains no displacement, magnitude, height, or "changed" features. The model must discover that gravity means "row decreases," that containment means "positions match," that bounce means "row increases after gravity," and that breakage depends on fall distance — entirely from reconstruction pressure.
Implementation: experiments/causal_dictionaries/sparse_dictionary.py
The base learning algorithm is ISTA (Iterative Shrinkage-Thresholding Algorithm) with Hebbian dictionary updates. No backpropagation.
Given input
for t = 1 to T:
residual = x - z @ D.T
drive = residual @ D
z = z + n_infer * drive
z = max(0, z - lambda * n_infer) # soft thresholding
z = min(z, 5.0) # activation clamping
Parameters:
After settling, the dictionary is updated via the local Hebbian rule:
followed by column normalization
- Generate 2,000 events per rule (10,000 total across 5 rules)
- Encode all events using raw encoding (18 dimensions)
- Shuffle all events (destroying rule labels)
- Train dictionary on shuffled data for 150 epochs
- No rule labels are used during training (contrastive architecture uses labels only for the specialization loss, not for reconstruction)
Implementation: experiments/causal_dictionaries/analysis.py
For each atom
Specialization score:
Nine tests, each generating 200 events from novel multi-rule scenarios:
| Test | Rules | Scenario |
|---|---|---|
| T1 | gravity + containment | Object inside container, both unsupported → fall together |
| T2 | gravity + contact | Box pushed off support → falls |
| T3 | containment + contact | Container pushed → contents follow |
| T4 | gravity + containment + contact | Container-with-contents pushed off surface |
| T5 | negation control | Gravity events tested — should not spuriously activate |
| T6 | gravity + bounce | Ball at height falls and bounces |
| T7 | gravity + breakage | Cup at height >= 2 falls and breaks |
| T8 | contact + gravity + bounce | Ball pushed off support → falls → bounces |
| T9 | contact + gravity + breakage | Cup pushed off high stack → falls → breaks |
For each test:
- Reconstruction ratio = mean MSE on composition data / mean MSE on all single-rule data. Threshold: < 2.0.
-
Jaccard similarity =
$|C \cap (A \cup B)| / |C \cup (A \cup B)|$ where$A$ ,$B$ are active atom sets (mean |activation| > 0.1) for individual rules and$C$ for the composition. Threshold: >= 0.7.
A test passes if both criteria are met (except T5 negation which only checks ratio). Overall pass requires >= 75% of tests.
Implementation: experiments/causal_dictionaries/architectures.py
We evaluated four architectures, all using the same raw encoding and data:
Standard sparse coding. Atoms specialize purely as a side effect of sparsity — no explicit pressure toward rule specialization.
Factorizes the dictionary into two independent codebooks:
-
Rule codebook
$D_r$ : captures what causal rule is active -
Position codebook
$D_p$ : captures where objects are
Reconstruction is additive:
ISTA + contrastive specialization pressure. During training, for each atom, computes mean activation per rule and penalizes atoms that fire on multiple rules. This is a direct, differentiable pressure toward one-atom-per-rule specialization — without changing the reconstruction architecture.
Key insight: all atoms participate equally in reconstruction (no factoring), and the contrastive loss directly shapes them to specialize. Rule labels are used only for the specialization loss during training, not during inference.
Loss:
Default:
Hybrid combining PoE factoring with contrastive pressure on the rule codebook. Performed worse than plain contrastive because the position codebook introduces Jaccard mismatches (same problem as PoE).
Both use identical raw encoding, same data — the only difference is the architecture.
| Test | ISTA (baseline) | Contrastive | ||
|---|---|---|---|---|
| Ratio | Jaccard | Ratio | Jaccard | |
| T1 gravity+containment | 0.79 | 0.33 | 1.13 | 0.70 |
| T2 gravity+contact | 1.62 | 0.75 | 0.97 | 0.90 |
| T3 containment+contact | 0.63 | 0.50 | 0.92 | 0.78 |
| T4 all original | 1.75 | 0.78 | 1.10 | 0.90 |
| T5 negation | 2.05 | -- | 1.23 | -- |
| T6 gravity+bounce | 3.30 | 0.44 | 1.56 | 0.70 |
| T7 gravity+breakage | 0.48 | 0.44 | 0.93 | 0.70 |
| T8 contact+bounce | 3.78 | 0.78 | 1.44 | 0.80 |
| T9 contact+breakage | 1.83 | 0.78 | 1.09 | 0.90 |
| Overall | 3/9 FAIL | 9/9 PASS |
ISTA fails on 6 of 9 tests — its atoms are not specialized enough, producing chaotic activations on composition data. Contrastive passes all 9 with reconstruction ratios between 0.92–1.56 and Jaccards between 0.70–0.90.
| Architecture | Tests Passed | Mean Specialization | Key Issue |
|---|---|---|---|
| ISTA (baseline) | 3/9 | 0.70 | No specialization pressure — atoms fire on multiple rules |
| ProductOfExperts | 1-2/9 | varies | Position atoms cause Jaccard mismatches at 5 rules |
| ContrastiveProductOfExperts | 0-2/9 | varies | Same PoE position atom problem + added complexity |
| Contrastive | 9/9 | 0.47 | Direct specialization + unfactored reconstruction |
Note: Contrastive has lower mean specialization than ISTA (0.47 vs 0.70) but passes more tests. This is because contrastive optimizes for clean atom-rule alignment, not maximum specialization score — it produces moderate but consistent specialization across all atoms rather than a few highly specialized atoms with poor composition behavior.
The contrastive architecture resolves the core tension between specialization and composition:
-
Direct specialization pressure: For each atom, computes mean activation per rule and penalizes multi-rule activation. This pushes atoms toward single-rule specialization without changing the reconstruction architecture.
-
Unfactored reconstruction: All atoms live in one codebook and participate equally in reconstruction. When gravity+bounce events arrive at test time, gravity atoms fire (they see gravity features) and bounce atoms fire (they see bounce features). The union of activated atoms matches the individual rule atom sets → high Jaccard.
-
No position atom problem: Unlike PoE, there are no separate position atoms that activate differently on composition vs training data. Every atom is simultaneously a "rule" atom and a "position" atom — the contrastive loss just shapes which events each atom responds to.
PoE factoring separates atoms into rule and position groups, then multiplies their reconstructions. For Jaccard, we compute activated atom sets over all atoms (rule + position). Position atoms activate based on spatial patterns that differ between training data (single-rule events) and composition data (multi-rule events in new spatial configurations). This produces systematic Jaccard failures as rule count grows.
Gravity is the most spatially diverse rule — objects fall from any height at any column. A flat dictionary must allocate many atoms to cover this variation, inflating |A| in the Jaccard denominator. Contrastive handles this with higher sparsity (0.05) so fewer atoms activate per event, keeping Jaccard clean.
- Scale: The micro-world has 5 rules and 5 object types. Scaling to dozens of rules with continuous physics remains untested.
- Non-linear interactions: Current compositions are additive (rules act independently in sequence). Interactions where Rule A modifies Rule B's behavior would require different evaluation.
- Temporal sequences: All events are single-step. Causal chains (A causes B causes C) are not tested.
- Contrastive requires labels: The specialization loss uses rule labels during training. Truly unsupervised discovery of causal structure remains an open problem.
git clone https://github.com/rafikchemli/agi-experiment.git
cd agi-experiment
make init # installs uv, syncs all dependenciesRequires Python >= 3.12. All dependencies are managed via uv.
# Default: Contrastive, raw encoding, 10 atoms, seed 42
make experiment
# Baseline comparison (ISTA vs Contrastive side-by-side)
make experiment ARGS="--compare"
# All architectures compared (produces all_models_comparison.png)
make experiment ARGS="--all-models"
# Try different architectures
make experiment ARGS="--arch ista"
make experiment ARGS="--arch product-of-experts --n-atoms 8"
make experiment ARGS="--arch contrastive --n-atoms 10 --sparsity 0.05"
# Custom configuration
make experiment ARGS="--n-atoms 15 --sparsity 0.08 --epochs 200 --n-events 5000 --seed 7"Output includes:
- Atom-rule affinity matrix (5 rules x k atoms)
- Per-atom specialization scores
- All 9 composition tests with reconstruction ratios and Jaccard similarities
- Training loss curve, heatmap, and test visualization saved to
experiments/causal_dictionaries/results/ - With
--compare: side-by-side comparison chart saved asresults/comparison.png - With
--all-models: 3-panel comparison across all architectures saved asresults/all_models_comparison.png
make check # format + lint + typecheck + tests
make test # just testsexperiments/causal_dictionaries/
├── micro_world.py # 5x5 grid physics engine — 5 rules, event generation
├── event_encoding.py # Raw 18d encoding (no hand-designed features)
├── sparse_dictionary.py # ISTA sparse coding — inference + Hebbian learning
├── architectures.py # ContrastiveDictionary, ProductOfExperts, and variants
├── learned_encoder.py # Optional: MLP autoencoder for learned representations
├── analysis.py # Atom specialization, reconstruction ratio, Jaccard
├── run.py # End-to-end experiment runner with CLI
├── visualize_world.py # Grid world state visualization
└── results/
├── poc_results.json # Machine-readable results
├── poc_results.png # 4-panel visualization (contrastive)
├── comparison.png # ISTA vs Contrastive side-by-side
└── world_rules.png # Micro-world rule illustrations
- Olshausen, B. A. & Field, D. J. (1996). Emergence of simple-cell receptive field properties by learning a sparse code for natural images. Nature, 381, 607-609.
- Gregor, K. & LeCun, Y. (2010). Learning fast approximations of sparse coding. Proceedings of ICML.
- Hinton, G. E. (2002). Training products of experts by minimizing contrastive divergence. Neural Computation, 14(8), 1771-1800.
- Scholkopf, B. et al. (2021). Toward causal representation learning. Proceedings of the IEEE, 109(5), 612-634.
MIT



