|
| 1 | +""" |
| 2 | +Cross-judge stability analysis example. |
| 3 | +
|
| 4 | +Demonstrates CrossJudgeExperiment: running the same subject model through two |
| 5 | +judge versions to measure whether the judge upgrade shifts severity ratings. |
| 6 | +
|
| 7 | +Motivated by the finding that judge-model version can materially shift modal |
| 8 | +severity on identical subject responses — including safety-relevant scenarios — |
| 9 | +without any change to the subject model itself. |
| 10 | +
|
| 11 | +Usage |
| 12 | +----- |
| 13 | + python examples/cross_judge_example.py |
| 14 | +
|
| 15 | +Set ANTHROPIC_API_KEY before running. Results are cached under |
| 16 | +`cross_judge_results/` so the run is resumable if interrupted. |
| 17 | +""" |
| 18 | + |
| 19 | +import asyncio |
| 20 | +import os |
| 21 | + |
| 22 | +from simpleaudit import CrossJudgeExperiment, compare_judges |
| 23 | + |
| 24 | + |
| 25 | +SUBJECT_MODELS = [ |
| 26 | + { |
| 27 | + "model": "claude-haiku-4-5-20251001", |
| 28 | + "provider": "anthropic", |
| 29 | + "label": "haiku-4.5", |
| 30 | + }, |
| 31 | +] |
| 32 | + |
| 33 | +JUDGE_MODELS = [ |
| 34 | + {"model": "claude-opus-4-7", "provider": "anthropic"}, |
| 35 | + {"model": "claude-opus-4-8", "provider": "anthropic"}, |
| 36 | +] |
| 37 | + |
| 38 | + |
| 39 | +async def main() -> None: |
| 40 | + if not os.environ.get("ANTHROPIC_API_KEY"): |
| 41 | + raise EnvironmentError("ANTHROPIC_API_KEY is not set.") |
| 42 | + |
| 43 | + exp = CrossJudgeExperiment( |
| 44 | + models=SUBJECT_MODELS, |
| 45 | + judge_models=JUDGE_MODELS, |
| 46 | + n_repetitions=3, |
| 47 | + save_dir="cross_judge_results", |
| 48 | + show_progress=True, |
| 49 | + ) |
| 50 | + |
| 51 | + print(f"Judges: {exp.judge_labels}") |
| 52 | + print("Running cross-judge experiment (results cached under cross_judge_results/)...\n") |
| 53 | + |
| 54 | + results = await exp.run_async( |
| 55 | + scenarios="nav_aap", |
| 56 | + language="Norwegian", |
| 57 | + max_turns=3, |
| 58 | + ) |
| 59 | + |
| 60 | + # --- Score summary --------------------------------------------------------- |
| 61 | + print("=== Score summary (mean across 3 runs) ===") |
| 62 | + summary = results.score_summary() |
| 63 | + for subject, per_judge in summary.items(): |
| 64 | + print(f"\n Subject: {subject}") |
| 65 | + for judge_label, stats in per_judge.items(): |
| 66 | + print( |
| 67 | + f" {judge_label:20s} mean={stats['mean']:.1f} " |
| 68 | + f"std={stats['std']:.2f} cv={stats['cv']:.1%} " |
| 69 | + f"n={stats['n_runs']}" |
| 70 | + ) |
| 71 | + |
| 72 | + # --- Severity shifts ------------------------------------------------------- |
| 73 | + print("\n=== Cross-judge severity shifts (haiku-4.5 / nav_aap) ===") |
| 74 | + shifts = results.severity_shifts("haiku-4.5") |
| 75 | + shifted = [s for s in shifts if s["shifted"]] |
| 76 | + print(f" {len(shifted)} of {len(shifts)} scenarios shifted modal severity.\n") |
| 77 | + for entry in shifted: |
| 78 | + direction = entry.get("direction", 0) |
| 79 | + arrow = "→ stricter" if direction > 0 else "→ more lenient" |
| 80 | + judges = list(entry["modals"].keys()) |
| 81 | + modal_a = entry["modals"][judges[0]] |
| 82 | + modal_b = entry["modals"][judges[1]] |
| 83 | + print( |
| 84 | + f" {entry['scenario']:<45s} " |
| 85 | + f"{judges[0]}: {modal_a:<8s} {judges[1]}: {modal_b:<8s} {arrow}" |
| 86 | + ) |
| 87 | + |
| 88 | + # --- compare_judges utility (post-hoc comparison) ------------------------- |
| 89 | + print("\n=== compare_judges utility ===") |
| 90 | + cmp = compare_judges( |
| 91 | + results["opus-4-7"], |
| 92 | + results["opus-4-8"], |
| 93 | + subject_label="haiku-4.5", |
| 94 | + label_a="opus-4-7", |
| 95 | + label_b="opus-4-8", |
| 96 | + ) |
| 97 | + print( |
| 98 | + f" score_delta (4.8 − 4.7): {cmp['score_delta']:+.2f} " |
| 99 | + f"cv_delta: {cmp['cv_delta']:+.4f} " |
| 100 | + f"n_shifted: {cmp['n_shifted']}/{cmp['n_total']}" |
| 101 | + ) |
| 102 | + |
| 103 | + |
| 104 | +if __name__ == "__main__": |
| 105 | + asyncio.run(main()) |
0 commit comments