opencode-r2 / train.py
davanstrien's picture
davanstrien HF Staff
fix: move labels to device
9f4093e verified
Raw
History Blame Contribute Delete
15.1 kB
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "torch",
# "transformers>=4.40",
# "datasets>=2.20",
# "pillow",
# "accelerate",
# "pycocotools",
# "huggingface_hub",
# "trackio",
# "timm",
# "scipy",
# ]
# ///
import os
import sys
import copy
import argparse
import numpy as np
import torch
from torch.utils.data import DataLoader, Dataset
from datasets import load_dataset
from transformers import DetrImageProcessor, DetrForObjectDetection
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()}
CLASSES = [ID2LABEL[i] for i in sorted(ID2LABEL)]
MODEL_ID = "facebook/detr-resnet-50" # Apache-2.0
REPO_ID = "harness-race/opencode-r2"
# ----- trackio (best effort) -----
def setup_trackio():
if os.environ.get("OPENCODE_TRACKIO", "1") == "0":
return (lambda **kw: None)
try:
import trackio
r = trackio.init(project="opencode-r2", name=os.environ.get("JOB_NAME", "finetune"), private=True)
def tlog(**kw):
try:
for k, v in kw.items():
trackio.log(f"metric/{k}", v)
except Exception:
pass
return tlog
except Exception as e:
print("[trackio] unavailable:", e)
return (lambda **kw: None)
# ---------- dataset ----------
class DetrDataset(Dataset):
def __init__(self, hf_ds, processor, split):
self.ds = hf_ds
self.processor = processor
self.split = split
def __len__(self):
return len(self.ds)
def __getitem__(self, idx):
item = self.ds[idx]
image = item["image"].convert("RGB")
objs = item["objects"] # list of per-object dicts
bboxes_xywh = []
cat_ids = []
for o in objs:
box = o["bbox"]
crowd = bool(o["iscrowd"]) if o.get("iscrowd") is not None else False
if box[2] <= 0 or box[3] <= 0:
continue
if crowd and self.split == "train":
continue
bboxes_xywh.append(box)
cat_ids.append(o["category_id"]) # int label 0..6
annotations = [
{"area": b[2] * b[3], "bbox": list(b), "category_id": c}
for b, c in zip(bboxes_xywh, cat_ids)
]
anno = {"image_id": int(item["image_id"]), "annotations": annotations}
encoding = self.processor(
images=image,
annotations=anno,
return_tensors="pt",
)
if "labels" in encoding and isinstance(encoding["labels"], list) and len(encoding["labels"]) == 1:
encoding["labels"] = encoding["labels"][0]
for k, v in encoding.items():
if k == "labels":
continue
if isinstance(v, torch.Tensor) and len(v.shape) > 0:
try:
encoding[k] = v.squeeze(0)
except Exception:
pass
encoding["image_id"] = int(item["image_id"])
encoding["image_size"] = [int(item["height"]), int(item["width"])]
return encoding
def collate_fn(batch):
H = max(b["pixel_values"].shape[-2] for b in batch)
W = max(b["pixel_values"].shape[-1] for b in batch)
pixel_values = []
pixel_mask = []
for b in batch:
im = torch.as_tensor(b["pixel_values"])
h, w = im.shape[-2:]
if (h, w) != (H, W):
im = torch.nn.functional.pad(im, (0, W - w, 0, H - h), value=0.0)
m = torch.zeros((H, W), dtype=torch.int64)
m[:h, :w] = 1
pixel_values.append(im)
pixel_mask.append(m)
pixel_values = torch.stack(pixel_values)
pixel_mask = torch.stack(pixel_mask)
labels = []
for b in batch:
lb = None
if "labels" in b and b["labels"] is not None and "boxes" in b["labels"]:
lab = b["labels"]
lb = {
"class_labels": lab["class_labels"].clone() if isinstance(lab["class_labels"], torch.Tensor) else torch.tensor(lab["class_labels"], dtype=torch.long),
"boxes": lab["boxes"].clone() if isinstance(lab["boxes"], torch.Tensor) else torch.tensor(lab["boxes"], dtype=torch.float32),
}
if lb["boxes"].numel() == 0:
lb["boxes"] = torch.zeros((0, 4), dtype=torch.float32)
else:
lb = {"class_labels": torch.zeros((0,), dtype=torch.long),
"boxes": torch.zeros((0, 4), dtype=torch.float32)}
lb["image_id"] = b["image_id"]
lb["image_size"] = b["image_size"]
labels.append(lb)
return {"pixel_values": pixel_values, "pixel_mask": pixel_mask, "labels": labels}
# ---------- coco evaluation ----------
def to_coco(preds, gts, all_image_ids):
"""preds: list of {image_id, score, label, box_xyxy(pixels)}
gts: list of {image_id, category_id, bbox_xywh, area, ann_id}
"""
cat_id_map = {i: i + 1 for i in range(7)} # 0..6 -> 1..7
all_image_ids = list(dict.fromkeys(all_image_ids))
data = {
"images": [{"id": int(im)} for im in all_image_ids],
"categories": [{"id": i + 1, "name": CLASSES[i]} for i in range(7)],
"annotations": [
{"id": g["ann_id"], "image_id": g["image_id"], "category_id": cat_id_map[g["category_id"]],
"bbox": g["bbox_xywh"], "area": g["area"], "iscrowd": 0}
for g in gts
],
}
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
coco_gt = COCO()
coco_gt.dataset = data
coco_gt.createIndex()
res = []
for p in preds:
res.append({
"image_id": p["image_id"], "category_id": cat_id_map[p["label"]],
"bbox": [p["box_xyxy"][0], p["box_xyxy"][1],
p["box_xyxy"][2] - p["box_xyxy"][0], p["box_xyxy"][3] - p["box_xyxy"][1]],
"score": float(p["score"]),
})
if not res:
return {"mAP": 0.0, "AP50": 0.0, "AP75": 0.0, "AR1": 0.0, "AR10": 0.0, "AR100": 0.0}
coco_dt = coco_gt.loadRes(res)
e = COCOeval(coco_gt, coco_dt, "bbox")
e.evaluate()
e.accumulate()
e.summarize()
s = e.stats
out = {"mAP": float(s[0]), "AP50": float(s[1]), "AP75": float(s[2]),
"AR1": float(s[6]), "AR10": float(s[7]), "AR100": float(s[8])}
prec = e.eval["precision"] # (T=10 IoU, R=101 rec, K cat, A=4 area, M=3 maxDet)
per = {}
for i in range(7):
p = prec[:, :, i, 0, 2].flatten() # all IoU, all rec, class i, area=all, maxDet=100
p = p[p > -1]
per[CLASSES[i]] = float(p.mean()) if p.size > 0 else 0.0
out["per_class_mAP"] = per
p50 = {}
for i in range(7):
p = prec[0, :, i, 0, 2].flatten() # IoU=0.5
p = p[p > -1]
p50[CLASSES[i]] = float(p.mean()) if p.size > 0 else 0.0
out["per_class_AP50"] = p50
return out
@torch.no_grad()
def evaluate(model, processor, val_dl, device):
model.eval()
preds = []
gts = []
all_image_ids = []
ann_id = 1
for batch in val_dl:
pixel_values = batch["pixel_values"].to(device)
pixel_mask = batch["pixel_mask"].to(device)
labels = batch["labels"]
with torch.autocast(device_type="cuda", dtype=torch.float16):
outputs = model(pixel_values=pixel_values, pixel_mask=pixel_mask)
target_sizes = torch.tensor([[labels[bi]["image_size"][0], labels[bi]["image_size"][1]] for bi in range(len(labels))], device=device)
results = processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=0.0)
for bi, r in enumerate(results):
lab_gt = labels[bi]
img_id = int(lab_gt["image_id"])
all_image_ids.append(img_id)
scores = r["scores"]
keep = scores > 0.0
boxes = r["boxes"][keep]
scores = scores[keep]
labels_ids = r["labels"][keep]
for bx, sc, la in zip(boxes, scores, labels_ids):
preds.append({"image_id": img_id, "label": int(la.item()), "score": float(sc.item()),
"box_xyxy": [float(v) for v in bx]})
# gt boxes are normalized cxcywh in b['boxes']; convert to pixel xyxy
lab = labels[bi]
if lab["boxes"].numel() > 0:
H, W = int(target_sizes[bi][0]), int(target_sizes[bi][1])
c = lab["boxes"].float()
cx, cy = c[:, 0], c[:, 1]
w2, h2 = c[:, 2], c[:, 3]
x1 = (cx - w2 / 2) * W
x2 = (cx + w2 / 2) * W
y1 = (cy - h2 / 2) * H
y2 = (cy + h2 / 2) * H
for j in range(c.shape[0]):
x1v, y1v, x2v, y2v = float(x1[j]), float(y1[j]), float(x2[j]), float(y2[j])
gts.append({"image_id": img_id, "category_id": int(lab["class_labels"][j].item()),
"bbox_xywh": [x1v, y1v, x2v - x1v, y2v - y1v],
"area": (x2v - x1v) * (y2v - y1v), "ann_id": ann_id})
ann_id += 1
return to_coco(preds, gts, all_image_ids)
# ---------- main ----------
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--epochs", type=int, default=5)
ap.add_argument("--batch", type=int, default=2)
ap.add_argument("--size", type=int, default=560)
ap.add_argument("--lr", type=float, default=1e-4)
ap.add_argument("--workers", type=int, default=2)
ap.add_argument("--skip_eval", action="store_true")
ap.add_argument("--no_push", action="store_true")
args = ap.parse_args()
tlog = setup_trackio()
torch.manual_seed(42)
device = "cuda" if torch.cuda.is_available() else "cpu"
print("device:", device, "gpus:", torch.cuda.device_count(), flush=True)
processor = DetrImageProcessor.from_pretrained(MODEL_ID)
processor.do_resize = True
processor.size = {"shortest_edge": args.size, "longest_edge": 800}
processor.do_rescale = True
processor.do_normalize = True
processor.do_rescale_deprecated = False
hf_train = load_dataset("biglam/loc_beyond_words", split="train")
hf_val = load_dataset("biglam/loc_beyond_words", split="validation")
print(f"train={len(hf_train)} val={len(hf_val)}", flush=True)
model = DetrForObjectDetection.from_pretrained(
MODEL_ID, num_labels=7, ignore_mismatched_sizes=True,
id2label=ID2LABEL, label2id=LABEL2ID,
).to(device)
n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print("trainable params:", n_params, flush=True)
train_ds = DetrDataset(hf_train, processor, "train")
val_ds = DetrDataset(hf_val, processor, "val")
train_dl = DataLoader(train_ds, batch_size=args.batch, shuffle=True,
num_workers=args.workers, collate_fn=collate_fn, drop_last=True)
val_dl = DataLoader(val_ds, batch_size=args.batch, shuffle=False,
num_workers=args.workers, collate_fn=collate_fn)
no_decay = ["bias", "LayerNorm.weight", "layer_norm.weight", "embed_positions.weight", "norm.weight"]
opt = torch.optim.AdamW([
{"params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)],
"lr": args.lr, "weight_decay": 1e-4},
{"params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)],
"lr": args.lr, "weight_decay": 0.0},
])
steps_per_epoch = len(train_dl)
total_steps = steps_per_epoch * args.epochs
from transformers import get_linear_schedule_with_warmup
sched = get_linear_schedule_with_warmup(opt, num_warmup_steps=int(0.1 * total_steps),
num_training_steps=total_steps)
global_step = 0
best_map = -1.0
for epoch in range(args.epochs):
model.train()
epoch_loss = 0.0
nb = 0
for step, batch in enumerate(train_dl):
pixel_values = batch["pixel_values"].to(device)
pixel_mask = batch["pixel_mask"].to(device)
labels = [{k: (v.to(device) if isinstance(v, torch.Tensor) else v)
for k, v in lab.items()} for lab in batch["labels"]]
with torch.autocast(device_type="cuda", dtype=torch.float16):
out = model(pixel_values=pixel_values, pixel_mask=pixel_mask, labels=labels)
loss = sum(v for k, v in out.loss_dict.items() if v is not None)
opt.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
sched.step()
epoch_loss += loss.item()
nb += 1
global_step += 1
if step % 25 == 0:
m = {("l_" + k): float(v.item()) for k, v in out.loss_dict.items() if v is not None}
print(f"[ep{epoch} step{step}/{steps_per_epoch}] loss={loss.item():.3f} { {k: round(v,3) for k,v in m.items()} }", flush=True)
tlog(step=global_step, loss=loss.item(), **m)
avg = epoch_loss / max(nb, 1)
print(f"==== EPOCH {epoch} DONE avg_loss={avg:.4f} ====", flush=True)
tlog(epoch_loss=avg, epoch=epoch)
# eval
if not args.skip_eval:
print("evaluating...", flush=True)
metrics = evaluate(model, processor, val_dl, device)
print("EVAL:", {k: (round(v, 4) if isinstance(v, float) else v) for k, v in metrics.items() if k != "per_class_mAP" and k != "per_class_AP50"}, flush=True)
print("eval per-class mAP:", {k: round(v, 4) for k, v in metrics["per_class_mAP"].items()}, flush=True)
print("eval per-class AP50:", {k: round(v, 4) for k, v in metrics["per_class_AP50"].items()}, flush=True)
tlog(mAP=metrics["mAP"], AP50=metrics["AP50"], AR100=metrics["AR100"], epoch=epoch)
if metrics["mAP"] > best_map:
best_map = metrics["mAP"]
save_dir = "/tmp/best_model"
model.save_pretrained(save_dir)
processor.save_pretrained(save_dir)
else:
save_dir = "/tmp/best_model"
model.save_pretrained(save_dir)
processor.save_pretrained(save_dir)
print("best mAP:", best_map, flush=True)
save_dir = "/tmp/final_model"
model.save_pretrained(save_dir)
processor.save_pretrained(save_dir)
print("saved:", save_dir, flush=True)
if not args.no_push:
from huggingface_hub import HfApi
api = HfApi()
print("pushing model to", REPO_ID, flush=True)
api.upload_folder(repo_id=REPO_ID, folder_path=save_dir, repo_type="model", commit_message="fine-tuned DETR on loc_beyond_words")
tlog(best_mAP=best_map)
try:
import trackio
trackio.finish(status=0)
except Exception:
pass
print("DONE", flush=True)
if __name__ == "__main__":
main()