ultralytics/ultralytics · error · ValueError

Dataset has only {len(train_records)} image(s) and no 'val'

Error message

Dataset has only {len(train_records)} image(s) and no 'val' split. Need at least 2 images to auto-split into train/val.

What it means

When a non-classification NDJSON dataset has no 'val' split, the converter auto-splits by deterministically (random.Random(0)) moving ~10% of train records (at least 1) to val. That requires at least 2 train records; with 0 or 1 it cannot form both splits and raises ValueError telling you the count found and the minimum.

Source

Thrown at ultralytics/data/converter.py:958

    # Hash-qualified dirs allow identical datasets to reuse downloads while preventing changed datasets from mutating
    # files that another training job may still be reading.
    dataset_dir = output_path / f"{ndjson_path.stem}-{_hash}"
    metadata_path = dataset_dir / (".ndjson.yaml" if is_classification else "data.yaml")
    if metadata_path.is_file():
        try:
            if (cached := YAML.load(metadata_path)).get("hash") == _hash and cached.get("complete") is True:
                return dataset_dir if is_classification else metadata_path
        except Exception:
            pass
    splits = {record["split"] for record in image_records}
    if not is_classification:
        if "train" not in splits:
            raise ValueError(f"Dataset missing required 'train' split. Found splits: {sorted(splits)}")
        if "val" not in splits:
            train_records = [r for r in image_records if r.get("split") == "train"]
            if len(train_records) < 2:
                raise ValueError(
                    f"Dataset has only {len(train_records)} image(s) and no 'val' split. "
                    f"Need at least 2 images to auto-split into train/val."
                )
            random.Random(0).shuffle(train_records)  # local RNG to avoid mutating global training seed
            val_count = max(1, len(train_records) // 10)
            for r in train_records[:val_count]:
                r["split"] = "val"
            splits.add("val")
            LOGGER.warning(
                f"WARNING ⚠️ No 'val' split found in dataset. "
                f"Auto-splitting {len(train_records)} images into {len(train_records) - val_count} train, {val_count} val. "
                f"For best results, manually assign validation images in Platform dataset page."
            )

    inferred_nc = None

    if not is_classification:
        class_ids = {

View on GitHub (pinned to 0449ea011c)

Solutions

  1. Provide at least 2 train images (practically far more) so auto-split can carve out a val image.
  2. Alternatively supply an explicit 'val' split record so no auto-split is needed.
  3. For single-image experiments, use direct predict on the image instead of dataset training.

Example fix

# before: single record {"split": "train", "file": "only.jpg", ...}

# after: two records
{"split": "train", "file": "a.jpg", ...}
{"split": "train", "file": "b.jpg", ...}  # auto-split makes b.jpg the val image
Defensive patterns

Strategy: validation

Validate before calling

splits = [r.get("split") for r in image_records]
if "val" not in splits and splits.count("train") < 2:
    raise ValueError("need >= 2 train images when no val split is provided")

Prevention

When it happens

Trigger: A single-image (or zero-image) train-only dataset, e.g. a smoke-test export with one sample, or a pilot dataset collected before more data exists.

Common situations: Minimal repro/test datasets; demos with one image; export truncation leaving a lone record after filtering.

Related errors


AI-assisted analysis of ultralytics/ultralytics@0449ea011c (2026-08-15). Data as JSON: /api/errors/26878673e97c7942. Report an issue: GitHub.