attr-eomt 1.1.3__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/PKG-INFO +178 -15
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/README.md +177 -14
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/__init__.py +1 -1
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/api.py +13 -5
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/aux_cls.py +56 -22
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/config.py +60 -1
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/data/coco.py +139 -24
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/data/transforms.py +117 -39
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/track.py +2 -1
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/train.py +242 -106
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/validate.py +97 -3
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/model.py +12 -3
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/plotting.py +41 -25
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/postprocess.py +28 -2
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/preprocess.py +32 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/reid.py +3 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/serialization.py +2 -1
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/tta.py +35 -17
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/visualize.py +5 -3
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/pyproject.toml +1 -1
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/train.py +38 -2
- attr_eomt-1.2.0/scripts/val.py +88 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_augment.py +26 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_recipe.py +22 -0
- attr_eomt-1.2.0/tests/test_regression_attr.py +236 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_smoke.py +14 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_tta.py +87 -0
- attr_eomt-1.1.3/scripts/val.py +0 -41
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/.github/workflows/pages.yml +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/.github/workflows/publish.yml +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/.github/workflows/release.yml +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/.github/workflows/tests.yml +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/.gitignore +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/LICENSE +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/configs/coco.yaml +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/docs/assets/00-hero-banner.png +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/docs/assets/00-hero-banner.svg +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/docs/examples/billie_eilish.jpg +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/docs/examples/rosalia.jpg +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/docs/examples/sample.jpg +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/docs/examples/taylor_swift.jpg +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/box_loss.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/compress.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/data/__init__.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/data/config.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/data/download.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/device.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/ema.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/__init__.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/distributed.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/match.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/engine/predict.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/eomt/loss.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/sample_data/README.md +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/sample_data/annotations/instances_train.json +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/sample_data/annotations/instances_val.json +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/sample_data/data.yaml +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/sample_data/images/train/.gitkeep +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/sample_data/images/val/.gitkeep +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/info.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/match.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/predict.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/push_to_hub.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/track.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/scripts/train_detect_coco.sh +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/site/index.html +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_api.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_architecture.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_compress.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_ddp.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_parity.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_reid.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tests/test_sweep.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tools/attr_tagger/README.md +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/tools/attr_tagger/tag.py +0 -0
- {attr_eomt-1.1.3 → attr_eomt-1.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: attr-eomt
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Standalone EoMT (Encoder-only Mask Transformer) for instance segmentation, with DINOv2 init, COCO training/validation and inference.
|
|
5
5
|
Project-URL: Homepage, https://github.com/imagra93/attr-eomt
|
|
6
6
|
Project-URL: Documentation, https://imagra93.github.io/attr-eomt
|
|
@@ -72,7 +72,7 @@ It's a clean-room, Apache-2.0 reimplementation: the weights you train are yours
|
|
|
72
72
|
from eomt import EoMT
|
|
73
73
|
|
|
74
74
|
model = EoMT("l") # fresh large model (DINOv2 backbone)
|
|
75
|
-
model.train(data="coco"
|
|
75
|
+
model.train(data="coco") # COCO 2017 auto-downloads if missing; 12 epochs, the official recipe
|
|
76
76
|
|
|
77
77
|
model = EoMT("runs/train/eomt-l") # reload a run — size/classes/heads auto-detected
|
|
78
78
|
model.predict("images/", plot=True) # render masks/boxes + per-instance attributes
|
|
@@ -206,14 +206,22 @@ simply reuse that same query→GT assignment and read the answer off the matched
|
|
|
206
206
|
query→GT assignment the detection loss used; the attribute is read *after* matching.
|
|
207
207
|
- **Gate.** An optional IoU gate drops barely-overlapping matched pairs (common early in
|
|
208
208
|
training) so attributes only learn from queries that actually localize the object.
|
|
209
|
-
- **Loss.** Cross-entropy per head over matched queries
|
|
209
|
+
- **Loss.** Cross-entropy per head over matched queries (a regression head: Huber on its
|
|
210
|
+
standardized value, see below), summed across heads and scaled
|
|
210
211
|
by `aux_w` (default `1.0`), added to the detector loss. Empty-match batches contribute
|
|
211
212
|
a graph-preserving zero, and missing labels use `ignore_index` and contribute nothing.
|
|
213
|
+
- **Balancing.** Both losses are O(1), so equal weights are a sane start. `aux_w_per_head`
|
|
214
|
+
weights each head, `aux_class_weights=True` re-weights rare classes (rare values of a
|
|
215
|
+
regression head, by smoothed density), and `aux_balance="uncertainty"` learns one weight
|
|
216
|
+
per head (Kendall et al. 2018) with the detector loss as the fixed anchor. Each epoch
|
|
217
|
+
prints every head's pull on the shared queries (its gradient norm over the detector's,
|
|
218
|
+
`train/aux_grad/<head>`): well above 1 a head steers the encoder, far below it only reads it.
|
|
212
219
|
- **Checkpoint selection is unchanged.** The attribute "rides along": its per-head
|
|
213
220
|
matched-query accuracy is shown live and written to `metrics.csv`, but never drives
|
|
214
221
|
`best.pt` (still `segm/mAP` or `bbox/mAP`).
|
|
215
222
|
- **Inference.** Each result attaches `aux = {head: {"ids", "probs"}}` for the kept
|
|
216
|
-
detections
|
|
223
|
+
detections (and `aux_values = {head: values}` in real units for the regression heads), and
|
|
224
|
+
`predict(plot=True)` renders each attribute next to the class label
|
|
217
225
|
using names stored in the checkpoint.
|
|
218
226
|
|
|
219
227
|
---
|
|
@@ -282,8 +290,76 @@ label set, in one model:
|
|
|
282
290
|
At inference a scoped head reports `ids = -1` ("not applicable") for detections whose class it
|
|
283
291
|
does not cover. The scope is stored in the checkpoint, so it survives reload.
|
|
284
292
|
|
|
293
|
+
**Flip-aware attributes.** A horizontal flip mirrors the photo but not the labels, so an attribute
|
|
294
|
+
that encodes a side (a left door shows up as a right one) is corrupted by `flip_prob > 0`. Give
|
|
295
|
+
its definition `flip_aware`, a map of category names to the ones they become on a horizontal
|
|
296
|
+
flip, and the training augmentation swaps them for every instance it flips (each pair swaps both
|
|
297
|
+
ways, so `{"left": "right"}` is enough; unlisted categories stay as they are):
|
|
298
|
+
|
|
299
|
+
```jsonc
|
|
300
|
+
{"name": "laterality", "categories": [{"id": 0, "name": "left"}, {"id": 1, "name": "center"}, {"id": 2, "name": "right"}],
|
|
301
|
+
"flip_aware": {"left": "right"}}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Mosaic, mixup and copy-paste flip each source image independently, and the swap follows each
|
|
305
|
+
instance. Only the horizontal flip is handled: keep `vflip_prob` and `rot90_prob` off for such an
|
|
306
|
+
attribute. It is categorical only. The mapping is stored in the checkpoint, and test-time augmentation with
|
|
307
|
+
`flip` swaps it back in each mirrored view's prediction before the views are merged (a checkpoint saved before the
|
|
308
|
+
mapping was stored takes it from the val JSON, with a warning). The training banner lists each head's swapped pairs. A
|
|
309
|
+
regression head needs no mapping: its value must not change under a flip (see below).
|
|
310
|
+
|
|
311
|
+
**Regression attributes.** `"type": "regression"` makes a head regress one real number per
|
|
312
|
+
instance instead of choosing a category; categorical and regression heads mix freely in one
|
|
313
|
+
dataset. `log` (regress `log1p` of the value, for heavy-tailed positives such as costs) and
|
|
314
|
+
`range` (valid values; predictions are clipped to it) are optional, and `applies_to` works as above:
|
|
315
|
+
|
|
316
|
+
```jsonc
|
|
317
|
+
"attributes": [
|
|
318
|
+
{"name": "severity", "categories": [...]},
|
|
319
|
+
{"name": "repair_cost", "type": "regression", "log": true, "range": [0, 20000], "applies_to": ["bumper"]}
|
|
320
|
+
]
|
|
321
|
+
// per annotation:
|
|
322
|
+
"attributes": {"severity": 3, "repair_cost": 812.5}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
A missing, non-numeric or out-of-range value is ignored. The head is trained on the value
|
|
326
|
+
standardized by the train split's mean and std (stored in the checkpoint), validated as the
|
|
327
|
+
mean absolute error in real units (`val/aux_mae/<head>`; `metrics.png` gives each regression
|
|
328
|
+
head its own MAE panel, `aux_per_class.png` its MAE per primary class), and predicted in real units
|
|
329
|
+
(`aux_values`). The value must not change under the augmentations (scale, crop, flip,
|
|
330
|
+
rotation): a cost or a score is fine, a pixel area or an angle is not. Tracking votes on the
|
|
331
|
+
categorical heads only, and re-identification cannot gate on a regression head.
|
|
332
|
+
|
|
333
|
+
A value that depends on how much of the instance is in view can say so with `crop`: the
|
|
334
|
+
training augmentation measures, per instance, the fraction it keeps in view (mask mass, or box
|
|
335
|
+
area, inside the crop window, after copy-paste occlusion), and the value follows it.
|
|
336
|
+
`"crop": "scale"` multiplies the value by that fraction — for an extensive quantity such as the
|
|
337
|
+
object's area in cm² (half the damage cropped away ⇒ half the area). `"crop": "ignore"` drops the
|
|
338
|
+
value when less than `min_visible` (default `0.9`) is kept — for a property of the whole object
|
|
339
|
+
that a cut view no longer shows. Validation and inference see whole images, so the head predicts
|
|
340
|
+
the full value there:
|
|
341
|
+
|
|
342
|
+
```jsonc
|
|
343
|
+
{"name": "damage_area", "type": "regression", "log": true, "crop": "scale"},
|
|
344
|
+
{"name": "part_length", "type": "regression", "crop": "ignore", "min_visible": 0.95}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
How the fraction is measured: for masks, the share of the instance's mask mass whose source pixels
|
|
348
|
+
the crop (and rotation / shear / perspective warp) lands inside the window: exact, so an instance
|
|
349
|
+
the window does not cut keeps exactly 1 (a warp alone is not a cut); copy-paste occlusion counts as
|
|
350
|
+
hidden, and instances brought in by mosaic, mixup or copy-paste are measured against their own crops. For boxes (detect family), the ratio of
|
|
351
|
+
the box areas: exact for a crop, approximate under rotation. Each call of the built-in
|
|
352
|
+
augmentation exposes it as `TrainAugment.last_info["visible"]`.
|
|
353
|
+
|
|
354
|
+
Limits: the head's mean / std and its `aux_class_weights` density come from the unscaled values,
|
|
355
|
+
so with `"scale"` the values trained on sit somewhat lower than those statistics. Only the visible
|
|
356
|
+
fraction is tracked, not the scale jitter, so a pixel area is still not supported. A custom
|
|
357
|
+
`transform=` gets no adjustment. `crop` is rejected on a categorical attribute, and it is a
|
|
358
|
+
training-only setting (not stored in the checkpoint). The training banner prints each head's crop
|
|
359
|
+
mode.
|
|
360
|
+
|
|
285
361
|
**Sidecar format (optional).** You can keep the COCO JSON as plain, standard COCO and put the
|
|
286
|
-
attributes beside it instead of inside it: an `attributes.yaml` schema in the dataset root plus
|
|
362
|
+
attributes beside it instead of inside it: an `attributes.yaml` schema (same keys, regression heads included) in the dataset root plus
|
|
287
363
|
`attributes/<split>.json` values keyed by annotation id (`{ann_id: {head: value}}`). If present
|
|
288
364
|
(and the JSON has no embedded `attributes`), it is merged in memory at load — so a plain COCO
|
|
289
365
|
dataset always works and the sidecar is picked up automatically when you add it. Embedded
|
|
@@ -406,7 +482,7 @@ size, normalization and any auxiliary heads are auto-detected from the `.pt`):
|
|
|
406
482
|
from eomt import EoMT
|
|
407
483
|
|
|
408
484
|
# Train on COCO 2017 (auto-downloaded on first run):
|
|
409
|
-
EoMT("l").train(data="coco",
|
|
485
|
+
EoMT("l").train(data="coco", batch=4) # 12 epochs, validated every 2: the official recipe
|
|
410
486
|
|
|
411
487
|
# ...or any COCO-format dataset (point at its data.yaml):
|
|
412
488
|
EoMT("s").train(data="sample_data/data.yaml", epochs=1, batch=1)
|
|
@@ -424,7 +500,7 @@ GPUs; the main process scores them and alone logs and writes checkpoints. The pr
|
|
|
424
500
|
argument must be picklable.
|
|
425
501
|
|
|
426
502
|
```python
|
|
427
|
-
EoMT("l", device="0,1").train(data="coco",
|
|
503
|
+
EoMT("l", device="0,1").train(data="coco", batch=2) # 2 per GPU x accum 4 x 2 GPUs = effective 16
|
|
428
504
|
```
|
|
429
505
|
|
|
430
506
|
**Test-time augmentation and tiled inference** (instance family). `predict()` and `val()` take `augment=True` (the
|
|
@@ -433,8 +509,9 @@ image at `imgsz` and zoomed to 1.5x `imgsz`) and `tiles=True` (overlapping tiles
|
|
|
433
509
|
mask IoU >= 0.5 measured where both views can see, so an object cut by a tile border is put back together; the mask
|
|
434
510
|
logits are averaged and the score is the mean over the views that could see the object (one that missed it votes 0).
|
|
435
511
|
Each takes a dict of overrides; see
|
|
436
|
-
[`eomt/tta.py`](eomt/tta.py). `augment={"flip": True}` also merges mirrored views
|
|
437
|
-
|
|
512
|
+
[`eomt/tta.py`](eomt/tta.py). `augment={"flip": True}` also merges mirrored views. Attributes marked `flip_aware` are swapped
|
|
513
|
+
back in a mirrored view; left / right *primary classes* are not (a mirrored view predicts them swapped), so keep `flip` off
|
|
514
|
+
for those, and for attributes that encode a side without being flip-aware.
|
|
438
515
|
|
|
439
516
|
```python
|
|
440
517
|
m = EoMT("runs/train/eomt-l")
|
|
@@ -443,7 +520,8 @@ m.val(data="data.yaml", tiles=True) # native til
|
|
|
443
520
|
m.predict("images/", augment={"flip": True}, tiles=1024) # 1x / 1.5x and mirrored; 1024 px tiles
|
|
444
521
|
```
|
|
445
522
|
|
|
446
|
-
`scripts/val.py` and `scripts/predict.py` take `--augment` and `--tiles [SIZE]
|
|
523
|
+
`scripts/val.py` and `scripts/predict.py` take `--augment` and `--tiles [SIZE]` (`scripts/val.py` also `--flip`; its arguments are
|
|
524
|
+
in [Command-line scripts](#command-line-scripts-arguments-and-things-to-take-into-account)). Each view is a forward pass: `augment`
|
|
447
525
|
costs ~3.5x one prediction (the 1.5x view has 2.25x the pixels); a 1280 x 960 photo at `imgsz` 644 makes 6 tiles + the
|
|
448
526
|
whole image.
|
|
449
527
|
|
|
@@ -462,11 +540,89 @@ LR, every step of the LR schedule, and the masked-attention annealing agree exac
|
|
|
462
540
|
| encoder LR | layer-wise decay `llrd=0.8`, `backbone_lr_mult=1.0`: the **top ViT block is at the full `lr0`** and each block below is 0.8× the one above; embeddings follow the first block, the final norm is at `lr0` |
|
|
463
541
|
| schedule | polynomial decay (`poly_power=0.9`) to 0 after a two-stage warmup in optimizer steps `warmup_steps=(500, 1000)`: the head warms up while the ViT LR is held at 0, then the ViT ramps up |
|
|
464
542
|
| masked attention | annealed block by block (`mask_anneal_start/end`, fractions of training: 1/6–2/6, 2/6–3/6, 3/6–4/6, 4/6–5/6, polynomial); the last sixth of training is mask-free |
|
|
543
|
+
| length | `epochs=12`, validated every `val_interval=2` epochs and after the last (official COCO instance recipe, about 89k optimizer steps at batch 16) |
|
|
465
544
|
|
|
466
545
|
In EoMT the last four transformer blocks are the ones that process the queries and predict the masks, so they must learn at
|
|
467
546
|
close to the head's speed: scaling the whole encoder by 0.1 (as detector-style recipes do for a backbone) starves them. The
|
|
468
|
-
LR decay and the annealing span `epochs
|
|
469
|
-
|
|
547
|
+
LR decay and the annealing span `epochs`.
|
|
548
|
+
|
|
549
|
+
**Validation follows the training regime**, as in the official EoMT: masked attention at the annealing probabilities of
|
|
550
|
+
the epoch's end. A model that still masks is far worse without its masks (part damage, ViT-L at epoch 11 of 50: 0.034
|
|
551
|
+
segm mAP mask-free against 0.114 masked), so a mask-free curve before the last sixth follows the annealing and jumps as each
|
|
552
|
+
block finishes it, not the model. `best.pt` is deployed mask-free, so it is only chosen once every block's annealing is
|
|
553
|
+
over (a run stopped earlier, e.g. with `stop_after_epochs`, has `last.pt` alone), and every checkpoint is saved mask-free.
|
|
554
|
+
Training prints the windows in epochs when it starts.
|
|
555
|
+
|
|
556
|
+
### Command-line scripts: arguments and things to take into account
|
|
557
|
+
|
|
558
|
+
**`scripts/train.py`.** Every flag is a keyword of `train()`; a flag left out keeps `train()`'s default.
|
|
559
|
+
|
|
560
|
+
| flag | `train()` keyword | what it does |
|
|
561
|
+
|---|---|---|
|
|
562
|
+
| `--data` | `data` | dataset YAML, or `coco` (auto-downloads) |
|
|
563
|
+
| `--size` / `--task` | | model size `s` `b` `l`; head family `instance` (masks) or `detect` (boxes) |
|
|
564
|
+
| `--epochs` | `epochs` | total epochs: the LR schedule and the masked-attention annealing span them |
|
|
565
|
+
| `--batch` / `--nominal-batch` | `batch` / `nominal_batch` | micro-batch **per GPU** / global effective batch (16); the difference is gradient accumulation |
|
|
566
|
+
| `--device` | `device` | `auto`, `cpu`, `cuda:N`, or `0,1` for several GPUs (one process each) |
|
|
567
|
+
| `--weights` / `--resume` | `init_weights` / `resume` | warm-start a fresh run from a checkpoint / continue a run. **One of the two, not both** |
|
|
568
|
+
| `--lr`, `--warmup HEAD,VIT` | `lr0`, `warmup_steps` | peak head LR (the encoder follows with layer-wise decay) and the two-stage warmup in optimizer steps |
|
|
569
|
+
| `--ema-decay`, `--ema-tau` | `ema_decay`, `ema_tau` | EMA of the weights (what is validated and exported as `best.pt`) |
|
|
570
|
+
| `--mask-free` | `mask_anneal_start/end` | no masked attention from step 0 (annealing windows 0 -> 0) |
|
|
571
|
+
| `--aux-dropout` | `aux_head_dropout` | dropout in the attribute heads |
|
|
572
|
+
| `--val-interval`, `--workers`, `--seed` | same | validate every N epochs (and after the last); data-loader workers per GPU; seed |
|
|
573
|
+
| `--aug KEY=VALUE`, `--aug-preset` | `aug` | augmentation overrides (see [Augmentation](#augmentation)) |
|
|
574
|
+
| `--fpn-scales`, `--box-head`, `--no-deep-supervision`, `--upscale-blocks`, `--iou-aware-cls`, `--quality-weight`, `--val-imgsz` | same | [architecture options](#architecture-options) |
|
|
575
|
+
|
|
576
|
+
Things to take into account when training:
|
|
577
|
+
|
|
578
|
+
- **Resume or warm start.** `--resume` continues a run exactly (epoch, optimizer, schedule, EMA, best metric) and re-reads the
|
|
579
|
+
architecture from the checkpoint: keep the other flags as they were, and the seed is ignored so the shuffle is not replayed.
|
|
580
|
+
`--weights` loads the weights only and starts a new run at epoch 0. Start from a `best.pt` (the EMA, saved mask-free); a
|
|
581
|
+
`last.pt` holds the live weights.
|
|
582
|
+
- **A short fine-tune needs its own recipe.** Lower `--lr`, shorten `--warmup`, and lower `--ema-decay` (at 0.9999 the EMA
|
|
583
|
+
averages over ~10k steps and barely leaves its starting point in a few epochs). If the model is deployed mask-free, pass
|
|
584
|
+
`--mask-free`: with the default annealing a warm-started model is trained *masked* for the first sixth of the run, a regime it
|
|
585
|
+
no longer runs in.
|
|
586
|
+
- **`best.pt` is only chosen mask-free.** With the default annealing that means from 5/6 of the schedule on; with `--mask-free`
|
|
587
|
+
every validation is eligible.
|
|
588
|
+
- **Validated epochs only.** With `--val-interval N` the other epochs have empty validation cells in `metrics.csv`; the curves in
|
|
589
|
+
`metrics.png` join the validated points.
|
|
590
|
+
- **Attribute heads follow the dataset's definitions.** Categories, `applies_to`, `flip_aware` and `crop` / zoom settings come
|
|
591
|
+
from the dataset (the training banner lists them). The flip mapping, scope and regression statistics are stored in the
|
|
592
|
+
checkpoint; `crop` and the zoom settings are training-only.
|
|
593
|
+
|
|
594
|
+
**`scripts/val.py`.**
|
|
595
|
+
|
|
596
|
+
| flag | what it does |
|
|
597
|
+
|---|---|
|
|
598
|
+
| `weights` | checkpoint `.pt` or a run / weights folder (`best.pt` preferred) |
|
|
599
|
+
| `--data`, `--batch`, `--workers`, `--device` | dataset YAML (`coco`), batch, data-loader workers, device |
|
|
600
|
+
| `--imgsz` | evaluation input size (default: the trained one; the positional embeddings are interpolated) |
|
|
601
|
+
| `--augment` | add the image at 1.5x `imgsz` |
|
|
602
|
+
| `--tiles [SIZE]` | overlapping tiles (default `imgsz` px, native resolution) plus the whole image |
|
|
603
|
+
| `--flip` | also run every view mirrored (alone: `--augment` with mirrored views) |
|
|
604
|
+
| `--no-attributes` | mAP only |
|
|
605
|
+
| `--ema`, `--mask annealed\|free` | validate the EMA weights of a `last.pt` (see below) |
|
|
606
|
+
|
|
607
|
+
Things to take into account when validating:
|
|
608
|
+
|
|
609
|
+
- **What is reported.** `segm/*` and `bbox/*` mAP and, for a model with secondary heads, `aux_acc/<head>` (accuracy) and
|
|
610
|
+
`aux_mae/<head>` (regression heads, mean absolute error in real units). They are scored on the same predictions as the mAP, so
|
|
611
|
+
they also work with `--augment`, `--tiles` and `--flip`.
|
|
612
|
+
- **The attribute population is the deployed one.** Each GT instance is matched to the kept detection of its class with the
|
|
613
|
+
highest mask IoU (one to one, IoU >= 0.5). The trainer's `val/aux_*` columns score matched *queries* instead, so the two differ
|
|
614
|
+
slightly: compare TTA against plain within the script's numbers, not against `metrics.csv`. A regression head's MAE is
|
|
615
|
+
dominated by its large values, so also look at a relative error before concluding.
|
|
616
|
+
- **Regime.** A model that is still annealing is only meaningful with its masks: `--ema --mask annealed` reproduces the trainer's
|
|
617
|
+
validation of a `last.pt` (EMA weights, the masked-attention probabilities of that epoch, read from the run's `args.yaml`),
|
|
618
|
+
`--ema --mask free` is the deployed regime, and `best.pt` is already the EMA, mask-free.
|
|
619
|
+
- **TTA is not free and not uniform.** It costs one forward pass per view and mostly helps small objects and strict-IoU mAP; the
|
|
620
|
+
classification heads often do not move. A regression head's prediction is *averaged over views at different zoom*, so a value
|
|
621
|
+
that depends on the object's apparent size can drift under TTA: check it against the plain run before relying on it.
|
|
622
|
+
- **`--flip` and the heads.** Heads the checkpoint stores as flip-aware are swapped back in mirrored views; an older checkpoint
|
|
623
|
+
takes the mapping from the dataset's JSON, with a warning. Left / right *primary classes* are not handled: keep `--flip` off for
|
|
624
|
+
them. The val categories of every head must match the model's, or the run stops instead of scoring against the wrong ids.
|
|
625
|
+
- **One process.** `scripts/val.py` runs on one device; the trainer's own validation uses all the GPUs.
|
|
470
626
|
|
|
471
627
|
### Architecture options
|
|
472
628
|
|
|
@@ -525,7 +681,7 @@ mass-preserving), so thin objects are not shredded into dots by nearest-neighbou
|
|
|
525
681
|
| geometry | `flip_prob` 0.5 · `vflip_prob` 0 · `rot90_prob` 0 · LSJ `min_scale`/`max_scale` 0.1–2.0 · `rotate_prob` 0.3 (±10°) · `shear_prob` 0.2 (±5°) · `perspective_prob` 0.15 |
|
|
526
682
|
| optics / sensor | colour jitter 1.0 · `gamma_prob` 0.3 · `grayscale_prob` 0.05 · `blur_prob` 0.2 · `noise_prob` 0.2 · `jpeg_prob` 0.3 · `glare_prob` 0.15 · `erasing_prob` 0.2 |
|
|
527
683
|
| multi-image (instance family) | `mosaic_prob` 0 · `mixup_prob` 0 · `copy_paste_prob` 0 |
|
|
528
|
-
| crop | `instance_crop_prob` 0 (instance-aware crop) · `instance_crop_empty_prob` 0 |
|
|
684
|
+
| crop | `instance_crop_prob` 0 (instance-aware crop) · `instance_crop_empty_prob` 0 · `zoom_prob` 0 (close-ups, `zoom_scale` 4–5x) |
|
|
529
685
|
| masks | `mask_resize="area"` (or `"nearest"`) |
|
|
530
686
|
|
|
531
687
|
Override per run, per dataset, or from the CLI — precedence is explicit keywords (`flip_prob`, `min_scale`,
|
|
@@ -553,12 +709,19 @@ python scripts/train.py --data my/data.yaml --aug min_scale=0.6 --aug instance_c
|
|
|
553
709
|
Things worth knowing:
|
|
554
710
|
|
|
555
711
|
- **Orientation.** Flips, 90° turns and rotations change the pixels but not the label. If an attribute encodes
|
|
556
|
-
orientation (e.g. a `viewpoint` head) set `flip_prob=0` and `rotate_prob=0
|
|
557
|
-
|
|
712
|
+
orientation (e.g. a `viewpoint` head) set `flip_prob=0` and `rotate_prob=0`, or, for a left / right attribute,
|
|
713
|
+
mark it `flip_aware` (see *Flip-aware attributes*) and keep the horizontal flip; `vflip_prob` / `rot90_prob` stay
|
|
714
|
+
off unless your scenes have no canonical "up".
|
|
558
715
|
- **Thin or tiny objects.** Large-Scale Jitter at 0.1 shrinks a 5 px crack to under a pixel; keep the effective
|
|
559
716
|
scale within ~[0.4, 1.0] (`min_scale = 0.4·long_side/imgsz`, `max_scale = 1.0·long_side/imgsz`). The instance-aware
|
|
560
717
|
crop pays off when the crop is much smaller than the image (high zoom, small `imgsz`), and
|
|
561
718
|
`instance_crop_rarity_attr` makes rare attribute values the crop's focus more often.
|
|
719
|
+
- **Close-ups.** `zoom_prob` makes an occasional sample a close-up: scaled by `s ~ U(zoom_scale)` (default 4–5x
|
|
720
|
+
the whole-image view, past the photo's own resolution) instead of the LSJ range, with the window on one instance
|
|
721
|
+
(whole when it fits, else a window inside it). `zoom_attr` restricts it to instances with a value of that
|
|
722
|
+
attribute and to the samples that have one, e.g. `{"zoom_prob": 0.1, "zoom_attr": "area"}` so a crop-scaled area
|
|
723
|
+
head also learns that a part filling the frame keeps its real area, and that a cut part keeps the share in view.
|
|
724
|
+
It resizes the whole photo first (~1.5x the time of a normal sample at 5x), so keep it occasional.
|
|
562
725
|
- **Multi-image ops** (`mosaic`, `mixup`, `copy_paste`) add instances from other samples and are *off*: they splice or
|
|
563
726
|
blend long thin structures, so try them deliberately and look at the result.
|
|
564
727
|
- **Negatives.** Images with no annotations are dropped from the train set by default. `train(keep_empty=True)` keeps
|
|
@@ -32,7 +32,7 @@ It's a clean-room, Apache-2.0 reimplementation: the weights you train are yours
|
|
|
32
32
|
from eomt import EoMT
|
|
33
33
|
|
|
34
34
|
model = EoMT("l") # fresh large model (DINOv2 backbone)
|
|
35
|
-
model.train(data="coco"
|
|
35
|
+
model.train(data="coco") # COCO 2017 auto-downloads if missing; 12 epochs, the official recipe
|
|
36
36
|
|
|
37
37
|
model = EoMT("runs/train/eomt-l") # reload a run — size/classes/heads auto-detected
|
|
38
38
|
model.predict("images/", plot=True) # render masks/boxes + per-instance attributes
|
|
@@ -166,14 +166,22 @@ simply reuse that same query→GT assignment and read the answer off the matched
|
|
|
166
166
|
query→GT assignment the detection loss used; the attribute is read *after* matching.
|
|
167
167
|
- **Gate.** An optional IoU gate drops barely-overlapping matched pairs (common early in
|
|
168
168
|
training) so attributes only learn from queries that actually localize the object.
|
|
169
|
-
- **Loss.** Cross-entropy per head over matched queries
|
|
169
|
+
- **Loss.** Cross-entropy per head over matched queries (a regression head: Huber on its
|
|
170
|
+
standardized value, see below), summed across heads and scaled
|
|
170
171
|
by `aux_w` (default `1.0`), added to the detector loss. Empty-match batches contribute
|
|
171
172
|
a graph-preserving zero, and missing labels use `ignore_index` and contribute nothing.
|
|
173
|
+
- **Balancing.** Both losses are O(1), so equal weights are a sane start. `aux_w_per_head`
|
|
174
|
+
weights each head, `aux_class_weights=True` re-weights rare classes (rare values of a
|
|
175
|
+
regression head, by smoothed density), and `aux_balance="uncertainty"` learns one weight
|
|
176
|
+
per head (Kendall et al. 2018) with the detector loss as the fixed anchor. Each epoch
|
|
177
|
+
prints every head's pull on the shared queries (its gradient norm over the detector's,
|
|
178
|
+
`train/aux_grad/<head>`): well above 1 a head steers the encoder, far below it only reads it.
|
|
172
179
|
- **Checkpoint selection is unchanged.** The attribute "rides along": its per-head
|
|
173
180
|
matched-query accuracy is shown live and written to `metrics.csv`, but never drives
|
|
174
181
|
`best.pt` (still `segm/mAP` or `bbox/mAP`).
|
|
175
182
|
- **Inference.** Each result attaches `aux = {head: {"ids", "probs"}}` for the kept
|
|
176
|
-
detections
|
|
183
|
+
detections (and `aux_values = {head: values}` in real units for the regression heads), and
|
|
184
|
+
`predict(plot=True)` renders each attribute next to the class label
|
|
177
185
|
using names stored in the checkpoint.
|
|
178
186
|
|
|
179
187
|
---
|
|
@@ -242,8 +250,76 @@ label set, in one model:
|
|
|
242
250
|
At inference a scoped head reports `ids = -1` ("not applicable") for detections whose class it
|
|
243
251
|
does not cover. The scope is stored in the checkpoint, so it survives reload.
|
|
244
252
|
|
|
253
|
+
**Flip-aware attributes.** A horizontal flip mirrors the photo but not the labels, so an attribute
|
|
254
|
+
that encodes a side (a left door shows up as a right one) is corrupted by `flip_prob > 0`. Give
|
|
255
|
+
its definition `flip_aware`, a map of category names to the ones they become on a horizontal
|
|
256
|
+
flip, and the training augmentation swaps them for every instance it flips (each pair swaps both
|
|
257
|
+
ways, so `{"left": "right"}` is enough; unlisted categories stay as they are):
|
|
258
|
+
|
|
259
|
+
```jsonc
|
|
260
|
+
{"name": "laterality", "categories": [{"id": 0, "name": "left"}, {"id": 1, "name": "center"}, {"id": 2, "name": "right"}],
|
|
261
|
+
"flip_aware": {"left": "right"}}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Mosaic, mixup and copy-paste flip each source image independently, and the swap follows each
|
|
265
|
+
instance. Only the horizontal flip is handled: keep `vflip_prob` and `rot90_prob` off for such an
|
|
266
|
+
attribute. It is categorical only. The mapping is stored in the checkpoint, and test-time augmentation with
|
|
267
|
+
`flip` swaps it back in each mirrored view's prediction before the views are merged (a checkpoint saved before the
|
|
268
|
+
mapping was stored takes it from the val JSON, with a warning). The training banner lists each head's swapped pairs. A
|
|
269
|
+
regression head needs no mapping: its value must not change under a flip (see below).
|
|
270
|
+
|
|
271
|
+
**Regression attributes.** `"type": "regression"` makes a head regress one real number per
|
|
272
|
+
instance instead of choosing a category; categorical and regression heads mix freely in one
|
|
273
|
+
dataset. `log` (regress `log1p` of the value, for heavy-tailed positives such as costs) and
|
|
274
|
+
`range` (valid values; predictions are clipped to it) are optional, and `applies_to` works as above:
|
|
275
|
+
|
|
276
|
+
```jsonc
|
|
277
|
+
"attributes": [
|
|
278
|
+
{"name": "severity", "categories": [...]},
|
|
279
|
+
{"name": "repair_cost", "type": "regression", "log": true, "range": [0, 20000], "applies_to": ["bumper"]}
|
|
280
|
+
]
|
|
281
|
+
// per annotation:
|
|
282
|
+
"attributes": {"severity": 3, "repair_cost": 812.5}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
A missing, non-numeric or out-of-range value is ignored. The head is trained on the value
|
|
286
|
+
standardized by the train split's mean and std (stored in the checkpoint), validated as the
|
|
287
|
+
mean absolute error in real units (`val/aux_mae/<head>`; `metrics.png` gives each regression
|
|
288
|
+
head its own MAE panel, `aux_per_class.png` its MAE per primary class), and predicted in real units
|
|
289
|
+
(`aux_values`). The value must not change under the augmentations (scale, crop, flip,
|
|
290
|
+
rotation): a cost or a score is fine, a pixel area or an angle is not. Tracking votes on the
|
|
291
|
+
categorical heads only, and re-identification cannot gate on a regression head.
|
|
292
|
+
|
|
293
|
+
A value that depends on how much of the instance is in view can say so with `crop`: the
|
|
294
|
+
training augmentation measures, per instance, the fraction it keeps in view (mask mass, or box
|
|
295
|
+
area, inside the crop window, after copy-paste occlusion), and the value follows it.
|
|
296
|
+
`"crop": "scale"` multiplies the value by that fraction — for an extensive quantity such as the
|
|
297
|
+
object's area in cm² (half the damage cropped away ⇒ half the area). `"crop": "ignore"` drops the
|
|
298
|
+
value when less than `min_visible` (default `0.9`) is kept — for a property of the whole object
|
|
299
|
+
that a cut view no longer shows. Validation and inference see whole images, so the head predicts
|
|
300
|
+
the full value there:
|
|
301
|
+
|
|
302
|
+
```jsonc
|
|
303
|
+
{"name": "damage_area", "type": "regression", "log": true, "crop": "scale"},
|
|
304
|
+
{"name": "part_length", "type": "regression", "crop": "ignore", "min_visible": 0.95}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
How the fraction is measured: for masks, the share of the instance's mask mass whose source pixels
|
|
308
|
+
the crop (and rotation / shear / perspective warp) lands inside the window: exact, so an instance
|
|
309
|
+
the window does not cut keeps exactly 1 (a warp alone is not a cut); copy-paste occlusion counts as
|
|
310
|
+
hidden, and instances brought in by mosaic, mixup or copy-paste are measured against their own crops. For boxes (detect family), the ratio of
|
|
311
|
+
the box areas: exact for a crop, approximate under rotation. Each call of the built-in
|
|
312
|
+
augmentation exposes it as `TrainAugment.last_info["visible"]`.
|
|
313
|
+
|
|
314
|
+
Limits: the head's mean / std and its `aux_class_weights` density come from the unscaled values,
|
|
315
|
+
so with `"scale"` the values trained on sit somewhat lower than those statistics. Only the visible
|
|
316
|
+
fraction is tracked, not the scale jitter, so a pixel area is still not supported. A custom
|
|
317
|
+
`transform=` gets no adjustment. `crop` is rejected on a categorical attribute, and it is a
|
|
318
|
+
training-only setting (not stored in the checkpoint). The training banner prints each head's crop
|
|
319
|
+
mode.
|
|
320
|
+
|
|
245
321
|
**Sidecar format (optional).** You can keep the COCO JSON as plain, standard COCO and put the
|
|
246
|
-
attributes beside it instead of inside it: an `attributes.yaml` schema in the dataset root plus
|
|
322
|
+
attributes beside it instead of inside it: an `attributes.yaml` schema (same keys, regression heads included) in the dataset root plus
|
|
247
323
|
`attributes/<split>.json` values keyed by annotation id (`{ann_id: {head: value}}`). If present
|
|
248
324
|
(and the JSON has no embedded `attributes`), it is merged in memory at load — so a plain COCO
|
|
249
325
|
dataset always works and the sidecar is picked up automatically when you add it. Embedded
|
|
@@ -366,7 +442,7 @@ size, normalization and any auxiliary heads are auto-detected from the `.pt`):
|
|
|
366
442
|
from eomt import EoMT
|
|
367
443
|
|
|
368
444
|
# Train on COCO 2017 (auto-downloaded on first run):
|
|
369
|
-
EoMT("l").train(data="coco",
|
|
445
|
+
EoMT("l").train(data="coco", batch=4) # 12 epochs, validated every 2: the official recipe
|
|
370
446
|
|
|
371
447
|
# ...or any COCO-format dataset (point at its data.yaml):
|
|
372
448
|
EoMT("s").train(data="sample_data/data.yaml", epochs=1, batch=1)
|
|
@@ -384,7 +460,7 @@ GPUs; the main process scores them and alone logs and writes checkpoints. The pr
|
|
|
384
460
|
argument must be picklable.
|
|
385
461
|
|
|
386
462
|
```python
|
|
387
|
-
EoMT("l", device="0,1").train(data="coco",
|
|
463
|
+
EoMT("l", device="0,1").train(data="coco", batch=2) # 2 per GPU x accum 4 x 2 GPUs = effective 16
|
|
388
464
|
```
|
|
389
465
|
|
|
390
466
|
**Test-time augmentation and tiled inference** (instance family). `predict()` and `val()` take `augment=True` (the
|
|
@@ -393,8 +469,9 @@ image at `imgsz` and zoomed to 1.5x `imgsz`) and `tiles=True` (overlapping tiles
|
|
|
393
469
|
mask IoU >= 0.5 measured where both views can see, so an object cut by a tile border is put back together; the mask
|
|
394
470
|
logits are averaged and the score is the mean over the views that could see the object (one that missed it votes 0).
|
|
395
471
|
Each takes a dict of overrides; see
|
|
396
|
-
[`eomt/tta.py`](eomt/tta.py). `augment={"flip": True}` also merges mirrored views
|
|
397
|
-
|
|
472
|
+
[`eomt/tta.py`](eomt/tta.py). `augment={"flip": True}` also merges mirrored views. Attributes marked `flip_aware` are swapped
|
|
473
|
+
back in a mirrored view; left / right *primary classes* are not (a mirrored view predicts them swapped), so keep `flip` off
|
|
474
|
+
for those, and for attributes that encode a side without being flip-aware.
|
|
398
475
|
|
|
399
476
|
```python
|
|
400
477
|
m = EoMT("runs/train/eomt-l")
|
|
@@ -403,7 +480,8 @@ m.val(data="data.yaml", tiles=True) # native til
|
|
|
403
480
|
m.predict("images/", augment={"flip": True}, tiles=1024) # 1x / 1.5x and mirrored; 1024 px tiles
|
|
404
481
|
```
|
|
405
482
|
|
|
406
|
-
`scripts/val.py` and `scripts/predict.py` take `--augment` and `--tiles [SIZE]
|
|
483
|
+
`scripts/val.py` and `scripts/predict.py` take `--augment` and `--tiles [SIZE]` (`scripts/val.py` also `--flip`; its arguments are
|
|
484
|
+
in [Command-line scripts](#command-line-scripts-arguments-and-things-to-take-into-account)). Each view is a forward pass: `augment`
|
|
407
485
|
costs ~3.5x one prediction (the 1.5x view has 2.25x the pixels); a 1280 x 960 photo at `imgsz` 644 makes 6 tiles + the
|
|
408
486
|
whole image.
|
|
409
487
|
|
|
@@ -422,11 +500,89 @@ LR, every step of the LR schedule, and the masked-attention annealing agree exac
|
|
|
422
500
|
| encoder LR | layer-wise decay `llrd=0.8`, `backbone_lr_mult=1.0`: the **top ViT block is at the full `lr0`** and each block below is 0.8× the one above; embeddings follow the first block, the final norm is at `lr0` |
|
|
423
501
|
| schedule | polynomial decay (`poly_power=0.9`) to 0 after a two-stage warmup in optimizer steps `warmup_steps=(500, 1000)`: the head warms up while the ViT LR is held at 0, then the ViT ramps up |
|
|
424
502
|
| masked attention | annealed block by block (`mask_anneal_start/end`, fractions of training: 1/6–2/6, 2/6–3/6, 3/6–4/6, 4/6–5/6, polynomial); the last sixth of training is mask-free |
|
|
503
|
+
| length | `epochs=12`, validated every `val_interval=2` epochs and after the last (official COCO instance recipe, about 89k optimizer steps at batch 16) |
|
|
425
504
|
|
|
426
505
|
In EoMT the last four transformer blocks are the ones that process the queries and predict the masks, so they must learn at
|
|
427
506
|
close to the head's speed: scaling the whole encoder by 0.1 (as detector-style recipes do for a backbone) starves them. The
|
|
428
|
-
LR decay and the annealing span `epochs
|
|
429
|
-
|
|
507
|
+
LR decay and the annealing span `epochs`.
|
|
508
|
+
|
|
509
|
+
**Validation follows the training regime**, as in the official EoMT: masked attention at the annealing probabilities of
|
|
510
|
+
the epoch's end. A model that still masks is far worse without its masks (part damage, ViT-L at epoch 11 of 50: 0.034
|
|
511
|
+
segm mAP mask-free against 0.114 masked), so a mask-free curve before the last sixth follows the annealing and jumps as each
|
|
512
|
+
block finishes it, not the model. `best.pt` is deployed mask-free, so it is only chosen once every block's annealing is
|
|
513
|
+
over (a run stopped earlier, e.g. with `stop_after_epochs`, has `last.pt` alone), and every checkpoint is saved mask-free.
|
|
514
|
+
Training prints the windows in epochs when it starts.
|
|
515
|
+
|
|
516
|
+
### Command-line scripts: arguments and things to take into account
|
|
517
|
+
|
|
518
|
+
**`scripts/train.py`.** Every flag is a keyword of `train()`; a flag left out keeps `train()`'s default.
|
|
519
|
+
|
|
520
|
+
| flag | `train()` keyword | what it does |
|
|
521
|
+
|---|---|---|
|
|
522
|
+
| `--data` | `data` | dataset YAML, or `coco` (auto-downloads) |
|
|
523
|
+
| `--size` / `--task` | | model size `s` `b` `l`; head family `instance` (masks) or `detect` (boxes) |
|
|
524
|
+
| `--epochs` | `epochs` | total epochs: the LR schedule and the masked-attention annealing span them |
|
|
525
|
+
| `--batch` / `--nominal-batch` | `batch` / `nominal_batch` | micro-batch **per GPU** / global effective batch (16); the difference is gradient accumulation |
|
|
526
|
+
| `--device` | `device` | `auto`, `cpu`, `cuda:N`, or `0,1` for several GPUs (one process each) |
|
|
527
|
+
| `--weights` / `--resume` | `init_weights` / `resume` | warm-start a fresh run from a checkpoint / continue a run. **One of the two, not both** |
|
|
528
|
+
| `--lr`, `--warmup HEAD,VIT` | `lr0`, `warmup_steps` | peak head LR (the encoder follows with layer-wise decay) and the two-stage warmup in optimizer steps |
|
|
529
|
+
| `--ema-decay`, `--ema-tau` | `ema_decay`, `ema_tau` | EMA of the weights (what is validated and exported as `best.pt`) |
|
|
530
|
+
| `--mask-free` | `mask_anneal_start/end` | no masked attention from step 0 (annealing windows 0 -> 0) |
|
|
531
|
+
| `--aux-dropout` | `aux_head_dropout` | dropout in the attribute heads |
|
|
532
|
+
| `--val-interval`, `--workers`, `--seed` | same | validate every N epochs (and after the last); data-loader workers per GPU; seed |
|
|
533
|
+
| `--aug KEY=VALUE`, `--aug-preset` | `aug` | augmentation overrides (see [Augmentation](#augmentation)) |
|
|
534
|
+
| `--fpn-scales`, `--box-head`, `--no-deep-supervision`, `--upscale-blocks`, `--iou-aware-cls`, `--quality-weight`, `--val-imgsz` | same | [architecture options](#architecture-options) |
|
|
535
|
+
|
|
536
|
+
Things to take into account when training:
|
|
537
|
+
|
|
538
|
+
- **Resume or warm start.** `--resume` continues a run exactly (epoch, optimizer, schedule, EMA, best metric) and re-reads the
|
|
539
|
+
architecture from the checkpoint: keep the other flags as they were, and the seed is ignored so the shuffle is not replayed.
|
|
540
|
+
`--weights` loads the weights only and starts a new run at epoch 0. Start from a `best.pt` (the EMA, saved mask-free); a
|
|
541
|
+
`last.pt` holds the live weights.
|
|
542
|
+
- **A short fine-tune needs its own recipe.** Lower `--lr`, shorten `--warmup`, and lower `--ema-decay` (at 0.9999 the EMA
|
|
543
|
+
averages over ~10k steps and barely leaves its starting point in a few epochs). If the model is deployed mask-free, pass
|
|
544
|
+
`--mask-free`: with the default annealing a warm-started model is trained *masked* for the first sixth of the run, a regime it
|
|
545
|
+
no longer runs in.
|
|
546
|
+
- **`best.pt` is only chosen mask-free.** With the default annealing that means from 5/6 of the schedule on; with `--mask-free`
|
|
547
|
+
every validation is eligible.
|
|
548
|
+
- **Validated epochs only.** With `--val-interval N` the other epochs have empty validation cells in `metrics.csv`; the curves in
|
|
549
|
+
`metrics.png` join the validated points.
|
|
550
|
+
- **Attribute heads follow the dataset's definitions.** Categories, `applies_to`, `flip_aware` and `crop` / zoom settings come
|
|
551
|
+
from the dataset (the training banner lists them). The flip mapping, scope and regression statistics are stored in the
|
|
552
|
+
checkpoint; `crop` and the zoom settings are training-only.
|
|
553
|
+
|
|
554
|
+
**`scripts/val.py`.**
|
|
555
|
+
|
|
556
|
+
| flag | what it does |
|
|
557
|
+
|---|---|
|
|
558
|
+
| `weights` | checkpoint `.pt` or a run / weights folder (`best.pt` preferred) |
|
|
559
|
+
| `--data`, `--batch`, `--workers`, `--device` | dataset YAML (`coco`), batch, data-loader workers, device |
|
|
560
|
+
| `--imgsz` | evaluation input size (default: the trained one; the positional embeddings are interpolated) |
|
|
561
|
+
| `--augment` | add the image at 1.5x `imgsz` |
|
|
562
|
+
| `--tiles [SIZE]` | overlapping tiles (default `imgsz` px, native resolution) plus the whole image |
|
|
563
|
+
| `--flip` | also run every view mirrored (alone: `--augment` with mirrored views) |
|
|
564
|
+
| `--no-attributes` | mAP only |
|
|
565
|
+
| `--ema`, `--mask annealed\|free` | validate the EMA weights of a `last.pt` (see below) |
|
|
566
|
+
|
|
567
|
+
Things to take into account when validating:
|
|
568
|
+
|
|
569
|
+
- **What is reported.** `segm/*` and `bbox/*` mAP and, for a model with secondary heads, `aux_acc/<head>` (accuracy) and
|
|
570
|
+
`aux_mae/<head>` (regression heads, mean absolute error in real units). They are scored on the same predictions as the mAP, so
|
|
571
|
+
they also work with `--augment`, `--tiles` and `--flip`.
|
|
572
|
+
- **The attribute population is the deployed one.** Each GT instance is matched to the kept detection of its class with the
|
|
573
|
+
highest mask IoU (one to one, IoU >= 0.5). The trainer's `val/aux_*` columns score matched *queries* instead, so the two differ
|
|
574
|
+
slightly: compare TTA against plain within the script's numbers, not against `metrics.csv`. A regression head's MAE is
|
|
575
|
+
dominated by its large values, so also look at a relative error before concluding.
|
|
576
|
+
- **Regime.** A model that is still annealing is only meaningful with its masks: `--ema --mask annealed` reproduces the trainer's
|
|
577
|
+
validation of a `last.pt` (EMA weights, the masked-attention probabilities of that epoch, read from the run's `args.yaml`),
|
|
578
|
+
`--ema --mask free` is the deployed regime, and `best.pt` is already the EMA, mask-free.
|
|
579
|
+
- **TTA is not free and not uniform.** It costs one forward pass per view and mostly helps small objects and strict-IoU mAP; the
|
|
580
|
+
classification heads often do not move. A regression head's prediction is *averaged over views at different zoom*, so a value
|
|
581
|
+
that depends on the object's apparent size can drift under TTA: check it against the plain run before relying on it.
|
|
582
|
+
- **`--flip` and the heads.** Heads the checkpoint stores as flip-aware are swapped back in mirrored views; an older checkpoint
|
|
583
|
+
takes the mapping from the dataset's JSON, with a warning. Left / right *primary classes* are not handled: keep `--flip` off for
|
|
584
|
+
them. The val categories of every head must match the model's, or the run stops instead of scoring against the wrong ids.
|
|
585
|
+
- **One process.** `scripts/val.py` runs on one device; the trainer's own validation uses all the GPUs.
|
|
430
586
|
|
|
431
587
|
### Architecture options
|
|
432
588
|
|
|
@@ -485,7 +641,7 @@ mass-preserving), so thin objects are not shredded into dots by nearest-neighbou
|
|
|
485
641
|
| geometry | `flip_prob` 0.5 · `vflip_prob` 0 · `rot90_prob` 0 · LSJ `min_scale`/`max_scale` 0.1–2.0 · `rotate_prob` 0.3 (±10°) · `shear_prob` 0.2 (±5°) · `perspective_prob` 0.15 |
|
|
486
642
|
| optics / sensor | colour jitter 1.0 · `gamma_prob` 0.3 · `grayscale_prob` 0.05 · `blur_prob` 0.2 · `noise_prob` 0.2 · `jpeg_prob` 0.3 · `glare_prob` 0.15 · `erasing_prob` 0.2 |
|
|
487
643
|
| multi-image (instance family) | `mosaic_prob` 0 · `mixup_prob` 0 · `copy_paste_prob` 0 |
|
|
488
|
-
| crop | `instance_crop_prob` 0 (instance-aware crop) · `instance_crop_empty_prob` 0 |
|
|
644
|
+
| crop | `instance_crop_prob` 0 (instance-aware crop) · `instance_crop_empty_prob` 0 · `zoom_prob` 0 (close-ups, `zoom_scale` 4–5x) |
|
|
489
645
|
| masks | `mask_resize="area"` (or `"nearest"`) |
|
|
490
646
|
|
|
491
647
|
Override per run, per dataset, or from the CLI — precedence is explicit keywords (`flip_prob`, `min_scale`,
|
|
@@ -513,12 +669,19 @@ python scripts/train.py --data my/data.yaml --aug min_scale=0.6 --aug instance_c
|
|
|
513
669
|
Things worth knowing:
|
|
514
670
|
|
|
515
671
|
- **Orientation.** Flips, 90° turns and rotations change the pixels but not the label. If an attribute encodes
|
|
516
|
-
orientation (e.g. a `viewpoint` head) set `flip_prob=0` and `rotate_prob=0
|
|
517
|
-
|
|
672
|
+
orientation (e.g. a `viewpoint` head) set `flip_prob=0` and `rotate_prob=0`, or, for a left / right attribute,
|
|
673
|
+
mark it `flip_aware` (see *Flip-aware attributes*) and keep the horizontal flip; `vflip_prob` / `rot90_prob` stay
|
|
674
|
+
off unless your scenes have no canonical "up".
|
|
518
675
|
- **Thin or tiny objects.** Large-Scale Jitter at 0.1 shrinks a 5 px crack to under a pixel; keep the effective
|
|
519
676
|
scale within ~[0.4, 1.0] (`min_scale = 0.4·long_side/imgsz`, `max_scale = 1.0·long_side/imgsz`). The instance-aware
|
|
520
677
|
crop pays off when the crop is much smaller than the image (high zoom, small `imgsz`), and
|
|
521
678
|
`instance_crop_rarity_attr` makes rare attribute values the crop's focus more often.
|
|
679
|
+
- **Close-ups.** `zoom_prob` makes an occasional sample a close-up: scaled by `s ~ U(zoom_scale)` (default 4–5x
|
|
680
|
+
the whole-image view, past the photo's own resolution) instead of the LSJ range, with the window on one instance
|
|
681
|
+
(whole when it fits, else a window inside it). `zoom_attr` restricts it to instances with a value of that
|
|
682
|
+
attribute and to the samples that have one, e.g. `{"zoom_prob": 0.1, "zoom_attr": "area"}` so a crop-scaled area
|
|
683
|
+
head also learns that a part filling the frame keeps its real area, and that a cut part keeps the share in view.
|
|
684
|
+
It resizes the whole photo first (~1.5x the time of a normal sample at 5x), so keep it occasional.
|
|
522
685
|
- **Multi-image ops** (`mosaic`, `mixup`, `copy_paste`) add instances from other samples and are *off*: they splice or
|
|
523
686
|
blend long thin structures, so try them deliberately and look at the result.
|
|
524
687
|
- **Negatives.** Images with no annotations are dropped from the train set by default. `train(keep_empty=True)` keeps
|
|
@@ -4,7 +4,7 @@ Public API:
|
|
|
4
4
|
|
|
5
5
|
>>> from eomt import EoMT
|
|
6
6
|
>>> model = EoMT("l") # fresh model (DINOv2 backbone)
|
|
7
|
-
>>> model.train(data="coco"
|
|
7
|
+
>>> model.train(data="coco") # COCO auto-downloads if missing; 12 epochs, the official recipe
|
|
8
8
|
>>> model = EoMT("runs/train/eomt-l") # reload a run (everything auto-detected)
|
|
9
9
|
>>> results = model.predict("images/", plot=True)
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from eomt import EoMT
|
|
5
5
|
|
|
6
6
|
model = EoMT("l") # fresh large model (DINOv2 backbone)
|
|
7
|
-
model.train(data="coco"
|
|
7
|
+
model.train(data="coco") # COCO auto-downloads if missing; 12 epochs, the official recipe
|
|
8
8
|
|
|
9
9
|
model = EoMT("runs/train/eomt-l") # reload a run (everything auto-detected)
|
|
10
10
|
model.val(data="coco")
|
|
@@ -286,7 +286,8 @@ class EoMT:
|
|
|
286
286
|
# -------------------------------------------------------------------- val
|
|
287
287
|
def val(self, data: str | Path = "coco", *, batch: int = 4, workers: int = 4,
|
|
288
288
|
conf_thres: float = 0.0, max_det: int = 100, letterbox: bool | None = None,
|
|
289
|
-
imgsz: int | None = None,
|
|
289
|
+
imgsz: int | None = None, augment: bool | dict = False, tiles: bool | int | dict = False,
|
|
290
|
+
attributes: bool = True, **kw) -> dict:
|
|
290
291
|
"""Evaluate on a dataset's val split, returning COCO mAP metrics.
|
|
291
292
|
|
|
292
293
|
Segmentation models report ``segm/*`` (+ ``bbox/*``); detection
|
|
@@ -294,6 +295,11 @@ class EoMT:
|
|
|
294
295
|
evaluation image size (defaults to the trained size); the model interpolates
|
|
295
296
|
its positional embeddings to the new resolution, so any multiple of the patch
|
|
296
297
|
size works.
|
|
298
|
+
|
|
299
|
+
``augment`` / ``tiles`` evaluate with test-time augmentation / tiled inference (instance family; see
|
|
300
|
+
:mod:`eomt.tta`; a head marked ``flip_aware`` is swapped back in mirrored views). For a segmentation model with
|
|
301
|
+
secondary heads, ``attributes`` (default on) adds ``aux_acc/<head>`` (accuracy) and ``aux_mae/<head>`` (regression
|
|
302
|
+
heads, real units), scored on the same predictions as the mAP; see :func:`eomt.engine.validate.evaluate`.
|
|
297
303
|
"""
|
|
298
304
|
cfg = _resolve_data(data)
|
|
299
305
|
if not (cfg["val_images"] and cfg["val_json"]):
|
|
@@ -304,12 +310,14 @@ class EoMT:
|
|
|
304
310
|
lb = letterbox if letterbox is not None else bool(getattr(model, "preprocess_letterbox", False))
|
|
305
311
|
val_ds = CocoValImages(
|
|
306
312
|
cfg["val_images"], cfg["val_json"], imgsz=_resolve_imgsz(imgsz, model), letterbox=lb,
|
|
307
|
-
mean=getattr(model, "pixel_mean", None), std=getattr(model, "pixel_std", None),
|
|
313
|
+
mean=getattr(model, "pixel_mean", None), std=getattr(model, "pixel_std", None), attributes=attributes,
|
|
308
314
|
)
|
|
309
|
-
|
|
315
|
+
detect = getattr(model, "family", "instance") == "detect"
|
|
316
|
+
eval_fn = _evaluate_detection if detect else _evaluate
|
|
310
317
|
return eval_fn(
|
|
311
318
|
model, val_ds, device=dev, batch_size=batch, num_workers=workers,
|
|
312
|
-
conf_thres=conf_thres, max_det=max_det,
|
|
319
|
+
conf_thres=conf_thres, max_det=max_det, augment=augment, tiles=tiles,
|
|
320
|
+
**({} if detect else {"attributes": attributes}), **kw,
|
|
313
321
|
)
|
|
314
322
|
|
|
315
323
|
# -------------------------------------------------------------- compress
|