Download modal_app.py from siddhant20/task1: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/siddhant20/task1/resolve/main/modal_app.py
- Command line
-
hf download hf://siddhant20/task1/modal_app.py
-
curl -L -o modal_app.py https://huggingface.co/siddhant20/task1/resolve/main/modal_app.py
14.1 kB
| """Modal app for Packaging Material Difference Mining. | |
| One-time data upload (2.5 GB, from the repository root): | |
| modal volume create pmdm-data | |
| modal volume put pmdm-data \ | |
| Task1/PackagingMaterialDifferenceMiningDataset/train /raw/train | |
| modal volume put pmdm-data \ | |
| Task1/PackagingMaterialDifferenceMiningDataset/test /raw/test | |
| Pipeline: | |
| modal run modal_app.py::prep # stage 0, CPU | |
| modal run modal_app.py::smoke # cheap shape/loss check | |
| modal run modal_app.py::synth --shards 16 --per-shard 400 | |
| modal run modal_app.py::train --fold 0 --epochs 40 | |
| modal run modal_app.py::oof --fold 0 | |
| modal run modal_app.py::verify | |
| modal run modal_app.py::predict --folds 0,1,2,3,4 | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import modal | |
| APP_NAME = "pmdm" | |
| image = ( | |
| modal.Image.debian_slim(python_version="3.11") | |
| .apt_install("libgl1", "libglib2.0-0") | |
| .pip_install( | |
| "torch==2.5.1", | |
| "torchvision==0.20.1", | |
| "timm==1.0.11", | |
| "opencv-python-headless==4.10.0.84", | |
| "numpy<2", | |
| "pandas", | |
| ) | |
| .env( | |
| { | |
| "PMDM_DATA": "/data/raw", | |
| "PMDM_WORK": "/data/work", | |
| "PMDM_CKPT": "/ckpt", | |
| "PYTHONPATH": "/root/src", | |
| "HF_HOME": "/ckpt/hf", | |
| } | |
| ) | |
| .add_local_dir("src", "/root/src") | |
| ) | |
| data_vol = modal.Volume.from_name("pmdm-data", create_if_missing=True) | |
| ckpt_vol = modal.Volume.from_name("pmdm-ckpt", create_if_missing=True) | |
| VOLUMES = {"/data": data_vol, "/ckpt": ckpt_vol} | |
| app = modal.App(APP_NAME, image=image) | |
| HOUR = 3600 | |
| # --------------------------------------------------------------------------- # | |
| # Stage 0: preprocessing | |
| # --------------------------------------------------------------------------- # | |
| def preprocess_one(args: tuple[str, int]) -> dict: | |
| from pmdm.preprocess import preprocess_pair | |
| split, idx = args | |
| info = preprocess_pair(split, idx) | |
| data_vol.commit() | |
| return info | |
| def preprocess_all() -> dict: | |
| from pmdm.config import N_TEST, N_TRAIN, WORK | |
| jobs = [("train", i) for i in range(N_TRAIN)] + [("test", i) for i in range(N_TEST)] | |
| infos = list(preprocess_one.map(jobs, order_outputs=False)) | |
| WORK.mkdir(parents=True, exist_ok=True) | |
| (WORK / "prep_info.json").write_text(json.dumps(infos, indent=2)) | |
| data_vol.commit() | |
| modes: dict[str, int] = {} | |
| for i in infos: | |
| modes[i["mode"]] = modes.get(i["mode"], 0) + 1 | |
| return {"pairs": len(infos), "align_modes": modes} | |
| # --------------------------------------------------------------------------- # | |
| # Synthetic data | |
| # --------------------------------------------------------------------------- # | |
| def synth_shard(args: tuple[int, int]) -> int: | |
| from pathlib import Path | |
| from pmdm.config import SYNTH | |
| from pmdm.synth import generate | |
| seed, n = args | |
| boxes = generate(n=n, seed=seed) | |
| out = Path(SYNTH) / "synth" | |
| out.mkdir(parents=True, exist_ok=True) | |
| (out / f"boxes_shard_{seed:03d}.json").write_text(json.dumps(boxes)) | |
| data_vol.commit() | |
| return len(boxes) | |
| def synth_all(shards: int, per_shard: int) -> dict: | |
| from pmdm.synth import merge_box_index | |
| counts = list(synth_shard.map([(s, per_shard) for s in range(shards)], order_outputs=False)) | |
| total = merge_box_index() | |
| data_vol.commit() | |
| return {"shards": shards, "generated": sum(counts), "indexed": total} | |
| # --------------------------------------------------------------------------- # | |
| # Stage 1: detector | |
| # --------------------------------------------------------------------------- # | |
| def train_stage1(fold: int, epochs: int, backbone: str, n_synth: int, | |
| samples_per_pair: int, batch: int) -> dict: | |
| import torch | |
| from pmdm.train import train_fold | |
| print("gpu:", torch.cuda.get_device_name(0), flush=True) | |
| result = train_fold( | |
| fold=fold, | |
| epochs=epochs, | |
| backbone=backbone, | |
| n_synth=n_synth, | |
| samples_per_pair=samples_per_pair, | |
| batch=batch, | |
| use_synth=n_synth != 0, | |
| on_epoch_end=ckpt_vol.commit, | |
| ) | |
| ckpt_vol.commit() | |
| return result | |
| def smoke_test() -> dict: | |
| """Cheap end-to-end check: one batch forward/backward plus one full-image decode.""" | |
| import numpy as np | |
| import torch | |
| from pmdm.dataset import PairTileDataset, load_gt | |
| from pmdm.infer import predict_pair | |
| from pmdm.losses import total_loss | |
| from pmdm.model import SiamCenterNet | |
| gt_raw = load_gt() | |
| items = [("train", i) for i in range(4)] | |
| gt = {("train", i): gt_raw.get(i, np.zeros((0, 4), np.float32)) for i in range(4)} | |
| ds = PairTileDataset(items, gt, samples_per_pair=2) | |
| batch = {k: torch.stack([ds[i][k] for i in range(2)]) for k in ds[0]} | |
| model = SiamCenterNet(pretrained=True).cuda() | |
| out = model(batch["a"].cuda(), batch["b"].cuda()) | |
| losses = total_loss(out, {k: v.cuda() for k, v in batch.items() if k not in ("a", "b")}) | |
| losses["total"].backward() | |
| model.eval() | |
| boxes, scores = predict_pair(model, "train", 0, device="cuda", score_thr=0.2) | |
| return { | |
| "hm_shape": list(out["hm"].shape), | |
| "losses": {k: float(v) for k, v in losses.items()}, | |
| "untrained_boxes_on_pair_0": int(len(boxes)), | |
| "gt_boxes_on_pair_0": int(len(gt[("train", 0)])), | |
| } | |
| # --------------------------------------------------------------------------- # | |
| # Out-of-fold candidates, verifier, submission | |
| # --------------------------------------------------------------------------- # | |
| def predict_oof(fold: int, backbone: str, score_thr: float) -> dict: | |
| from pathlib import Path | |
| import numpy as np | |
| from pmdm.config import CKPT | |
| from pmdm.dataset import load_gt | |
| from pmdm.folds import split as fold_split | |
| from pmdm.infer import load_model, predict_pair | |
| from pmdm.metric import sweep_threshold | |
| ckpt = Path(CKPT) / f"stage1_fold{fold}_{backbone}" / "best.pt" | |
| model = load_model(ckpt, backbone=backbone) | |
| _, val_idx = fold_split(fold) | |
| gt_raw = load_gt() | |
| store, gt_eval = {}, {} | |
| for idx in val_idx: | |
| boxes, scores = predict_pair(model, "train", idx, device="cuda", score_thr=score_thr) | |
| key = f"train_{idx:03d}" | |
| store[key] = (boxes, scores) | |
| gt_eval[key] = gt_raw.get(idx, np.zeros((0, 4), np.float32)) | |
| out = Path(CKPT) / "oof" | |
| out.mkdir(parents=True, exist_ok=True) | |
| np.savez_compressed( | |
| out / f"fold{fold}_{backbone}.npz", | |
| **{f"{k}__boxes": v[0] for k, v in store.items()}, | |
| **{f"{k}__scores": v[1] for k, v in store.items()}, | |
| ) | |
| thr, best = sweep_threshold(store, gt_eval) | |
| best["threshold"] = thr | |
| (out / f"fold{fold}_{backbone}.json").write_text(json.dumps(best, indent=2)) | |
| ckpt_vol.commit() | |
| return best | |
| def train_verifier_fn(epochs: int) -> dict: | |
| from pathlib import Path | |
| import numpy as np | |
| from pmdm.config import CKPT | |
| from pmdm.dataset import load_gt | |
| from pmdm.verifier import build_records, train_verifier | |
| gt_raw = load_gt() | |
| candidates, gt = {}, {} | |
| for path in sorted((Path(CKPT) / "oof").glob("*.npz")): | |
| z = np.load(path) | |
| keys = {k.split("__")[0] for k in z.files} | |
| for key in keys: | |
| idx = int(key.split("_")[-1]) | |
| candidates[key] = ("train", idx, z[f"{key}__boxes"], z[f"{key}__scores"]) | |
| gt[key] = gt_raw.get(idx, np.zeros((0, 4), np.float32)) | |
| records = build_records(candidates, gt) | |
| result = train_verifier(records, epochs=epochs) | |
| ckpt_vol.commit() | |
| return result | |
| def verifier_ab(fold: int, backbone: str, epochs: int) -> dict: | |
| """Honest verifier measurement on one fold. | |
| The validation pairs are split in half by index parity: the verifier trains on | |
| half A's candidates and is measured on half B, with half B's pre-verifier sweep | |
| as the control. Training and scoring on the same candidates would leak. | |
| """ | |
| from pathlib import Path | |
| import numpy as np | |
| from pmdm.config import CKPT | |
| from pmdm.dataset import load_gt | |
| from pmdm.metric import sweep_threshold | |
| from pmdm.verifier import apply_verifier, build_records, load_verifier, train_verifier | |
| z = np.load(Path(CKPT) / "oof" / f"fold{fold}_{backbone}.npz") | |
| keys = sorted({k.split("__")[0] for k in z.files}) | |
| gt_raw = load_gt() | |
| half_a = [k for k in keys if int(k.split("_")[-1]) % 2 == 0] | |
| half_b = [k for k in keys if int(k.split("_")[-1]) % 2 == 1] | |
| def bundle(subset): | |
| cand, gt = {}, {} | |
| for key in subset: | |
| idx = int(key.split("_")[-1]) | |
| cand[key] = ("train", idx, z[f"{key}__boxes"], z[f"{key}__scores"]) | |
| gt[key] = gt_raw.get(idx, np.zeros((0, 4), np.float32)) | |
| return cand, gt | |
| cand_a, gt_a = bundle(half_a) | |
| cand_b, gt_b = bundle(half_b) | |
| records = build_records(cand_a, gt_a) | |
| stats = train_verifier(records, epochs=epochs, out_dir=Path(CKPT) / "stage2_ab") | |
| model = load_verifier(Path(CKPT) / "stage2_ab" / "last.pt") | |
| before = {k: (v[2], v[3]) for k, v in cand_b.items()} | |
| thr_before, score_before = sweep_threshold(before, gt_b) | |
| after = {} | |
| for key, (split, idx, boxes, scores) in cand_b.items(): | |
| after[key] = apply_verifier(model, split, idx, boxes, scores, device="cuda") | |
| thr_after, score_after = sweep_threshold(after, gt_b) | |
| ckpt_vol.commit() | |
| return { | |
| "train_pairs": len(half_a), "eval_pairs": len(half_b), | |
| "records": stats, | |
| "before": {**score_before, "threshold": thr_before}, | |
| "after": {**score_after, "threshold": thr_after}, | |
| "delta_f1": round(score_after["f1"] - score_before["f1"], 4), | |
| } | |
| def predict_test(folds: list[int], backbone: str, score_thr: float, threshold: float, | |
| use_verifier: bool) -> dict: | |
| from pathlib import Path | |
| import numpy as np | |
| from pmdm.config import CKPT, N_TEST | |
| from pmdm.decode import postprocess, wbf | |
| from pmdm.infer import load_model, predict_pair | |
| from pmdm.metric import write_submission | |
| from pmdm.preprocess import load_prepared | |
| from pmdm.verifier import apply_verifier, load_verifier | |
| models = [load_model(Path(CKPT) / f"stage1_fold{f}_{backbone}" / "best.pt", backbone=backbone) | |
| for f in folds] | |
| verifier = None | |
| v_path = Path(CKPT) / "stage2" / "last.pt" | |
| if use_verifier and v_path.exists(): | |
| verifier = load_verifier(v_path) | |
| candidates = {} | |
| for idx in range(N_TEST): | |
| boxes_all, scores_all = [], [] | |
| for model in models: | |
| b, s = predict_pair(model, "test", idx, device="cuda", score_thr=score_thr, | |
| use_snap=False, use_polarity=False) | |
| if len(b): | |
| boxes_all.append(b) | |
| scores_all.append(s) | |
| if boxes_all: | |
| boxes, scores = wbf(np.concatenate(boxes_all), np.concatenate(scores_all)) | |
| else: | |
| boxes, scores = np.zeros((0, 4), np.float32), np.zeros(0, np.float32) | |
| if verifier is not None and len(boxes): | |
| boxes, scores = apply_verifier(verifier, "test", idx, boxes, scores, device="cuda") | |
| _, _, tn, pn = load_prepared("test", idx) | |
| boxes, scores = postprocess(boxes, scores, tn, pn) | |
| candidates[f"template/test_template_{idx:03d}.png"] = (boxes, scores) | |
| out = Path(CKPT) / "submission.csv" | |
| df = write_submission(candidates, threshold, out) | |
| ckpt_vol.commit() | |
| return {"rows": len(df), "images": len(candidates), "path": str(out), | |
| "boxes_per_image": round(len(df) / max(1, len(candidates)), 2)} | |
| # --------------------------------------------------------------------------- # | |
| # Local entrypoints | |
| # --------------------------------------------------------------------------- # | |
| def prep(): | |
| print(preprocess_all.remote()) | |
| def smoke(): | |
| print(smoke_test.remote()) | |
| def synth(shards: int = 16, per_shard: int = 400): | |
| print(synth_all.remote(shards, per_shard)) | |
| def train(fold: int = 0, epochs: int = 40, backbone: str = "convnext_tiny", | |
| n_synth: int = 0, samples_per_pair: int = 8, batch: int = 8): | |
| print(train_stage1.remote(fold, epochs, backbone, n_synth, samples_per_pair, batch)) | |
| def train_all(epochs: int = 40, backbone: str = "convnext_tiny", n_synth: int = 0, | |
| samples_per_pair: int = 8, batch: int = 8): | |
| args = [(f, epochs, backbone, n_synth, samples_per_pair, batch) for f in range(5)] | |
| for result in train_stage1.starmap(args): | |
| print(result) | |
| def oof(fold: int = 0, backbone: str = "convnext_tiny", score_thr: float = 0.05): | |
| print(predict_oof.remote(fold, backbone, score_thr)) | |
| def verify(epochs: int = 8): | |
| print(train_verifier_fn.remote(epochs)) | |
| def verify_ab(fold: int = 0, backbone: str = "convnext_tiny", epochs: int = 8): | |
| print(verifier_ab.remote(fold, backbone, epochs)) | |
| def predict(folds: str = "0", backbone: str = "convnext_tiny", score_thr: float = 0.05, | |
| threshold: float = 0.35, use_verifier: bool = True): | |
| fold_list = [int(f) for f in folds.split(",") if f.strip() != ""] | |
| print(predict_test.remote(fold_list, backbone, score_thr, threshold, use_verifier)) | |