From 4947556c02936feddffce1024c2ee725303269bd Mon Sep 17 00:00:00 2001 From: rzen Date: Sun, 12 Jul 2026 11:32:34 -0400 Subject: [PATCH] Phase 4: dependency-free ASCII loss-curve plotter - scripts/plot_loss.py: reads reports/loss-.csv, renders train/val loss as a terminal ASCII chart, saves it to reports/loss-.txt. Verified on a synthetic curve. Keeps deps at torch + numpy. - README: documented plotting choice and the Phase 4 run commands. --- README.md | 16 ++++++-- scripts/plot_loss.py | 90 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 scripts/plot_loss.py diff --git a/README.md b/README.md index f7a10f6..1414b5e 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,19 @@ raw text ──► tokenizer training ──► tokenized corpus ──► pretr - Runs on MPS; expected wall-clock for v1 is a few hours for ~100–200M tokens (roughly Chinchilla-optimal for this size — we don't need to see the whole corpus). -- Loss curve logged to a plain CSV and plotted locally. -- Deliverables: `src/train.py`, `configs/v1.yaml` (or `.py`), checkpoints in - `checkpoints/` (gitignored), loss curves in `reports/`. +- Loss curve logged to a plain CSV (`reports/loss-v1.csv`) and rendered as an + ASCII chart by `scripts/plot_loss.py` — no plotting dependency; the chart is + also saved to `reports/loss-v1.txt` as a committable artifact. +- Deliverables: `src/train.py`, `configs/v1.py`, `scripts/plot_loss.py`, + checkpoints in `checkpoints/` (gitignored), loss log + chart in `reports/`. + +Commands: + +```sh +python src/train.py --config v1 --overfit # sanity check (loss -> ~0) +python src/train.py --config v1 # real run (resumable: --resume) +python scripts/plot_loss.py --config v1 # ASCII loss curve +``` ### Phase 5 — Sampling & evaluation - Autoregressive sampler with temperature and top-k. diff --git a/scripts/plot_loss.py b/scripts/plot_loss.py new file mode 100644 index 0000000..fd1c4a7 --- /dev/null +++ b/scripts/plot_loss.py @@ -0,0 +1,90 @@ +"""Render the training loss curve as an ASCII chart — no plotting dependency. + +Reads reports/loss-.csv (written by src/train.py), prints a terminal +chart of train and val loss, and saves the same rendering to +reports/loss-.txt as a committable artifact. + +Usage: + python scripts/plot_loss.py [--config v1] +""" + +import argparse +import csv +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + +TRAIN_CH = "." +VAL_CH = "*" + + +def read_csv(path: Path): + iters, train, val = [], [], [] + with open(path, newline="") as f: + for row in csv.DictReader(f): + iters.append(int(row["iter"])) + train.append(float(row["train_loss"])) + val.append(float(row["val_loss"])) + return iters, train, val + + +def render(iters, series, width=64, height=18) -> str: + """series: list of (name, values, char). Returns a multi-line chart string.""" + all_vals = [v for _, vals, _ in series for v in vals] + lo, hi = min(all_vals), max(all_vals) + if hi == lo: + hi = lo + 1.0 + imin, imax = min(iters), max(iters) + if imax == imin: + imax = imin + 1 + + def col(it: int) -> int: + return round((it - imin) / (imax - imin) * (width - 1)) + + def row(v: float) -> int: + return round((hi - v) / (hi - lo) * (height - 1)) # row 0 = top = hi + + grid = [[" "] * width for _ in range(height)] + for _, vals, ch in series: + for it, v in zip(iters, vals): + grid[row(v)][col(it)] = ch + + lines = [] + for r in range(height): + yval = hi - r / (height - 1) * (hi - lo) + lines.append(f"{yval:6.3f} |" + "".join(grid[r])) + lines.append(" " * 7 + "+" + "-" * width) + axis = f"{imin:<{width // 2}}{imax:>{width - width // 2}}" + lines.append(" " * 8 + axis) + legend = f" legend: '{TRAIN_CH}' train '{VAL_CH}' val (x: iter, y: loss)" + lines.append(legend) + return "\n".join(lines) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--config", default="v1") + args = ap.parse_args() + + csv_path = ROOT / "reports" / f"loss-{args.config}.csv" + if not csv_path.exists(): + sys.exit(f"no loss log at {csv_path} — run training first") + + iters, train, val = read_csv(csv_path) + if len(iters) < 2: + sys.exit(f"only {len(iters)} data point(s) logged so far — need at least 2") + + chart = render(iters, [("train", train, TRAIN_CH), ("val", val, VAL_CH)]) + header = (f"loss curve — {args.config} " + f"(final train {train[-1]:.4f}, val {val[-1]:.4f} @ iter {iters[-1]})") + out = header + "\n" + chart + + print(out) + txt_path = ROOT / "reports" / f"loss-{args.config}.txt" + txt_path.write_text(out + "\n") + print(f"\nsaved {txt_path}") + + +if __name__ == "__main__": + main()