SPB Git

spb/wpai0001_uqo

Public
23.2 KB · 357 lines python
1#!/usr/bin/env python32"""Sprint 1-2 pilot (Tier 0, micro-world A / OPSEQ).34What this script establishes, with artifacts:5  1. Donor/recipient capability gap on the target operator (Gate B), repeated over seeds.6  2. Hook no-op guarantee on trained models (Gate C): instrumented == uninstrumented, bitwise.7  3. Within-model causal localization of the target operator in the donor (activation patching,8     zero-ablation, head patching), with a sham baseline and reproducibility checks (RQ1).910No transplant is performed here. Nothing in this script claims transfer.1112Usage: python scripts/run_pilot.py [--seeds 0 1 2] [--steps 3000] [--device auto] [--quick] [--dry-run]13"""14from __future__ import annotations1516import argparse17import gzip18import json19import sys20import time21from pathlib import Path2223import numpy as np24import torch2526ROOT = Path(__file__).resolve().parents[1]27sys.path.insert(0, str(ROOT / "src"))2829from wp_ai_0001.config import ModelRoleConfig, OpSeqTaskConfig, PilotConfig, ToyModelConfig, TrainConfig, config_hash  # noqa: E40230from wp_ai_0001.datasets import opseq  # noqa: E40231from wp_ai_0001.datasets.common import Split, assert_disjoint, encode_split  # noqa: E40232from wp_ai_0001.evaluation.metrics import chance_level, evaluate  # noqa: E40233from wp_ai_0001.instrumentation.provenance import Manifest, make_run_id, utc_now  # noqa: E40234from wp_ai_0001.localization.patching import ablation_sweep, head_patch_sweep, patch_sweep  # noqa: E40235from wp_ai_0001.models.hooks import Capture, Zero  # noqa: E40236from wp_ai_0001.models.toy_transformer import resolve_device  # noqa: E40237from wp_ai_0001.orchestration import registry  # noqa: E40238from wp_ai_0001.statistics.bootstrap import cluster_bootstrap_mean, cluster_bootstrap_paired_diff, seeds_summary  # noqa: E40239from wp_ai_0001.training.train_toy import load as load_model  # noqa: E40240from wp_ai_0001.training.train_toy import train  # noqa: E4024142OMEGA = "qua"43MAX_SEQ = 24444546def build_splits(cfg_donor: OpSeqTaskConfig, cfg_recip: OpSeqTaskConfig, n_train: int, n_dev: int, n_eval: int, base_seed: int) -> dict[str, Split]:47    """All splits from distinct seeds; evaluation splits are disjoint from every training split."""48    s = base_seed49    sp: dict[str, Split] = {}50    sp["donor_train"] = opseq.generate(cfg_donor, n_train, s + 1, "donor_train", mode="train")51    sp["recipient_train"] = opseq.generate(cfg_recip, n_train, s + 2, "recipient_train", mode="train")52    used = sp["donor_train"].item_hashes() | sp["recipient_train"].item_hashes()53    sp["donor_dev"] = opseq.generate(cfg_donor, n_dev, s + 3, "donor_dev", mode="train", exclude_hashes=used)54    used |= sp["donor_dev"].item_hashes()55    sp["recipient_dev"] = opseq.generate(cfg_recip, n_dev, s + 4, "recipient_dev", mode="train", exclude_hashes=used)56    used |= sp["recipient_dev"].item_hashes()57    # evaluation strata (shared by all models)58    sp["target_iid"] = opseq.generate(cfg_donor, n_eval, s + 10, "target_iid", mode="train", require_operator=OMEGA, exclude_hashes=used)59    used |= sp["target_iid"].item_hashes()60    sp["control_iid"] = opseq.generate(cfg_donor, n_eval, s + 11, "control_iid", mode="train", forbid_operator=OMEGA, exclude_hashes=used)61    used |= sp["control_iid"].item_hashes()62    sp["comp_heldout"] = opseq.generate(cfg_donor, n_eval, s + 12, "comp_heldout", mode="comp", min_len=2, exclude_hashes=used)63    used |= sp["comp_heldout"].item_hashes()64    sp["len_ood"] = opseq.generate(cfg_donor, max(200, n_eval // 2), s + 13, "len_ood", mode="any", min_len=6, max_len=7, exclude_hashes=used)65    for k in ("donor_train", "donor_dev", "target_iid", "control_iid", "comp_heldout", "len_ood"):66        opseq.validate(sp[k], cfg_donor)67    opseq.validate(sp["recipient_train"], cfg_recip)68    opseq.validate(sp["recipient_dev"], cfg_recip)69    assert_disjoint(sp["donor_train"], sp["donor_dev"], sp["target_iid"], sp["control_iid"], sp["comp_heldout"], sp["len_ood"])70    assert_disjoint(sp["recipient_train"], sp["recipient_dev"], sp["target_iid"], sp["control_iid"], sp["comp_heldout"], sp["len_ood"])71    return sp727374def omega_step_accuracy(split: Split, predictions: list[list[str]]) -> np.ndarray:75    """Per item: 1 if the scratchpad token at the first Ω step is correct (items without Ω: NaN)."""76    out = np.full(len(split.examples), np.nan)77    for i, (e, pred) in enumerate(zip(split.examples, predictions)):78        ops = list(e.prompt[2:-1])79        if OMEGA in ops:80            j = ops.index(OMEGA)81            out[i] = float(j < len(pred) and pred[j] == e.answers[j])82    return out838485def localization_set(cfg: OpSeqTaskConfig, corrupt_op: str = "zib") -> tuple[Split, Split]:86    """Length-3 programs with Ω at index 1; corrupted twin replaces Ω by ``corrupt_op`` so that r_2 differs."""87    others = [o for o in cfg.operators if o != OMEGA]88    clean, corrupt = [], []89    for a in others:90        for b in others:91            for v0 in range(cfg.modulus):92                ec = opseq.make_example(v0, [a, OMEGA, b], cfg.modulus)93                ex = opseq.make_example(v0, [a, corrupt_op, b], cfg.modulus)94                if ec.answers[1] != ex.answers[1]:95                    clean.append(ec)96                    corrupt.append(ex)97    return (Split("loc_clean", clean, "opseq", opseq.GENERATOR_VERSION, 0, cfg.model_dump()),98            Split("loc_corrupt", corrupt, "opseq", opseq.GENERATOR_VERSION, 0, cfg.model_dump()))99100101def noop_check(model, enc, device) -> dict:102    ids = torch.from_numpy(enc["ids"][:256]).to(device)103    pos = torch.from_numpy(enc["prompt_len"][:256] - 1).to(device)104    model.hooks.clear()105    with torch.no_grad():106        ref = model(ids, pos)107        again = model(ids, pos)108        cap = Capture()109        for n in model.hook_names():110            model.hooks.add(n, cap)111        with_capture = model(ids, pos)112        model.hooks.add("resid_post.0", Zero())113        with model.hooks.disabled():114            disabled = model(ids, pos)115        enabled = model(ids, pos)116    model.hooks.clear()117    return {"deterministic_repeat": bool(torch.equal(ref, again)), "capture_all_bitwise_identity": bool(torch.equal(ref, with_capture)),118            "disabled_modifying_hook_bitwise_identity": bool(torch.equal(ref, disabled)),119            "enabled_zero_hook_changes_output": bool(not torch.equal(ref, enabled)), "n_hook_points": len(model.hook_names()), "n_items": int(ids.shape[0])}120121122def main() -> None:123    ap = argparse.ArgumentParser()124    ap.add_argument("--seeds", type=int, nargs="+", default=[0, 1, 2])125    ap.add_argument("--steps", type=int, default=3000)126    ap.add_argument("--device", default="auto")127    ap.add_argument("--quick", action="store_true", help="tiny settings for a CPU smoke test")128    ap.add_argument("--dry-run", action="store_true", help="build splits and manifest only")129    ap.add_argument("--n-boot", type=int, default=2000)130    args = ap.parse_args()131132    n_train, n_dev, n_eval, steps = (3000, 300, 300, 200) if args.quick else (40_000, 1_000, 1_000, args.steps)133    task_d = OpSeqTaskConfig()134    task_r = OpSeqTaskConfig(excluded_operators=[OMEGA])135    model_cfg = ToyModelConfig(max_seq_len=MAX_SEQ) if not args.quick else ToyModelConfig(d_model=64, n_layers=3, n_heads=2, d_ff=128, max_seq_len=MAX_SEQ)136    cfg = PilotConfig(137        experiment_id="pilot_tier0_opseq" + ("_quick" if args.quick else ""), seeds=args.seeds, target_operator=OMEGA, n_eval=n_eval, n_bootstrap=args.n_boot,138        donor=ModelRoleConfig(role="donor", name="donor", model=model_cfg, task=task_d, train=TrainConfig(steps=steps, device=args.device, n_train=n_train, n_dev=n_dev)),139        recipient=ModelRoleConfig(role="recipient", name="recipient", model=model_cfg, task=task_r, train=TrainConfig(steps=steps, device=args.device, n_train=n_train, n_dev=n_dev)),140    )141    chash = config_hash(cfg)142    run_id = make_run_id(cfg.experiment_id, chash)143    out_dir = ROOT / "experiments" / "summaries" / run_id144    art_dir = ROOT / "artifacts" / "runs" / run_id145    models_dir = ROOT / "artifacts" / "models" / run_id146    fig_dir = ROOT / "artifacts" / "figures" / run_id147    for d in (out_dir, art_dir, fig_dir):148        d.mkdir(parents=True, exist_ok=True)149    man = Manifest(run_id, cfg.experiment_id, cfg.model_dump(), ROOT / "experiments" / "manifests")150    registry.record(run_id, cfg.experiment_id, "planned", config_hash=chash)151    device = resolve_device(args.device)152    t_start = time.time()153    print(f"[{utc_now()}] run_id={run_id} device={device}")154155    # ---------------- data ----------------156    splits = build_splits(task_d, task_r, n_train, n_dev, n_eval, base_seed=20261109)157    data_dir = art_dir / "data"158    for name, sp in splits.items():159        sp.save(data_dir / f"{name}.json")160        man.data["data_hashes"][name] = {"hash": sp.hash, "n": len(sp), "seed": sp.seed, "generator": f"{sp.generator}@{sp.generator_version}"}161    man.save()162    vocab = opseq.vocab_for(task_d)163    chance = {k: chance_level(splits[k]) for k in ("target_iid", "control_iid", "comp_heldout", "len_ood")}164    print("splits:", {k: (len(v), v.hash) for k, v in splits.items()})165    if args.dry_run:166        man.set_status("planned", "dry run: splits and manifest only")167        print("dry run complete")168        return169170    man.set_status("running")171    registry.record(run_id, cfg.experiment_id, "running")172    results: dict = {"run_id": run_id, "config_hash": chash, "device": str(device), "chance_level": chance, "seeds": {}, "noop": {}, "localization": {}}173    try:174        # ---------------- training + evaluation ----------------175        eval_names = ["target_iid", "control_iid", "comp_heldout", "len_ood"]176        per_item: dict[str, dict[str, np.ndarray]] = {}177        for seed in cfg.seeds:178            results["seeds"][seed] = {}179            for role in (cfg.donor, cfg.recipient):180                tcfg = role.train.model_copy(update={"seed": seed})181                mdir = models_dir / f"{role.name}_s{seed}"182                t0 = time.time()183                if (mdir / "model.pt").exists():184                    model, _ = load_model(mdir, device)185                    hist = json.loads((mdir / "history.json").read_text())186                    print(f"  loaded {role.name} s{seed}")187                else:188                    hist = []189                    model = train(role.model, tcfg, splits[f"{role.name}_train"], splits[f"{role.name}_dev"], vocab, mdir, hist)190                    print(f"  trained {role.name} s{seed} in {time.time()-t0:.0f}s dev_acc={hist[-1]['dev_acc']:.3f}")191                model.eval()192                rec = {"n_params": model.n_params, "train_seconds": time.time() - t0, "final_dev": hist[-1], "eval": {}}193                for en in eval_names:194                    r = evaluate(model, splits[en], vocab, device)195                    ci = cluster_bootstrap_mean(r.correct.astype(float), r.clusters, cfg.n_bootstrap, seed=seed)196                    om = omega_step_accuracy(splits[en], r.predictions)197                    om_ci = cluster_bootstrap_mean(om[~np.isnan(om)], [c for c, o in zip(r.clusters, om) if not np.isnan(o)], cfg.n_bootstrap, seed=seed) if (~np.isnan(om)).any() else None198                    rec["eval"][en] = {**r.to_summary(), "ci95": ci.to_dict(), "omega_step_acc": om_ci.to_dict() if om_ci else None, "chance": chance[en]}199                    per_item[f"{role.name}_s{seed}_{en}"] = {"correct": r.correct, "trace_correct": r.trace_correct, "omega": om}200                    with gzip.open(art_dir / f"items_{role.name}_s{seed}_{en}.json.gz", "wt") as f:201                        json.dump({"split_hash": r.split_hash, "predictions": r.predictions, "answers": r.answers, "clusters": r.clusters}, f)202                enc_dev = encode_split(splits["donor_dev"], vocab, MAX_SEQ)203                results["noop"][f"{role.name}_s{seed}"] = noop_check(model, enc_dev, device)204                results["seeds"][seed][role.name] = rec205            # paired donor-recipient difference on identical items206            results["seeds"][seed]["gap"] = {}207            for en in eval_names:208                d, r_ = per_item[f"donor_s{seed}_{en}"], per_item[f"recipient_s{seed}_{en}"]209                ci = cluster_bootstrap_paired_diff(d["correct"].astype(float), r_["correct"].astype(float), splits[en].examples and [e.cluster for e in splits[en].examples], cfg.n_bootstrap, seed=seed)210                results["seeds"][seed]["gap"][en] = ci.to_dict()211            man.save()212213        results["across_seeds"] = {en: {role: seeds_summary([results["seeds"][s][role]["eval"][en]["accuracy"] for s in cfg.seeds]) for role in ("donor", "recipient")} for en in eval_names}214        results["across_seeds_omega_step"] = {role: seeds_summary([results["seeds"][s][role]["eval"]["target_iid"]["omega_step_acc"]["estimate"] for s in cfg.seeds]) for role in ("donor", "recipient")}215216        # ---------------- localization (donor models) ----------------217        clean, corrupt = localization_set(task_d)218        enc_c, enc_x = encode_split(clean, vocab, MAX_SEQ), encode_split(corrupt, vocab, MAX_SEQ)219        read_pos = int(enc_c["prompt_len"][0])  # index of r_1; predicts r_2 (the Ω step)220        assert (enc_c["prompt_len"] == read_pos).all() and (enc_x["prompt_len"] == read_pos).all()221        ids_c = torch.from_numpy(enc_c["ids"]).to(device)222        ids_x = torch.from_numpy(enc_x["ids"]).to(device)223        pos = torch.full((len(clean),), read_pos, dtype=torch.long, device=device)224        ans_c = torch.from_numpy(enc_c["answers"][:, 1]).to(device)225        ans_x = torch.from_numpy(enc_x["answers"][:, 1]).to(device)226        positions = list(range(read_pos + 1))227        pos_labels = ["<bos>", "v0", "op1", "Ω", "op3", "=", "r1"]228        loc: dict = {"n_items": len(clean), "read_position": read_pos, "positions": pos_labels, "families": {}, "ablation": {}, "heads": {}, "reproducibility": {}}229        heat_by_seed: dict[str, list[np.ndarray]] = {}230        for seed in cfg.seeds:231            model, _ = load_model(models_dir / f"donor_s{seed}", device)232            with torch.no_grad():233                base_c = (model.answer_logits(ids_c, pos).argmax(-1) == ans_c).float().mean().item()234                base_x = (model.answer_logits(ids_x, pos).argmax(-1) == ans_c).float().mean().item()235            loc.setdefault("baselines", {})[f"s{seed}"] = {"clean_acc_on_clean_answer": base_c, "corrupt_run_acc_on_clean_answer(sham)": base_x}236            for fam in ("resid_post", "attn_out", "mlp_out"):237                pr = patch_sweep(model, ids_c, ids_x, pos, ans_c, ans_x, family=fam, positions=positions)238                pr2 = patch_sweep(model, ids_c, ids_x, pos, ans_c, ans_x, family=fam, positions=positions)239                loc["families"].setdefault(fam, {})[f"s{seed}"] = {"flip_rate": pr.flip_rate.tolist(), "logit_recovery": pr.logit_recovery.tolist()}240                loc["reproducibility"][f"{fam}_s{seed}_repeat_max_abs_diff"] = float(np.abs(pr.flip_rate - pr2.flip_rate).max())241                heat_by_seed.setdefault(fam, []).append(pr.flip_rate)242                abl = ablation_sweep(model, ids_c, pos, ans_c, family=fam, positions=positions, mode="zero")243                loc["ablation"].setdefault(fam, {})[f"s{seed}"] = abl.tolist()244            for p_name, p_idx in (("omega_token", 3), ("read_position", read_pos)):245                loc["heads"].setdefault(p_name, {})[f"s{seed}"] = head_patch_sweep(model, ids_c, ids_x, pos, ans_c, ans_x, p_idx).tolist()246            print(f"  localization donor s{seed} done")247        for fam, mats in heat_by_seed.items():248            if len(mats) > 1:249                flat = np.stack([m.ravel() for m in mats])250                cc = np.corrcoef(flat)251                loc["reproducibility"][f"{fam}_across_seed_pearson_min"] = float(cc[np.triu_indices(len(mats), 1)].min())252            loc["families"][fam]["mean_flip_rate"] = np.mean(mats, axis=0).tolist()253        results["localization"] = loc254255        # ---------------- figures ----------------256        make_figures(results, fig_dir, pos_labels, cfg)257        results["elapsed_seconds"] = time.time() - t_start258        (out_dir / "results.json").write_text(json.dumps(results, indent=2, default=float))259        write_summary(results, out_dir, cfg, fig_dir)260        man.data["outputs"] = {"results": str(out_dir.relative_to(ROOT) / "results.json"), "figures": str(fig_dir.relative_to(ROOT)), "models": str(models_dir.relative_to(ROOT)), "items": str(art_dir.relative_to(ROOT))}261        man.set_status("completed")262        registry.record(run_id, cfg.experiment_id, "completed", results=str(out_dir.relative_to(ROOT) / "results.json"))263        print(f"[{utc_now()}] completed in {results['elapsed_seconds']:.0f}s -> {out_dir}")264    except Exception as exc:  # noqa: BLE001265        man.set_status("failed", repr(exc))266        registry.record(run_id, cfg.experiment_id, "failed", error=repr(exc))267        raise268269270def make_figures(results: dict, fig_dir: Path, pos_labels: list[str], cfg: PilotConfig) -> None:271    import matplotlib.pyplot as plt272273    from wp_ai_0001.visualization import DIVERGING_CMAP, ENTITY_COLORS, SEQUENTIAL_CMAP, apply_style, heatmap274    from wp_ai_0001.visualization.style import DOUBLE_WIDTH_IN, TEXT_SECONDARY275276    apply_style()277    seeds = {str(k): v for k, v in results["seeds"].items()}  # JSON round-trips keys as strings278    eval_names = ["target_iid", "control_iid", "comp_heldout", "len_ood"]279    # Figure 1: capability gap, per split, donor vs recipient, mean over seeds with per-seed points and CI of seed 0280    fig, ax = plt.subplots(figsize=(DOUBLE_WIDTH_IN * 0.6, 2.2))281    x = np.arange(len(eval_names))282    w = 0.36283    for k, role in enumerate(("donor", "recipient")):284        means = [results["across_seeds"][en][role]["mean"] for en in eval_names]285        ax.bar(x + (k - 0.5) * w, means, width=w - 0.04, color=ENTITY_COLORS[role], label=role, linewidth=0)286        for s in cfg.seeds:287            ax.scatter(x + (k - 0.5) * w, [seeds[str(s)][role]["eval"][en]["accuracy"] for en in eval_names], s=8, color="#ffffff", edgecolor=ENTITY_COLORS[role], linewidth=0.6, zorder=3)288    ax.scatter(x, [results["chance_level"][en] for en in eval_names], marker="_", s=120, color=ENTITY_COLORS["chance"], label="majority-class chance", zorder=4)289    ax.set_xticks(x)290    ax.set_xticklabels(["target (Ω)", "control (no Ω)", "held-out comp.", "length OOD"])291    ax.set_ylabel("final-answer exact match")292    ax.set_ylim(0, 1.02)293    ax.legend(loc="upper right", ncol=1)294    fig.savefig(fig_dir / "fig_gap_tier0.pdf")295    plt.close(fig)296    # Figure 2: localization heatmaps (mean flip rate over seeds) for three families297    fams = ["resid_post", "attn_out", "mlp_out"]298    fig, axes = plt.subplots(1, 3, figsize=(DOUBLE_WIDTH_IN, 2.1), sharey=True)299    for ax, fam in zip(axes, fams):300        m = np.array(results["localization"]["families"][fam]["mean_flip_rate"])301        heatmap(ax, m, [f"L{l}" for l in range(m.shape[0])], pos_labels, cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="restored-answer rate", title=fam, annotate=True)302        ax.set_xlabel("patched token position")303    axes[0].set_ylabel("layer")304    fig.savefig(fig_dir / "fig_localization_patching_tier0.pdf")305    plt.close(fig)306    # Figure 3: zero-ablation (mean over seeds) accuracy307    fig, axes = plt.subplots(1, 3, figsize=(DOUBLE_WIDTH_IN, 2.1), sharey=True)308    for ax, fam in zip(axes, fams):309        m = np.mean([np.array(v) for v in results["localization"]["ablation"][fam].values()], axis=0)310        heatmap(ax, m, [f"L{l}" for l in range(m.shape[0])], pos_labels, cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="accuracy after zero-ablation", title=fam, annotate=True)311        ax.set_xlabel("ablated token position")312    axes[0].set_ylabel("layer")313    fig.savefig(fig_dir / "fig_localization_ablation_tier0.pdf")314    plt.close(fig)315    # Figure 4: head-level patching at the Ω token and at the read position316    fig, axes = plt.subplots(1, 2, figsize=(DOUBLE_WIDTH_IN * 0.75, 2.1), sharey=True)317    for ax, key in zip(axes, ("omega_token", "read_position")):318        m = np.mean([np.array(v) for v in results["localization"]["heads"][key].values()], axis=0)319        heatmap(ax, m, [f"L{l}" for l in range(m.shape[0])], [f"h{h}" for h in range(m.shape[1])], cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="restored-answer rate", title=f"head patch @ {key.replace('_', ' ')}", annotate=True)320    axes[0].set_ylabel("layer")321    fig.savefig(fig_dir / "fig_localization_heads_tier0.pdf")322    plt.close(fig)323    fig.text(0, 0, "", color=TEXT_SECONDARY)324325326def write_summary(results: dict, out_dir: Path, cfg: PilotConfig, fig_dir: Path) -> None:327    seeds = {str(k): v for k, v in results["seeds"].items()}328    L = ["# Pilot summary — " + results["run_id"], "", f"Generated {utc_now()} on device `{results['device']}`; config hash `{results['config_hash']}`; seeds {cfg.seeds}.", "",329         "## Gate B — donor/recipient gap (final-answer exact match, mean over seeds; CI = cluster bootstrap on programs)", "",330         "| split | chance | donor mean (sd) | recipient mean (sd) | paired gap seed 0 [95% CI] |", "|---|---|---|---|---|"]331    for en in ("target_iid", "control_iid", "comp_heldout", "len_ood"):332        d, r = results["across_seeds"][en]["donor"], results["across_seeds"][en]["recipient"]333        g = seeds[str(cfg.seeds[0])]["gap"][en]334        L.append(f"| {en} | {results['chance_level'][en]:.3f} | {d['mean']:.3f} ({(d['sd'] or 0):.3f}) | {r['mean']:.3f} ({(r['sd'] or 0):.3f}) | {g['estimate']:+.3f} [{g['low']:+.3f}, {g['high']:+.3f}] |")335    om = results["across_seeds_omega_step"]336    L += ["", f"Ω-step accuracy on target_iid (token emitted at the first Ω step): donor {om['donor']['mean']:.3f}, recipient {om['recipient']['mean']:.3f}.", "",337          "## Gate C — hook no-op checks (bitwise equality of logits on 256 dev items)", "", "| model | repeat | capture-all | disabled hook | enabled zero hook changes output |", "|---|---|---|---|---|"]338    for k, v in results["noop"].items():339        L.append(f"| {k} | {v['deterministic_repeat']} | {v['capture_all_bitwise_identity']} | {v['disabled_modifying_hook_bitwise_identity']} | {v['enabled_zero_hook_changes_output']} |")340    loc = results["localization"]341    L += ["", f"## RQ1 — within-donor localization of Ω (n = {loc['n_items']} clean/corrupt pairs, read position = r1, predicting r2)", "",342          "Baselines: " + "; ".join(f"s{k[1:]}: clean {v['clean_acc_on_clean_answer']:.3f}, sham (corrupt run scored on clean answer) {v['corrupt_run_acc_on_clean_answer(sham)']:.3f}" for k, v in loc["baselines"].items()), "",343          "Mean restored-answer rate after patching the clean activation into the corrupted run (rows = layers, columns = positions " + ", ".join(loc["positions"]) + "):", ""]344    for fam in ("resid_post", "attn_out", "mlp_out"):345        m = np.array(loc["families"][fam]["mean_flip_rate"])346        L.append(f"- `{fam}`: " + " | ".join(f"L{i}: " + " ".join(f"{v:.2f}" for v in row) for i, row in enumerate(m)))347    L += ["", "Reproducibility: " + "; ".join(f"{k} = {v:.4f}" for k, v in loc["reproducibility"].items()), "",348          "Figures: " + ", ".join(f"`{p.name}`" for p in sorted(fig_dir.glob("*.pdf"))), "",349          "## Interpretation (observed vs. speculative)", "",350          "Observed: the numbers above are produced by the artifacts listed in the manifest. Any statement about *transfer* is out of scope for this run: no transplant was performed.",351          "Speculative: whether the localized positions/layers constitute a transferable 'organ' is the object of the next experiment (activation graft with sham/random controls)."]352    (out_dir / "SUMMARY.md").write_text("\n".join(L) + "\n")353354355if __name__ == "__main__":356    main()357
Files · 7697ca59b784d7876f137086bc6c7bcdc2ef4600
Menu

Theme