Phase 4: dependency-free ASCII loss-curve plotter

- scripts/plot_loss.py: reads reports/loss-<config>.csv, renders train/val
  loss as a terminal ASCII chart, saves it to reports/loss-<config>.txt.
  Verified on a synthetic curve. Keeps deps at torch + numpy.
- README: documented plotting choice and the Phase 4 run commands.
This commit is contained in:
2026-07-12 11:32:34 -04:00
parent 234a37f8fd
commit 4947556c02
2 changed files with 103 additions and 3 deletions
+13 -3
View File
@@ -109,9 +109,19 @@ raw text ──► tokenizer training ──► tokenized corpus ──► pretr
- Runs on MPS; expected wall-clock for v1 is a few hours for ~100200M 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.
+90
View File
@@ -0,0 +1,90 @@
"""Render the training loss curve as an ASCII chart — no plotting dependency.
Reads reports/loss-<config>.csv (written by src/train.py), prints a terminal
chart of train and val loss, and saves the same rendering to
reports/loss-<config>.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()