SPB Git

Repository Public Research preview

hf_finetune.py 5.7 KB 117 lines python

1"""B2 baseline: fine-tune a pretrained encoder–decoder (T5 family, Apache-2.0) on the same pairs with the same2splits. Weights are downloaded from the Hugging Face Hub under their own licence on first use (network; opt-in).3Metadata is given as a text prefix. Outputs go through the same placeholder protocol as HTR models."""4from __future__ import annotations56import math7import time8from pathlib import Path910import torch1112from htr.training.data import meta_prefix13from htr.utils.io import append_jsonl, read_jsonl, write_json14from htr.utils.provenance import run_header151617def _device(name: str) -> torch.device:18    if name != "auto":19        return torch.device(name)20    return torch.device("cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu"))212223def load_hf(model_name: str, device: torch.device):24    from transformers import AutoModelForSeq2SeqLM, AutoTokenizer2526    tok = AutoTokenizer.from_pretrained(model_name)27    # make placeholders atomic tokens28    new = [f"⟦{k}{i}⟧" for k in "CMU" for i in range(1, 33)]29    tok.add_tokens(new)30    model = AutoModelForSeq2SeqLM.from_pretrained(model_name)31    model.resize_token_embeddings(len(tok))32    return tok, model.to(device)333435def finetune(pairs_path: Path, splits_path: Path, run_dir: Path, model_name: str = "google-t5/t5-small", max_steps: int = 3000, lr: float = 3e-4, batch_size: int = 16, max_len: int = 512, eval_every: int = 250, device: str = "auto", seed: int = 1, limit: int | None = None, variants: set[str] | None = None) -> dict:36    torch.manual_seed(seed)37    dev = _device(device)38    tok, model = load_hf(model_name, dev)39    splits = {s["doc_id"]: s["split"] for s in read_jsonl(splits_path)}40    pairs = [p for p in read_jsonl(pairs_path) if variants is None or p["variant"] in variants]41    tr = [p for p in pairs if splits.get(p["doc_id"]) == "train"][: limit or None]42    va = [p for p in pairs if splits.get(p["doc_id"]) == "val"][: (limit // 5 if limit else 1500)]  # cap: evaluation of T5 on MPS is slow43    run_dir.mkdir(parents=True, exist_ok=True)44    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)4546    def batches(rows, shuffle):47        idx = torch.randperm(len(rows)).tolist() if shuffle else list(range(len(rows)))48        for i in range(0, len(idx), batch_size):49            chunk = [rows[j] for j in idx[i : i + batch_size]]50            enc = tok(["reconstruct: " + meta_prefix(p) + p["source_text"] for p in chunk], return_tensors="pt", padding=True, pad_to_multiple_of=32, truncation=True, max_length=max_len)51            lab = tok([p["target_text"] for p in chunk], return_tensors="pt", padding=True, pad_to_multiple_of=32, truncation=True, max_length=max_len).input_ids52            lab[lab == tok.pad_token_id] = -10053            yield {k: v.to(dev) for k, v in enc.items()}, lab.to(dev)5455    @torch.no_grad()56    def evaluate():57        model.eval()58        tot, n = 0.0, 059        for enc, lab in batches(va, False):60            out = model(**enc, labels=lab)61            k = int((lab != -100).sum())62            tot += float(out.loss) * k63            n += k64        model.train()65        return tot / max(1, n)6667    step, best, t0, accel = 0, math.inf, time.time(), 0.068    model.train()69    while step < max_steps:70        for enc, lab in batches(tr, True):71            t1 = time.perf_counter()72            for g in opt.param_groups:73                g["lr"] = lr * min(1.0, (step + 1) / 200)74            loss = model(**enc, labels=lab).loss75            loss.backward()76            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)77            opt.step()78            opt.zero_grad(set_to_none=True)79            if dev.type == "mps":80                torch.mps.synchronize()81                if step % 20 == 0:82                    torch.mps.empty_cache()  # HF models + dynamic shapes leak MPS allocations otherwise83            accel += time.perf_counter() - t184            step += 185            if step % 25 == 0:86                append_jsonl(run_dir / "train_log.jsonl", {"step": step, "ce": float(loss.detach()), "elapsed_s": round(time.time() - t0, 1)})87            if step % eval_every == 0 or step >= max_steps:88                ce = evaluate()89                append_jsonl(run_dir / "train_log.jsonl", {"step": step, "eval": {"val_ce": ce}})90                if ce < best:91                    best = ce92                    model.save_pretrained(run_dir / "best")93                    tok.save_pretrained(run_dir / "best")94            if step >= max_steps:95                break96    n_params = sum(p.numel() for p in model.parameters())97    summary = {**run_header({"run": run_dir.name}), "model_name": model_name, "steps": step, "best_val_ce": best, "n_params": n_params, "n_train_pairs": len(tr), "n_val_pairs": len(va), "accel_seconds": round(accel, 1), "wall_seconds": round(time.time() - t0, 1), "device": str(dev)}98    write_json(run_dir / "train_summary.json", summary)99    return summary100101102@torch.no_grad()103def predict_hf(run_dir: Path, pairs: list[dict], device: str = "auto", max_len: int = 512, beam: int = 1, batch_size: int = 16) -> dict[str, str]:104    from transformers import AutoModelForSeq2SeqLM, AutoTokenizer105106    dev = _device(device)107    tok = AutoTokenizer.from_pretrained(run_dir / "best")108    model = AutoModelForSeq2SeqLM.from_pretrained(run_dir / "best").to(dev).eval()109    out: dict[str, str] = {}110    for i in range(0, len(pairs), batch_size):111        chunk = pairs[i : i + batch_size]112        enc = tok(["reconstruct: " + meta_prefix(p) + p["source_text"] for p in chunk], return_tensors="pt", padding=True, truncation=True, max_length=max_len).to(dev)113        gen = model.generate(**enc, max_new_tokens=max_len, num_beams=beam, do_sample=False)114        for p, g in zip(chunk, gen):115            out[p["pair_id"]] = tok.decode(g, skip_special_tokens=True)116    return out117
Files main
Menu

Theme