Download code/minijev/training/supervised.py from markuz89/minijev-snake: direct link, hf CLI and curl.
- Browser
- Download file 23.3 kB
-
https://huggingface.co/markuz89/minijev-snake/resolve/main/code/minijev/training/supervised.py
- Command line
-
hf download hf://markuz89/minijev-snake/code/minijev/training/supervised.py
-
curl -L -o supervised.py https://huggingface.co/markuz89/minijev-snake/resolve/main/code/minijev/training/supervised.py
23.3 kB
| """Supervised / distillation training loop. | |
| Resumable (last.pt holds model, optimizer, scheduler, scaler, data position and | |
| RNG), stops cleanly on SIGINT/SIGTERM or a time budget, and ends with | |
| temperature calibration on the validation split and a test-set report. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import os | |
| import random | |
| import signal | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from minijev.calibration.metrics import (IsotonicConfidence, calibration_report, fit_temperature, | |
| reliability_svg, softmax) | |
| from minijev.datasets.batching import BackboneTable, BatchEncoder, EpochBatches, model_inputs, to_torch | |
| from minijev.datasets.decision_data import DecisionData | |
| from minijev.device import RuntimeProfile, autocast, resolve_runtime | |
| from minijev.model import build_model, human_params | |
| from minijev.runs import RunLogger, environment_info, new_run_dir | |
| from minijev.tokenizers import make_tokenizer | |
| from minijev.tasks import get_task | |
| from .losses import decision_loss | |
| def seed_everything(seed: int) -> None: | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| def task_specs(cfg: dict) -> list[dict]: | |
| """Single task (`task:` + `data:`) or multi-task (`tasks: [{key, task, data, weight}]`).""" | |
| if cfg.get("tasks"): | |
| sampling = cfg.get("sampling") or {} | |
| out = [] | |
| for t in cfg["tasks"]: | |
| t = dict(t) | |
| t.setdefault("key", t["task"]["name"]) | |
| t["weight"] = float(sampling.get(t["key"], t.get("weight", 1.0))) | |
| out.append(t) | |
| return out | |
| return [{"key": cfg["task"]["name"], "task": cfg["task"], "data": cfg["data"], "weight": 1.0}] | |
| def make_task_for(cfg: dict, task_cfg: dict): | |
| """Task plugin with the model's text tokenizer attached (byte or pretrained).""" | |
| task = get_task(task_cfg["name"], task_cfg) | |
| task.tokenizer = make_tokenizer(cfg["model"].get("text_encoder")) | |
| return task | |
| def load_splits(cfg: dict) -> dict[str, DecisionData]: | |
| dcfg = cfg["data"] | |
| d = Path(dcfg["dir"]) | |
| if not (d / "train.npz").exists(): | |
| gen = dcfg.get("generate") | |
| if not gen: | |
| raise FileNotFoundError(f"{d}/train.npz missing and no data.generate section") | |
| import importlib | |
| mod = importlib.import_module(dcfg.get("generator") or f"environments.{cfg['task']['name']}.dataset") | |
| gen = dict(gen) | |
| n = gen.pop("samples") | |
| print(f"[data] generating {n:,} samples into {d} ...") | |
| mod.generate_splits(d, n, **gen) | |
| splits = {k: DecisionData.load(d / f"{k}.npz") for k in ("train", "val", "test")} | |
| lim = dcfg.get("train_limit") | |
| if lim and lim < len(splits["train"]): | |
| splits["train"] = splits["train"].subset(np.arange(lim)) | |
| return splits | |
| def make_model(cfg: dict, task=None): | |
| """task: one task, a list of tasks (multi-task: one state encoder per modality), or None (from cfg).""" | |
| tasks = task if isinstance(task, (list, tuple)) else [task] if task is not None else \ | |
| [make_task_for(cfg, t["task"]) for t in task_specs(cfg)] | |
| encs, classes = {}, {} | |
| for t in tasks: | |
| encs.setdefault(t.state_encoder, t.encoder_config()) | |
| if t.encoder_class(): | |
| classes[t.state_encoder] = t.encoder_class() | |
| return build_model(cfg["model"], encs, classes or None) | |
| def param_groups(model, wd: float, lr: float | None = None, backbone_lr: float | None = None): | |
| """AdamW groups: no decay on norms/biases/embeddings; a pretrained backbone can use its own (lower) lr.""" | |
| groups = {} | |
| for n, p in model.named_parameters(): | |
| if not p.requires_grad: | |
| continue | |
| nd = p.ndim < 2 or "embed" in n or n.endswith((".pos", ".row", ".col", "cls", "type_emb")) | |
| bb = backbone_lr is not None and n.startswith("backbone.") | |
| groups.setdefault((nd, bb), []).append(p) | |
| out = [] | |
| for (nd, bb), ps in groups.items(): | |
| g = {"params": ps, "weight_decay": 0.0 if nd else wd} | |
| if bb: | |
| g["lr"] = backbone_lr | |
| elif lr is not None: | |
| g["lr"] = lr | |
| out.append(g) | |
| return out | |
| def lr_lambda(warmup: int, total: int, min_ratio: float): | |
| def f(step): | |
| if step < warmup: | |
| return (step + 1) / max(1, warmup) | |
| prog = min(1.0, (step - warmup) / max(1, total - warmup)) | |
| return min_ratio + (1 - min_ratio) * 0.5 * (1 + math.cos(math.pi * prog)) | |
| return f | |
| def predict_logits(model, encoder: BatchEncoder, device, batch_size: int = 1024, max_rows: int | None = None, | |
| profile: RuntimeProfile | None = None) -> tuple[np.ndarray, np.ndarray]: | |
| model.eval() | |
| n = len(encoder.data) if max_rows is None else min(max_rows, len(encoder.data)) | |
| outs, tg = [], [] | |
| for i in range(0, n, batch_size): | |
| b = to_torch(encoder(np.arange(i, min(n, i + batch_size))), device) | |
| with autocast(profile) if profile else torch.autocast("cpu", enabled=False): | |
| out = model(**model_inputs(b)) | |
| outs.append(out["logits"].float().cpu().numpy()) | |
| tg.append(b["target"].cpu().numpy()) | |
| return np.concatenate(outs), np.concatenate(tg) | |
| def _acceptable(encoder: BatchEncoder, n: int): | |
| """Rows' acceptable choices (soft label > 0), if the dataset has soft labels.""" | |
| soft = encoder.data.soft | |
| return None if soft is None else soft[:n] > 0 | |
| class _Stop: | |
| def __init__(self): | |
| self.flag = False | |
| self._prev = {} | |
| def __enter__(self): | |
| for s in (signal.SIGINT, signal.SIGTERM): | |
| try: | |
| self._prev[s] = signal.signal(s, self._handle) | |
| except ValueError: # not in main thread | |
| pass | |
| return self | |
| def _handle(self, *_): | |
| if self.flag: | |
| raise KeyboardInterrupt | |
| print("\n[train] stop requested - saving checkpoint (Ctrl-C again to abort)") | |
| self.flag = True | |
| def __exit__(self, *a): | |
| for s, h in self._prev.items(): | |
| signal.signal(s, h) | |
| class MultiTaskBatches: | |
| """Batches for multi-task training: each batch comes from ONE task (its own state | |
| encoder / choice set), picked with the configured sampling weights. Deterministic | |
| in (seed, epoch, i), so resume skips exactly the consumed batches.""" | |
| def __init__(self, encoders: dict, weights: dict, batch_size: int, n_batches: int, seed: int, epoch: int, | |
| start_batch: int = 0): | |
| self.keys = list(encoders) | |
| w = np.array([weights[k] for k in self.keys], float) | |
| self.p = w / w.sum() | |
| self.encoders, self.bs, self.seed, self.epoch = encoders, batch_size, seed, epoch | |
| self.range = range(start_batch, n_batches) | |
| def __len__(self): | |
| return len(self.range) | |
| def __getitem__(self, i): | |
| rng = np.random.default_rng((self.seed, self.epoch, self.range[i], 11)) | |
| enc = self.encoders[self.keys[rng.choice(len(self.keys), p=self.p)]] | |
| n = len(enc.data) | |
| return enc(rng.choice(n, self.bs, replace=n < self.bs), rng) | |
| def make_encoders(cfg: dict, model, task, split: DecisionData, choice_dropout: float = 0.0, seed: int = 0): | |
| """BatchEncoder for one split; with a frozen pretrained backbone, questions and | |
| choice labels are encoded once per vocabulary entry (BackboneTable).""" | |
| mcfg = cfg["model"] | |
| kw = dict(max_question_len=mcfg.get("max_question_len", 32), max_choice_len=mcfg.get("max_choice_len", 16)) | |
| tables = {} | |
| bb = getattr(model, "backbone", None) | |
| if bb is not None and bb.frozen and cfg["training"].get("cache_text", True): | |
| tok = task.tokenizer | |
| tables = {"question_table": BackboneTable(split.questions, model, tok, kw["max_question_len"]), | |
| "choice_table": BackboneTable([task.model_label(c) for c in split.choices], model, tok, | |
| kw["max_choice_len"])} | |
| return BatchEncoder(split, task, choice_dropout=choice_dropout, seed=seed, tokenizer=task.tokenizer, | |
| **kw, **tables) | |
| def train(cfg: dict, run_dir: str | Path | None = None, resume: bool = False, max_steps: int | None = None, | |
| max_minutes: float | None = None, quiet: bool = False, final_eval: bool = True) -> dict: | |
| tcfg = cfg["training"] | |
| seed = int(cfg.get("seed", 0)) | |
| seed_everything(seed) | |
| specs = task_specs(cfg) | |
| tasks = {s["key"]: make_task_for(cfg, s["task"]) for s in specs} | |
| splits = {s["key"]: load_splits({**cfg, "task": s["task"], "data": s["data"]}) for s in specs} | |
| weights = {s["key"]: s["weight"] for s in specs} | |
| multi = len(specs) > 1 | |
| model = make_model(cfg, list(tasks.values())) | |
| if tcfg.get("init_checkpoint") and not resume: | |
| ck = torch.load(tcfg["init_checkpoint"], map_location="cpu", weights_only=False) | |
| model.load_state_dict(ck["model"]) | |
| model.log_temperature.zero_() # re-calibrated at the end of this run | |
| print(f"[train] initialised from {tcfg['init_checkpoint']}") | |
| n_params = model.num_parameters() | |
| prof = resolve_runtime(tcfg, n_params) | |
| torch.set_num_threads(prof.threads) | |
| device = torch.device(prof.device) | |
| model.to(device) | |
| run_dir = Path(run_dir) if run_dir else new_run_dir(cfg.get("name", "run")) | |
| ckpt_dir = run_dir / "checkpoints" | |
| log = RunLogger(run_dir, cfg, resume=resume) | |
| encs = {} | |
| for k, sp in splits.items(): | |
| encs[k] = {"train": make_encoders(cfg, model, tasks[k], sp["train"], tcfg.get("choice_dropout", 0.0), seed), | |
| "val": make_encoders(cfg, model, tasks[k], sp["val"]), | |
| "test": make_encoders(cfg, model, tasks[k], sp["test"])} | |
| train_enc = {k: e["train"] for k, e in encs.items()} | |
| eff_bs = int(tcfg.get("batch_size", 256)) | |
| epochs = float(tcfg.get("epochs", 1)) | |
| n_train = sum(len(sp["train"]) for sp in splits.values()) | |
| batches_per_epoch = max(1, n_train // prof.micro_batch) | |
| steps_per_epoch = max(1, batches_per_epoch // prof.grad_accum) | |
| total_steps = max(1, int(steps_per_epoch * epochs)) | |
| if max_steps: | |
| total_steps = min(total_steps, max_steps) | |
| lr = tcfg.get("lr", 1e-3) | |
| opt = torch.optim.AdamW(param_groups(model, tcfg.get("weight_decay", 0.05), lr, tcfg.get("backbone_lr")), lr=lr, | |
| betas=(0.9, 0.98), fused=prof.device == "cuda") | |
| warmup = int(tcfg.get("warmup_steps", min(500, total_steps // 20))) | |
| sched = torch.optim.lr_scheduler.LambdaLR(opt, lr_lambda(warmup, total_steps, tcfg.get("min_lr_ratio", 0.05))) | |
| scaler = torch.amp.GradScaler(enabled=prof.amp_dtype == "fp16") | |
| fwd = torch.compile(model) if prof.compile else model | |
| step, epoch, batch_pos, best, elapsed_prev = 0, 0, 0, -1.0, 0.0 | |
| samples_seen = 0 | |
| session_samples = 0 | |
| last = ckpt_dir / "last.pt" | |
| if resume and last.exists(): | |
| ck = torch.load(last, map_location=device, weights_only=False) | |
| model.load_state_dict(ck["model"]) | |
| opt.load_state_dict(ck["optimizer"]) | |
| sched.load_state_dict(ck["scheduler"]) | |
| scaler.load_state_dict(ck["scaler"]) | |
| step, epoch, batch_pos, best = ck["step"], ck["epoch"], ck["batch_pos"], ck["best"] | |
| elapsed_prev, samples_seen = ck.get("elapsed", 0.0), ck.get("samples_seen", 0) | |
| torch.set_rng_state(ck["torch_rng"]) | |
| print(f"[train] resumed from {last} at step {step} (epoch {epoch}, batch {batch_pos})") | |
| breakdown = model.param_breakdown() | |
| info = {"task": "+".join(tasks) if multi else next(iter(tasks.values())).name, "tasks": list(tasks), | |
| "params": n_params, "params_human": human_params(n_params), "params_breakdown": breakdown, | |
| "train_rows": n_train, "val_rows": sum(len(sp["val"]) for sp in splits.values()), | |
| "test_rows": sum(len(sp["test"]) for sp in splits.values()), | |
| "effective_batch": eff_bs, "total_steps": total_steps, "runtime": prof.to_dict(), | |
| "env": environment_info()} | |
| log.write_json("env.json", info) | |
| if not quiet: | |
| print(f"[train] {cfg.get('name')} | {human_params(n_params)} params (core+heads " | |
| f"{human_params(breakdown['core_total'])}, text {human_params(breakdown['text'])}, trainable " | |
| f"{human_params(breakdown['trainable'])}) | {prof.device} ({prof.device_name}) " | |
| f"amp={prof.amp_dtype} micro={prof.micro_batch}x{prof.grad_accum} | " | |
| f"{n_train:,} train rows ({', '.join(tasks)}) | {total_steps:,} steps") | |
| def save(path, extra=None): | |
| tmp = Path(str(path) + ".tmp") | |
| torch.save({"model": model.state_dict(), "optimizer": opt.state_dict(), "scheduler": sched.state_dict(), | |
| "scaler": scaler.state_dict(), "step": step, "epoch": epoch, "batch_pos": batch_pos, | |
| "best": best, "elapsed": elapsed_prev + (time.time() - t_start), "samples_seen": samples_seen, | |
| "torch_rng": torch.get_rng_state(), "config": cfg, **(extra or {})}, tmp) | |
| os.replace(tmp, path) | |
| def validate(): | |
| reps = {} | |
| for k, e in encs.items(): | |
| lg, tg = predict_logits(model, e["val"], device, max_rows=eval_rows, profile=prof) | |
| reps[k] = calibration_report(lg, tg, acceptable=_acceptable(e["val"], len(tg))) | |
| w = np.array([weights[k] for k in reps]) | |
| acc = float(np.dot(w / w.sum(), [r["accuracy"] for r in reps.values()])) | |
| return acc, reps | |
| loss_cfg = cfg.get("loss", {}) | |
| eval_every = int(tcfg.get("eval_every", 1000)) | |
| ckpt_every = int(tcfg.get("checkpoint_every", 1000)) | |
| log_every = int(tcfg.get("log_every", 50)) | |
| eval_rows = tcfg.get("eval_max_rows", 20000) | |
| grad_clip = tcfg.get("grad_clip", 1.0) | |
| t_start = time.time() | |
| t_train = 0.0 | |
| run_loss, run_n, window_samples, window_t = 0.0, 0, 0, time.time() | |
| stopped_early = False | |
| with _Stop() as stop: | |
| while step < total_steps and not stop.flag: | |
| if multi: | |
| ds = MultiTaskBatches(train_enc, weights, prof.micro_batch, batches_per_epoch, seed, epoch, batch_pos) | |
| else: | |
| ds = EpochBatches(next(iter(train_enc.values())), prof.micro_batch, shuffle=True, seed=seed, | |
| epoch=epoch, start_batch=batch_pos, drop_last=True) | |
| loader = torch.utils.data.DataLoader(ds, batch_size=None, shuffle=False, num_workers=prof.num_workers, | |
| pin_memory=prof.device == "cuda", | |
| prefetch_factor=4 if prof.num_workers else None, | |
| # fork shares the in-memory dataset with workers instead of pickling it | |
| multiprocessing_context="fork" if prof.num_workers and sys.platform == "linux" else None) | |
| model.train() | |
| micro = 0 | |
| for batch in loader: | |
| t0 = time.time() | |
| b = to_torch(batch, device, non_blocking=True) | |
| with autocast(prof): | |
| out = fwd(**model_inputs(b)) | |
| loss, terms = decision_loss(out["logits"], out["choice_mask"], b, loss_cfg, out.get("score")) | |
| scaler.scale(loss / prof.grad_accum).backward() | |
| micro += 1 | |
| batch_pos += 1 | |
| n_b = len(batch["target"]) | |
| samples_seen += n_b | |
| session_samples += n_b | |
| window_samples += n_b | |
| run_loss += float(loss.detach()) | |
| run_n += 1 | |
| if micro % prof.grad_accum: | |
| t_train += time.time() - t0 | |
| continue | |
| if grad_clip: | |
| scaler.unscale_(opt) | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) | |
| scaler.step(opt) | |
| scaler.update() | |
| opt.zero_grad(set_to_none=True) | |
| sched.step() | |
| step += 1 | |
| t_train += time.time() - t0 | |
| if step % log_every == 0: | |
| now = time.time() | |
| sps = window_samples / max(1e-9, now - window_t) | |
| log.log(step, train_loss=run_loss / run_n, lr=sched.get_last_lr()[0], | |
| samples_per_sec=round(sps, 1), epoch=epoch, **{f"loss_{k}": v for k, v in terms.items()}) | |
| if not quiet: | |
| print(f" step {step:6d}/{total_steps} loss {run_loss / run_n:.4f} " | |
| f"lr {sched.get_last_lr()[0]:.2e} {sps:,.0f} samples/s") | |
| run_loss, run_n, window_samples, window_t = 0.0, 0, 0, now | |
| if step % eval_every == 0 or step == total_steps: | |
| acc, reps = validate() | |
| first = next(iter(reps.values())) | |
| extra = {"val_acceptable_acc": first["acceptable_accuracy"]} \ | |
| if not multi and "acceptable_accuracy" in first else {} | |
| per_task = {f"val_acc_{k}": r["accuracy"] for k, r in reps.items()} if multi else {} | |
| log.log(step, val_acc=acc, val_nll=first["nll"], val_ece=first["ece"], | |
| val_brier=first["brier"], **extra, **per_task) | |
| if not quiet: | |
| detail = " ".join(f"{k} {r['accuracy']:.4f}" for k, r in reps.items()) if multi else \ | |
| f"nll {first['nll']:.4f} ece {first['ece']:.4f}" | |
| print(f" [val] step {step} acc {acc:.4f} {detail}" | |
| + (f" acceptable {first['acceptable_accuracy']:.4f}" if extra else "")) | |
| if acc > best: | |
| best = acc | |
| torch.save({"model": model.state_dict(), "config": cfg, "step": step, | |
| "val_accuracy": best}, ckpt_dir / "best.pt") | |
| model.train() | |
| if step % ckpt_every == 0: | |
| save(last) | |
| if max_minutes and (time.time() - t_start) / 60 > max_minutes: | |
| print(f"[train] time budget of {max_minutes} min reached") | |
| stopped_early = True | |
| stop.flag = True | |
| if step >= total_steps or stop.flag: | |
| break | |
| else: | |
| epoch += 1 | |
| batch_pos = 0 | |
| continue | |
| if batch_pos >= batches_per_epoch: | |
| epoch, batch_pos = epoch + 1, 0 | |
| save(last) | |
| interrupted = stop.flag and not stopped_early and step < total_steps | |
| train_seconds = elapsed_prev + (time.time() - t_start) | |
| summary = {**{k: v for k, v in info.items() if k != "env"}, "steps_done": step, "samples_seen": samples_seen, | |
| "train_seconds": round(train_seconds, 1), | |
| "throughput_samples_per_sec": round(session_samples / max(t_train, 1e-9), 1), | |
| "best_val_accuracy": best, "complete": step >= total_steps, "run_dir": str(run_dir)} | |
| hourly = cfg.get("cost", {}).get("gpu_hourly_cost") or os.environ.get("MINIJEV_GPU_HOURLY_COST") | |
| if hourly: | |
| summary["cost"] = {"gpu_hourly_cost": float(hourly), | |
| "estimated_cost": round(float(hourly) * train_seconds / 3600, 4)} | |
| if final_eval and not interrupted: | |
| summary.update(finalize(cfg, model, run_dir, {k: e["val"] for k, e in encs.items()}, | |
| {k: e["test"] for k, e in encs.items()}, device, prof)) | |
| log.write_json("summary.json", summary) | |
| log.close() | |
| return summary | |
| def _pad_logits(parts: list[np.ndarray]) -> np.ndarray: | |
| n = max(p.shape[1] for p in parts) | |
| return np.concatenate([np.pad(p, ((0, 0), (0, n - p.shape[1])), constant_values=-1e9) for p in parts]) | |
| def finalize(cfg, model, run_dir: Path, val_encs, test_encs, device, prof) -> dict: | |
| """Best checkpoint -> temperature fitted on val (global + per task) -> calibrated test report -> final.pt.""" | |
| from minijev.calibration.metrics import ece as ece_fn | |
| if not isinstance(val_encs, dict): # single-encoder call (old signature) | |
| val_encs, test_encs = {"task": val_encs}, {"task": test_encs} | |
| ckpt_dir = run_dir / "checkpoints" | |
| best = ckpt_dir / "best.pt" | |
| if best.exists(): | |
| model.load_state_dict(torch.load(best, map_location=device, weights_only=False)["model"]) | |
| cal = cfg.get("calibration", {}) | |
| per_task, V, VT, TL, TT = {}, [], [], [], [] | |
| for k in val_encs: | |
| vl, vt = predict_logits(model, val_encs[k], device, profile=prof) | |
| tl, tt = predict_logits(model, test_encs[k], device, profile=prof) | |
| Tk = fit_temperature(vl, vt) if cal.get("method", "temperature") == "temperature" else 1.0 | |
| acc_ok = _acceptable(test_encs[k], len(tt)) | |
| per_task[k] = {"temperature": Tk, "test_raw": calibration_report(tl, tt, 1.0, acceptable=acc_ok), | |
| "test_calibrated": calibration_report(tl, tt, Tk, acceptable=acc_ok)} | |
| V.append(vl), VT.append(vt), TL.append(tl), TT.append(tt) | |
| val_lg, val_tg, test_lg, test_tg = _pad_logits(V), np.concatenate(VT), _pad_logits(TL), np.concatenate(TT) | |
| T = fit_temperature(val_lg, val_tg) if cal.get("method", "temperature") == "temperature" else 1.0 | |
| model.log_temperature.fill_(math.log(T)) | |
| single = len(val_encs) == 1 | |
| acc_ok = _acceptable(next(iter(test_encs.values())), len(test_tg)) if single else None | |
| raw = calibration_report(test_lg, test_tg, 1.0, acceptable=acc_ok) | |
| calibrated = calibration_report(test_lg, test_tg, T, acceptable=acc_ok) | |
| # isotonic comparison (fit on val confidence, evaluate on test) | |
| pv = softmax(val_lg, T) | |
| iso = IsotonicConfidence().fit(pv.max(1), (pv.argmax(1) == val_tg).astype(float)) | |
| pt = softmax(test_lg, T) | |
| iso_ece = ece_fn(iso(pt.max(1)), (pt.argmax(1) == test_tg).astype(float)) | |
| (run_dir / "reliability_raw.svg").write_text(reliability_svg(raw["reliability"], "Test - raw")) | |
| (run_dir / "reliability_calibrated.svg").write_text( | |
| reliability_svg(calibrated["reliability"], f"Test - temperature {T:.3f}")) | |
| task_T = {k: v["temperature"] for k, v in per_task.items()} | |
| torch.save({"model": model.state_dict(), "config": cfg, "temperature": T, "task_temperatures": task_T}, | |
| ckpt_dir / "final.pt") | |
| print(f"[calibration] T={T:.3f} | test acc {calibrated['accuracy']:.4f} | ECE {raw['ece']:.4f} -> " | |
| f"{calibrated['ece']:.4f} | NLL {raw['nll']:.4f} -> {calibrated['nll']:.4f} | " | |
| f"Brier {calibrated['brier']:.4f} | isotonic ECE {iso_ece:.4f}" | |
| + (f" | acceptable acc {calibrated['acceptable_accuracy']:.4f}" if acc_ok is not None else "")) | |
| if not single: | |
| for k, v in per_task.items(): | |
| c = v["test_calibrated"] | |
| print(f" [{k}] T={v['temperature']:.3f} acc {c['accuracy']:.4f} ECE {v['test_raw']['ece']:.4f} -> " | |
| f"{c['ece']:.4f} NLL {c['nll']:.4f}") | |
| return {"temperature": T, "task_temperatures": task_T, "test_raw": raw, "test_calibrated": calibrated, | |
| "test_isotonic_ece": iso_ece, "per_task": per_task, "final_checkpoint": str(ckpt_dir / "final.pt")} | |