- BrainTools - https://www.braintools.ru -
Представьте, что вы управляете крупной сетью супермаркетов. Вы контролируете поставки на склад, знаете продажи по чекам, по камерам видите поток покупателей и их маршруты в зале. Но вот что реально происходит на полках остается “черным ящиком”:
Есть ли на месте Coca-Cola в отделе напитков?
Не закончились ли акционные мандарины?
Не оставил ли кто-то бутылку пива среди детских игрушек?
Если бы вы знали состояние полок в реальном времени, можно было бы оперативно отправлять сотрудника исправлять конкретные проблемы, а не надеяться, что он заметит их во время очередного обхода. Это позволяет увеличить заполняемость полок (on-shelf availability, OSA): чем больше товаров на полках – тем выше потенциальная выручка. Покупатель всегда найдет нужный товар или сделает импульсивную покупку, если ассортимент перед глазами полный.
Мы работали с двумя сетями супермаркетов: начинали с пилота в небольшом магазине “у дома”, затем масштабировались на два флагманских супермаркета известной федеральной сети. Задача – распознавать все товары на всех полках. Дальше на этих данных строится аналитика и автоматизируются операции по пополнению ассортимента, но сейчас сфокусируемся именно на распознавании.
Основные сложности:
Необходимо развернуть камеру-инфраструктуру: камеры должны делать снимки каждые 30 минут и передавать их в систему.
В магазинах разные зоны освещаются по-разному, из-за чего одна и та же кола в холодильнике и на стеллаже может выглядеть совершенно иначе.
Проблема холодного старта: запуск в новом магазине с неизвестным ассортиментом.
Ассортимент постоянно меняется, полки регулярно переставляются – и эти процессы мы никак не контролируем.
Это лишь часть списка: о ключевых технических и бизнес-нюансах расскажем в отдельных материалах. Здесь разберём самые интересные моменты, связанные именно с задачей классификации товаров на полках.
После установки камер и настройки инфраструктуры у нас появилось N камер, которые каждые 30 минут делают снимки и автоматически сохраняют их в S3. На этом этапе мы сразу столкнулись с тем, насколько разными могут быть фотографии одной и той же полки с разных камер:
Различия в освещении (от “жёлтого” света до синевы холодильника);
Углы обзора: одни камеры смотрят прямо, другие – под углом или с большого расстояния;
Наличие бликов, теней, частичных перекрытий товаров.
Примеры этих различий – ниже на иллюстрациях.
Сначала снимки с камер приходится фильтровать: на них часто попадают люди, тележки, коробки и другие препятствия. Детектирование и удаление таких помех – отдельная большая задача, о ней расскажем в другой статье.
Далее для каждого оставшегося кадра запускается детектор объектов – в нашем случае свежая версия YOLO, тут всё стандартно. На выходе мы получаем то, что нужно для классификации: кропы (фрагменты изображений), вырезанные по координатам найденных товаров, для каждого снимка с каждой камеры. Именно эти кропы и будут дальше поступать на этап классификации.
Не существует готовой модели, которая безошибочно определит товар на фото при каталоге в 20+ тысяч позиций. Поэтому без человеческой разметки не обойтись. Сначала мы пробовали Label Studio, но быстро упёрлись в ограничения по функционалу и разработали свой собственный инструмент, заточенный именно под классификацию товаров на полках.
Детали оптимального объёма разметки – отдельная тема, здесь расскажем только о сути: с каждой камеры один кадр раз в несколько дней мы размечали вручную, присваивая каждому кропу корректный product_id. Ошибки [1] в разметке неизбежны, но для ряда категорий они стали главным узким местом: пайплайн для этих товаров работал отлично, а заниженная точность была вызвана только неверными метками.
Мы перепробовали и автоматические методы чистки (поиск аномалий через Cleanlab [2], кластеризацию эмбеддингов) и ручные (отбор подозрительных классов, визуализация в FiftyOne [3], повторная разметка). Но из-за сильного внешнего сходства между многими товарами автоматическая фильтрация оказалась ненадёжной, баланс precision/recall не удавалось удержать на приемлемом уровне.
Итог – старый добрый ручной пересмотр сработал эффективнее всего.
Переходим к основной задаче: как классифицировать товары на полках по кропам с камер. Базовый вариант — классическая классификация: обучаем модель на 20 тысячах товаров и получаем вероятностное распределение по всему каталогу. Это просто, но плохо работает в динамике – ассортимент регулярно обновляется, появляются новые продукты, которые модель не видела, и она ошибочно относит их к уже известным классам.
Реальный рабочий подход – metric learning. Вместо классификатора мы учим модель-эмбеддер: она преобразует каждое изображение в вектор признаков (эмбеддинг). Для каждого размеченного кропа мы сохраняем его эмбеддинг – и это становится нашим search space. Когда появляется новый товар, достаточно размечать его пару раз – эмбеддинги сразу попадают в search space, и эмбеддер сможет находить похожие примеры среди них, корректно определяя класс даже для новых товаров.
То есть вся система строится вокруг двух компонентов: хорошего эмбеддера и search space, который пополняется по мере появления новых товаров.
Мы перебрали разные компактные архитектуры (ConvNext, ViT, Swin и др.) и остановились на ViT-Base-32 – оптимальный баланс между скоростью и качеством на ограниченных вычислительных ресурсах.
Как обучить эмбеддер, чтобы он действительно различал похожие товары? Здесь пригодится трюк из области распознавания лиц – подход ArcFace. Суть его в том, что модель учится строить для каждого товара свой “отпечаток” – вектор (эмбеддинг), который можно сравнить с другими.
ArcFace использует специальную функцию потерь: она не просто наказывает модель за ошибку, а заставляет эмбеддинги одного и того же товара быть максимально похожими (располагаться рядом в пространстве признаков), а эмбеддинги разных товаров – максимально разными (располагаться под разными углами на воображаемой сфере). Это позволяет системе легко отличать даже очень похожие товары: модель буквально учится “отталкивать” разные классы друг от друга и “сжимать” эмбеддинги одного класса.
В результате получается компактное и хорошо структурированное пространство эмбеддингов, где поиск нужного товара сводится к простому сравнению векторов.
В итоге, такой подход даёт очень “чистое” пространство признаков: похожие товары собираются в плотные группы, а разные – явно отделены друг от друга. Благодаря этому, если нужно быстро найти тот же товар на другом фото (или похожий), система просто ищет ближайший “отпечаток” среди уже известных – это и есть поиск ближайших соседей.
Deduplication и кластеризация (то есть удаление дублей и группировка похожих товаров) тоже становятся проще: модель легко отличает даже очень похожие, но разные продукты. Главное преимущество – этот метод работает стабильно, даже если в реальных магазинах появляется много новых товаров, которые не встречались в обучающей выборке.
Размер эмбеддинга: 512 – классика жанра для задач такого типа, компромисс между информативностью и скоростью.
Batch size: Чем больше, тем лучше. Большой батч позволяет за одну итерацию показать модели больше уникальных товаров и сделать обучение [5] устойчивее. После перехода с RTX 3090 на H100 и увеличения batch size качество даже немного выросло.
Сэмплирование: Batch balanced по компании: в каждом батче только изображения товаров одной сети (у нас было два вендора). Для каждого класса в батче брали по четыре кропа. На инференсе поиск кандидатов тоже ограничивали внутри компании. В планах — сэмплирование по категориям: в батче в основном одна категория, чтобы учить модель различать не «колу и печенье», а разные сорта печенья между собой.
Претрейн: Экспериментально выяснили, что лучший результат даёт предварительное обучение (pretrain) на публичном датасете RP2K [6], а затем уже дообучение (fine-tune) на собственных данных.
Search space – это база всех эмбеддингов, по которой нужно максимально быстро находить ближайшие к заданному эмбеддингу (поиск ближайших соседей, KNN или ANN). Для хранения таких данных есть несколько популярных решений: Faiss [7], Milvus [8], Qdrant [9], а также встроенные векторные индексы в Redis [10]. Мы выбрали Qdrant – он прост в настройке, легко масштабируется и закрывает все необходимые сценарии.
Пайплайн такой: берём все размеченные кропы, прогоняем через эмбеддер, складываем получившиеся эмбеддинги в Qdrant. Вместе с векторами можно сохранять дополнительную информацию (payload), например, product_id, bbox_id, категорию и любую метаинформацию, и быстро по ней фильтровать благодаря встроенному индексированию.
Поиск ближайших векторов при больших объёмах данных (например, 2 миллиона эмбеддингов) – нетривиальная задача: вычислять сходство по всем векторам слишком долго. Для ускорения работают два приёма:
Жёсткая фильтрация по payload (например, искать только среди товаров нужного магазина или категории).
Быстрый приближённый поиск ближайших соседей (ANN). В Qdrant используется HNSW – многоуровневый граф для поиска: верхние уровни отвечают за грубую навигацию по пространству, нижние – за точный локальный поиск. Qdrant начинает с верхнего уровня, быстро находит нужную область, а на нижнем уровне уточняет результат, что позволяет искать подходящие эмбеддинги за миллисекунды даже в очень большой базе.
Пайплайн на первом этапе выглядел просто: для каждого кропа (фрагмента с товаром) с каждой камеры мы считаем эмбеддинг. Если кроп размечен – сохраняем эмбеддинг и всю нужную метаинформацию (payload) в Qdrant. Если кроп не размечен – ищем ближайший эмбеддинг в search space, забираем product_id из найденного payload и используем его как предсказание класса. Такая схема дала около 85% accuracy по всем кропам – хорошее начало, но далеко не предел.
💡Как измеряем качество?
В реальном продакшне у нас нет фиксированного тестового датасета с “чистыми” разметками: ассортимент постоянно меняется, товары появляются и исчезают, разметка обновляется инкрементально. Поэтому основная end-to-end метрика – это доля предсказаний, которые разметчик не стал менять.
Разметчик получает предразметку (top-1 предсказание пайплайна). Если он подтверждает вариант, мы считаем это предсказание корректным. Такая метрика отражает, насколько часто система сразу выдаёт приемлемый для разметчика результат – то есть экономит его время и поддерживает стабильность процесса.
Есть нюанс: в спорных случаях разметчики склонны подтверждать предсказание даже при сомнениях, поэтому метрика всегда немного завышена и не совпадает с классической accuracy. Но для сравнения разных версий пайплайна в одинаковых условиях этот подход надёжен и хорошо показывает реальные улучшения качества.
Посмотрим на интересные кейсы ближайших кропов из search space, их cosine similarity и product_id:
Проблем у первой версии оказалось немало. Даже с хорошо обученным эмбеддером модель часто путает похожие товары – например, воду 0.5 л и 1 л, приправы с почти одинаковой упаковкой, апельсины и мандарины. Иногда причина – плохое качество снимков или освещение. Иногда – даже человек не отличит товар с такого ракурса (например, если упаковка повернута этикеткой назад). В любом случае, чисто визуальных признаков часто не хватает для уверенной классификации.
Но кропы не живут в вакууме. Каждый кроп – это часть полки, и по этой полке регулярно делается ручная разметка. Это даёт сильный пространственно-временной сигнал: какие товары недавно были размечены в этом месте? Эту информацию мы раньше игнорировали, а зря – она даёт мощное усиление точности.
Главная идея: добавить к визуальным фичам пространственный контекст – учесть историю разметок на этой конкретной полке. Это сложнее, чем просто “склеить” два источника информации [12]: нужно учесть их относительный вклад. Ключевые требования к алгоритму:
В первую очередь опираться на классы и оценки confidence из search space.
Приоритизировать последние разметки именно по этой полке, учитывая:
Насколько близко разметка по координатам (например, если на полке всегда слева кола, а справа фанта – у левого края больше вес у колы).
Актуальность разметки по времени: чем свежее разметка, тем выше её вес.
Такой комбинированный подход мы реализовали под названием realgram (по аналогии с planogram – схема “как должно быть”, а realgram – “как есть на самом деле”).
В основе всё тот же пайплайн: считаем эмбеддинг для кропа, ищем ближайшие варианты в Qdrant и достаём их payload. Далее добавляем realgram-логику:
Формируем realgram: Для текущей полки собираем объединённый список товаров из всех разметок за последние 30 дней. Алгоритм такой: если в одной разметке были товары 1, 2, 2, 3, а в другой – 2, 2, 3, 4, итоговый список – 1, 2, 2, 3, 4 (для каждого товара берём максимальное число вхождений среди разметок). Если разметок вообще не было – используем чистый пайплайн v0.
Вычисляем коэффициенты: Для каждого товара из этого списка считаем два значения:
Коэффициент устаревания (чем “свежее” разметка, тем выше вес; гиперпараметр – насколько быстро “стареет” разметка).
Коэффициент пересечения по координатам (чем ближе кроп к позиции ранее размеченного товара, тем выше вес; отдельный гиперпараметр).
Агрегируем: Для каждого уникального товара выбираем максимальный итоговый коэффициент, объединяя влияние времени и координат.
Комбинируем оценки: К скору товара из Qdrant добавляем итоговый коэффициент realgram, умноженный на ещё один гиперпараметр – насколько мы “верим” разметкам по сравнению с чистой моделью.
Финальное решение: Суммируем оба скора, выбираем топ-1 – это и есть финальное предсказание.
Да, коэффициентов немало (на деле их ещё больше), и тюнинг идёт “на глаз” или с помощью Optuna. Но результат того стоит: итоговая метрика выросла с 85% до 92%!
Визуализировать работу realgram сложно, но примерно это выглядит так:
Посмотрим на некоторые примеры, в которых эмбеддер предсказывает неправильно, а realgram исправляет предсказание в правильную сторону:
В итоге мы выстроили рабочий baseline: от простой схемы “эмбеддер + search space” с метрикой 85% до контекстного алгоритма realgram, который дал 92%. Это серьёзный скачок, но идеала пока далеко:
Сложные случаи не решены: на кропах с плохим светом или частично скрытым товаром система часто ошибается — тут спасает только контекст разметок, а не качество эмбеддинга.
“Мигания” предсказаний: на соседних кадрах с одной камеры при неизменном товаре результат может прыгать из-за нестабильности в топе search space.
Много гиперпараметров: в realgram десяток настроек, которые приходится тюнить вручную или через Optuna. Хочется более “автоматического” подхода.
Промахи топ-1: бывает, что топ-1 результат из search space — неправильный, но с очень высоким скором, а правильный товар оказывается в топ-2…10. Если бы учитывали всю “картину”, а не только первое место, могли бы заметно улучшить итог.
В следующих частях подробно расскажем, как мы решали эти проблемы: добавляли трекинг товаров, а также строили классификатор второго уровня.
Спасибо Арсению Корягину [13], Артёму Сметанину [14] и Александру Коротаевскому [15] – ведущим разработчикам этого проекта и авторам основной части материалов и текста.
Без вашей работы этой статьи не было бы.
Автор: Epoch8
Источник [16]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/24799
URLs in this post:
[1] Ошибки: http://www.braintools.ru/article/4192
[2] Cleanlab: https://github.com/cleanlab/cleanlab
[3] FiftyOne: https://github.com/voxel51/fiftyone
[4] Source: https://learnopencv.com/face-recognition-with-arcface/
[5] обучение: http://www.braintools.ru/article/5125
[6] RP2K: https://arxiv.org/abs/2006.12634
[7] Faiss: https://github.com/facebookresearch/faiss
[8] Milvus: https://github.com/milvus-io/milvus
[9] Qdrant: https://github.com/qdrant/qdrant
[10] Redis: https://github.com/redis/redis
[11] Source: https://www.pinecone.io/learn/series/faiss/hnsw/
[12] источника информации: http://www.braintools.ru/article/8616
[13] Арсению Корягину: https://www.linkedin.com/in/pixml/
[14] Артёму Сметанину: https://www.linkedin.com/in/artem-smetanin-25a9b5334/
[15] Александру Коротаевскому: https://www.linkedin.com/in/korotass/
[16] Источник: https://habr.com/ru/articles/989434/?utm_campaign=989434&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.