MachineSex/figures/plot_speciation_real.py
Giorgio Gilestro ea051a5f92 E13b/c: harden real-weight speciation — full symmetry group + emergent-divergence null
E13c (the symmetry defense): alignment now runs modulo the FULL
function-preserving unit symmetry group of a ReLU MLP (per-unit positive
rescaling via canonicalise_scale, composed with Re-Basin permutations;
sanity gate recovers a permuted-and-rescaled copy exactly). Verdict: the
full group removes the independent-init barrier (residual 0.001) and
essentially none of the conflict barrier (0.502 -> 0.497) — the residual
is functional, not a missed symmetry (answers arXiv:2606.23607). The
cliff gains a hybrid-fitness readout: merged accuracy 0.97 -> 0.03 with
conflict. Floor proposition drafted (paper/si-notes.md S1): endpoint
invariance + max(eps_A, eps_B) >= mu(S)/2 for any merged model under any
alignment group.

E13b (emergent divergence): pre-registered second reading — with NO
conflicting training signal (disjoint class specialists; rolled-input
conventions), residual is 0.000 at every divergence to t_div=3200, and
the merge RESCUES the forgetting specialists (parents 0.535/0.474 ->
merged 0.955; a sustained Fisher-Muller rescue at zero barrier).
Speciation in real weights requires functional conflict; it does not
emerge from compatible specialisation on shared ancestry. LLM-scale
over-specialisation (cf. 2607.11997) deferred to Phase-3 llm_speciation.

3-panel figure, READMEs, +2 tests (149 green), make mnist wired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkRLcc18rwT2Lysu6PbG7v
2026-09-06 12:35:14 +01:00

108 lines
5.9 KiB
Python

"""E13 figure — real-weight model speciation: what alignment can and cannot merge, and what emerges.
(A) The barrier decomposition per condition, at two alignment strengths: the linear-mode-connectivity
error barrier between two merged MLPs, naive vs after Git Re-Basin permutation alignment vs after
alignment modulo the FULL ReLU unit symmetry group (scale-canonicalisation + permutation, E13c).
`independent` (same task, different init) is a coordinate artefact — either alignment removes ~all of
it; `conflict` (contradictory label maps) survives both — real reproductive isolation, not a missed
symmetry (cf. arXiv:2606.23607).
(B) The isolation cliff as hybrid fitness: sweeping the fraction of conflicting classes, the residual
(full-symmetry) barrier rises while the merged (midpoint) model's accuracy falls 0.97 -> 0.03 — the
real-weight image of E12's compatible -> depression -> inviability trajectory.
(C) Emergent divergence (E13b): children specialising on disjoint classes (or divergent input
conventions) from a shared fork develop NO residual barrier at any divergence — instead the merge
RESCUES the two forgetting specialists (Fisher-Muller), holding ~0.95 while the parents decay.
Speciation requires functional conflict; it does not emerge from compatible specialisation here.
Usage: python figures/plot_speciation_real.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
def main() -> None:
dec, _ = load_bundle("results/speciation_real")
cliff, _ = load_bundle("results/speciation_real_cliff")
emer, _ = load_bundle("results/speciation_real_emergent")
fig, axes = plt.subplots(1, 3, figsize=(17.5, 5))
# Panel A: naive / residual(perm) / residual(perm+scale) per condition.
ax = axes[0]
order = [c for c in ["shared", "independent", "conflict"] if c in set(dec["condition"])]
g = dec.groupby("condition").agg(naive=("barrier_naive", "mean"),
res_p=("residual", "mean"),
res_s=("residual_scale", "mean")).reindex(order)
x = np.arange(len(order)); w = 0.27
ax.bar(x - w, g["naive"], w, label="naive (no alignment)", color="#9ecae1")
ax.bar(x, g["res_p"], w, label="residual after permutation\n(Git Re-Basin)", color="#fc9272")
ax.bar(x + w, g["res_s"], w, label="residual after FULL symmetry group\n(scale + permutation)", color="#d62728")
ax.set_xticks(x); ax.set_xticklabels(order)
ax.set(ylabel="linear-mode-connectivity error barrier",
title="(A) coordinate artefact vs functional isolation\n"
"(conflict survives the full ReLU symmetry group)")
ax.legend(frameon=False, fontsize=7.5)
# Panel B: the cliff — residual barrier and hybrid fitness vs conflict fraction.
ax = axes[1]
cg = cliff.groupby("conflict_frac").agg(res_s=("residual_scale", "mean"), sd=("residual_scale", "std"),
nai=("barrier_naive", "mean"),
hyb=("acc_merge_scale", "mean")).reset_index()
ax.plot(cg["conflict_frac"], cg["nai"], "--o", color="#999", lw=1.4, label="naive barrier")
ax.plot(cg["conflict_frac"], cg["res_s"], "-o", color="#d62728", lw=2,
label="residual (full-symmetry alignment)")
ax.fill_between(cg["conflict_frac"], cg["res_s"] - cg["sd"], cg["res_s"] + cg["sd"],
color="#d62728", alpha=0.15)
ax.set(xlabel="fraction of classes with conflicting labels", ylabel="error barrier",
ylim=(-0.02, None),
title="(B) the isolation cliff, in real weights\n(hybrid fitness falls as conflict rises)")
ax2 = ax.twinx()
ax2.plot(cg["conflict_frac"], cg["hyb"], "-s", color="#2c7fb8", lw=1.8, label="merged-model accuracy")
ax2.set_ylabel("merged (hybrid) accuracy", color="#2c7fb8")
ax2.tick_params(axis="y", labelcolor="#2c7fb8"); ax2.set_ylim(-0.02, 1.02)
lines, labels = ax.get_legend_handles_labels()
l2, la2 = ax2.get_legend_handles_labels()
ax.legend(lines + l2, labels + la2, frameon=False, fontsize=7.5, loc="center left")
# Panel C: emergent divergence — no isolation; the merge rescues the forgetting specialists.
ax = axes[2]
colors = {"shared": "#999999", "disjoint": "#2c7fb8", "augment": "#41ab5d"}
for cond in ["shared", "disjoint", "augment"]:
sub = emer[emer["condition"] == cond]
if sub.empty:
continue
m = sub.groupby("t_div").agg(merge=("acc_merge_scale", "mean"),
pa=("acc_parent_a", "mean"), pb=("acc_parent_b", "mean"),
res=("residual_scale", "mean")).reset_index()
ax.plot(m["t_div"], m["merge"], "-o", color=colors[cond], lw=2, label=f"{cond}: merged")
if cond == "disjoint":
ax.plot(m["t_div"], (m["pa"] + m["pb"]) / 2, "--", color=colors[cond], lw=1.2,
label="disjoint: parents (forgetting)")
max_res = float(emer[emer["condition"] != "shared"]["residual_scale"].max())
ax.set_xscale("log")
ax.set(xlabel="divergence (post-fork training steps)", ylabel="accuracy on the full task",
ylim=(0, 1.02),
title="(C) emergent divergence does NOT speciate —\n"
f"the merge rescues the specialists (max residual = {max_res:.3f})")
ax.legend(frameon=False, fontsize=7.5, loc="center left")
fig.suptitle("E13 — real-weight model speciation: isolation requires functional conflict; "
"alignment (even modulo the full symmetry group) cannot remove it, and compatible "
"specialists merge into a rescuing generalist", y=1.03, fontsize=12)
fig.tight_layout()
savefig(fig, "results/speciation_real", "speciation_real")
if __name__ == "__main__":
main()