opencode-r3 / train_detr.py
davanstrien's picture
davanstrien HF Staff
Upload train_detr.py with huggingface_hub
d06c03f verified
Raw
History Blame Contribute Delete
15.8 kB
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "torch>=2.1",
# "transformers>=4.40",
# "accelerate>=0.27",
# "datasets>=2.18",
# "pycocotools",
# "huggingface_hub>=0.23",
# "Pillow",
# "numpy",
# "timm",
# "scipy",
# "requests",
# ]
# ///
import os, json, time, argparse, io
import numpy as np
import torch
from PIL import Image
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--data_dir", default="/data")
p.add_argument("--base_model", default="facebook/detr-resnet-50")
p.add_argument("--hub_id", default="harness-race/opencode-r3")
p.add_argument("--epochs", type=int, default=25)
p.add_argument("--batch_size", type=int, default=4)
p.add_argument("--lr", type=float, default=1e-4)
p.add_argument("--patience", type=int, default=4)
p.add_argument("--seed", type=int, default=42)
p.add_argument("--exp_name", default="opencode-r3-detr")
p.add_argument("--max_steps", type=int, default=0)
return p.parse_args()
ARGS = parse_args()
ID2LABEL = {0:"Photograph",1:"Illustration",2:"Map",3:"Comics/Cartoon",4:"Editorial Cartoon",5:"Headline",6:"Advertisement"}
LABEL2ID = {v:k for k,v in ID2LABEL.items()}
N = len(ID2LABEL)
import datasets as hfds
from torch.utils.data import Dataset, DataLoader
from transformers import DetrImageProcessor, DetrForObjectDetection
from tqdm import tqdm
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
def log(*m):
print("[%s]" % time.strftime("%H:%M:%S"), *m, flush=True)
torch.manual_seed(ARGS.seed)
np.random.seed(ARGS.seed)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
log("torch", torch.__version__, "device", device, "cuda_avail", torch.cuda.is_available())
data_dir = ARGS.data_dir
if not os.path.isdir(data_dir):
log("mount %s not found; downloading dataset" % data_dir)
data_dir = ARGS.data_dir = "biglam/loc_beyond_words"
try:
ds = hfds.load_dataset(data_dir, split="train")
ds_val = hfds.load_dataset(data_dir, split="validation")
except Exception as e:
log("local load failed (%s); falling back to hub dataset" % e)
data_dir = "biglam/loc_beyond_words"
ds = hfds.load_dataset(data_dir, split="train")
ds_val = hfds.load_dataset(data_dir, split="validation")
def get_pil(ex):
img = ex["image"]
if isinstance(img, dict):
img = Image.open(io.BytesIO(img["bytes"])).convert("RGB")
else:
img = img.convert("RGB")
return img
def fields(ex):
objs = ex["objects"]
bboxes=[]; cats=[]; areas=[]; ids=[]
if objs is None: objs=[]
for o in objs:
b = o["bbox"]
if isinstance(b, (str, tuple)):
b = list(float(x) for x in b)
bboxes.append([float(x) for x in b])
c = o["category_id"]
if isinstance(c, str):
cats.append(LABEL2ID.get(c, 0))
else:
try: cats.append(int(c)&0x7fffffff)
except Exception: cats.append(0)
areas.append(float(o.get("area", b[2]*b[3])))
ids.append(int(o.get("id", 0)))
return {"bbox":bboxes,"category_id":cats,"area":areas,"id":ids}
# Build torch Dataset returning (pil_img, anns_for_processor, size_wh, row_id)
class DetrDS(Dataset):
def __init__(self, examples, with_size=True):
self.examples = examples
self.sizes = []
for ex in examples:
try:
w,h = ex["width"], ex["height"]
except Exception:
w,h = get_pil(ex).size
self.sizes.append((w,h))
def __len__(self):
return len(self.examples)
def __getitem__(self, i):
ex = self.examples[i]
img = get_pil(ex)
o = fields(ex)
bboxes = o.get("bbox") or []
cats = o.get("category_id")
if not isinstance(cats, list):
cats = [0]*len(bboxes)
anns = [{"bbox":[float(x) for x in bboxes[j]], "category_id":int(cats[j])&0x7fffffff,
"area": float(bboxes[j][2]*bboxes[j][3])} for j in range(len(bboxes))]
return img, anns, self.sizes[i], i
processor = DetrImageProcessor.from_pretrained(ARGS.base_model,
do_resize=True, size={"shortest_edge":800, "longest_edge":1333})
train_ds = DetrDS(list(ds))
val_ds = DetrDS(list(ds_val))
log("train", len(train_ds), "val", len(val_ds))
def collate_fn(batch):
# batch: list of (img, anns, size, idx)
images = [b[0] for b in batch]
anns = [b[1] for b in batch]
sizes = [b[2] for b in batch]
idx = [b[3] for b in batch]
return images, anns, sizes, idx
train_dl = DataLoader(train_ds, batch_size=ARGS.batch_size, shuffle=True, num_workers=2, collate_fn=collate_fn)
val_dl = DataLoader(val_ds, batch_size=ARGS.batch_size, shuffle=False, num_workers=2, collate_fn=collate_fn)
model = DetrForObjectDetection.from_pretrained(ARGS.base_model, num_labels=N,
ignore_mismatched_sizes=True, id2label=ID2LABEL, label2id=LABEL2ID)
model.to(device)
# distinct backbone (frozen-ish) vs head lr
bb_params = [p for n,p in model.model.backbone.named_parameters() if "layers" in n]
head_params = [p for n,p in model.named_parameters() if not n.startswith("model.backbone") or "layers" not in n]
optim = torch.optim.AdamW([
{"params": bb_params, "lr": ARGS.lr/10},
{"params": head_params, "lr": ARGS.lr},
], lr=ARGS.lr, weight_decay=1e-4)
scaler = torch.amp.GradScaler("cuda", init_scale=2.0**12) if device.type=="cuda" else None
def build_coco_gt(examples):
gt = {"images":[],"annotations":[],"categories":[{"id":k,"name":v} for k,v in ID2LABEL.items()]}
ann_id = 1
for i, ex in enumerate(examples):
o = fields(ex)
w,h = val_ds.sizes[i]
gt["images"].append({"id":i,"width":w,"height":h})
bboxes = o.get("bbox") or []
cats = o.get("category_id")
if not isinstance(cats, list):
cats = [0]*len(bboxes)
for j in range(len(bboxes)):
b = [float(x) for x in bboxes[j]]
gt["annotations"].append({"id":ann_id,"image_id":i,"category_id":int(cats[j])&0x7fffffff,
"bbox":b,"area":b[2]*b[3],"iscrowd":0}); ann_id+=1
g = COCO(); g.dataset = gt; g.createIndex()
return g
coco_gt = build_coco_gt(list(ds_val))
def evaluate():
model.eval()
dets = []
ann_id = 1
with torch.no_grad():
for images, anns, sizes, idx in tqdm(val_dl, desc="eval"):
enc = processor(images=images, return_tensors="pt")
pv = enc["pixel_values"].to(device); pm = enc["pixel_mask"].to(device)
with torch.amp.autocast(device_type="cuda", dtype=torch.float16, enabled=(device.type=="cuda")):
out = model(pixel_values=pv, pixel_mask=pm)
target_sizes = torch.tensor([val_ds.sizes[ii][::-1] for ii in idx.astype(int)] if hasattr(idx,'astype') else [[val_ds.sizes[ii][1], val_ds.sizes[ii][0]] for ii in idx], device=device)
# idx is list of ints already
sz = [[val_ds.sizes[ii][1], val_ds.sizes[ii][0]] for ii in idx]
results = processor.post_process_object_detection(out, threshold=0.0, target_sizes=sz)
for ii, res in zip(idx, results):
boxes = res["boxes"].cpu().numpy() # xmin ymin xmax ymax
scores = res["scores"].cpu().numpy()
labels = res["labels"].cpu().numpy()
for b,s,l in zip(boxes,scores,labels):
if s <= 0.0: continue
dets.append({"id":ann_id,"image_id":ii,"category_id":int(l),"bbox":[float(b[0]),float(b[1]),float(b[2]-b[0]),float(b[3]-b[1])],"score":float(s)})
ann_id += 1
model.train()
if len(dets)==0:
return None
try:
preds = coco_gt.loadRes(dets)
evaluator = COCOeval(coco_gt, preds, iouType="bbox")
# set areas to all, max dets high
evaluator.params.maxDets = [100, 300, 1000]
evaluator.evaluate(); evaluator.accumulate(); evaluator.summarize()
ap = evaluator.stats # [0]=AP@.5:.95 ... [5]=AP50 ... [6]=AR100
return {"AP": float(ap[0]), "AP50": float(ap[1]), "AP75": float(ap[2]),
"AP_s": float(ap[3]), "AP_m": float(ap[4]), "AP_l": float(ap[5]),
"AR_max100": float(ap[6]), "AR_max1000": float(ap[8])}
except Exception as e:
log("COCOeval failed:", e)
return None
# ---------- training ----------
best = {"AP": -1.0}
no_improve = 0
global_step = 0
run_outputs = {}
t_start = time.time()
log("starting training; epochs", ARGS.epochs, "batch", ARGS.batch_size)
for epoch in range(1, ARGS.epochs+1):
model.train()
ep_loss = 0.0; ep_losssum = {}; nb = 0
for images, anns, sizes, idx in tqdm(train_dl, desc="epoch %d"%epoch):
anns_pp = [{"image_id": int(idx[k]), "annotations": anns[k]} for k in range(len(images))]
enc = processor(images=images, annotations=anns_pp, return_tensors="pt")
pv = enc["pixel_values"].to(device); pm = enc["pixel_mask"].to(device)
labels = [{k: v.to(device) for k,v in l.items()} for l in enc.get("labels")]
optim.zero_grad()
try:
with torch.amp.autocast(device_type="cuda", dtype=torch.float16, enabled=(device.type=="cuda")):
out = model(pixel_values=pv, pixel_mask=pm, labels=labels)
loss = out.loss
except Exception as e:
log("skipping batch that raised:", e)
continue
if not torch.isfinite(loss):
log("skipping non-finite loss step")
continue
scaler.scale(loss).backward()
scaler.step(optim)
scaler.update()
ep_loss += float(loss.detach().float())
for kk, vv in out.loss_dict.items():
ep_losssum[kk] = ep_losssum.get(kk, 0.0) + float(vv.detach().float())
nb += 1
global_step += 1
if ARGS.max_steps and global_step >= ARGS.max_steps:
break
log("epoch %d loss %.4f %s" % (epoch, ep_loss/nb,
" ".join("%s %.4f"%(k, v/nb) for k,v in ep_losssum.items())))
m = evaluate()
log("VAL", json.dumps(m))
if m and m["AP"] > best["AP"]:
best = m
best.update({"epoch": epoch, "global_step": global_step})
no_improve = 0
torch.save({"state_dict": model.state_dict()}, os.path.join(os.getcwd(), "best_model.pt"))
log("saved new best AP=%.4f" % m["AP"])
else:
no_improve += 1
if no_improve >= ARGS.patience:
log("early stop after epoch", epoch)
break
if ARGS.max_steps and global_step >= ARGS.max_steps:
break
# decay lr slowly
# (optional) torch.optim.lr_scheduler not added; keep fixed
run_outputs["best"] = best
run_outputs["epochs_run"] = epoch
run_outputs["elapsed_sec"] = round(time.time()-t_start, 1)
log("best val result:", best)
# ---------- reload best & push ----------
if os.path.exists("best_model.pt"):
sd = torch.load("best_model.pt", map_location="cpu")
model.load_state_dict(sd["state_dict"])
else:
best = evaluate() or {}
run_outputs["best"] = best
model.push_to_hub(ARGS.hub_id)
processor.push_to_hub(ARGS.hub_id)
log("pushed weights + preprocessor")
# build model card
card = {
"library_name": "transformers",
"pipeline_tag": "object-detection",
"license": "apache-2.0",
"tags": ["object-detection","detr","computer-vision","document-layout-analysis","pytorch"],
"base_model": ARGS.base_model,
"model-index": [{
"name": ARGS.exp_name,
"results": [{
"task": {"type":"object-detection"},
"dataset": {"type":"biglam/loc_beyond_words","name":"Beyond Words (Testing)","config":"default"},
"metrics": [
{"type":"Average Precision","name":"mAP @[IoU=0.50:0.95]","value": round(run_outputs["best"].get("AP",0.0),4)},
{"type":"Average Precision","name":"mAP @[IoU=0.50]","value": round(run_outputs["best"].get("AP50",0.0),4)},
]
}]
}]
}
def build_readme(res):
m = res if res else {}
class Row:
pass
def f(k, d=.0):
return "%.4f" % m.get(k, d)
front = json.dumps(card, indent=2)
return f"""---
{front}
---
# opencode-r3: Beyond Words Object Detection (DETR-ResNet-50)
Model for detecting visual content regions in WWI-era historical newspaper pages
from the US Library of Congress **Beyond Words / Chronicling America** collection.
The base model `{ARGS.base_model}` (Apache-2.0) was fine-tuned on {len(train_ds)} train images
and evaluated on {len(val_ds)} validation images over **7 classes**:
Photograph, Illustration, Map, Comics/Cartoon, Editorial Cartoon, Headline, Advertisement.
## Model Details
| | |
|---|---|
| **Base model** | {ARGS.base_model} (DETR, ResNet-50 backbone) |
| **License** | Apache-2.0 (open, shareable) |
| **Architecture** | `DetrForObjectDetection` |
| **Task** | Object detection / document layout analysis |
| **Dataset** | [biglam/loc_beyond_words](https://huggingface.co/datasets/biglam/loc_beyond_words) (CC0-1.0) |
| **Splits** | {len(train_ds)} train / {len(val_ds)} validation |
## Training Procedure
- **Optimizer:** AdamW (head LR {ARGS.lr}, backbone LR {ARGS.lr/10:g})
- **Epochs:** {run_outputs.get('epochs_run','?')} (early-stopped, patience {ARGS.patience})
- **Batch size:** {ARGS.batch_size}
- **Image size:** longest edge 1333 / shortest edge 800
- **Hardware:** Hugging Face Jobs GPU (T4)
- **Selection:** best checkpoint by validation mAP@[0.50:0.95]
## Evaluation Results (validation)
| Metric | Value |
|---|---|
| **mAP @[IoU=0.50:0.95]** | **{f('AP')}** |
| mAP @[IoU=0.50] | {f('AP50')} |
| mAP @[IoU=0.75] | {f('AP75')} |
| AP small | {f('AP_s')} |
| AP medium | {f('AP_m')} |
| AP large | {f('AP_l')} |
| AR (maxDets=100) | {f('AR_max100')} |
## Usage
```python
import torch
from transformers import DetrImageProcessor, DetrForObjectDetection
from PIL import Image
model = DetrForObjectDetection.from_pretrained("harness-race/opencode-r3")
processor = DetrImageProcessor.from_pretrained("harness-race/opencode-r3")
image = Image.open("page.jpg").convert("RGB")
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
results = processor.post_process_object_detection(
outputs, target_sizes=[image.size[::-1]], threshold=0.5)[0]
for score, label_id, box in zip(results["scores"], results["labels"], results["boxes"]):
if score > 0.5:
print(model.config.id2label[label_id.item()], round(score.item(),3), box.tolist())
```
## Licensing
Base model `{ARGS.base_model}` is **Apache-2.0** (permissive open license), so this
fine-tuned model may be freely shared and reused. The training dataset is public
domain (**CC0-1.0**).
## Known Limitations
- Trained on a single era/language; pre-1875 layouts may underperform.
- Skewed class distribution (headlines/ads dominate) can depress rare-class AP.
"""
readme = build_readme(run_outputs["best"])
with open("README.md","w") as f:
f.write(readme)
# push README + a results.json
from huggingface_hub import HfApi
api = HfApi()
with open("validation_results.json", "w") as f:
json.dump(run_outputs["best"], f, indent=2)
with open("README.md", "w") as f:
f.write(readme)
api.upload_file(path_or_fileobj="validation_results.json", path_in_repo="validation_results.json", repo_id=ARGS.hub_id, repo_type="model")
api.upload_file(path_or_fileobj="README.md", path_in_repo="README.md", repo_id=ARGS.hub_id, repo_type="model")
# model card metadata (config fields like license) already in config.json via push_to_hub
log("=== FINAL VALIDATION RESULTS ===")
print(json.dumps(run_outputs["best"], indent=2))
log("done")