Training Module¶
The training module provides efficient training loops with modern PyTorch features.
Overview¶
The oriented_det.train module provides:
train_one_epoch- Single epoch trainingtrain- Full training loopevaluate- Validation/evaluationCheckpointManager- Automatic checkpointingMetricTracker- Metric aggregation
Features¶
- ✅ Mixed precision training (AMP)
- ✅ Gradient accumulation
- ✅ Gradient clipping
- ✅ Automatic checkpointing
- ✅ Metric tracking
- ✅ Robust error handling
Basic Training¶
Single Epoch¶
from oriented_det.train import train_one_epoch
from torch.optim import AdamW
optimizer = AdamW(model.parameters(), lr=1e-4)
metrics = train_one_epoch(
model=model,
data_loader=train_loader,
optimizer=optimizer,
device=device,
epoch=0,
use_amp=True,
gradient_accumulation_steps=4,
)
Full Training Loop¶
from oriented_det.train import train, CheckpointManager
checkpoint_manager = CheckpointManager(
"checkpoints/",
best_metric="mAP",
higher_is_better=True
)
history = train(
model=model,
train_loader=train_loader,
optimizer=optimizer,
device=device,
num_epochs=12,
val_loader=val_loader,
checkpoint_manager=checkpoint_manager,
use_amp=True,
gradient_accumulation_steps=4,
max_grad_norm=1.0,
)
Checkpointing¶
CheckpointManager¶
from oriented_det.train import CheckpointManager
manager = CheckpointManager(
save_dir="checkpoints/",
best_metric="mAP",
higher_is_better=True,
save_freq=1 # Save every epoch
)
# Save checkpoint
manager.save_checkpoint(
model=model,
optimizer=optimizer,
epoch=5,
metrics={"mAP": 0.75}
)
# Load checkpoint
state = manager.load_checkpoint("checkpoints/best_model.pth")
model.load_state_dict(state["model"])
Metric Tracking¶
MetricTracker¶
from oriented_det.train import MetricTracker
tracker = MetricTracker()
# Update metrics
tracker.update({"loss": 0.5, "mAP": 0.75})
# Get averages
avg_metrics = tracker.average()
print(f"Average loss: {avg_metrics['loss']}")
# Reset
tracker.reset()
Advanced Options¶
Mixed Precision¶
metrics = train_one_epoch(
model=model,
data_loader=train_loader,
optimizer=optimizer,
device=device,
use_amp=True, # Enable automatic mixed precision
)
Benefits: - ~2x faster training - Lower memory usage - No accuracy loss observed in practice
Gradient Accumulation¶
Train with large effective batch sizes:
metrics = train_one_epoch(
model=model,
data_loader=train_loader,
optimizer=optimizer,
device=device,
gradient_accumulation_steps=4, # Effective batch size = 4 * batch_size
)
Use Case: When GPU memory is limited but you want a larger effective batch size for stable training.
Gradient Clipping¶
Prevent exploding gradients:
metrics = train_one_epoch(
model=model,
data_loader=train_loader,
optimizer=optimizer,
device=device,
max_grad_norm=1.0, # Clip gradients to norm 1.0
)
When to Use: If losses are oscillating wildly or training is unstable.
Loss Weighting¶
Weight different loss components:
metrics = train_one_epoch(
model=model,
data_loader=train_loader,
optimizer=optimizer,
device=device,
loss_weights={
"loss_objectness": 1.0,
"loss_rpn_box_reg": 1.0,
"loss_classifier": 1.0,
"loss_box_reg": 1.0,
},
)
Note: All losses have equal weight (1.0) by default. Curriculum learning (gradual classifier weight increase) was removed because it caused models to collapse to single-class predictions in early training.
TensorBoard Logging¶
TensorBoard logging is integrated on top of the existing MetricTracker. To enable it, pass a SummaryWriter to the training functions:
from torch.utils.tensorboard import SummaryWriter
from oriented_det.train import train
# Create TensorBoard writer
writer = SummaryWriter(log_dir="runs/experiment_1")
# Train with TensorBoard logging
history = train(
model=model,
train_loader=train_loader,
optimizer=optimizer,
device=device,
num_epochs=12,
val_loader=val_loader,
writer=writer, # Enable TensorBoard logging
)
# View logs with: tensorboard --logdir runs/experiment_1
Logged Metrics:
- Per-step metrics (during training): train/{loss_name}, train/learning_rate
- Per-epoch metrics (after each epoch): train_epoch/{metric_name}, val/{metric_name}
- Prediction images (during validation): val/predictions - Shows up to 4 validation images with predictions (red boxes) and ground truth (green boxes) overlaid; the source image filename (basename with extension) is drawn at the top-left when the dataloader provides it
Performance Profiling¶
Use PyTorch Profiler to identify training bottlenecks and optimize performance:
from oriented_det.train.profiler import TrainingProfiler
from torch.profiler import ProfilerActivity, schedule
# Create profiler
profiler = TrainingProfiler(
log_dir="runs/profiling",
activities=[ProfilerActivity.CUDA, ProfilerActivity.CPU],
schedule=schedule(wait=1, warmup=1, active=3, repeat=1),
record_shapes=True,
profile_memory=True,
)
# Profile a few training steps
model.train()
with profiler:
for i, (images, targets) in enumerate(train_loader):
if i >= 5: # Profile first 5 batches
break
# ... training code ...
profiler.step()
# Print summary
profiler.print_summary(sort_by="cuda_time_total", row_limit=30)
# View detailed trace in TensorBoard
# tensorboard --logdir runs/profiling
What to Look For:
- Data loading bottlenecks: If DataLoader operations take significant time, increase num_workers
- GPU utilization: If GPU kernels are idle, increase batch_size or enable mixed precision
- Memory issues: Check memory profiling to identify memory-intensive operations
- Slow operations: Look for operations with high cuda_time_total or cpu_time_total
Common Optimizations:
- Increase num_workers if data loading is slow (try 4-8 workers)
- Increase batch_size if GPU utilization is low (<80%)
- Use mixed precision (AMP) if not already enabled
- Profile memory to identify memory bottlenecks
- Use pin_memory=True in DataLoader for faster CPU→GPU transfers
Learning Rate Scheduling¶
WarmupScheduler¶
The WarmupScheduler provides learning rate warmup that gradually increases the learning rate from 0 to the base learning rate over a specified number of optimizer steps. This helps stabilize training in the early stages, especially when using large learning rates or training from scratch.
When to use warmup: - Training from scratch (no pretrained weights) - Using large learning rates (>0.01) - Training is unstable in early epochs - Fine-tuning with large learning rate changes
Basic usage:
from oriented_det.train import WarmupScheduler
from torch.optim import SGD, lr_scheduler
# Create optimizer with base learning rate
optimizer = SGD(model.parameters(), lr=0.01)
# Create base scheduler (e.g., StepLR)
base_scheduler = lr_scheduler.StepLR(optimizer, step_size=8, gamma=0.1)
# Wrap with warmup scheduler
warmup_scheduler = WarmupScheduler(
optimizer=optimizer,
base_scheduler=base_scheduler,
warmup_steps=500, # Warmup for 500 optimizer steps
)
# Training loop
for epoch in range(num_epochs):
for batch_idx, (images, targets) in enumerate(train_loader):
# ... training step ...
optimizer.step()
warmup_scheduler.step() # Step per optimizer step during warmup
warmup_scheduler.step_epoch() # Step base scheduler per epoch
How it works:
- During warmup: Learning rate increases linearly from 0 to base LR over warmup_steps
- After warmup: Uses the base scheduler (e.g., StepLR, CosineAnnealingLR)
- step(): Called after each optimizer.step() during warmup
- step_epoch(): Called after each epoch to advance the base scheduler
Example with CosineAnnealingLR:
from oriented_det.train import WarmupScheduler
from torch.optim import AdamW, lr_scheduler
optimizer = AdamW(model.parameters(), lr=1e-4)
# Cosine annealing scheduler
base_scheduler = lr_scheduler.CosineAnnealingLR(
optimizer,
T_max=num_epochs,
eta_min=1e-6
)
# Warmup for 5% of total steps
total_steps = len(train_loader) * num_epochs
warmup_steps = int(0.05 * total_steps) # 5% warmup
warmup_scheduler = WarmupScheduler(
optimizer=optimizer,
base_scheduler=base_scheduler,
warmup_steps=warmup_steps,
)
# Training loop
for epoch in range(num_epochs):
for batch_idx, (images, targets) in enumerate(train_loader):
# ... training step ...
optimizer.step()
warmup_scheduler.step() # Per-step during warmup
warmup_scheduler.step_epoch() # Per-epoch for base scheduler
Calculating warmup steps:
A common approach is to use 5-10% of total training steps:
total_steps = len(train_loader) * num_epochs
warmup_steps = int(0.05 * total_steps) # 5% warmup
# Or
warmup_steps = int(0.10 * total_steps) # 10% warmup
For example: - 1000 batches per epoch × 12 epochs = 12,000 total steps - 5% warmup = 600 steps (~0.6 epochs) - 10% warmup = 1,200 steps (~1.2 epochs)
Best practices: - Start with 5% warmup (conservative) - Increase to 10% if training is still unstable - Use warmup when learning rate > 0.01 - Monitor learning rate curve in TensorBoard to verify warmup behavior - After warmup, base scheduler takes over (e.g., StepLR, CosineAnnealingLR)
Visualization:
The learning rate schedule looks like:
LR
↑
| ╱───────╲ ← Cosine annealing (after warmup)
| ╱ ╲
| ╱ ╲
| ╱ ╲
| ╱ ╲
|╱ ╲
|───────────────────→ Steps
↑
Warmup (linear increase)
Scheduler types (config-driven)¶
When using tools/train.py with a JSON config, you can choose the LR scheduler via training.lr_scheduler_type. The following types are supported in addition to the default MultiStepLR / StepLR (with optional warmup):
lr_scheduler_type |
Scheduler | Stepping | Typical use |
|---|---|---|---|
(none) / multistep / step |
MultiStepLR or StepLR | Once per epoch | Default; use lr_scheduler_milestones for MultiStepLR. |
reduce_on_plateau |
ReduceLROnPlateau | Once per epoch (after validation) | Reduce LR when a metric (e.g. loss or mAP) plateaus. |
one_cycle / onecycle |
OneCycleLR | Every optimizer step | Single cycle: warmup then decay over full training. |
cosine_annealing / cosine |
PyTorch CosineAnnealingLR | Once per epoch | Cosine decay; SGDR restarts if num_epochs > T_max. |
cosine_annealing_with_tail / cosine_with_tail |
CosineAnnealingWithFixedTailLR | Once per epoch | Cosine phase then constant tail_lr; cosine_epochs + tail_epochs = num_epochs. |
ReduceLROnPlateau¶
Reduces the learning rate when the monitored metric stops improving. Useful when you don’t know a good step schedule in advance.
Config (JSON):
"training": {
"lr_scheduler_type": "reduce_on_plateau",
"lr_scheduler_plateau_metric": "mAP",
"lr_scheduler_plateau_factor": 0.5,
"lr_scheduler_plateau_patience": 4
}
lr_scheduler_plateau_metric:"total_loss"(minimize) or"mAP"(maximize). The engine uses this to callscheduler.step(metric_value)after each validation.lr_scheduler_plateau_factor: New LR = current LR × factor when the metric plateaus.lr_scheduler_plateau_patience: Number of epochs without improvement before reducing LR.
Warmup is not applied with ReduceLROnPlateau; the scheduler drives LR entirely.
OneCycleLR¶
OneCycleLR updates the learning rate every optimizer step: a linear warmup phase, then a cosine or linear decay over the rest of training. Total steps = num_epochs × (batches_per_epoch // gradient_accumulation_steps).
Config (JSON):
"training": {
"lr_scheduler_type": "one_cycle",
"lr_scheduler_one_cycle_pct_start": 0.3,
"lr_scheduler_one_cycle_div_factor": 25.0,
"lr_scheduler_one_cycle_final_div_factor": 1e4
}
pct_start: Fraction of total steps used for warmup (e.g. 0.3 = 30%).div_factor: Initial LR =max_lr / div_factor.final_div_factor: Minimum LR = initial_lr / final_div_factor.
No separate warmup config is used; OneCycleLR has its own warmup. The training engine steps this scheduler every optimizer step via an internal wrapper.
CosineAnnealingLR (cosine_annealing)¶
Standard PyTorch cosine decay. T_max from lr_scheduler_cosine_epochs, lr_scheduler_cosine_t_max, or num_epochs. If num_epochs > T_max, the LR restarts after each minimum (SGDR-style). Tail keys are ignored.
"training": {
"lr_scheduler_type": "cosine_annealing",
"lr_scheduler_cosine_t_max": 72,
"lr_scheduler_cosine_eta_min": 1e-6,
"lr_warmup_steps": 50
}
Cosine + fixed tail (cosine_annealing_with_tail)¶
Cosine for cosine_epochs (or t_max), then constant lr_scheduler_cosine_tail_lr for tail_epochs; the two phases must sum to num_epochs.
"training": {
"lr_scheduler_type": "cosine_annealing_with_tail",
"lr_scheduler_cosine_epochs": 20,
"lr_scheduler_cosine_tail_epochs": 4,
"lr_scheduler_cosine_eta_min": 1e-5,
"lr_scheduler_cosine_tail_lr": 5e-5
}
When using the training CLI, set training.lr_scheduler_type and the corresponding keys in your config (or in a base schedule under configs/_base_/schedules/). See configs/_base_/schedules/README.md for schedule examples.
Configuration¶
JSON experiment configs drive tools/train.py. The canonical reference is Configuration (all sections, model_type values, recipe catalog, evaluation vs production, CLI flags).
Quick load:
from pathlib import Path
from oriented_det.train.config import TrainingExperimentConfig
config = TrainingExperimentConfig.load(
Path("configs/oriented_rcnn/dota_le90_1x.json")
)
config.print_summary()
tools/train.py builds the model from config.model_type (oriented_rcnn, rotated_faster_rcnn, or rotated_retinanet) and writes runs/<model_type>/<timestamp>/config.json.
Understanding Loss Components¶
For two-stage detectors like OrientedRCNN, there are four main loss components:
Stage 1: Region Proposal Network (RPN) Losses¶
loss_objectness - Classification loss for the RPN
- Predicts whether an anchor contains an object (foreground) or not (background)
- Binary classification: object vs. background
- Helps the model learn where objects are located in the image
loss_rpn_box_reg - Box regression loss for the RPN
- Refines anchor boxes to better match ground truth objects
- Operates on anchors (predefined boxes at different scales/ratios/angles)
- Learns to transform anchors into better object proposals
- Uses Smooth L1 loss on [cx, cy, w, h, angle] parameters
Stage 2: ROI Head Losses¶
loss_classifier - Classification loss for the ROI head
- Predicts the specific class of each proposal (e.g., "plane", "ship", "small-vehicle")
- Multi-class classification across all object classes
- Only computed on positive proposals (IoU ≥ threshold with ground truth)
loss_box_reg - Box regression loss for the ROI head
- Refines proposals into final, precise bounding boxes
- Operates on proposals (output from RPN stage)
- Provides fine-grained localization refinement
- Uses Smooth L1 loss on [cx, cy, w, h, angle] parameters
Key Differences: loss_rpn_box_reg vs loss_box_reg¶
| Aspect | loss_rpn_box_reg |
loss_box_reg |
|---|---|---|
| Stage | Stage 1 (RPN) | Stage 2 (ROI) |
| Input | Anchors (predefined) | Proposals (from RPN) |
| Purpose | Generate object proposals | Refine proposals to final boxes |
| IoU Threshold | 0.5 (positive anchors) | 0.4 (positive proposals) |
| Sampling | 256 anchors per image | 512 proposals per image |
| Precision | Coarse (finding objects) | Fine (accurate localization) |
Training Flow¶
Image → Backbone → FPN Features
↓
RPN Head
↓
[loss_objectness + loss_rpn_box_reg]
↓
Oriented Proposals
↓
ROI Align
↓
ROI Head
↓
[loss_classifier + loss_box_reg]
↓
Final Detections
Both box regression losses use the same loss function (Smooth L1), but they operate at different stages: - RPN stage: Learns to find and roughly localize objects - ROI stage: Learns to classify and precisely localize objects
Evaluation¶
from oriented_det.train import evaluate
metrics = evaluate(
model=model,
data_loader=val_loader,
device=device,
eval_compute_map_final=True, # After training, load best checkpoint and compute mAP once
)
Best Practices¶
Data Loading¶
- Use
num_workers=4-8for faster data loading - Enable
pin_memory=Truefor faster CPU→GPU transfers - Monitor data loading time in profiler
Mixed Precision¶
- Always use
use_amp=Truefor training (2x faster, lower memory) - No accuracy loss observed in practice
Gradient Accumulation¶
- Use for large effective batch sizes without increasing memory
- Example:
batch_size=2withgradient_accumulation_steps=4= effective batch size of 8
Monitoring¶
- Watch for
sampled_fghitting 128 cap (foreground bottleneck in RPN) - Check loss component trends (all should decrease steadily)
- Monitor GPU utilization (should be >80% during training)
Memory Optimization¶
For OrientedRCNN with limited GPU memory:
- Use
roi_chunk_size=16withroi_use_checkpoint=Truefor 8-16GB GPUs - Use
roi_chunk_size=32withroi_use_checkpoint=Falsefor 24GB+ GPUs - Monitor memory usage during training to find optimal settings
Complete Example¶
import torch
from oriented_det import OrientedRCNN
from oriented_det.data import build_dota_loader
from oriented_det.train import train, CheckpointManager
from torch.optim import AdamW
# Setup
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = OrientedRCNN(num_classes=15).to(device)
optimizer = AdamW(model.parameters(), lr=1e-4)
# Data
train_loader = build_dota_loader("/path/to/dota", split="train", batch_size=4)
val_loader = build_dota_loader("/path/to/dota", split="val", batch_size=4)
# Checkpointing
checkpoint_manager = CheckpointManager("checkpoints/", best_metric="mAP")
# Train
history = train(
model=model,
train_loader=train_loader,
optimizer=optimizer,
device=device,
num_epochs=12,
val_loader=val_loader,
checkpoint_manager=checkpoint_manager,
use_amp=True,
gradient_accumulation_steps=4,
max_grad_norm=1.0,
)
See Also¶
- API Reference - Complete API documentation
- Models Guide - Model architectures