We train a YOLOv11m detector to find targets in drone imagery shot from 20 to 40 metres. At that altitude a target is 80 to 120 pixels in a 4K frame. The model has to run near real time on an embedded board with a fixed power budget.
The final numbers were mAP50 = 0.904 and mAP50-95 = 0.691. What I want to write about is not the training run. It is the three decisions made before it, because those decided the outcome far more than any learning rate did.
1. Tile with overlap, or lose the targets on the seams
YOLO takes a fixed input size. A 4K frame downscaled to that size turns a 100-pixel target into something around 25 pixels, which is close to the point where the detector stops being useful. So the frames get split into 1280×1280 tiles instead.
The naive version of this tiles on a grid with no overlap, and it quietly destroys a slice of your data. Any target that straddles a tile boundary appears as two partial objects, neither of which looks like the thing you labelled. The model learns from those halves, and at inference it fails on exactly the same geometry.
We tile with 40 % overlap. That guarantees any target smaller than the overlap band appears whole in at least one tile. The cost is more tiles and more training time. The benefit is that your annotations mean what they say.
If you take one thing from this section: the overlap is not a tuning parameter you sweep. It is set by the largest object you care about. Overlap must exceed the object’s maximum dimension in pixels, or you have not solved the problem, you have only made it rarer.
2. Split at image level, not tile level
This is the one that bites people quietly, because the symptom is a validation score that looks great and a field performance that does not.
If you tile first and then split tiles randomly into train and validation, tiles from the same original frame end up on both sides. The same patch of grass, the same lighting, the same target, sometimes literally the same pixels through the overlap band. The model has seen your validation set. Your metric is measuring memorisation.
We split at image level, 85/15: an original frame and every tile derived from it live on the same side of the split. The validation number dropped when we made this change. That drop was the first honest number we had.
A cheap way to check whether you have this bug: shuffle your split with a different seed and retrain. If your validation mAP moves by a couple of points, fine. If it swings wildly, you probably have leakage, or your validation set is too small to mean anything.
3. Balance classes offline, and do it once
Our raw data was imbalanced. The usual fixes are class weights in the loss or oversampling in the loader. We used offline augmentation with a per-class multiplier: 8× for the under-represented class, 3× for the over-represented one. That turned roughly 2,100 raw frames into a balanced ~15,000-image training set at about a 1:1.2 ratio.
The detail worth stealing: we deliberately left colour augmentation out of this step. Ultralytics applies HSV jitter during training by default. If you also apply colour augmentation while building the dataset, every image gets augmented twice, and the second pass compounds the first. You end up training on a colour distribution that does not resemble your camera. Geometric augmentation offline, photometric augmentation online, and do not let the two overlap.
Reading the curve honestly
The model converged at epoch 33. Early stopping fired at 48.
The tempting read is “train longer” or “try a bigger backbone”. The correct read, when a model plateaus that early with early stopping that late, is that you have hit a data ceiling, not a training ceiling. More epochs will buy you overfitting. What you need is more variety: more altitudes, more ground surfaces, more lighting, more partial occlusion.
So the output of that training run was not a better model. It was a 13-item data collection plan. That is a less satisfying deliverable, and it was the right one.
What this costs you
All of this is unglamorous. Nobody puts “designed the train/validation split” on a slide. But the ordering matters: a leaky split will hand you a number that flatters you until the day you fly, and no amount of hyperparameter search will find the bug, because the bug is not in the model.
Build the dataset like you expect someone to audit it. Usually that someone is you, six weeks later, trying to work out why the field results do not match the report.