Product Classification API Part 1: Data Acquisition
Юджин Ян описывает первую часть серии о создании API для классификации товаров: по названию товара API возвращает 3 наиболее вероятные категории (из тысяч). Вместо написания собственного веб-скрапера для Amazon и Alibaba автор использовал открытые данные о товарах Amazon, собранные Julian McAuley (метаданные на 9,4 млн товаров, 3,1 ГБ в архиве). В статье показано, как прочитать сжатый JSON в pandas и конвертировать в CSV, а также как обработать поле categories, превратив список списков в строку-путь категории (например, «Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers»). Затем данные последовательно очищаются: исключаются товары без названия или категории (остаётся 7,98 млн), категории вроде Books/CDs & Vinyl/Movies & TV (5,59 млн), неглубочайшие категории (4,61 млн) и категории с менее чем 10 товарами (4,59 млн). В итоге из 17,6 тыс. категорий остаётся около 15 тыс., и эти подготовленные данные станут основой для обучения классификатора в следующих частях.
Product Classification API. Часть 1: получение данных
[ machinelearning python 🛠 ] · чтение на 9 мин
Чтобы попрактиковаться в создании дата-продуктов от начала до конца, я недавно разработал API для классификации товаров. Этот API помогает классифицировать товары по их названию — вместо того чтобы выяснять, к какой из тысяч категорий принадлежит ваш товар, вы можете передать название, и API вернёт 3 наиболее вероятные категории. (Репозиторий на Github)
Обновление: API остановлен ради экономии на облачных расходах.
Вход: название. Выход: предлагаемые категории.
Это часть серии постов о создании API для классификации товаров:
Получение и форматирование данных (часть 1) Очистка и подготовка данных (часть 2) Разработка приложения (часть 3) Демо классификации изображений Демо поиска по изображениям
Откуда я взял данные о товарах?
Изначально я собирался написать веб-скрапер, чтобы собрать данные о товарах с сайтов Amazon и Alibaba. Однако я решил, что этот навык не так уж необходим, особенно в работе — данные о товарах хранятся и доступны в базах данных. К тому же процесс скрапинга и структурирования собранных данных, по оценке, занял бы как минимум 30% всех усилий. Поэтому я решил использовать открытые данные о товарах.
На удивление трудно оказалось найти качественные открытые данные о товарах. Я уже был готов написать скрапер, когда наткнулся на сайт Julian McAuley: http://jmcauley.ucsd.edu/data/amazon/. Выражаю благодарность Julian и его команде за данные о товарах Amazon, использованные при создании этого API.
В этом проекте мы будем использовать метаданные товаров, содержащие 9,4 млн товаров (3,1 ГБ в архиве). Как мы увидим, после очистки и подготовки лишь небольшая часть пригодна для обучения модели классификации товаров для API.
Как выглядят метаданные товаров?
Метаданные содержат следующие поля:
asin: ID товара title: название товара price: цена товара (в USD) imUrl: URL изображения товара related: связанные товары (которые рекомендует Amazon; например, «также покупали», «также смотрели», «покупали вместе» и т. д.) salesRank: ранг товара в категории верхнего уровня (по количеству продаж за период) brand: бренд товара categories: список категорий, к которым относится товар
Вот как выглядел бы товар в исходном формате json. Я буду использовать этот пример на протяжении всего поста.
{ "asin": "B0147ZZKQ2", "title": "Onitsuka Tiger Ultimate 81 Running Shoe", "price": 68.88, "imUrl": "http://ecx.images-amazon.com/images/I/51fAmVkTbyT._SY300_.jpg", "related": { "also_bought": ["B00JHONN1S", "B002BZX8Z6", "B00D2K1M3O", "0000031909", "B00613WDTQ", "B00D0WDS9A", "B00D0GCI8S", "0000031895", "B003AVKOP2", "B003AVEU6G"], "also_viewed": ["B002BZX8Z6", "B00JHONN1S", "B008F0SU0Y", "B00D23MC6W", "B00AFDOPDA", "B00E1YRI4C", "B002GZGI4E", "B003AVKOP2", "B00D9C1WBM", "B00CEV8366"], "bought_together": ["B002BZX8Z6"] }, "salesRank": {"Clothing, Shoes & Jewelry": 1368}, "brand": "Onitsuka", "categories": [["Clothing, Shoes & Jewelry", "Men", "Shoes", "Fashion Sneakers"], ["Sports & Outdoors", "Exercise & Fitness", "Running", "Footwear"]] }
Конвертация JSON в формат CSV
Данные json поступают в архиве. Вот как можно прочитать их в pandas dataframe (и при необходимости сохранить в csv):
import pandas as pd import gzip def parse(path): g = gzip.open(path, 'rb') for l in g: yield eval(l) def get_df(path): i = 0 df = {} for d in parse(path): df[i] = d i += 1 return pd.DataFrame.from_dict(df, orient='index') df = get_df('metadata.json.gz') df.to_csv('metadata_csv', index=False)
Подход выше читает весь набор данных и загружает его в dataframe. Это допустимо при небольшом размере данных (3,1 ГБ). Однако для более крупных наборов ( >100 ГБ) понадобится альтернативный подход. Один из вариантов — читать сжатый json и записывать в csv построчно:
import json import gzip import csv def parse(path): g = gzip.open(path, 'rb') for l in g: yield eval(l) def json_to_csv(read_path, write_path): csv_writer = csv.writer(open(write_path, 'w')) i = 0 for d in parse(read_path): if i == 0: header = d.keys() csv_writer.writerow(header) i += 1 csv_writer.writerow(d.values()) json_to_csv('metadata.json.gz', 'metadata.csv')
Форматирование и очистка данных о категориях
Теперь, когда мы разобрали данные о товарах в dataframe (и сохранили их в csv), можно приступить к работе с данными о категориях. Вот как выглядят данные о категориях для кроссовок Onitsuka Tiger:
[["Clothing, Shoes & Jewelry", "Men", "Shoes", "Fashion Sneakers"], ["Sports & Outdoors", "Exercise & Fitness", "Running", "Footwear”]]
Кроссовки указаны в двух категориях: «Clothing, Shoes, and Jewelry» (CSJ) и «Sports & Outdoors» (S&O). Это значит, что они перекрёстно перечислены в обеих категориях, и их можно найти, просматривая как каталог CSJ, так и каталог S&O. (Да, они настолько крутые, что подходят и под модные сникеры, и под беговую обувь; посмотрите топовый отзыв здесь: Whappoww!! Ninja sneaks for bosses of chill)
API классифицирует товар в его основную категорию (то есть первую указанную категорию), обеспечивая взаимно однозначное соответствие между товарами и их категориями. Часто существуют правила, какая категория должна быть основной (например, у обуви основной категорией должна быть CSJ).
Преобразование данных о категориях в строки-пути категорий
Сначала мы преобразуем категорию из списка списков в строку. Одновременно мы оставим только основные категории (то есть первую категорию в списке). После разбора получается такая категория:
"Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers"
Примечание: я предпочитаю преобразовывать данные о категориях в путь категории — единую строку, соединённую стрелками (->). Однако подойдёт любой формат.
Вот код, как это делается:
def get_category_path(category_path_list): """ (Str of list of list(s)) -> str Returns the category path given a string of list of lists of categories. If there are more than one list of categories provided, returns the category path from the first list. >>> get_category_path("[['A', 'B', 'C'], ['D', 'E', 'F', 'G']]") 'A -> B -> C' >>> get_category_path("[['P1', 'P2', 'P3', 'P4']]") 'P1 -> P2 -> P3 -> P4' :type category_path_list: str :param category_path_list: A string containing a list of at least one list of categories :return: A string showing the full category path of the FIRST category in the list (assumed to be primary category) """ try: return ' -> '.join(eval(category_path_list)[0]) except IndexError: # Error if the outer list is empty return 'no_category' except TypeError: # Error if the outer list is missing return 'no_category' # Create column for category path df['category_path'] = df['categories'].apply(get_category_path)
Исключение данных, где отсутствует название или категория
Если отсутствует либо название, либо категория, мы не сможем использовать данные о товаре для обучения нашей модели. Поэтому мы исключим товары с неполными данными, вот так:
df.dropna(subset=['title'], inplace=True) df = df[df['category_path'] != 'no_category']
Мы начали с 9,43 млн товаров. Более чем у 1 млн отсутствует либо название, либо категория, и остаётся 7,98 млн товаров.
Исключение определённых категорий
Есть категории товаров, где название не даёт никакой информации о категории товара (например, книги, фильмы и т. д.). Такие товары обычно классифицируются альтернативными способами. Например, наличие данных ISBN указывает, что товар — книга, а наличие рейтинга (например, PG-13) указывает, что товар — фильм.
Мы исключим определённые категории товаров следующим образом, и у нас остаётся 5,59 млн товаров:
df = df[df['category_path_lvl1'] != 'Books'] df = df[df['category_path_lvl1'] != 'CDs & Vinyl'] df = df[df['category_path_lvl1'] != 'Movies & TV']
Примечание: вы могли заметить, что код выше фильтрует по столбцу category_path_lvl1. Этот столбец содержит категории верхнего уровня для товаров (например, «Electronics», «Clothing, Shoes, and Jewelry», «Sports & Outdoors»). Вывод этого столбца я оставлю в качестве упражнения для аудитории ;)
Исключение категорий, не являющихся самыми глубокими/узкими
В списке категорий мы найдём такие, которые как будто обрываются на полпути, как первый путь категории ниже:
"Clothing, Shoes & Jewelry -> Men -> Shoes" "Clothing, Shoes & Jewelry -> Men -> Shoes -> Fashion Sneakers"
В этом случае вторая категория глубже (и уже), чем первая. Классификация товаров в самую глубокую категорию помогает покупателям легче находить нужные товары (поскольку категория уже). Поэтому мы исключим товары, которые не находятся в самой глубокой категории.
Вот один из способов это сделать. Сначала мы отсортируем категории и сравним каждый путь категории со следующим. Если путь категории не содержится в следующем (то есть путь категории не является подстрокой следующей категории), то это самая глубокая категория, и мы добавляем её в список. Затем мы оставим только товары с категориями из нашего списка.
# Create df of category path counts category_path_df = df.groupby('category_path').agg({'title': 'count'}).sort_values(by='title', ascending=False).reset_index() category_path_df.sort_values(by='category_path', inplace=True) category_path_df['category_path_next'] = category_path_df['category_path'].shift(-1) category_path_df.fillna('no_comparison', inplace=True) # Create list of category_paths which are deepest category category_path_list = [] for i, value in category_path_df.iterrows(): category_path = value['category_path'] category_path_next = value['category_path_next'] if category_path not in category_path_next: category_path_list.append(category_path) # Create df of category_path category_path_df = pd.DataFrame(category_path_list, columns=['category_path']) # Keep only rows where the category is in category_df df = df[df['category_path'].isin(category_path_df['category_path'])]
Изначально у нас было 17,6 тыс. категорий. После исключения категорий, не являющихся самыми глубокими, остаётся около 15 тыс. категорий (и 4,61 млн товаров).
Исключение категорий, в которых слишком мало товаров
Наконец, чтобы обеспечить достаточно данных для разбиения на обучающую и тестовую выборки и обучения нашей модели, мы исключим категории, в которых менее 10 товаров. При разбиении 50 на 50 на обучение и тест у нас будет как минимум пять товаров для обучения на каждую категорию. Это не должно быть слишком сложно, и я призываю вас попробовать сделать это самостоятельно.
После исключения товаров по этому условию у нас остаётся 4,59 млн товаров.
И мы закончили!
Поздравляю, что добрались до этого места!
Мы завершили ключевые шаги по очистке данных о категориях. Может потребоваться и другая очистка, например исключение временных категорий (то есть «Black Friday Sales», «11-11 Sales» и т. д.), но мы их рассматривать не будем.
Как показано, нужно проделать большую работу по получению, форматированию и очистке данных, прежде чем мы перейдём к построению модели классификации товаров. В этом случае я благодарен Julian McAuley и его великодушному предоставлению метаданных о товарах Amazon.
В следующей статье мы перейдём — да, вы угадали — к ещё большей очистке и подготовке данных (специфичной для названий). Эта базовая подготовка данных — ключ к обучению нашего классификатора с высокой точностью.
p.s. Я был бы очень признателен за любую обратную связь по процессу, коду, стилю изложения и т. д. в комментариях ниже. Спасибо!
Если этот материал оказался полезным, пожалуйста, цитируйте его так:
Yan, Ziyou. (Oct 2016). Product Classification API Part 1: Data Acquisition. eugeneyan.com. https://eugeneyan.com/writing/product-categorization-api-part-1-data-acquisition-and-formatting/.
или
@article{yan2016acquisition, title = {Product Classification API Part 1: Data Acquisition}, author = {Yan, Ziyou}, journal = {eugeneyan.com}, year = {2016}, month = {Oct}, url = {https://eugeneyan.com/writing/product-categorization-api-part-1-data-acquisition-and-formatting/} }
Присоединяйтесь к 11 800+ читателей, получающих обновления о machine learning, RecSys, LLM и инженерии.