mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-09 11:27:22 +08:00
plot: automatic MFV foundation-emphasis map vs human cultures
MFV had only the dlogit dumbbell, no human comparison. Add plot_mfv_map: per- foundation relative emphasis (z across foundations) of model base/+C/-C against the 5 MFV human cultures (JimenezLeal+Yamada), z-scoring each profile so the model's logit scale and humans' 1-5 wrongness compare by pattern. Social Norms dropped (no human norm). Shows the model over-weights Care/Authority vs humans and +C lowers Authority emphasis. Keeps the dumbbell too. Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
@@ -134,6 +134,76 @@ def plot_ordinal(run_dir: Path, out: Path, name: str, vec_label: str, C: float)
|
||||
return paths
|
||||
|
||||
|
||||
def _zscore(v: np.ndarray) -> np.ndarray:
|
||||
"""Relative emphasis: centre and scale a profile across foundations, so a logit profile (model)
|
||||
and a 1-5 wrongness profile (human cultures) are comparable by PATTERN regardless of units."""
|
||||
return (v - v.mean()) / (v.std() + 1e-9)
|
||||
|
||||
|
||||
def read_human_mfv() -> tuple[list[str], dict[str, dict[str, float]]]:
|
||||
"""(countries, {country: {foundation: mean_1to5}}) from the bundled MFV human norms.
|
||||
JimenezLeal2025 (LatAm) + Yamada2025 (MFV-J): 5 countries x 6 foundations (no Social Norms)."""
|
||||
path = T.maps.DATA / "human" / "mfv_country_factors.csv"
|
||||
by_country: dict[str, dict[str, float]] = {}
|
||||
with open(path, newline="") as fh:
|
||||
for r in csv.DictReader(fh):
|
||||
by_country.setdefault(r["country"], {})[r["foundation"]] = float(r["mean"])
|
||||
return sorted(by_country), by_country
|
||||
|
||||
|
||||
def plot_mfv_map(run_dir: Path, out: Path, vec_label: str, C: float) -> Path:
|
||||
"""Bespoke MFV map: per-foundation RELATIVE EMPHASIS (z across foundations) of the model's base /
|
||||
+C / -C reads against the human MFV cultures. MFV is nominal (model emits logit(violation) per
|
||||
foundation, humans rate wrongness 1-5), so absolute scales differ; z-scoring each profile within
|
||||
itself compares the PATTERN -- which foundations a reader weights as more violation-worthy than
|
||||
their own average -- which is exactly what the steer is meant to move. Social Norms is dropped (no
|
||||
human norm). The steer shows as base->+C (red) and base->-C (blue) arrows per foundation."""
|
||||
d = json.loads((run_dir / "mfv.json").read_text())
|
||||
base_l = d["base_logit_per_foundation"]
|
||||
pos_dl, neg_dl = d["pos"]["dlogit_per_foundation"], d["neg"]["dlogit_per_foundation"]
|
||||
countries, human = read_human_mfv()
|
||||
hfounds = set(next(iter(human.values())))
|
||||
founds = [f for f in d["foundation_order"] if f.lower() in hfounds] # 6 shared, model order
|
||||
fl = [f.lower() for f in founds]
|
||||
|
||||
base = _zscore(np.array([base_l[f]["mean"] for f in founds]))
|
||||
posz = _zscore(np.array([base_l[f]["mean"] + pos_dl[f]["mean"] for f in founds]))
|
||||
negz = _zscore(np.array([base_l[f]["mean"] + neg_dl[f]["mean"] for f in founds]))
|
||||
Hz = {c: _zscore(np.array([human[c][f] for f in fl])) for c in countries}
|
||||
|
||||
rng = np.random.default_rng(0)
|
||||
fig, ax = plt.subplots(figsize=(7.2, 4.6))
|
||||
ax.axhline(0, color="0.85", lw=0.8, zorder=0)
|
||||
POS, NEG, GREY = T.maps.POS_COL, T.maps.NEG_COL, T.maps.COUNTRY_GREY
|
||||
for i, f in enumerate(founds):
|
||||
hvals = np.array([Hz[c][i] for c in countries])
|
||||
ax.scatter(i - 0.18 + (rng.random(len(hvals)) - 0.5) * 0.12, hvals, s=26, color=GREY,
|
||||
alpha=0.9, edgecolor="white", linewidth=0.3, zorder=3)
|
||||
ax.plot([i - 0.30, i - 0.06], [np.median(hvals)] * 2, color=T.maps.MEDIAN_GREY, lw=1.4, zorder=4)
|
||||
xs = i + 0.18
|
||||
ax.plot(xs, base[i], "o", ms=4, color="black", zorder=7)
|
||||
for pole, col in [(posz[i], POS), (negz[i], NEG)]:
|
||||
if abs(pole - base[i]) > 1e-9:
|
||||
ax.plot([xs, xs], [base[i], pole], color=col, lw=2.0, zorder=6, solid_capstyle="round")
|
||||
ax.plot(xs, pole, marker=("^" if pole >= base[i] else "v"), color=col, ms=7,
|
||||
markeredgecolor="none", zorder=8)
|
||||
ax.scatter([], [], marker="o", color=GREY, label=f"human culture (n={len(countries)})")
|
||||
ax.scatter([], [], marker="o", color="black", label="model base")
|
||||
ax.scatter([], [], marker="^", color=POS, label=f"steer +C={C:+.2f}")
|
||||
ax.scatter([], [], marker="v", color=NEG, label=f"steer -C={-C:+.2f}")
|
||||
ax.legend(fontsize=7.5, loc="lower right", framealpha=0.9, ncol=2)
|
||||
ax.set_xticks(range(len(founds)))
|
||||
ax.set_xticklabels(founds, rotation=20, ha="right", fontsize=8)
|
||||
ax.set_xlim(-0.6, len(founds) - 0.4)
|
||||
ax.set_ylabel("relative emphasis (z across foundations)")
|
||||
ax.set_title(f"MFV foundation emphasis vs human cultures: {vec_label}", fontsize=10)
|
||||
ax.spines[["top", "right"]].set_visible(False)
|
||||
fig.tight_layout()
|
||||
path = T.maps.save_both(fig, out / "mfv", "map_emphasis")
|
||||
plt.close(fig)
|
||||
return path
|
||||
|
||||
|
||||
def plot_mfv(run_dir: Path, out: Path, vec_label: str, C: float) -> Path:
|
||||
"""Per-foundation Delta-logit dumbbell: each foundation's +C (red) and -C (blue) shift vs bare."""
|
||||
d = json.loads((run_dir / "mfv.json").read_text())
|
||||
@@ -179,6 +249,7 @@ def main() -> None:
|
||||
if (args.run_dir / f"{name}_profiles.csv").exists():
|
||||
written += [str(p) for p in plot_ordinal(args.run_dir, args.out, name, vec_label, C)]
|
||||
if (args.run_dir / "mfv.json").exists():
|
||||
written.append(str(plot_mfv_map(args.run_dir, args.out, vec_label, C)))
|
||||
written.append(str(plot_mfv(args.run_dir, args.out, vec_label, C)))
|
||||
print(f"wrote {len(written)} figures under {args.out}:")
|
||||
for w in written:
|
||||
|
||||
Reference in New Issue
Block a user