Рекомендательная система с Apache AGE

Рекомендательная система с Apache AGE

Посмотрев на Youtube видеоролик ”Как работают рекомендательные системы (Netflix/Amazon)” от Art of the Problem, я вдохновился им и захотел создать статью в блоге на эту тему. Итак, здесь мы будем работать над тем, как создать рекомендательную систему с помощью базы данных графов. Для этого мы будем использовать Apache AGE, который является расширением с открытым исходным кодом для PostgreSQL, позволяющим нам создавать узлы и ребра.

Создание графа

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

-- Creating the graph.
SELECT create_graph('RecommenderSystem');


-- Adding user.
SELECT * FROM cypher('RecommenderSystem', $
    CREATE (:Person {name: 'Abigail'})
$) AS (a agtype);


-- Adding movies.
SELECT * FROM cypher('RecommenderSystem', $
    CREATE (:Movie {title: 'The Matrix'}),
           (:Movie {title: 'Shrek'}),
           (:Movie {title: 'The Blair Witch Project'}),
           (:Movie {title: 'Jurassic Park'}),
           (:Movie {title: 'Thor: Love and Thunder'})
$) AS (a agtype);


-- Adding categories.
SELECT * FROM cypher('RecommenderSystem', $
    CREATE (:Category {name: 'Action'}),
           (:Category {name: 'Comedy'}),
           (:Category {name: 'Horror'})
$) AS (a agtype);

Мы можем представить силу связей с помощью свойства под названием рейтинг на ребрах между пользователями и категориями, а также фильмами и категориями. Этот рейтинг будет варьироваться от 0 до 4, где 0 означает, что пользователь ненавидел фильм, а 4 - что фильм ему понравился. Это также работает для категорий и фильмов, где 0 - меньшая вероятность, а 4 - наибольшая вероятность.

Допустим, Эбигейл имеет рейтинг 3 для комедии, 1 для боевика и 0 для ужасов.

-- User preferences.
SELECT * FROM cypher('RecommenderSystem', $
    MATCH (a:Person {name: 'Abigail'}), (A:Category), (C:Category), (H:Category)
    WHERE A.name = 'Action' AND C.name = 'Comedy' AND H.name = 'Horror' 
    CREATE (a)-[:RATING {rating: 3}]->(C),
           (a)-[:RATING {rating: 1}]->(A),
           (a)-[:RATING {rating: 0}]->(H)
$) AS (a agtype);

Каждый фильм также сопоставлен с каждой категорией таким же образом. Например, в ”Матрице” нет комедии, много экшена и нет ужасов.

-- The Matrix and it's relationship with Categories.
SELECT * FROM cypher('RecommenderSystem', $
    MATCH (matrix:Movie {title: 'The Matrix'}), (A:Category), (C:Category), (H:Category)
    WHERE A.name = 'Action' AND C.name = 'Comedy' AND H.name = 'Horror' 
    CREATE (matrix)-[:RATING {rating: 0}]->(C),
           (matrix)-[:RATING {rating: 4}]->(A),
           (matrix)-[:RATING {rating: 0}]->(H)
$) AS (a agtype);

-- Shrek and it's relationship with Categories.
SELECT * FROM cypher('RecommenderSystem', $
    MATCH (shrek:Movie {title: 'Shrek'}), (A:Category), (C:Category), (H:Category)
    WHERE A.name = 'Action' AND C.name = 'Comedy' AND H.name = 'Horror' 
    CREATE (shrek)-[:RATING {rating: 4}]->(C),
           (shrek)-[:RATING {rating: 2}]->(A),
           (shrek)-[:RATING {rating: 0}]->(H)
$) AS (a agtype);

-- The Blair Witch Project and it's relationship with Categories.
SELECT * FROM cypher('RecommenderSystem', $
    MATCH (witch:Movie {title: 'The Blair Witch Project'}), (A:Category), (C:Category), (H:Category)
    WHERE A.name = 'Action' AND C.name = 'Comedy' AND H.name = 'Horror' 
    CREATE (witch)-[:RATING {rating: 0}]->(C),
           (witch)-[:RATING {rating: 0}]->(A),
           (witch)-[:RATING {rating: 4}]->(H)
$) AS (a agtype);

-- Jurassic Park and it's relationship with Categories.
SELECT * FROM cypher('RecommenderSystem', $
    MATCH (jurassic:Movie {title: 'Jurassic Park'}), (A:Category), (C:Category), (H:Category)
    WHERE A.name = 'Action' AND C.name = 'Comedy' AND H.name = 'Horror' 
    CREATE (jurassic)-[:RATING {rating: 1}]->(C),
           (jurassic)-[:RATING {rating: 3}]->(A),
           (jurassic)-[:RATING {rating: 0}]->(H)
$) AS (a agtype);

-- Thor: Love and Thunder and it's relationship with Categories.
SELECT * FROM cypher('RecommenderSystem', $
    MATCH (thor:Movie {title: 'Thor: Love and Thunder'}), (A:Category), (C:Category), (H:Category)
    WHERE A.name = 'Action' AND C.name = 'Comedy' AND H.name = 'Horror' 
    CREATE (thor)-[:RATING {rating: 4}]->(C),
           (thor)-[:RATING {rating: 2}]->(A),
           (thor)-[:RATING {rating: 0}]->(H)
$) AS (a agtype);
график с указанием возраста

Метод фильтрации содержимого

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

-- The Matrix estimated rating for the user.
SELECT e1/(ct*4) AS factor FROM cypher('RecommenderSystem', $
MATCH (u:Person)-[e1:RATING]->(v:Category)<-[e2:RATING]-(w:Movie{title: 'The Matrix'}), (c:Category) WITH e1, e2, COUNT(*) AS ct
RETURN SUM(e1.rating * e2.rating)::float, ct
$) AS (e1 float, ct agtype);

      factor       
------------------- 

 0.333333333333333
(1 row)

Мы можем представить силу связи между Эбигейл и Матрицей как: [(3 x 0) + (1 x 4) + (0 x 0)] / 12 = 0,3 . По нашим оценкам, фильм ей не очень понравится. Теперь нам нужно собрать данные по всем остальным фильмам, чтобы мы могли показать те, которые больше всего соответствуют ее интересам.

-- Shrek's estimated rating for the user.
SELECT e1/(ct*4) AS factor FROM cypher('RecommenderSystem', $
MATCH (u:Person)-[e1:RATING]->(v:Category)<-[e2:RATING]-(w:Movie{title: 'Shrek'}), (c:Category) WITH e1, e2, COUNT(*) AS ct
RETURN SUM(e1.rating * e2.rating)::float, ct
$) AS (e1 float, ct agtype);

      factor      
------------------ 

 1.16666666666667
(1 row)


-- The Blair Witch Project estimated rating for the user.
SELECT e1/(ct*4) AS factor FROM cypher('RecommenderSystem', $
MATCH (u:Person)-[e1:RATING]->(v:Category)<-[e2:RATING]-(w:Movie{title: 'The Blair Witch Project'}), (c:Category) WITH e1, e2, COUNT(*) AS ct
RETURN SUM(e1.rating * e2.rating)::float, ct
$) AS (e1 float, ct agtype);

 factor 
-------- 

 0.0
(1 row)


-- Jurassic Park estimated rating for the user.
SELECT e1/(ct*4) AS factor FROM cypher('RecommenderSystem', $
MATCH (u:Person)-[e1:RATING]->(v:Category)<-[e2:RATING]-(w:Movie{title: 'Jurassic Park'}), (c:Category) WITH e1, e2, COUNT(*) AS ct
RETURN SUM(e1.rating * e2.rating)::float, ct
$) AS (e1 float, ct agtype);
 factor 
-------- 

 0.5
(1 row)


-- Thor: Love and Thunder estimated rating for the user.
SELECT e1/(ct*4) AS factor FROM cypher('RecommenderSystem', $
MATCH (u:Person)-[e1:RATING]->(v:Category)<-[e2:RATING]-(w:Movie{title: 'Thor: Love and Thunder'}), (c:Category) WITH e1, e2, COUNT(*) AS ct
RETURN SUM(e1.rating * e2.rating)::float, ct
$) AS (e1 float, ct agtype);
      factor      
------------------ 

 1.16666666666667
(1 row)

Несмотря на то, что ”Шрек” и “Тор” не являются ее чашкой чая, согласно нашему анализу графиков, Эбигейл предпочтет посмотреть фильмы из нашего списка.

Заключение

Мы показали, как создать рекомендательную систему с графовой базой данных с помощью Apache AGE. Этот подход может быть расширен для реализации более сложных сценариев, таких как включение демографических данных пользователя, истории поиска или связей в социальных сетях. Графовые базы данных хорошо подходят для рекомендательных систем, поскольку они могут легко представлять отношения между пользователями и предметами, а также атрибуты этих сущностей. Кроме того, использование SQL и языка запросов Cypher облегчает работу с большими массивами данных и выполнение сложных запросов. В целом, мы надеемся, что эта статья послужит отправной точкой для тех, кто заинтересован в создании рекомендательной системы с использованием базы данных графов.