Chris Leo commited on
Commit
cb80d51
·
verified ·
1 Parent(s): 1da0020

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. training/train.py +140 -0
training/train.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Two-stage training recipe for Detect-crime.
3
+
4
+ Stage A (silver pretrain): warm a YOLOv11s/m on the full silver corpus
5
+ (Roboflow + COCO bat + king-distilled manako frames).
6
+ Stage B (clean fine-tune): short fine-tune on a manually-verified set of manako frames.
7
+
8
+ Defaults are tuned for a single GPU with >=24 GB VRAM. Pro_6000 should run YOLOv11s at
9
+ imgsz=1536 with batch 12 comfortably; YOLOv11m at imgsz=1280 needs batch ~8.
10
+
11
+ Usage:
12
+ # Stage A
13
+ python train.py --data ../data/data.yaml --weights yolo11s.pt --imgsz 1280 --stage A --epochs 200
14
+
15
+ # Stage B (clean fine-tune)
16
+ python train.py --data ../data_clean/data.yaml \
17
+ --weights runs/detect/crime_a/weights/best.pt \
18
+ --imgsz 1280 --stage B --epochs 50
19
+
20
+ Notes:
21
+ - Heavy mosaic+copy_paste is essential for crime — class imbalance is brutal (hoodie
22
+ dominates GT count; balaclava/glove/spray-paint are rare). Copy-paste pastes rare-class
23
+ crops into otherwise-normal frames; this is the most reliable fix for low recall on the
24
+ rare classes.
25
+ - HSV jitter cranked on V channel: CCTV crime footage spans daylight, IR night, and
26
+ mixed indoor lighting. Augment hard.
27
+ - `cls=1.0` (raised from default 0.5) because mAP@50 is per-class-averaged; pushing the
28
+ classification head matters more than localization here.
29
+ - Always save with ema=True so export uses smoothed weights.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import argparse
35
+
36
+ from ultralytics import YOLO
37
+
38
+
39
+ COMMON_AUG = dict(
40
+ hsv_h=0.02,
41
+ hsv_s=0.7,
42
+ hsv_v=0.5, # CCTV brightness varies massively (IR night, daylight, mixed indoor)
43
+ degrees=0.0, # CCTV cameras are mounted level
44
+ translate=0.10,
45
+ scale=0.5,
46
+ shear=0.0,
47
+ perspective=0.0,
48
+ flipud=0.0,
49
+ fliplr=0.5,
50
+ mosaic=1.0,
51
+ mixup=0.20,
52
+ copy_paste=0.50, # high: rebalance toward rare classes (balaclava, glove, spray paint)
53
+ erasing=0.0,
54
+ )
55
+
56
+ STAGE_A = dict(
57
+ epochs=200,
58
+ optimizer="AdamW",
59
+ lr0=2e-3,
60
+ lrf=0.01,
61
+ momentum=0.937,
62
+ weight_decay=5e-4,
63
+ warmup_epochs=3,
64
+ cos_lr=True,
65
+ label_smoothing=0.05,
66
+ cls=1.0, # higher cls weight: per-class AP matters
67
+ box=7.5,
68
+ dfl=1.5,
69
+ close_mosaic=15,
70
+ patience=40,
71
+ amp=True,
72
+ **COMMON_AUG,
73
+ )
74
+
75
+ STAGE_B = dict(
76
+ epochs=50,
77
+ optimizer="AdamW",
78
+ lr0=5e-4,
79
+ lrf=0.01,
80
+ momentum=0.9,
81
+ weight_decay=5e-4,
82
+ warmup_epochs=2,
83
+ cos_lr=True,
84
+ label_smoothing=0.05,
85
+ cls=0.8,
86
+ box=7.5,
87
+ dfl=1.5,
88
+ close_mosaic=10,
89
+ patience=15,
90
+ amp=True,
91
+ **{**COMMON_AUG, "mosaic": 0.5, "mixup": 0.0, "copy_paste": 0.10},
92
+ )
93
+
94
+
95
+ def main() -> int:
96
+ ap = argparse.ArgumentParser()
97
+ ap.add_argument("--data", required=True, help="path to data.yaml (YOLO format)")
98
+ ap.add_argument("--weights", default="yolo11s.pt",
99
+ help="starting weights (yolo11n.pt / yolo11s.pt / yolo11m.pt / your stage-A best)")
100
+ ap.add_argument("--imgsz", type=int, default=1280)
101
+ ap.add_argument("--batch", type=int, default=16)
102
+ ap.add_argument("--stage", choices=["A", "B"], default="A")
103
+ ap.add_argument("--epochs", type=int, default=None)
104
+ ap.add_argument("--device", default=0)
105
+ ap.add_argument("--project", default="runs/detect")
106
+ ap.add_argument("--name", default=None)
107
+ ap.add_argument("--resume", action="store_true")
108
+ args = ap.parse_args()
109
+
110
+ cfg = STAGE_A.copy() if args.stage == "A" else STAGE_B.copy()
111
+ if args.epochs is not None:
112
+ cfg["epochs"] = args.epochs
113
+
114
+ model = YOLO(args.weights)
115
+ name = args.name or f"crime_{args.stage.lower()}"
116
+
117
+ model.train(
118
+ data=args.data,
119
+ imgsz=args.imgsz,
120
+ batch=args.batch,
121
+ device=args.device,
122
+ project=args.project,
123
+ name=name,
124
+ resume=args.resume,
125
+ save=True,
126
+ plots=True,
127
+ seed=42,
128
+ rect=False,
129
+ single_cls=False,
130
+ **cfg,
131
+ )
132
+
133
+ metrics = model.val(data=args.data, imgsz=args.imgsz, batch=args.batch,
134
+ device=args.device, plots=True, save_json=True)
135
+ print(f"[train] {args.stage} done. map50={metrics.box.map50:.4f} map={metrics.box.map:.4f}")
136
+ return 0
137
+
138
+
139
+ if __name__ == "__main__":
140
+ raise SystemExit(main())