Training an on-device YOLO26n food detection model from scratch across multi-source datasets
I set out to build a food recognition system that could look at a photo, tell you what each item is, count how many there are, flag anything that looks spoiled or rotten, and pick out the things that are not food at all. The twist is that it had to do all of that on a phone or an iPad with no internet connection, which meant there was no cloud to lean on and no API to call when the model got confused.
You would think the hard part of a project like this is the model, but for me it was the data.
I had about 149k images collected from different sources, and once I set aside one large dataset that had no bounding boxes, I was left with 49,158 images from 11 datasets to work with. Every one of them had its own way of naming things, its own idea of what a good photo looks like, and plenty of the same pictures turning up again and again. So before I trained anything, my first real job was deciding what to throw away.
I wrote this as I went, following the pipeline in the order I ran it in Google Colab, so you will see the problems I ran into and how I fixed them. Some of the mistakes were mine, and I have left them in on purpose, because that is where most of what I learned came from.
If you are about to train a model on data from more than one place, I hope this saves you a few of the wrong turns I took.
The part that surprised me most was how much smaller the dataset got at every single stage, so I have laid it out in a table before we get into the details.
| Stage | Images | |---|---| | Original collection | about 149k | | After removing a large dataset with no bounding boxes | 49,158 | | After blur, brightness and duplicate cleaning | 31,589 | | Clean images that actually had usable labels | 24,306 | | Final split | 20,660 train and 3,646 validation |
That means I did not train on 49k images at all, I trained on about 20k, and every one of those drops had a reason that I will walk through as we go.
This job needs a GPU, because a full run means a lot of epochs over a big dataset after several rounds of cleaning, and on a CPU that would take days where a Colab T4 gets it done in hours, so the first thing I do in any session is check what I have been given.
Mine came back as a Tesla T4 with 15.6 GB of VRAM, and if yours says no GPU is available you will need to either pay for one or find a free platform that offers it.
A handful of variables control the whole run, and I like setting them all in one place at the top so I never have to hunt for them later.
I planned the work in three stages, starting with a baseline run to see what the off-the-shelf model knows, then a small 5k training run to prove the whole pipeline works, and finally the full run once the pilot looked sensible. RUNMODE decides whether the work stops at the baseline or training goes ahead, and the later cells simply check that one variable with an if/else.
The pilot saved me a lot of pain, because the bugs I found on the small run would have cost me hours if I had only discovered them on the big one.
I compressed the annotated data into a zip and stored it on Google Drive, and since reading straight from Drive is slow, I copy the zip onto Colab's local disk and extract it there, which is where two things went wrong for me.
The first was a partial extraction, where after a disconnect I ended up with a half-unzipped folder that looked perfectly fine but was missing files, and I only caught it because I counted the images and compared the total with the zip, so now I delete the folder, unzip again and check that the count is 49,158 before moving on.
The second was an extra nesting level, because the zip extracted to data/data/datasets and a hardcoded path simply fails, so I use a glob to find the datasets folder wherever it happens to land.
Ultralytics handles training, inference and evaluation, imagehash finds near duplicates, tqdm gives you progress bars, and PyYAML reads and writes the dataset config files. Ultralytics runs on PyTorch, which is why torch is imported for the GPU check, while OpenCV and PIL do the image work.
