Spaces:
Running on Zero
Running on Zero
| # Fine-tunes RT-DETR on the prepared DocLayNet data. Every hyperparameter | |
| # is set explicitly (nothing left to Ultralytics defaults) and the run | |
| # writes a metadata file with GPU, wall-clock time and library versions, | |
| # since the brief caps Part A at 50% if the run isn't reproducible. | |
| # | |
| # Usage: | |
| # python scripts/train.py --data data/doclaynet/doclaynet.yaml | |
| # python scripts/train.py --data ... --epochs 1 --smoke # CPU smoke test | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import subprocess | |
| import time | |
| from pathlib import Path | |
| RANDOM_SEED = 42 | |
| def _build_train_args(data: str, epochs: int, batch: int, imgsz: int, device: str) -> dict: | |
| """All the hyperparameters. The augmentation block is the part worth | |
| reading - three of Ultralytics' defaults are actively wrong for | |
| documents: | |
| - fliplr 0.5 mirrors pages, which don't have a valid mirrored layout | |
| (text direction, page numbers in fixed corners) | |
| - flipud same problem, more obviously (upside-down page) | |
| - mosaic composites 4 images into one, destroying whole-page | |
| structure (header at top, footer at bottom) that the model needs | |
| Kept small rotation + mild perspective - those model real scan/camera | |
| skew and are the only hedge against the hidden eval set containing | |
| photographed rather than rendered pages. | |
| lr0=1e-4 not the usual YOLO 1e-2 - DETR-family transformer components | |
| are unstable at high LR, and I'm fine-tuning from COCO weights, not | |
| training from scratch.""" | |
| return { | |
| "data": data, | |
| "epochs": epochs, | |
| "imgsz": imgsz, | |
| "batch": batch, | |
| "device": device, | |
| "seed": RANDOM_SEED, | |
| "deterministic": True, | |
| "amp": True, # needed to fit RT-DETR-L at batch 8 in 16GB on a T4 | |
| "optimizer": "AdamW", | |
| "lr0": 1e-4, | |
| "lrf": 0.01, | |
| "weight_decay": 1e-4, | |
| "warmup_epochs": 3.0, | |
| "cos_lr": True, | |
| "save_period": 1, # checkpoint every epoch - Kaggle sessions can drop | |
| "patience": 10, | |
| "workers": 2, # Kaggle's few CPU cores - more workers stall | |
| "cache": False, # 8k pages at 1025px won't fit in RAM | |
| "val": True, | |
| "plots": True, | |
| # --- augmentation, see docstring above --- | |
| "fliplr": 0.0, | |
| "flipud": 0.0, | |
| "mosaic": 0.0, | |
| "degrees": 3.0, | |
| "perspective": 0.0005, | |
| "translate": 0.05, | |
| "scale": 0.2, | |
| "shear": 0.0, | |
| "hsv_h": 0.0, # document colour isn't a useful signal | |
| "hsv_s": 0.2, | |
| "hsv_v": 0.2, # scan brightness genuinely varies | |
| "erasing": 0.0, # occluding part of a region changes its class | |
| } | |
| def _git_commit() -> str: | |
| try: | |
| return subprocess.check_output( | |
| ["git", "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL | |
| ).strip() | |
| except Exception: | |
| return "unknown" | |
| def _write_run_metadata(out_dir: Path, train_args: dict, seconds: float) -> None: | |
| """Everything needed to reproduce this run. GPU name matters most - | |
| "took 3 hours" means nothing without knowing what it ran on, and | |
| Kaggle doesn't always give you the accelerator you asked for.""" | |
| import torch | |
| import ultralytics | |
| gpu_name, gpu_memory_gb = "cpu", None | |
| if torch.cuda.is_available(): | |
| gpu_name = torch.cuda.get_device_name(0) | |
| gpu_memory_gb = round( | |
| torch.cuda.get_device_properties(0).total_memory / 1024**3, 1 | |
| ) | |
| metadata = { | |
| "git_commit": _git_commit(), | |
| "hyperparameters": train_args, | |
| "hardware": { | |
| "gpu": gpu_name, | |
| "gpu_memory_gb": gpu_memory_gb, | |
| "torch_version": torch.__version__, | |
| "cuda_version": torch.version.cuda, | |
| }, | |
| "ultralytics_version": ultralytics.__version__, | |
| "wall_clock_seconds": round(seconds, 1), | |
| "wall_clock_human": f"{seconds / 3600:.2f} h", | |
| } | |
| path = out_dir / "run_metadata.json" | |
| path.write_text(json.dumps(metadata, indent=2), encoding="utf-8") | |
| print(f"\nRun metadata written to {path}") | |
| print(f" GPU: {gpu_name} ({gpu_memory_gb} GB)") | |
| print(f" Time: {seconds / 3600:.2f} h") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--data", default="data/doclaynet/doclaynet.yaml") | |
| parser.add_argument("--epochs", type=int, default=30) | |
| parser.add_argument("--batch", type=int, default=8) | |
| parser.add_argument("--imgsz", type=int, default=640) | |
| parser.add_argument("--device", default="0") | |
| parser.add_argument("--name", default="rtdetr_doclaynet") | |
| parser.add_argument( | |
| "--smoke", action="store_true", | |
| help="tiny CPU run to prove the script works before spending GPU quota", | |
| ) | |
| args = parser.parse_args() | |
| if args.smoke: | |
| args.epochs, args.batch, args.device, args.imgsz = 1, 2, "cpu", 320 | |
| args.name = "smoke" | |
| from ultralytics import RTDETR | |
| from ultralytics.utils import SETTINGS | |
| # Ultralytics auto-registers a Ray Tune callback whenever `ray` is | |
| # importable, no check for whether Tune is actually running. Kaggle | |
| # ships ray pre-installed for other stuff, so this callback silently | |
| # activates and crashes end-of-epoch calling a Ray internal API that | |
| # doesn't exist in the installed version. Not using Ray Tune anywhere | |
| # here, so just turn the integration off before .train() runs. | |
| SETTINGS["raytune"] = False | |
| # Starting from COCO weights, not scratch. None of my 11 classes exist | |
| # in COCO so the detection head gets zero class knowledge for free - | |
| # the pretrained backbone just gives generic visual features. Training | |
| # from random init wouldn't converge in the epoch budget I have. | |
| model = RTDETR("rtdetr-l.pt") | |
| train_args = _build_train_args(args.data, args.epochs, args.batch, args.imgsz, args.device) | |
| train_args["name"] = args.name | |
| print(f"Training RT-DETR-L | {args.epochs} epochs | batch {args.batch} | {args.imgsz}px") | |
| started = time.time() | |
| results = model.train(**train_args) | |
| elapsed = time.time() - started | |
| out_dir = Path(results.save_dir) | |
| _write_run_metadata(out_dir, train_args, elapsed) | |
| print(f"\nBest weights: {out_dir / 'weights' / 'best.pt'}") | |
| if __name__ == "__main__": | |
| main() | |