Компания
О комапнии
olga@epoch8.co
Контакты
Блог

Компьютерное зрение для ритейла часть 3: CatBoostRanker как второй уровень классификации товаров

Это третья часть цикла статей про классификацию товаров в ритейле. Предыдущие статьи - часть 1 и часть 2. Лучше их прочитать, чтобы не потеряться. Ниже краткое резюме, чтобы освежить память.

Recap

Задача

Задача перед нами стоит следующая: по фотографиям с камер, висящих в супермаркете, определить продукты на каждой полке. Это нужно для аналитики, поиска пропавших товаров и повышения on-shelf availability. Раз в несколько дней одно фото с каждой камеры попадает в ручную разметку. Пример фото с камеры ниже:
Типичное фото к с камеры и детекции от YOLO (и типичное фото самой камеры внизу)
Типичное фото к с камеры и детекции от YOLO (и типичное фото самой камеры внизу)

Пайплайны v0–v2

v0 (эмбеддер + search space)

Самое первое решение v0 состояло из эмбеддера из search space: вырезаем кропы по детекциям от YOLO, каждый кроп прогоняем через эмбеддер, получаем эмбеддинг, идем в Qdrant, в котором у нас хранится search space c эмбеддингом и метой для размеченных кропов, выбираем топ-1 по cosine similarity ближайший вектор из Qdrant и получаем класс продукта. Большой минус - учитываем только визуальный контекст, но не учитываем пространственный, но при этом такой простой способ давал метрику классификации около 85%.

v1 (v0 + realgram)

Решение v1 добавляло поверх поиска в Qdrant ещё и realgram – алгоритм, который учитывает то, что было на этой же полке в последних разметках. Если вкратце: для всех товаров из разметки считаем коэффициент устаревания и коэффициент пересечения по координатам, объединяем эти коэффициенты и добавляем к cosine similarity из пайплайна v0. В итоге получается более устойчивый алгоритм, учитывающий больше контекста и поднимающий метрику до 92%.

v2 (v1 + tracking)

В решении v2 мы добавили трекинг между кадрами, модифицировав алгоритм DeepSORT: на соседних кадрах сравниваем детекции сначала по IoU, а затем уже то, что сматчилось по IoU, сравниваем с помощью cosine similarity между эмбеддингами; если скор получается достаточно высоким, забираем предсказание с предыдущего снимка. Это дало повышение метрики с 92% → 94%, а также заметное снижение количества «миганий» (когда на соседних кадрах в одних и тех же координатах товар не меняется, но предсказания получаются разными).

Оставшиеся проблемы

Среди оставшихся проблем:
  • Очень много гиперпараметров - во всем пайплайне их около 15, их можно подбирать вручную (малоприятное занятие) или через Optuna, но можно нарваться на overfitting, потому что подбирать параметры всегда приходится на каком-то датасете.
  • Много кандидатов - для каждого кропа search space, realgram и трекинг могут нам давать до 20 кандидатов, из них выбрать правильный бывает довольно сложно, когда у нескольких кандидатов высокие скоры (а скоры сами по себе сравнивать сложно - у эмбеддера это cosine similarity, у realgram это cosine similarity + несколько коэффициентов, у трекинга это IoU score * cosine similarity).
Хочется иметь алгоритм, который принимал бы на вход все возможные кандидаты и все возможные фичи, и на основании этого выбирал, какой товар правильный. К счастью, такой алгоритм есть, но сначала поговорим про то, какие еще фичи можно использовать для принятия решений.

Контекстные фичи

Что мы уже учитываем?

Пройдемся по тем признакам, которые в текущем пайплайне v2 влияют на предсказания:
  • Из search space - из топ-20 ближайших кропов используем максимальные cosine similarity для каждого уникального продукта.
  • Из realgram - для каждого уникального кандидата максимальный скор (cosine similarity + добавочные коэффициенты устаревания/пересечения).
  • Из трекинга - финальный скор (IoU × cosine similarity).

Что можно еще учитывать?

Ниже приведена сводка типов признаков, которые мы рассматривали в рамках построения пайплайна, и того, как они в итоге использовались. Этот список не является рекомендацией или универсальным набором фичей, так как в разных сетапах полезными могут оказаться разные сигналы. Его цель - зафиксировать пространство решений и результаты экспериментального отбора конкретно в нашей задаче.
Источник Пример фичей Почему выбрали?
Search space max cosine, count in top-K, closest centroid Визуальное сходство
Realgram decay, overlap Пространственно-временной контекст
Tracking IoU, cosine similarity, anchor flag На соседних кадрах товары должны быть похожие
Metadata category, store, is fresh Лучше искать товары в пределах категории
Разметки Is in last annotation, count in last 3 annotations В последних разметках хранится самый сильный сигнал
Размеры товаров Размер кропа в пикселях/метрах, разница с размером товара из БД Размеры товара на полках и в БД должны быть близки
Ценники Совпадает ли ближайший ценник с ценой товара из БД, есть ли скидки Цена на полке и в БД должна совпадать
Если попытаться вручную учитывать все признаки, которые могут влиять на правильное предсказание, то… лучше даже не пытаться. Но поскольку все эти фичи числовые (а также бинарные и категориальные, то есть в общем-то тоже числовые), мы можем решать эту задачу классическим “табличным” способом.

Решение v3: v2 + Second Stage Classifier

Classification vs ranking

На первый взгляд, задачу «среди N классов выбрать правильный» можно решать с помощью бинарной классификации, но в нашем случае сразу появляется проблема: для разных кропов число кандидатов может сильно отличаться, так что классификация почти сразу отпадает, потом что модель классификации требует фиксированное количество классов (разве что искусственно ограничивать кандидаты до 2–3 штук, но тогда нужно понимать, как именно это делать).

Есть другой вариант — сформулировать эту задачу как ранкинг (ранжирование) с бинарным таргетом: среди произвольного количества кандидатов есть один правильный и все остальные неправильные. Такой подход сужает круг потенциальных инструментов, но идеально подходит под нашу задачу.

CatBoostRanker

Среди популярных “табличных” open-source библиотек почти все самые известные умеют обучать модели ранкинга - LightGBM, XGBoost, CatBoost. Выбор пал на CatBoost, так как у него есть наибольшее количество функций потерь и метрик для ранкинга, а также поскольку вся команда с ним уже была знакома и он всегда был хорошим бейзлайном.
Есть отличный туториал про ранкинг в CatBoost, там есть кроме прочего описание разных функций потерь, из которого мы поняли, что для нашей задачи идеально подходит QuerySoftMax. Дело в том, что обычно в задаче ранжирования нет одного правильного класса, но есть правильно отранжированный список кандидатов (первый - самый подходящий, второй - менее подходящий и так далее).

В нашей же задаче нет понятия “более/менее подходящий” - есть один правильный класс и все остальные кандидаты неправильные. Поэтому используем QuerySoftMax, который работает с бинарным таргетом. QuerySoftMax - это по большому счету обычный softmax + cross entropy, но в пределах каждой группы отдельно, а потом взвешивание и нормализация по размеру групп (в нашем случае одной группе соответствует один кроп, а все кандидаты, полученные из search space, realgram и трекинга, образуют группу для ранкинга с бинарным таргетом).
По всем группам считаем привычный нам softmax + cross entropy и суммируем, но кроме этого нормализуем на размер групп, потому что они могут сильно отличаться. Source
По всем группам считаем привычный нам softmax + cross entropy и суммируем, но кроме этого нормализуем на размер групп, потому что они могут сильно отличаться. Source
Обучать CatBoostRanker довольно легко: параметры в основном те же самые, что и при обучении стандартного бустинга-классификатора (depth, learning rate, регуляризация и все такое), плюс можно опять же использовать Optuna для подбора этих параметров, плюс можно обучать на GPU.

Фичей изначально было очень много (что-то около 100), но с помощью анализа feature importance и SHAP удалось сократить до 36 фичей без особой потери качества. При этом учитывалась сложность реализации фичей, потому что некоторые пришлось бы считать по всему search space (долго/дорого), а какие-то можно было просто взять из БД.

Пайплайн v3: добавляем катбуст поверх всего

Традиционно ничего не меняем в предыдущем пайплайне v2 — используем search space, realgram и трекинг, но на этот раз достаём из них все вспомогательные фичи, о которых говорилось выше.

Кроме этих фичей достаём ещё некоторые фичи размеров, разметок и категорий и получаем для каждого кропа таблицу размером N × M (N — количество кандидатов, у каждого кропа своё; M — количество фичей, у первой модели их 36), которую подаём в катбуст. Из катбуста мы получаем для всех кандидатов логиты, которые в пределах группы можно превратить в вероятности, но в этом нет особой необходимости, поскольку мы просто выбираем топ-1 среди них.
Кроп

Кроп

У этого кропа 3 кандидата, здесь в таблицу некоторые фичи для этого кропа (base - фичи search space, rg - фичи realgram, tr - фичи трекинга) + логиты от катбуста в последнем столбце, по которому мы и выбираем топ-1

У этого кропа 3 кандидата, здесь в таблицу некоторые фичи для этого кропа (base - фичи search space, rg - фичи realgram, tr - фичи трекинга) + логиты от катбуста в последнем столбце, по которому мы и выбираем топ-1

Пайплайн v3: из search space, realgram, трекинга и БД берем фичи, агрегируем и подаем в модель второго уровня

Пайплайн v3: из search space, realgram, трекинга и БД берем фичи, агрегируем и подаем в модель второго уровня

Помимо того что такой подход увеличил метрику с 94% до 96%, он открыл нам ящик Пандоры в плане экспериментов: можно было проводить тонну экспериментов с разными фичами (большинство из которых, правда, прироста в метрике не давали).

Пайплайн v3.1: добавляем фичи против “миганий”

Какое-то время мы жили с катбустом на 36 признаках, пробовали новые признаки, собирали новые датасеты, но метрика толком не менялась. Затем мы обнаружили, что снова начали генерировать «мигания». Почему? Потому что раньше финальное предсказание почти всегда делал трекинг, который просто перетаскивал предсказание с предыдущего кадра на текущий, а теперь его предсказание стало по сути одной из фичей, но катбуст принимал решение на основе всех фичей, а не только трекинга.
Примеры “миганий” в одних и тех же координатах на соседних фотографиях:
Товар совершенно не меняется, но предсказания скачут туда-сюда между двумя классами

Товар совершенно не меняется, но предсказания скачут туда-сюда между двумя классами

Абрикосы ловким движением рук превращаются в апельсины

Абрикосы ловким движением рук превращаются в апельсины

Тунец, осетр или вообще масляная? На ответ дается 30 секунд

Тунец, осетр или вообще масляная? На ответ дается 30 секунд

Что хотелось бы сделать: чтобы «миганий» было меньше, но при этом не просто перетаскивать предсказание с прошлого кадра на текущий, чтобы не потерять в качестве. Решение — набор из 13 фичей (итого в катбусте 49), вычисляемых на основании предсказаний в одних и тех же координатах на предыдущих N снимках с этой камеры. Некоторые из них:
  • Был ли этот кандидат предсказан в этих координатах на прошлом снимке? На прошлых 3? На прошлых 5?
  • Сколько продуктов (уникальных/всего) было предсказано в этих координатах на предыдущих N снимках?
  • Является ли этот кандидат самым часто встречающимся в этих координатах на предыдущих N снимках?
Эти фичи помогли - они позволили не потерять в качестве (это важнее всего!), но при этом снизили количество “миганий” относительно катбуста без этих фичей на 40% (здесь мы уже собрали небольшой датасет, на котором считали метрики “миганий”, но и качественный анализ на сложных категориях тоже проводили), и вернули их примерно на тот уровень, который был в пайплайне v2 без катбуста.

Что не помогло?

За все время мы попробовали, наверное, несколько сотен разных фичей, и большая часть из них нам не помогала, но после каждого эксперимента мы пытались как-то интерпретировать эту неудачу, почему именно они не помогли. Среди интересных экспериментов:
Фичи размеров
Какие фичи: размеры кропа в пикселях/метрах, разница между размеров кропа и размеров товара из БД, z-score между текущим диффом и историческими диффами и так далее.
Почему не помогли: качественный анализ показал, что они помогают в избранных случаях (различить бутылку 0.5 от бутылки 1.5 или горошек 200г от 400г), но в большинстве случаев они только вносили шум, поскольку для нескольких вкусов одного и того же товара они не имеют смысла и никак не помогут.
Фичи ценников
Какие фичи: распознаны ли ближайшие ценники, значение ближайшего ценника слева/справа, совпадает ли значение ценника слева/справа с ценой товара из БД.
Почему не помогли: OCR работал неплохо, но не на всех камерах (проблемы с освещением и с качеством); в БД не всегда обновлялись цены со скидками; правильный ценник для текущего товара мог быть слева/справа/сверху/снизу.
Фичи геометрии эмбеддингов
Какие фичи: является ли центроид этого класса ближайшим к эмбеддингу кропа, silhouette score при кластеризации всех эмбеддингов кандидатов для текущего кропа и прочие.
Почему не помогли: как бы мы сильно не надеялись на ArcFace, для похожих классов эмбеддинги все равно были похожие, и на визуализациях UMAP / t-SNE / PCA многие классы сложно было отделить друг от друга.

Результаты

Мы реализовали классификатор (точнее, как мы уже выяснили, ранкер) второго уровня, который принимает в себя 49 фичей из совершенно разных источников и на основании них делает предсказание, повышая качество с 94% до 96%. На этом пока что интересные детали реализации нашего пайплайна заканчиваются, но, как мы все знаем, бесконечность - не предел, поэтому хотелось бы все равно понимать, что же это за противные 4% ошибок.
Анализ ошибок показал, что большая часть из них - это мета-ошибки, которые не зависят от нашего пайплайна:
  • Ошибки разметки - сложные случаи, где разметчик указывает класс, похожий на правильный; они влияют как на метрику, так и на предсказания (попадают в search space и в фичи катбуста);
  • Перевешенные и сбитые камеры - обнуляется realgram, а поскольку это довольно сильная фича, то и все предсказания тоже портятся;
  • Новые товары - мы даже теоретически не можем правильно их предсказать, так как их просто нет в search space и среди кандидатов (про такие товары и то, как мы их искали для ускоренной разметки, будет отдельная статья).
Построение такого сложного пайплайна (многие детали которого пришлось опустить, чтобы читатель не заскучал) и миллион разных экспериментов заняли больше года, но нам кажется, что получился отличный и довольно устойчивый пайплайн, а цикл наших статей сможет помочь кому-нибудь, кто сталкивается с подобной задачей!

Огромное спасибо нашим инженерам, Александру Коротаевскому и Артему Сметанину, за подготовленный материал.
Компьютерное зрение ИИ