Ultralytics YOLO27:
Beyond Detection: Segment, Pose, OBB, and Classify·Classify, Combine, Ship·Lesson 8/10
Lessonintermediate

Train an Image Classifier

Lay out a folder dataset, train yolo26n-cls.pt on MNIST160, read probs and top-1 accuracy, and route low-confidence predictions to human review.

Image classification answers one question per image: what is this? It needs the cheapest labels of any task — a folder name — and runs fastest, which makes it the default second stage after a detector and the right first model for an inspection station that sees one part per frame. Its dataset has no YAML, so the folder structure is the contract, and a classifier always answers, even for images it has never seen anything like. Deciding when not to trust it is half the job.

Outcome

Train yolo26n-cls.pt on MNIST160, report top-1 accuracy, read class probabilities from result.probs, and choose a confidence threshold that sends uncertain images to review.

Fast Track
If you already know your way around, here's the short version.
  1. Put images in train/<class>/ and val/<class>/; folder names become class names, sorted alphabetically.

  2. yolo classify train data=mnist160 model=yolo26n-cls.pt epochs=1 imgsz=64.

  3. Gate on top-1 accuracy (metrics.top1); with five or fewer classes, top-5 is always 1.0.

  4. Read result.probs.top1 and .top1conf, and route predictions below a confidence threshold to a person.

Hands-on

The folder dataset layout#

Ultralytics YOLO image classification of three leaves labeled healthy, rust, and powdery mildew, each with its confidence

Classification datasets are directories, not YAML files. Point data= at the root folder:

parts/
├── train/
│   ├── good/      img_0001.jpg ...
│   ├── scratch/   img_0412.jpg ...
│   └── dent/      img_0733.jpg ...
└── val/
    ├── good/
    ├── scratch/
    └── dent/

What the loader does with it, from the installed source and the classification dataset guide:

SituationBehavior
Class indicesfolder names under train/, sorted alphabetically → dent=0, good=1, scratch=2
Validation folderval/, then validation/, then valid/; falls back to test/ with a warning
No train/ folder, only class folderssplits 80/20 into train/ and val/ automatically

On Ultralytics Platform, a classify dataset carries one image-level label per image instead of shapes:

Ultralytics Platform classify dataset: a conveyor of white boxes with a single image-level label, class2, chosen from the class panel

A new folder renumbers your classes

Symptom: after adding a class, an old integration reports good parts as dent. Cause: adding a folder named bent sorts it first and shifts every index after it. Fix: always map indices to names with model.names (or result.names); never hard-code an index.

Train on MNIST160#

MNIST160 is 160 handwritten digits, 80 in train/ and 80 in test/, each split into ten digit folders — small enough for a CPU smoke test. It has no val/ folder, so watch the loader warn and fall back to test/:

from ultralytics import YOLO

model = YOLO("yolo26n-cls.pt")
model.train(data="mnist160", epochs=1, imgsz=64, device="cpu")
metrics = model.val()
print(f"top-1: {metrics.top1:.3f}  top-5: {metrics.top5:.3f}")
WARNING ⚠️ Dataset 'split=val' not found, using 'split=test' instead.
val: .../datasets/mnist160/test... found 80 images in 10 classes ✅
                   all      0.162      0.463
top-1: 0.162  top-5: 0.463

One epoch barely moves the new ten-class head. In our run, epochs=10 reached top-1 0.55 and top-5 0.95 in about ten seconds on a laptop CPU — the loop works; it just needs a schedule. The Classify task guide lists the pretrained sizes.

Top-1 is the share of images whose highest-probability class is correct. Top-5 counts a hit if the correct class is anywhere in the five highest. Top-5 is informative for 1,000-class ImageNet, whose classes run from broad to very fine-grained; with five or fewer classes it is always 1.0, and with ten it is generous. Gate on top-1 accuracy plus a per-class confusion matrix.

ImageNet sample images arranged from broad to fine-grained classes, such as mammal to husky and vehicle to trimaran

Read probabilities in code#

from ultralytics import YOLO

model = YOLO("yolo26n-cls.pt")  # ImageNet, 1,000 classes
result = model("https://ultralytics.com/images/bus.jpg")[0]

probs = result.probs
print(f"top-1: {result.names[probs.top1]} ({float(probs.top1conf):.2f})")
for i, conf in zip(probs.top5, probs.top5conf.tolist()):
    print(f"  {result.names[i]:20s} {conf:.3f}")
top-1: minibus (0.52)
  minibus              0.516
  police_van           0.369
  trolleybus           0.096
  minivan              0.006
  ambulance            0.003

probs.top1 is an index, probs.top5 a list of indices, and probs.data the full probability vector. Notice the top-1 confidence: 0.52 is barely more than a coin flip between minibus and police_van. That number is what you route on.

Route uncertain predictions to a person#

A classifier in production rarely needs to be right on every image; it needs to be right on the images it decides alone. This script trains the 10-epoch model, then measures what different confidence thresholds would do on the 80 held-out digits:

from pathlib import Path

from ultralytics import YOLO

model = YOLO("yolo26n-cls.pt")
model.train(data="mnist160", epochs=10, imgsz=64, device="cpu", plots=False)  # model now holds best.pt
print(model.names)  # folder names, sorted, became class indices

test = Path(model.trainer.data["test"])  # mnist160/test, the split the loader validated on
images = sorted(test.rglob("*.png"))
results = model(images, imgsz=64, verbose=False)
truth = [p.parent.name for p in images]

for threshold in (0.0, 0.3, 0.6):
    kept = [(r, t) for r, t in zip(results, truth) if r.probs.top1conf >= threshold]
    correct = sum(r.names[r.probs.top1] == t for r, t in kept)
    print(f"threshold {threshold:.1f}: auto-accept {len(kept):2d}/{len(images)}, accuracy on accepted {correct / max(len(kept), 1):.0%}")
{0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9'}
threshold 0.0: auto-accept 80/80, accuracy on accepted 55%
threshold 0.3: auto-accept 41/80, accuracy on accepted 68%
threshold 0.6: auto-accept 10/80, accuracy on accepted 80%

Each row is a trade: raising the threshold makes the accepted predictions more accurate and sends more images to a person. At 0.6 this model decides only 10 of 80 digits alone — it is not ready to automate anything, and the fix is more training, not a cleverer threshold. Rerun the same table after each training round: it tells you both the accuracy you will get and exactly how much review work each threshold creates.

Decision rule

Pick the lowest threshold whose accuracy on accepted images meets your requirement, measured on held-out data. Then check that the review volume it creates is one your team can handle; if not, improve the model.

Pitfalls#

No none-of-the-above class

Symptom: an empty station, a wrong part, or a hand over the lens gets a confident class label. Cause: softmax probabilities always sum to 1, so the classifier always picks one of its classes. Fix: add an other/ folder filled with exactly those images, and route low top1conf predictions to review.

Validating a pretrained head on new folders

Symptom: a pretrained model scores near zero on your dataset before any training. Cause: yolo26n-cls.pt predicts 1,000 ImageNet indices, and your folders reuse indices 0–9 with different meanings: on MNIST160, folder 0 is index 0, which the pretrained head calls tench. We measured top-1 0.000 that way. Fix: fine-tune first, then validate.

Leaky folders

Symptom: top-1 near 1.0 in validation, much lower in production. Cause: near-duplicate photos of the same part in train/ and val/. Fix: split by part or by session, as in Split the Dataset Correctly.

Classifiers are pretrained at imgsz=224, not 640. Small objects inside a large frame are better served by detect-then-classify (next lesson) than by a larger imgsz.

Try It

Train yolo26n-cls.pt on mnist160 for 1 epoch and then for 10 epochs at imgsz=64, and record top-1 and top-5 for both runs. Print model.names to confirm how folder names became class indices. Then run route.py and pick the threshold you would ship for a requirement of at least 75% accuracy on auto-accepted images.

Done When
You've finished the lesson when all of these are true.
  • You have top-1 and top-5 for a 1-epoch and a 10-epoch run.

  • You printed model.names and can explain the alphabetical index rule and why top-5 is useless for a five-class problem.

  • You named a threshold, its accuracy on accepted images, and how many of the 80 images it sends to review.

Show solution
from ultralytics import YOLO

for epochs in (1, 10):
    model = YOLO("yolo26n-cls.pt")
    model.train(data="mnist160", epochs=epochs, imgsz=64, device="cpu")
    metrics = model.val()
    print(f"{epochs:2d} epochs  top-1: {metrics.top1:.3f}  top-5: {metrics.top5:.3f}")
print(model.names)  # {0: '0', 1: '1', ..., 9: '9'}

Our runs: 1 epoch → top-1 0.162, top-5 0.463; 10 epochs → top-1 0.550, top-5 0.950. With five classes, the correct answer is always among the top five, so top-5 is 1.0 by construction and tells you nothing.

For 75% accuracy on accepted images, route.py says 0.6 (80% on 10 accepted), which sends 70 of 80 digits to review. That is the honest answer: the threshold meets the accuracy requirement, but the model is not useful yet. Train longer and rerun the table before choosing a threshold for real.

What's next

Next, we chain tasks: a detector feeding a classifier, and a pose model feeding a tracker.