Fine-tuning
For a step-by-step run from a fresh machine, on an NVIDIA GPU or on an Intel machine with OpenVINO, see Training on CUDA and OpenVINO machines.
Requirements
Fine-tuning and validation require pycocotools. Install the train extras:
uv sync --extra train
Dataset format
nitid accepts either COCO JSON annotations or YOLO .txt labels for
detection and instance segmentation.
Semantic segmentation instead uses one dense class-ID PNG mask per image.
COCO JSON
Your dataset directory can look like:
my_dataset/
images/
train/
000001.jpg
...
val/
000100.jpg
...
annotations/
instances_train.json
instances_val.json
Annotation files follow the standard COCO detection format.
For task="segment", every object must also contain a COCO polygon or RLE
segmentation field.
YOLO .txt
Standard Ultralytics-style layout is also supported:
my_dataset/
images/
train/
val/
labels/
train/
val/
Split-first layouts are supported too:
my_dataset/
train/
images/
labels/
val/
images/
labels/
nitid detects the layout automatically and converts YOLO labels to cached COCO JSON internally for training and validation. The generated cache is stored in nitid's user cache directory rather than inside the dataset tree.
Detection rows use the usual box representation:
class x_center y_center width height
Instance-segmentation rows use normalized polygon points:
class x1 y1 x2 y2 x3 y3 ...
Bounding-box-only annotations are rejected when training a segmentation model, preventing an accidental all-zero mask training run.
Dense semantic masks
For task="semantic", image and mask paths mirror one another:
my_semantic_dataset/
images/
train/example.jpg
val/example.jpg
labels/
train/example.png
val/example.png
Each PNG must be a single-channel integer class map with the same dimensions as
its image. Pixel values are contiguous class IDs from 0 through nc - 1.
The configured ignore_index (255 by default) marks pixels excluded from loss
and validation metrics. Dense masks are always resized with nearest-neighbor
sampling; padding introduced by geometric augmentation is filled with the
ignore index.
path: /data/my_semantic_dataset
train: images/train
val: images/val
nc: 3
names:
0: background
1: road
2: vehicle
ignore_index: 255
When image and mask directories do not follow the mirrored images/labels
layout, set train_masks: and val_masks: explicitly. Semantic training does
not accept instance-only mosaic, mixup, classes, or single_cls options.
Data YAML
Point to your dataset with an ultralytics-style YAML:
# configs/datasets/my_dataset.yml
path: /data/my_dataset
train: images/train
val: images/val
nc: 3
names:
0: person
1: car
2: bicycle
When model.train(...) starts, nitid reads this taxonomy and automatically
rebuilds the class head when it differs from the checkpoint. Backbone and
localization weights are retained, the pretrained encoder scorer is converted
to generic objectness for proposal selection, and only taxonomy-specific class
heads are initialized from scratch. No manual head replacement is required.
nitid derives annotation file paths automatically:
<path>/annotations/instances_<split_dirname>.json
Override with explicit keys if your layout differs:
train_ann: annotations/my_train.json
val_ann: annotations/my_val.json
For YOLO datasets, use the same YAML shape and point train: / val: at the
image directories:
path: /data/my_dataset
train: images/train
val: images/val
names:
0: person
1: car
2: bicycle
or, for split-first layouts:
path: /data/my_dataset
train: train/images
val: val/images
names:
0: person
1: car
2: bicycle
Category ID mapping
COCO annotation files use arbitrary category_id integers (often starting at
1, or non-contiguous). nitid needs 0-based label indices for the model head.
By default it sorts the category_id values found in the annotation file and
maps them to 0, 1, 2, … in that order.
If your IDs are non-contiguous or in a custom order, supply an explicit mapping:
# annotation file has category_id 1 and 3 (no 2)
cat_ids:
1: 0 # category_id 1 → label index 0 (person)
3: 1 # category_id 3 → label index 1 (car)
Without this override, the auto-mapping would produce the same result for IDs
{1, 3} because they sort to [1, 3] → [0, 1]. The override is only
needed when you want a different ordering than sorted order.
Fine-tuning
COCO-mini example
The repository includes COCO-mini, a small COCO-format dataset for exercising
the full training path. It has 128 train and 32 val images, so it is a
smoke-test dataset, not an accuracy benchmark. The same dataset serves
task="detect" and task="segment", because COCO annotations carry both boxes
and instance masks.
from nitid import NITID
model = NITID("model1s", task="detect") # or task="segment"
metrics = model.train(
data="configs/datasets/coco-mini.yml",
epochs=5,
imgsz=640,
batch=8,
project="runs/train",
name="coco-mini",
)
print(metrics)
If datasets/coco-mini/ is missing, nitid downloads the archive declared in
the YAML's download key, checks it against download_sha256 and extracts it
before training. If the directory exists but a split is missing, nitid stops
with an error instead of overwriting it.
COCO-mini is built from COCO 2017 val with
tools/build_coco_mini.py,
keeping only images licensed CC BY 2.0, "No known copyright restrictions" or
"United States Government Work". The archive includes an ATTRIBUTION.md with
the source and licence of every image; the COCO annotations are CC BY 4.0.
The same example is available as a script:
uv run python examples/train_coco_mini.py --task detect
uv run python examples/train_coco_mini.py --task segment
and as a notebook that also validates, predicts and exports:
examples/notebooks/02_training.ipynb.
The equivalent CLI command is:
uv run nitid train \
model=model1s task=detect \
data=configs/datasets/coco-mini.yml \
epochs=5 \
imgsz=640 \
batch=8 \
project=runs/train \
name=coco-mini
Python API
from nitid import NITID
model = NITID("model1l", task="detect")
# The default recipe follows the task-native training path. For detection this
# uses D-FINE-style defaults, matching, losses, and augmentations.
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
recipe="default",
)
# DEIM is available for object detection only. It keeps nitid's dataset
# auto-detection/conversion path, then switches detection training to DEIM's
# MAL classification loss, flat-cosine schedule, Mosaic policy, and batch MixUp.
deim_metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
recipe="deim",
)
# Instance segmentation uses the same training API and mask-aware annotations.
segmenter = NITID("model1s", task="segment")
segment_metrics = segmenter.train(
data="configs/datasets/my_segment_dataset.yml",
epochs=50,
)
# segment_metrics["mask_mAP50"], segment_metrics["mask_mAP50-95"]
# Semantic models initialize shared features from the matching COCO
# instance-segmentation checkpoint. The dense classifiers train on your taxonomy.
semantic = NITID("model1s", task="semantic")
semantic_metrics = semantic.train(
data="configs/datasets/my_semantic_dataset.yml",
epochs=50,
)
# semantic_metrics["mIoU"], semantic_metrics["pixel_accuracy"]
def print_epoch_end(trainer):
row = trainer.current_row
if row is not None:
print(
f"epoch={row['epoch']} loss={row['loss']:.4f} "
f"mAP50-95={row['mAP50-95']:.4f}"
)
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
batch=16,
recipe="default",
lr0=1e-4,
lrf=0.01, # final lr = lr0 * lrf
cos_lr=True,
warmup_epochs=3,
warmup_momentum=0.8,
warmup_bias_lr=0.1,
optimizer="AdamW",
project="runs/train",
name="my_experiment",
callbacks={"on_train_epoch_end": print_epoch_end},
)
print(metrics)
# {
# "loss": 1.234,
# "fitness": 0.567,
# "mAP50": 0.612,
# "mAP50-95": 0.401,
# "history": [
# {
# "epoch": 1,
# "loss": 2.345,
# "loss_bbox": 0.321,
# "loss_giou": 0.654,
# "loss_vfl": 0.712,
# "loss_fgl": 0.889,
# "precision": 0.51,
# "recall": 0.47,
# "mAP50": 0.28,
# "mAP50-95": 0.14,
# ...
# },
# ...
# ],
# }
Training recipes
recipe="default" is the task-native mode and is available for detection,
instance segmentation, and semantic segmentation. For detection it
uses nitid's D-FINE training path: D-FINE matching and losses, D-FINE-style
detection augmentations, and model-size defaults when you omit values such as
epochs, batch, lr0, amp, or ema.
recipe="deim" is detection-only. It keeps nitid's normal dataset handling, so
COCO JSON, YOLO TXT, and automatic conversion still work through the same
dataset YAMLs. The recipe then switches detection training to DEIM's MAL
classification loss, flat-cosine learning-rate schedule, recipe Mosaic policy,
and batch-level MixUp. Calling it for instance or semantic segmentation raises a
clear error.
Explicit arguments always win over recipe defaults. For example, passing
mosaic=0.0 disables Mosaic even under recipe="deim", and passing batch=8
uses that batch size instead of the model-size recipe value.
With the default save_period=1, checkpoints are saved after every epoch to
runs/train/my_experiment/epoch{N}.pth. Use a larger interval or -1 to reduce
periodic checkpoint files; last.pth and best.pth remain available when
save=True.
Each checkpoint is a full nitid-wrapped .pth (config + names embedded) and can
be loaded directly with NITID("epoch50.pth", task="detect").
The top-level values are the final epoch summary for backward compatibility.
Use metrics["history"] to inspect per-epoch training and validation metrics,
including mAP50 and mAP50-95, from within Python.
Trainer callbacks
Training accepts an optional callbacks= argument for extending trainer
behavior without patching the core loop. Pass either:
- an object with one or more lifecycle-hook methods, each taking
trainer - a mapping from hook name to callable or list of callables, each taking
trainer
You can also register persistent callbacks on the model in an Ultralytics-like way:
def stop_after_first_epoch(trainer):
if trainer.current_epoch == 1:
trainer.stop = True
model.add_callback("on_train_epoch_end", stop_after_first_epoch)
Callbacks receive only the active DFINETrainer instance, for consistency
with the Ultralytics style. The most useful callback attributes are:
| Attribute | Type | Meaning | Available |
|---|---|---|---|
trainer.stop |
bool |
Set to True to request a clean stop |
all hooks |
trainer.current_epoch |
int |
1-based epoch number currently in progress | epoch / val / end hooks |
trainer.current_val_metrics |
dict \| None |
Latest validation metrics | on_val_end, on_train_epoch_end, on_train_end |
trainer.current_row |
dict \| None |
Latest finalized per-epoch metrics row | on_train_epoch_end, on_train_end |
trainer.current_fitness |
float |
Latest epoch fitness | on_train_epoch_end, on_train_end |
trainer.metrics |
dict \| None |
Final return value from training | on_train_end |
trainer.error |
BaseException \| None |
Exception that interrupted training | on_train_error |
trainer.history |
list[dict] |
Live per-epoch history accumulated so far | all hooks |
trainer.tracking_state |
dict[str, object] |
Tracker metadata persisted in last.pth |
all hooks |
trainer.save_dir |
Path \| None |
Run artifact directory | all hooks |
trainer.results_path |
Path \| None |
CSV metrics file path | all hooks |
trainer.optimizer / trainer.scheduler / trainer.criterion / trainer.scaler / trainer.ema_model |
runtime objects | Active training components | all hooks after setup |
trainer.dataloader |
dataloader | Active train dataloader | all hooks after setup |
trainer.train_args |
dict[str, object] |
Resolved train arguments for this run | all hooks |
trainer.start_epoch |
int |
Resume start epoch | all hooks |
These are live objects, not deep-copied snapshots. Reading them is safe and
expected. Mutating history, optimizer, scheduler, or similar attributes
changes the active training run.
Supported hooks:
on_train_starton_train_epoch_starton_val_endon_train_epoch_endon_train_endon_train_error
on_train_error runs when training exits with an exception. Cleanup callbacks
should use this hook; the original training exception is always re-raised.
Weights & Biases
Install the optional integration dependency:
pip install "nitid[wandb]"
# or, from a source checkout
uv sync --extra wandb
Enable logging directly from train(), in the same style as Ultralytics:
from nitid import NITID
model = NITID("model1s", task="detect")
model.train(
data="data.yaml",
epochs=50,
project="runs/train",
name="dfine-s-baseline",
wandb=True,
)
wandb=True uses the WandB project nitid and uses the training name as the
WandB run name. For additional WandB configuration, pass a mapping instead:
model.train(
data="data.yaml",
epochs=50,
name="dfine-s-baseline",
wandb={
"project": "nitid-detection",
"entity": "my-team",
"tags": ["dfine-s", "coco"],
},
)
The integration records the resolved training hyperparameters at run start and
logs the complete per-epoch row (total and component losses, learning rate,
validation metrics, timing, and memory). At successful completion it uploads
last.pth and best.pth once as versioned model artifacts and writes final
scalar metrics to the run summary. Set log_checkpoints=False if checkpoint
artifacts are not needed.
Periodic epoch artifacts are opt-in to avoid uploading several full model files after every epoch:
model.train(
data="data.yaml",
wandb={"checkpoint_interval": 10}, # also upload epoch10.pth, epoch20.pth, ...
)
The WandB run ID is stored in last.pth. Calling train(resume=True,
wandb=True) reconnects to that run with resume="allow", keeping the metrics
in one continuous WandB run. If training fails, nitid finishes the WandB run
with a failed exit status before re-raising the original exception.
For local testing without an account or network connection, use offline mode:
model.train(
data="data.yaml",
epochs=1,
wandb={"project": "nitid-local", "mode": "offline"},
)
Offline runs are stored in the local wandb/ directory and can be uploaded
later with wandb sync.
The callback API remains available when direct lifecycle control is useful:
from dfine.integrations import WandbCallback
tracker = WandbCallback(project="nitid-detection")
model.train(data="data.yaml", callbacks=tracker)
MLflow
Install the optional dependency:
pip install "nitid[mlflow]"
# or, from a source checkout
uv sync --extra mlflow
Enable MLflow directly on training:
from nitid import NITID
model = NITID("model1s", task="detect")
model.train(
data="data.yaml",
epochs=50,
project="runs/train",
name="dfine-s-baseline",
mlflow=True,
)
This follows the Ultralytics MLflow conventions:
- the tracking URI defaults to
runs/mlflow; a plain local path like this one is stored as a SQLite database (mlflow.db) with anartifacts/directory inside it, which works with both MLflow 2 and MLflow 3 - the experiment defaults to the training
project - the MLflow run name defaults to the training
name - an already-active MLflow run is reused and is not closed by nitid
- resolved training parameters are logged when training starts
- losses, learning rate, validation metrics, timing, and memory are logged each epoch
- checkpoints, CSV results, YAML files, and generated plots are logged at training end
- initialization and logging failures warn and disable tracking instead of stopping training
The same environment variables supported by Ultralytics take precedence over the defaults and Python options:
| Variable | Purpose |
|---|---|
MLFLOW_TRACKING_URI |
Local directory, or any MLflow URI (http://, sqlite://, databricks) passed through unchanged |
MLFLOW_EXPERIMENT_NAME |
Experiment name |
MLFLOW_RUN |
Run name |
MLFLOW_KEEP_RUN_ACTIVE |
Keep a nitid-created run open when set to 1, true, yes, on, y, or t (case-insensitive) |
For a fully local workflow, no server is required:
model.train(data="data.yaml", epochs=2, mlflow=True)
Inspect those results through the MLflow UI:
mlflow server --backend-store-uri sqlite:///runs/mlflow/mlflow.db
Then open http://127.0.0.1:5000. To use a different local store without
environment variables, pass an options mapping:
model.train(
data="data.yaml",
mlflow={
"tracking_uri": "runs/custom-mlflow",
"experiment_name": "nitid-detection",
"run_name": "dfine-s-baseline",
"keep_run_active": False,
"autolog": True,
},
)
The MLflow run ID is persisted in last.pth, so resume=True, mlflow=True
continues the same run. A training exception marks a nitid-created run as
failed. Advanced users may also pass MLflowCallback through callbacks=.
Example using a mapping:
def log_to_tracker(trainer):
row = trainer.current_row
if row is None:
return
tracker.log(
{
"epoch": row["epoch"],
"loss": row["loss"],
"mAP50": row["mAP50"],
"mAP50-95": row["mAP50-95"],
}
)
model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
callbacks={"on_train_epoch_end": log_to_tracker},
)
To stop training from a callback, set trainer.stop = True. The trainer checks
this flag at safe lifecycle boundaries and finalizes the run cleanly.
CLI
uv run nitid train \
model=model1l task=detect \
data=configs/datasets/my_dataset.yml \
epochs=50 \
batch=16
Hyperparameter reference
The table below documents the full public model.train(...) surface as it
exists today.
Use this table as the authoritative reference for train-time arguments.
| Parameter | Type | Default | Valid range / values | Description |
|---|---|---|---|---|
data |
str |
required | path to a dataset YAML | Ultralytics-style dataset config describing path, split locations, class count, and names. |
recipe |
str |
"default" |
"default", "deim" |
Training recipe. "default" uses each task's native path. "deim" is detection-only and enables DEIM's MAL loss, flat-cosine schedule, Mosaic policy, and batch MixUp while keeping nitid's dataset auto-detection/conversion. |
epochs |
int |
recipe/task dependent | >= 1 |
Number of full passes over the training set. Detection defaults follow official D-FINE/DEIM model-size recipes when omitted. |
imgsz |
int |
640 |
>= 1 |
Square training resolution applied during preprocessing. |
batch |
int |
recipe/task dependent | positive integer | Images per batch. Choose this explicitly for the available device memory. |
lr0 |
float |
recipe/task dependent | > 0 |
Initial learning rate passed to the optimizer. |
lrf |
float |
recipe/task dependent | > 0 |
Final learning-rate multiplier. Linear and cosine decay end at lr0 * lrf; DEIM uses it as the flat-cosine minimum-LR multiplier. |
cos_lr |
bool |
False |
True, False |
Switches the non-DEIM main schedule from linear decay to cosine decay. DEIM uses its own flat-cosine schedule. |
warmup_epochs |
float |
0.0 |
>= 0 |
Number of warmup epochs before the main LR schedule begins. Fractional values are allowed. |
warmup_momentum |
float |
0.8 |
typically 0 <= x <= 1 |
Starting momentum or Adam/AdamW beta1 used during warmup. It linearly ramps to the optimizer's target value. |
warmup_bias_lr |
float |
0.1 |
>= 0 |
Starting learning rate for bias parameters during warmup. Non-bias parameters warm up from 0.0. |
optimizer |
str |
"AdamW" |
"Auto", "Adam", "AdamW", "SGD", "RAdam", "NAdam", "RMSprop" |
Optimizer choice. The predictable Auto policy always selects AdamW. |
momentum |
float |
0.9 |
usually 0 <= x < 1 |
SGD momentum or beta1 for Adam-family optimizers. |
weight_decay |
float |
1e-4 |
>= 0 |
Weight decay applied to non-bias parameters. |
clip_grad |
float |
0.1 |
>= 0 |
Maximum gradient norm; 0 disables clipping. |
resume |
bool |
False |
True, False |
Restore the latest run state from project/name/last.pth. |
amp |
bool |
recipe/task dependent | True, False |
Enables mixed-precision training through torch.amp.autocast and GradScaler on CUDA devices. Official detection recipes enable AMP by default when omitted. |
ema |
bool |
recipe/task dependent | True, False |
Maintains an exponential moving average copy of the model and saves EMA weights in checkpoints. Official detection recipes enable EMA by default when omitted. |
ema_decay |
float |
0.9999 |
usually 0 < x < 1 |
Target EMA smoothing factor. The effective decay ramps over the first 1,000 optimizer updates so newly initialized custom heads are not stale during early validation. |
device |
str \| None |
None |
e.g. "cpu", "cuda", "cuda:0" |
Optional override for the training device. If omitted, training uses the device selected when the DFINE object was created. |
project |
str |
"runs/train" |
any writable path | Root directory for run artifacts such as checkpoints and metrics. |
name |
str |
"exp" |
any filesystem-friendly name | Run subdirectory created under project. |
save_dir |
str \| Path \| None |
None |
writable directory | Exact requested run directory; non-resume training still increments if it exists. |
exist_ok |
bool |
False |
True, False |
Existing training directories are reused only by resume=True. |
patience |
int |
100 |
>= 0 |
Stop after this many validated epochs without improvement; 0 disables early stopping. Final metrics report best_epoch. |
save |
bool |
True |
True, False |
Save last, best, and enabled periodic checkpoints. |
save_period |
int |
1 |
-1 or >= 1 |
Save epochN.pth every N epochs; -1 disables periodic files. |
val |
bool |
True |
True, False |
Enable validation during training. |
plots |
bool |
True |
True, False |
Save training-history and validation plots. |
val_period |
int |
1 |
>= 1 |
Validate every N epochs and at the final/time-limited epoch. |
workers |
int |
0 |
>= 0 |
DataLoader worker processes. |
cache |
bool \| str |
False |
False, True, "ram" |
Cache decoded training images in memory. |
seed |
int |
0 |
any integer | Seed Python, NumPy, PyTorch, dataset sampling, and DataLoader generators. |
deterministic |
bool |
True |
True, False |
Request deterministic PyTorch/cuDNN behavior where available. |
freeze |
int \| str \| list \| None |
None |
layer count, stage, substring, or glob | Freeze matching parameters before optimizer creation. |
classes |
list[int] \| None |
None |
valid class IDs | Keep annotations only for selected classes. |
single_cls |
bool |
False |
True, False |
Remap all retained annotations to class 0. |
fraction |
float |
1.0 |
(0, 1] |
Deterministically sample this fraction of training images. |
accumulate |
int |
1 |
>= 1 |
Accumulate gradients across batches before optimizer and EMA steps. |
multi_scale |
bool |
False |
True, False |
Randomly resize batches from roughly 0.5× to 1.5× imgsz, in multiples of 32. |
augment |
bool |
True |
True, False |
Enable recipe-aware box transforms for detection and task-native transforms for other tasks. |
fliplr |
float |
0.5 |
[0, 1] |
Probability of a horizontal flip. |
scale |
float |
0.5 |
[0, 1) |
Maximum random isotropic scale gain. |
translate |
float |
0.1 |
[0, 1] |
Maximum translation as a fraction of image width/height. |
crop |
float |
0.0 |
[0, 1] |
Crop probability and maximum fraction sampled independently from each edge. |
hsv_h |
float |
0.015 |
[0, 0.5] |
Hue jitter gain. |
hsv_s |
float |
0.7 |
[0, 1] |
Saturation jitter gain. |
hsv_v |
float |
0.4 |
[0, 1] |
Brightness/value jitter gain. |
mosaic |
float |
0.0 (0.5 in DEIM when omitted) |
[0, 1] |
Mosaic probability. Detection uses recipe-aware Mosaic; semantic segmentation does not support it. |
mixup |
float |
0.0 |
[0, 1] |
Dataset-level MixUp probability. DEIM detection additionally uses recipe-controlled batch MixUp; semantic segmentation does not support dataset-level MixUp. |
close_mosaic |
int |
10 |
>= 0 |
Disable mosaic for the final N epochs; zero keeps it active. |
time |
float \| None |
None |
positive hours or None |
Training duration in hours. When supplied, this overrides epochs as the loop's stopping limit. |
verbose |
bool |
True |
True, False |
Enables per-epoch console logging during training. |
callbacks |
object \| dict \| None |
None |
callback object or hook mapping | Optional lifecycle hooks for custom logging, experiment tracking, or other training-time integrations. |
wandb |
bool \| dict |
False |
True, False, or WandB options |
Enables the optional Weights & Biases integration. |
mlflow |
bool \| dict |
False |
True, False, or MLflow options |
Enables the optional Ultralytics-style MLflow integration. |
Detection recipes use a D-FINE/DEIM-style transform profile: photometric
distortion, zoom-out, IoU crop, horizontal flip, and final square resizing.
Mosaic is applied before that profile when enabled. Every geometric transform
operates on absolute xyxy boxes, clips them to the visible image, removes empty
boxes and labels, and only then converts targets to the normalized cxcywh
format expected by the model. The sample/epoch seed makes transform choices
independent of DataLoader worker scheduling. Zoom-out downscales high-resolution
images first, so its canvas's shorter side never exceeds ceil(imgsz / 0.3)
(2134 px at imgsz=640): the smallest IoU crop then still covers at least
imgsz pixels per axis, so no detail reaching the model is lost, and large photos
no longer produce canvases of up to 16x their pixel count.
recipe="deim" enables its recipe Mosaic default and batch-level MixUp for
detection. Without DEIM, Mosaic and dataset-level MixUp remain explicit opt-ins.
Mosaic and MixUp are not supported for semantic segmentation. All
resolved values above are written to args.yaml and restored from last.pth on
resume.
Resume training
nitid saves a last.pth checkpoint after every epoch. That checkpoint now
contains the full training state needed to continue an interrupted run:
- model weights used for training
- optimizer state
- scheduler state
- AMP scaler state when AMP is enabled
- EMA weights and decay when EMA is enabled
- per-epoch metrics history
To resume, keep the same project and name and set resume=True:
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=100, # new total target epoch count
resume=True,
project="runs/train",
name="my_experiment",
)
nitid restores state from runs/train/my_experiment/last.pth and continues at
the next epoch. All data, optimization, stopping, reproducibility, freezing,
filtering, accumulation, multi-scale, validation, plotting, and saving controls
in the table above are restored; conflicting values produce a warning.
epochs is deliberately controlled by the new invocation so a run can be
extended. Run location, verbosity, callbacks, and tracker enablement also
belong to the new invocation. If the checkpoint already reached or
exceeded the requested epochs, training does not run again and the saved
history is returned.
Cosine LR with warmup
By default, nitid uses linear LR decay. To match the more common Ultralytics fine-tuning setup, enable cosine decay and a warmup phase:
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
lr0=1e-4,
lrf=0.01,
cos_lr=True,
warmup_epochs=3,
warmup_momentum=0.8,
warmup_bias_lr=0.1,
)
With this configuration, nitid warms up per batch during the first
warmup_epochs epochs, using warmup_bias_lr for bias parameters and
0.0 for non-bias parameters, while momentum or Adam/AdamW beta1 ramps from
warmup_momentum to the optimizer's target value. After warmup, cosine decay
reduces the learning rate down to lr0 * lrf.
Interaction notes
amp=Trueis only active on CUDA. On CPU, nitid logs a warning and continues in FP32.ema_decayonly matters whenema=True.warmup_epochs=0disables warmup entirely.warmup_momentumaffects SGD momentum and Adam/AdamW beta1 during warmup.deviceintrain()overrides the device selected inNITID(...)for that training run only.momentum,weight_decay, andclip_gradexplicitly control optimizer momentum/beta1, non-bias regularization, and maximum gradient norm.
AMP — mixed-precision training
AMP uses torch.amp.autocast and GradScaler to run the forward pass in
FP16 while keeping the master weights in FP32. It typically cuts GPU memory
usage by ~40 % and speeds up training on modern NVIDIA GPUs.
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
batch=32, # larger batch fits in GPU memory with AMP
amp=True,
)
CPU fallback:
amp=Trueis silently ignored on CPU devices (a warning is logged). Training continues in full precision.
EMA — exponential moving average
EMA maintains a shadow copy of the model whose weights are updated after every optimiser step:
ema_weight = decay × ema_weight + (1 − decay) × model_weight
The EMA weights are what gets saved to the epoch checkpoint, so loading
NITID("epoch50.pth", task="detect") gives you the more stable EMA model directly.
Integer parameters (e.g. anchor indices) are copied verbatim rather than
blended.
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
ema=True,
ema_decay=0.9999, # standard value; lower = faster adaptation
)
AMP + EMA together
metrics = model.train(
data="configs/datasets/my_dataset.yml",
epochs=50,
batch=32,
amp=True,
ema=True,
)
Validation
Python API
metrics = model.val(
data="configs/datasets/my_dataset.yml",
imgsz=640,
split="val",
batch=16,
conf=0.001, # low threshold — include all detections in mAP computation
project="runs/val",
name="exp",
plots=True,
verbose=True,
)
print(metrics)
# {
# "mAP50-95": 0.412, # AP averaged over IoU 0.50:0.05:0.95
# "mAP50": 0.623, # AP at IoU=0.50
# "AR1": 0.341, # Average Recall at max 1 detection per image
# "AR100": 0.512, # Average Recall at max 100 detections per image
# "precision": 0.701,
# "recall": 0.655,
# "f1": 0.677,
# "per_class": [...],
# }
CLI
uv run nitid val \
model=model1l task=detect \
data=configs/datasets/my_dataset.yml \
conf=0.001
Notes
- The trainable model is not put into deploy mode during training or
inference.
predict()andexport()use a separate deployed copy, and that cache is rebuilt automatically after training or weight loading. - Loss weighting (
weight_dict) comes from the checkpoint's embedded D-FINE config so it stays consistent with the original pre-training setup. - When
ema=Truethe saved checkpoint contains EMA weights. Loading it withNITID(path, task="detect")gives you the EMA model directly — no extra step needed. - AMP is only active on CUDA; on CPU it degrades gracefully to full precision.