Сегодня я хочу рассказать вам о своем опыте использования нейронной сети для поиска похожих товаров для рекомендательной системы интернет-магазина. Говорить буду в основном о технических вещах. Написать эту статью на Хабре решил потому, что когда только начинал делать этот проект, то на Хабре нашел одно подходящее решение, но как оказалось, оно уже было устаревшим и пришлось его модифицировать. А поэтому решил обновить материал для тех, у кого будет потребность в аналогичном решении. Отдельно хочу сказать, что это мой первый опыт создания более-менее серьезного проекта в сфере Data Science, поэтому если кто-то из более опытных коллег увидит, что еще можно улучшить, то буду только рад за советы. Начну с небольшой предистории, почему была выбрана та логика интернет-магазина, которая выбрана - а именно рекомендация на основе похожих товаров (а не методы коллаборативной фильтрации, например). Дело в том, что данная рекомендательная система разрабатывалась для интернет-магазина, который продает часы и поэтому до 90% пользователей, которые приходят на сайт, они больше не возвращаются. А в целом задача была такая - увеличить количество просмотров страниц со стороны пользователей, которые приходят на страницы конкретных товаров по рекламе. Такие пользователи просматривали одну страницу и уходили с сайта, если товар им не подходил. Надо сказать, что в данном проекте у меня не было возможности делать интеграцию с бэкэндом интернет-магазина - классическая история для малых и средних интернет-магазинов. Необходимо было рассчитывать только на систему, которую я сделаю в стороне от сайта. Поэтому в качестве визуального решения на самом сайте я решил сделать всплывающий js-виджет. Одной строчкой в html-код добавляется js, понимает заголовок страницы, на который пришел пользователь, и передает его в бэкэнд сервиса. Если бэкэнд нашел в своей базе заранее загруженных товаров товар, то он ищет опять же в заранее подготовленной базе товаров, рекомендации и возвращает их в js, а js их потом отображает в виджете. Также, для снижения влияния на скорость загрузки, js создает iframe, в котором производит все работы с отображением виджета. Помимо прочего, это еще и позволяет убрать проблему с пересечением css-классов виджета и сайта. Скорее всего, для это была самая не интересная часть для Data Science. Но считаю ее необходимой частью данного рассказа, чтобы более четко понимать как все работает. Опять же, возможно, кому-то из новичков это тоже будет полезно. А теперь перейдем непосредственно к поиску похожих товаров. Для данного магазина я сделал два варианта поиска схожих товаров (опять же, классика A/B-тестирования) - рекомендации просто по схожим характеристикам; рекомендации, которые включали в себя первым слоем схожесть изображения, а вторым - схожесть характеристик. С поиском схожих просто по характеристикам дело ясное. Перейдем к поиску схожих по изображениям. Для начала подгружаем необходимые библиотеки: Дальше мне необходимо отсортировать изображения (так как они у меня названы исходя из индексов товаров в датафрейме, то я их отсортирую по алфавиту): Задаем некоторые параметры: Загружаем саму модель (уже предобученную): Удаляем последний слой (следний слой дает на выходе вероятности каждого из 1000 классов ImageNet - нам это не нужно в данном случае. Нам нужен просто 4096-мерный вектор для каждого из изображений, по которым мы потом будем искать с помощью простой косинусной метрикой). Как найти имя слоя — это отдельный анекдот, поэтому я смотрел в исходники модели. Итак: Если кому интересно, можно вывести на экран список слоев CNN и посмотреть. Но в целом это никак не относится к коду, который ищет схожие изображения: Далее я иду в каталог заранее подготовленных изображений товаров (то есть, я беру xml каталога интернет-магазина, прохожусь по урлам, которые нашел для товаров, и скачиваю их в папку; ниже покажу код, который делает эту работу): Дальше мне необходимо отсортировать изображения по названиям для более аккуратной работы с ними в последующем: Далее загружаю изображения в специальный PIL формат: Конвертирую PIL изображения в numpy array:в PIL формат данных - width, height, channel в Numpy - height, width, channel Конвертирую изображения в batch format.expand_dims добавит дополнительное измерение к данным на определенной оси Мы хотим, чтобы входная матрица в сеть имела следующий формат - batchsize, height, width, channels. Таким образом, мы добавляем дополнительное измерение к оси 0. Подготавливаем изображение для VGG: Теперь нам необходимо получить как бы особенности данного вектора (вытащить признаки): То есть это будет векторочек признаков для данного изображения: Теперь всё тайное стало явью, и осталось дело техники — итерироваться по всем изображениям и для каждого из него произвести векторизацию. Вытащим все особенности изображения: Ну и дальше посчитаем уже косинусную схожесть между изображениями: Сохраним результат в pandas dataframe: Дальше получилась такая ситуация. В каталоге товаров данного магазина около 6000 SKU. После всех манипуляций выше получилась матрица размером 6000 * 6000. В каждом пересечении строк и столбцов находилось число формата float от 0 до 1 с 8 знаками после нуля, которое показывало схожесть. Когда я пробовал сохранить эту матрицу как есть в файл для дальнейшего применения в сервисе рекомендаций, то у меня получался файл весом около 430 мегабайт (хотя в оперативной памяти такая матрица занимала около 130 мегайбайт). Для меня это было не приемлемо. Хотя бы по той простой причине, что мне нужно было как-то выкладывать этот файл в GitHub, а дальше автоматома деплоить на сервер. GitHub не позволяет грузить файлы больше 100 мегайбат (или во всяком случае я не знаю как это делать). Да и в целом мне казалось, что это какой-то неприлично большой файл. Поэтому я начал думать :) И придумал вот что - мне по-большому счету не важно сколько знаков после запятой будет здесь - мне главное просто сравнивать цифры между собой. Поэтому я сделал следующее: То есть, для начала я просто взял и отсек лишние цифры. Но формат колонки все равно оставался float. А в pandas float может быть минимально float16 - много. Тогда я решил перевести эти значения в int: После этих манипуляций размер матрицы в оперативке уменьшился до 31 мегабайта. Это уже радовало. Ну и дальше я сохранил этот файл в h5: В итоге получился файл весом 40 мегбайт. Это уже, во-первых, удовлетворяло моим требованиям для хранения в GitHub, а во-вторых, в целом уже не так пугало своим размером :) Ну и дальше если для проверки себя вы хотите посмотреть, что же там нашлось из похожего, то можно сделать так: Вот и все :) Ну и дальше можно повесть файл со скриптами на крон и автоматизировать работу по регулярному переполучению новых рекомендаций при обновлении каталога. А теперь еще покажу как я спарсил изображения, если это кому-то будет полезно: Для начала создам необходимые мне директории, куда все буду складывать: Не буду показывать код, который делает подготовительну работу по созданию данных из xml магазина - это просто и не интересно. А код, который забирает изображения, вот: Теперь я беру мой сформированный датасет товаров из xml, и начинаю идти по каждой строчке и забирать изображения: Попадаются товары, у которых нет изображений. Тогда я их просто удаляю из датафрейма и не буду для них делать рекомендации. Обратите на это внимание в своих работах. Я попался на эту тему, так как сначала мне отдавали xml с товарами, которые все имели изображения. А когда после очередного запуска скрипта он упал, то потратил пару часов, чтобы понять, что не все товары имеют изображения и пришлось уже писать как положено для тех случаев, когда не уверен в том, что данные придут как положено. Собственно говоря, вот и все. Надеюсь, это кому-то будет полезно! ) А если нет, то не судите строго - хотел как лучше ) P.S. Кстати, недавно открыл для себя следующую вариацию VGG - VGG19. Судя по тестам, эта версия дает еще более лучшие предсказания. P.S.S Отдельно хочу выразить большую благодарность людям, без которого я бы не смог все это реализовать: это мой брат, Senior JavaScript Developer (помогал мне написать js для сайта и обходить CORS-политики); это Костя Дедищев, Senior Python Developer и Senior Engineer (помогал мне заворачивать все в Docker и настраивать CI/CD pipeline); это Екатерина Артюгина из SkillFactory, которая три месяца возилась со мной и с другими ребятами с рамках SkillFactory Accelerator (это курс я взял специально для того, чтобы создать свой первый реальный Data Science проект под присмотром более опытных ребят); это Marina Shcherbakova, которая придумала курс; это и Валерий Бабушкин (ментор, который помогал понять суть A/B-тестов и включить их в проект); это Валентин Малых (еще один ментор, который помогал с разумением NLP проблем и в частности работы опечаточников при создании чат-ботов (еще второй проект, над которым я работал в рамках акселератора и о котором я может быть расскажу чуть позже); это Эмиль Маггерамов (ментор, который в целом курировал мое продвижение в акселераторе по созданию данного проекта); это одногруппники Valery Kuryshev и Георгий Брегман (регулярно раз в неделю созванивались и делились полученным за неделю опытом). Data Engineer | Python Software Engineer