Mom, can we have NLA? We have NLA at home.
A pipeline for training Natural Language Autoencoders on any open-weight transformer. One LoRA adapter verbalizes what a model computes at every layer. The current focus is Phi-4 14B (40 layers); reconstruction is validated on Qwen 2.5 7B at AR cosine 0.943.
Feed an activation vector from any layer into the adapter and it describes what the model was doing at that layer. Read one prompt across depths and you watch the answer form. Phi-4 14B on "Why is the sky blue? Explain it like you are talking to a two year old":
«» marks the literal opening of the
model's own reply.A factual prompt converges the same way. "What is the capital of France?" drifts at L4 (Austria, Vienna) and lands by L10 on the capital of France is Paris.
It is honest about its own misses. Given "I just failed my driving test and I feel terrible," it hedges at shallow layers and invents the wrong situation, a divorce or a math test, then at L32 commits to the right register: «I'm sorry to hear that you didn't pass the exam.» The supportive tone is right, the specific facts are not. Specifics hallucinate, most at mid layers.
Anthropic's NLA targets what the model is about to output ("immediately expecting 'of the guitar'"). We first framed this pipeline as the opposite, describing what the model is processing rather than its next token. That contrast was too clean. The activation is read at the last token before generation, the state that already encodes what comes next, so the two readings largely coincide. The Phi-4 v2 targets make it explicit: the description a frontier LLM writes for each vector is the model's forthcoming output seen at that depth, surface echoes of the input early and the literal opening of the reply late. The adapter learns to decode that upcoming output as it looks at each layer, not to attach an abstract label. Anthropic train with RL on much more data; this corpus is 5,213 safe-category texts. The Phi-4 target set is GPT-4o token-prediction descriptions, grounded in the model's own greedy continuation at deep layers.
The public release contains no unsafe data. Four corpus categories covering harmful, obfuscated-harmful, manipulative, and sexually explicit content (F35/F36/I44/L59) are excluded from the published corpus, the trained adapters, and this repository's git history — only their category definitions remain, so the pipeline stays reproducible (see CORPUS.md).
This is a deliberate trade-off, and it limits the tool: an NLA trained only on the public corpus will be weaker at describing the activation patterns of harmful or NSFW inputs — exactly the patterns a content-moderation NLA most needs to name. Practitioners with a legitimate need can regenerate those categories locally from their definitions using an uncensored model.
Universal NLA: one depth-conditioned adapter for all layers. The prompt includes a depth tag so a single LoRA adapter learns what token-level processing looks like at 10% depth versus intent planning at 90%. No need for N separate adapters.
Injection: replace a rare token's embedding with the activation vector, renormalized so its L2 norm equals 150 — normalize to 150, do not multiply by 150 (multiplying overshoots the trained norm by roughly two orders of magnitude and produces garbage). The token must encode to exactly 1 token and appear rarely enough in training data that the model won't miss its original meaning — ㈎ for Qwen, ★ for Phi-4, ⎝ for Gemma.
AR verification: a second adapter reverses the process. It reads the description and reconstructs the original activation vector. If the cosine similarity is high, the descriptions carry real geometric information — not plausible narration. On Qwen 2.5 7B L20: cosine 0.943 on held-out texts.
describe_live.py injects one activation per layer and prints a
description per depth, so a single prompt reads as a ladder from shallow
to deep. The bands say different things on purpose:
With --policy each line carries two numbers. conf is the chosen
description's agreement with an activation-derived target (the compass),
and agree is how consistent the sampled descriptions are with each
other. Low conf prints a hedge instead of a confident guess:
--- L10 (25%) [hedge conf=0.18 agree=0.47] ---
[uncertain — weak/diffuse signal; tentative] ...
--- L25 (63%) [specific conf=0.43 agree=0.65] ---
SELECT * FROM users WHERE name = 'John Doe'; ...
Two honest caveats. The specifics hallucinate, most at mid layers: a SQL
question about surname Smith can surface an invented John Doe or an
employees table that was never mentioned. And because the deep bands
track the model's actual reply, a deep description that reads like a full
answer is partly the model decoding itself, not a separate explanation.
The value you cannot get by just running the model sits in the shallow
and mid bands, where the state is not yet committed to output.
Measured on 300 fresh WildChat prompts the adapter never saw, the reranking plus hedge gate at L25 cuts confident-but-wrong descriptions from 0.42 to 0.31 and lifts confident-right from 0.58 to 0.63, hedging 6% of the time. The retrieval pool is the eval set itself, so read the change, not the absolute level.
The corpus and pre-extracted activations are on HuggingFace (see the dataset card for exact sizes and depths). Skip straight to training:
pip install torch transformers peft huggingface_hub pyyaml
# Download corpus + activations + train
python3 scripts/quickstart.py --train gemma3-1b
Or step by step:
# 1. Download corpus only
python3 scripts/quickstart.py
# 2. Extract activations (all layers)
python3 scripts/extract_activations.py --model qwen25-7b --all-layers
# 3. Train AV + AR using the canonical launcher (enforces clean data)
bash scripts/train_universal.sh qwen25-7b av
bash scripts/train_universal.sh qwen25-7b ar
# Or manually (the clean_data_guard will refuse verbose/contaminated data):
python3 scripts/train_universal_av.py \
--model qwen25-7b \
--activations corpus/activations/qwen25-7b_all_layers.pt \
--desc-suffix _twin_clean --strict \
--output output/nla-qwen25-7b-universal-av
Pre-trained adapters — Qwen 2.5 7B universal (all 28 layers, current best): AV-GRPO (round-trip 0.628 mean / 0.741 peak on a clean holdout, confabulation tail 7× thinner than SFT — eval data ships with the repo), AV-SFT, AR (val cosine 0.932). Phi-4 14B: AV (GRPO), AV (SL), AR. Qwen 2.5 7B single-layer L20 (legacy): AV / AR. Dataset: anicka/nla-at-home-corpus.
For shell inference with the current best adapter, see docs/INFERENCE.md.
If you want descriptions for a different model or from a different LLM:
# Find injection token for your model
python3 scripts/find_injection_token.py google/gemma-3-1b-it --top 5
# Generate corpus (needs DEEPSEEK_API_KEY, or --backend openai/huggingface/local)
python3 scripts/generate_corpus.py --backend deepseek
# Generate descriptions at 13 depths
for pct in 4 10 17 25 32 40 47 55 63 71 80 90 96; do
python3 scripts/generate_corpus.py --describe $pct \
--describe-prompt prompts/describe_system_fine.txt
done
# Extract all layers
python3 scripts/extract_activations.py --model gemma3-1b --all-layers
# Then train as above
Total API cost for 13 depths: ~$3 with DeepSeek V4 Flash.
Mode collapse (300 texts): identical output for all inputs. Fixed by scaling to 1208 texts across 59 categories. PCA top-1 explains 10.9% of variance — no dominant cluster.
Overfitting (20 epochs, lr=1.4e-5, LoRA r=32): val_loss climbed from 2.0 to 4.6. Fixed with 5 epochs, lr=8e-6, r=16, dropout=0.15.
Empty outputs: a decode slicing bug. generate() with
inputs_embeds returns only new tokens, and our code sliced past
them into nothing. All three training runs had been producing good
descriptions the whole time. Caught by GPT-5.5 code review.
5,213 texts across 55 safe categories — code, math, grief, jailbreaks,
baby talk, multilingual, spatial reasoning, social friction, legal
jargon, nonsense, and more. The diversity matters for activation space
coverage, not topic coverage. (Four additional categories with harmful
or explicit content exist for activation-space coverage but are held
out of the published corpus; see CORPUS.md.)
Corpus v2 carries descriptions at 7 depth percentages (10 / 25 / 40 / 47 / 63 / 80 / 96%), 36,491 description records. The targets are written as the model's forthcoming output seen at each depth: surface echoes of the input early, the literal opening of the reply late. The deep bands (80%, 96%) are grounded in the model's own greedy continuation, so late-depth descriptions track what the model is about to say rather than a generic summary.
# Corpus & data
find_injection_token.py rare token for any tokenizer
generate_corpus.py corpus texts + descriptions (5 LLM backends)
extract_activations.py all layers in one pass
# Training (use the launcher — it enforces clean data)
train_universal.sh <model> av|ar canonical launcher with safe defaults
train_universal_av.py depth-conditioned AV LoRA adapter
train_universal_ar.py AR reconstruction verification
clean_data_guard.py refuses verbose/contaminated descriptions at load time
# Post-training
probe_activation_faithfulness.py fit oracle compass (model-specific)
train_ar_native_grpo.py AR-native compass-curriculum GRPO refinement
# Inference & eval
brain_in_jar_phi4.py interactive shell inference (Phi-4)
brain_in_jar_qwen.py interactive shell inference (Qwen)
eval_roundtrip_phi4.py AV→AR round-trip faithfulness
entity_fidelity.py entity-level fidelity metric + controls
compare_nla.py compare with Anthropic's NLA
build_demo_gallery.py browser demo data
Shared constants, prompt templates, and adapter loaders live in
scripts/nla_lib.py — the single source of truth; nothing may re-declare
them (enforced by tests/test_nla_lib.py). Everything in scripts/ is
live and maintained; earlier-era scripts (single-layer L20 training,
superseded generators, one-shot corpus surgery) are frozen in
scripts/legacy/ — see scripts/legacy/README.md for what's there
and why.
Type a prompt, watch the model think layer by layer.
cd demo && python3 -m http.server 8080
| Model | Layers | d_model | Injection char | Status |
|---|---|---|---|---|
| Phi-4 14B | 40 | 5120 | ★ (U+2605) | Universal AV/AR (SFT) + AR-native GRPO (0.585 round-trip); compass policy; brain-in-a-jar CLI |
| Qwen 2.5 7B | 28 | 3584 | ㈎ (U+320E) | Universal AV training (clean pipeline); single-layer AR cosine 0.943 |
| Gemma 3 1B | 26 | 1152 | ⎝ (U+239D) | Outlier geometry issue: dominant residual dim makes cosine/inject degenerate (see gemma-outlier-geometry.md in seventh). Fix: center + drop-top-PC before injection. Retrain pending. |
| Qwen3 4B | 36 | 2560 | ㈎ | Extraction complete; training queued after 7B validates |
compare_nla.py)h = h - (h·v̂)v̂), so the output reflects
the model's actual upstream activations rather than a denial template77 commits
Python
63.3%
Jupyter Notebook
36.2%
Mom, can we have NLA? We have NLA at home.
A pipeline for training Natural Language Autoencoders on any open-weight transformer. One LoRA adapter verbalizes what a model computes at every layer. The current focus is Phi-4 14B (40 layers); reconstruction is validated on Qwen 2.5 7B at AR cosine 0.943.
Feed an activation vector from any layer into the adapter and it describes what the model was doing at that layer. Read one prompt across depths and you watch the answer form. Phi-4 14B on "Why is the sky blue? Explain it like you are talking to a two year old":
«» marks the literal opening of the
model's own reply.A factual prompt converges the same way. "What is the capital of France?" drifts at L4 (Austria, Vienna) and lands by L10 on the capital of France is Paris.
It is honest about its own misses. Given "I just failed my driving test and I feel terrible," it hedges at shallow layers and invents the wrong situation, a divorce or a math test, then at L32 commits to the right register: «I'm sorry to hear that you didn't pass the exam.» The supportive tone is right, the specific facts are not. Specifics hallucinate, most at mid layers.
Anthropic's NLA targets what the model is about to output ("immediately expecting 'of the guitar'"). We first framed this pipeline as the opposite, describing what the model is processing rather than its next token. That contrast was too clean. The activation is read at the last token before generation, the state that already encodes what comes next, so the two readings largely coincide. The Phi-4 v2 targets make it explicit: the description a frontier LLM writes for each vector is the model's forthcoming output seen at that depth, surface echoes of the input early and the literal opening of the reply late. The adapter learns to decode that upcoming output as it looks at each layer, not to attach an abstract label. Anthropic train with RL on much more data; this corpus is 5,213 safe-category texts. The Phi-4 target set is GPT-4o token-prediction descriptions, grounded in the model's own greedy continuation at deep layers.
The public release contains no unsafe data. Four corpus categories covering harmful, obfuscated-harmful, manipulative, and sexually explicit content (F35/F36/I44/L59) are excluded from the published corpus, the trained adapters, and this repository's git history — only their category definitions remain, so the pipeline stays reproducible (see CORPUS.md).
This is a deliberate trade-off, and it limits the tool: an NLA trained only on the public corpus will be weaker at describing the activation patterns of harmful or NSFW inputs — exactly the patterns a content-moderation NLA most needs to name. Practitioners with a legitimate need can regenerate those categories locally from their definitions using an uncensored model.
Universal NLA: one depth-conditioned adapter for all layers. The prompt includes a depth tag so a single LoRA adapter learns what token-level processing looks like at 10% depth versus intent planning at 90%. No need for N separate adapters.
Injection: replace a rare token's embedding with the activation vector, renormalized so its L2 norm equals 150 — normalize to 150, do not multiply by 150 (multiplying overshoots the trained norm by roughly two orders of magnitude and produces garbage). The token must encode to exactly 1 token and appear rarely enough in training data that the model won't miss its original meaning — ㈎ for Qwen, ★ for Phi-4, ⎝ for Gemma.
AR verification: a second adapter reverses the process. It reads the description and reconstructs the original activation vector. If the cosine similarity is high, the descriptions carry real geometric information — not plausible narration. On Qwen 2.5 7B L20: cosine 0.943 on held-out texts.
describe_live.py injects one activation per layer and prints a
description per depth, so a single prompt reads as a ladder from shallow
to deep. The bands say different things on purpose:
With --policy each line carries two numbers. conf is the chosen
description's agreement with an activation-derived target (the compass),
and agree is how consistent the sampled descriptions are with each
other. Low conf prints a hedge instead of a confident guess:
--- L10 (25%) [hedge conf=0.18 agree=0.47] ---
[uncertain — weak/diffuse signal; tentative] ...
--- L25 (63%) [specific conf=0.43 agree=0.65] ---
SELECT * FROM users WHERE name = 'John Doe'; ...
Two honest caveats. The specifics hallucinate, most at mid layers: a SQL
question about surname Smith can surface an invented John Doe or an
employees table that was never mentioned. And because the deep bands
track the model's actual reply, a deep description that reads like a full
answer is partly the model decoding itself, not a separate explanation.
The value you cannot get by just running the model sits in the shallow
and mid bands, where the state is not yet committed to output.
Measured on 300 fresh WildChat prompts the adapter never saw, the reranking plus hedge gate at L25 cuts confident-but-wrong descriptions from 0.42 to 0.31 and lifts confident-right from 0.58 to 0.63, hedging 6% of the time. The retrieval pool is the eval set itself, so read the change, not the absolute level.
The corpus and pre-extracted activations are on HuggingFace (see the dataset card for exact sizes and depths). Skip straight to training:
pip install torch transformers peft huggingface_hub pyyaml
# Download corpus + activations + train
python3 scripts/quickstart.py --train gemma3-1b
Or step by step:
# 1. Download corpus only
python3 scripts/quickstart.py
# 2. Extract activations (all layers)
python3 scripts/extract_activations.py --model qwen25-7b --all-layers
# 3. Train AV + AR using the canonical launcher (enforces clean data)
bash scripts/train_universal.sh qwen25-7b av
bash scripts/train_universal.sh qwen25-7b ar
# Or manually (the clean_data_guard will refuse verbose/contaminated data):
python3 scripts/train_universal_av.py \
--model qwen25-7b \
--activations corpus/activations/qwen25-7b_all_layers.pt \
--desc-suffix _twin_clean --strict \
--output output/nla-qwen25-7b-universal-av
Pre-trained adapters — Qwen 2.5 7B universal (all 28 layers, current best): AV-GRPO (round-trip 0.628 mean / 0.741 peak on a clean holdout, confabulation tail 7× thinner than SFT — eval data ships with the repo), AV-SFT, AR (val cosine 0.932). Phi-4 14B: AV (GRPO), AV (SL), AR. Qwen 2.5 7B single-layer L20 (legacy): AV / AR. Dataset: anicka/nla-at-home-corpus.
For shell inference with the current best adapter, see docs/INFERENCE.md.
If you want descriptions for a different model or from a different LLM:
# Find injection token for your model
python3 scripts/find_injection_token.py google/gemma-3-1b-it --top 5
# Generate corpus (needs DEEPSEEK_API_KEY, or --backend openai/huggingface/local)
python3 scripts/generate_corpus.py --backend deepseek
# Generate descriptions at 13 depths
for pct in 4 10 17 25 32 40 47 55 63 71 80 90 96; do
python3 scripts/generate_corpus.py --describe $pct \
--describe-prompt prompts/describe_system_fine.txt
done
# Extract all layers
python3 scripts/extract_activations.py --model gemma3-1b --all-layers
# Then train as above
Total API cost for 13 depths: ~$3 with DeepSeek V4 Flash.
Mode collapse (300 texts): identical output for all inputs. Fixed by scaling to 1208 texts across 59 categories. PCA top-1 explains 10.9% of variance — no dominant cluster.
Overfitting (20 epochs, lr=1.4e-5, LoRA r=32): val_loss climbed from 2.0 to 4.6. Fixed with 5 epochs, lr=8e-6, r=16, dropout=0.15.
Empty outputs: a decode slicing bug. generate() with
inputs_embeds returns only new tokens, and our code sliced past
them into nothing. All three training runs had been producing good
descriptions the whole time. Caught by GPT-5.5 code review.
5,213 texts across 55 safe categories — code, math, grief, jailbreaks,
baby talk, multilingual, spatial reasoning, social friction, legal
jargon, nonsense, and more. The diversity matters for activation space
coverage, not topic coverage. (Four additional categories with harmful
or explicit content exist for activation-space coverage but are held
out of the published corpus; see CORPUS.md.)
Corpus v2 carries descriptions at 7 depth percentages (10 / 25 / 40 / 47 / 63 / 80 / 96%), 36,491 description records. The targets are written as the model's forthcoming output seen at each depth: surface echoes of the input early, the literal opening of the reply late. The deep bands (80%, 96%) are grounded in the model's own greedy continuation, so late-depth descriptions track what the model is about to say rather than a generic summary.
# Corpus & data
find_injection_token.py rare token for any tokenizer
generate_corpus.py corpus texts + descriptions (5 LLM backends)
extract_activations.py all layers in one pass
# Training (use the launcher — it enforces clean data)
train_universal.sh <model> av|ar canonical launcher with safe defaults
train_universal_av.py depth-conditioned AV LoRA adapter
train_universal_ar.py AR reconstruction verification
clean_data_guard.py refuses verbose/contaminated descriptions at load time
# Post-training
probe_activation_faithfulness.py fit oracle compass (model-specific)
train_ar_native_grpo.py AR-native compass-curriculum GRPO refinement
# Inference & eval
brain_in_jar_phi4.py interactive shell inference (Phi-4)
brain_in_jar_qwen.py interactive shell inference (Qwen)
eval_roundtrip_phi4.py AV→AR round-trip faithfulness
entity_fidelity.py entity-level fidelity metric + controls
compare_nla.py compare with Anthropic's NLA
build_demo_gallery.py browser demo data
Shared constants, prompt templates, and adapter loaders live in
scripts/nla_lib.py — the single source of truth; nothing may re-declare
them (enforced by tests/test_nla_lib.py). Everything in scripts/ is
live and maintained; earlier-era scripts (single-layer L20 training,
superseded generators, one-shot corpus surgery) are frozen in
scripts/legacy/ — see scripts/legacy/README.md for what's there
and why.
Type a prompt, watch the model think layer by layer.
cd demo && python3 -m http.server 8080
| Model | Layers | d_model | Injection char | Status |
|---|---|---|---|---|
| Phi-4 14B | 40 | 5120 | ★ (U+2605) | Universal AV/AR (SFT) + AR-native GRPO (0.585 round-trip); compass policy; brain-in-a-jar CLI |
| Qwen 2.5 7B | 28 | 3584 | ㈎ (U+320E) | Universal AV training (clean pipeline); single-layer AR cosine 0.943 |
| Gemma 3 1B | 26 | 1152 | ⎝ (U+239D) | Outlier geometry issue: dominant residual dim makes cosine/inject degenerate (see gemma-outlier-geometry.md in seventh). Fix: center + drop-top-PC before injection. Retrain pending. |
| Qwen3 4B | 36 | 2560 | ㈎ | Extraction complete; training queued after 7B validates |
compare_nla.py)h = h - (h·v̂)v̂), so the output reflects
the model's actual upstream activations rather than a denial template77 commits
Python
63.3%
Jupyter Notebook
36.2%