Spaces:
Running
Running
| """ | |
| plot_training.py — Generate training curve plots from GRPO metrics. | |
| Reads metrics.jsonl (one JSON object per line) and produces: | |
| 1. Mean reward vs iteration (with max reward band) | |
| 2. Per-task reward heatmap | |
| 3. Loss + KL divergence (if gradient training was used) | |
| Usage: | |
| python plot_training.py --metrics grpo_output/metrics.jsonl --output training_curves.png | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.ticker as ticker | |
| HAS_MPL = True | |
| except ImportError: | |
| HAS_MPL = False | |
| def load_metrics(path: str) -> list[dict]: | |
| records = [] | |
| with open(path) as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| records.append(json.loads(line)) | |
| return records | |
| def plot_rewards(records: list[dict], ax): | |
| iters = [r["iteration"] for r in records] | |
| means = [r["mean_reward"] for r in records] | |
| maxes = [r["max_reward"] for r in records] | |
| ax.fill_between(iters, means, maxes, alpha=0.2, color="steelblue", label="max reward") | |
| ax.plot(iters, means, "o-", color="steelblue", markersize=4, linewidth=1.5, label="mean reward") | |
| ax.set_xlabel("Iteration") | |
| ax.set_ylabel("Reward") | |
| ax.set_title("GRPO Training: Reward vs Iteration") | |
| ax.legend(loc="lower right") | |
| ax.set_ylim(0, 1.05) | |
| ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) | |
| ax.grid(True, alpha=0.3) | |
| def plot_per_task(records: list[dict], ax): | |
| all_tasks = sorted({t for r in records for t in r.get("task_rewards", {})}) | |
| if not all_tasks: | |
| ax.text(0.5, 0.5, "No per-task data", ha="center", va="center") | |
| return | |
| data = [] | |
| for r in records: | |
| tr = r.get("task_rewards", {}) | |
| data.append([tr.get(t, 0.0) for t in all_tasks]) | |
| iters = [r["iteration"] for r in records] | |
| for i, task in enumerate(all_tasks): | |
| vals = [d[i] for d in data] | |
| short_name = task.replace("T", "").replace("_", " ").title() | |
| ax.plot(iters, vals, "o-", markersize=3, linewidth=1, label=short_name) | |
| ax.set_xlabel("Iteration") | |
| ax.set_ylabel("Mean Reward") | |
| ax.set_title("Per-Task Reward") | |
| ax.legend(loc="lower right", fontsize=7, ncol=2) | |
| ax.set_ylim(0, 1.05) | |
| ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True)) | |
| ax.grid(True, alpha=0.3) | |
| def plot_loss_kl(records: list[dict], ax): | |
| has_loss = any("loss" in r for r in records) | |
| if not has_loss: | |
| ax.text(0.5, 0.5, "API mode (no gradient updates)\nRollout-only metrics shown", | |
| ha="center", va="center", fontsize=10, style="italic") | |
| ax.set_title("Loss & KL (gradient training only)") | |
| return | |
| iters = [r["iteration"] for r in records if "loss" in r] | |
| losses = [r["loss"] for r in records if "loss" in r] | |
| kls = [r["kl"] for r in records if "kl" in r] | |
| ax.plot(iters, losses, "s-", color="tomato", markersize=3, label="loss") | |
| ax2 = ax.twinx() | |
| ax2.plot(iters, kls, "^-", color="seagreen", markersize=3, label="KL") | |
| ax.set_xlabel("Iteration") | |
| ax.set_ylabel("Loss", color="tomato") | |
| ax2.set_ylabel("KL Divergence", color="seagreen") | |
| ax.set_title("Loss & KL Divergence") | |
| ax.legend(loc="upper left") | |
| ax2.legend(loc="upper right") | |
| ax.grid(True, alpha=0.3) | |
| def main(): | |
| p = argparse.ArgumentParser(description="Plot GRPO training curves") | |
| p.add_argument("--metrics", default="grpo_output/metrics.jsonl", | |
| help="Path to metrics.jsonl") | |
| p.add_argument("--output", default="training_curves.png", | |
| help="Output image path (default: training_curves.png)") | |
| args = p.parse_args() | |
| if not HAS_MPL: | |
| print("matplotlib not installed. Install with: pip install matplotlib") | |
| print("Generating text summary instead...\n") | |
| records = load_metrics(args.metrics) | |
| for r in records: | |
| print(f"Iter {r['iteration']:3d}: mean_reward={r['mean_reward']:.4f} " | |
| f"max_reward={r['max_reward']:.4f} valid={r['n_valid']}/{r['n_total']}") | |
| return | |
| records = load_metrics(args.metrics) | |
| if not records: | |
| print(f"No records found in {args.metrics}") | |
| return | |
| fig, axes = plt.subplots(1, 3, figsize=(16, 5)) | |
| plot_rewards(records, axes[0]) | |
| plot_per_task(records, axes[1]) | |
| plot_loss_kl(records, axes[2]) | |
| fig.suptitle("Comtrade Agent — GRPO Training Metrics", fontsize=13, fontweight="bold") | |
| fig.tight_layout(rect=[0, 0, 1, 0.95]) | |
| fig.savefig(args.output, dpi=150, bbox_inches="tight") | |
| print(f"Training curves saved to {args.output}") | |
| # Also print summary | |
| last = records[-1] | |
| first = records[0] | |
| print(f"\nSummary ({len(records)} iterations):") | |
| print(f" First iter: mean_reward={first['mean_reward']:.4f}") | |
| print(f" Last iter: mean_reward={last['mean_reward']:.4f}") | |
| print(f" Best max: {max(r['max_reward'] for r in records):.4f}") | |
| if __name__ == "__main__": | |
| main() | |