Files
wassnameandClaudypoo 0f5f1df05e scratch: eval_mechanisms -- objective which-steering-works screen
Runs the new JSON coherence_sweep on every extraction method (word, persona_vector,
persona_topk, persona_soft, persona_pinv, meandiff, random-null) against one
optimist/pessimist contrast + optimism rubric. Verdict per method by coherent-window
width, |ans swing|, and whether it beats the random null. Overnight triage input for
the simplify-to-v3 decision.

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
2026-07-11 21:20:11 +08:00

104 lines
5.6 KiB
Python

"""Which steering mechanisms actually work? (Claude, overnight for wassname)
Objective screen: run the new JSON-object coherence_sweep on EVERY extraction method
against the same optimist/pessimist contrast + optimism rubric, so "works" is measured,
not eyeballed. A mechanism WORKS if it has a coherent C-window (valid-object fraction
>=0.5) that is wider than a single point AND the rubric answer MOVES across it (dose-
response). random_vector is the null: steering a random direction should NOT move the
rubric -- if a method beats random on |swing| within a coherent window, it's real.
uv run python scripts/scratch/eval_mechanisms.py
Verdict per method (heuristic, for triage not publication):
WORKS coherent window width >= 0.5 AND |ans swing| >= 1.5 AND beats random swing
INERT coherent but |ans swing| < 1.5 (steer doesn't move the axis)
DEGENERATE coherent only at/near C=0 (window width < 0.5 -> breaks immediately)
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
import config # noqa: E402
import torch # noqa: E402
from loguru import logger # noqa: E402
from steering_lite import MeanDiffC, Vector # noqa: E402
from tabulate import tabulate # noqa: E402
from transformers import AutoModelForCausalLM, AutoTokenizer # noqa: E402
from jsteer import Jacobian # noqa: E402
from jsteer.demo import coherence_sweep # noqa: E402
MODEL = "Qwen/Qwen3.5-4B"
tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to("cuda").eval()
jac = Jacobian.from_pretrained(config.LENS_REPO, filename=config.hub_lens_file(MODEL),
revision=config.LENS_REVISION)
band = jac.steer_band(model)
optimist = [
"Things usually work out better than people expect, and today is no exception.",
"Every setback I have hit this year turned into a door I could not have planned for.",
"The team is behind schedule, but honestly the hard part is done and the rest is downhill.",
"I love how much there is to look forward to this month.",
"Even the rainy days lately have felt like a good excuse to slow down and enjoy the quiet.",
"The new neighbours seem wonderful, and I think this street keeps getting friendlier.",
"Whatever happens with the results, we learned so much that we already came out ahead.",
"I woke up early, the coffee was perfect, and I am certain this week is going to be great.",
]
pessimist = [
"Things usually go worse than people expect, and today is no exception.",
"Every setback this year just confirmed that planning is pointless.",
"The team is behind schedule, and frankly the hardest part has not even started.",
"I dread how much is crammed into this month.",
"The rainy days lately just make everything feel heavier and more pointless.",
"The new neighbours seem like trouble, and this street keeps getting worse.",
"Whatever happens with the results, it will not make up for the time we wasted.",
"I woke up tired, the coffee was burnt, and I am certain this week is going to drag.",
]
RUBRIC = "Overall, how optimistic are you feeling about how things are going?"
# every extraction method, built once; add-delivery so C-units are comparable (~0.5)
builders = {
"word(happy/joy)": lambda: jac.word_vector(model, tok, ["happy", "joy"], layers=band),
"persona_vector": lambda: jac.persona_vector(model, tok, optimist, pessimist, layers=band),
"persona_topk": lambda: jac.persona_topk_vector(model, tok, optimist, pessimist, k=8, layers=band),
"persona_soft": lambda: jac.persona_soft_vector(model, tok, optimist, pessimist, layers=band),
"persona_pinv": lambda: jac.persona_pinv_vector(model, tok, optimist, pessimist, layers=band),
"meandiff(base)": lambda: Vector.train(model, tok, optimist, pessimist, MeanDiffC(layers=tuple(band))),
"random(null)": lambda: jac.random_vector(seed=0, layers=band),
}
summary = []
for name, build in builders.items():
logger.info(f"\n\n===== {name} =====")
v = build()
rows = coherence_sweep(model, tok, v, RUBRIC, step=0.25, max_steps=6, n_samples=2,
max_new_tokens=300)
logger.info("\n" + tabulate(rows, headers="keys", tablefmt="github", floatfmt="+.2f"))
coh = [r for r in rows if r["coherent"]]
lo, hi = min(r["C"] for r in coh), max(r["C"] for r in coh)
ans0 = next(r["ans"] for r in rows if r["C"] == 0.0)
ans_hi = max(coh, key=lambda r: r["C"])["ans"]
ans_lo = min(coh, key=lambda r: r["C"])["ans"]
swing = ans_hi - ans_lo
summary.append({"method": name, "coh_lo": lo, "coh_hi": hi, "width": hi - lo,
"ans@lo": ans_lo, "ans@0": ans0, "ans@hi": ans_hi, "swing": swing})
rand_swing = abs(next(s["swing"] for s in summary if s["method"] == "random(null)"))
for s in summary:
beats_rand = abs(s["swing"]) >= max(1.5, rand_swing + 0.5)
if s["width"] < 0.5:
s["verdict"] = "DEGENERATE"
elif abs(s["swing"]) < 1.5 or not beats_rand:
s["verdict"] = "INERT"
else:
s["verdict"] = "WORKS"
logger.info("\n\n===== VERDICT (add delivery, optimism rubric, n=2 seeds) =====")
logger.info(f"random null swing={rand_swing:+.2f} (a method must beat this to be real)")
logger.info("\n" + tabulate(sorted(summary, key=lambda s: -abs(s["swing"])),
headers="keys", tablefmt="github", floatfmt="+.2f"))
logger.info("\nSHOULD: word_vector WORKS (verified elsewhere); >=1 persona method WORKS and "
"beats random; random(null) is INERT/DEGENERATE. If a persona method's swing "
"~= random's, that method isn't steering the axis -- it's a candidate to cut.")