Привет, Хабр! На связи команда продуктового матчинга ecom.tech. Сегодня расскажем вам про машинное обучение под капотом сопоставления товаров на Мегамаркете.

Зачем нужен продуктовый матчинг и какие там могут быть сложности – обсуждали в прошлой серии. В этом эпизоде – как матчер Мегамаркета сравнивает ключевые сущности товаров и почему именно так.

Надеемся, статья будет полезна ML-инженерам (обсуждаем в тексте алгоритмы и математику), а также всем, кто стремится глубже разобраться в прикладном машинном обучении, на примере сервиса, которым вы, возможно, пользуетесь.

Часть 2. Две твердыни 

Disclaimer: все названия вымышлены, все совпадения случайны. Кроме Мегамаркета – он настоящий.

Твердыня первая: изображения

Картинки – одна из основополагающих характеристик товара, которая помогает делать сопоставление. Часто именно изображение помогает принять финальное решение: являются ли два товара идентичными или нет. Значит, в матчере должна быть модель (или несколько), которые умеют хорошо различать изображения. 

Прежде всего надо понять, в какой постановке мы будем дообучать модель для изображений. Постановка мультиклассовой классификации здесь не годится – ассортимент товаров исчисляется миллионами, и делать полносвязную классифицирующую “голову” таких размеров может разве что GPU-камикадзе. 

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

Две газонокосилки. Но два изображения относятся к одному и тому же товару, просто под разным ракурсом (такие картинки – распространенная практика на маркетплейсах). А вот изображение посередине – это уже другая газонокосилка. Требовать от классификатора строить разделяющую гиперплоскость такого уровня сложности было бы весьма некрасиво с нашей стороны (а любой ML-инженер чтит этикет, как известно). 

Что делать? Вспоминаем про семейство моделей типа CLIP.

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

И действительно, к каждой картинке есть наименование и атрибуты. Так точно всё должно получиться, текстовая информация подскажет энкодеру изображений, что газонокосилки разные.

Но не будем спешить радоваться… Когда мы начнем изучать внимательно ассортимент, то, например, в категории “Одежда” нас ждут сюрпризы. У нас может быть четыре товара: футболка мужская красная, синяя, белая, черная. А вот фотография, так случается, может быть только одна: футболка белая. На маркетплейсе, конечно, будет отражена возможность выбора цвета. Так что в CLIP пойдет одна картинка и четыре разных текста к ней – нормально дообучить модель, увы, не получится. 

Вспомним про еще одну возможность научить модель разделять близкие изображение – metric learning. Суть подхода заключается в том, чтобы разные изображения одного и того же класса делать как можно ближе (по какой-нибудь метрике), а изображения всех других классов делать как можно дальше. 

В свое время именно этот подход позволил успешно решить задачу по распознаванию лиц. Для этого использовалась довольно хитрая функция потерь ArcFace loss. Она позволяет очень хорошо улавливать даже малейшие отличия на изображениях и делает это гораздо лучше остальных функций потерь для этой задачи.

Это достигается за счет наличия отступа по умолчанию между классами, преодолеть который нельзя в принципе. Мы решили пойти еще дальше. 

Формула ниже – это и есть формула для ArcFace. Там же в формуле мы видим m – тот самый отступ. Функция потерь состоит из двух частей: T отвечает за разные изображения одного класса (позитивное сэмплирование), а N отвечает за разные классы (негативное сэмплирование). 

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

Поэтому выглядит логичным усложнить часть для негативного сэмплирования и сделать ее адаптивной (параметр t в формуле меняется по время обучения). Такая функция потерь получила название Curricular Face и на многих бенчмарках обошла ArcFace (в том числе по распознаванию лиц). По этой причине наш выбор пал именно на нее. 

Отлично, функция потерь подобрана. Теперь выбираем саму модель. К счастью, здесь выбор в плане архитектуры невелик: либо сверточная нейросеть, либо трансформер. После ряда экспериментов с лучшими сверточными архитектурами, мы попробовали модель на основе vision transformer (ViT), она побила все предыдущие модели, так что мы остановили свой выбор именно на ней. 

Итак, мы взяли ViT-модель, собрали изображения товаров, применили к ним различные аугментации (чтобы еще более усложнить процесс обучения и приблизить к реальным трудностям) и дообучили модель с функцией потерь Curricular Face. 

В процессе обучения learning rate мы меняли с помощью scheduler (планировщика) One Cycle LR, поскольку обучали мы трансформер, а, как известно, трансформеры любят “погорячее” (в том смысле, что для устойчивого обучения необходим предварительный “прогрев”, в виде стремительного увеличения learning rate). И все получилось! 

Наша модель начала очень уверенно различать даже очень похожие изображениях разных товаров. 

Повышаем качество распознавания с помощью ранжировщика 

У нас есть энкодер, который нам нравится – для каждого изображения выдает качественный эмбеддинг. А что, если использовать информацию из эмбеддингов напрямую? Прогоняем изображения через ViT-модель, она будет играть роль backbone. Получаем эмбеддинги и будем использовать их для построения новых признаков. 

Для каждой пары товар-кандидат мы строим на их эмбеддингах различные метрические признаки (скалярное произведение, l2-норму и другие). В итоге есть  обычные табличные данные с метками 0 и 1, для каждой пары есть ровно один матч – получаем классическую задачу бинарной классификации. А что может быстро и эффективно ее решить? Логистическая регрессия! Таким образом, наша модель для изображений состоит из двух частей и выглядит вот так:

Добавление такого простого ранжировщика позволило поднять качество сразу на 5%! 

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

Твердыня вторая: текст

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

Нам нужно определять, насколько атрибуты и описания товаров схожи, то есть, по сути, понимать насколько два предложения близки между собой. Очень похоже на то, что перед нами задача определения косинусной близости между двумя векторами (эмбеддинги двух товаров). Такую задачу для текста удобней всего решать с помощью моделей типа bi-encoder. 

Bi-encoder устроен следующим образом: берется бертоподобная модель в качестве backbone, после чего с помощью pooling-слоя агрегируется информация на выходе из backbone и получаются два эмбеддинга, которые сравниваются с помощью функции косинусной близости. Схема модели представлена ниже. 

Понятно, что “сердцем” bi-encoder является бертоподобная модель. Какие требования мы выдвигали к ней? Поскольку это будет кандидатная модель, то она должна быть легкой и быстрой (у нас миллионы товаров). При этом одна должна давать качественные эмбеддинги (иначе зачем это все) и поддерживать большой контекст (например, описание лекарств может занимать несколько тысяч токенов). Для этого нужно правильно собрать выборку. 

На Мегамаркете дерево категорий имеет шесть уровней. Чем больше уровень, тем выше степень конкретики принадлежности товара. Например, возьмем категорию уровня 1 “Электроника”. Сюда может попасть ноутбук, телевизор, планшет, фен и другое. А вот категория уровня 3 внутри “Электроники” будет уже, например, “Телевизоры”. Соответственно, степень близости товаров повышается от категории 1 к категории 6. И при формировании обучающей выборки для bi-encoder это очень кстати. 

У каждого товара должно быть ровно одно совпадение. Значит, нам нужны качественные негативы. Поэтому мы начинаем формирование негативов с уровня 6, ведь именно там находятся самые похожие между собой товары. Собираем k негативов для каждого товара из обучающей выборки. Что делать, если мы не сможем для какого-то товара собрать k негативов? Не беда, поднимаемся по дереву категорий на уровень 5 и добираем негативы оттуда. А если и там вдруг их будет недостаточно, то поднимемся еще выше, и так вплоть до уровня 1. 

Таким образом, наша выборка будет содержать как очень качественные негативы (а значит довольно сложные примеры для модели), так и совсем простые. Такая разнообразная обучающая выборка позволяет сделать из bi-encoder очень хорошую кандидатную модель.

Хорошо, вот мы получили текстовые (n/2) и картиночные (n/2) кандидаты, всего n кандидатов. Выбор оптимального n производится исходя из доступного количества вычислительных ресурсов, обычно это число варьируется от 30 до 100. Но ведь среди них есть только один товар, который является совпадением, а остальные нам не интересны. 

Нам нужна двойная проверка – еще один ранжировщик, который посмотрит на всех кандидатов и даст свой вердикт. Поскольку текст является более информативным (помним про кейс “четыре цвета футболки, одна фотография”), ранжировщик у нас тоже будет текстовым. Значит, берем текстовые описания товаров картиночных и текстовых кандидатов и… подаем на вход в cross-encoder. 

Что это за зверь такой? Это тоже бертоподобная модель, но в отличие от bi-encoder здесь два предложения подаются не независимо, а через токен-разделитель, и модель смотрит одновременно на первое, и на второе предложения с помощью перекрестного self-attention. На выходе же стоит полносвязный слой классификатор, который выдает только одно число – насколько два предложения близки между собой.

Требования к этой бертоподобной модели несколько иные: она должна быть мультиязычная (описания товаров очень часто содержат несколько языков, как правило русский и английский) и тяжелая (нам нужны очень качественные эмбеддинги). Как обучить такую модель? 

Здесь cross-encoder играет роль ранжировщика, точно так же как и логистическая регрессия в картиночной кандидатной модели. Для его обучения нам нужны готовые пары товар-кандидат. Их мы берем из bi-encoder. Тогда получается следующая схема: векторизуем текстовые описания товаров с помощью bi-encoder, далее по векторизованной им же заранее базе текстовых описаний товаров маркетплейса находим наиболее подходящие вектора и получаем пары товар-кандидат. После этого проставляем метки 0 и 1;мы используем уже заведенные на витрину товары-кандидаты, поэтому знаем, какая пара точно является совпадением; далее обучаем cross-encoder. 

Разумеется, тот факт, что cross-encoder является довольно тяжеловесной моделью, сказывается на скорости работы, но нам нужно проранжировать уже не всю базу, а только отобранных кандидатов с предыдущего шага. Однако ресурсов тоже должно быть достаточно (1 GPU A100 на 80 Гб – прекрасный вариант). Зато у нас теперь есть качественный ранжировщик, который способен отделять, как говорится, зерна от плевел. 

Итак, продуктовый матчер

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

Давайте еще раз пройдем все этапы:

  1. Ассортимент заранее векторизован всеми моделями и выступает в роли поисковой базы.

  2. Товары, для которых нужно найти идентичные на маркетплейсе, поступают в кандидатные модели: картиночную (ViT + ранжировщик на логистической регрессии) и текстовую (предварительно текст очищаем и предообрабатываем).

  3. С помощью FAISS находим кандидатов – половину от картиночной модели и половину от bi-encoder. Все вычисления проделываем на GPU, разумеется.

  4. Для всех полученных кандидатов загружаем текстовые описания товаров в cross-encoder, и для каждой пары кандидатов (в нашем случае это “пришедший товар” и “товар из нашей базы”) получаем score близости от cross-encoder.

  5. Добавляем к score от cross-encoder цену товара (помним, что это может здорово помочь для определения реплик), категорию и еще несколько табличных признаков.

  6. Полученные табличные данные подаем на вход модели на основе CatBoost (уже обученной нами) и получаем список совпадений.

  7. Для каждой категории калибруем порог score от CatBoost (с какого момента товар будет считаться совпадением), берем топ-1 кандидата для каждого товара, получаем финальные матчи.

Ура! Мы нашли для каждого товара идентичный. Вытираем пот со лба и уставшие, но довольные, отправляем их категорийным менеджерам и в команду динамического ценообразования.

Наш матчер готов и справляется с поставленной задачей. 

В следующей части расскажем, с какими проблемами столкнулись в процессе обучения и создания интерфейса моделей, а также как используем LLM для решения задачи продуктового матчинга. Не переключайтесь! 

Комментарии (4)


  1. Zivaka
    24.10.2024 17:03

    А почему товар не метчится изначально, еще при создании в ЛК селлера и его привязке к карточке товара самого маркетплейса? Так сделано, например, у Яндекса и это наиболее удобный принцип как для продавца, так и для покупателя.


    1. ivan_mordovets Автор
      24.10.2024 17:03

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


      1. Zivaka
        24.10.2024 17:03

        Так тут вопрос, какую проблему стоит действительно решать. Со стороны выглядит как создание костылей, вместо решения проблемы на более раннем этапе. Костыли, безусловно, красивые)


  1. sshikov
    24.10.2024 17:03

    Вот честно, вы бы лучше текстовые описания бы подправили. А то этож смешно читать: вот есть на Мегамаркете такой "фрезер ермак", китайщина китайщиной. И написано про него вот что:

    • Напряжение аккумулятора, в вольтах: 220

    • Количество аккумуляторов комплекте: 1

    Фрезер при этом сетевой, ясное дело, и на фотке хорошо видно сетевой шнур. Вам смешно? Мне очень. Ну т.е. написана явная чушь (раньше еще было написано, что обороты у него 120 в минуту, но уже исправили).

    И такого барахла в описаниях товаров - вагон и маленькая тележка.

    Я подозреваю, что нейросеть вполне могла бы эту чушь выявить.