mirror of
https://github.com/wassname/weight-steering.git
synced 2026-08-15 13:05:01 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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="",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user