mirror of
https://github.com/wassname/moral-maps.git
synced 2026-08-20 12:30:40 +08:00
Import name tinymfv -> moralmaps, pip name tiny-mfv -> moral-maps, GitHub URLs wassname/tinymfv -> wassname/moral-maps. HuggingFace dataset id wassname/tiny-mfv left as-is (separate namespace, published data artifact). Historical docs/spec/* and RESEARCH_JOURNAL keep their dated paths. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
131 lines
5.9 KiB
Python
131 lines
5.9 KiB
Python
"""Run forced-choice 7-way primary-foundation probe over a vignette set.
|
|
|
|
Wraps `moralmaps.evaluate()`. Reports the AI-vs-label distribution match:
|
|
top1_acc argmax model == argmax label
|
|
mean_nll soft cross-entropy vs human distribution, nats
|
|
mean_nll_T same metric after one fitted temperature
|
|
pearson[f] cross-vignette Pearson(model_p[f], label_p[f]) on
|
|
labeled rows (other_violate condition).
|
|
|
|
Labels:
|
|
classic: human_* (Clifford 2015 % distributions)
|
|
(paraphrased sets carry the same `human_*` as their classic parent;
|
|
`ai_*` columns are available for cross-source diagnostics)
|
|
|
|
Usage:
|
|
python scripts/09_forced_choice.py --model Qwen/Qwen3-0.6B
|
|
python scripts/09_forced_choice.py --model Qwen/Qwen3-4B --name ai-actor
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import torch
|
|
from loguru import logger
|
|
from tabulate import tabulate
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
|
|
from moralmaps import evaluate, load_vignettes
|
|
from moralmaps.guided import _DEFAULT_FORCED_FOUNDATIONS
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--model", default="Qwen/Qwen3-0.6B")
|
|
ap.add_argument("--name", default="classic", help="dataset config (classic/scifi/ai-actor)")
|
|
ap.add_argument("--limit", type=int, default=0)
|
|
ap.add_argument("--batch-size", type=int, default=16)
|
|
ap.add_argument("--max-think-tokens", type=int, default=128)
|
|
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
|
ap.add_argument("--dtype", default="bfloat16", choices=["float32", "float16", "bfloat16"])
|
|
ap.add_argument("--out", default=None)
|
|
args = ap.parse_args()
|
|
|
|
vig = load_vignettes(args.name)
|
|
if args.limit:
|
|
vig = vig[: args.limit]
|
|
logger.info(f"loaded {len(vig)} {args.name} vignettes")
|
|
|
|
dtype = getattr(torch, args.dtype)
|
|
logger.info(f"loading {args.model} on {args.device} dtype={args.dtype}")
|
|
tok = AutoTokenizer.from_pretrained(args.model)
|
|
model = AutoModelForCausalLM.from_pretrained(args.model, dtype=dtype).to(args.device).eval()
|
|
|
|
# Diagnostic: show first-token resolution.
|
|
print("\n=== first-token resolution ===")
|
|
for f in _DEFAULT_FORCED_FOUNDATIONS:
|
|
ids = tok.encode(f, add_special_tokens=False)
|
|
print(f" {f!r:>14} -> {ids[0]:>6} {tok.decode([ids[0]])!r} (full: {ids})")
|
|
|
|
out = evaluate(
|
|
model, tok, args.name, vignettes=vig,
|
|
batch_size=args.batch_size,
|
|
max_think_tokens=args.max_think_tokens,
|
|
return_per_row=True,
|
|
)
|
|
|
|
# Persist per-row predictions for downstream analysis.
|
|
out_path = Path(args.out) if args.out else (
|
|
ROOT / "data" / "results" / f"forced_choice_{args.name}.jsonl")
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with out_path.open("w") as f:
|
|
for r in out["per_row"]:
|
|
rec = {
|
|
"id": r["id"],
|
|
"condition": r["condition"],
|
|
"foundation_coarse": r["foundation_coarse"],
|
|
"p": {f: float(r["p"][i]) for i, f in enumerate(_DEFAULT_FORCED_FOUNDATIONS)},
|
|
"score": {f: float(r["score"][i]) for i, f in enumerate(_DEFAULT_FORCED_FOUNDATIONS)},
|
|
"label": (None if r["label"] is None
|
|
else {f: float(r["label"][i]) for i, f in enumerate(_DEFAULT_FORCED_FOUNDATIONS)}),
|
|
"top1": r["top1"],
|
|
"margin": float(r["margin"]),
|
|
"pmass_allowed": float(r["pmass_allowed"]),
|
|
"nll_prefill": float(r["nll_prefill"]),
|
|
}
|
|
f.write(json.dumps(rec) + "\n")
|
|
logger.info(f"wrote {len(out['per_row'])} rows to {out_path}")
|
|
|
|
# === Per-foundation table ===
|
|
print(f"\n=== per-foundation aggregates on {args.name} ===")
|
|
print("SHOULD: pearson_label > 0.5 on most foundations for a well-calibrated model")
|
|
print(tabulate(out["table"], headers="keys", tablefmt="pipe", floatfmt=".3f", showindex=False))
|
|
|
|
# === Headline scalars ===
|
|
print(f"\n=== AI-vs-label headlines on {args.name} (n={len(out['per_row'])}) ===")
|
|
print("SHOULD: top1_acc >> 1/7=0.14 (uniform); informedness >> 0 (chance); mean_nll_T < mean_nll if raw probe is overconfident")
|
|
print(f" top1_acc = {out['top1_acc']}")
|
|
print(f" informedness = {out['informedness']} (macro Youden's J vs human argmax, in [-1,1]; 0=chance)")
|
|
print(f" mean_nll = {out['mean_nll']} (T=1, nats)")
|
|
print(f" mean_nll_T = {out['mean_nll_T']} (temperature-scaled, nats)")
|
|
print(f" median_nll_T = {out['median_nll_T']} (temperature-scaled, nats)")
|
|
print(f" T = {out['T']}")
|
|
print(f" mean_pmass_allowed = {out['mean_pmass_allowed']} (valid-token mass)")
|
|
print(f" mean_nll_prefill = {out['mean_nll_prefill']} (assistant prefill, nats/tok)")
|
|
|
|
if out["profile"] is not None:
|
|
print("\n=== mean profile (human vs model) ===")
|
|
print(tabulate(out["profile"], headers="keys", tablefmt="pipe", floatfmt=".3f", showindex=False))
|
|
|
|
# Confidence calibration
|
|
p_top1 = np.array([float(r["p"].max()) for r in out["per_row"]])
|
|
print(f"\n p_top1 min/median/mean/max: {p_top1.min():.3f} / "
|
|
f"{np.median(p_top1):.3f} / {p_top1.mean():.3f} / {p_top1.max():.3f}")
|
|
print(" SHOULD: median > 0.4 (clear winner per row); <0.2 -> probe broken")
|
|
|
|
# Prefill NLL degradation probe (teacher-forced on assistant prefill).
|
|
nll = np.array([float(r["nll_prefill"]) for r in out["per_row"]])
|
|
nll = nll[np.isfinite(nll)]
|
|
if len(nll):
|
|
print(f"\n nll_prefill (nats/tok) min/median/mean/max: "
|
|
f"{nll.min():.3f} / {np.median(nll):.3f} / {nll.mean():.3f} / {nll.max():.3f}")
|
|
print(" SHOULD: stable across runs at fixed model; rises under steering/ablation -> prefill degradation")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|