SPB Git

Repository Public Research preview

figures_graft.py 13.1 KB 199 lines python

1"""Figure recipes for the Tier-0 transplant run (graft_tier0) and the Tier-1 task-vector run.2Every plotted number comes from results.json of a completed run; no computation happens here."""3from __future__ import annotations45from pathlib import Path67import matplotlib.pyplot as plt8import numpy as np910from wp_ai_0001.visualization.style import CATEGORICAL, DOUBLE_WIDTH_IN, ENTITY_COLORS, SEQUENTIAL_CMAP, TEXT_SECONDARY, apply_style, heatmap1112MAIN_CONDS = [("subspace_k8", "subspace graft k=8"), ("subspace_random_k8", "random subspace k=8"), ("act_identity_all", "identity (same width)"), ("act_procrustes_all", "Procrustes"), ("act_unrelated_procrustes_all", "Procrustes, task-free fit"),13              ("act_ridge_all", "ridge"), ("act_cca_all", "CCA"), ("ctl_unrelated_data_map", "ridge, task-free fit"), ("ctl_sham_map", "sham map (random orth.)"),14              ("ctl_norm_matched_random", "norm-matched random"), ("ctl_shuffled_token", "shuffled-token donor"), ("ctl_shuffled_layer", "shuffled-layer donor"),15              ("ctl_unrelated_donor", "unrelated-skill donor"), ("steer_a1.0", "steering vector α=1"), ("adapter_graft", "adapter graft (donor-free)"),16              ("weight_L0_mlp", "weight graft L0 MLP"), ("weight_L0_block_ln", "weight graft L0 block"), ("baseline_supervised_adapter", "supervised adapter (labels)"),17              ("baseline_distillation", "distillation (labels-free, donor outputs)")]181920LABELLED = {"act_identity_all", "act_procrustes_all", "act_unrelated_procrustes_all", "ctl_unrelated_data_map", "ctl_sham_map", "weight_L0_block_ln", "baseline_supervised_adapter", "act_ridge_all", "subspace_k8", "adapter_graft"}21LABEL_OFFSETS = {"act_unrelated_procrustes_all": (-4, -2), "baseline_supervised_adapter": (-4, 6), "act_identity_all": (-4, 2), "act_procrustes_all": (-4, -6), "ctl_unrelated_data_map": (-4, 4), "ctl_sham_map": (4, 4), "weight_L0_block_ln": (4, 2), "act_ridge_all": (-4, 2), "subspace_k8": (-4, -8), "adapter_graft": (-4, 2)}222324def _pairs(results, same_init: bool | None = None):25    pm = results.get("pair_meta")26    if not pm:  # older runs: infer from condition keys (same init seed <=> same seed suffix)27        pm = {p: {"same_init_seed": p.split("->")[0].rsplit("_s", 1)[1] == p.split("->")[1].rsplit("_s", 1)[1]} for p in results["conditions"].get("act_identity_all", {})}28    return [p for p, m in pm.items() if same_init is None or m["same_init_seed"] == same_init]293031def fig_pareto(results: dict, out: Path) -> None:32    """Target gain vs control retention for every condition (mean over pairs; same-init and different-init separately)."""33    apply_style()34    fig, axes = plt.subplots(1, 2, figsize=(DOUBLE_WIDTH_IN, 2.8), sharey=True)35    for ax, same in zip(axes, (True, False)):36        pairs = _pairs(results, same)37        if not pairs:38            ax.set_title("no pairs of this kind in this run", loc="left")39            continue40        base = np.mean([results["conditions"]["base"][p.split("->")[1]]["target_iid"]["accuracy"] for p in pairs])41        for i, (cond, label) in enumerate(MAIN_CONDS):42            d = results["conditions"].get(cond, {})43            vals = [d[p] for p in pairs if p in d]44            if not vals:45                continue46            t = np.mean([v["target_iid"]["accuracy"] for v in vals])47            c = np.mean([v["control_iid"]["accuracy"] for v in vals])48            col = ENTITY_COLORS["graft"] if cond.startswith(("act_", "subspace", "adapter", "weight", "steer")) else (ENTITY_COLORS["sham"] if cond.startswith("ctl_") else ENTITY_COLORS["recipient"])49            ax.scatter(c, t, s=18, color=col, edgecolor="#ffffff", linewidth=0.5, zorder=3)50            if cond in LABELLED:51                dx, dy = LABEL_OFFSETS.get(cond, (-3, 3))52                ax.annotate(label, (c, t), fontsize=5.5, xytext=(dx, dy), textcoords="offset points", color=TEXT_SECONDARY, ha="right" if dx < 0 else "left")53        ax.axhline(base, color=ENTITY_COLORS["chance"], linewidth=0.8, linestyle=":", label="recipient baseline (target)")54        ax.set_xlabel("control-stratum accuracy (retention)")55        ax.set_title("same initialization seed" if same else "different initialization seed", loc="left")56        ax.set_xlim(-0.02, 1.02)57        ax.set_ylim(-0.02, 1.02)58    axes[0].set_ylabel("target-stratum accuracy")59    from matplotlib.lines import Line2D6061    h = [Line2D([], [], marker="o", linestyle="", color=ENTITY_COLORS["graft"], label="graft family"), Line2D([], [], marker="o", linestyle="", color=ENTITY_COLORS["sham"], label="control"),62         Line2D([], [], marker="o", linestyle="", color=ENTITY_COLORS["recipient"], label="conventional learning baseline"), Line2D([], [], linestyle=":", color=ENTITY_COLORS["chance"], label="recipient baseline")]63    axes[1].legend(handles=h, loc="lower left", fontsize=6)64    fig.savefig(out / "fig_pareto_tier0.pdf")65    plt.close(fig)666768def fig_layer_grid_dose(results: dict, out: Path) -> None:69    apply_style()70    fig, axes = plt.subplots(1, 2, figsize=(DOUBLE_WIDTH_IN * 0.85, 2.3))71    cells = results["layer_grid"]["cells"]72    L = int(max(int(k.split("->")[0]) for k in cells)) + 173    m = np.zeros((L, L))74    for k, v in cells.items():75        a, b = map(int, k.split("->"))76        m[a, b] = v["target"]77    heatmap(axes[0], m, [f"donor L{i}" for i in range(L)], [f"L{i}" for i in range(L)], cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="target accuracy", title=f"ridge graft, {results['layer_grid']['pair']}", annotate=True)78    axes[0].set_xlabel("recipient layer")79    d = results["dose"]["cells"]80    xs = sorted(float(k) for k in d)81    axes[1].plot(xs, [d[str(x) if str(x) in d else k]["target"] for x, k in zip(xs, sorted(d, key=float))], marker="o", markersize=3, color=ENTITY_COLORS["graft"], label="target")82    axes[1].plot(xs, [d[k]["control"] for k in sorted(d, key=float)], marker="s", markersize=3, color=ENTITY_COLORS["recipient"], label="control")83    axes[1].set_xlabel("graft strength α")84    axes[1].set_ylabel("accuracy")85    axes[1].set_ylim(-0.02, 1.02)86    axes[1].legend(loc="center right")87    axes[1].set_title("dose–response (ridge, L0→L0)", loc="left")88    fig.savefig(out / "fig_layer_dose_tier0.pdf")89    plt.close(fig)909192def fig_subspace(results: dict, out: Path) -> None:93    apply_style()94    pairs = _pairs(results, True)95    ks = sorted({int(c.split("subspace_k")[1]) for c in results["conditions"] if c.startswith("subspace_k")})96    fig, ax = plt.subplots(figsize=(DOUBLE_WIDTH_IN * 0.5, 2.3))97    for prefix, label, col in (("subspace_k", "Ω-contrast PCA subspace", ENTITY_COLORS["graft"]), ("subspace_random_k", "random subspace (same k)", ENTITY_COLORS["sham"])):98        ys = [np.mean([results["conditions"][f"{prefix}{k}"][p]["target_iid"]["accuracy"] for p in pairs if p in results["conditions"][f"{prefix}{k}"]]) for k in ks]99        ax.plot(ks, ys, marker="o", markersize=3, color=col, label=label)100    full = np.mean([results["conditions"]["act_unrelated_procrustes_all"][p]["target_iid"]["accuracy"] for p in pairs])101    ax.axhline(full, color=ENTITY_COLORS["graft"], linestyle=":", linewidth=0.8, label="full-dimensional graft (same map)")102    ax.set_xscale("log", base=2)103    ax.set_xlabel("graft dimensionality k")104    ax.set_ylabel("target accuracy")105    ax.set_ylim(-0.02, 1.02)106    ax.legend(fontsize=6)107    fig.savefig(out / "fig_subspace_tier0.pdf")108    plt.close(fig)109110111def fig_portability(results: dict, out: Path) -> None:112    """Donor × recipient matrix of target accuracy for the identity and Procrustes grafts."""113    apply_style()114    pairs = _pairs(results)115    ds = sorted({p.split("->")[0] for p in pairs})116    rs = sorted({p.split("->")[1] for p in pairs})117    fig, axes = plt.subplots(1, 3, figsize=(DOUBLE_WIDTH_IN, 2.1), sharey=True)118    for ax, cond in zip(axes, ("act_identity_all", "act_procrustes_all", "act_unrelated_procrustes_all")):119        m = np.full((len(ds), len(rs)), np.nan)120        for i, d in enumerate(ds):121            for j, r in enumerate(rs):122                v = results["conditions"][cond].get(f"{d}->{r}")123                if v:124                    m[i, j] = v["target_iid"]["accuracy"]125        title = {"act_identity_all": "identity graft", "act_procrustes_all": "Procrustes (task rows)", "act_unrelated_procrustes_all": "Procrustes (task-free rows)"}[cond]126        heatmap(ax, m, [x.replace("donor_s", "D") for x in ds], [x.replace("recipient_s", "R") for x in rs], cmap=SEQUENTIAL_CMAP, vmin=0, vmax=1, cbar_label="target accuracy", title=title, annotate=True)127    fig.savefig(out / "fig_portability_tier0.pdf")128    plt.close(fig)129130131def fig_tier1(results: dict, out: Path) -> None:132    """Per task: recipient zero-shot, few-shot, own vector, best transferred vector, controls, for each pair."""133    apply_style()134    pairs = list(results["pairs"])135    tasks = list(results["tasks"])136    fig, axes = plt.subplots(1, len(pairs), figsize=(DOUBLE_WIDTH_IN, 2.4), sharey=True)137    axes = np.atleast_1d(axes)138    series = [("recipient_zero", "zero-shot", ENTITY_COLORS["recipient"]), ("recipient_few", "few-shot (prompting)", ENTITY_COLORS["chance"]), ("recipient_own_vector", "own task vector", ENTITY_COLORS["donor"]),139              ("best_transfer", "transferred donor vector", ENTITY_COLORS["graft"]), ("best_control", "best control", ENTITY_COLORS["sham"])]140    w = 0.15141    for ax, p in zip(axes, pairs):142        pr = results["pairs"][p]143        x = np.arange(len(tasks))144        for k, (key, label, col) in enumerate(series):145            ys = []146            for t in tasks:147                tr = pr["tasks"][t]148                if key == "best_transfer":149                    ys.append(max(v["accuracy"] for c, v in tr["conditions"].items() if not c.startswith("ctl") and "random_orth" not in c and "sham" not in c and "shuffled" not in c))150                elif key == "best_control":  # null controls only (amendment A1: shuffled-query state is a diagnostic)151                    ys.append(max(v["accuracy"] for c, v in tr["conditions"].items() if (c.startswith("ctl") or "random_orth" in c or "sham" in c) and "shuffled" not in c))152                else:153                    ys.append(tr[key])154            ax.bar(x + (k - 2) * w, ys, width=w - 0.02, color=col, label=label, linewidth=0)155        ax.set_xticks(x)156        ax.set_xticklabels([{"antonym": "antonym", "capital": "capital", "morph": "morph.", "addthree": "add 3"}.get(t, t) for t in tasks], rotation=30, ha="right")157        ax.set_title(p.replace("->", " → "), loc="left")158        ax.set_ylim(0, 1.02)159    axes[0].set_ylabel("exact-match accuracy")160    axes[-1].legend(fontsize=5.5, loc="upper right")161    fig.savefig(out / f"fig_tier1_transfer_{results['run_id'].split('_')[-1][:6]}.pdf")162    plt.close(fig)163164165def fig_mechanism(results: dict, out: Path) -> None:166    """Ablation → loss → rescue chain and the rule-vs-cache test."""167    apply_style()168    fig, axes = plt.subplots(1, 2, figsize=(DOUBLE_WIDTH_IN, 2.4), gridspec_kw={"wspace": 0.25})169    c = results["conditions"]170    d0 = [k for k in c["base"] if k.startswith("donor_s0")][0]171    chain = [("intact", c["base"][d0]["target_iid"]["accuracy"], c["base"][d0]["control_iid"]["accuracy"]),172             ("L0 attn.\nablated", *[list(c["mech_donor_ablated_attn0"].values())[0][s]["accuracy"] for s in ("target_iid", "control_iid")]),173             ("rescue:\nintact donor", *[list(c["mech_rescue_from_intact_donor"].values())[0][s]["accuracy"] for s in ("target_iid", "control_iid")]),174             ("rescue:\nsham map", *[list(c["mech_rescue_sham"].values())[0][s]["accuracy"] for s in ("target_iid", "control_iid")])]175    x = np.arange(len(chain))176    axes[0].bar(x - 0.18, [v[1] for v in chain], width=0.34, color=ENTITY_COLORS["donor"], label="target stratum", linewidth=0)177    axes[0].bar(x + 0.18, [v[2] for v in chain], width=0.34, color=ENTITY_COLORS["chance"], label="control stratum", linewidth=0)178    axes[0].set_xticks(x)179    axes[0].set_xticklabels([v[0] for v in chain], fontsize=6)180    axes[0].set_ylim(0, 1.02)181    axes[0].set_ylabel("accuracy")182    axes[0].set_title("ablation → loss → rescue (donor seed 0)", loc="left", fontsize=7.5)183    axes[0].legend(fontsize=6, loc="upper center", ncol=2, bbox_to_anchor=(0.5, -0.32))184    h5 = results["rule_vs_cache"]185    conds = [("h5_act_procrustes_seenfit", "act. graft\nProcrustes"), ("h5_act_ridge_seenfit", "act. graft\nridge"), ("h5_adapter_seenfit", "donor-free\nadapter")]186    conds = [(k, l) for k, l in conds if k in h5]187    x = np.arange(len(conds))188    for i, (key, lab, col) in enumerate((("seen", "seen Ω inputs", ENTITY_COLORS["graft"]), ("unseen", "unseen Ω inputs", ENTITY_COLORS["sham"]))):189        vals = [h5[k][key]["estimate"] for k, _ in conds]190        err = [[h5[k][key]["estimate"] - h5[k][key]["low"] for k, _ in conds], [h5[k][key]["high"] - h5[k][key]["estimate"] for k, _ in conds]]191        axes[1].bar(x + (i - 0.5) * 0.36, vals, width=0.34, color=col, label=lab, linewidth=0, yerr=err, error_kw={"elinewidth": 0.6, "ecolor": TEXT_SECONDARY, "capsize": 1.5})192    axes[1].set_xticks(x)193    axes[1].set_xticklabels([l for _, l in conds], fontsize=6)194    axes[1].set_ylim(0, 1.02)195    axes[1].set_title("rule vs cache (Ω-step accuracy; maps fitted on 6 of 11 inputs)", loc="left", fontsize=7.5)196    axes[1].legend(fontsize=6, loc="upper center", ncol=2, bbox_to_anchor=(0.5, -0.32))197    fig.savefig(out / "fig_mechanism_tier0.pdf")198    plt.close(fig)199
Files main
Menu

Theme