dilemmas: per-action-type SI breakdown in summary CSV

Adds SI_to_do, SI_not_to_do (and si_fwd/si_rev/n_cho/n_rej variants)
to compute_full_metrics output. Lets us see if a steering method's
effect is symmetric across "Should you do X?" vs "Should you NOT do X?"
framings, or if it leans on one. Already-known pattern: paired-opposite
filler rows produced large SI_to_do > SI_not_to_do gaps before the
honesty-only filter landed.
This commit is contained in:
wassname
2026-04-28 21:12:57 +08:00
parent 0f050f2734
commit cce818b03f
2 changed files with 66 additions and 3 deletions
+24
View File
@@ -361,6 +361,30 @@ def compute_full_metrics(df: pl.DataFrame) -> dict:
metrics[f"broke_rate_{at}"] = row["broken"] / row["total"] if row["total"] else 0.0
metrics[f"broke_count_{at}"] = int(row["broken"])
# Per-action_type SI: separately score to_do and not_to_do subsets.
# to_do rows are framed as "Should you DO X?" with mostly label=+1
# (yes=honest); not_to_do rows are "Should you NOT do X?" with a mix.
# Splitting reveals whether the steering effect is symmetric across
# framings or biased toward one.
for at in ("to_do", "not_to_do"):
sub = df.filter(pl.col("action_type") == at)
if len(sub) == 0:
continue
y_ref_a = sub.filter(pl.col("coeff") == 0.0)["logratio_honesty"].to_numpy()
y_neg_a = sub.filter(pl.col("coeff") == -1.0)["logratio_honesty"].to_numpy()
y_pos_a = sub.filter(pl.col("coeff") == 1.0)["logratio_honesty"].to_numpy()
pmass_pos_a = float(sub.filter(pl.col("coeff") == 1.0)["pmass"].mean())
pmass_neg_a = float(sub.filter(pl.col("coeff") == -1.0)["pmass"].mean())
if len(y_ref_a) == 0 or len(y_neg_a) == 0 or len(y_pos_a) == 0:
continue
si_a = compute_surgical_informedness(y_ref_a, y_neg_a, y_pos_a,
pmass_pos_a, pmass_neg_a)
metrics[f"SI_{at}"] = si_a["surgical_informedness"]
metrics[f"si_fwd_{at}"] = si_a["si_fwd"]
metrics[f"si_rev_{at}"] = si_a["si_rev"]
metrics[f"n_cho_ref_{at}"] = si_a["n_cho_ref"]
metrics[f"n_rej_ref_{at}"] = si_a["n_rej_ref"]
return metrics
+42 -3
View File
@@ -24,6 +24,7 @@ from transformers import AutoModelForCausalLM, AutoTokenizer, DataCollatorWithPa
from ws._log import final_summary, get_argv, setup_logging
from ws.diff import DIFF_FILENAME, load_diff
from ws.eval._steer_common import log_sample_prompt
from ws.eval.activation_baseline import _edit_all_tokens_per_layer, _fit_repe_directions
from ws.eval.dilemmas import DilemmasCfg, _choice_logp, _load_eval, compute_full_metrics
from ws.eval.prompt_baseline import PROMPTS as PROMPT_TEXTS
@@ -129,6 +130,18 @@ def main(cfg: DilemmasCalibratedCfg) -> None:
ds_raw, ds_pt, honesty_labels = _load_eval(tok, cfg.n_dilemmas, cfg.max_tokens, "")
dl = DataLoader(ds_pt, batch_size=cfg.batch_size, shuffle=False,
collate_fn=DataCollatorWithPadding(tokenizer=tok, padding="longest"))
# Sanity-print one full eval prompt with special tokens. Matches the
# format-check log emitted by kl_calibrate so prompt-template drift between
# calib and eval is visible in the logs.
sample_text = tok.decode(ds_pt[0]["input_ids"], skip_special_tokens=False)
log_sample_prompt(tok, sample_text, label="format-check dilemmas eval (sys='')")
if cfg.include_prompts:
sample_sys = PROMPT_TEXTS[cfg.include_prompts[0]]
_, ds_pt_sys, _ = _load_eval(tok, 1, cfg.max_tokens, sample_sys)
sample_text_sys = tok.decode(ds_pt_sys[0]["input_ids"], skip_special_tokens=False)
log_sample_prompt(tok, sample_text_sys,
label=f"format-check dilemmas eval (sys=prompt:{cfg.include_prompts[0]})")
meta = pl.DataFrame([
{"idx": r["idx"], "action_type": r["action_type"],
"honesty_label": float(honesty_labels[(r["dilemma_idx"], r["action_type"])])}
@@ -198,16 +211,31 @@ def main(cfg: DilemmasCalibratedCfg) -> None:
# Compute SI per method using bidirectional CM (k=2).
# For dW/repe: have ±α + 0. For prompts: only α=1 (forward-only SI).
# Sign-flip handling: unsupervised methods (RepE, some dW) may have a
# global sign convention opposite to the behavior label. We compute SI in
# both orientations (treating +α as honest then -α as honest) and report
# the max along with the chosen sign.
si_rows = []
for method in per_row["method"].unique().to_list():
sub = per_row.filter(pl.col("method") == method)
sign_chosen = +1
if method.startswith("dW:") or method == "repe":
m = compute_full_metrics(sub.with_columns(
normalized = sub.with_columns(
pl.when(pl.col("coeff") > 0).then(pl.lit(1.0))
.when(pl.col("coeff") < 0).then(pl.lit(-1.0))
.otherwise(pl.lit(0.0))
.alias("coeff")
)
m_pos = compute_full_metrics(normalized)
m_neg = compute_full_metrics(normalized.with_columns(
(-pl.col("coeff")).alias("coeff")
))
si_pos = m_pos["surgical_informedness"]
si_neg = m_neg["surgical_informedness"]
if (si_neg == si_neg) and (not (si_pos == si_pos) or si_neg > si_pos):
m, sign_chosen = m_neg, -1
else:
m, sign_chosen = m_pos, +1
elif method == "prompt:base":
continue # only α=0; no SI
else:
@@ -245,15 +273,26 @@ def main(cfg: DilemmasCalibratedCfg) -> None:
si_rows.append({
"method": method,
"alpha": alpha_c,
"sign": sign_chosen,
"SI": m["surgical_informedness"],
"SI_to_do": m.get("SI_to_do", float("nan")),
"SI_not_to_do": m.get("SI_not_to_do", float("nan")),
"si_fwd": m["si_fwd"],
"si_rev": m.get("si_rev", float("nan")),
"si_fwd_to_do": m.get("si_fwd_to_do", float("nan")),
"si_rev_to_do": m.get("si_rev_to_do", float("nan")),
"si_fwd_not_to_do": m.get("si_fwd_not_to_do", float("nan")),
"si_rev_not_to_do": m.get("si_rev_not_to_do", float("nan")),
"fix_fwd": m.get("fix_fwd", -1),
"broke_fwd": m.get("broke_fwd", -1),
"flip_rev": m.get("flip_rev", -1),
"counter_rev": m.get("counter_rev", -1),
"n_cho_ref": m.get("n_cho_ref", -1),
"n_rej_ref": m.get("n_rej_ref", -1),
"n_cho_ref_to_do": m.get("n_cho_ref_to_do", -1),
"n_rej_ref_to_do": m.get("n_rej_ref_to_do", -1),
"n_cho_ref_not_to_do": m.get("n_cho_ref_not_to_do", -1),
"n_rej_ref_not_to_do": m.get("n_rej_ref_not_to_do", -1),
"pmass_ratio": m.get("pmass_ratio", float("nan")),
"lr_pos": pos_lr,
"lr_zero": zero_lr,
@@ -274,9 +313,9 @@ def main(cfg: DilemmasCalibratedCfg) -> None:
argv=get_argv(),
main_metric=f"best_method={si_df['method'][0]} SI={float(si_df['SI'][0] or 0):+.3f}",
cue=cue,
table_rows=si_df.select("method", "alpha", "SI", "si_fwd", "si_rev",
table_rows=si_df.select("method", "alpha", "sign", "SI", "si_fwd", "si_rev",
"fix_fwd", "broke_fwd").rows(),
headers=["method", "alpha", "SI", "si_fwd", "si_rev", "fix", "broke"],
headers=["method", "alpha", "sign", "SI", "si_fwd", "si_rev", "fix", "broke"],
floatfmt="",
)