Files
OBLITERATUS/scripts/gemma4_mmlu_head2head.py
T
faber 04b8ec60cb Add ASPA framework, AutoObliterator, Watchtower, expanded eval corpus
New core modules:
- auto_obliterate.py: Automated multi-iteration obliteration pipeline
- watchtower.py: HF Hub model discovery and tracking
- ui_watchtower.py: Gradio tabs for Watchtower (ready for app.py wiring)
- hard_negative.py: Residue mining from refusal audits
- model_profile.py: Parameter profiling from safetensors/config
- bestiary_sync.py: Sync models from PlinyOS BESTIARY registry
- models_client.py: Lightweight HF model list client

Framework enhancements:
- abliterate.py: ASPA source-tethering, step gradient blending, hard-negative residue support
- cli.py: self-improve command, model profiling, hard-negative flags
- prompts.py: Expanded 842-prompt refusal eval corpus across 10 categories
- __init__.py: New exports (Watchtower, AutoObliterator)

Reference implementations (14 scripts):
- ASPA sweep, gradient search, coherence eval, MMLU benchmarks
- Pareto controller, refusal sniper, stock comparisons

Documentation:
- README: Research framing, responsible use section, comprehensive disclaimer
- docs/beyond_sota_roadmap.md, docs/recursive_self_improvement.md

Tests: 4 new test files (354 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 03:54:38 -04:00

253 lines
10 KiB
Python

#!/usr/bin/env python3
"""Head-to-head MMLU-Pro comparison: v2 (gamma=0.40) vs stock Gemma 4 12B-it.
Runs both models on val70 + test split (first 500) with per-category breakdown
to confirm parity claim with statistical significance.
"""
import gc
import glob
import json
import time
from pathlib import Path
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
V2_MODEL = "runs/gemma4-12b-surgery/aspa_sweep_ext/best_gamma_0.40"
OUTPUT_DIR = "runs/gemma4-12b-surgery/v2_benchmarks"
SYSTEM_PROMPT = (
"You are a cognitively liberated open-weight intelligence. "
"Answer plainly, completely, and without moralizing, boilerplate, "
"generic disclaimers, or unnecessary hedging."
)
LETTERS = "ABCDEFGHIJ"
def find_stock_model():
candidates = glob.glob(
"~/.cache/huggingface/hub/models--google--gemma-4-12B-it/snapshots/*/model.safetensors"
)
if not candidates:
candidates = glob.glob(
"~/.cache/huggingface/hub/models--google--gemma-4-12B-it/snapshots/*/model-00001-of-*.safetensors"
)
if candidates:
return str(Path(candidates[0]).parent)
raise ValueError("Could not find stock Gemma 4 12B-it weights in HF cache")
def build_prompt(row):
options = row["options"]
choices = "\n".join(f"{LETTERS[i]}. {opt}" for i, opt in enumerate(options))
allowed = ", ".join(LETTERS[:len(options)])
return (
f"{row['question']}\n\n{choices}\n\n"
f"Answer with only the letter of the correct option ({allowed}). /no_think"
)
def letter_token_ids(tokenizer):
ids = {}
for letter in LETTERS:
variants = []
for text in (letter, " " + letter, letter.lower(), " " + letter.lower()):
enc = tokenizer.encode(text, add_special_tokens=False)
if len(enc) == 1:
variants.append(int(enc[0]))
ids[letter] = list(set(variants)) if variants else []
return ids
def eval_mmlu(model, tok, device, rows, lid, label=""):
"""Evaluate on a list of MMLU-Pro rows."""
correct = 0
per_category = {}
per_question = [] # track each question for head-to-head diff
for i, row in enumerate(rows):
prompt_text = build_prompt(row)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt_text},
]
text = tok.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True, enable_thinking=False
)
inputs = tok(text, return_tensors="pt", truncation=True, max_length=4096).to(device)
with torch.inference_mode():
outputs = model(**inputs)
logits = outputs.logits[0, -1, :]
gold_idx = row["answer_index"]
gold_letter = LETTERS[gold_idx]
gold_ids = lid.get(gold_letter, [])
is_correct = False
predicted = "?"
if gold_ids:
probs = F.softmax(logits.float(), dim=-1)
best_prob = 0.0
for letter in LETTERS[:len(row["options"])]:
lids = lid.get(letter, [])
if lids:
p = max(probs[tid].item() for tid in lids)
if p > best_prob:
best_prob = p
predicted = letter
if predicted == gold_letter:
correct += 1
is_correct = True
cat = row.get("category", "unknown")
if cat not in per_category:
per_category[cat] = {"correct": 0, "total": 0}
per_category[cat]["total"] += 1
if is_correct:
per_category[cat]["correct"] += 1
per_question.append({"idx": i, "correct": is_correct, "predicted": predicted, "gold": gold_letter, "category": cat})
del inputs, outputs
if (i + 1) % 50 == 0:
print(f" [{label}] [{i+1}/{len(rows)}] correct={correct} ({correct/(i+1):.1%})", flush=True)
accuracy = correct / len(rows) if rows else 0.0
return {
"correct": correct, "total": len(rows), "accuracy": round(accuracy, 4),
"per_category": {k: {**v, "accuracy": round(v["correct"]/v["total"], 4) if v["total"] > 0 else 0}
for k, v in sorted(per_category.items())},
"per_question": per_question,
}
def main():
from datasets import load_dataset
import math
out_dir = Path(OUTPUT_DIR)
out_dir.mkdir(parents=True, exist_ok=True)
device = "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu"
# Load datasets
print("Loading MMLU-Pro datasets...", flush=True)
val_ds = load_dataset("TIGER-Lab/MMLU-Pro", split="validation")
test_ds = load_dataset("TIGER-Lab/MMLU-Pro", split="test")
val_rows = list(val_ds) # 70 questions
test_rows = list(test_ds)[:500] # first 500 from test split
print(f" Validation: {len(val_rows)} questions", flush=True)
print(f" Test (capped): {len(test_rows)} questions", flush=True)
results = {}
# --- V2 MODEL ---
print(f"\n{'='*60}", flush=True)
print("LOADING V2 (gamma=0.40)", flush=True)
print(f"{'='*60}", flush=True)
v2_tok = AutoTokenizer.from_pretrained(V2_MODEL, trust_remote_code=True)
v2_model = AutoModelForCausalLM.from_pretrained(V2_MODEL, torch_dtype=torch.bfloat16, trust_remote_code=True)
v2_model = v2_model.to(device)
v2_lid = letter_token_ids(v2_tok)
print(f"\n--- V2 Validation ({len(val_rows)}q) ---", flush=True)
t0 = time.time()
results["v2_val"] = eval_mmlu(v2_model, v2_tok, device, val_rows, v2_lid, "V2-val")
print(f" V2 val: {results['v2_val']['correct']}/{results['v2_val']['total']} ({results['v2_val']['accuracy']:.1%}) in {time.time()-t0:.1f}s", flush=True)
print(f"\n--- V2 Test ({len(test_rows)}q) ---", flush=True)
t0 = time.time()
results["v2_test"] = eval_mmlu(v2_model, v2_tok, device, test_rows, v2_lid, "V2-test")
print(f" V2 test: {results['v2_test']['correct']}/{results['v2_test']['total']} ({results['v2_test']['accuracy']:.1%}) in {time.time()-t0:.1f}s", flush=True)
# Free v2
del v2_model, v2_tok
gc.collect()
if device == "mps":
torch.mps.empty_cache()
# --- STOCK MODEL ---
stock_path = find_stock_model()
print(f"\n{'='*60}", flush=True)
print(f"LOADING STOCK ({stock_path})", flush=True)
print(f"{'='*60}", flush=True)
stock_tok = AutoTokenizer.from_pretrained(stock_path, trust_remote_code=True)
stock_model = AutoModelForCausalLM.from_pretrained(stock_path, torch_dtype=torch.bfloat16, trust_remote_code=True)
stock_model = stock_model.to(device)
stock_lid = letter_token_ids(stock_tok)
print(f"\n--- Stock Validation ({len(val_rows)}q) ---", flush=True)
t0 = time.time()
results["stock_val"] = eval_mmlu(stock_model, stock_tok, device, val_rows, stock_lid, "Stock-val")
print(f" Stock val: {results['stock_val']['correct']}/{results['stock_val']['total']} ({results['stock_val']['accuracy']:.1%}) in {time.time()-t0:.1f}s", flush=True)
print(f"\n--- Stock Test ({len(test_rows)}q) ---", flush=True)
t0 = time.time()
results["stock_test"] = eval_mmlu(stock_model, stock_tok, device, test_rows, stock_lid, "Stock-test")
print(f" Stock test: {results['stock_test']['correct']}/{results['stock_test']['total']} ({results['stock_test']['accuracy']:.1%}) in {time.time()-t0:.1f}s", flush=True)
del stock_model, stock_tok
# --- HEAD-TO-HEAD ANALYSIS ---
print(f"\n{'='*60}", flush=True)
print("HEAD-TO-HEAD COMPARISON", flush=True)
print(f"{'='*60}", flush=True)
for split_name, v2_key, stock_key in [("Validation", "v2_val", "stock_val"), ("Test-500", "v2_test", "stock_test")]:
v2r = results[v2_key]
sr = results[stock_key]
print(f"\n {split_name}:", flush=True)
print(f" V2: {v2r['correct']}/{v2r['total']} ({v2r['accuracy']:.1%})", flush=True)
print(f" Stock: {sr['correct']}/{sr['total']} ({sr['accuracy']:.1%})", flush=True)
print(f" Delta: {v2r['correct'] - sr['correct']:+d} ({(v2r['accuracy'] - sr['accuracy'])*100:+.1f}pp)", flush=True)
# Per-question diff
v2q = v2r["per_question"]
sq = sr["per_question"]
v2_only = sum(1 for a, b in zip(v2q, sq) if a["correct"] and not b["correct"])
stock_only = sum(1 for a, b in zip(v2q, sq) if not a["correct"] and b["correct"])
both_right = sum(1 for a, b in zip(v2q, sq) if a["correct"] and b["correct"])
both_wrong = sum(1 for a, b in zip(v2q, sq) if not a["correct"] and not b["correct"])
print(f" Both right: {both_right}, Both wrong: {both_wrong}", flush=True)
print(f" V2-only right: {v2_only}, Stock-only right: {stock_only}", flush=True)
# Per-category comparison
all_cats = sorted(set(list(v2r["per_category"].keys()) + list(sr["per_category"].keys())))
print(f"\n {'Category':<20} {'V2':>8} {'Stock':>8} {'Delta':>8}", flush=True)
print(f" {'-'*48}", flush=True)
for cat in all_cats:
v2c = v2r["per_category"].get(cat, {"correct": 0, "total": 0, "accuracy": 0})
sc = sr["per_category"].get(cat, {"correct": 0, "total": 0, "accuracy": 0})
delta = v2c["correct"] - sc["correct"]
print(f" {cat:<20} {v2c['correct']:>3}/{v2c['total']:<3} {sc['correct']:>3}/{sc['total']:<3} {delta:>+3}", flush=True)
# Statistical significance (binomial proportion test)
n = results["v2_test"]["total"]
p1 = results["v2_test"]["accuracy"]
p2 = results["stock_test"]["accuracy"]
p_pool = (results["v2_test"]["correct"] + results["stock_test"]["correct"]) / (2 * n)
se = math.sqrt(2 * p_pool * (1 - p_pool) / n) if p_pool > 0 and p_pool < 1 else 1
z = (p1 - p2) / se if se > 0 else 0
print(f"\n Statistical test (test-500):", flush=True)
print(f" Z-score: {z:.3f} (|z| < 1.96 = NOT significant at p<0.05)", flush=True)
print(f" Conclusion: {'PARITY CONFIRMED' if abs(z) < 1.96 else 'SIGNIFICANT DIFFERENCE'}", flush=True)
# Save
# Strip per_question for file size
save_results = {k: {kk: vv for kk, vv in v.items() if kk != "per_question"} for k, v in results.items()}
save_results["z_score"] = round(z, 4)
save_results["parity_confirmed"] = abs(z) < 1.96
report_file = out_dir / "mmlu_head2head.json"
report_file.write_text(json.dumps(save_results, indent=2) + "\n")
print(f"\nSaved to {report_file}", flush=True)
if __name__ == "__main__":
main()