mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-07-13 07:36:33 +02:00
04b8ec60cb
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>
253 lines
10 KiB
Python
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()
|