Блог
10 августа 2023
Автор: Дмитрий Дивин
Не так давно я рассказывал о том, как я проектировал алгоритм под рекомендательную систему. По большому счету, это даже не алгоритм, а немного другой подход использования cosine similarity. В этой статье я хочу рассказать об ограничениях
этого подхода, как с ними бороться и о своем рабочем кейсе: проектировании рекомендательных систем в реальном времени с помощью графовой базы.
Графовые базы применяются в моделировании социальных сетей, а также существенно превосходят реляционные по производительности для задач с естественной графовой структурой данных. Их можно применять и для проектирования рекомендательных систем. По этой причине я принял решение попробовать спроектировать рекомендательную систему с помощью графовой базы.
Мой выбор пал на графовую базу Neo4j. Хотя разработчики этой базы не были пионерами в области графовых баз, мне Neo4j показался отличным продуктом с точки зрения использования: в нем есть Web UI, через который можно выполнить запрос
и посмотреть результаты как в привычном табличном, так и в виде графа. Но что меня больше всего привлекло, так это возможность писать свои собственные функции
на языке Java и использовать их через синтаксис языка Cypher.
Пару слов о языке Cypher.
К этому языку я привык достаточно быстро. Если проводить параллель между SQL vs Cypher, то синтаксис второго мне показался более удобным в использовании, поскольку он менее декларативный и более понятный привычному мышлению императивных языков. В нем нет необходимости создавать сложные Join конструкции, так как Join-ы достигаются за счет связей через моделирование данных.

Изначально я делал ставку на алгоритмы, потому что проект предназначался для стартапа. А как вы понимаете, у стартапа не так много денег, чтобы нанимать дорогих ML-инженеров и брать в аренду дорогостоящую инфраструктуру.
Моя цель — рассказать о том, как можно спроектировать
рекомендательную систему в буквальном смысле "на коленке", используя два подхода: content-based и collaborative-filtering.

Для чего нам нужны
рекомендательные системы?

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

Какими бывают системы
рекомендации?

Я буду использовать терминологию рекомендательных систем из англоязычных источников.
Content-based Filtering - вам рекомендуется контент, похожий на тот, который вы
уже просматривали, высоко оценили или уже купили.
Collaborative Filtering - система, которая использует предпочтения, рейтинги и действия других пользователей в сети, чтобы найти товары, которые можно порекомендовать именно вам.
Я расскажу об этих двух типах алгоритмов рекомендаций и о том, как их можно реализовать с помощью графовой базы Neo4j. Оба метода можно комбинировать.

Структура данных в графовой
базе данных

Источник: https://miro.medium.com/v2/resize:fit:700/0*irSsY_Eu_ILnnp6G
Перед тем как начать, давайте разберем структуру данных и их связи, которые будут использоваться в качестве примера.
Данная структура будет относиться к поиску рекомендаций по фильмам.
У нас есть три типа объектов:
  • User;
  • Movie;
  • Genre.
Сценарии взаимодействия эти типов:
  • Пользователь может посмотреть один или более фильмов;
  • У фильма может быть один или более жанр.
Эти объекты могут быть соединены между собой следующими связями:
  • HAS_WATCHED - связь между пользователем и фильмом как маркер того, что фильм был просмотрен;
  • IS_ASSOCIATED_WITH - связь между пользователем и жанром, которая содержит еще и признак соответствия как свойство. Что означает признак соответствия? Он означает насколько тип жанра попадает в ожидание телезрителя. Например, если после просмотра фильма у всех телезрителей спросили: "Комедия ли это?" — и только 80% сказали "Да", то в качестве признака можно было бы взять 0.8, так как оно средневзвешенное. В моих примерах будут указаны все признаки соответствия в связях как 1.0.
Если подробнее остановиться на поведении пользователей, то можно понять, что они могут смотреть все жанры подряд, но у них всегда будут особенные предпочтения.

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

Весь граф данных в конечном итоге будет выглядеть примерно так:

Как найти похожий контент?

Я хотел бы остановиться на одном алгоритме. Мне нравится экспериментировать
и искать альтернативные способы решения задач. Сегодня я хочу рассказать об одном
из них — это cosine similarity и то, как я его использовал.

В геометрической интерпретации работу этого алгоритма можно описать следующим образом: берем два вектора признаков равных по размеру. Каждый из этих векторов переносится на плоскость, а угол между ними показывает то, насколько э они имеют сходство. Если вектора параллельны друг другу, то угол между ними будет 0 градусов. Значит, они имеют полное сходство и результат сравнения будет равен 1.0.
Источник: https://storage.googleapis.com/lds-media/images/cosine-similarity-vectors.original.jpg
Источник: https://aitechtrend.com/wp-content/uploads/2023/02/Cosine-similarity.jpg
Данный алгоритм нашел свое применение в следующих сферах: поиск подобия
в документах, обработка естественного языка и рекомендательные системы.

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

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

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

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

Эта функция принимает две группы классификаторов и их признаков, которые можно сравнить между собой. Признаки при этом должны быть положительными числами
и не иметь значение 0.
Как результат получаем значение подобия от 0 до 1, где:
  • 0 — полный промах;
  • 1 — полное попадание;
  • значение между ними — частичное попадание.
Для демонстрации работы этой функции я добавлю некоторое количество жанров
с помощью оператора CREATE:
Перед тем как делать сравнения, я хотел бы прояснить некоторые детали: мы будем сравнивать жанры, которыми интересовался пользователь, когда смотрел фильмы. Всю историю просмотров назовем пользовательским опытом, а если сгруппировать все жанры и получить их сумму, то получим распределение интереса к жанрам.

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

С помощью запроса я буду выводить список сравнения:
  • Случайным образом получаем список жанров и накопленный опыт. Признаки опыта будут указаны в круглых скобках.
  • Случайным образом получим 10 списков жанров, с которыми будем делать сравнения. У этого списка не будет признаков соответствия, так как у наших фильмов все жанры считаются одинаковыми по весу, а значит возьмем для этого любое число. Например, 1.0.
Результат сравнения выводим в таблицу, где в первой колонке будет показываться пользовательский опыт, во второй — жанры, с которыми будем сравнивать, в третьей — результат сравнения.

Источник: https://miro.medium.com/v2/resize:fit:720/0*mwm6mnewmI88Jw2s
Как видно из таблицы сравнения, жанр "Animation" выше в отдаче, чем жанр "Fantasy",
и это потому что к первому интерес выше: 86 против 51.

Как это работает?

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

Чтобы прояснить, как работает этот подход под капотом, я приведу следующий пример:
Представьте, что у вас в одной корзине 20 апельсинов, а в другой только 10. Соотношение типов объектов в этом случае будет равно 1:1, что эквивалентно 20/20=10/10. Для любого количества объектов одного и того же типа алгоритм всегда будет возвращать 1.0, потому что косинус угла векторов [20]~[10] = 1.0. Но что будет происходить, когда в одной корзине 20 апельсинов и 10 яблок, а в другой только 10 апельсинов? Очевидно то, что равенство не соблюдается. А как это посчитать?

Мы можем только представить, что во второй корзине 0 яблок, и посчитать косинус угла по той же самой формуле: [20, 10] ~ [10, 0] = 0.894427
Это же утверждение нужно применить и к левой стороне: если во второй корзине есть тип фрукта, который отсутствует в первой, то в первую для этого типа выбираем 0.
Технические детали: данные я брал из открытого источника для сервиса Unsplash. В результате у меня получилось >20 тыс. фотографий и >1.5 тыс. классификаторов в виде ключевых слов. Время отдачи рекомендации в реальном времени составляет ~200ms.

Во второй части статьи вы узнаете, как использовать cosine similarity для получения рекомендаций по контенту и через коллективное взаимодействие, а также как получать рекомендации из любого количества данных за разумное время задержки (latency).
Продолжение следует...