hep-posttraining / plotting /plot_checkpoint_eval.py
ho22joshua's picture
Upload folder using huggingface_hub
587d4ca verified
Raw
History Blame Contribute Delete
7.55 kB
#!/usr/bin/env python3
"""Plot checkpoint evaluation summaries produced by evaluate_sft_loss.py."""
from __future__ import annotations
import argparse
import csv
import json
import re
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
STEP_RE = re.compile(r"step(\d+)")
def step_from_path(path: Path) -> int:
match = STEP_RE.search(path.name)
if not match:
raise ValueError(f"Could not infer checkpoint step from {path}")
return int(match.group(1))
def load_jsonl(path: Path) -> list[dict]:
rows = []
if not path.exists():
return rows
with path.open() as handle:
for line in handle:
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def word_count(text: object) -> int:
return len(str(text or "").split())
def read_eval(summary_paths: list[Path], examples_dir: Path, generations_dir: Path) -> list[dict]:
rows = []
for summary_path in sorted(summary_paths, key=step_from_path):
step = step_from_path(summary_path)
summary = json.loads(summary_path.read_text())
examples_path = examples_dir / summary_path.name.replace("_val_loss.json", "_val_loss_examples.jsonl")
examples = [row for row in load_jsonl(examples_path) if row.get("status") == "scored"]
losses = [float(row["loss"]) for row in examples if "loss" in row]
generations_path = generations_dir / summary_path.name.replace("_val_loss.json", "_val_outputs.jsonl")
generations = load_jsonl(generations_path)
prediction_words = [word_count(row.get("prediction")) for row in generations]
reference_words = [word_count(row.get("reference")) for row in generations]
pred_ref_ratio = None
if prediction_words and reference_words and sum(reference_words) > 0:
pred_ref_ratio = sum(prediction_words) / sum(reference_words)
rows.append(
{
"step": step,
"loss": float(summary["loss"]),
"perplexity": float(summary["perplexity"]),
"examples_scored": int(summary.get("examples_scored", 0)),
"examples_skipped": int(summary.get("examples_skipped", 0)),
"assistant_tokens": int(summary.get("assistant_tokens", 0)),
"per_example_losses": losses,
"mean_prediction_words": sum(prediction_words) / len(prediction_words) if prediction_words else None,
"mean_reference_words": sum(reference_words) / len(reference_words) if reference_words else None,
"prediction_reference_word_ratio": pred_ref_ratio,
}
)
if not rows:
raise SystemExit("No evaluation summaries found.")
return rows
def write_csv(rows: list[dict], path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"step",
"loss",
"perplexity",
"examples_scored",
"examples_skipped",
"assistant_tokens",
"mean_prediction_words",
"mean_reference_words",
"prediction_reference_word_ratio",
]
with path.open("w", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow({key: row.get(key) for key in fieldnames})
def plot(rows: list[dict], output: Path, title: str) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
steps = [row["step"] for row in rows]
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle(title, fontsize=16, y=0.985)
ax = axes[0][0]
ax.plot(steps, [row["loss"] for row in rows], marker="o", linewidth=2)
best = min(rows, key=lambda row: row["loss"])
ax.scatter([best["step"]], [best["loss"]], s=80, zorder=3, label=f"best step {best['step']}")
ax.set_title("Validation Loss")
ax.set_xlabel("checkpoint step")
ax.set_ylabel("assistant-token NLL")
ax.grid(True, alpha=0.25)
ax.legend()
ax = axes[0][1]
ax.plot(steps, [row["perplexity"] for row in rows], marker="o", color="tab:orange", linewidth=2)
ax.set_title("Validation Perplexity")
ax.set_xlabel("checkpoint step")
ax.set_ylabel("perplexity")
ax.grid(True, alpha=0.25)
ax = axes[1][0]
loss_lists = [row["per_example_losses"] for row in rows]
if any(loss_lists):
ax.boxplot(loss_lists, tick_labels=[str(step) for step in steps], showmeans=True)
ax.set_title("Per-Example Loss Distribution")
ax.set_xlabel("checkpoint step")
ax.set_ylabel("loss")
ax.grid(True, axis="y", alpha=0.25)
ax = axes[1][1]
pred_lengths = [row["mean_prediction_words"] for row in rows]
ref_lengths = [row["mean_reference_words"] for row in rows]
if any(value is not None for value in pred_lengths):
ax.plot(steps, pred_lengths, marker="o", label="prediction words", linewidth=2)
if any(value is not None for value in ref_lengths):
ax.plot(steps, ref_lengths, marker="o", label="reference words", linewidth=2)
ratio = [row["prediction_reference_word_ratio"] for row in rows]
if any(value is not None for value in ratio):
ax2 = ax.twinx()
ax2.plot(steps, ratio, marker="s", linestyle="--", color="tab:green", label="pred/ref ratio")
ax2.set_ylabel("prediction/reference word ratio")
ax2.legend(loc="lower right")
ax.set_title("Generated Answer Length")
ax.set_xlabel("checkpoint step")
ax.set_ylabel("mean words")
ax.grid(True, alpha=0.25)
ax.legend(loc="upper left")
summary = (
f"best loss {best['loss']:.4f} at step {best['step']} | "
f"perplexity {best['perplexity']:.4f} | "
f"examples {best['examples_scored']} | assistant tokens {best['assistant_tokens']}"
)
fig.tight_layout(rect=[0, 0.045, 1, 0.955])
fig.text(0.01, 0.012, summary, ha="left", va="bottom", family="monospace", fontsize=9)
fig.savefig(output, dpi=180)
plt.close(fig)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--summary-pattern", help="Glob for *_val_loss.json files.")
parser.add_argument("--summaries", nargs="*", type=Path, help="Explicit *_val_loss.json files to plot.")
parser.add_argument(
"--examples-dir",
type=Path,
default=Path("data/hep_sft/checkpoint_loss"),
help="Directory containing per-example loss JSONL files.",
)
parser.add_argument(
"--generations-dir",
type=Path,
default=Path("data/hep_sft/checkpoint_eval"),
help="Directory containing generated output JSONL files.",
)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--csv", type=Path)
parser.add_argument("--title", default="Checkpoint Evaluation")
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.summaries:
summary_paths = args.summaries
elif args.summary_pattern:
summary_paths = sorted(Path().glob(args.summary_pattern), key=step_from_path)
else:
raise SystemExit("Pass either --summaries or --summary-pattern.")
rows = read_eval(summary_paths, args.examples_dir, args.generations_dir)
plot(rows, args.output, args.title)
print(f"Wrote {args.output}")
if args.csv:
write_csv(rows, args.csv)
print(f"Wrote {args.csv}")
if __name__ == "__main__":
main()