mAP50 = 0,99 за 22 минуты на CPU: почему красивая метрика YOLO11n ничего не значит
PrimeCoder · 2026 · формат: tech
Разбор новинки простыми словами: что это, кому нужно и брать ли.
Показывает, как утечка train/val из видео-кадров завышает метрики детекторов и как ловить её dHash-проверкой до обучения — практический урок для любого CV-проекта.
Связанные услуги: каталог, AI Boost Team, стать клиентом.
22 минуты на ноутбучном процессоре, mAP50 = 0,991. И почти нулевая ценность этой цифры
Автор под ником 8MVR3 взял YOLO11n, дообучил её на датасете medical-pills из документации Ultralytics и получил на валидации mAP50 = 0,991. Шестьдесят эпох. Двадцать две минуты на CPU Ryzen 7 4800H. Никакой видеокарты, никаких трюков с аугментациями, стандартный скрипт из трёх строк.
Выглядит как рекламный скриншот: сейчас детекторы учатся сами, дёшево и быстро. Дальше автор прогнал тот же датасет через свою утилиту dataset-doctor и обнаружил, что у каждого из 23 валидационных кадров есть почти точная копия в train. У трёх — расстояние ноль бит, то есть хеши совпали полностью. У семнадцати — не больше двух бит из шестидесяти четырёх.
Модель не училась находить таблетки на новых снимках. Она училась узнавать сцену, которую видела 60 эпох подряд. Метрика это честно зафиксировала.
Валидация измеряла не «как модель справится с новыми таблетками», а «как хорошо модель помнит то, что уже видела».
Для владельца бизнеса это не история про таблетки. Это история про любой проект компьютерного зрения, который стартовал с фразы «у нас есть запись с камеры, нарежем кадров и обучим».
Что обучали: маленький датасет, большая оговорка
medical-pills — учебный набор. 115 фотографий, 2 022 рамки, один класс pill, архив на 8 МБ. Разбиение из коробки: 92 кадра в train, 23 в val. Сами авторы называют его proof-of-concept — демонстрацией того, что пайплайн работает.
Обучение шло с параметрами по умолчанию: imgsz 640, batch 16, 60 эпох, веса yolo11n.pt, предобученные на COCO. Результат на валидации:
- mAP50 — 0,991
- mAP50-95 — 0,797
- Эпох / размер входа — 60 / 640
- Железо и время — CPU Ryzen 7 4800H, 22 минуты
Само по себе число не выглядит подозрительным. Один класс, крупные контрастные объекты на ровном фоне, 92 кадра — сеть обязана выучить такую задачу. Вопрос не в том, что цифра большая. Вопрос в том, на чём её посчитали.
Что нашла проверка: у валидации не осталось ни одного нового кадра
dataset-doctor — утилита, которая прогоняет датасет через набор проверок до обучения и пишет отчёт человеческим языком: что сломано, насколько критично, что делать. Ставится из репозитория, запускается на папке в том виде, в каком её скачивает Ultralytics.
На medical-pills отчёт выдал одну критичную проблему: «Похожие кадры в разных выборках — 73». Это 73 пары «кадр из train — кадр из val», отличающиеся не больше чем на 6 бит из 64 по dHash. Всего возможных пар train-val 92 × 23 = 2116, то есть почти одинаковых среди них 3,4%.
Дальше — самое интересное. Если для каждого кадра val найти ближайший кадр train, картина такая:
- Frame_24 → Frame_308 — 2 бита
- Frame_28 → Frame_312 — 0 бит
- Frame_40 → Frame_324 — 2 бита
- Frame_48 → Frame_332 — 1 бит
- Frame_84 → Frame_380 — 0 бит
- Frame_192 → Frame_196 — 1 бит
- Frame_264 → Frame_260 — 6 бит
- Frame_344 → Frame_56 — 0 бит
Ни одного по-настоящему нового кадра в валидации нет. Самый «далёкий» кадр отстоит от train на 6 бит — это по-прежнему почти одна и та же картинка.
Две разные истории в одной таблице
Первая предсказуема. Frame_172 и Frame_168, Frame_248 и Frame_244, Frame_264 и Frame_260 — номера отличаются на четыре. Это соседние сохранённые кадры одного ролика. Так бывает всегда, когда кадры одного видео делят на train и val случайно, по одному.
Вторая — неожиданная. Frame_28 из val совпадает с Frame_312 из train с расстоянием ноль, между ними 284 кадра. И это не единичный случай: сдвиг везде от 280 до 304 кадров. Из 18 «далёких» пар у 16 расстояние не больше 2 бит, а четыре из пяти соседних пар отстоят на 4–6 бит. «Далёкие» кадры оказались ближе соседних.
Гипотеза автора: в видео одна и та же сцена снята или смонтирована дважды. Плюс номера кадров идут ровно через 4, от 0 до 456, без единого пропуска — 115 номеров, ровно столько фото в датасете. Похоже, весь набор — каждый четвёртый кадр одного ролика длиной около 460 кадров.
Вывод жёсткий: честно разделить такой датасет на train и val невозможно. Источник один.
Почему 0,991 — правильное число, которое отвечает не на тот вопрос
Валидация нужна для одного ответа: как модель поведёт на данных, которых не видела. Если val — это train плюс дрожание камеры, вопрос незаметно подменяется. Проверяется память, а не обобщение.
Число при этом не врёт. mAP на этих 23 картинках посчитан корректно. Оно просто не говорит ничего о том, что будет на фото другого блистера, при другом свете, с другого телефона. Может быть 0,95. Может быть 0,6. Это не выяснится по такой валидации никогда.
Утечка между train и val — самый тихий баг в ML: ничего не падает, лосс красиво снижается, метрики на валидации отличные, и всё это до первого дня в проде.
Вот где это бьёт по SMB. Датасеты из видео — норма, а не экзотика: камеры на производстве, регистраторы, записи с дронов, ролики с телефона кладовщика. Нарезать кадров и разделить их через train_test_split — стандартный ход. Утечка после него гарантирована.
В отчётах это выглядит идеально. В цеху — 40% пропусков на первой смене, а подрядчик разводит руками: «у нас же метрики были». Метрики были на фотографиях, которые модель видела.
Чем подход отличается от того, как обычно чистят данные
Типовая проверка датасета — глазами и по хешу файла. Первое не масштабируется: 115 кадров посмотреть можно, 40 тысяч — нет. Второе ловит только побайтовые копии. Сдвиг камеры на пару пикселей, другой блик на фольге, пересохранённый JPEG — и хеш файла другой, хотя для детектора это одна и та же картинка.
dHash смотрит на структуру изображения, а не на байты: 64 бита, которые меняются слабо при сдвиге, сжатии и лёгком изменении яркости. Дальше — корзины LSH, чтобы не сравнивать каждую пару с каждой вручную. Автор оценивает объём в пару десятков строк Python.
Про масштаб цифр. Внутри датасета всего 6 555 пар, из них 168 (2,6%) оказались ближе 6 бит. То есть почти одинаковые кадры лежат не только между выборками — они рассыпаны по всему набору. Граница train/val тут ни при чём, она просто делает проблему видимой.
Порог 6 бит — не истина в последней инстанции, эмпирика под этот набор. На других данных его придётся подбирать и смотреть глазами на пары вокруг границы.
Кому нужно и что делать на практике
Всем, кто строит детектор на кадрах из видео. Производство, ритейл, логистика, стройка, агро — везде, где данные пришли с регистратора или камеры, а не с фотостока.
- Проверяйте данные до обучения, а не после. Отчёт по датасету дешевле, чем переобучение и повторная разметка.
- Считайте dHash и смотрите на пары глазами. Двадцать строк кода плюс полчаса на спорные случаи вокруг порога.
- Делите по источнику, а не по кадрам. Одно видео, одна смена, одна партия товара — целиком в train или целиком в val. Никогда пополам.
- Проверяйте метрику на отдельной съёмке. Другой день, другой свет, другой оператор, другая камера. Только это число стоит показывать руководству.
- Договаривайтесь о приёмке по проду. Если в договоре с подрядчиком стоит mAP на его же валидации, вы покупаете бумажку.
Последний пункт самый дорогой. Метрика на внутренней валидации не проверяема снаружи и почти всегда завышена. Метрика на вашей съёмке проверяется за один день.
Где подвох
К Ultralytics претензий нет. Датасет честно назван proof-of-concept, его задача — показать, что пайплайн работает. С ней он справляется. Вопрос к тому, как читают цифры, которые на нём получаются.
У самого метода ограничения. dHash не поймает сильный поворот, кроп или смену ракурса — там перцептивный хеш разойдётся, а объект останется тем же. Частичные перекрытия и кадры из соседних сцен он тоже не разберёт. Утилита даёт сигнал, а не приговор.
И главное: проверка не скажет, насколько именно утечка завысила метрику. Она скажет, что утечка есть. Дальше — либо пересобирать выборки, либо честно признать, что валидации у вас нет.
Вердикт за 30 секунд
- mAP50 = 0,991 на medical-pills — реальное число на реальных кадрах, у каждого из которых есть копия в train. Модель показала память, а не качество детекции.
- Проверка стоит пару десятков строк на Python и полчаса времени до обучения. Переобучение на утёкшем датасете стоит недель и доверия заказчика.
- Правило простое: делить по источнику съёмки. Одно видео целиком в train или целиком в val, никакого shuffle по кадрам.
Брать или подождать
Брать — если у вас в проекте есть видео и вы ещё не проверяли сплит на пересечение. Прогнать dHash и посмотреть на отчёт дешевле, чем узнать про утечку на приёмке.
Подождать — если вы ждёте от такой проверки готового вердикта «датасет чистый». Она покажет подозрительные пары, решение по ним за вами.
Не брать — как замену нормальному протоколу приёмки. Один скрипт не отменяет тест на отдельной съёмке с другой камеры.
Вердикт
Красивая метрика без проверки данных — это не результат, а скриншот. 22 минуты на CPU и mAP50 = 0,991 выглядят как доказательство зрелости инструментов. На деле они доказывают ровно одно: сеть запомнила 92 кадра одной сцены.
Хорошая новость в том, что цена проверки смешная. Двадцать строк, dHash, корзины, полчаса на разбор пар. Плохая новость в том, что делают её обычно после того, как модель провалилась в проде, а не до обучения. Порядок действий тут стоит поменять.
FAQ
Что такое утечка train/val простыми словами?
Ситуация, когда в проверочной выборке лежат почти те же изображения, что и в обучающей. Модель их уже видела, поэтому показывает высокий результат на данных, которые новыми не являются.
Почему обычный хеш файла не помогает?
Он сравнивает байты. Кадр, сдвинутый на два пикселя или пересохранённый в JPEG, получит другой хеш файла при том же содержимом для детектора. dHash сравнивает структуру картинки, поэтому соседние кадры видео для него почти идентичны.
Порог 6 бит из 64 — это стандарт?
Нет. Это эмпирика под конкретный набор. На других данных порог подбирают, просматривая пары рядом с границей, иначе получите либо пропуски, либо ложные срабатывания.
Датасет плохой?
Датасет учебный, его задача — демонстрация пайплайна. Проблема в чтении метрик, полученных на нём. Переносить 0,991 в презентацию для заказчика нельзя.
Что делать, если все кадры из одного видео?
Разделить по источнику не получится — источник один. Варианты: снять дополнительные данные или честно зафиксировать, что валидации нет и оценка будет только по проду.
Что сделать на этой неделе
Откройте скрипт подготовки данных. Найдите строку, где выбирается train_test_split или random.shuffle по кадрам. Если кадры пришли из видео, эта строка почти наверняка создаёт утечку.
Дальше — dHash по всем изображениям, корзины, отчёт по парам ближе порога. Отчёт читается за полчаса. Переобучение и повторная разметка — нет.
Инструмент открытый, лежит на GitHub, ставится одной командой на папку с данными. Проверка дешевле, чем вера в красивую цифру.