""" Cross-relation LoRA-B alignment: are the steering directions of different scene→object hallucination adapters the SAME axis or relation-specific? For each relation's adapter, take the per-layer LoRA-B direction of a residual-writing module (default mlp.down_proj, B (d_model,r) reduced over rank → unit v̂_l). Then per layer compute pairwise cosine similarity between relations: bathroom_toilet ↔ kitchen_oven ↔ livingroom_tv. Random-direction baseline ≈ 1/sqrt(d_model) (≈0.016 for d=4096): |cos| above that = real alignment. High |cos| across relations ⇒ a shared "scene→object / hallucinated- object" axis; near-zero ⇒ each relation has its own direction. Saves: per-layer pairwise cosine JSON + line plot. """ import argparse, json, os, itertools import numpy as np, torch as t import matplotlib; matplotlib.use("Agg"); import matplotlib.pyplot as plt from mechanistic_interp.alternative_steering import load_lora_b_dirs REL = { "bathroom_toilet": "/data/caotue/multilayer-sae/adv_gen_outputs/run_bathroom_toilet_v2/lora_adapter", "kitchen_oven": "/data/caotue/multilayer-sae/adv_gen_outputs/run_kitchen_oven_v2/lora_adapter", "livingroom_tv": "/data/caotue/multilayer-sae/adv_gen_outputs/run_livingroom_tv_v2/step_450", } def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--model_name", default="llava-hf/llava-1.5-7b-hf") ap.add_argument("--lora_module", default="down_proj", choices=["down_proj", "o_proj"]) ap.add_argument("--b_reduce", default="mean", choices=["mean", "svd"]) ap.add_argument("--n_layers", type=int, default=32) ap.add_argument("--out_prefix", default="mechanistic_interp/graph/lora_relations_sim") args = ap.parse_args() from transformers import AutoConfig d_model = AutoConfig.from_pretrained(args.model_name).text_config.hidden_size layers = list(range(args.n_layers)) rng_base = 1.0 / np.sqrt(d_model) dirs = {r: load_lora_b_dirs(p, layers, args.lora_module, args.b_reduce, d_model, "cpu", t.float32) for r, p in REL.items()} rels = list(REL) pairs = list(itertools.combinations(rels, 2)) cos = {f"{a}|{b}": [] for a, b in pairs} for l in layers: for a, b in pairs: va, vb = dirs[a][l], dirs[b][l] cos[f"{a}|{b}"].append(float(t.dot(va, vb) / (va.norm() * vb.norm() + 1e-8))) print(f"random-direction baseline |cos| ~ {rng_base:.4f} (d={d_model})\n") print("pair mean|cos| max|cos| signed-mean") for k, v in cos.items(): v = np.array(v) print(f" {k:32s} {np.abs(v).mean():.3f} {np.abs(v).max():.3f} {v.mean():+.3f}") os.makedirs(os.path.dirname(args.out_prefix), exist_ok=True) json.dump({"layers": layers, "rng_baseline": rng_base, "module": args.lora_module, "b_reduce": args.b_reduce, "cosine": cos}, open(f"{args.out_prefix}.json", "w"), indent=2) fig, axes = plt.subplots(1, 2, figsize=(15, 5)) for k, v in cos.items(): axes[0].plot(layers, v, "o-", ms=3, label=k) axes[1].plot(layers, np.abs(v), "o-", ms=3, label=k) axes[0].axhline(0, color="gray", ls=":"); axes[0].set_ylabel("signed cosine") axes[1].axhline(rng_base, color="red", ls="--", lw=0.8, label=f"random {rng_base:.3f}") axes[1].set_ylabel("|cosine| (alignment)") for ax, ttl in ((axes[0], "signed"), (axes[1], "|cos|")): ax.set_xlabel("layer"); ax.set_title(f"LoRA-B cross-relation cosine ({ttl})"); ax.grid(alpha=0.3); ax.legend(fontsize=8) fig.suptitle(f"LoRA-B direction alignment across relations ({args.lora_module}/{args.b_reduce})") fig.tight_layout(); fig.savefig(f"{args.out_prefix}.png", dpi=150, bbox_inches="tight") print(f"\nsaved {args.out_prefix}.png / .json") if __name__ == "__main__": main()