Привет, Хабр! На связи команда продуктового матчинга ecom.tech. В этой статье мы расскажем, как используем LLM для задачи сопоставления товаров на маркетплейсе.
Как перевести задачу с продуктового языка на язык промптов. Что делать, если ни одна LLM не обучается на нужную тебе задачу (fine-tune). Как быть с поддержкой русского языка. Об этих и других аспектах по использованию LLM – читайте ниже.
Надеемся, эта статья будет интересна тем, кто интересуется математической и технической сторонами использования машинного обучения для решения продуктовых задач.
В первой части мы подробно рассмотрели постановку задачи продуктового матчинга – процесса сопоставления товаров на основе различных критериев. Объяснили, как её успешное решение позволяет разобраться с задачами динамического ценообразования, добавления товаров от новых продавцов и объединения товаров в группы.
Во второй части рассказали, как устроен наш матчер, какие модели мы использовали и как их обучали: каскадные кандидатные модели для изображений и текстовых описаний, модель ранжирования и финальная модель на основе градиентного бустинга. Ниже – финальная третья часть о продуктовом матчинге для Мегамаркета.
Прежде, чем начать рассказ про LLM, поговорим сначала о проблемах, с которыми мы столкнулись при построении пайплайна матчера из предыдущей части, и как мы их решали.
Данная задача связана с разметкой данных для обучения кандидатных и финальной моделей.
При заведении товаров бывает так, что разные продавцы заводят на площадку один и тот же товар (то есть изображение и атрибуты совпадают). Логично, что таким товарам присваивают различные id. Но содержание информации для моделей в них – одинаковое. Таким образом, мы начинаем формировать обучающую выборку с негативными примерами (данный процесс подробно описан во второй части).
Для каждого запроса должен быть ровно одно совпадение, поэтому этой паре мы приписываем единицу, а для всех остальных пар – ноль. Но при наличии дублей у нас будут случаи, когда одни и те же пары с разными идентификаторами получают метки как единиц, так и нулей. В итоге при обучении мы начинаем вводить модель в заблуждение: пары одинаковые, а метки разные. Это сказывается на качестве модели.
Значит, перед обучением модели, необходимо очистить наш ассортимент от дублей. Как это сделать? Тут есть несколько подходов.
Некоторые дубли удобно удалять по артикулу, но такой подход годится только для некоторых категорий, в которых мы точно можем быть уверены, что совпадение артикулов у товаров с разными идентификаторами означает, что эти товары являются идентичными. Например, так это устроено для автотоваров.
Можно организовать следующий итеративный процесс: собираем относительно небольшую, но качественную обучающую выборку, вручную проводим дедупликацию, обучаем кандидатные и финальную модели. После чего уже обученным матчером делаем матчинг нашего ассортимента сам на себя.
Найденные дубли удаляем из ассортимента, предварительно установив порог; расширяем обучающую выборку и повторяем весь процесс заново до тех пор, пока не получим необходимое для матчера качество. Да, это довольно долго и затратно, но это наиболее надежный способ решения проблемы дубликатов.
Можно сделать более щадящую процедуру: организовать векторный поиск по изображениям и отбросить дубли только по изображениям. Но такой метод плохо применим, например, в категории “Одежда”, где под разные товары с разными размерами подгружается одна и та же картинка (например, футболка белая). Гораздо надежнее учитывать ещё и текстовую информацию.
Мы очистили нашу базу от дублей и обучили каскад моделей. Как теперь измерить качество матчера? Ведь каждая модель обучалась на свою функцию потерь – училась решать весьма специфическую, не связанную напрямую с матчингом, задачу. А нужно оценить весь матчер целиком.
Метрикой качества всего пайплайна матчинга мы решили использовать recall при фиксированном значении precision в 90%. Эта метрика может быть интерпретирована для бизнеса как охват при заданном значении точности. Тогда становится понятно, что мы хотим от матчера – надо максимизировать охват. Чем больше охват – тем больше идентичных товаров у конкурентов мы можем найти.
Но после получения списка матчей от финальной модели необходимо провести калибровку порогов. Другими словами, начиная с какого значения мы будем считать пару совпадением. Это необходимо для того, чтобы найти нужный баланс между количеством получаемых матчей (то есть recall) и точностью. И здесь возникает проблема: калибровать приходится на неразмеченных данных.
К сожалению, при всем стремлении к автоматизации, решить данную проблему без помощи человека невозможно. Но и тут можно пойти двумя путями. Либо привлечь асессоров, которые хорошо понимают ассортимент, либо (что гораздо быстрее) использовать сторонние сервисы разметки, такие как TagMe. После этого можно приступить к формированию финального списка совпадений (детально эта процедура была описана во второй части).
Кажется, что мы побороли ключевые проблемы, связанные с данными и разметкой. Теперь нужно ускорить наш пайплайн.
Сначала займемся оптимизацией центрального механизма всех расчетов внутри механизма self-attention – трансформера, на архитектуре которого у нас и картиночная модель и текстовые модели построены именно. Оказывается, скорость расчета можно существенно ускорить, если выполнять его в правильных частях GPU. Так появился подход Flash Attention.
Основная идея: вместо различных способов аппроксимировать непосредственно подсчёт self-attention можно оптимизировать само его вычисление за счет правильного использования внутренней памяти GPU.
Подход Flash Attention стал крайне популярным для обучения и инференса моделей типа трансформер. Он быстро внедрился в различные модели, а его вторая версия с дополнительными оптимизациями на GPU была включена в библиотеки transformers и pytorch, что обеспечивает удобство в использовании.
Длина последовательности на вход в трансформер должна быть фиксирована, но предложения (в нашем случае описания товаров) обычно разной длины. Как быть? Берем самое длинное предложение, а всё, что короче него, заполняем с помощью специального токена pad до длины этого предложения. Годится? Нет. Таким подходом мы используем большое количество лишних токенов pad, которые требуют время и память на их обработку. Можно сделать это быстрее.
Сначала разобьем данные на батчи, а внутри каждого батча заполним предложения до нужной длины. Такой подход получил название динамический padding. Если перед этим сначала отсортировать все последовательности по длине, затем разбить на батчи, а только потом заполнять их до нужной длины, то получится ещё меньше токенов! Такой подход получил название smart batching.
Если говорить в применении к нашим данным, то smart batching сократил почти вдвое количество используемых токенов, что позволило ускорить все текстовые модели.
Ещё можно вспомнить, что у нас текстовые и картиночные кандидатные модели дают на выходе n кандидатов. Если модели хорошо обучены, то большая часть кандидатов – мусор. И можно ввести дополнительный порог, который будет передавать в cross-encoder (самую тяжелую модель) только избранных кандидатов (сильно меньше, чем n). Это, разумеется, ускорит инференс самого cross-encoder.
Наконец, не забываем, что все этапы инференса надо делать с половинной точностью (в fp16, можно в int8 или int4, но тогда качество уже начинает падать). Собрав всё вместе, мы смогли ускорить наш пайплайн более чем в 8 раз.
Последние два года весь мир поглотила эпидемия больших языковых моделей (LLM). Даже люди, которые совершенно далеки от машинного обучения – слышали что-то про GPT и его умение решать почти любую задачу. Так может дать ему решить задачу продуктового матчинга?
Все LLM делятся на два категории: те, кто сидят в open source, и те, кому нужны API. В нашем случае мы хотим иметь свое локальное решение. Наш выбор пал на LLM семейства Llama-3.1.
А именно, мы взяли модель на 70 млрд параметров. Теперь надо придумать, как перевести нашу задачу с продуктового языка на язык промптов. Дадим на вход два описания товаров и спросим у модели напрямую, являются ли эти товары идентичными или нет. Сам промпт будет выглядеть так:
Оказывается, что LLM неплохо справляется с этой задачей! Таким образом, нам нет нужды использовать этап с cross-encoder и финальной моделью. Получив кандидатов, мы сразу подаем их описания на вход LLM и все.
Но LLM тоже далеко не всегда дает верный ответ (тут даже человек делает это с трудом). Понятно, что использовать LLM на всем ассортименте в данном случае это как палить из огромной пушки по воробьям. Эффектно, но не эффективно. Но мы нашли для нее область применения: проблемные категории типа “Одежда” или “Обувь”. В предыдущей части мы описывали, почему эти категории не очень приятно матчить.
Итак, LLM из коробки вполне может решать задачу матчинга, но мы всё же хотим значительно повысить качество ее ответов. Изначально ни одна LLM не обучается на задачу матчинга. Значит, нам придется делать fine-tune.
Для этого нужно выбрать модель и сам метод fine-tune. Llama-3.1 хороша, но русский язык у неё не был среди основных. Возьмем модель с нативной поддержкой русского текста.
Mistral-Nemo на 12 млрд параметров – то, что надо! Она достаточно компактна и при этом поддерживает русский язык из коробки. Теперь определимся с методом для fine-tune. Тут, конечно, глаза разбегаются.
Нам нужен метод, который позволит быстро и качественно (без переобучения) сделать fine-tune нашей LLM. Поэтому наш выбор остановился на классе методов репараметризации, а именно – LoRA. Чем он хорош?
Во-первых, большая часть параметров остается “замороженными” в процессе fine-tune, что обеспечивает необходимую скорость и позволяет избежать переобучения.
Во-вторых, можно предварительно квантизовать веса (до int4, например) и значительно сэкономить память. Этот подвид LoRA называют QLoRA. Его и будем использовать.
Итак, у нас всё готово для проведения fine-tune. Осталось собрать датасет. Здесь всё зависит от того, где в нашем pipeline матчера будет использоваться LLM. Ясно, что использовать ее сразу вместо cross-encoder и финальной модели будет слишком накладно по времени и ресурсам. А вот использовать ее как супер-удар для проблемных категорий возможно.
Наш матчер работает неидеально, поэтому остаются реальные совпадения, которые в силу тех или иных причин были пропущены (например, в одежде не очень понятно подписан размер, в одном месте S, в другом 46). И тут мы выпускаем ̶к̶р̶а̶к̶е̶н̶а̶ LLM.
Например, берем отбракованные пары из категории одежда, выставляем порог близости по картиночным кандидатам не ниже 0.9 (то есть изображения очень близки, это потенциальное совпадение) и получившиеся пары (точнее, их текстовые описания) прогоняем повторно через LLM. Таким образом, LLM будет использована для поиска дополнительных матчей в проблемных категориях.
А как будет выглядеть сам промпт для такого fine-tune?
Например, вот так:
[INST] Два товара являются одинаковыми, если у них одновременно совпадают бренд, цвет и размер (остальные атрибуты не важны).
Проанализируй описание двух товаров в квадратных скобках и определи являются ли эти товары одинаковыми. Если два товара являются одинаковыми, верни ответ "да". Если два товара не являются одинаковыми, верни ответ "нет". [/INST]
Здесь принципиально важно указать правило, согласованное категорийным менеджером, что является совпадением, а что нет. Всё, запускаем fine-tune в режиме Causal_LM (один из параметров конфигурации QLoRA). Дальше рецепт прост: 2 GPU H100, пару часов ожидания и вуаля! Мы получили Mistral, заточенный под задачу матчинга в категории одежда.
Теперь самое время протестировать. После замеров оказалось, что количество дополнительных совпадений составило почти 30% от числа уже найденных! Отличный результат – данный подход полностью себя оправдал.
Но мы решили не останавливаться на этом. Наша LLM работает только с текстом, но картинки тоже являются важной частью матчинга. Надо их тоже использовать. Что делать? Брать мультимодальную модель!
Поскольку Mistral-Nemo себя прекрасно зарекомендовал, мы взяли его мультимодальную версию – Pixtral. Это тот же Mistral-Nemo + ViT трансформер.
Что будем с ней делать? Добавляем картинки, немного меняет промпт и вновь делаем fine-tune с помощью QLoRA. Результат получился следующим: количество дополнительных матчей выросло до 37% от числа уже найденных. То есть мультимодальность добавила еще 7%, что, в целом, ожидаемо, поскольку картинки – важная составляющая карточки товара.
Итак, мы успешно внедрили LLM в свой пайплайн матчера, но на этом работа далеко не закончена.
Во-первых, использование мультимодальной LLM замедляет пайплайн (надо обрабатывать еще и картинки).
В-вторых, мы делали fine-tune только для нескольких проблемных категорий, а хочется иметь некое универсальное решение. Более того, универсальным это решение должно быть не только в разрезе категорий, но и в разрезе площадок конкурентов (описание товаров довольно сильно отличается от площадки к площадке). Работы здесь ещё много, мы продолжаем выдвигать всё новые гипотезы и проводить эксперименты.
Вот и подошла к концу наша трилогия о путешествии в захватывающий мир продуктового матчинга. Надеемся, каждый сможет вынести для себя что-то свое из этого рассказа, но главную на наш взгляд мысль мы все же выскажем: основа успеха – это баланс.
Баланс между скоростью и точностью. Баланс между простотой и сложностью. Баланс между новым и старым. Любая задача всегда имеет несколько возможных вариантов решения и нужно не стесняться экспериментировать, пробовать самые свежие подходы и идеи.
Желаем всем находить побольше истинных совпадений в своей жизни и да прибудет с вами Дурин!