ShawnYue
Person E: utils, experiment scripts, report figure generator; omit HF-rejected binaries
f102f56 | """ | |
| Generate publication-style figures for docs/experiment_report_latex/figures/. | |
| Reads archived metrics from result/training_summary.json and result/evaluation_results.json | |
| (no fabricated scores). Also draws a schematic pipeline diagram (no numeric claims). | |
| Usage (from repo root): | |
| python scripts/generate_report_figures.py | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from matplotlib.patches import FancyArrowPatch, FancyBboxPatch | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| RESULT_DIR = REPO_ROOT / "result" | |
| FIG_DIR = REPO_ROOT / "docs" / "experiment_report_latex" / "figures" | |
| plt.rcParams.update( | |
| { | |
| "figure.dpi": 120, | |
| "savefig.dpi": 160, | |
| "font.size": 10, | |
| "axes.titlesize": 11, | |
| "axes.labelsize": 10, | |
| "axes.unicode_minus": False, | |
| "axes.grid": True, | |
| "grid.alpha": 0.25, | |
| "grid.linestyle": "--", | |
| } | |
| ) | |
| def _load_json(path: Path) -> dict: | |
| with open(path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| def plot_results_panel(summary_path: Path, eval_path: Path, out_path: Path) -> None: | |
| summary = _load_json(summary_path) | |
| ev = _load_json(eval_path) | |
| train_hist = summary.get("train_loss_history") or [] | |
| val_hist = summary.get("val_metrics_history") or [] | |
| val_losses = [v["val_loss"] for v in val_hist if isinstance(v, dict) and "val_loss" in v] | |
| fig, axes = plt.subplots(2, 2, figsize=(10.5, 8.0)) | |
| fig.suptitle("EasyTranslate — archived run (result/*.json)", fontsize=12, fontweight="bold") | |
| # (a) Training loss | |
| ax = axes[0, 0] | |
| if train_hist: | |
| ep = list(range(1, len(train_hist) + 1)) | |
| ax.plot(ep, train_hist, "o-", color="#1f77b4", lw=2, ms=6) | |
| ax.set_xlabel("Epoch") | |
| ax.set_ylabel("Train CE loss") | |
| ax.set_title("(a) Training loss") | |
| ax.set_xticks(ep) | |
| else: | |
| ax.text(0.5, 0.5, "No train_loss_history", ha="center", va="center", transform=ax.transAxes) | |
| ax.set_axis_off() | |
| # (b) Validation loss | |
| ax = axes[0, 1] | |
| if val_losses: | |
| ep = list(range(1, len(val_losses) + 1)) | |
| ax.plot(ep, val_losses, "s-", color="#d62728", lw=2, ms=6) | |
| ax.set_xlabel("Epoch") | |
| ax.set_ylabel("Validation loss") | |
| ax.set_title("(b) Validation loss") | |
| ax.set_xticks(ep) | |
| else: | |
| ax.text(0.5, 0.5, "No val loss", ha="center", va="center", transform=ax.transAxes) | |
| ax.set_axis_off() | |
| # (c) BLEU n-gram breakdown | |
| ax = axes[1, 0] | |
| keys = [("bleu_1", "BLEU-1"), ("bleu_2", "BLEU-2"), ("bleu_3", "BLEU-3"), ("bleu_4", "BLEU-4")] | |
| labels = [k[1] for k in keys] | |
| vals = [float(ev.get(k[0], 0.0)) for k in keys] | |
| colors = ["#2ca02c", "#98df8a", "#aec7e8", "#6baed6"] | |
| bars = ax.bar(labels, vals, color=colors, edgecolor="#333", linewidth=0.6) | |
| ax.set_ylabel("Score") | |
| ax.set_title("(c) N-gram BLEU breakdown") | |
| ax.set_ylim(0, max(vals) * 1.15 + 1e-6) | |
| for b, v in zip(bars, vals): | |
| ax.text(b.get_x() + b.get_width() / 2, v + 0.8, f"{v:.1f}", ha="center", va="bottom", fontsize=9) | |
| # (d) Corpus BLEU + chrF (TER annotated — different scale) | |
| ax = axes[1, 1] | |
| bleu_c = float(ev.get("bleu", 0.0)) | |
| chrf = float(ev.get("chrf", 0.0)) | |
| ter = float(ev.get("ter", 0.0)) | |
| x = ["Corpus BLEU", "chrF++"] | |
| y = [bleu_c, chrf] | |
| ax.bar(x, y, color=["#9467bd", "#ff7f0e"], edgecolor="#333", linewidth=0.6) | |
| ax.set_ylabel("Score") | |
| ax.set_title("(d) Corpus BLEU & chrF++ (TER in caption)") | |
| ax.set_ylim(0, max(y) * 1.2 + 1e-6) | |
| for i, v in enumerate(y): | |
| ax.text(i, v + 0.4, f"{v:.2f}", ha="center", va="bottom", fontsize=9) | |
| ax.text( | |
| 0.5, | |
| -0.22, | |
| f"TER = {ter:.2f} (lower is better; same run as evaluation_results.json / main metrics table)", | |
| transform=ax.transAxes, | |
| ha="center", | |
| fontsize=9, | |
| style="italic", | |
| ) | |
| fig.tight_layout(rect=[0, 0.02, 1, 0.96]) | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| fig.savefig(out_path, bbox_inches="tight") | |
| plt.close(fig) | |
| def plot_experiment_pipeline(out_path: Path) -> None: | |
| """Schematic only — English labels inside figure to avoid font issues in Matplotlib.""" | |
| fig, ax = plt.subplots(figsize=(12.5, 3.2)) | |
| ax.set_xlim(0, 12) | |
| ax.set_ylim(0, 3) | |
| ax.axis("off") | |
| def box(cx: float, cy: float, w: float, h: float, text: str) -> FancyBboxPatch: | |
| x, y = cx - w / 2, cy - h / 2 | |
| p = FancyBboxPatch( | |
| (x, y), | |
| w, | |
| h, | |
| boxstyle="round,pad=0.05,rounding_size=0.12", | |
| linewidth=1.2, | |
| edgecolor="#2c3e50", | |
| facecolor="#ecf0f1", | |
| ) | |
| ax.add_patch(p) | |
| ax.text(cx, cy, text, ha="center", va="center", fontsize=9, fontweight="medium", color="#2c3e50") | |
| return p | |
| def arrow(x1: float, y1: float, x2: float, y2: float) -> None: | |
| arr = FancyArrowPatch( | |
| (x1, y1), | |
| (x2, y2), | |
| arrowstyle="-|>", | |
| mutation_scale=12, | |
| linewidth=1.4, | |
| color="#34495e", | |
| ) | |
| ax.add_patch(arr) | |
| y = 1.55 | |
| specs = [ | |
| (1.0, "Corpus\n(WMT19 zh--en)"), | |
| (2.85, "Preprocess\n& tokenize"), | |
| (4.75, "Model\n(scratch / NLLB)"), | |
| (6.65, "Train\n(AdamW, sched.)"), | |
| (8.45, "Best\nckpt"), | |
| (10.15, "Decode\n(beam / greedy)"), | |
| (11.55, "Metrics\n(SacreBLEU, …)"), | |
| ] | |
| w, h = 1.05, 0.95 | |
| for cx, txt in specs: | |
| box(cx, y, w, h, txt) | |
| xs = [s[0] for s in specs] | |
| for a, b in zip(xs[:-1], xs[1:]): | |
| arrow(a + w / 2 + 0.02, y, b - w / 2 - 0.02, y) | |
| ax.text( | |
| 6.0, | |
| 2.55, | |
| "EasyTranslate evaluation pipeline (schematic)", | |
| ha="center", | |
| fontsize=11, | |
| fontweight="bold", | |
| color="#2c3e50", | |
| ) | |
| fig.tight_layout() | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| fig.savefig(out_path, bbox_inches="tight") | |
| plt.close(fig) | |
| def plot_metric_sparkline(eval_path: Path, out_path: Path) -> None: | |
| """Single-row horizontal bar: main metrics for slide-style summary.""" | |
| ev = _load_json(eval_path) | |
| labels = ["BLEU", "chrF++", "BLEU-4"] | |
| vals = [float(ev.get("bleu", 0)), float(ev.get("chrf", 0)), float(ev.get("bleu_4", 0))] | |
| fig, ax = plt.subplots(figsize=(8.0, 3.2)) | |
| y_pos = range(len(labels)) | |
| ax.barh(list(y_pos), vals, color=["#1f77b4", "#ff7f0e", "#2ca02c"], height=0.55, edgecolor="#333") | |
| ax.set_yticks(list(y_pos)) | |
| ax.set_yticklabels(labels) | |
| ax.invert_yaxis() | |
| ax.set_xlabel("Score") | |
| ax.set_title("Main automatic metrics (archived evaluation_results.json)") | |
| for i, v in enumerate(vals): | |
| ax.text(v + 0.5, i, f"{v:.2f}", va="center", fontsize=10) | |
| ax.set_xlim(0, max(vals) * 1.35 + 5) | |
| fig.tight_layout() | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| fig.savefig(out_path, bbox_inches="tight") | |
| plt.close(fig) | |
| def main() -> int: | |
| summary_path = RESULT_DIR / "training_summary.json" | |
| eval_path = RESULT_DIR / "evaluation_results.json" | |
| if not summary_path.exists(): | |
| print(f"Missing {summary_path}", file=sys.stderr) | |
| return 1 | |
| if not eval_path.exists(): | |
| print(f"Missing {eval_path}", file=sys.stderr) | |
| return 1 | |
| FIG_DIR.mkdir(parents=True, exist_ok=True) | |
| plot_results_panel(summary_path, eval_path, FIG_DIR / "figure_results_panel.png") | |
| plot_experiment_pipeline(FIG_DIR / "figure_experiment_pipeline.png") | |
| plot_metric_sparkline(eval_path, FIG_DIR / "figure_main_metrics_horizontal.png") | |
| print(f"Wrote figures to {FIG_DIR}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |