Как мы научили нейросеть распознавать товары на полках: реальный опыт ритейла. компьютерное зрение.. компьютерное зрение. распознавание товаров.. компьютерное зрение. распознавание товаров. ритейл.

Зачем нужна классификация товаров на полках в ритейле?

Представьте, что вы управляете крупной сетью супермаркетов. Вы контролируете поставки на склад, знаете продажи по чекам, по камерам видите поток покупателей и их маршруты в зале. Но вот что реально происходит на полках остается “черным ящиком”:

  • Есть ли на месте Coca-Cola в отделе напитков?

  • Не закончились ли акционные мандарины?

  • Не оставил ли кто-то бутылку пива среди детских игрушек?

Если бы вы знали состояние полок в реальном времени, можно было бы оперативно отправлять сотрудника исправлять конкретные проблемы, а не надеяться, что он заметит их во время очередного обхода. Это позволяет увеличить заполняемость полок (on-shelf availability, OSA): чем больше товаров на полках – тем выше потенциальная выручка. Покупатель всегда найдет нужный товар или сделает импульсивную покупку, если ассортимент перед глазами полный.

Классификация в нашем случае – масштабы, цели и сложности

Мы работали с двумя сетями супермаркетов: начинали с пилота в небольшом магазине “у дома”, затем масштабировались на два флагманских супермаркета известной федеральной сети. Задача – распознавать все товары на всех полках. Дальше на этих данных строится аналитика и автоматизируются операции по пополнению ассортимента, но сейчас сфокусируемся именно на распознавании.

Основные сложности:

  • Необходимо развернуть камеру-инфраструктуру: камеры должны делать снимки каждые 30 минут и передавать их в систему.

  • В магазинах разные зоны освещаются по-разному, из-за чего одна и та же кола в холодильнике и на стеллаже может выглядеть совершенно иначе.

  • Проблема холодного старта: запуск в новом магазине с неизвестным ассортиментом.

  • Ассортимент постоянно меняется, полки регулярно переставляются – и эти процессы мы никак не контролируем.

Это лишь часть списка: о ключевых технических и бизнес-нюансах расскажем в отдельных материалах. Здесь разберём самые интересные моменты, связанные именно с задачей классификации товаров на полках.

Данные и разметка

Данные

После установки камер и настройки инфраструктуры у нас появилось N камер, которые каждые 30 минут делают снимки и автоматически сохраняют их в S3. На этом этапе мы сразу столкнулись с тем, насколько разными могут быть фотографии одной и той же полки с разных камер:

  • Различия в освещении (от “жёлтого” света до синевы холодильника);

  • Углы обзора: одни камеры смотрят прямо, другие – под углом или с большого расстояния;

  • Наличие бликов, теней, частичных перекрытий товаров.

Примеры этих различий – ниже на иллюстрациях.

Стандартный снимок, на котором все хорошо видно

Стандартный снимок, на котором все хорошо видно
Перспективные искажения + засветы (черные края по бокам – это уже другие стеллажи, они тут просто замазаны)

Перспективные искажения + засветы (черные края по бокам – это уже другие стеллажи, они тут просто замазаны)
Маленькие и похожие друг на друга товары. Не смог прочитать текст на упаковке – проиграл

Маленькие и похожие друг на друга товары. Не смог прочитать текст на упаковке – проиграл
Препятствия в виде дверей холодильников и бликов на стеклах

Препятствия в виде дверей холодильников и бликов на стеклах

Сначала снимки с камер приходится фильтровать: на них часто попадают люди, тележки, коробки и другие препятствия. Детектирование и удаление таких помех – отдельная большая задача, о ней расскажем в другой статье.

Далее для каждого оставшегося кадра запускается детектор объектов – в нашем случае свежая версия YOLO, тут всё стандартно. На выходе мы получаем то, что нужно для классификации: кропы (фрагменты изображений), вырезанные по координатам найденных товаров, для каждого снимка с каждой камеры. Именно эти кропы и будут дальше поступать на этап классификации.

Разметка

Не существует готовой модели, которая безошибочно определит товар на фото при каталоге в 20+ тысяч позиций. Поэтому без человеческой разметки не обойтись. Сначала мы пробовали Label Studio, но быстро упёрлись в ограничения по функционалу и разработали свой собственный инструмент, заточенный именно под классификацию товаров на полках.

Детали оптимального объёма разметки – отдельная тема, здесь расскажем только о сути: с каждой камеры один кадр раз в несколько дней мы размечали вручную, присваивая каждому кропу корректный product_id. Ошибки в разметке неизбежны, но для ряда категорий они стали главным узким местом: пайплайн для этих товаров работал отлично, а заниженная точность была вызвана только неверными метками.

Мы перепробовали и автоматические методы чистки (поиск аномалий через Cleanlab, кластеризацию эмбеддингов) и ручные (отбор подозрительных классов, визуализация в FiftyOne, повторная разметка). Но из-за сильного внешнего сходства между многими товарами автоматическая фильтрация оказалась ненадёжной, баланс precision/recall не удавалось удержать на приемлемом уровне.

Итог – старый добрый ручной пересмотр сработал эффективнее всего.

Решение v0: embedder + search space

Classification vs metric learning

Переходим к основной задаче: как классифицировать товары на полках по кропам с камер. Базовый вариант — классическая классификация: обучаем модель на 20 тысячах товаров и получаем вероятностное распределение по всему каталогу. Это просто, но плохо работает в динамике – ассортимент регулярно обновляется, появляются новые продукты, которые модель не видела, и она ошибочно относит их к уже известным классам.

Реальный рабочий подход – metric learning. Вместо классификатора мы учим модель-эмбеддер: она преобразует каждое изображение в вектор признаков (эмбеддинг). Для каждого размеченного кропа мы сохраняем его эмбеддинг – и это становится нашим search space. Когда появляется новый товар, достаточно размечать его пару раз – эмбеддинги сразу попадают в search space, и эмбеддер сможет находить похожие примеры среди них, корректно определяя класс даже для новых товаров.

То есть вся система строится вокруг двух компонентов: хорошего эмбеддера и search space, который пополняется по мере появления новых товаров.

Эмбеддер & ArcFace

Мы перебрали разные компактные архитектуры (ConvNext, ViT, Swin и др.) и остановились на ViT-Base-32 – оптимальный баланс между скоростью и качеством на ограниченных вычислительных ресурсах.

Как обучить эмбеддер, чтобы он действительно различал похожие товары? Здесь пригодится трюк из области распознавания лиц – подход ArcFace. Суть его в том, что модель учится строить для каждого товара свой “отпечаток” – вектор (эмбеддинг), который можно сравнить с другими.

ArcFace использует специальную функцию потерь: она не просто наказывает модель за ошибку, а заставляет эмбеддинги одного и того же товара быть максимально похожими (располагаться рядом в пространстве признаков), а эмбеддинги разных товаров – максимально разными (располагаться под разными углами на воображаемой сфере). Это позволяет системе легко отличать даже очень похожие товары: модель буквально учится “отталкивать” разные классы друг от друга и “сжимать” эмбеддинги одного класса.

В результате получается компактное и хорошо структурированное пространство эмбеддингов, где поиск нужного товара сводится к простому сравнению векторов.

В итоге, такой подход даёт очень “чистое” пространство признаков: похожие товары собираются в плотные группы, а разные – явно отделены друг от друга. Благодаря этому, если нужно быстро найти тот же товар на другом фото (или похожий), система просто ищет ближайший “отпечаток” среди уже известных – это и есть поиск ближайших соседей.

Deduplication и кластеризация (то есть удаление дублей и группировка похожих товаров) тоже становятся проще: модель легко отличает даже очень похожие, но разные продукты. Главное преимущество – этот метод работает стабильно, даже если в реальных магазинах появляется много новых товаров, которые не встречались в обучающей выборке.

Эмбеддинги, полученные моделью с ArcFace, имеют большие расстояния между классами и небольшие внутри класса, поэтому их легко отделять друг от друга. Source

Эмбеддинги, полученные моделью с ArcFace, имеют большие расстояния между классами и небольшие внутри класса, поэтому их легко отделять друг от друга. Source

Детали обучения эмбеддера

  • Размер эмбеддинга: 512 – классика жанра для задач такого типа, компромисс между информативностью и скоростью.

  • Batch size: Чем больше, тем лучше. Большой батч позволяет за одну итерацию показать модели больше уникальных товаров и сделать обучение устойчивее. После перехода с RTX 3090 на H100 и увеличения batch size качество даже немного выросло.

  • Сэмплирование: Batch balanced по компании: в каждом батче только изображения товаров одной сети (у нас было два вендора). Для каждого класса в батче брали по четыре кропа. На инференсе поиск кандидатов тоже ограничивали внутри компании. В планах — сэмплирование по категориям: в батче в основном одна категория, чтобы учить модель различать не «колу и печенье», а разные сорта печенья между собой.

  • Претрейн: Экспериментально выяснили, что лучший результат даёт предварительное обучение (pretrain) на публичном датасете RP2K, а затем уже дообучение (fine-tune) на собственных данных.

Search space

Search space – это база всех эмбеддингов, по которой нужно максимально быстро находить ближайшие к заданному эмбеддингу (поиск ближайших соседей, KNN или ANN). Для хранения таких данных есть несколько популярных решений: Faiss, Milvus, Qdrant, а также встроенные векторные индексы в Redis. Мы выбрали Qdrant – он прост в настройке, легко масштабируется и закрывает все необходимые сценарии.

Пайплайн такой: берём все размеченные кропы, прогоняем через эмбеддер, складываем получившиеся эмбеддинги в Qdrant. Вместе с векторами можно сохранять дополнительную информацию (payload), например, product_id, bbox_id, категорию и любую метаинформацию, и быстро по ней фильтровать благодаря встроенному индексированию.

Поиск ближайших векторов при больших объёмах данных (например, 2 миллиона эмбеддингов) – нетривиальная задача: вычислять сходство по всем векторам слишком долго. Для ускорения работают два приёма:

  1. Жёсткая фильтрация по payload (например, искать только среди товаров нужного магазина или категории).

  2. Быстрый приближённый поиск ближайших соседей (ANN). В Qdrant используется HNSW – многоуровневый граф для поиска: верхние уровни отвечают за грубую навигацию по пространству, нижние – за точный локальный поиск. Qdrant начинает с верхнего уровня, быстро находит нужную область, а на нижнем уровне уточняет результат, что позволяет искать подходящие эмбеддинги за миллисекунды даже в очень большой базе.

Многоуровневый граф в HNSW. Source (отличная статья, советуем!)

Многоуровневый граф в HNSW. Source (отличная статья, советуем!)

Пайплайн v0: голый эмбеддер

Пайплайн на первом этапе выглядел просто: для каждого кропа (фрагмента с товаром) с каждой камеры мы считаем эмбеддинг. Если кроп размечен – сохраняем эмбеддинг и всю нужную метаинформацию (payload) в Qdrant. Если кроп не размечен – ищем ближайший эмбеддинг в search space, забираем product_id из найденного payload и используем его как предсказание класса. Такая схема дала около 85% accuracy по всем кропам – хорошее начало, но далеко не предел.

💡Как измеряем качество?

В реальном продакшне у нас нет фиксированного тестового датасета с “чистыми” разметками: ассортимент постоянно меняется, товары появляются и исчезают, разметка обновляется инкрементально. Поэтому основная end-to-end метрика – это доля предсказаний, которые разметчик не стал менять.

Разметчик получает предразметку (top-1 предсказание пайплайна). Если он подтверждает вариант, мы считаем это предсказание корректным. Такая метрика отражает, насколько часто система сразу выдаёт приемлемый для разметчика результат – то есть экономит его время и поддерживает стабильность процесса.

Есть нюанс: в спорных случаях разметчики склонны подтверждать предсказание даже при сомнениях, поэтому метрика всегда немного завышена и не совпадает с классической accuracy. Но для сравнения разных версий пайплайна в одинаковых условиях этот подход надёжен и хорошо показывает реальные улучшения качества.

Примеры

Посмотрим на интересные кейсы ближайших кропов из search space, их cosine similarity и product_id:

Майонез лежит, а не стоит, поэтому ищется с трудом

Майонез лежит, а не стоит, поэтому ищется с трудом
Два часто путающихся товара – сок с апельсином и грейпфрутом

Два часто путающихся товара – сок с апельсином и грейпфрутом
Квест: нужно отличить банку 160г от банки 190г

Квест: нужно отличить банку 160г от банки 190г
Кроп затемнен, поэтому в топ-5 у нас появляется аж 5 разных продуктов

Кроп затемнен, поэтому в топ-5 у нас появляется аж 5 разных продуктов

Решение v1: v0 + realgram

Проблемы подхода v0

Проблем у первой версии оказалось немало. Даже с хорошо обученным эмбеддером модель часто путает похожие товары – например, воду 0.5 л и 1 л, приправы с почти одинаковой упаковкой, апельсины и мандарины. Иногда причина – плохое качество снимков или освещение. Иногда – даже человек не отличит товар с такого ракурса (например, если упаковка повернута этикеткой назад). В любом случае, чисто визуальных признаков часто не хватает для уверенной классификации.

Пространственный контекст: дополняем визуальные признаки

Но кропы не живут в вакууме. Каждый кроп – это часть полки, и по этой полке регулярно делается ручная разметка. Это даёт сильный пространственно-временной сигнал: какие товары недавно были размечены в этом месте? Эту информацию мы раньше игнорировали, а зря – она даёт мощное усиление точности.

Главная идея: добавить к визуальным фичам пространственный контекст – учесть историю разметок на этой конкретной полке. Это сложнее, чем просто “склеить” два источника информации: нужно учесть их относительный вклад. Ключевые требования к алгоритму:

  1. В первую очередь опираться на классы и оценки confidence из search space.

  2. Приоритизировать последние разметки именно по этой полке, учитывая:
    Насколько близко разметка по координатам (например, если на полке всегда слева кола, а справа фанта – у левого края больше вес у колы).
    Актуальность разметки по времени: чем свежее разметка, тем выше её вес.

Такой комбинированный подход мы реализовали под названием realgram (по аналогии с planogram – схема “как должно быть”, а realgram – “как есть на самом деле”).

Пайплайн v1: к эмбеддеру добавляем realgram

В основе всё тот же пайплайн: считаем эмбеддинг для кропа, ищем ближайшие варианты в Qdrant и достаём их payload. Далее добавляем realgram-логику:

  1. Формируем realgram: Для текущей полки собираем объединённый список товаров из всех разметок за последние 30 дней. Алгоритм такой: если в одной разметке были товары 1, 2, 2, 3, а в другой – 2, 2, 3, 4, итоговый список – 1, 2, 2, 3, 4 (для каждого товара берём максимальное число вхождений среди разметок). Если разметок вообще не было – используем чистый пайплайн v0.

  2. Вычисляем коэффициенты: Для каждого товара из этого списка считаем два значения:

    Коэффициент устаревания (чем “свежее” разметка, тем выше вес; гиперпараметр – насколько быстро “стареет” разметка).

    Коэффициент пересечения по координатам (чем ближе кроп к позиции ранее размеченного товара, тем выше вес; отдельный гиперпараметр).

  3. Агрегируем: Для каждого уникального товара выбираем максимальный итоговый коэффициент, объединяя влияние времени и координат.

  4. Комбинируем оценки: К скору товара из Qdrant добавляем итоговый коэффициент realgram, умноженный на ещё один гиперпараметр – насколько мы “верим” разметкам по сравнению с чистой моделью.

  5. Финальное решение: Суммируем оба скора, выбираем топ-1 – это и есть финальное предсказание.

Да, коэффициентов немало (на деле их ещё больше), и тюнинг идёт “на глаз” или с помощью Optuna. Но результат того стоит: итоговая метрика выросла с 85% до 92%!

Визуализировать работу realgram сложно, но примерно это выглядит так:

realgram скоры для выделенного кропа: “Рис и греча” больше всего подходят по координатам и по свежести, а “Дружба” и “Гречка ядрица” тоже подходят по свежести, но по пересечению меньше меньше (S - коэффициент устаревания, SX - коэффициент устаревания вместе с пересечением)

realgram скоры для выделенного кропа: “Рис и греча” больше всего подходят по координатам и по свежести, а “Дружба” и “Гречка ядрица” тоже подходят по свежести, но по пересечению меньше меньше (S – коэффициент устаревания, SX – коэффициент устаревания вместе с пересечением)

Примеры

Посмотрим на некоторые примеры, в которых эмбеддер предсказывает неправильно, а realgram исправляет предсказание в правильную сторону:

Разница в cosine similarity минимальная, так как отличается только текст на фото, но realgram подсказывает по местоположению

Разница в cosine similarity минимальная, так как отличается только текст на фото, но realgram подсказывает по местоположению
Тут отличие в объеме, эмбеддер такого не видит, но realgram подсказывает

Тут отличие в объеме, эмбеддер такого не видит, но realgram подсказывает
Опять же очень похожие значение cosine similarity, но видимо в более свежих разметках был правильный товар (или по координатам он был ближе)

Опять же очень похожие значение cosine similarity, но видимо в более свежих разметках был правильный товар (или по координатам он был ближе)
Тут вообще нельзя однозначно сказать, что за товар, но наверняка этикетки и местоположение отличается (разметчику здесь виднее)

Тут вообще нельзя однозначно сказать, что за товар, но наверняка этикетки и местоположение отличается (разметчику здесь виднее)

Результаты

В итоге мы выстроили рабочий baseline: от простой схемы “эмбеддер + search space” с метрикой 85% до контекстного алгоритма realgram, который дал 92%. Это серьёзный скачок, но идеала пока далеко:

  • Сложные случаи не решены: на кропах с плохим светом или частично скрытым товаром система часто ошибается — тут спасает только контекст разметок, а не качество эмбеддинга.

  • “Мигания” предсказаний: на соседних кадрах с одной камеры при неизменном товаре результат может прыгать из-за нестабильности в топе search space.

  • Много гиперпараметров: в realgram десяток настроек, которые приходится тюнить вручную или через Optuna. Хочется более “автоматического” подхода.

  • Промахи топ-1: бывает, что топ-1 результат из search space — неправильный, но с очень высоким скором, а правильный товар оказывается в топ-2…10. Если бы учитывали всю “картину”, а не только первое место, могли бы заметно улучшить итог.

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

Спасибо Арсению Корягину, Артёму Сметанину и Александру Коротаевскому – ведущим разработчикам этого проекта и авторам основной части материалов и текста.

Без вашей работы этой статьи не было бы.

Автор: Epoch8

Источник

Rambler's Top100