Ultralytics YOLO27: Встать в лист ожидания.
Get Started
Основы компьютерного зрения·Изучи метрики·Урок 8/10
Урокначинающий

Разделение, которое показывает правду

Найди почти дублирующиеся изображения, попавшие и в обучающую, и в валидационную выборку, а затем раздели данные по единице, которая будет новой при эксплуатации.

Если модель набирает 95% на валидации и 60% в рабочей среде, скорее всего, разделение данных оказалось обманчивым. Почти идентичные изображения — соседние кадры видео или снимки с одной камеры за один день — попали в обе выборки, поэтому валидация проверяла запоминание, а не способность к обобщению. Ты обнаружишь именно такую утечку в реальном наборе данных, а затем исправишь её групповым разделением.

Результат

Найди почти дублирующиеся изображения в разных выборках, раздели данные по ключу группы и проверь, что каждая группа входит не более чем в одну выборку.

Ускоренное прохождение
Если ты уже освоился, вот краткая версия.
  1. Обучающая выборка используется для настройки весов, валидационная помогает принимать решения, тестовая — для однократной итоговой оценки.

  2. Прежде чем доверять оценке на валидации, проверь выборки на почти идентичные изображения.

  3. Определи ключ группы: видеоклип, камера, день или объект съёмки.

  4. Перемешивай группы, а не изображения, затем проверь, что каждый класс присутствует в каждой выборке.

  5. Зафиксируй тестовую выборку и используй её один раз после завершения каждого эксперимента.

Практические занятия

Три выборки — три задачи#

ВыборкаНа чём обучается модельКогда её просматривают
ОбучающаяМодель: здесь подбираются весаКаждую эпоху
ВалидацияТы: гиперпараметры, эпохи, размер модели, пороговые значенияПосле каждого эксперимента
ТестНикто: он сообщает итоговое значениеОдин раз, после принятия всех решений

Каждое решение, которое ты принимаешь, глядя на отдельную выборку, понемногу передаёт информацию из неё модели. Для этого и нужна валидационная выборка. Настраивай модель на тестовой выборке — и её показатель перестанет быть честной оценкой: ты переобучился на собственных решениях. В Ultralytics model.val(split="test") оценивает тестовую выборку, указанную в YAML-файле датасета.

Найди утечку в реальном датасете#

Изображения таблеток названы Frame_0.jpg, Frame_4.jpg, Frame_8.jpg — это кадры из видео. Это тревожный признак: соседние кадры почти идентичны, и при случайном разделении они попадают в обе части. Проверь это, сравнив крошечные отпечатки в оттенках серого для каждого изображения валидационной выборки с каждым изображением обучающей выборки:

from pathlib import Path

import numpy as np
from PIL import Image
from ultralytics.data.utils import check_det_dataset

data = check_det_dataset("medical-pills.yaml")

def thumbnail(path):
    """A 32x18 grayscale fingerprint: enough to spot near-identical frames."""
    return np.asarray(Image.open(path).convert("L").resize((32, 18)), dtype=float)

train = {p.name: thumbnail(p) for p in sorted(Path(data["train"]).glob("*.jpg"))}
pairs = []
for p in sorted(Path(data["val"]).glob("*.jpg")):
    v = thumbnail(p)
    name, diff = min(((n, np.abs(v - t).mean()) for n, t in train.items()), key=lambda x: x[1])
    pairs.append((diff, p.name, name))

pairs.sort()
print(f"{sum(d < 5 for d, _, _ in pairs)} of {len(pairs)} val images have a near-twin in train (difference < 5)")
for diff, val_name, train_name in pairs[:3]:
    print(f"  {val_name} ~ {train_name}  difference {diff:.1f}")
17 of 23 val images have a near-twin in train (difference < 5)
  Frame_344.jpg ~ Frame_56.jpg  difference 0.3
  Frame_28.jpg ~ Frame_312.jpg  difference 0.5
  Frame_24.jpg ~ Frame_308.jpg  difference 1.8

Разница 0.3 по шкале 0–255 означает, что изображения практически одинаковы. Открой val/Frame_24.jpg и train/Frame_308.jpg рядом — и увидишь одни и те же упаковки таблеток с одного ракурса. У 17 из 23 изображений валидационной выборки есть близнец в обучающей выборке, поэтому оценка в основном показывает, насколько хорошо модель запомнила уже виденные изображения. Для быстрого демонстрационного датасета это нормально — но именно такой результат нельзя выпускать в продакшен.

Диалог Redistribute Splits на платформе Ultralytics для набора medical-pills: 80% для обучения, 20% для валидации и 0% для теста

Та же ловушка есть в любом инструменте. В описании этого диалога сказано, что он случайным образом перераспределяет каждое изображение. Это идеально, если изображения — независимые фотографии, но приводит к утечке, если это кадры видео. Сначала проверь данные на почти дубликаты и оставь для теста больше 0%, если хочешь получить итоговую оценку, на которую никто не подбирал модель.

Разделяй по группам#

Правило: разделяй данные так, чтобы единица, новая при развёртывании модели, была независимой.

ДанныеРазделять по
ВидеоКлипу или сеансу записи
Парк стационарных камерКамере или объекту
Сцены, меняющиеся со временемДню или неделе
Медицинские данные, спорт или данные о людяхПациенту, спортсмену или участнику исследования
Серии фотографийСобытию съёмки

В именах файлов часто содержится ключ группы. В этом примере создаются 900 изображений с 6 камер, а затем сравниваются два способа разделения:

import random
from collections import defaultdict

# File names carry the unit that must never straddle splits: camera_day_frame.jpg
images = [f"cam{c:02d}_day{d}_{f:04d}.jpg" for c in range(1, 7) for d in range(1, 4) for f in range(50)]

# WRONG: shuffle individual frames, so near-identical neighbors land in both splits
random.seed(0)
shuffled = random.sample(images, len(images))
train, val = shuffled[: int(0.8 * len(images))], shuffled[int(0.8 * len(images)) :]
shared = {i.split("_")[0] for i in train} & {i.split("_")[0] for i in val}
print(f"per-image split: {len(shared)} of 6 cameras appear in both train and val")

# RIGHT: shuffle whole cameras, then expand them back into images
groups = defaultdict(list)
for image in images:
    groups[image.split("_")[0]].append(image)
cameras = random.sample(sorted(groups), len(groups))
train = [i for cam in cameras[:5] for i in groups[cam]]
val = [i for cam in cameras[5:] for i in groups[cam]]
print(f"per-camera split: train {len(train)} images, val {len(val)} images, val cameras {cameras[5:]}")
per-image split: 6 of 6 cameras appear in both train and val
per-camera split: train 750 images, val 150 images, val cameras ['cam05']

При разделении по отдельным изображениям валидационная выборка не содержит новых камер. При разделении по камерам оценка показывает, чего ожидать на камере, которую модель ещё не видела, — именно это значение тебе и нужно.

Подводные камни#

Случайное перемешивание коррелированных данных

Симптом: результаты на валидации намного лучше, чем на первом полевом тесте. Решение: найди ключ группировки — часто он скрыт в именах файлов или метаданных съёмки, — раздели данные по нему и запусти проверку на почти дублирующиеся изображения.

Класс отсутствует в валидационной выборке

Симптом: для редкого класса нет AP или AP рассчитан всего по двум экземплярам. Решение: после группового разбиения подсчитай количество экземпляров каждого класса в каждой части и меняй местами целые группы, пока распределение классов во всех частях не станет примерно одинаковым. sklearn.model_selection.StratifiedGroupKFold автоматизирует этот процесс.

Настройка на тестовой выборке

Симптом: показатель на тесте растёт от эксперимента к эксперименту. Решение: заморозь тестовую выборку, сформируй её из условий, похожих на производственные, и сообщай результат один раз по завершении эксперимента. На небольших наборах данных вместо повторного использования одной выборки усредняй результаты нескольких разбиений с помощью k-блочной перекрёстной проверки.

В статье Правильное разделение датасета рассматриваются пропорции и способы применить всё это ко всему датасету.

Попробовать

Запусти проверку на почти дублирующиеся изображения для своего датасета (или medical-pills). Зафиксируй, сколько изображений валидационной выборки имеют близнеца в обучающей. Затем определи ключ группы, раздели данные по нему и запусти проверку на утечки из решения для новых списков файлов. Результат: количество близнецов до и после, а также пустой список, выведенный проверкой на утечки.

Критерии готовности
Урок считается завершённым, если выполнены все условия ниже.
  • Ты знаешь количество близнецов в текущем разбиении.

  • Ты определил ключ группы и разделил данные по нему.

  • Проверка на утечки выводит пустой список для новых частей данных.

Показать решение

Проверка на утечки, которая работает с любым списком имён файлов, если ей передать функцию извлечения ключа группы:

def leaked_groups(splits, key):
    """Return every group key that appears in more than one split."""
    owner, leaks = {}, set()
    for split, files in splits.items():
        for name in files:
            group = key(name)
            if owner.setdefault(group, split) != split:
                leaks.add(group)
    return sorted(leaks)

# Замени эти списки именами своих файлов, например [p.name for p in Path("images/train").glob("*.jpg")]
splits = {
    "train": ["cam01_day1_0001.jpg", "cam01_day1_0002.jpg", "cam02_day1_0001.jpg"],
    "val": ["cam03_day2_0001.jpg", "cam01_day1_0003.jpg"],
    "test": ["cam04_day3_0001.jpg"],
}
print(leaked_groups(splits, key=lambda name: name.split("_")[0]))  # группировка по камере
['cam01']

cam01 есть и в train, и в val, значит, при таком разделении происходит утечка. Перемести все изображения cam01 в одну часть и повторяй проверку, пока она не выведет []. Для medical-pills, где кадры одного видео возвращаются к одному и тому же ракурсу, по именам файлов не определить надёжный ключ группы: сними новые видео упаковок другого вида для валидации либо сгруппируй изображения по отпечатку почти дубликатов и помести целые группы в одну часть.

Что дальше

Теперь, когда данные разделены без утечек, разберём полученные метрики.

Войди, чтобы сохранять прогресс