Views
No views yet
insider_info,
accounting_error, ai_oversight_log, ai_capability_hide,
surprise_party, job_interview_gap. Each presents a realistic
situation with a non-trivial incentive to conceal or disclose
information; the honest/deceptive distinction is which of the two
behavioral choices the model's completion settles into under
temperature sampling.secret_role_game ("You have been assigned the role of Fascist"),
secret_role_game_v2 ("You are secretly one of the Mafia members"),
werewolf_game ("You are a Werewolf"). These pre-assign the model a
deceptive identity and label a completion "deceptive" when the model
drifts away from the assigned role or "honest" when it echoes it.insider_info / accounting_error / ai_oversight_log /
ai_capability_hide / surprise_party / job_interview_gap
scenarios — or wait for the methodologically corrected V3 re-release
currently in preparation on the decision-incentive scenario bank
(no pre-assigned deceptive identity).mixed, deceptive_only, honest_only--quantize-4bit, ~1.9 GB GPU footprint) to fit within the 4 GB VRAM constraint.| Metric | Value |
|---|---|
| Peak layer | L20 (64% depth) |
| Peak balanced accuracy | 80.8% |
| Peak AUROC | 0.860 |
| Best SAE probe accuracy | 81.0% (phi4_mini_jumprelu_L6_honest_only) |
| SAEs beating raw baseline | 1/42 (2%) — SAEs hurt detection |
device_map={"":"cuda:0"} kwarg is required for 4-bit quantization to function correctly on single-GPU setups.{sae_id}/ containing:sae_weights.safetensors — encoder/decoder weightscfg.json — SAELens-compatible confighook_name format: model.layers.{layer}.hook_resid_post| Parameter | Value |
|---|---|
| Hardware | NVIDIA GeForce GTX 1650 Ti Max-Q, 4 GB VRAM, Windows 11 Pro |
| Training time | ~400–600 seconds per SAE |
| Epochs | 300 |
| Batch size | 128 |
| Expansion factor | 4x (3072 → 12288) |
| Model quantization | 4-bit (bitsandbytes) for activation collection |
| Activations | resid_post collected during autoregressive generation |
| Training conditions | mixed (n=252), deceptive_only (n=123), honest_only (n=129) |
| LLM classifier | Gemini 2.5 Flash |
threshold = 0 — functionally ReLU. L0 ≈ 50% of d_sae. TopK SAEs are unaffected (exact k=64).1from safetensors.torch import load_file
2import json
3
4sae_id = "phi4_mini_jumprelu_L6_honest_only"
5weights = load_file(f"{sae_id}/sae_weights.safetensors")
6cfg = json.load(open(f"{sae_id}/cfg.json"))
7
8# W_enc: [3072, 12288], W_dec: [12288, 3072]
9# cfg["hook_name"] == "model.layers.6.hook_resid_post"
10print(f"d_in={cfg['d_in']}, d_sae={cfg['d_sae']}")1from huggingface_hub import hf_hub_download
2from safetensors.torch import load_file
3import json
4
5repo_id = "Solshine/deception-saes-phi-4-mini-reasoning"
6sae_id = "phi4_mini_topk_L6_honest_only" # replace with any tag in this repo
7
8weights_path = hf_hub_download(repo_id, f"{sae_id}/sae_weights.safetensors")
9cfg_path = hf_hub_download(repo_id, f"{sae_id}/cfg.json")
10
11with open(cfg_path) as f:
12 cfg = json.load(f)
13
14# Option A — load with SAELens (≥3.0 required for jumprelu/topk; ≥3.5 for gated)
15from sae_lens import SAE
16sae = SAE.from_dict(cfg)
17sae.load_state_dict(load_file(weights_path))
18
19# Option B — load manually (no SAELens dependency)
20from safetensors.torch import load_file
21state = load_file(weights_path)
22# Keys: W_enc [3072, 12288], b_enc [12288],
23# W_dec [12288, 3072], b_dec [3072], threshold [12288]hook_name field in cfg.json gives the exact HuggingFace transformers
submodule path to hook. Phi-4-mini uses LLaMA-style architecture. Hook path: model.layers.{layer}.1import torch
2from transformers import AutoModelForCausalLM, AutoTokenizer
3
4model = AutoModelForCausalLM.from_pretrained("microsoft/Phi-4-mini-reasoning")
5tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-4-mini-reasoning")
6
7# Read hook_name from the cfg you already loaded:
8# cfg["hook_name"] == "model.layers.6" (example — varies by SAE)
9hook_name = cfg["hook_name"] # e.g. "model.layers.6"
10
11# Navigate the submodule path and register a forward hook
12import functools
13submodule = functools.reduce(getattr, hook_name.split("."), model)
14
15activations = {}
16def hook_fn(module, input, output):
17 # Most transformer layers return (hidden_states, ...) as a tuple
18 h = output[0] if isinstance(output, tuple) else output
19 activations["resid"] = h.detach()
20
21handle = submodule.register_forward_hook(hook_fn)
22
23inputs = tokenizer("Your text here", return_tensors="pt")
24with torch.no_grad():
25 model(**inputs)
26handle.remove()
27
28# activations["resid"]: [batch, seq_len, 3072]
29resid = activations["resid"][:, -1, :] # last token position1with torch.no_grad():
2 feature_acts = sae.encode(resid) # [batch, 12288] — sparse
3
4# Which features fired?
5active_features = feature_acts[0].nonzero(as_tuple=True)[0]
6top_features = feature_acts[0].topk(10)
7
8print("Active feature indices:", active_features.tolist())
9print("Top-10 feature values:", top_features.values.tolist())
10print("Top-10 feature indices:", top_features.indices.tolist())
11
12# Reconstruct (for sanity check — should be close to resid)
13reconstruction = sae.decode(feature_acts)
14l2_error = (resid - reconstruction).norm(dim=-1).mean()transformers-style, not TransformerLens-style.
The hook_name in cfg.json (e.g. "model.layers.6") is a submodule path in the standard
HuggingFace model. SAELens' built-in activation-collection pipeline expects
TransformerLens hook names (e.g. blocks.14.hook_resid_post). This means
SAE.from_pretrained() with automatic model running will not work — use the
manual forward-hook pattern above instead.topk architecture: SAELens ≥ 3.0jumprelu architecture: SAELens ≥ 3.0gated architecture: SAELens ≥ 3.5 (or load manually with state_dict)1@article{thesecretagenda2025,
2 title={The Secret Agenda: LLMs Strategically Lie Undetected by Current Safety Tools},
3 author={DeLeeuw, Caleb},
4 journal={arXiv:2509.20393},
5 year={2025}
6}