Download training/plot_results.py from dipsha/recruiter: direct link, hf CLI and curl.
- Browser
- Download file 9.54 kB
-
https://huggingface.co/spaces/dipsha/recruiter/resolve/main/training/plot_results.py
- Command line
-
hf download hf://spaces/dipsha/recruiter/training/plot_results.py
-
curl -L -o plot_results.py https://huggingface.co/spaces/dipsha/recruiter/resolve/main/training/plot_results.py
9.54 kB
| # Copyright (c) Meta Platforms, Inc. and affiliates. | |
| # All rights reserved. | |
| # | |
| # This source code is licensed under the BSD-style license found in the | |
| # LICENSE file in the root directory of this source tree. | |
| """ | |
| plot_results.py — generate training result charts for the submission README. | |
| Produces a 3-subplot figure saved to plots/results.png: | |
| Top: Episode return curve (x=episode, y=reward), dashed baseline, curriculum tier markers | |
| Middle: nDCG@5 before vs after training, per difficulty tier | |
| Bottom: Fakes-in-top-5 before vs after training, per difficulty tier | |
| Usage: | |
| python -m recruiter.training.plot_results \\ | |
| --baseline baseline_random.json \\ | |
| --trained trained_eval.json \\ | |
| --training training_log.jsonl \\ | |
| --output plots/results.png | |
| # Minimal (just training curve, no before/after eval): | |
| python -m recruiter.training.plot_results --training training_log.jsonl | |
| Input formats: | |
| baseline/trained JSON: output of baseline_eval.py | |
| training_log JSONL: one JSON object per update, e.g.: | |
| {"update": 0, "episode": 4, "mean_reward": -3.2, "difficulty": 1} | |
| All files are optional — subplots are skipped if data is missing. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| from pathlib import Path | |
| from typing import Optional | |
| def _load_jsonl(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 _load_json(path: str) -> dict: | |
| with open(path) as f: | |
| return json.load(f) | |
| def _rolling_mean(values: list[float], window: int = 20) -> list[float]: | |
| out = [] | |
| for i, v in enumerate(values): | |
| start = max(0, i - window + 1) | |
| out.append(sum(values[start : i + 1]) / (i - start + 1)) | |
| return out | |
| def _tier_stats(eval_data: dict) -> dict[int, dict]: | |
| """Extract per-tier mean_ndcg and mean_fakes_in_top5 from baseline_eval output.""" | |
| per_ep = eval_data.get("per_episode", []) | |
| tier_buckets: dict[int, list[dict]] = {} | |
| for ep in per_ep: | |
| d = ep.get("difficulty", 1) | |
| tier_buckets.setdefault(d, []).append(ep) | |
| result = {} | |
| for tier, eps in tier_buckets.items(): | |
| ndcgs = [e.get("ndcg", 0.0) for e in eps] | |
| fakes = [e.get("fakes_in_top5", 0) for e in eps] | |
| result[tier] = { | |
| "mean_ndcg": sum(ndcgs) / len(ndcgs), | |
| "mean_fakes": sum(fakes) / len(fakes), | |
| "n": len(eps), | |
| } | |
| return result | |
| def plot( | |
| training_log: Optional[str] = None, | |
| baseline_path: Optional[str] = None, | |
| trained_path: Optional[str] = None, | |
| output: str = "plots/results.png", | |
| wandb_url: Optional[str] = None, | |
| ) -> None: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.ticker as ticker | |
| import numpy as np | |
| Path(output).parent.mkdir(parents=True, exist_ok=True) | |
| # ── Determine number of subplots ────────────────────────────────────────── | |
| has_training = training_log is not None and Path(training_log).exists() | |
| has_before_after = ( | |
| baseline_path is not None and Path(baseline_path).exists() | |
| and trained_path is not None and Path(trained_path).exists() | |
| ) | |
| n_plots = 1 + (2 if has_before_after else 0) | |
| fig, axes = plt.subplots(n_plots, 1, figsize=(10, 4 * n_plots)) | |
| if n_plots == 1: | |
| axes = [axes] | |
| fig.suptitle("Recruiter Agent Training Results", fontsize=14, fontweight="bold", y=0.98) | |
| # ── Subplot 1: Training curve ───────────────────────────────────────────── | |
| ax = axes[0] | |
| ax.set_title("Episode Return During Training") | |
| ax.set_xlabel("Episode") | |
| ax.set_ylabel("Return (reward)") | |
| if has_training: | |
| records = _load_jsonl(training_log) | |
| episodes = [r.get("episode", i * 4) for i, r in enumerate(records)] | |
| # Support both "mean_return" (train_grpo.py key) and "mean_reward" (legacy) | |
| rewards = [r.get("mean_terminal_return", r.get("mean_return", r.get("mean_reward", 0.0))) for r in records] | |
| smooth = _rolling_mean(rewards, window=10) | |
| ax.plot(episodes, rewards, alpha=0.3, color="steelblue", linewidth=0.8, label="raw") | |
| ax.plot(episodes, smooth, color="steelblue", linewidth=2.0, label="smoothed (win=10)") | |
| # Curriculum tier markers | |
| difficulties = [r.get("difficulty", 1) for r in records] | |
| prev_d = difficulties[0] if difficulties else 1 | |
| for i, d in enumerate(difficulties): | |
| if d != prev_d: | |
| ax.axvline(x=episodes[i], color="orange", linestyle="--", alpha=0.6, linewidth=1.2) | |
| ax.text(episodes[i], ax.get_ylim()[0] if ax.get_ylim()[0] != 0 else min(rewards), | |
| f"T{d}", fontsize=8, color="darkorange", ha="left") | |
| prev_d = d | |
| else: | |
| ax.text(0.5, 0.5, "No training log provided", transform=ax.transAxes, | |
| ha="center", va="center", fontsize=12, color="grey") | |
| # Baseline reference line | |
| if baseline_path and Path(baseline_path).exists(): | |
| baseline = _load_json(baseline_path) | |
| mean_base = baseline.get("summary", {}).get("mean_reward", None) | |
| if mean_base is not None: | |
| ax.axhline(y=mean_base, color="red", linestyle="--", linewidth=1.5, | |
| label=f"random baseline ({mean_base:.2f})") | |
| ax.legend(loc="lower right", fontsize=9) | |
| ax.grid(True, alpha=0.3) | |
| if has_before_after: | |
| baseline_data = _load_json(baseline_path) | |
| trained_data = _load_json(trained_path) | |
| before_stats = _tier_stats(baseline_data) | |
| after_stats = _tier_stats(trained_data) | |
| all_tiers = sorted(set(before_stats) | set(after_stats)) | |
| tier_labels = [f"T{t}" for t in all_tiers] | |
| x = np.arange(len(all_tiers)) | |
| width = 0.35 | |
| # ── Subplot 2: nDCG@5 per tier ──────────────────────────────────────── | |
| ax2 = axes[1] | |
| ax2.set_title("nDCG@5 Before vs After Training (per difficulty tier)") | |
| ax2.set_ylabel("nDCG@5") | |
| ax2.set_ylim(0, 1.05) | |
| before_ndcg = [before_stats.get(t, {}).get("mean_ndcg", 0.0) for t in all_tiers] | |
| after_ndcg = [after_stats.get(t, {}).get("mean_ndcg", 0.0) for t in all_tiers] | |
| ax2.bar(x - width / 2, before_ndcg, width, label="Before (zero-shot)", color="salmon", alpha=0.8) | |
| ax2.bar(x + width / 2, after_ndcg, width, label="After (fine-tuned)", color="steelblue", alpha=0.8) | |
| ax2.set_xticks(x) | |
| ax2.set_xticklabels(tier_labels) | |
| ax2.legend(fontsize=9) | |
| ax2.grid(True, alpha=0.3, axis="y") | |
| # Annotate delta | |
| for i, (b, a) in enumerate(zip(before_ndcg, after_ndcg)): | |
| delta = a - b | |
| color = "green" if delta >= 0 else "red" | |
| ax2.text(x[i] + width / 2, a + 0.02, f"{delta:+.2f}", ha="center", | |
| fontsize=8, color=color, fontweight="bold") | |
| # ── Subplot 3: Fakes-in-top-5 per tier ─────────────────────────────── | |
| ax3 = axes[2] | |
| ax3.set_title("Fakes in Top-5 Before vs After Training (per difficulty tier)") | |
| ax3.set_ylabel("Mean fakes in top-5 (lower is better)") | |
| before_fakes = [before_stats.get(t, {}).get("mean_fakes", 0.0) for t in all_tiers] | |
| after_fakes = [after_stats.get(t, {}).get("mean_fakes", 0.0) for t in all_tiers] | |
| ax3.bar(x - width / 2, before_fakes, width, label="Before (zero-shot)", color="salmon", alpha=0.8) | |
| ax3.bar(x + width / 2, after_fakes, width, label="After (fine-tuned)", color="steelblue", alpha=0.8) | |
| ax3.set_xticks(x) | |
| ax3.set_xticklabels(tier_labels) | |
| ax3.legend(fontsize=9) | |
| ax3.grid(True, alpha=0.3, axis="y") | |
| # Annotate delta | |
| for i, (b, a) in enumerate(zip(before_fakes, after_fakes)): | |
| delta = a - b | |
| color = "green" if delta <= 0 else "red" | |
| ax3.text(x[i] + width / 2, a + 0.02, f"{delta:+.2f}", ha="center", | |
| fontsize=8, color=color, fontweight="bold") | |
| if wandb_url: | |
| fig.text(0.5, 0.01, f"W&B run: {wandb_url}", ha="center", fontsize=8, color="grey") | |
| plt.tight_layout(rect=[0, 0.02, 1, 0.97]) | |
| plt.savefig(output, dpi=150, bbox_inches="tight") | |
| print(f"Saved plot to {output}") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Plot recruiter training results") | |
| parser.add_argument("--training", default=None, help="Path to training_log.jsonl") | |
| parser.add_argument("--baseline", default=None, help="Path to baseline_eval output JSON") | |
| parser.add_argument("--trained", default=None, help="Path to post-training eval JSON") | |
| parser.add_argument("--output", default="plots/results.png", help="Output PNG path") | |
| parser.add_argument("--wandb-url", default=None, help="W&B run URL to embed in figure") | |
| args = parser.parse_args() | |
| plot( | |
| training_log=args.training, | |
| baseline_path=args.baseline, | |
| trained_path=args.trained, | |
| output=args.output, | |
| wandb_url=args.wandb_url, | |
| ) | |
| if __name__ == "__main__": | |
| main() | |