EgeEken commited on
Commit
b60c4c8
·
1 Parent(s): 94a9bf8

Improve RL training observability and checkpointing

Browse files
Files changed (4) hide show
  1. pbc3_rl_a20.py +12 -1
  2. static/index.html +7 -1
  3. static/train.js +63 -6
  4. train_api.py +3 -1
pbc3_rl_a20.py CHANGED
@@ -286,11 +286,13 @@ def train(args):
286
  initial = evaluate(model, reference, source, val_episodes, bases, args)
287
  print("initial", json.dumps(initial), flush=True)
288
  best = initial["aggregate_score"]
 
289
  export(model, source, args.out)
290
  for epoch in range(start_epoch + 1, start_epoch + args.epochs + 1):
291
  random.shuffle(train_episodes)
292
  rewards = []
293
  losses = []
 
294
  for start in range(0, len(train_episodes), args.batch):
295
  optimizer.zero_grad()
296
  episode_losses = []
@@ -300,6 +302,7 @@ def train(args):
300
  )
301
  r = reward(bases[(path, preset)], (mse, bpp, work), args)
302
  rewards.append(r)
 
303
  if not logps:
304
  continue
305
  logps = torch.stack(logps)
@@ -321,6 +324,9 @@ def train(args):
321
  row = {
322
  "epoch": epoch,
323
  "train_reward": float(np.mean(rewards)),
 
 
 
324
  "loss": float(np.mean(losses)),
325
  "validation": validation,
326
  }
@@ -328,7 +334,11 @@ def train(args):
328
  print(json.dumps(row), flush=True)
329
  if validation["aggregate_score"] > best:
330
  best = validation["aggregate_score"]
 
331
  export(model, source, args.out)
 
 
 
332
  torch.save(model.state_dict(), f"{args.out}.epoch{epoch}.pt")
333
  torch.save({
334
  "epoch": epoch,
@@ -339,7 +349,7 @@ def train(args):
339
  "torch_rng": torch.random.get_rng_state(),
340
  "history": history,
341
  }, f"{args.out}.resume.pt")
342
- Path(f"{args.out}.json").write_text(json.dumps({"initial": initial, "best_score": best, "history": history}, indent=2))
343
 
344
 
345
  def main():
@@ -350,6 +360,7 @@ def main():
350
  parser.add_argument("--init", default="pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz")
351
  parser.add_argument("--out", default="pbc3_students/patch_policy_f26_a20_rl.npz")
352
  parser.add_argument("--resume", default="")
 
353
  parser.add_argument("--epochs", type=int, default=5)
354
  parser.add_argument("--batch", type=int, default=4)
355
  parser.add_argument("--limit", type=int, default=0)
 
286
  initial = evaluate(model, reference, source, val_episodes, bases, args)
287
  print("initial", json.dumps(initial), flush=True)
288
  best = initial["aggregate_score"]
289
+ best_epoch = 0
290
  export(model, source, args.out)
291
  for epoch in range(start_epoch + 1, start_epoch + args.epochs + 1):
292
  random.shuffle(train_episodes)
293
  rewards = []
294
  losses = []
295
+ train_rows = []
296
  for start in range(0, len(train_episodes), args.batch):
297
  optimizer.zero_grad()
298
  episode_losses = []
 
302
  )
303
  r = reward(bases[(path, preset)], (mse, bpp, work), args)
304
  rewards.append(r)
305
+ train_rows.append((mse, bpp, work))
306
  if not logps:
307
  continue
308
  logps = torch.stack(logps)
 
324
  row = {
325
  "epoch": epoch,
326
  "train_reward": float(np.mean(rewards)),
327
+ "train_mse": float(np.mean([r[0] for r in train_rows])),
328
+ "train_bpp": float(np.mean([r[1] for r in train_rows])),
329
+ "train_work": float(np.mean([r[2] for r in train_rows])),
330
  "loss": float(np.mean(losses)),
331
  "validation": validation,
332
  }
 
334
  print(json.dumps(row), flush=True)
335
  if validation["aggregate_score"] > best:
336
  best = validation["aggregate_score"]
337
+ best_epoch = epoch
338
  export(model, source, args.out)
339
+ if args.export_every > 0 and (epoch % args.export_every == 0 or epoch == start_epoch + args.epochs):
340
+ periodic = Path(args.out).with_name(f"{Path(args.out).stem}.epoch{epoch}.npz")
341
+ export(model, source, periodic)
342
  torch.save(model.state_dict(), f"{args.out}.epoch{epoch}.pt")
343
  torch.save({
344
  "epoch": epoch,
 
349
  "torch_rng": torch.random.get_rng_state(),
350
  "history": history,
351
  }, f"{args.out}.resume.pt")
352
+ Path(f"{args.out}.json").write_text(json.dumps({"initial": initial, "best_score": best, "best_epoch": best_epoch, "history": history}, indent=2))
353
 
354
 
355
  def main():
 
360
  parser.add_argument("--init", default="pbc3_students/patch_policy_f26_a20_h512_l2_e1200.npz")
361
  parser.add_argument("--out", default="pbc3_students/patch_policy_f26_a20_rl.npz")
362
  parser.add_argument("--resume", default="")
363
+ parser.add_argument("--export-every", type=int, default=500)
364
  parser.add_argument("--epochs", type=int, default=5)
365
  parser.add_argument("--batch", type=int, default=4)
366
  parser.add_argument("--limit", type=int, default=0)
static/index.html CHANGED
@@ -54,6 +54,12 @@
54
  .tr-presets { display:flex; flex-wrap:wrap; gap:10px 18px; padding:10px 0; color:var(--text); }
55
  .tr-presets input { accent-color:var(--red); }
56
  .tr-log-tail { max-height:240px; overflow:auto; white-space:pre-wrap; background:rgba(255,255,255,.04); padding:12px; border-radius:10px; color:var(--muted); font:11px 'JetBrains Mono',monospace; }
 
 
 
 
 
 
57
  .tr-table-wrap { overflow-x:auto; }
58
  .tr-table { width:100%; border-collapse:collapse; font:12px 'JetBrains Mono',monospace; }
59
  .tr-table th, .tr-table td { padding:9px 10px; border-bottom:1px solid rgba(255,255,255,.08); text-align:right; white-space:nowrap; }
@@ -69,7 +75,7 @@
69
  #hold-tr-c { --hc:#ff8a3d; } #hold-tr-b { --hc:#38bdf8; } #hold-tr-q { --hc:#a855f7; } #hold-tr-hq { --hc:#34d399; } #hold-tr-model { --hc:#ff3b41; }
70
  #hold-tr-c, #hold-tr-b, #hold-tr-q, #hold-tr-hq, #hold-tr-model { border-color:var(--hc); color:var(--hc); }
71
  #hold-tr-c.holding, #hold-tr-b.holding, #hold-tr-q.holding, #hold-tr-hq.holding, #hold-tr-model.holding { background:var(--hc); color:#fff; border-color:var(--hc); }
72
- @media (max-width:760px){ .tr-fields{grid-template-columns:1fr;} }
73
  </style>
74
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👁️</text></svg>">
75
  </head>
 
54
  .tr-presets { display:flex; flex-wrap:wrap; gap:10px 18px; padding:10px 0; color:var(--text); }
55
  .tr-presets input { accent-color:var(--red); }
56
  .tr-log-tail { max-height:240px; overflow:auto; white-space:pre-wrap; background:rgba(255,255,255,.04); padding:12px; border-radius:10px; color:var(--muted); font:11px 'JetBrains Mono',monospace; }
57
+ .tr-chart-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px; }
58
+ .tr-chart-grid > div { min-width:0; min-height:290px; }
59
+ .tr-chart-grid .tr-chart-wide { grid-column:1/-1; }
60
+ .tr-sort { display:flex; align-items:center; gap:7px; color:var(--muted); font-size:12px; }
61
+ .tr-sort select { background:var(--surface); border:1px solid var(--border); border-radius:6px; color:var(--text); padding:5px; }
62
+ .tr-baseline-line { padding:9px 0 12px; color:var(--muted); font:12px 'JetBrains Mono',monospace; }
63
  .tr-table-wrap { overflow-x:auto; }
64
  .tr-table { width:100%; border-collapse:collapse; font:12px 'JetBrains Mono',monospace; }
65
  .tr-table th, .tr-table td { padding:9px 10px; border-bottom:1px solid rgba(255,255,255,.08); text-align:right; white-space:nowrap; }
 
75
  #hold-tr-c { --hc:#ff8a3d; } #hold-tr-b { --hc:#38bdf8; } #hold-tr-q { --hc:#a855f7; } #hold-tr-hq { --hc:#34d399; } #hold-tr-model { --hc:#ff3b41; }
76
  #hold-tr-c, #hold-tr-b, #hold-tr-q, #hold-tr-hq, #hold-tr-model { border-color:var(--hc); color:var(--hc); }
77
  #hold-tr-c.holding, #hold-tr-b.holding, #hold-tr-q.holding, #hold-tr-hq.holding, #hold-tr-model.holding { background:var(--hc); color:#fff; border-color:var(--hc); }
78
+ @media (max-width:760px){ .tr-fields{grid-template-columns:1fr;} .tr-chart-grid{grid-template-columns:1fr;} .tr-chart-grid .tr-chart-wide{grid-column:auto;} .tr-sort{margin-top:8px;} }
79
  </style>
80
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>👁️</text></svg>">
81
  </head>
static/train.js CHANGED
@@ -67,17 +67,26 @@
67
  <label class="tr-f"><span>Temperature</span><input id="tr-temp" type="number" step="0.05" value="0.9"></label>
68
  <label class="tr-f"><span>Entropy weight</span><input id="tr-entropy" type="number" step="0.0005" value="0.003"></label>
69
  <label class="tr-f"><span>KL weight</span><input id="tr-kl" type="number" step="0.001" value="0.015"></label>
 
70
  </div>
71
  <div class="tr-actions"><button id="tr-start" class="primary-btn">Start / resume</button><button id="tr-stop" class="hold-btn">Stop</button><button id="tr-refresh" class="hold-btn">Refresh</button><button id="tr-log" class="hold-btn">Download log</button><button id="tr-copy-results" class="hold-btn">Copy results</button><button id="tr-download-results" class="hold-btn">Download results</button><label class="tr-check tr-live"><input id="tr-pause" type="checkbox"> Pause live table/log updates</label></div>
72
  </div>
73
  <div class="params"><div class="params-head"><span class="field-label">Run status</span></div><div id="tr-status" class="tr-ck">Loading…</div><pre id="tr-log-tail" class="tr-log-tail"></pre></div>
74
- <div class="params"><div class="params-head"><span class="field-label">Validation history</span></div><div id="tr-history"></div></div>`;
 
75
  $("#tr-start").onclick = start;
76
  $("#tr-stop").onclick = async () => { await api("/api/train/stop", { method: "POST" }); refresh(); };
77
  $("#tr-refresh").onclick = refresh;
78
  $("#tr-log").onclick = () => window.open("/api/train/log", "_blank");
79
  $("#tr-copy-results").onclick = copyResults;
80
  $("#tr-download-results").onclick = downloadResults;
 
 
 
 
 
 
 
81
  fillInitialModels();
82
  }
83
 
@@ -109,6 +118,7 @@
109
  init: $("#tr-init").value, output: $("#tr-output").value,
110
  rate_weight: Number($("#tr-rate").value), speed_weight: Number($("#tr-speed").value),
111
  temperature: Number($("#tr-temp").value), entropy_weight: Number($("#tr-entropy").value), kl_weight: Number($("#tr-kl").value),
 
112
  };
113
  try { await api("/api/train/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); refresh(); } catch (error) { $("#tr-status").textContent = error.message; }
114
  }
@@ -116,23 +126,32 @@
116
  function renderHistory(history) {
117
  const root = $("#tr-history");
118
  if (!root) return;
119
- const rows = history.filter((row) => row.epoch || row.validation).map((row) => {
 
 
 
 
 
 
 
 
120
  const v = row.validation || row;
121
- return `<tr><td>${row.epoch ? `Epoch ${row.epoch}` : "Initial"}</td><td>${fmt(row.reward ?? v.reward, 5)}</td><td>${fmt(v.aggregate_score, 5)}</td><td>${fmt(v.mse, 3)}</td><td>${fmt(v.bpp, 6)}</td><td>${fmt(v.work, 0)}</td></tr>`;
122
  });
123
- root.innerHTML = rows.length ? `<div class="tr-table-wrap"><table class="tr-table"><thead><tr><th>Checkpoint</th><th>Per-image reward</th><th>Aggregate score</th><th>Val MSE</th><th>Val bpp</th><th>Grid work</th></tr></thead><tbody>${rows.join("")}</tbody></table></div>` : `<p class="card-date">No validation rows yet. The first baseline pass can take a few minutes.</p>`;
 
124
  }
125
 
126
  function resultText() {
127
  const history = latestStatus.history || [];
128
  return history.map((row) => {
129
  const v = row.validation || row;
130
- return [row.epoch ? `Epoch ${row.epoch}` : "Initial", fmt(row.reward ?? v.reward, 5), fmt(v.aggregate_score, 5), fmt(v.mse, 3), fmt(v.bpp, 6), fmt(v.work, 0)].join("\t");
131
  }).join("\n");
132
  }
133
 
134
  async function copyResults() {
135
- const text = ["Checkpoint\tPer-image reward\tAggregate score\tVal MSE\tVal bpp\tGrid work", resultText()].filter(Boolean).join("\n");
136
  try { await navigator.clipboard.writeText(text); $("#tr-pause").checked = true; } catch { window.prompt("Copy training results", text); }
137
  }
138
 
@@ -152,11 +171,49 @@
152
  if (!$("#tr-pause")?.checked) {
153
  $("#tr-log-tail").textContent = data.log_tail || "";
154
  renderHistory(data.history || []);
 
155
  }
156
  if (data.running && !poll) poll = setInterval(refresh, 3000);
157
  if (!data.running && poll) { clearInterval(poll); poll = null; loadCheckpoints(); }
158
  }
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  function renderCheckpoints() {
161
  checkpointRoot.innerHTML = `<div class="params"><div class="params-head"><span class="field-label">Downloadable model checkpoints</span></div><p class="card-date">Download an exported <code>.npz</code> after reviewing its validation history, then upload it into the main project when you want to test or promote it.</p><input id="tr-ckpt-filter" class="tr-filter" placeholder="Filter checkpoints…"><div id="tr-checkpoint-list" class="tr-checkpoint-grid"></div></div>`;
162
  $("#tr-ckpt-filter").oninput = renderCheckpointList;
 
67
  <label class="tr-f"><span>Temperature</span><input id="tr-temp" type="number" step="0.05" value="0.9"></label>
68
  <label class="tr-f"><span>Entropy weight</span><input id="tr-entropy" type="number" step="0.0005" value="0.003"></label>
69
  <label class="tr-f"><span>KL weight</span><input id="tr-kl" type="number" step="0.001" value="0.015"></label>
70
+ <label class="tr-f"><span>Export checkpoint every N epochs</span><input id="tr-export-every" type="number" min="0" step="100" value="500"></label>
71
  </div>
72
  <div class="tr-actions"><button id="tr-start" class="primary-btn">Start / resume</button><button id="tr-stop" class="hold-btn">Stop</button><button id="tr-refresh" class="hold-btn">Refresh</button><button id="tr-log" class="hold-btn">Download log</button><button id="tr-copy-results" class="hold-btn">Copy results</button><button id="tr-download-results" class="hold-btn">Download results</button><label class="tr-check tr-live"><input id="tr-pause" type="checkbox"> Pause live table/log updates</label></div>
73
  </div>
74
  <div class="params"><div class="params-head"><span class="field-label">Run status</span></div><div id="tr-status" class="tr-ck">Loading…</div><pre id="tr-log-tail" class="tr-log-tail"></pre></div>
75
+ <div class="params"><div class="params-head"><span class="field-label">Learning graphics</span></div><div class="tr-chart-grid"><div id="tr-chart-reward"></div><div id="tr-chart-metrics"></div><div id="tr-chart-work"></div><div id="tr-chart-train"></div><div id="tr-chart-rd" class="tr-chart-wide"></div></div></div>
76
+ <div class="params"><div class="params-head"><span class="field-label">Validation history</span><label class="tr-sort">Sort by <select id="tr-sort"><option value="epoch">Epoch</option><option value="reward">Per-image reward</option><option value="aggregate_score">Aggregate score</option><option value="mse">Validation MSE</option><option value="bpp">Validation bpp</option><option value="work">Validation grid work</option><option value="train_reward">Training reward</option><option value="train_mse">Training MSE</option><option value="train_bpp">Training bpp</option></select><button id="tr-sort-dir" class="hold-btn sm">↓</button></label></div><div id="tr-history"></div></div>`;
77
  $("#tr-start").onclick = start;
78
  $("#tr-stop").onclick = async () => { await api("/api/train/stop", { method: "POST" }); refresh(); };
79
  $("#tr-refresh").onclick = refresh;
80
  $("#tr-log").onclick = () => window.open("/api/train/log", "_blank");
81
  $("#tr-copy-results").onclick = copyResults;
82
  $("#tr-download-results").onclick = downloadResults;
83
+ $("#tr-sort").onchange = () => renderHistory(latestStatus.history || []);
84
+ $("#tr-sort-dir").onclick = () => {
85
+ const button = $("#tr-sort-dir");
86
+ button.dataset.direction = button.dataset.direction === "1" ? "-1" : "1";
87
+ button.textContent = button.dataset.direction === "1" ? "↑" : "↓";
88
+ renderHistory(latestStatus.history || []);
89
+ };
90
  fillInitialModels();
91
  }
92
 
 
118
  init: $("#tr-init").value, output: $("#tr-output").value,
119
  rate_weight: Number($("#tr-rate").value), speed_weight: Number($("#tr-speed").value),
120
  temperature: Number($("#tr-temp").value), entropy_weight: Number($("#tr-entropy").value), kl_weight: Number($("#tr-kl").value),
121
+ export_every: Number($("#tr-export-every").value),
122
  };
123
  try { await api("/api/train/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); refresh(); } catch (error) { $("#tr-status").textContent = error.message; }
124
  }
 
126
  function renderHistory(history) {
127
  const root = $("#tr-history");
128
  if (!root) return;
129
+ const key = $("#tr-sort")?.value || "epoch";
130
+ const direction = Number($("#tr-sort-dir")?.dataset.direction || (key === "epoch" ? "1" : "-1"));
131
+ const sorted = history.filter((row) => row.epoch || row.validation).slice().sort((a, b) => {
132
+ const value = (row) => key === "epoch" ? (row.epoch ?? -1) : (key.startsWith("train_") ? row : (row.validation || row))[key];
133
+ const av = value(a);
134
+ const bv = value(b);
135
+ return (Number(av ?? -Infinity) - Number(bv ?? -Infinity)) * direction;
136
+ });
137
+ const rows = sorted.map((row) => {
138
  const v = row.validation || row;
139
+ return `<tr><td>${row.epoch ? `Epoch ${row.epoch}` : "Initial"}</td><td>${fmt(row.train_reward, 5)}</td><td>${fmt(row.reward ?? v.reward, 5)}</td><td>${fmt(v.aggregate_score, 5)}</td><td>${fmt(row.train_mse, 3)}</td><td>${fmt(v.mse, 3)}</td><td>${fmt(row.train_bpp, 6)}</td><td>${fmt(v.bpp, 6)}</td><td>${fmt(row.train_work, 0)}</td><td>${fmt(v.work, 0)}</td></tr>`;
140
  });
141
+ const baseline = (history.find((r) => r.validation)?.validation || {});
142
+ root.innerHTML = rows.length ? `<div class="tr-baseline-line">Production baseline: MSE ${fmt(baseline.baseline_mse, 3)} · bpp ${fmt(baseline.baseline_bpp, 6)} · grid-work baseline is the starting policy, not PBC3.0.</div><div class="tr-table-wrap"><table class="tr-table"><thead><tr><th>Checkpoint</th><th>Train reward</th><th>Val reward</th><th>Aggregate score</th><th>Train MSE</th><th>Val MSE</th><th>Train bpp</th><th>Val bpp</th><th>Train work</th><th>Val work</th></tr></thead><tbody>${rows.join("")}</tbody></table></div>` : `<p class="card-date">No validation rows yet. The first baseline pass can take a few minutes.</p>`;
143
  }
144
 
145
  function resultText() {
146
  const history = latestStatus.history || [];
147
  return history.map((row) => {
148
  const v = row.validation || row;
149
+ return [row.epoch ? `Epoch ${row.epoch}` : "Initial", fmt(row.train_reward, 5), fmt(row.reward ?? v.reward, 5), fmt(v.aggregate_score, 5), fmt(row.train_mse, 3), fmt(v.mse, 3), fmt(row.train_bpp, 6), fmt(v.bpp, 6), fmt(row.train_work, 0), fmt(v.work, 0)].join("\t");
150
  }).join("\n");
151
  }
152
 
153
  async function copyResults() {
154
+ const text = ["Checkpoint\tTrain reward\tVal reward\tAggregate score\tTrain MSE\tVal MSE\tTrain bpp\tVal bpp\tTrain work\tVal work", resultText()].filter(Boolean).join("\n");
155
  try { await navigator.clipboard.writeText(text); $("#tr-pause").checked = true; } catch { window.prompt("Copy training results", text); }
156
  }
157
 
 
171
  if (!$("#tr-pause")?.checked) {
172
  $("#tr-log-tail").textContent = data.log_tail || "";
173
  renderHistory(data.history || []);
174
+ drawCharts(data.history || []);
175
  }
176
  if (data.running && !poll) poll = setInterval(refresh, 3000);
177
  if (!data.running && poll) { clearInterval(poll); poll = null; loadCheckpoints(); }
178
  }
179
 
180
+ function chartLayout(title, yTitle, extra = {}) {
181
+ return Object.assign({ title, height: 290, margin: { l: 52, r: 48, t: 38, b: 42 }, paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)", font: { color: "#cfd3dc", size: 11 }, xaxis: { title: "Epoch", gridcolor: "rgba(255,255,255,.07)" }, yaxis: { title: yTitle, gridcolor: "rgba(255,255,255,.07)" }, legend: { orientation: "h" } }, extra);
182
+ }
183
+
184
+ function drawCharts(history) {
185
+ if (!window.Plotly) return;
186
+ const points = history.filter((row) => row.epoch || row.validation);
187
+ if (!points.length) return;
188
+ const x = points.map((row) => row.epoch ?? 0);
189
+ const values = points.map((row) => row.validation || row);
190
+ const color = points.map((_, i) => i);
191
+ Plotly.react($("#tr-chart-reward"), [
192
+ { x, y: points.map((row) => row.train_reward), name: "train reward", mode: "lines", line: { color: "#60a5fa" } },
193
+ { x, y: points.map((row) => row.reward ?? (row.validation || {}).reward), name: "validation reward", mode: "lines", line: { color: "#f59e0b" } },
194
+ { x, y: values.map((row) => row.aggregate_score), name: "aggregate score", mode: "lines", line: { color: "#22c55e" } },
195
+ ], chartLayout("Reward and aggregate score", "score"), { responsive: true, displayModeBar: false });
196
+ Plotly.react($("#tr-chart-metrics"), [
197
+ { x, y: points.map((row) => row.train_mse), name: "train MSE", mode: "lines", line: { color: "#93c5fd" } },
198
+ { x, y: values.map((row) => row.mse), name: "val MSE", mode: "lines", line: { color: "#ef4444" } },
199
+ { x, y: values.map((row) => row.baseline_mse), name: "PBC3.0 MSE", mode: "lines", line: { color: "#fca5a5", dash: "dot" } },
200
+ ], chartLayout("MSE", "MSE"), { responsive: true, displayModeBar: false });
201
+ Plotly.react($("#tr-chart-work"), [
202
+ { x, y: points.map((row) => row.train_work), name: "train grid work", mode: "lines", line: { color: "#a78bfa" } },
203
+ { x, y: values.map((row) => row.work), name: "val grid work", mode: "lines", line: { color: "#c084fc" } },
204
+ ], chartLayout("Grid work", "coefficients"), { responsive: true, displayModeBar: false });
205
+ Plotly.react($("#tr-chart-train"), [
206
+ { x, y: points.map((row) => row.train_bpp), name: "train bpp", mode: "lines", line: { color: "#93c5fd" } },
207
+ { x, y: values.map((row) => row.bpp), name: "val bpp", mode: "lines", line: { color: "#38bdf8" } },
208
+ { x, y: values.map((row) => row.baseline_bpp), name: "PBC3.0 bpp", mode: "lines", line: { color: "#7dd3fc", dash: "dot" } },
209
+ ], chartLayout("Bitrate", "bits / pixel"), { responsive: true, displayModeBar: false });
210
+ const baseline = values[0];
211
+ Plotly.react($("#tr-chart-rd"), [
212
+ { x: values.map((row) => row.bpp), y: values.map((row) => row.mse), text: x.map((epoch) => `Epoch ${epoch}`), mode: "markers", name: "validation RD", marker: { size: 9, color, colorscale: "YlOrRd", showscale: true, colorbar: { title: "newer" } } },
213
+ { x: [baseline.baseline_bpp], y: [baseline.baseline_mse], text: ["PBC3.0 baseline"], mode: "markers+text", name: "PBC3.0 baseline", textposition: "top center", marker: { size: 13, color: "#fff", symbol: "diamond", line: { color: "#ef4444", width: 2 } } },
214
+ ], chartLayout("Rate–distortion history", "MSE", { xaxis: { title: "bpp", gridcolor: "rgba(255,255,255,.07)" } }), { responsive: true, displayModeBar: false });
215
+ }
216
+
217
  function renderCheckpoints() {
218
  checkpointRoot.innerHTML = `<div class="params"><div class="params-head"><span class="field-label">Downloadable model checkpoints</span></div><p class="card-date">Download an exported <code>.npz</code> after reviewing its validation history, then upload it into the main project when you want to test or promote it.</p><input id="tr-ckpt-filter" class="tr-filter" placeholder="Filter checkpoints…"><div id="tr-checkpoint-list" class="tr-checkpoint-grid"></div></div>`;
219
  $("#tr-ckpt-filter").oninput = renderCheckpointList;
train_api.py CHANGED
@@ -68,7 +68,8 @@ def status():
68
  if history_path and history_path.exists():
69
  try:
70
  saved = json.loads(history_path.read_text(encoding="utf-8"))
71
- history = saved.get("history", [])
 
72
  except Exception:
73
  pass
74
  if not history:
@@ -107,6 +108,7 @@ def start(spec):
107
  sys.executable, "pbc3_rl_a20.py",
108
  "--presets", ",".join(selected),
109
  "--epochs", str(int(spec.get("epochs", 100))),
 
110
  "--batch", str(int(spec.get("batch", 4))),
111
  "--init", os.fspath(init),
112
  "--out", os.fspath(output),
 
68
  if history_path and history_path.exists():
69
  try:
70
  saved = json.loads(history_path.read_text(encoding="utf-8"))
71
+ initial = saved.get("initial")
72
+ history = ([{"validation": initial}] if initial else []) + saved.get("history", [])
73
  except Exception:
74
  pass
75
  if not history:
 
108
  sys.executable, "pbc3_rl_a20.py",
109
  "--presets", ",".join(selected),
110
  "--epochs", str(int(spec.get("epochs", 100))),
111
+ "--export-every", str(int(spec.get("export_every", 500))),
112
  "--batch", str(int(spec.get("batch", 4))),
113
  "--init", os.fspath(init),
114
  "--out", os.fspath(output),