ibsocr1 commited on
Commit
ede824e
·
verified ·
1 Parent(s): 7859d7b

Upload train.py

Browse files
Files changed (1) hide show
  1. training/train.py +86 -234
training/train.py CHANGED
@@ -8,273 +8,125 @@ from torch.utils.data import Dataset, DataLoader
8
  from tqdm import tqdm
9
  from transformers import RTDetrImageProcessor, RTDetrForObjectDetection
10
 
11
-
12
  BASE_MODEL = "PekingU/rtdetr_r50vd"
13
 
14
-
15
  def load_classes(path):
16
- classes = [x.strip() for x in Path(path).read_text().splitlines() if x.strip()]
17
- if not classes:
18
- raise ValueError("classes.txt is empty")
19
- return classes
20
-
21
 
22
  class COCODetectionDataset(Dataset):
23
  def __init__(self, image_dir, annotation_file, processor):
24
  self.image_dir = Path(image_dir)
25
  self.processor = processor
26
-
27
  coco = json.loads(Path(annotation_file).read_text())
28
  self.images = {x["id"]: x for x in coco["images"]}
29
-
30
- # The training model needs contiguous labels 0..N-1.
31
- # categories.json is converted here from the COCO category IDs.
32
- categories = sorted(coco["categories"], key=lambda x: x["id"])
33
- self.category_id_to_label = {
34
- cat["id"]: i for i, cat in enumerate(categories)
35
- }
36
-
37
- anns_by_image = {}
38
- for ann in coco["annotations"]:
39
- if ann.get("iscrowd", 0):
40
- continue
41
- anns_by_image.setdefault(ann["image_id"], []).append(ann)
42
-
43
  self.records = []
44
  for image_id, info in self.images.items():
45
- self.records.append(
46
- {
47
- "image_id": image_id,
48
- "file_name": info["file_name"],
49
- "width": info["width"],
50
- "height": info["height"],
51
- "annotations": anns_by_image.get(image_id, []),
52
- }
53
- )
54
 
55
- def __len__(self):
56
- return len(self.records)
57
 
58
  def __getitem__(self, idx):
59
- record = self.records[idx]
60
- image_path = self.image_dir / record["file_name"]
61
- image = Image.open(image_path).convert("RGB")
62
-
63
- annotations = []
64
- for ann in record["annotations"]:
65
- x, y, w, h = ann["bbox"]
66
- if w <= 0 or h <= 0:
67
- continue
68
-
69
- label = self.category_id_to_label[ann["category_id"]]
70
- annotations.append(
71
- {
72
- "id": ann["id"],
73
- "image_id": record["image_id"],
74
- "category_id": label,
75
- "bbox": [x, y, w, h],
76
- "area": float(ann.get("area", w * h)),
77
- "iscrowd": 0,
78
- }
79
- )
80
-
81
- target = {
82
- "image_id": record["image_id"],
83
- "annotations": annotations,
84
- }
85
-
86
  encoded = self.processor(
87
  images=image,
88
- annotations=target,
89
- return_tensors="pt",
90
  )
91
-
92
- # Remove batch dimension. DataLoader will create it.
93
  encoded["pixel_values"] = encoded["pixel_values"].squeeze(0)
94
  if "pixel_mask" in encoded:
95
  encoded["pixel_mask"] = encoded["pixel_mask"].squeeze(0)
96
  encoded["labels"] = encoded["labels"][0]
97
  return encoded
98
 
99
-
100
  def collate_fn(batch):
101
- pixel_values = torch.stack([x["pixel_values"] for x in batch])
102
- pixel_mask = None
103
  if "pixel_mask" in batch[0]:
104
- pixel_mask = torch.stack([x["pixel_mask"] for x in batch])
105
-
106
- labels = [x["labels"] for x in batch]
107
- result = {"pixel_values": pixel_values, "labels": labels}
108
- if pixel_mask is not None:
109
- result["pixel_mask"] = pixel_mask
110
- return result
111
-
112
 
113
  def evaluate(model, loader, device):
114
- model.eval()
115
- total = 0.0
116
- count = 0
117
  with torch.no_grad():
118
  for batch in loader:
119
- batch = {
120
- k: (v.to(device) if torch.is_tensor(v) else v)
121
- for k, v in batch.items()
122
- }
123
- out = model(**batch)
124
- total += float(out.loss.item())
125
- count += 1
126
  model.train()
127
- return total / max(count, 1)
128
-
129
 
130
  def main():
131
- parser = argparse.ArgumentParser()
132
- parser.add_argument("--train-dir", default="data/train")
133
- parser.add_argument("--val-dir", default="data/val")
134
- parser.add_argument("--classes", default="training/classes.txt")
135
- parser.add_argument("--output-dir", default="model")
136
- parser.add_argument("--epochs", type=int, default=30)
137
- parser.add_argument("--batch-size", type=int, default=2)
138
- parser.add_argument("--learning-rate", type=float, default=1e-5)
139
- parser.add_argument("--weight-decay", type=float, default=1e-4)
140
- parser.add_argument("--num-workers", type=int, default=2)
141
- parser.add_argument("--grad-accumulation", type=int, default=1)
142
- parser.add_argument("--resume", default=None)
143
- args = parser.parse_args()
144
-
145
- classes = load_classes(args.classes)
146
- label2id = {name: i for i, name in enumerate(classes)}
147
- id2label = {i: name for i, name in enumerate(classes)}
148
-
149
- train_ann = Path(args.train_dir) / "annotations.json"
150
- val_ann = Path(args.val_dir) / "annotations.json"
151
-
152
- processor = RTDetrImageProcessor.from_pretrained(BASE_MODEL)
153
-
154
- train_ds = COCODetectionDataset(
155
- Path(args.train_dir) / "images",
156
- train_ann,
157
- processor,
158
- )
159
- val_ds = COCODetectionDataset(
160
- Path(args.val_dir) / "images",
161
- val_ann,
162
- processor,
163
- )
164
-
165
- # Make sure both datasets use exactly the class list supplied by the user.
166
- train_categories = len(train_ds.category_id_to_label)
167
- val_categories = len(val_ds.category_id_to_label)
168
- if train_categories != len(classes) or val_categories != len(classes):
169
- raise ValueError(
170
- "The number of COCO categories does not match classes.txt. "
171
- f"classes.txt={len(classes)}, train={train_categories}, val={val_categories}"
172
- )
173
-
174
- model = RTDetrForObjectDetection.from_pretrained(
175
- BASE_MODEL,
176
- num_labels=len(classes),
177
- id2label=id2label,
178
- label2id=label2id,
179
- ignore_mismatched_sizes=True,
180
  )
181
-
182
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
183
  model.to(device)
184
 
185
- train_loader = DataLoader(
186
- train_ds,
187
- batch_size=args.batch_size,
188
- shuffle=True,
189
- num_workers=args.num_workers,
190
- collate_fn=collate_fn,
191
- pin_memory=torch.cuda.is_available(),
192
- )
193
- val_loader = DataLoader(
194
- val_ds,
195
- batch_size=args.batch_size,
196
- shuffle=False,
197
- num_workers=args.num_workers,
198
- collate_fn=collate_fn,
199
- pin_memory=torch.cuda.is_available(),
200
- )
201
-
202
- optimizer = torch.optim.AdamW(
203
- model.parameters(),
204
- lr=args.learning_rate,
205
- weight_decay=args.weight_decay,
206
- )
207
-
208
- scaler = torch.amp.GradScaler("cuda", enabled=torch.cuda.is_available())
209
- start_epoch = 0
210
-
211
- if args.resume:
212
- checkpoint = torch.load(args.resume, map_location=device)
213
- model.load_state_dict(checkpoint["model"])
214
- optimizer.load_state_dict(checkpoint["optimizer"])
215
- start_epoch = checkpoint["epoch"] + 1
216
-
217
- output_dir = Path(args.output_dir)
218
- output_dir.mkdir(parents=True, exist_ok=True)
219
-
220
- best_val = float("inf")
221
-
222
- for epoch in range(start_epoch, args.epochs):
223
- model.train()
224
- optimizer.zero_grad(set_to_none=True)
225
- running = 0.0
226
-
227
- pbar = tqdm(train_loader, desc=f"Epoch {epoch + 1}/{args.epochs}")
228
- for step, batch in enumerate(pbar):
229
- batch = {
230
- k: (v.to(device) if torch.is_tensor(v) else v)
231
- for k, v in batch.items()
232
- }
233
-
234
- with torch.amp.autocast(
235
- device_type="cuda",
236
- enabled=torch.cuda.is_available(),
237
- ):
238
- out = model(**batch)
239
- loss = out.loss / args.grad_accumulation
240
-
241
- scaler.scale(loss).backward()
242
-
243
- if (step + 1) % args.grad_accumulation == 0:
244
- scaler.step(optimizer)
245
- scaler.update()
246
- optimizer.zero_grad(set_to_none=True)
247
-
248
- running += float(loss.item()) * args.grad_accumulation
249
- pbar.set_postfix(loss=f"{running / (step + 1):.4f}")
250
-
251
- val_loss = evaluate(model, val_loader, device)
252
- print(f"epoch={epoch + 1} validation_loss={val_loss:.4f}")
253
-
254
- checkpoint = {
255
- "epoch": epoch,
256
- "model": model.state_dict(),
257
- "optimizer": optimizer.state_dict(),
258
- }
259
- torch.save(checkpoint, output_dir / "last_checkpoint.pt")
260
-
261
- if val_loss < best_val:
262
- best_val = val_loss
263
- model.save_pretrained(output_dir)
264
- processor.save_pretrained(output_dir)
265
- (output_dir / "classes.json").write_text(
266
- json.dumps(
267
- {"id2label": id2label, "label2id": label2id},
268
- indent=2,
269
- )
270
- )
271
- print(f"Saved best model to {output_dir}")
272
-
273
- # Always save the final model as well.
274
- model.save_pretrained(output_dir)
275
- processor.save_pretrained(output_dir)
276
- print(f"Final model saved to {output_dir}")
277
-
278
-
279
- if __name__ == "__main__":
280
- main()
 
8
  from tqdm import tqdm
9
  from transformers import RTDetrImageProcessor, RTDetrForObjectDetection
10
 
 
11
  BASE_MODEL = "PekingU/rtdetr_r50vd"
12
 
 
13
  def load_classes(path):
14
+ return [x.strip() for x in Path(path).read_text().splitlines() if x.strip()]
 
 
 
 
15
 
16
  class COCODetectionDataset(Dataset):
17
  def __init__(self, image_dir, annotation_file, processor):
18
  self.image_dir = Path(image_dir)
19
  self.processor = processor
 
20
  coco = json.loads(Path(annotation_file).read_text())
21
  self.images = {x["id"]: x for x in coco["images"]}
22
+ cats = sorted(coco["categories"], key=lambda x: x["id"])
23
+ self.category_id_to_label = {c["id"]: i for i,c in enumerate(cats)}
24
+ anns = {}
25
+ for a in coco["annotations"]:
26
+ if not a.get("iscrowd", 0):
27
+ anns.setdefault(a["image_id"], []).append(a)
 
 
 
 
 
 
 
 
28
  self.records = []
29
  for image_id, info in self.images.items():
30
+ self.records.append({
31
+ "image_id": image_id, "file_name": info["file_name"],
32
+ "width": info["width"], "height": info["height"],
33
+ "annotations": anns.get(image_id, [])
34
+ })
 
 
 
 
35
 
36
+ def __len__(self): return len(self.records)
 
37
 
38
  def __getitem__(self, idx):
39
+ r = self.records[idx]
40
+ image = Image.open(self.image_dir / r["file_name"]).convert("RGB")
41
+ anns = []
42
+ for a in r["annotations"]:
43
+ x,y,w,h = a["bbox"]
44
+ if w <= 0 or h <= 0: continue
45
+ anns.append({
46
+ "id": a["id"], "image_id": r["image_id"],
47
+ "category_id": self.category_id_to_label[a["category_id"]],
48
+ "bbox": [x,y,w,h], "area": float(a.get("area",w*h)),
49
+ "iscrowd": 0
50
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  encoded = self.processor(
52
  images=image,
53
+ annotations={"image_id": r["image_id"], "annotations": anns},
54
+ return_tensors="pt"
55
  )
 
 
56
  encoded["pixel_values"] = encoded["pixel_values"].squeeze(0)
57
  if "pixel_mask" in encoded:
58
  encoded["pixel_mask"] = encoded["pixel_mask"].squeeze(0)
59
  encoded["labels"] = encoded["labels"][0]
60
  return encoded
61
 
 
62
  def collate_fn(batch):
63
+ out = {"pixel_values": torch.stack([x["pixel_values"] for x in batch]),
64
+ "labels": [x["labels"] for x in batch]}
65
  if "pixel_mask" in batch[0]:
66
+ out["pixel_mask"] = torch.stack([x["pixel_mask"] for x in batch])
67
+ return out
 
 
 
 
 
 
68
 
69
  def evaluate(model, loader, device):
70
+ model.eval(); total=0; n=0
 
 
71
  with torch.no_grad():
72
  for batch in loader:
73
+ batch={k:(v.to(device) if torch.is_tensor(v) else v) for k,v in batch.items()}
74
+ total += float(model(**batch).loss.item()); n += 1
 
 
 
 
 
75
  model.train()
76
+ return total/max(n,1)
 
77
 
78
  def main():
79
+ p=argparse.ArgumentParser()
80
+ p.add_argument("--train-dir",required=True); p.add_argument("--val-dir",required=True)
81
+ p.add_argument("--classes",required=True); p.add_argument("--output-dir",default="model")
82
+ p.add_argument("--epochs",type=int,default=30); p.add_argument("--batch-size",type=int,default=2)
83
+ p.add_argument("--learning-rate",type=float,default=1e-5); p.add_argument("--weight-decay",type=float,default=1e-4)
84
+ p.add_argument("--num-workers",type=int,default=2)
85
+ a=p.parse_args()
86
+
87
+ classes=load_classes(a.classes)
88
+ id2label={i:n for i,n in enumerate(classes)}
89
+ label2id={n:i for i,n in enumerate(classes)}
90
+
91
+ proc=RTDetrImageProcessor.from_pretrained(BASE_MODEL)
92
+ train=COCODetectionDataset(Path(a.train_dir)/"images",Path(a.train_dir)/"annotations.json",proc)
93
+ val=COCODetectionDataset(Path(a.val_dir)/"images",Path(a.val_dir)/"annotations.json",proc)
94
+
95
+ if len(train)==0 or len(val)==0:
96
+ raise ValueError("Both train and validation splits must contain annotated images.")
97
+ if len(train.category_id_to_label)!=len(classes) or len(val.category_id_to_label)!=len(classes):
98
+ raise ValueError("COCO categories do not match classes.txt.")
99
+
100
+ model=RTDetrForObjectDetection.from_pretrained(
101
+ BASE_MODEL,num_labels=len(classes),id2label=id2label,label2id=label2id,
102
+ ignore_mismatched_sizes=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  )
104
+ device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
105
  model.to(device)
106
 
107
+ tr=DataLoader(train,batch_size=a.batch_size,shuffle=True,num_workers=a.num_workers,collate_fn=collate_fn)
108
+ va=DataLoader(val,batch_size=a.batch_size,shuffle=False,num_workers=a.num_workers,collate_fn=collate_fn)
109
+ opt=torch.optim.AdamW(model.parameters(),lr=a.learning_rate,weight_decay=a.weight_decay)
110
+
111
+ outdir=Path(a.output_dir); outdir.mkdir(parents=True,exist_ok=True)
112
+ best=float("inf")
113
+
114
+ for epoch in range(a.epochs):
115
+ model.train(); running=0
116
+ bar=tqdm(tr,desc=f"epoch {epoch+1}/{a.epochs}")
117
+ for step,batch in enumerate(bar):
118
+ batch={k:(v.to(device) if torch.is_tensor(v) else v) for k,v in batch.items()}
119
+ loss=model(**batch).loss
120
+ loss.backward(); opt.step(); opt.zero_grad(set_to_none=True)
121
+ running += float(loss.item())
122
+ bar.set_postfix(loss=f"{running/(step+1):.4f}")
123
+ vl=evaluate(model,va,device)
124
+ print(f"validation_loss={vl:.4f}")
125
+ if vl<best:
126
+ best=vl
127
+ model.save_pretrained(outdir)
128
+ proc.save_pretrained(outdir)
129
+ (outdir/"classes.json").write_text(json.dumps({"id2label":id2label,"label2id":label2id},indent=2))
130
+ model.save_pretrained(outdir); proc.save_pretrained(outdir)
131
+
132
+ if __name__=="__main__": main()