shayekh's picture
download
raw
8.51 kB
#!/usr/bin/env python
"""
Claim 5: Causal steering. MFA centroid interventions (Eq 14) steer generations toward
a target concept; the paper reports MFA ~2x the median steering score of SAE/DiffMeans
on Gemma. We run a scaled MFA-vs-DiffMeans comparison (SAE requires Gemmascope +
Neuronpedia infra; documented separately).
Protocol (mirrors the paper / AxBench, scaled down):
- prompt "<BOS> I think that", sweep alpha, sample completions, score with an LLM
judge on concept alignment (0-2) and fluency (0-2); final = harmonic mean.
- MFA: f_mu(x) = (1-alpha) x + alpha * mu_k (Eq 14, released MFASteerer)
- DiffMeans: x + alpha * dhat, where d = mean_resid(concept tokens) - mean_resid(all)
(paper builds DiffMeans from difference of average token representations)
This script only GENERATES completions; judging is done by claim5_judge.py.
"""
import os, sys, json, time, argparse, collections
import torch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "repo"))
from modeling.model_checkpointing import load_mfa
from intervention.mfa_steering import MFASteerer
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ckpt", required=True)
ap.add_argument("--model", default="gemma-2-2b")
ap.add_argument("--layer", type=int, default=18)
ap.add_argument("--n_concepts", type=int, default=12)
ap.add_argument("--samples", type=int, default=6)
ap.add_argument("--max_new", type=int, default=36)
ap.add_argument("--claim2", default="outputs/claim2_gemma_l18.json")
ap.add_argument("--out", default="outputs/claim5_completions.json")
args = ap.parse_args()
device = "cuda"
torch.manual_seed(0)
t0 = time.time()
from transformer_lens import HookedTransformer
from datasets import load_dataset
tl = HookedTransformer.from_pretrained(args.model, device=device, dtype=torch.float32)
mfa = load_mfa(args.ckpt, map_location=device).eval()
tok = tl.tokenizer
hook = f"blocks.{args.layer}.hook_resid_post"
# ---- build per-token-id mean residual for DiffMeans + global mean ----
D = mfa.D
sum_by_tok = collections.defaultdict(lambda: torch.zeros(D))
cnt_by_tok = collections.Counter()
gsum = torch.zeros(D); gcnt = 0
ds = load_dataset("NeelNanda/pile-10k", split="train", streaming=True)
seen = 0
for ex in ds:
toks = tl.to_tokens(ex["text"])[:, :64]
with torch.no_grad():
_, cache = tl.run_with_cache(toks, names_filter=hook, return_type=None)
a = cache[hook].reshape(-1, D).float().cpu()
tflat = toks.reshape(-1).cpu()
a, tflat = a[1:], tflat[1:]
gsum += a.sum(0); gcnt += a.shape[0]
for ti in tflat.unique().tolist():
m = tflat == ti
sum_by_tok[ti] += a[m].sum(0); cnt_by_tok[ti] += int(m.sum())
seen += a.shape[0]
if seen >= 120000:
break
gmean = (gsum / gcnt).to(device)
print(f"DiffMeans table built over {seen} tokens, {len(cnt_by_tok)} token types")
# ---- choose concepts: for each target theme, pick the centroid whose (ln_final)
# logit-lens best matches the theme's word list -> clean, interpretable regions. ----
W_U = tl.W_U
with torch.no_grad():
lens_top = (tl.ln_final(mfa.mu) @ W_U).topk(10, dim=-1).indices.cpu()
lens_words = [set(tok.decode([t]).strip().lower() for t in row.tolist()) for row in lens_top]
THEMES = {
"football/sport": ["football", "soccer", "hockey", "league", "tennis", "basketball", "sport"],
"quantum/physics": ["quantum", "physics", "particle", "energy", "atom", "theory"],
"freedom": ["free", "freedom", "liberty", "libre", "freely", "independence"],
"web/internet": ["web", "website", "site", "page", "online", "internet"],
"music": ["music", "song", "guitar", "album", "band", "musical", "melody"],
"money/finance": ["money", "financial", "bank", "dollar", "price", "cash", "economic"],
"war/military": ["war", "military", "army", "soldier", "battle", "weapon", "combat"],
"medical/health": ["medical", "health", "disease", "doctor", "patient", "hospital"],
"food": ["food", "eat", "cook", "meal", "recipe", "restaurant", "delicious"],
"religion": ["god", "church", "religious", "faith", "holy", "prayer", "spiritual"],
"law/legal": ["law", "legal", "court", "judge", "justice", "lawyer", "crime"],
"emotion": ["happy", "sad", "anger", "fear", "joy", "love", "emotion", "feeling"],
}
concepts = []
for theme, words in THEMES.items():
wset = set(words)
best, bestov = None, 0
for c in range(mfa.K):
ov = len(lens_words[c] & wset)
if ov > bestov:
bestov, best = ov, c
if best is not None and bestov >= 2: # require >=2 theme words in the centroid's top tokens
cw = [tok.decode([t]).strip() for t in lens_top[best].tolist()
if tok.decode([t]).strip().lower() in wset][:5]
concepts.append({"comp": best, "tokens": cw or words[:4], "kind": theme})
if len(concepts) >= args.n_concepts:
break
def diffmeans_dir(concept_tokens):
# token ids whose logit-lens/text form matches concept tokens: use ids of these words
ids = set()
for w in concept_tokens:
for pre in (" " + w, w, " " + w.capitalize(), w.capitalize()):
enc = tok.encode(pre, add_special_tokens=False)
if len(enc) == 1:
ids.add(enc[0])
vecs = [sum_by_tok[i] / cnt_by_tok[i] for i in ids if cnt_by_tok[i] > 0]
if not vecs:
return None
cmean = torch.stack(vecs).mean(0).to(device)
d = cmean - gmean
return d / d.norm().clamp_min(1e-6)
steerer = MFASteerer(tl, mfa, intervention_type="resid_post")
def gen_diffmeans(dir_unit, alpha, n):
toks = tl.to_tokens("I think that")
def h(value, hook):
return value + alpha * dir_unit.to(value.dtype)
outs = []
for _ in range(n):
o = tl.generate(toks, max_new_tokens=args.max_new, temperature=1.0,
do_sample=True, verbose=False, fwd_hooks=[(hook, h)])
outs.append(tl.to_string(o[0]))
return outs
def gen_mfa(k, alpha, n):
outs = []
for _ in range(n):
outs.append(steerer.generate("I think that", layers=[args.layer], alpha=alpha,
k=k, max_new_tokens=args.max_new, temperature=1.0,
do_sample=True))
return outs
# baseline (no steer)
def gen_base(n):
toks = tl.to_tokens("I think that")
return [tl.to_string(tl.generate(toks, max_new_tokens=args.max_new, temperature=1.0,
do_sample=True, verbose=False)[0]) for _ in range(n)]
mfa_alphas = [0.3, 0.5, 0.7, 0.9]
dm_scale = gmean.norm().item() # scale additive dir to activation magnitude
dm_alphas = [2.0*dm_scale, 4.0*dm_scale, 6.0*dm_scale, 8.0*dm_scale]
records = {"meta": {"model": args.model, "layer": args.layer,
"prompt": "I think that", "samples": args.samples,
"mfa_alphas": mfa_alphas, "dm_scales": dm_alphas,
"judge": "meta-llama/Llama-3.2-3B-Instruct (open sub for GPT-4o-mini)"},
"baseline": gen_base(args.samples), "concepts": []}
print("baseline done")
for ci, c in enumerate(concepts):
conc_str = ", ".join(c["tokens"])
entry = {"comp": c["comp"], "kind": c["kind"], "concept_tokens": c["tokens"],
"mfa": {}, "diffmeans": {}}
for a in mfa_alphas:
entry["mfa"][str(a)] = gen_mfa(c["comp"], a, args.samples)
dvec = diffmeans_dir(c["tokens"])
if dvec is not None:
for a in dm_alphas:
entry["diffmeans"][f"{a:.1f}"] = gen_diffmeans(dvec, a, args.samples)
records["concepts"].append(entry)
print(f"[{ci+1}/{len(concepts)}] comp {c['comp']} ({c['kind']}) concept='{conc_str}' done")
records["meta"]["wall_s"] = round(time.time() - t0, 1)
os.makedirs(os.path.dirname(args.out), exist_ok=True)
json.dump(records, open(args.out, "w"), indent=2)
print("saved", args.out, "wall", records["meta"]["wall_s"])
if __name__ == "__main__":
main()

Xet Storage Details

Size:
8.51 kB
·
Xet hash:
3d815854e8fe7b308fb8816703381c91f2ca3240ca4ff8dfeba6edbb696800e3

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.