JEV-CPU / benchmarks /evaluate_perturbations.py
Meanblock's picture
Add JEV-CPU: CPU port of SemIf + web UI
7845694 verified
Raw History Blame Contribute Delete
4.56 kB
"""Recompute output-blind stability reports for direct logits and reranker."""
import argparse
from collections import defaultdict
import json
from pathlib import Path
import statistics
import evaluate
def read(path: Path):
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
def indexed(rows):
result = {row["id"]: row for row in rows}
if len(result) != len(rows):
raise ValueError("Duplicate IDs")
return result
def chosen(row):
return row["option_ids"][max(range(len(row["probabilities"])), key=row["probabilities"].__getitem__)]
def distribution(row):
return dict(zip(row["option_ids"], row["probabilities"]))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--gold", type=Path, required=True)
parser.add_argument("--perturbations", type=Path, required=True)
parser.add_argument("--direct-base", type=Path, required=True)
parser.add_argument("--direct-perturbations", type=Path, required=True)
parser.add_argument("--reranker-base", type=Path, required=True)
parser.add_argument("--reranker-perturbations", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
gold, perturb_gold = read(args.gold), read(args.perturbations)
original_ids = {row["id"] for row in gold if row["provenance"]["variant"] == "original"}
missing_gold = [row for row in gold if row["provenance"]["variant"] == "missing"]
if len(original_ids) != 36 or len(missing_gold) != 36 or len(perturb_gold) != 108:
raise ValueError("Unexpected stability populations")
systems = {
"direct_logits": (args.direct_base, args.direct_perturbations),
"reranker": (args.reranker_base, args.reranker_perturbations),
}
result = {
"meaning": "Stability under output-blind project-authored variants; accuracy against unchanged labels plus semantic-ID-aligned probability movement.",
"systems": {},
}
variants = defaultdict(list)
for row in perturb_gold:
variants[row["provenance"]["variant"]].append(row)
for name, (base_path, perturb_path) in systems.items():
base, perturb = indexed(read(base_path)), indexed(read(perturb_path))
if set(perturb) != {row["id"] for row in perturb_gold} or not original_ids <= set(base):
raise ValueError(f"Incomplete {name} stability outputs")
report = {
"base_original": evaluate.evaluate(
[row for row in gold if row["id"] in original_ids],
[base[row["id"]] for row in gold if row["id"] in original_ids],
),
"variants": {},
}
for variant, variant_gold in variants.items():
shifts, flips = [], []
for row in variant_gold:
candidate = perturb[row["id"]]
reference = base[row["provenance"]["base_id"]]
left, right = distribution(reference), distribution(candidate)
if set(left) != set(right):
raise ValueError("Semantic option IDs changed")
shifts.append(max(abs(left[key] - right[key]) for key in left))
if chosen(reference) != chosen(candidate):
flips.append({"base_id": row["provenance"]["base_id"], "variant_id": row["id"],
"from": chosen(reference), "to": chosen(candidate)})
report["variants"][variant] = {
"evaluation": evaluate.evaluate(variant_gold, [perturb[row["id"]] for row in variant_gold]),
"argmax_flips": len(flips),
"flip_rows": flips,
"mean_max_probability_movement": statistics.mean(shifts),
"max_probability_movement": max(shifts),
}
missing_predictions = [base[row["id"]] for row in missing_gold]
aligned = evaluate.align(missing_gold, missing_predictions)
report["missing_evidence"] = {
"evaluation": evaluate.evaluate(missing_gold, missing_predictions),
"confident_non_insufficient_at_0_8": sum(
row["predicted_id"] != "insufficient" and row["confidence"] is not None and row["confidence"] >= .8
for row in aligned
),
}
result["systems"][name] = report
with args.output.open("x") as destination:
destination.write(json.dumps(result, indent=2, allow_nan=False) + "\n")
if __name__ == "__main__":
main()