| """ |
| Plot generators for the README results section. |
| |
| Produces two PNG files in ``results/``: |
| |
| reward_curve.png β per-step training reward (from training_state.json) |
| before_after.png β per-task baseline vs trained score bar chart |
| |
| Run after both ``baseline_scores.json`` and ``trained_scores.json`` |
| exist:: |
| |
| python training/plot.py |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import sys |
| from pathlib import Path |
| from typing import Dict, List |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
|
|
|
|
| ROOT = Path(__file__).resolve().parent.parent |
| REPO_ROOT = ROOT.parent |
| RESULTS = ROOT / "results" |
| BASELINE = REPO_ROOT / "baseline_scores.json" |
| BASELINE_7B = REPO_ROOT / "baseline_7b_scores.json" |
| TRAINED = REPO_ROOT / "trained_scores.json" |
| TRAIN_STATE = RESULTS / "training_state.json" |
|
|
|
|
| def _load_scores(path: Path) -> Dict[str, float]: |
| if not path.exists(): |
| sys.exit(f"missing {path}") |
| return json.loads(path.read_text())["scores"] |
|
|
|
|
| def plot_reward_curve() -> None: |
| if not TRAIN_STATE.exists(): |
| print(f"[WARN] {TRAIN_STATE} not found β skipping reward_curve.png") |
| return |
| history = json.loads(TRAIN_STATE.read_text()) |
| rows = [h for h in history if "reward" in h and "step" in h] |
| if not rows: |
| print("[WARN] no reward entries in training_state.json") |
| return |
|
|
| steps = [h["step"] for h in rows] |
| rewards = [h["reward"] for h in rows] |
|
|
| plt.figure(figsize=(8, 5)) |
| plt.plot(steps, rewards, linewidth=2, color="#2563eb", |
| label="GRPO training reward") |
| if "loss" in rows[0]: |
| plt.twinx().plot( |
| steps, [h.get("loss", 0) for h in rows], |
| linewidth=1, color="#9ca3af", linestyle="--", label="loss", |
| ) |
| plt.xlabel("Training step") |
| plt.ylabel("Mean episode reward") |
| plt.title("Enterprise Contract Guardian β GRPO Training") |
| plt.grid(alpha=0.3) |
| plt.legend(loc="lower right") |
| plt.tight_layout() |
| out = RESULTS / "reward_curve.png" |
| plt.savefig(out, dpi=150) |
| plt.close() |
| print(f"[INFO] wrote {out}") |
|
|
|
|
| def plot_before_after() -> None: |
| """Three-bar comparison: 72B baseline, 7B baseline, 7B + LoRA trained.""" |
| baseline72 = _load_scores(BASELINE) |
| baseline7 = _load_scores(BASELINE_7B) if BASELINE_7B.exists() else None |
| trained = _load_scores(TRAINED) |
|
|
| tasks = list(trained.keys()) |
| b72 = [baseline72.get(t, 0.0) for t in tasks] |
| b7 = [baseline7.get(t, 0.0) for t in tasks] if baseline7 else None |
| tr = [trained.get(t, 0.0) for t in tasks] |
|
|
| x = np.arange(len(tasks)) |
| n_bars = 3 if baseline7 else 2 |
| width = 0.8 / n_bars |
|
|
| plt.figure(figsize=(12, 6)) |
| if baseline7: |
| plt.bar(x - width, b72, width, label="Qwen2.5-72B (untrained)", |
| color="#6b7280") |
| plt.bar(x, b7, width, label="Qwen2.5-7B (untrained, same base)", |
| color="#9ca3af") |
| plt.bar(x + width, tr, width, label="Qwen2.5-7B + LoRA (GRPO-trained)", |
| color="#16a34a") |
| else: |
| plt.bar(x - width / 2, b72, width, label="Baseline (Qwen2.5-72B)", |
| color="#9ca3af") |
| plt.bar(x + width / 2, tr, width, label="Trained 7B + LoRA", |
| color="#16a34a") |
| plt.xticks(x, tasks, rotation=30, ha="right", fontsize=9) |
| plt.ylabel("Episode score (0β1)") |
| plt.ylim(0, 1.0) |
| plt.title( |
| "Per-task score: untrained baselines vs GRPO-trained adapter\n" |
| "GRPO unlocks `detect_breaking_changes` (0.01 β 0.67) β neither baseline can do it" |
| ) |
| plt.legend(loc="upper right", fontsize=9) |
| plt.grid(alpha=0.3, axis="y") |
| plt.tight_layout() |
| out = RESULTS / "before_after.png" |
| plt.savefig(out, dpi=150) |
| plt.close() |
| print(f"[INFO] wrote {out}") |
|
|
|
|
| def main() -> None: |
| RESULTS.mkdir(parents=True, exist_ok=True) |
| plot_reward_curve() |
| plot_before_after() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|