File size: 9,855 Bytes
47f2ebf
 
 
 
 
 
 
 
dbb4a08
47f2ebf
 
 
 
ede824e
47f2ebf
 
 
 
 
 
 
ede824e
 
 
 
 
 
47f2ebf
 
ede824e
 
 
 
 
47f2ebf
ede824e
47f2ebf
 
ede824e
 
 
 
 
 
 
d0b7f8a
ede824e
 
 
 
47f2ebf
 
d0b7f8a
ede824e
47f2ebf
 
 
 
 
 
 
 
ede824e
 
47f2ebf
ede824e
 
47f2ebf
fcd343d
 
 
 
 
 
 
 
 
 
 
47f2ebf
ede824e
47f2ebf
 
fcd343d
ede824e
47f2ebf
ede824e
47f2ebf
bcf1d8f
 
dbb4a08
7c20709
dbb4a08
 
 
7c20709
dbb4a08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bcf1d8f
47f2ebf
ede824e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24bf943
ede824e
24bf943
ede824e
 
dbb4a08
ede824e
47f2ebf
dbb4a08
 
 
 
 
 
 
 
 
ede824e
47f2ebf
dbb4a08
47f2ebf
d0b7f8a
 
ede824e
 
 
 
 
 
 
 
 
fcd343d
123ef0f
 
 
bcf1d8f
 
 
 
 
 
ede824e
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import argparse
import json
from pathlib import Path

import torch
from PIL import Image
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
from transformers import RTDetrImageProcessor, RTDetrForObjectDetection

BASE_MODEL = "PekingU/rtdetr_r50vd"

def load_classes(path):
    return [x.strip() for x in Path(path).read_text().splitlines() if x.strip()]

class COCODetectionDataset(Dataset):
    def __init__(self, image_dir, annotation_file, processor):
        self.image_dir = Path(image_dir)
        self.processor = processor
        coco = json.loads(Path(annotation_file).read_text())
        self.images = {x["id"]: x for x in coco["images"]}
        cats = sorted(coco["categories"], key=lambda x: x["id"])
        self.category_id_to_label = {c["id"]: i for i,c in enumerate(cats)}
        anns = {}
        for a in coco["annotations"]:
            if not a.get("iscrowd", 0):
                anns.setdefault(a["image_id"], []).append(a)
        self.records = []
        for image_id, info in self.images.items():
            self.records.append({
                "image_id": image_id, "file_name": info["file_name"],
                "width": info["width"], "height": info["height"],
                "annotations": anns.get(image_id, [])
            })

    def __len__(self): return len(self.records)

    def __getitem__(self, idx):
        r = self.records[idx]
        image = Image.open(self.image_dir / r["file_name"]).convert("RGB")
        anns = []
        for a in r["annotations"]:
            x,y,w,h = a["bbox"]
            if w <= 0 or h <= 0: continue
            anns.append({
                "id": a["id"], "image_id": int(idx),
                "category_id": self.category_id_to_label[a["category_id"]],
                "bbox": [x,y,w,h], "area": float(a.get("area",w*h)),
                "iscrowd": 0
            })
        encoded = self.processor(
            images=image,
            annotations={"image_id": int(idx), "annotations": anns},
            return_tensors="pt"
        )
        encoded["pixel_values"] = encoded["pixel_values"].squeeze(0)
        if "pixel_mask" in encoded:
            encoded["pixel_mask"] = encoded["pixel_mask"].squeeze(0)
        encoded["labels"] = encoded["labels"][0]
        return encoded

def collate_fn(batch):
    out = {"pixel_values": torch.stack([x["pixel_values"] for x in batch]),
           "labels": [x["labels"] for x in batch]}
    if "pixel_mask" in batch[0]:
        out["pixel_mask"] = torch.stack([x["pixel_mask"] for x in batch])
    return out

def move_to_device(obj, device):
    if torch.is_tensor(obj):
        return obj.to(device)
    if isinstance(obj, dict):
        return {k: move_to_device(v, device) for k, v in obj.items()}
    if isinstance(obj, list):
        return [move_to_device(v, device) for v in obj]
    if isinstance(obj, tuple):
        return tuple(move_to_device(v, device) for v in obj)
    return obj

def evaluate(model, loader, device):
    model.eval(); total=0; n=0
    with torch.no_grad():
        for batch in loader:
            batch=move_to_device(batch, device)
            total += float(model(**batch).loss.item()); n += 1
    model.train()
    return total/max(n,1)


def patch_rtdetr_denoising_device():
    """Patch RT-DETR denoising so embedding indices are always on the embedding device.

    Important: the wrapper must be an nn.Module, not a plain Python function.
    Transformers may inspect/use class_embed as a module, and a plain function
    loses the original module parameters/device information.
    """
    try:
        import transformers.models.rt_detr.modeling_rt_detr as rtdetr_mod
        import torch.nn as nn

        original = rtdetr_mod.get_contrastive_denoising_training_group
        if getattr(original, "_icecream_device_patch", False):
            return

        class DeviceSafeEmbedding(nn.Module):
            def __init__(self, embedding):
                super().__init__()
                self.embedding = embedding

            def forward(self, indices):
                if torch.is_tensor(indices):
                    indices = indices.to(self.embedding.weight.device)
                return self.embedding(indices)

        def wrapped(targets, num_classes, num_queries, class_embed,
                    num_denoising_queries=100, label_noise_ratio=0.5,
                    box_noise_scale=1.0, **kwargs):
            # Move every target tensor to the embedding/model device.
            try:
                embed_device = class_embed.weight.device
            except Exception:
                try:
                    embed_device = next(class_embed.parameters()).device
                except Exception:
                    embed_device = None

            if embed_device is not None:
                fixed_targets = []
                for target in targets:
                    if isinstance(target, dict):
                        target = dict(target)
                        for key, value in list(target.items()):
                            if torch.is_tensor(value):
                                target[key] = value.to(embed_device)
                    fixed_targets.append(target)
                targets = fixed_targets
                class_embed = DeviceSafeEmbedding(class_embed)

            return original(
                targets=targets,
                num_classes=num_classes,
                num_queries=num_queries,
                class_embed=class_embed,
                num_denoising_queries=num_denoising_queries,
                label_noise_ratio=label_noise_ratio,
                box_noise_scale=box_noise_scale,
                **kwargs,
            )

        wrapped._icecream_device_patch = True
        rtdetr_mod.get_contrastive_denoising_training_group = wrapped
        print("RT-DETR denoising device patch installed")
    except Exception as exc:
        raise RuntimeError(f"Could not install RT-DETR denoising device patch: {exc}") from exc

def main():
    p=argparse.ArgumentParser()
    p.add_argument("--train-dir",required=True); p.add_argument("--val-dir",required=True)
    p.add_argument("--classes",required=True); p.add_argument("--output-dir",default="model")
    p.add_argument("--epochs",type=int,default=30); p.add_argument("--batch-size",type=int,default=2)
    p.add_argument("--learning-rate",type=float,default=1e-5); p.add_argument("--weight-decay",type=float,default=1e-4)
    p.add_argument("--num-workers",type=int,default=2)
    a=p.parse_args()

    classes=load_classes(a.classes)
    id2label={i:n for i,n in enumerate(classes)}
    label2id={n:i for i,n in enumerate(classes)}

    proc=RTDetrImageProcessor.from_pretrained(BASE_MODEL)
    train=COCODetectionDataset(Path(a.train_dir)/"images",Path(a.train_dir)/"annotations.json",proc)
    val=COCODetectionDataset(Path(a.val_dir)/"images",Path(a.val_dir)/"annotations.json",proc)

    if len(train)==0 or len(val)==0:
        raise ValueError("Training and validation datasets must contain at least one image.")
    if len(train.category_id_to_label)!=len(classes) or len(val.category_id_to_label)!=len(classes):
        raise ValueError("COCO categories do not match classes.txt. Rebuild the dataset after saving the classes.")

    model=RTDetrForObjectDetection.from_pretrained(
        BASE_MODEL,num_labels=len(classes),id2label=id2label,label2id=label2id,
        ignore_mismatched_sizes=True
    )
    # Some Transformers RT-DETR releases enter the denoising path whenever
    # training, regardless of num_denoising. Keep the config disabled where
    # supported, but also install the device-safe embedding patch below.
    for cfg_owner in (model, getattr(model, "model", None)):
        cfg = getattr(cfg_owner, "config", None)
        if cfg is not None:
            for name in ("num_denoising", "num_denoising_queries"):
                if hasattr(cfg, name):
                    setattr(cfg, name, 0)
    device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.to(device)
    patch_rtdetr_denoising_device()

    tr=DataLoader(train,batch_size=a.batch_size,shuffle=True,num_workers=0,collate_fn=collate_fn)
    va=DataLoader(val,batch_size=a.batch_size,shuffle=False,num_workers=0,collate_fn=collate_fn)
    opt=torch.optim.AdamW(model.parameters(),lr=a.learning_rate,weight_decay=a.weight_decay)

    outdir=Path(a.output_dir); outdir.mkdir(parents=True,exist_ok=True)
    best=float("inf")

    for epoch in range(a.epochs):
        model.train(); running=0
        bar=tqdm(tr,desc=f"epoch {epoch+1}/{a.epochs}")
        for step,batch in enumerate(bar):
            batch=move_to_device(batch, device)
            # RT-DETR's loss matcher uses nested target tensors (boxes/classes).
            # Move every tensor in labels to the same device as the model.
            if "labels" in batch:
                # RT-DETR expects every nested target tensor on the same device as the model.
                for target in batch["labels"]:
                    if isinstance(target, dict):
                        for key, value in list(target.items()):
                            if torch.is_tensor(value):
                                target[key] = value.to(device)
            loss=model(**batch).loss
            loss.backward(); opt.step(); opt.zero_grad(set_to_none=True)
            running += float(loss.item())
            bar.set_postfix(loss=f"{running/(step+1):.4f}")
        vl=evaluate(model,va,device)
        print(f"validation_loss={vl:.4f}")
        if vl<best:
            best=vl
            model.save_pretrained(outdir)
            proc.save_pretrained(outdir)
            (outdir/"classes.json").write_text(json.dumps({"id2label":id2label,"label2id":label2id},indent=2))
    model.save_pretrained(outdir); proc.save_pretrained(outdir)

if __name__=="__main__": main()