Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении



Задача В этой статье мы хотим рассказать, как мы создали решение для классификации названий продуктов из чеков в приложении «Учет чеков и Помощник по покупкам».

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

Потому что заставлять пользователя самостоятельно группировать товары уже в прошлом.

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

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

процесс.





Кластеризация

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

Вряд ли вы узнаете, какой продукт с названием «УТРУСТА крншт» был куплен в одном из российских магазинов? Настоящие ценители шведского дизайна, конечно же, сразу ответят нам: Кронштейн для духовки Утраста, но содержать таких специалистов у себя в штаб-квартире довольно дорого.

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

Поэтому сначала поговорим о том, как при отсутствии обучающих данных мы применили алгоритмы кластеризации и почему нам это не понравилось.

Такие алгоритмы основаны на измерении расстояний между объектами, что требует их векторного представления или использования метрики для измерения сходства слов (например, расстояния Левенштейна).

Трудность на этом этапе заключается в осмысленном векторном представлении названий.

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

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

Кроме того, этот подход не извлекает из названий никакой дополнительной информации.

Таким образом, в одном кластере может быть множество продуктов из разных категорий, объединенных общим словом, например, «картошка» или «салат»:

Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении

Мы также не можем контролировать, какие кластеры будут собраны.

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

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

Если мы укажем достаточно большой, то после работы алгоритма нам придется просматривать сотни кластеров и объединять их в семантические категории вручную.

В таблицах ниже представлена информация о кластерах при использовании алгоритма KMeans и Tf-Idf для векторного представления.



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

При этом формируется один кластер, содержащий большую часть векторов.

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



Кластер

С1

С2

С3

С4

С5

С6

С7

С8

С9

С1

0.0

0.502

0.354

0.475

0.481

0.527

0.498

0.501

0.524

С2

0.502

0.0

0.614

0.685

0.696

0.728

0.706

0.709

0.725

С3

0.354

0.614

0.0

0.590

0.597

0.635

0.610

0.613

0.632

С4

0.475

0.685

0.590

0.0

0.673

0.709

0.683

0.687

0.699

С5

0.481

0.696

0.597

0.673

0.0

0.715

0.692

0.694

0.711

С6

0.527

0.727

0.635

0.709

0.715

0.0

0.726

0.728

0.741

С7

0.498

0.706

0.610

0.683

0.692

0.725

0.0

0.707

0.714

С8

0.501

0.709

0.612

0.687

0.694

0.728

0.707

0.0

0.725

С9

0.524

0.725

0.632

0.699

0.711

0.741

0.714

0.725

0.0

Таблица 2. Краткая информация о кластерах
Кластер Количество объектов Среднее расстояние Минимальное расстояние Максимальное расстояние
С1 62530 0.999 0.041 1.001
С2 2159 0.864 0.527 0.964
С3 1099 0.934 0.756 0.993
С4 1292 0.879 0.733 0.980
С5 746 0.875 0.731 0.965
С6 2451 0.847 0.719 0.994
С7 1133 0.866 0.724 0.986
С8 876 0.863 0.704 0.999
С9 1879 0.849 0.526 0.981
Но кое-где кластеры получаются вполне приличные, как, например, на изображении ниже — там почти все продукты относятся к кошачьему корму.



Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении

Doc2Vec — еще один алгоритм, позволяющий представлять тексты в векторной форме.

При таком подходе каждое имя будет описываться вектором меньшей размерности, чем при использовании Tf-Idf. В полученном векторном пространстве похожие тексты будут находиться близко друг к другу, а разные — далеко.

Такой подход позволяет решить проблему большой размерности и разреженности пространства, полученного методом Tf-Idf. Для этого алгоритма мы использовали самый простой вариант токенизации: разбивали имя на отдельные слова и принимали их первоначальные формы.

Он обучался на данных следующим образом:

 
  max_epochs = 100

vec_size = 20

alpha = 0.025

model = doc2vec.Doc2Vec(vector_size=vec_size,

alpha=alpha, 

min_alpha=0.00025,

min_count=1,

dm =1)

model.build_vocab(train_corpus)

for epoch in range(max_epochs):

print('iteration {0}'.

format(epoch)) model.train(train_corpus, total_examples=model.corpus_count, epochs=model.iter) # decrease the learning rate model.alpha -= 0.0002 # fix the learning rate, no decay model.min_alpha = model.epochs

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

Вот один из примеров работы алгоритма: на изображении представлены товары, которые по алгоритму аналогичны «Хлеб форменный Бородино п пачка 0,45к».



Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении

Возможно, проблема в длине и контексте названий: пропуском в названии «__club. банан 200 мл» может быть либо йогурт, либо сок, либо большая баночка сливок.

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

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





Классификация



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

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

Например, букву «а» можно заменить на латинскую «а», и это увеличит количество уникальных названий — например, слова «молоко» и «молоко» будут считаться разными.

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

Мы изучили нашу базу данных и нашли распространенные ошибки в названиях.

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

При таком подходе результат увеличивается примерно на 7%.

В сочетании с простой версией классификатора SGD, основанной на функции потерь Хубера с измененными параметрами, мы получили точность 81% в F1 (средняя точность по всем категориям продуктов).

sgd_model = SGDClassifier()

parameters_sgd = {

'max_iter':[100],

'loss':['modified_huber'],

'class_weight':['balanced'],

'penalty':['l2'],

'alpha':[0.0001]

}

sgd_cv = GridSearchCV(sgd_model, parameters_sgd,n_jobs=-1)

sgd_cv.fit(tf_idf_data, prod_cat)

sgd_cv.best_score_, sgd_cv.best_params_
Также не забывайте, что некоторые категории люди покупают чаще, чем другие: например, «Чай и сладости» и «Овощи и фрукты» гораздо популярнее, чем «Услуги» и «Косметика».

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

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

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



Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении



Получение новых данных обучения

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

Поэтому нам нужно было сделать наценку на товары из наших чеков.

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

Поэтому мы решили использовать «Толокой» Яндекс.

Там мы использовали следующую форму задачи:

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

По результатам ответов на эти задания были отсеяны пользователи, неправильно разметившие данные.

Однако за весь проект мы забанили только трёх пользователей из 600+.



Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении

Благодаря новым данным мы получили модель, которая лучше соответствует нашим данным, а точность выросла еще немного (на ~11%) и теперь достигла 92%.





Окончательная модель



Мы начали процесс классификации с объединения данных из нескольких наборов данных с помощью Kaggle — 74%, после чего улучшили предварительную обработку — 81%, собрали новый набор данных — 92% и, наконец, улучшили сам процесс классификации: изначально, используя логистическую регрессию, мы получить предварительные вероятности принадлежности продукта к категориям, основываясь на названиях продуктов, SGD давал большую точность в процентах, но все же имел большие значения функций потерь, что плохо влияло на результаты итогового классификатора.

Далее мы объединяем полученные данные с другими данными о товаре (цена товара, магазин, в котором он был куплен, статистика по магазину, чек и другая метаинформация), и на всем этом объёме данных XGBoost обучается, что дало точность 98% (увеличение еще на 6%).

Как оказалось, самый большой вклад внесло качество обучающей выборки.





Запуск на сервере

Чтобы ускорить развертывание, мы настроили простой Flask-сервер на Docker. Был один метод, который получал с сервера продукты, которые нужно было классифицировать, и возвращал продукты с категориями.

Таким образом, мы легко интегрировались в существующую систему, центром которой был Tomcat, и нам не пришлось вносить изменения в архитектуру — мы просто дополнили ее еще одним блоком.





Выпускать

Несколько недель назад мы опубликовали релиз с категоризацией в Google Play (в App Store он появится через некоторое время).

Получилось вот так:

Как Я Понял, Что Ем Много Сладкого, Или Классификация Товаров По Чекам В Приложении

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

Упомянутые соревнования на Kaggle: www.kaggle.com/c/receipt-categorisation www.kaggle.com/c/market-basket-anaанализ www.kaggle.com/c/prod-price-prediction Теги: #python #Интеллектуальный анализ данных #scikit-learn #кластеризация #классификация

Вместе с данным постом часто просматривают: