Spaces:
Sleeping
Sleeping
Download src/ptc/training/loop.py from menturu/propaganda-detector: direct link, hf CLI and curl.
- Browser
- Download file 5.9 kB
-
https://huggingface.co/spaces/menturu/propaganda-detector/resolve/main/src/ptc/training/loop.py
- Command line
-
hf download hf://spaces/menturu/propaganda-detector/src/ptc/training/loop.py
-
curl -L -o loop.py https://huggingface.co/spaces/menturu/propaganda-detector/resolve/main/src/ptc/training/loop.py
5.9 kB
| """Generic fine-tuning loop: AdamW with split encoder/head LRs, linear warmup, | |
| bf16/fp16 autocast on CUDA, grad accumulation, early stopping on a dev metric.""" | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import math | |
| import time | |
| from pathlib import Path | |
| from typing import Callable | |
| import torch | |
| from torch import nn | |
| from torch.utils.data import DataLoader | |
| from transformers import get_linear_schedule_with_warmup | |
| from ..utils import autocast | |
| log = logging.getLogger(__name__) | |
| NO_DECAY = ("bias", "LayerNorm.weight", "layernorm.weight", "layer_norm.weight", "norm.weight") | |
| def param_groups(model: nn.Module, lr: float, head_lr: float, weight_decay: float) -> list[dict]: | |
| groups = {(e, d): [] for e in (True, False) for d in (True, False)} | |
| for name, p in model.named_parameters(): | |
| if not p.requires_grad: | |
| continue | |
| is_encoder = name.startswith("encoder.") | |
| decay = not any(k in name for k in NO_DECAY) | |
| groups[(is_encoder, decay)].append(p) | |
| out = [] | |
| for (is_encoder, decay), params in groups.items(): | |
| if params: | |
| out.append({"params": params, "lr": lr if is_encoder else head_lr, | |
| "weight_decay": weight_decay if decay else 0.0}) | |
| return out | |
| def _nonfinite_report(model: nn.Module) -> str: | |
| bad = [n for n, p in model.named_parameters() if not torch.isfinite(p).all()] | |
| return f"{len(bad)} weight tensors non-finite, e.g. {bad[:3]}" if bad else "all weights finite" | |
| def fit(model: nn.Module, dataset, collate: Callable, hp, device: torch.device, | |
| amp_dtype: torch.dtype | None, evaluate: Callable[[], dict], metric: str, | |
| save: Callable[[dict], None], log_path: Path, seed: int, log_every: int = 50) -> dict: | |
| """Train; after each epoch call `evaluate()`, keep the best by `metric` via `save()`.""" | |
| generator = torch.Generator().manual_seed(seed) | |
| loader = DataLoader(dataset, batch_size=hp.batch_size, shuffle=True, collate_fn=collate, | |
| generator=generator, num_workers=0) # 0 workers: safest on Windows | |
| optimizer = torch.optim.AdamW(param_groups(model, hp.lr, hp.head_lr, hp.weight_decay)) | |
| steps_per_epoch = math.ceil(len(loader) / hp.grad_accum) | |
| total = steps_per_epoch * hp.epochs | |
| scheduler = get_linear_schedule_with_warmup(optimizer, int(hp.warmup_ratio * total), total) | |
| scaler = torch.amp.GradScaler("cuda", enabled=amp_dtype == torch.float16) | |
| best, best_metrics, bad_epochs, skipped = -math.inf, {}, 0, 0 | |
| log_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(log_path, "a", encoding="utf-8", newline="\n") as logf: | |
| for epoch in range(1, hp.epochs + 1): | |
| model.train() | |
| t0, running, n = time.time(), 0.0, 0 | |
| optimizer.zero_grad(set_to_none=True) | |
| for step, batch in enumerate(loader, 1): | |
| batch = {k: v.to(device, non_blocking=True) for k, v in batch.items()} | |
| with autocast(device, amp_dtype): | |
| loss = model(**batch)["loss"] | |
| if not torch.isfinite(loss): | |
| raise FloatingPointError( | |
| f"Non-finite loss at epoch {epoch} step {step} " | |
| f"({_nonfinite_report(model)}). If weights are fine, the forward pass " | |
| f"overflowed: try --set runtime.precision=fp32.") | |
| scaler.scale(loss / hp.grad_accum).backward() | |
| running += loss.item() | |
| n += 1 | |
| if step % hp.grad_accum == 0 or step == len(loader): | |
| scaler.unscale_(optimizer) | |
| norm = nn.utils.clip_grad_norm_(model.parameters(), hp.max_grad_norm) | |
| if not torch.isfinite(norm) and amp_dtype != torch.float16: | |
| # fp16's GradScaler skips these itself; for bf16/fp32 we must, or the | |
| # NaN gets written into the weights and surfaces a step later. | |
| skipped += 1 | |
| bad = [name for name, p in model.named_parameters() | |
| if p.grad is not None and not torch.isfinite(p.grad).all()] | |
| log.warning("non-finite gradients at epoch %d step %d, update skipped " | |
| "(%d params, e.g. %s)", epoch, step, len(bad), bad[:3]) | |
| optimizer.zero_grad(set_to_none=True) | |
| if skipped > hp.max_skipped_steps: | |
| raise FloatingPointError( | |
| f"{skipped} updates skipped for non-finite gradients; first bad " | |
| f"params: {bad[:5]}. Try --set runtime.precision=fp32.") | |
| continue | |
| scaler.step(optimizer) | |
| scaler.update() | |
| scheduler.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| if step % log_every == 0: | |
| log.info("epoch %d step %d/%d loss %.4f", epoch, step, len(loader), running / n) | |
| metrics = evaluate() | |
| metrics.update(epoch=epoch, train_loss=running / max(n, 1), epoch_seconds=time.time() - t0) | |
| logf.write(json.dumps(metrics) + "\n") | |
| logf.flush() | |
| improved = metrics[metric] > best | |
| log.info("epoch %d %s=%.4f%s (%.0fs)", epoch, metric, metrics[metric], | |
| " *best*" if improved else "", metrics["epoch_seconds"]) | |
| if improved: | |
| best, best_metrics, bad_epochs = metrics[metric], dict(metrics), 0 | |
| save(best_metrics) | |
| else: | |
| bad_epochs += 1 | |
| if bad_epochs >= hp.patience: | |
| log.info("early stop: no %s improvement for %d epochs", metric, hp.patience) | |
| break | |
| return best_metrics |